From 1835c952a70242698d4a4adae2e63f89c5326a9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6khan=20Arkan?= Date: Wed, 2 Sep 2026 17:09:16 +0300 Subject: [PATCH 1/6] Expose catalogue search bindings --- dotnet/test/Unit/CatalogueConformanceTests.cs | 99 +++++++ go/rpc/catalogue_conformance_test.go | 138 ++++++++++ java/scripts/codegen/java.ts | 107 ++++++-- .../rpc/CatalogAiSkillCandidate.java | 93 +++++++ .../CatalogAiSkillCandidateProvenance.java | 31 +++ .../generated/rpc/CatalogCandidate.java | 35 +++ .../generated/rpc/CatalogCandidateSource.java | 35 +++ .../rpc/CatalogCandidateSourceEmbedded.java | 30 +++ .../rpc/CatalogCandidateSourceUrl.java | 37 +++ .../rpc/CatalogMcpServerCandidate.java | 93 +++++++ .../CatalogMcpServerCandidateProvenance.java | 31 +++ .../rpc/CatalogMcpServerInstallability.java | 35 +++ .../generated/rpc/CatalogSearchSucceeded.java | 6 +- .../generated/CatalogConformanceTest.java | 107 ++++++++ nodejs/src/generated/rpc.ts | 16 +- nodejs/test/catalogue-conformance.test.ts | 151 +++++++++++ python/copilot/generated/rpc.py | 242 +++++------------- python/test_rpc_generated.py | 130 ++++++++++ rust/tests/catalogue_conformance_test.rs | 103 ++++++++ scripts/codegen/catalogue-conformance.ts | 99 +++++++ scripts/codegen/package.json | 3 +- scripts/codegen/python.ts | 1 + scripts/codegen/typescript.ts | 22 +- 23 files changed, 1440 insertions(+), 204 deletions(-) create mode 100644 dotnet/test/Unit/CatalogueConformanceTests.cs create mode 100644 go/rpc/catalogue_conformance_test.go create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogAiSkillCandidate.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogAiSkillCandidateProvenance.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidate.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSource.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSourceEmbedded.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSourceUrl.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerCandidate.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerCandidateProvenance.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerInstallability.java create mode 100644 java/sdk/src/test/java/com/github/copilot/generated/CatalogConformanceTest.java create mode 100644 nodejs/test/catalogue-conformance.test.ts create mode 100644 rust/tests/catalogue_conformance_test.rs create mode 100644 scripts/codegen/catalogue-conformance.ts diff --git a/dotnet/test/Unit/CatalogueConformanceTests.cs b/dotnet/test/Unit/CatalogueConformanceTests.cs new file mode 100644 index 0000000000..6249665c9e --- /dev/null +++ b/dotnet/test/Unit/CatalogueConformanceTests.cs @@ -0,0 +1,99 @@ +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using GitHub.Copilot.Rpc; +using Xunit; + +#pragma warning disable GHCP001 // The catalogue search schema is experimental in CLI 1.0.83-2. + +namespace GitHub.Copilot.Test.Unit; + +public class CatalogueConformanceTests +{ + private const string OpaqueMcpHandle = "opaque:mcp/01-do-not-parse"; + private const string OpaqueSkillHandle = "opaque:skill/02-do-not-parse"; + private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web) + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver(), + }; + + [Fact] + public void CatalogSearchResult_PreservesTypedCandidatesAndOpaqueHandles() + { + const string json = """ + { + "kind": "succeeded", + "searchId": "search-01", + "candidates": [ + { + "kind": "mcp-server", + "handle": "opaque:mcp/01-do-not-parse", + "handleExpiresAt": "2026-09-02T12:00:00Z", + "mediaType": "application/mcp-server-card+json", + "installability": "installable", + "displayName": "Example MCP", + "rawCard": { "secret": "must-not-survive" }, + "source": { "kind": "url", "url": "https://catalog.example/mcp.json" }, + "provenance": { + "authority": "catalog.example", + "observedAt": "2026-09-02T11:00:00Z", + "mediaType": "application/mcp-server-card+json" + } + }, + { + "kind": "ai-skill", + "handle": "opaque:skill/02-do-not-parse", + "handleExpiresAt": "2026-09-02T12:00:00Z", + "mediaType": "application/ai-skill", + "installability": "not-installable-kind", + "displayName": "Example skill", + "rawCard": { "secret": "must-not-survive" }, + "source": { "kind": "embedded" }, + "provenance": { + "authority": "catalog.example", + "observedAt": "2026-09-02T11:00:00Z", + "mediaType": "application/ai-skill" + } + } + ], + "truncated": false, + "negotiated": { + "runtimeProtocolVersion": 1, + "grantedCapabilities": ["mcp-server-card", "ai-skill-discovery"] + } + } + """; + + var result = Assert.IsType( + JsonSerializer.Deserialize(json, SerializerOptions)); + var mcp = Assert.IsType(result.Candidates[0]); + var skill = Assert.IsType(result.Candidates[1]); + Assert.Equal(OpaqueMcpHandle, mcp.Handle); + Assert.Equal(OpaqueSkillHandle, skill.Handle); + Assert.IsType(mcp.Source); + Assert.IsType(skill.Source); + + using var encoded = JsonDocument.Parse(JsonSerializer.Serialize( + result, SerializerOptions)); + foreach (var candidate in encoded.RootElement.GetProperty("candidates").EnumerateArray()) + { + Assert.False(candidate.TryGetProperty("card", out _)); + Assert.False(candidate.TryGetProperty("cardData", out _)); + Assert.False(candidate.TryGetProperty("rawCard", out _)); + } + } + + [Fact] + public void CatalogSearchResult_PreservesRefusalsAndFailures() + { + var authentication = JsonSerializer.Deserialize( + """{"kind":"authentication-required","reason":"no-credential","message":"Sign in is required."}""", + SerializerOptions); + Assert.IsType(authentication); + + var network = Assert.IsType( + JsonSerializer.Deserialize( + """{"kind":"network-failure","reason":"timeout","retryAfterSeconds":30,"message":"The catalogue timed out."}""", + SerializerOptions)); + Assert.Equal(30, network.RetryAfterSeconds); + } +} diff --git a/go/rpc/catalogue_conformance_test.go b/go/rpc/catalogue_conformance_test.go new file mode 100644 index 0000000000..a18e7140c5 --- /dev/null +++ b/go/rpc/catalogue_conformance_test.go @@ -0,0 +1,138 @@ +package rpc + +import ( + "encoding/json" + "testing" +) + +const ( + opaqueMCPHandle = "opaque:mcp/01-do-not-parse" + opaqueSkillHandle = "opaque:skill/02-do-not-parse" +) + +func TestCatalogSearchResultPreservesCandidateSemantics(t *testing.T) { + result, err := unmarshalCatalogSearchResult([]byte(`{ + "kind":"succeeded", + "searchId":"search-01", + "candidates":[ + { + "kind":"mcp-server", + "handle":"opaque:mcp/01-do-not-parse", + "handleExpiresAt":"2026-09-02T12:00:00Z", + "mediaType":"application/mcp-server-card+json", + "installability":"installable", + "displayName":"Example MCP", + "rawCard":{"secret":"must-not-survive"}, + "source":{"kind":"url","url":"https://catalog.example/mcp.json"}, + "provenance":{ + "authority":"catalog.example", + "observedAt":"2026-09-02T11:00:00Z", + "mediaType":"application/mcp-server-card+json" + } + }, + { + "kind":"ai-skill", + "handle":"opaque:skill/02-do-not-parse", + "handleExpiresAt":"2026-09-02T12:00:00Z", + "mediaType":"application/ai-skill", + "installability":"not-installable-kind", + "displayName":"Example skill", + "rawCard":{"secret":"must-not-survive"}, + "source":{"kind":"embedded"}, + "provenance":{ + "authority":"catalog.example", + "observedAt":"2026-09-02T11:00:00Z", + "mediaType":"application/ai-skill" + } + } + ], + "truncated":false, + "negotiated":{ + "runtimeProtocolVersion":1, + "grantedCapabilities":["mcp-server-card","ai-skill-discovery"] + } + }`)) + if err != nil { + t.Fatalf("unmarshal catalogue success: %v", err) + } + + success, ok := result.(*CatalogSearchSucceeded) + if !ok { + t.Fatalf("catalogue result = %T, want *CatalogSearchSucceeded", result) + } + mcp, ok := success.Candidates[0].(*CatalogMCPServerCandidate) + if !ok { + t.Fatalf("first candidate = %T, want *CatalogMCPServerCandidate", success.Candidates[0]) + } + skill, ok := success.Candidates[1].(*CatalogAiSkillCandidate) + if !ok { + t.Fatalf("second candidate = %T, want *CatalogAiSkillCandidate", success.Candidates[1]) + } + if mcp.Handle != opaqueMCPHandle || skill.Handle != opaqueSkillHandle { + t.Fatalf("opaque handles changed: %q, %q", mcp.Handle, skill.Handle) + } + if _, ok := mcp.Source.(*CatalogCandidateSourceURL); !ok { + t.Fatalf("MCP source = %T, want *CatalogCandidateSourceURL", mcp.Source) + } + if _, ok := skill.Source.(*CatalogCandidateSourceEmbedded); !ok { + t.Fatalf("skill source = %T, want *CatalogCandidateSourceEmbedded", skill.Source) + } + + encoded, err := json.Marshal(success) + if err != nil { + t.Fatalf("marshal catalogue success: %v", err) + } + var wire map[string]any + if err := json.Unmarshal(encoded, &wire); err != nil { + t.Fatalf("decode catalogue wire result: %v", err) + } + for _, candidate := range wire["candidates"].([]any) { + fields := candidate.(map[string]any) + for _, forbidden := range []string{"card", "cardData", "rawCard"} { + if _, exists := fields[forbidden]; exists { + t.Fatalf("candidate leaked %q: %s", forbidden, encoded) + } + } + } +} + +func TestCatalogSearchResultPreservesRefusalsAndFailures(t *testing.T) { + tests := []struct { + name string + payload string + assert func(*testing.T, CatalogSearchResult) + }{ + { + name: "authentication required", + payload: `{"kind":"authentication-required","reason":"no-credential","message":"Sign in is required."}`, + assert: func(t *testing.T, result CatalogSearchResult) { + if _, ok := result.(*CatalogAuthenticationRequiredError); !ok { + t.Fatalf("result = %T, want *CatalogAuthenticationRequiredError", result) + } + }, + }, + { + name: "network failure", + payload: `{"kind":"network-failure","reason":"timeout","retryAfterSeconds":30,"message":"The catalogue timed out."}`, + assert: func(t *testing.T, result CatalogSearchResult) { + failure, ok := result.(*CatalogNetworkFailureError) + if !ok { + t.Fatalf("result = %T, want *CatalogNetworkFailureError", result) + } + if failure.RetryAfterSeconds == nil || *failure.RetryAfterSeconds != 30 { + t.Fatalf("retryAfterSeconds = %v, want 30", failure.RetryAfterSeconds) + } + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result, err := unmarshalCatalogSearchResult([]byte(test.payload)) + if err != nil { + t.Fatalf("unmarshal catalogue result: %v", err) + } + test.assert(t, result) + }) + } +} diff --git a/java/scripts/codegen/java.ts b/java/scripts/codegen/java.ts index 785049afa1..96bdb72c18 100644 --- a/java/scripts/codegen/java.ts +++ b/java/scripts/codegen/java.ts @@ -252,6 +252,7 @@ interface JavaTypeResult { // Set before each schema generation pass; used by schemaTypeToJava and helpers. let currentDefinitions: Record = {}; const pendingStandaloneTypes = new Map(); +const promotedNestedUnionTypes = new Set(); const generatedSessionEventTypeNames = new Set(); // Cross-schema definitions: keyed by schema filename (e.g. "session-events.schema.json"), @@ -300,14 +301,14 @@ function resolveMethodParamsSchema(method: RpcMethodNode): JSONSchema7 | undefin if (!params || typeof params !== "object") return undefined; if (params.properties) return params; if (!Array.isArray(params.anyOf)) return undefined; - const objectVariants = resolveAnyOfVariants(params.anyOf as JSONSchema7[]).filter((variant) => !!variant.properties); + const objectVariants = resolveUnionVariants(params.anyOf as JSONSchema7[]).filter((variant) => !!variant.properties); return hasOmissionSentinel(params) && objectVariants.length === 1 ? objectVariants[0] : undefined; } function resolveMethodParamsUnionSchema(method: RpcMethodNode): JSONSchema7 | undefined { const params = resolveRef(method.params ?? undefined); if (!params || typeof params !== "object" || !Array.isArray(params.anyOf)) return undefined; - const variants = resolveAnyOfVariants(params.anyOf as JSONSchema7[]); + const variants = resolveUnionVariants(params.anyOf as JSONSchema7[]); return variants.length > 1 && findDiscriminator(variants) ? params : undefined; } @@ -365,8 +366,8 @@ function findDiscriminator(variants: JSONSchema7[]): DiscriminatorInfo | null { /** * Resolve anyOf variants, handling $ref to definitions. */ -function resolveAnyOfVariants(anyOf: JSONSchema7[]): JSONSchema7[] { - return anyOf +function resolveUnionVariants(variants: JSONSchema7[]): JSONSchema7[] { + return variants .map((v) => { if (v.$ref) { const name = v.$ref.replace(/^#\/definitions\//, ""); @@ -377,6 +378,42 @@ function resolveAnyOfVariants(anyOf: JSONSchema7[]): JSONSchema7[] { .filter((v) => v.type !== "null"); } +function collectPromotedNestedUnionTypes(root: unknown): void { + const visitedDefinitions = new Set(); + const visit = (node: unknown): void => { + if (Array.isArray(node)) { + for (const item of node) visit(item); + return; + } + if (!node || typeof node !== "object") return; + + const schema = node as JSONSchema7; + if (schema.$ref?.startsWith("#/definitions/")) { + const name = schema.$ref.slice("#/definitions/".length); + if (visitedDefinitions.has(name)) return; + visitedDefinitions.add(name); + const definition = currentDefinitions[name]; + if (definition) { + const union = definition.anyOf ?? definition.oneOf; + if ( + union + && Array.isArray(union) + && findDiscriminator(resolveUnionVariants(union as JSONSchema7[])) + ) { + promotedNestedUnionTypes.add(name); + } + visit(definition); + } + return; + } + + for (const value of Object.values(node as Record)) { + visit(value); + } + }; + visit(root); +} + /** * Generate a polymorphic base class and variant subclasses for a discriminated union result type. */ @@ -386,8 +423,8 @@ async function generatePolymorphicResultClass( packageName: string, packageDir: string ): Promise { - const anyOf = schema.anyOf as JSONSchema7[]; - const variants = resolveAnyOfVariants(anyOf); + const union = (schema.anyOf ?? schema.oneOf) as JSONSchema7[]; + const variants = resolveUnionVariants(union); const discriminator = findDiscriminator(variants); if (!discriminator) { @@ -408,7 +445,7 @@ async function generatePolymorphicResultClass( variantInfos.push({ discriminatorValue: discValue, variantClassName, schema: variantSchema }); } - // Generate the abstract base class + // Generate the polymorphic base class const baseLines: string[] = []; baseLines.push(COPYRIGHT); baseLines.push(""); @@ -608,6 +645,18 @@ function schemaTypeToJava( const name = schema.$ref.replace(/^#\/definitions\//, ""); const resolved = currentDefinitions[name]; if (resolved) { + const resolvedUnion = resolved.anyOf ?? resolved.oneOf; + if ( + promotedNestedUnionTypes.has(name) + && resolvedUnion + && Array.isArray(resolvedUnion) + ) { + const variants = resolveUnionVariants(resolvedUnion as JSONSchema7[]); + if (variants.length > 1 && findDiscriminator(variants)) { + pendingStandaloneTypes.set(name, resolved); + return { javaType: name, imports }; + } + } // Enum or object types → register for standalone generation, return ref name if ((resolved.type === "string" && resolved.enum) || (resolved.type === "object" && resolved.properties)) { @@ -622,17 +671,25 @@ function schemaTypeToJava( return { javaType: name, imports }; } - if (schema.anyOf) { - const hasNull = schema.anyOf.some((s) => typeof s === "object" && (s as JSONSchema7).type === "null"); - const nonNull = schema.anyOf.filter((s) => typeof s === "object" && (s as JSONSchema7).type !== "null"); + const union = schema.anyOf ?? schema.oneOf; + if (union) { + const hasNull = union.some((s) => typeof s === "object" && (s as JSONSchema7).type === "null"); + const nonNull = union.filter((s) => typeof s === "object" && (s as JSONSchema7).type !== "null"); if (nonNull.length === 1) { const result = schemaTypeToJava(nonNull[0] as JSONSchema7, required && !hasNull, context, propName, nestedTypes); return result; } - // Multi-branch anyOf: fall through to Object, matching the C# generator's - // behavior. Java has no union types, so Object is the correct erasure for - // anyOf[string, object] and similar multi-variant schemas. - console.warn(`[codegen] ${context}.${propName}: anyOf with ${nonNull.length} non-null branches — falling back to Object`); + const variants = resolveUnionVariants(nonNull as JSONSchema7[]); + if ( + variants.length > 1 + && findDiscriminator(variants) + && schema.title + && promotedNestedUnionTypes.has(schema.title) + ) { + pendingStandaloneTypes.set(schema.title, schema); + return { javaType: schema.title, imports }; + } + console.warn(`[codegen] ${context}.${propName}: union with ${nonNull.length} non-null branches — falling back to Object`); return { javaType: "Object", imports }; } @@ -1183,12 +1240,15 @@ async function generatePendingStandaloneTypes( await generateStandaloneEnum(name, schema, packageName, packageDir, headerComment); } else if (schema.type === "object" && schema.properties) { await generateStandaloneRecord(name, schema, packageName, packageDir, headerComment); - } else if (schema.anyOf && Array.isArray(schema.anyOf)) { - const variants = resolveAnyOfVariants(schema.anyOf as JSONSchema7[]); + } else if ( + (schema.anyOf && Array.isArray(schema.anyOf)) + || (schema.oneOf && Array.isArray(schema.oneOf)) + ) { + const variants = resolveUnionVariants((schema.anyOf ?? schema.oneOf) as JSONSchema7[]); if (variants.length > 1 && findDiscriminator(variants)) { await generatePolymorphicResultClass(name, schema, packageName, packageDir); } else { - console.warn(`[codegen] Cannot generate standalone type for ${name}: anyOf without discriminator`); + console.warn(`[codegen] Cannot generate standalone type for ${name}: union without discriminator`); } } else { console.warn(`[codegen] Cannot generate standalone type for ${name}: type=${schema.type}`); @@ -1391,7 +1451,16 @@ async function generateRpcTypes(schemaPath: string): Promise { // Set module-level definitions for $ref resolution currentDefinitions = (schema.definitions ?? {}) as Record; pendingStandaloneTypes.clear(); + promotedNestedUnionTypes.clear(); crossSchemaDefinitions.clear(); + for (const section of [schema.server, schema.session, schema.clientSession, schema.clientGlobal]) { + if (!section) continue; + for (const [, method] of collectRpcMethods(section)) { + if (method.rpcMethod === "catalog.search") { + collectPromotedNestedUnionTypes(method); + } + } + } // Load cross-schema definitions (session-events) so that cross-schema $ref values // like "session-events.schema.json#/definitions/Foo" can be resolved. @@ -1477,7 +1546,7 @@ async function generateRpcTypes(schemaPath: string): Promise { pendingStandaloneTypes.set(resultRefName, resultSchema); } else if (resultRefName && resultSchema.anyOf && Array.isArray(resultSchema.anyOf)) { // anyOf discriminated union → generate polymorphic hierarchy - const variants = resolveAnyOfVariants(resultSchema.anyOf as JSONSchema7[]); + const variants = resolveUnionVariants(resultSchema.anyOf as JSONSchema7[]); if (variants.length > 1 && findDiscriminator(variants)) { if (!generatedClasses.has(resultRefName)) { generatedClasses.set(resultRefName, true); @@ -1645,7 +1714,7 @@ function wrapperResultClassName(method: RpcMethodNode): string { } // anyOf discriminated union → use the definition name if (resolved.anyOf && Array.isArray(resolved.anyOf)) { - const variants = resolveAnyOfVariants(resolved.anyOf as JSONSchema7[]); + const variants = resolveUnionVariants(resolved.anyOf as JSONSchema7[]); if (variants.length > 1 && findDiscriminator(variants)) { return refName; } 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 new file mode 100644 index 0000000000..59e70f4935 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogAiSkillCandidate.java @@ -0,0 +1,93 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package 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 inert AI skill catalog result. AI skills are discovery-only and cannot be represented as installable through this surface. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class CatalogAiSkillCandidate extends CatalogCandidate { + + @JsonProperty("kind") + private final String kind = "ai-skill"; + + @Override + public String getKind() { return kind; } + + /** 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. */ + @JsonProperty("handle") + private String handle; + + /** ISO 8601 timestamp after which the handle is stale and will be rejected. */ + @JsonProperty("handleExpiresAt") + private String handleExpiresAt; + + /** Media type of the underlying AI skill card */ + @JsonProperty("mediaType") + private String mediaType; + + /** AI skills are discovery-only and cannot be installed through this surface */ + @JsonProperty("installability") + private String installability; + + /** Display name taken verbatim from the card. Inert untrusted text. */ + @JsonProperty("displayName") + private String displayName; + + /** Description taken verbatim from the card. Inert untrusted text. */ + @JsonProperty("description") + private String description; + + /** Publisher taken verbatim from the card. Inert untrusted text. */ + @JsonProperty("publisher") + private String publisher; + + /** 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. */ + @JsonProperty("source") + private CatalogCandidateSource source; + + /** Where the catalog reference was observed, without the card itself or any content digest. */ + @JsonProperty("provenance") + private CatalogAiSkillCandidateProvenance provenance; + + public String getHandle() { return handle; } + public void setHandle(String handle) { this.handle = handle; } + + public String getHandleExpiresAt() { return handleExpiresAt; } + public void setHandleExpiresAt(String handleExpiresAt) { this.handleExpiresAt = handleExpiresAt; } + + public String getMediaType() { return mediaType; } + public void setMediaType(String mediaType) { this.mediaType = mediaType; } + + public String getInstallability() { return installability; } + public void setInstallability(String installability) { this.installability = installability; } + + public String getDisplayName() { return displayName; } + public void setDisplayName(String displayName) { this.displayName = displayName; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public String getPublisher() { return publisher; } + public void setPublisher(String publisher) { this.publisher = publisher; } + + public CatalogCandidateSource getSource() { return source; } + public void setSource(CatalogCandidateSource source) { this.source = source; } + + public CatalogAiSkillCandidateProvenance getProvenance() { return provenance; } + public void setProvenance(CatalogAiSkillCandidateProvenance provenance) { this.provenance = provenance; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogAiSkillCandidateProvenance.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogAiSkillCandidateProvenance.java new file mode 100644 index 0000000000..0a2eff24e8 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogAiSkillCandidateProvenance.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; + +/** + * 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. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record CatalogAiSkillCandidateProvenance( + /** Host of the catalog authority that advertised the reference, without path, query, or credentials. Inert untrusted data. */ + @JsonProperty("authority") String authority, + /** ISO 8601 timestamp at which the runtime observed the catalog reference. This is not a retrieval or validation timestamp. */ + @JsonProperty("observedAt") String observedAt, + /** Media type advertised for the referenced AI skill card */ + @JsonProperty("mediaType") String mediaType +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidate.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidate.java new file mode 100644 index 0000000000..7fc982cc21 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidate.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; + +/** + * One inert catalog result, represented as an MCP server or discovery-only AI skill variant so kind, media type, provenance, and installability cannot contradict each other. + * + * @since 1.0.0 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = CatalogMcpServerCandidate.class, name = "mcp-server"), + @JsonSubTypes.Type(value = CatalogAiSkillCandidate.class, name = "ai-skill") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public abstract class CatalogCandidate { + + /** + * 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/CatalogCandidateSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSource.java new file mode 100644 index 0000000000..90d9667d0d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSource.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; + +/** + * 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. + * + * @since 1.0.0 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = CatalogCandidateSourceUrl.class, name = "url"), + @JsonSubTypes.Type(value = CatalogCandidateSourceEmbedded.class, name = "embedded") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public abstract class CatalogCandidateSource { + + /** + * 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/CatalogCandidateSourceEmbedded.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSourceEmbedded.java new file mode 100644 index 0000000000..6814a0c4f2 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSourceEmbedded.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 javax.annotation.processing.Generated; + +/** + * Candidate whose card reference arrived inline. The document and its content-derived properties stay behind the runtime boundary. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class CatalogCandidateSourceEmbedded extends CatalogCandidateSource { + + @JsonProperty("kind") + private final String kind = "embedded"; + + @Override + public String getKind() { return kind; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSourceUrl.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSourceUrl.java new file mode 100644 index 0000000000..1c9b6d8905 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSourceUrl.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; + +/** + * Candidate whose card is retrieved from a URL through the runtime's hardened fetch boundary. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class CatalogCandidateSourceUrl extends CatalogCandidateSource { + + @JsonProperty("kind") + private final String kind = "url"; + + @Override + public String getKind() { return kind; } + + /** Card URL as advertised. Inert untrusted data: the runtime retrieves it only through its own hardened boundary, and it is never logged. */ + @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/CatalogMcpServerCandidate.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerCandidate.java new file mode 100644 index 0000000000..8183ca422a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerCandidate.java @@ -0,0 +1,93 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package 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 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. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class CatalogMcpServerCandidate extends CatalogCandidate { + + @JsonProperty("kind") + private final String kind = "mcp-server"; + + @Override + public String getKind() { return kind; } + + /** 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. */ + @JsonProperty("handle") + private String handle; + + /** ISO 8601 timestamp after which the handle is stale and will be rejected. */ + @JsonProperty("handleExpiresAt") + private String handleExpiresAt; + + /** JSON MCP media type of the underlying card. */ + @JsonProperty("mediaType") + private McpServerCardMediaType mediaType; + + /** Whether this MCP server can be planned for installation, and if policy prevents it. */ + @JsonProperty("installability") + private CatalogMcpServerInstallability installability; + + /** Display name taken verbatim from the card. Inert untrusted text. */ + @JsonProperty("displayName") + private String displayName; + + /** Description taken verbatim from the card. Inert untrusted text. */ + @JsonProperty("description") + private String description; + + /** Publisher taken verbatim from the card. Inert untrusted text. */ + @JsonProperty("publisher") + private String publisher; + + /** 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. */ + @JsonProperty("source") + private CatalogCandidateSource source; + + /** Where the catalog reference was observed, without the card itself or any content digest. */ + @JsonProperty("provenance") + private CatalogMcpServerCandidateProvenance provenance; + + public String getHandle() { return handle; } + public void setHandle(String handle) { this.handle = handle; } + + public String getHandleExpiresAt() { return handleExpiresAt; } + public void setHandleExpiresAt(String handleExpiresAt) { this.handleExpiresAt = handleExpiresAt; } + + public McpServerCardMediaType getMediaType() { return mediaType; } + public void setMediaType(McpServerCardMediaType mediaType) { this.mediaType = mediaType; } + + public CatalogMcpServerInstallability getInstallability() { return installability; } + public void setInstallability(CatalogMcpServerInstallability installability) { this.installability = installability; } + + public String getDisplayName() { return displayName; } + public void setDisplayName(String displayName) { this.displayName = displayName; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public String getPublisher() { return publisher; } + public void setPublisher(String publisher) { this.publisher = publisher; } + + public CatalogCandidateSource getSource() { return source; } + public void setSource(CatalogCandidateSource source) { this.source = source; } + + public CatalogMcpServerCandidateProvenance getProvenance() { return provenance; } + public void setProvenance(CatalogMcpServerCandidateProvenance provenance) { this.provenance = provenance; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerCandidateProvenance.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerCandidateProvenance.java new file mode 100644 index 0000000000..3ef13704ff --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerCandidateProvenance.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; + +/** + * 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. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record CatalogMcpServerCandidateProvenance( + /** Host of the catalog authority that advertised the reference, without path, query, or credentials. Inert untrusted data. */ + @JsonProperty("authority") String authority, + /** ISO 8601 timestamp at which the runtime observed the catalog reference. This is not a retrieval or validation timestamp. */ + @JsonProperty("observedAt") String observedAt, + /** JSON MCP media type advertised for the referenced card. */ + @JsonProperty("mediaType") McpServerCardMediaType mediaType +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerInstallability.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerInstallability.java new file mode 100644 index 0000000000..4478a81bd3 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerInstallability.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 an MCP server candidate can be planned for installation + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum CatalogMcpServerInstallability { + /** The {@code installable} variant. */ + INSTALLABLE("installable"), + /** The {@code not-installable-policy} variant. */ + NOT_INSTALLABLE_POLICY("not-installable-policy"); + + private final String value; + CatalogMcpServerInstallability(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static CatalogMcpServerInstallability fromValue(String value) { + for (CatalogMcpServerInstallability v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown CatalogMcpServerInstallability value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchSucceeded.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchSucceeded.java index 8ecd11788a..b49f78faff 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchSucceeded.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchSucceeded.java @@ -35,7 +35,7 @@ public final class CatalogSearchSucceeded extends CatalogSearchResult { /** Matching candidates, never more than the requested limit. All text is inert untrusted data. */ @JsonProperty("candidates") - private List candidates; + private List candidates; /** Whether further matches existed beyond the requested limit. */ @JsonProperty("truncated") @@ -48,8 +48,8 @@ public final class CatalogSearchSucceeded extends CatalogSearchResult { public String getSearchId() { return searchId; } public void setSearchId(String searchId) { this.searchId = searchId; } - public List getCandidates() { return candidates; } - public void setCandidates(List candidates) { this.candidates = candidates; } + public List getCandidates() { return candidates; } + public void setCandidates(List candidates) { this.candidates = candidates; } public Boolean getTruncated() { return truncated; } public void setTruncated(Boolean truncated) { this.truncated = truncated; } diff --git a/java/sdk/src/test/java/com/github/copilot/generated/CatalogConformanceTest.java b/java/sdk/src/test/java/com/github/copilot/generated/CatalogConformanceTest.java new file mode 100644 index 0000000000..5e9e4fadbf --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/generated/CatalogConformanceTest.java @@ -0,0 +1,107 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.generated; + +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 org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.generated.rpc.CatalogAiSkillCandidate; +import com.github.copilot.generated.rpc.CatalogAuthenticationRequiredError; +import com.github.copilot.generated.rpc.CatalogCandidateSourceEmbedded; +import com.github.copilot.generated.rpc.CatalogCandidateSourceUrl; +import com.github.copilot.generated.rpc.CatalogMcpServerCandidate; +import com.github.copilot.generated.rpc.CatalogNetworkFailureError; +import com.github.copilot.generated.rpc.CatalogSearchResult; +import com.github.copilot.generated.rpc.CatalogSearchSucceeded; + +class CatalogConformanceTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final String OPAQUE_MCP_HANDLE = "opaque:mcp/01-do-not-parse"; + private static final String OPAQUE_SKILL_HANDLE = "opaque:skill/02-do-not-parse"; + + @Test + void preservesTypedCandidatesAndOpaqueHandles() throws Exception { + var result = MAPPER.readValue(""" + { + "kind": "succeeded", + "searchId": "search-01", + "candidates": [ + { + "kind": "mcp-server", + "handle": "opaque:mcp/01-do-not-parse", + "handleExpiresAt": "2026-09-02T12:00:00Z", + "mediaType": "application/mcp-server-card+json", + "installability": "installable", + "displayName": "Example MCP", + "rawCard": { "secret": "must-not-survive" }, + "source": { "kind": "url", "url": "https://catalog.example/mcp.json" }, + "provenance": { + "authority": "catalog.example", + "observedAt": "2026-09-02T11:00:00Z", + "mediaType": "application/mcp-server-card+json" + } + }, + { + "kind": "ai-skill", + "handle": "opaque:skill/02-do-not-parse", + "handleExpiresAt": "2026-09-02T12:00:00Z", + "mediaType": "application/ai-skill", + "installability": "not-installable-kind", + "displayName": "Example skill", + "rawCard": { "secret": "must-not-survive" }, + "source": { "kind": "embedded" }, + "provenance": { + "authority": "catalog.example", + "observedAt": "2026-09-02T11:00:00Z", + "mediaType": "application/ai-skill" + } + } + ], + "truncated": false, + "negotiated": { + "runtimeProtocolVersion": 1, + "grantedCapabilities": ["mcp-server-card", "ai-skill-discovery"] + } + } + """, CatalogSearchResult.class); + + var success = assertInstanceOf(CatalogSearchSucceeded.class, result); + var mcp = assertInstanceOf(CatalogMcpServerCandidate.class, success.getCandidates().get(0)); + var skill = assertInstanceOf(CatalogAiSkillCandidate.class, success.getCandidates().get(1)); + assertEquals(OPAQUE_MCP_HANDLE, mcp.getHandle()); + assertEquals(OPAQUE_SKILL_HANDLE, skill.getHandle()); + assertInstanceOf(CatalogCandidateSourceUrl.class, mcp.getSource()); + assertInstanceOf(CatalogCandidateSourceEmbedded.class, skill.getSource()); + + JsonNode encoded = MAPPER.valueToTree(success); + for (JsonNode candidate : encoded.get("candidates")) { + assertFalse(candidate.has("card")); + assertFalse(candidate.has("cardData")); + assertFalse(candidate.has("rawCard")); + } + } + + @Test + void preservesRefusalsAndFailures() throws Exception { + var authentication = MAPPER.readValue(""" + {"kind":"authentication-required","reason":"no-credential","message":"Sign in is required."} + """, CatalogSearchResult.class); + assertInstanceOf(CatalogAuthenticationRequiredError.class, authentication); + + var network = assertInstanceOf(CatalogNetworkFailureError.class, + MAPPER.readValue( + """ + {"kind":"network-failure","reason":"timeout","retryAfterSeconds":30,"message":"The catalogue timed out."} + """, + CatalogSearchResult.class)); + assertEquals(30L, network.getRetryAfterSeconds()); + } +} diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index f4978de1ff..62b864b6cb 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -23710,6 +23710,18 @@ export interface SessionFsSqliteExistsRequest { sessionId: string; } +const FORBIDDEN_CATALOG_RESPONSE_FIELDS = new Set(["card", "cardData", "rawCard"]); + +function sanitizeCatalogSearchResult(value: unknown): unknown { + if (Array.isArray(value)) return value.map(sanitizeCatalogSearchResult); + if (value === null || typeof value !== "object") return value; + return Object.fromEntries( + Object.entries(value) + .filter(([key]) => !FORBIDDEN_CATALOG_RESPONSE_FIELDS.has(key)) + .map(([key, child]) => [key, sanitizeCatalogSearchResult(child)]), + ); +} + /** Create typed server-scoped RPC methods (no session required). */ export function createServerRpc(connection: MessageConnection) { return { @@ -23935,7 +23947,9 @@ export function createServerRpc(connection: MessageConnection) { * @returns Outcome of a catalog.search call: either bounded inert candidates, or one typed refusal. Never a partial success. */ search: async (params: CatalogSearchRequest): Promise => - connection.sendRequest("catalog.search", params), + sanitizeCatalogSearchResult( + await connection.sendRequest("catalog.search", params), + ) as CatalogSearchResult, }, /** @experimental */ plugins: { diff --git a/nodejs/test/catalogue-conformance.test.ts b/nodejs/test/catalogue-conformance.test.ts new file mode 100644 index 0000000000..769bbd5c31 --- /dev/null +++ b/nodejs/test/catalogue-conformance.test.ts @@ -0,0 +1,151 @@ +import { PassThrough } from "node:stream"; + +import { + createMessageConnection, + StreamMessageReader, + StreamMessageWriter, +} from "vscode-jsonrpc/node.js"; +import { describe, expect, it, onTestFinished } from "vitest"; + +import { + type CatalogSearchRequest, + type CatalogSearchResult, + createServerRpc, +} from "../src/generated/rpc.js"; + +const OPAQUE_MCP_HANDLE = "opaque:mcp/01-do-not-parse"; +const OPAQUE_SKILL_HANDLE = "opaque:skill/02-do-not-parse"; + +function successResult(): CatalogSearchResult { + return { + kind: "succeeded", + searchId: "search-01", + candidates: [ + { + kind: "mcp-server", + handle: OPAQUE_MCP_HANDLE, + handleExpiresAt: "2026-09-02T12:00:00Z", + mediaType: "application/mcp-server-card+json", + installability: "installable", + displayName: "Example MCP", + source: { kind: "url", url: "https://catalog.example/mcp.json" }, + provenance: { + authority: "catalog.example", + observedAt: "2026-09-02T11:00:00Z", + mediaType: "application/mcp-server-card+json", + }, + }, + { + kind: "ai-skill", + handle: OPAQUE_SKILL_HANDLE, + handleExpiresAt: "2026-09-02T12:00:00Z", + mediaType: "application/ai-skill", + installability: "not-installable-kind", + displayName: "Example skill", + source: { kind: "embedded" }, + provenance: { + authority: "catalog.example", + observedAt: "2026-09-02T11:00:00Z", + mediaType: "application/ai-skill", + }, + }, + ], + truncated: false, + negotiated: { + runtimeProtocolVersion: 1, + grantedCapabilities: ["mcp-server-card", "ai-skill-discovery"], + }, + }; +} + +function successWireResult(): unknown { + const result = successResult(); + if (result.kind !== "succeeded") throw new Error("Expected a successful search."); + return { + ...result, + candidates: result.candidates.map((candidate) => ({ + ...candidate, + card: { secret: "must-not-survive" }, + cardData: { secret: "must-not-survive" }, + rawCard: { secret: "must-not-survive" }, + })), + }; +} + +describe("catalogue binding conformance", () => { + it("transports typed candidates, refusals, and failures unchanged", async () => { + const clientToServer = new PassThrough(); + const serverToClient = new PassThrough(); + const client = createMessageConnection( + new StreamMessageReader(serverToClient), + new StreamMessageWriter(clientToServer) + ); + const server = createMessageConnection( + new StreamMessageReader(clientToServer), + new StreamMessageWriter(serverToClient) + ); + onTestFinished(() => { + client.dispose(); + server.dispose(); + }); + + server.onRequest("catalog.search", (params: CatalogSearchRequest) => { + if (params.query === "authentication") { + return { + kind: "authentication-required", + reason: "no-credential", + message: "Sign in is required.", + } satisfies CatalogSearchResult; + } + if (params.query === "network") { + return { + kind: "network-failure", + reason: "timeout", + retryAfterSeconds: 30, + message: "The catalogue timed out.", + } satisfies CatalogSearchResult; + } + return successWireResult(); + }); + client.listen(); + server.listen(); + + const rpc = createServerRpc(client); + const request = { + contract: { protocolVersion: 1, requiredCapabilities: [] }, + query: "success", + }; + const success = await rpc.catalog.search(request); + expect(success.kind).toBe("succeeded"); + if (success.kind !== "succeeded") throw new Error("Expected a successful search."); + + expect(success.candidates.map((candidate) => candidate.kind)).toEqual([ + "mcp-server", + "ai-skill", + ]); + expect(success.candidates.map((candidate) => candidate.handle)).toEqual([ + OPAQUE_MCP_HANDLE, + OPAQUE_SKILL_HANDLE, + ]); + const encodedCandidates = JSON.parse(JSON.stringify(success)).candidates as Array< + Record + >; + for (const candidate of encodedCandidates) { + expect(candidate).not.toHaveProperty("card"); + expect(candidate).not.toHaveProperty("cardData"); + expect(candidate).not.toHaveProperty("rawCard"); + } + + await expect( + rpc.catalog.search({ ...request, query: "authentication" }) + ).resolves.toMatchObject({ + kind: "authentication-required", + reason: "no-credential", + }); + await expect(rpc.catalog.search({ ...request, query: "network" })).resolves.toMatchObject({ + kind: "network-failure", + reason: "timeout", + retryAfterSeconds: 30, + }); + }); +}); diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index ac4cc5441d..1d134beff4 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -16618,47 +16618,6 @@ def to_dict(self) -> dict: result["observedAt"] = from_str(self.observed_at) return result -@dataclass -class CatalogCandidateProvenance: - """Where the catalog reference was observed, without the card itself or any content digest. - - 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. - - 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. - """ - authority: str - """Host of the catalog authority that advertised the reference, without path, query, or - credentials. Inert untrusted data. - """ - media_type: CatalogMediaType - """JSON MCP media type advertised for the referenced card. - - Media type advertised for the referenced AI skill card - """ - observed_at: str - """ISO 8601 timestamp at which the runtime observed the catalog reference. This is not a - retrieval or validation timestamp. - """ - - @staticmethod - def from_dict(obj: Any) -> 'CatalogCandidateProvenance': - assert isinstance(obj, dict) - authority = from_str(obj.get("authority")) - media_type = CatalogMediaType(obj.get("mediaType")) - observed_at = from_str(obj.get("observedAt")) - return CatalogCandidateProvenance(authority, media_type, observed_at) - - def to_dict(self) -> dict: - result: dict = {} - result["authority"] = from_str(self.authority) - result["mediaType"] = to_enum(CatalogMediaType, self.media_type) - 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 CatalogMCPServerCandidateProvenance: @@ -26193,6 +26152,50 @@ def to_dict(self) -> dict: result["validatedAt"] = from_str(self.validated_at) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class CatalogSearchSucceeded: + """A completed catalog search: inert candidate summaries, each carrying a single-use handle.""" + + candidates: list[CatalogCandidate] + """Matching candidates, never more than the requested limit. All text is inert untrusted + data. + """ + kind: ClassVar[str] = "succeeded" + """Discriminator: the search completed""" + + negotiated: CatalogNegotiatedContract + """Protocol version and capabilities the runtime honoured.""" + + search_id: str + """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. + """ + truncated: bool + """Whether further matches existed beyond the requested limit.""" + + @staticmethod + def from_dict(obj: Any) -> 'CatalogSearchSucceeded': + assert isinstance(obj, dict) + candidates = from_list(_load_CatalogCandidate, obj.get("candidates")) + negotiated = CatalogNegotiatedContract.from_dict(obj.get("negotiated")) + search_id = from_str(obj.get("searchId")) + truncated = from_bool(obj.get("truncated")) + return CatalogSearchSucceeded(candidates, negotiated, search_id, truncated) + + def to_dict(self) -> dict: + result: dict = {} + result["candidates"] = from_list(lambda x: (x).to_dict(), self.candidates) + result["kind"] = self.kind + result["negotiated"] = to_class(CatalogNegotiatedContract, self.negotiated) + result["searchId"] = from_str(self.search_id) + result["truncated"] = from_bool(self.truncated) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SlashCommandInfo: @@ -26358,7 +26361,7 @@ class CatalogAISkillCandidate: installability: Installability """AI skills are discovery-only and cannot be installed through this surface""" - kind: CatalogAISkillCandidateKind + kind: ClassVar[str] = "ai-skill" """Discriminator: this candidate describes an AI skill""" media_type: MediaType @@ -26384,13 +26387,12 @@ def from_dict(obj: Any) -> 'CatalogAISkillCandidate': handle = from_str(obj.get("handle")) handle_expires_at = from_str(obj.get("handleExpiresAt")) installability = Installability(obj.get("installability")) - kind = CatalogAISkillCandidateKind(obj.get("kind")) media_type = MediaType(obj.get("mediaType")) provenance = CatalogAISkillCandidateProvenance.from_dict(obj.get("provenance")) source = _load_CatalogCandidateSource(obj.get("source")) description = from_union([from_str, from_none], obj.get("description")) publisher = from_union([from_str, from_none], obj.get("publisher")) - return CatalogAISkillCandidate(display_name, handle, handle_expires_at, installability, kind, media_type, provenance, source, description, publisher) + return CatalogAISkillCandidate(display_name, handle, handle_expires_at, installability, media_type, provenance, source, description, publisher) def to_dict(self) -> dict: result: dict = {} @@ -26398,7 +26400,7 @@ def to_dict(self) -> dict: result["handle"] = from_str(self.handle) result["handleExpiresAt"] = from_str(self.handle_expires_at) result["installability"] = to_enum(Installability, self.installability) - result["kind"] = to_enum(CatalogAISkillCandidateKind, self.kind) + result["kind"] = self.kind result["mediaType"] = to_enum(MediaType, self.media_type) result["provenance"] = to_class(CatalogAISkillCandidateProvenance, self.provenance) result["source"] = (self.source).to_dict() @@ -26408,89 +26410,6 @@ def to_dict(self) -> dict: result["publisher"] = from_union([from_str, from_none], self.publisher) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class CatalogCandidate: - """One inert catalog result, represented as an MCP server or discovery-only AI skill variant - so kind, media type, provenance, and installability cannot contradict each other. - - 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. - - An inert AI skill catalog result. AI skills are discovery-only and cannot be represented - as installable through this surface. - """ - display_name: str - """Display name taken verbatim from the card. Inert untrusted text.""" - - handle: str - """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_expires_at: str - """ISO 8601 timestamp after which the handle is stale and will be rejected.""" - - installability: CatalogCandidateInstallability - """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 - """ - kind: CatalogCandidateKind - """Discriminator: this candidate describes an MCP server - - Discriminator: this candidate describes an AI skill - """ - media_type: CatalogMediaType - """JSON MCP media type of the underlying card. - - Media type of the underlying AI skill card - """ - provenance: CatalogCandidateProvenance - """Where the catalog reference was observed, without the card itself or any content digest.""" - - source: CatalogCandidateSource - """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. - """ - description: str | None = None - """Description taken verbatim from the card. Inert untrusted text.""" - - publisher: str | None = None - """Publisher taken verbatim from the card. Inert untrusted text.""" - - @staticmethod - def from_dict(obj: Any) -> 'CatalogCandidate': - assert isinstance(obj, dict) - display_name = from_str(obj.get("displayName")) - handle = from_str(obj.get("handle")) - handle_expires_at = from_str(obj.get("handleExpiresAt")) - installability = CatalogCandidateInstallability(obj.get("installability")) - kind = CatalogCandidateKind(obj.get("kind")) - media_type = CatalogMediaType(obj.get("mediaType")) - provenance = CatalogCandidateProvenance.from_dict(obj.get("provenance")) - source = _load_CatalogCandidateSource(obj.get("source")) - description = from_union([from_str, from_none], obj.get("description")) - publisher = from_union([from_str, from_none], obj.get("publisher")) - return CatalogCandidate(display_name, handle, handle_expires_at, installability, kind, media_type, provenance, source, description, publisher) - - def to_dict(self) -> dict: - result: 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(CatalogCandidateInstallability, self.installability) - result["kind"] = to_enum(CatalogCandidateKind, self.kind) - result["mediaType"] = to_enum(CatalogMediaType, self.media_type) - result["provenance"] = to_class(CatalogCandidateProvenance, self.provenance) - result["source"] = (self.source).to_dict() - if self.description is not None: - result["description"] = from_union([from_str, from_none], self.description) - if self.publisher is not None: - result["publisher"] = from_union([from_str, from_none], self.publisher) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class CatalogMCPServerCandidate: @@ -26512,7 +26431,7 @@ class CatalogMCPServerCandidate: installability: CatalogMCPServerInstallabilityEnum """Whether this MCP server can be planned for installation, and if policy prevents it.""" - kind: CatalogMCPServerCandidateKind + kind: ClassVar[str] = "mcp-server" """Discriminator: this candidate describes an MCP server""" media_type: MCPServerCardMediaType @@ -26538,13 +26457,12 @@ def from_dict(obj: Any) -> 'CatalogMCPServerCandidate': handle = from_str(obj.get("handle")) handle_expires_at = from_str(obj.get("handleExpiresAt")) installability = CatalogMCPServerInstallabilityEnum(obj.get("installability")) - kind = CatalogMCPServerCandidateKind(obj.get("kind")) media_type = MCPServerCardMediaType(obj.get("mediaType")) provenance = CatalogMCPServerCandidateProvenance.from_dict(obj.get("provenance")) source = _load_CatalogCandidateSource(obj.get("source")) description = from_union([from_str, from_none], obj.get("description")) publisher = from_union([from_str, from_none], obj.get("publisher")) - return CatalogMCPServerCandidate(display_name, handle, handle_expires_at, installability, kind, media_type, provenance, source, description, publisher) + return CatalogMCPServerCandidate(display_name, handle, handle_expires_at, installability, media_type, provenance, source, description, publisher) def to_dict(self) -> dict: result: dict = {} @@ -26552,7 +26470,7 @@ def to_dict(self) -> dict: result["handle"] = from_str(self.handle) result["handleExpiresAt"] = from_str(self.handle_expires_at) result["installability"] = to_enum(CatalogMCPServerInstallabilityEnum, self.installability) - result["kind"] = to_enum(CatalogMCPServerCandidateKind, self.kind) + result["kind"] = self.kind result["mediaType"] = to_enum(MCPServerCardMediaType, self.media_type) result["provenance"] = to_class(CatalogMCPServerCandidateProvenance, self.provenance) result["source"] = (self.source).to_dict() @@ -30795,50 +30713,6 @@ 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 CatalogSearchSucceeded: - """A completed catalog search: inert candidate summaries, each carrying a single-use handle.""" - - candidates: list[CatalogCandidate] - """Matching candidates, never more than the requested limit. All text is inert untrusted - data. - """ - kind: ClassVar[str] = "succeeded" - """Discriminator: the search completed""" - - negotiated: CatalogNegotiatedContract - """Protocol version and capabilities the runtime honoured.""" - - search_id: str - """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. - """ - truncated: bool - """Whether further matches existed beyond the requested limit.""" - - @staticmethod - def from_dict(obj: Any) -> 'CatalogSearchSucceeded': - assert isinstance(obj, dict) - candidates = from_list(CatalogCandidate.from_dict, obj.get("candidates")) - negotiated = CatalogNegotiatedContract.from_dict(obj.get("negotiated")) - search_id = from_str(obj.get("searchId")) - truncated = from_bool(obj.get("truncated")) - return CatalogSearchSucceeded(candidates, negotiated, search_id, truncated) - - def to_dict(self) -> dict: - result: dict = {} - result["candidates"] = from_list(lambda x: to_class(CatalogCandidate, x), self.candidates) - result["kind"] = self.kind - result["negotiated"] = to_class(CatalogNegotiatedContract, self.negotiated) - result["searchId"] = from_str(self.search_id) - result["truncated"] = from_bool(self.truncated) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class HandlePendingToolCallRequest: @@ -37796,7 +37670,7 @@ def from_dict(obj: Any) -> 'RPC': catalog_ai_skill_candidate_provenance = CatalogAISkillCandidateProvenance.from_dict(obj.get("CatalogAiSkillCandidateProvenance")) catalog_authentication_required_error = CatalogAuthenticationRequiredError.from_dict(obj.get("CatalogAuthenticationRequiredError")) catalog_authentication_required_reason = CatalogAuthenticationRequiredReason(obj.get("CatalogAuthenticationRequiredReason")) - catalog_candidate = CatalogCandidate.from_dict(obj.get("CatalogCandidate")) + catalog_candidate = _load_CatalogCandidate(obj.get("CatalogCandidate")) catalog_candidate_kind = CatalogCandidateKind(obj.get("CatalogCandidateKind")) catalog_candidate_source = _load_CatalogCandidateSource(obj.get("CatalogCandidateSource")) catalog_candidate_source_embedded = CatalogCandidateSourceEmbedded.from_dict(obj.get("CatalogCandidateSourceEmbedded")) @@ -39016,7 +38890,7 @@ def to_dict(self) -> dict: result["CatalogAiSkillCandidateProvenance"] = to_class(CatalogAISkillCandidateProvenance, self.catalog_ai_skill_candidate_provenance) result["CatalogAuthenticationRequiredError"] = to_class(CatalogAuthenticationRequiredError, self.catalog_authentication_required_error) result["CatalogAuthenticationRequiredReason"] = to_enum(CatalogAuthenticationRequiredReason, self.catalog_authentication_required_reason) - result["CatalogCandidate"] = to_class(CatalogCandidate, self.catalog_candidate) + result["CatalogCandidate"] = (self.catalog_candidate).to_dict() result["CatalogCandidateKind"] = to_enum(CatalogCandidateKind, self.catalog_candidate_kind) result["CatalogCandidateSource"] = (self.catalog_candidate_source).to_dict() result["CatalogCandidateSourceEmbedded"] = to_class(CatalogCandidateSourceEmbedded, self.catalog_candidate_source_embedded) @@ -40182,6 +40056,17 @@ def _load_AuthInfo(obj: Any) -> "AuthInfo": case "api-key": return APIKeyAuthInfo.from_dict(obj) case _: raise ValueError(f"Unknown AuthInfo type: {kind!r}") +# One inert catalog result, represented as an MCP server or discovery-only AI skill variant so kind, media type, provenance, and installability cannot contradict each other. +CatalogCandidate = CatalogMCPServerCandidate | CatalogAISkillCandidate + +def _load_CatalogCandidate(obj: Any) -> "CatalogCandidate": + assert isinstance(obj, dict) + kind = obj.get("kind") + match kind: + case "mcp-server": return CatalogMCPServerCandidate.from_dict(obj) + case "ai-skill": return CatalogAISkillCandidate.from_dict(obj) + case _: raise ValueError(f"Unknown CatalogCandidate kind: {kind!r}") + # 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. CatalogCandidateSource = CatalogCandidateSourceURL | CatalogCandidateSourceEmbedded @@ -43548,7 +43433,6 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "CatalogCandidate", "CatalogCandidateInstallability", "CatalogCandidateKind", - "CatalogCandidateProvenance", "CatalogCandidateSource", "CatalogCandidateSourceEmbedded", "CatalogCandidateSourceKind", diff --git a/python/test_rpc_generated.py b/python/test_rpc_generated.py index a21dcc0b6e..06b920486e 100644 --- a/python/test_rpc_generated.py +++ b/python/test_rpc_generated.py @@ -7,6 +7,15 @@ from copilot.rpc import ( BuiltinToolInputSchemaType, + CatalogAISkillCandidate, + CatalogAuthenticationRequiredError, + CatalogCandidateSourceEmbedded, + CatalogCandidateSourceURL, + CatalogClientContract, + CatalogMCPServerCandidate, + CatalogNetworkFailureError, + CatalogSearchRequest, + CatalogSearchSucceeded, CommandsApi, CommandsInvokeRequest, CommandsRespondToQueuedCommandRequest, @@ -17,12 +26,16 @@ RemoteControlStatusResult, RemoteSessionMetadataValue, SandboxConfig, + ServerCatalogApi, SessionList, SlashCommandTextResult, TaskAgentInfo, UIElicitationSchemaType, ) +OPAQUE_MCP_HANDLE = "opaque:mcp/01-do-not-parse" +OPAQUE_SKILL_HANDLE = "opaque:skill/02-do-not-parse" + def test_sandbox_config_round_trips_allow_bypass_and_omits_when_absent(): configured = SandboxConfig(enabled=True, allow_bypass=True) @@ -140,3 +153,120 @@ def test_queued_command_result_serializes_boolean_discriminator( assert request.to_dict()["result"]["handled"] is expected_handled assert isinstance(round_tripped.result, type(variant)) + + +@pytest.mark.asyncio +async def test_catalog_search_preserves_typed_candidates_and_opaque_handles(): + client = AsyncMock() + client.request = AsyncMock( + return_value={ + "kind": "succeeded", + "searchId": "search-01", + "candidates": [ + { + "kind": "mcp-server", + "handle": OPAQUE_MCP_HANDLE, + "handleExpiresAt": "2026-09-02T12:00:00Z", + "mediaType": "application/mcp-server-card+json", + "installability": "installable", + "displayName": "Example MCP", + "rawCard": {"secret": "must-not-survive"}, + "source": { + "kind": "url", + "url": "https://catalog.example/mcp.json", + }, + "provenance": { + "authority": "catalog.example", + "observedAt": "2026-09-02T11:00:00Z", + "mediaType": "application/mcp-server-card+json", + }, + }, + { + "kind": "ai-skill", + "handle": OPAQUE_SKILL_HANDLE, + "handleExpiresAt": "2026-09-02T12:00:00Z", + "mediaType": "application/ai-skill", + "installability": "not-installable-kind", + "displayName": "Example skill", + "rawCard": {"secret": "must-not-survive"}, + "source": {"kind": "embedded"}, + "provenance": { + "authority": "catalog.example", + "observedAt": "2026-09-02T11:00:00Z", + "mediaType": "application/ai-skill", + }, + }, + ], + "truncated": False, + "negotiated": { + "runtimeProtocolVersion": 1, + "grantedCapabilities": [ + "mcp-server-card", + "ai-skill-discovery", + ], + }, + } + ) + api = ServerCatalogApi(client) + + result = await api.search( + CatalogSearchRequest( + contract=CatalogClientContract( + protocol_version=1, + required_capabilities=[], + ), + query="example", + ) + ) + + assert isinstance(result, CatalogSearchSucceeded) + mcp, skill = result.candidates + assert isinstance(mcp, CatalogMCPServerCandidate) + assert isinstance(skill, CatalogAISkillCandidate) + assert mcp.handle == OPAQUE_MCP_HANDLE + assert skill.handle == OPAQUE_SKILL_HANDLE + assert isinstance(mcp.source, CatalogCandidateSourceURL) + assert isinstance(skill.source, CatalogCandidateSourceEmbedded) + for candidate in result.to_dict()["candidates"]: + assert {"card", "cardData", "rawCard"}.isdisjoint(candidate) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("payload", "expected_type"), + [ + ( + { + "kind": "authentication-required", + "reason": "no-credential", + "message": "Sign in is required.", + }, + CatalogAuthenticationRequiredError, + ), + ( + { + "kind": "network-failure", + "reason": "timeout", + "retryAfterSeconds": 30, + "message": "The catalogue timed out.", + }, + CatalogNetworkFailureError, + ), + ], +) +async def test_catalog_search_preserves_refusals_and_failures(payload, expected_type): + client = AsyncMock() + client.request = AsyncMock(return_value=payload) + api = ServerCatalogApi(client) + + result = await api.search( + CatalogSearchRequest( + contract=CatalogClientContract( + protocol_version=1, + required_capabilities=[], + ), + query="example", + ) + ) + + assert isinstance(result, expected_type) diff --git a/rust/tests/catalogue_conformance_test.rs b/rust/tests/catalogue_conformance_test.rs new file mode 100644 index 0000000000..035b5ebd66 --- /dev/null +++ b/rust/tests/catalogue_conformance_test.rs @@ -0,0 +1,103 @@ +#![allow(clippy::unwrap_used)] + +use github_copilot_sdk::rpc::{CatalogCandidate, CatalogCandidateSource, CatalogSearchResult}; + +const OPAQUE_MCP_HANDLE: &str = "opaque:mcp/01-do-not-parse"; +const OPAQUE_SKILL_HANDLE: &str = "opaque:skill/02-do-not-parse"; + +#[test] +fn catalog_search_result_preserves_candidate_semantics() { + let result: CatalogSearchResult = serde_json::from_value(serde_json::json!({ + "kind": "succeeded", + "searchId": "search-01", + "candidates": [ + { + "kind": "mcp-server", + "handle": OPAQUE_MCP_HANDLE, + "handleExpiresAt": "2026-09-02T12:00:00Z", + "mediaType": "application/mcp-server-card+json", + "installability": "installable", + "displayName": "Example MCP", + "rawCard": {"secret": "must-not-survive"}, + "source": {"kind": "url", "url": "https://catalog.example/mcp.json"}, + "provenance": { + "authority": "catalog.example", + "observedAt": "2026-09-02T11:00:00Z", + "mediaType": "application/mcp-server-card+json" + } + }, + { + "kind": "ai-skill", + "handle": OPAQUE_SKILL_HANDLE, + "handleExpiresAt": "2026-09-02T12:00:00Z", + "mediaType": "application/ai-skill", + "installability": "not-installable-kind", + "displayName": "Example skill", + "rawCard": {"secret": "must-not-survive"}, + "source": {"kind": "embedded"}, + "provenance": { + "authority": "catalog.example", + "observedAt": "2026-09-02T11:00:00Z", + "mediaType": "application/ai-skill" + } + } + ], + "truncated": false, + "negotiated": { + "runtimeProtocolVersion": 1, + "grantedCapabilities": ["mcp-server-card", "ai-skill-discovery"] + } + })) + .unwrap(); + + let CatalogSearchResult::Succeeded(success) = &result else { + panic!("expected a successful catalogue search"); + }; + let CatalogCandidate::McpServer(mcp) = &success.candidates[0] else { + panic!("expected an MCP server candidate"); + }; + let CatalogCandidate::AiSkill(skill) = &success.candidates[1] else { + panic!("expected an AI skill candidate"); + }; + assert_eq!(mcp.handle, OPAQUE_MCP_HANDLE); + assert_eq!(skill.handle, OPAQUE_SKILL_HANDLE); + assert!(matches!(mcp.source, CatalogCandidateSource::Url(_))); + assert!(matches!(skill.source, CatalogCandidateSource::Embedded(_))); + + let wire = serde_json::to_value(&result).unwrap(); + for candidate in wire["candidates"].as_array().unwrap() { + let fields = candidate.as_object().unwrap(); + for forbidden in ["card", "cardData", "rawCard"] { + assert!( + !fields.contains_key(forbidden), + "candidate leaked {forbidden}" + ); + } + } +} + +#[test] +fn catalog_search_result_preserves_refusals_and_failures() { + let authentication: CatalogSearchResult = serde_json::from_value(serde_json::json!({ + "kind": "authentication-required", + "reason": "no-credential", + "message": "Sign in is required." + })) + .unwrap(); + assert!(matches!( + authentication, + CatalogSearchResult::AuthenticationRequired(_) + )); + + let network: CatalogSearchResult = serde_json::from_value(serde_json::json!({ + "kind": "network-failure", + "reason": "timeout", + "retryAfterSeconds": 30, + "message": "The catalogue timed out." + })) + .unwrap(); + let CatalogSearchResult::NetworkFailure(failure) = network else { + panic!("expected a network failure"); + }; + assert_eq!(failure.retry_after_seconds, Some(30)); +} diff --git a/scripts/codegen/catalogue-conformance.ts b/scripts/codegen/catalogue-conformance.ts new file mode 100644 index 0000000000..fdbfd7fee4 --- /dev/null +++ b/scripts/codegen/catalogue-conformance.ts @@ -0,0 +1,99 @@ +import fs from "fs/promises"; +import path from "path"; + +import { getApiSchemaPath, REPO_ROOT } from "./utils.js"; + +const FORBIDDEN_CANDIDATE_FIELDS = ["card", "cardData", "rawCard"]; + +function assert(condition: unknown, message: string): asserts condition { + if (!condition) throw new Error(`Catalogue schema conformance failed: ${message}`); +} + +function referencedDefinitionNames(schema: { anyOf?: Array<{ $ref?: string }> }): string[] { + return (schema.anyOf ?? []).map((variant) => variant.$ref?.split("/").at(-1) ?? ""); +} + +const schemaPath = await getApiSchemaPath(); +const packageRoot = path.dirname(path.dirname(schemaPath)); +const sdkPackageLock = JSON.parse( + await fs.readFile(path.join(REPO_ROOT, "nodejs/package-lock.json"), "utf8") +) as { packages?: Record }; +const expectedPackageVersion = + sdkPackageLock.packages?.["node_modules/@github/copilot"]?.version; +const packageJson = JSON.parse( + await fs.readFile(path.join(packageRoot, "package.json"), "utf8") +) as { version?: string }; +const schema = JSON.parse(await fs.readFile(schemaPath, "utf8")) as { + definitions: Record< + string, + { + anyOf?: Array<{ $ref?: string }>; + properties?: Record; + } + >; + server?: { + catalog?: { + search?: { + rpcMethod?: string; + params?: { $ref?: string }; + result?: { $ref?: string }; + }; + }; + }; +}; + +assert( + expectedPackageVersion !== undefined + && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(expectedPackageVersion), + "nodejs/package-lock.json must pin @github/copilot to an exact version" +); +assert( + packageJson.version === expectedPackageVersion, + `expected @github/copilot ${expectedPackageVersion}, received ${packageJson.version ?? "unknown"}` +); +assert( + schema.server?.catalog?.search?.rpcMethod === "catalog.search", + "catalog.search is missing" +); +assert( + schema.server.catalog.search.params?.$ref === "#/definitions/CatalogSearchRequest", + "catalog.search request is not typed" +); +assert( + schema.server.catalog.search.result?.$ref === "#/definitions/CatalogSearchResult", + "catalog.search result is not typed" +); + +const candidateVariants = referencedDefinitionNames(schema.definitions.CatalogCandidate); +assert( + candidateVariants.join(",") === "CatalogMcpServerCandidate,CatalogAiSkillCandidate", + `unexpected candidate variants: ${candidateVariants.join(", ")}` +); +const sourceVariants = referencedDefinitionNames(schema.definitions.CatalogCandidateSource); +assert( + sourceVariants.join(",") === "CatalogCandidateSourceUrl,CatalogCandidateSourceEmbedded", + `unexpected candidate source variants: ${sourceVariants.join(", ")}` +); + +for (const name of candidateVariants) { + const properties = schema.definitions[name]?.properties; + assert(properties?.handle?.type === "string", `${name}.handle must remain an opaque string`); + for (const field of FORBIDDEN_CANDIDATE_FIELDS) { + assert(!(field in properties), `${name} exposes forbidden raw card field ${field}`); + } +} + +const resultVariants = new Set( + referencedDefinitionNames(schema.definitions.CatalogSearchResult) +); +for (const name of [ + "CatalogSearchSucceeded", + "CatalogAuthenticationRequiredError", + "CatalogNetworkFailureError", + "CatalogContractViolationError", + "CatalogUnavailableError", +]) { + assert(resultVariants.has(name), `CatalogSearchResult is missing ${name}`); +} + +console.log(`Catalogue schema conformance: @github/copilot ${packageJson.version}`); diff --git a/scripts/codegen/package.json b/scripts/codegen/package.json index 8e65352916..ed4092891f 100644 --- a/scripts/codegen/package.json +++ b/scripts/codegen/package.json @@ -3,7 +3,8 @@ "private": true, "type": "module", "scripts": { - "generate": "tsx typescript.ts && tsx csharp.ts && tsx python.ts && tsx go.ts && tsx rust.ts", + "conformance": "tsx catalogue-conformance.ts", + "generate": "npm run conformance && tsx typescript.ts && tsx csharp.ts && tsx python.ts && tsx go.ts && tsx rust.ts", "generate:ts": "tsx typescript.ts", "generate:csharp": "tsx csharp.ts", "generate:python": "tsx python.ts", diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index b3bfcf8bc9..34002ceaf1 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -413,6 +413,7 @@ function postProcessRefBasedDiscriminatedUnionsForPython( const acronymCandidates = (name: string): string[] => { const substitutions: Array<[RegExp, string]> = [ [/Api/g, "API"], + [/Ai/g, "AI"], [/Mcp/g, "MCP"], [/Url/g, "URL"], [/Json/g, "JSON"], diff --git a/scripts/codegen/typescript.ts b/scripts/codegen/typescript.ts index f5e8acb146..d83f9b2e05 100644 --- a/scripts/codegen/typescript.ts +++ b/scripts/codegen/typescript.ts @@ -885,6 +885,20 @@ function hasInternalMethods(node: Record): boolean { } if (schema.server) { + if (collectRpcMethods(schema.server).some((method) => method.rpcMethod === "catalog.search")) { + lines.push(`const FORBIDDEN_CATALOG_RESPONSE_FIELDS = new Set(["card", "cardData", "rawCard"]);`); + lines.push(""); + lines.push(`function sanitizeCatalogSearchResult(value: unknown): unknown {`); + lines.push(` if (Array.isArray(value)) return value.map(sanitizeCatalogSearchResult);`); + lines.push(` if (value === null || typeof value !== "object") return value;`); + lines.push(` return Object.fromEntries(`); + lines.push(` Object.entries(value)`); + lines.push(` .filter(([key]) => !FORBIDDEN_CATALOG_RESPONSE_FIELDS.has(key))`); + lines.push(` .map(([key, child]) => [key, sanitizeCatalogSearchResult(child)]),`); + lines.push(` );`); + lines.push(`}`); + lines.push(""); + } lines.push(`/** Create typed server-scoped RPC methods (no session required). */`); lines.push(`export function createServerRpc(connection: MessageConnection) {`); lines.push(` return {`); @@ -1016,7 +1030,13 @@ function emitGroup( includeExperimental: (value as RpcMethod).stability === "experimental" && !parentExperimental, }); lines.push(`${indent}${key}: async (${sigParams.join(", ")}): Promise<${resultType}> =>`); - lines.push(`${indent} connection.sendRequest("${rpcMethod}", ${bodyArg}),`); + if (rpcMethod === "catalog.search") { + lines.push(`${indent} sanitizeCatalogSearchResult(`); + lines.push(`${indent} await connection.sendRequest("${rpcMethod}", ${bodyArg}),`); + lines.push(`${indent} ) as ${resultType},`); + } else { + lines.push(`${indent} connection.sendRequest("${rpcMethod}", ${bodyArg}),`); + } } else if (typeof value === "object" && value !== null) { const groupExperimental = isNodeFullyExperimental(value as Record); const groupDeprecated = isNodeFullyDeprecated(value as Record); From ef23568f2ae92435d8ca8b9dba8e6293382ca3d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6khan=20Arkan?= Date: Thu, 3 Sep 2026 10:35:23 +0300 Subject: [PATCH 2/6] Address catalogue conformance findings --- go/rpc/catalogue_conformance_test.go | 17 ++++++++++++ go/rpc/zrpc.go | 10 ------- go/rpc/zrpc_encoding.go | 14 +--------- java/scripts/codegen/package-lock.json | 2 +- java/scripts/codegen/package.json | 2 +- java/sdk/pom.xml | 1 + python/copilot/generated/rpc.py | 6 ++-- python/test_rpc_generated.py | 35 ++++++++++++++++++++++++ scripts/codegen/catalogue-conformance.ts | 9 ++++++ scripts/codegen/go.ts | 4 +-- scripts/codegen/python.ts | 24 ++++++++++++---- 11 files changed, 88 insertions(+), 36 deletions(-) diff --git a/go/rpc/catalogue_conformance_test.go b/go/rpc/catalogue_conformance_test.go index a18e7140c5..88585d3114 100644 --- a/go/rpc/catalogue_conformance_test.go +++ b/go/rpc/catalogue_conformance_test.go @@ -136,3 +136,20 @@ func TestCatalogSearchResultPreservesRefusalsAndFailures(t *testing.T) { }) } } + +func TestCatalogSearchResultRejectsUnknownCandidateKinds(t *testing.T) { + _, err := unmarshalCatalogSearchResult([]byte(`{ + "kind":"succeeded", + "searchId":"search-unknown", + "candidates":[{ + "kind":"future-kind", + "handle":"opaque:future/03-do-not-parse", + "rawCard":{"secret":"must-not-survive"} + }], + "truncated":false, + "negotiated":{"runtimeProtocolVersion":1,"grantedCapabilities":[]} + }`)) + if err == nil { + t.Fatal("unknown catalogue candidate kind with rawCard must be rejected") + } +} diff --git a/go/rpc/zrpc.go b/go/rpc/zrpc.go index 3c9d2d46de..5d0f0638db 100644 --- a/go/rpc/zrpc.go +++ b/go/rpc/zrpc.go @@ -1465,16 +1465,6 @@ type CatalogCandidate interface { Kind() CatalogCandidateKind } -type RawCatalogCandidateData struct { - Discriminator CatalogCandidateKind - Raw json.RawMessage -} - -func (RawCatalogCandidateData) catalogCandidate() {} -func (r RawCatalogCandidateData) Kind() CatalogCandidateKind { - return r.Discriminator -} - // An inert AI skill catalog result. AI skills are discovery-only and cannot be represented // as installable through this surface. // Experimental: CatalogAiSkillCandidate is part of an experimental API and may change or be diff --git a/go/rpc/zrpc_encoding.go b/go/rpc/zrpc_encoding.go index 13d190be22..e08a9ffe9d 100644 --- a/go/rpc/zrpc_encoding.go +++ b/go/rpc/zrpc_encoding.go @@ -696,20 +696,8 @@ func unmarshalCatalogCandidate(data []byte) (CatalogCandidate, error) { return nil, err } return &d, nil - default: - return &RawCatalogCandidateData{Discriminator: raw.Kind, Raw: data}, nil - } -} - -func (r RawCatalogCandidateData) MarshalJSON() ([]byte, error) { - if r.Raw != nil { - return r.Raw, nil } - return json.Marshal(struct { - Kind CatalogCandidateKind `json:"kind"` - }{ - Kind: r.Discriminator, - }) + return nil, errors.New("data did not match any union variant for CatalogCandidate") } func unmarshalCatalogCandidateSource(data []byte) (CatalogCandidateSource, error) { diff --git a/java/scripts/codegen/package-lock.json b/java/scripts/codegen/package-lock.json index 5e10cd839b..0d01b57ac0 100644 --- a/java/scripts/codegen/package-lock.json +++ b/java/scripts/codegen/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "copilot-sdk-java-codegen", "dependencies": { - "@github/copilot": "^1.0.83-5", + "@github/copilot": "1.0.83-5", "json-schema": "^0.4.0", "tsx": "^4.23.13" } diff --git a/java/scripts/codegen/package.json b/java/scripts/codegen/package.json index 5ef4484fb7..59e1aea0d2 100644 --- a/java/scripts/codegen/package.json +++ b/java/scripts/codegen/package.json @@ -7,7 +7,7 @@ "generate:java": "tsx java.ts" }, "dependencies": { - "@github/copilot": "^1.0.83-5", + "@github/copilot": "1.0.83-5", "json-schema": "^0.4.0", "tsx": "^4.23.13" } diff --git a/java/sdk/pom.xml b/java/sdk/pom.xml index aa40b25189..01374dec39 100644 --- a/java/sdk/pom.xml +++ b/java/sdk/pom.xml @@ -796,6 +796,7 @@ did not produce the multi-release output. Re-build on JDK 25+ and verify the ${project.parent.basedir}/scripts/codegen install + --save-exact @github/copilot@${copilot.schema.version} diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index 1d134beff4..52a07e576e 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -40065,7 +40065,7 @@ def _load_CatalogCandidate(obj: Any) -> "CatalogCandidate": match kind: case "mcp-server": return CatalogMCPServerCandidate.from_dict(obj) case "ai-skill": return CatalogAISkillCandidate.from_dict(obj) - case _: raise ValueError(f"Unknown CatalogCandidate kind: {kind!r}") + raise ValueError(f"Unknown CatalogCandidate kind: {kind!r}") # 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. CatalogCandidateSource = CatalogCandidateSourceURL | CatalogCandidateSourceEmbedded @@ -40076,7 +40076,7 @@ def _load_CatalogCandidateSource(obj: Any) -> "CatalogCandidateSource": match kind: case "url": return CatalogCandidateSourceURL.from_dict(obj) case "embedded": return CatalogCandidateSourceEmbedded.from_dict(obj) - case _: raise ValueError(f"Unknown CatalogCandidateSource kind: {kind!r}") + raise ValueError(f"Unknown CatalogCandidateSource kind: {kind!r}") # Outcome of a catalog.search call: either bounded inert candidates, or one typed refusal. Never a partial success. CatalogSearchResult = CatalogSearchSucceeded | CatalogNegotiationRefusedError | CatalogUnsupportedKindError | CatalogInvalidRequestError | CatalogAuthenticationRequiredError | CatalogPolicyRejectedError | CatalogNetworkFailureError | CatalogUnsafeRetrievalError | CatalogMalformedCardError | CatalogContractViolationError | CatalogUnavailableError @@ -40096,7 +40096,7 @@ def _load_CatalogSearchResult(obj: Any) -> "CatalogSearchResult": case "malformed-card": return CatalogMalformedCardError.from_dict(obj) case "contract-violation": return CatalogContractViolationError.from_dict(obj) case "unavailable": return CatalogUnavailableError.from_dict(obj) - case _: raise ValueError(f"Unknown CatalogSearchResult kind: {kind!r}") + raise ValueError(f"Unknown CatalogSearchResult kind: {kind!r}") # A content block within a tool result, which may be text, terminal output, image, audio, or a resource ExternalToolTextResultForLlmContent = ExternalToolTextResultForLlmContentText | ExternalToolTextResultForLlmContentTerminal | ExternalToolTextResultForLlmContentShellExit | ExternalToolTextResultForLlmContentImage | ExternalToolTextResultForLlmContentAudio | ExternalToolTextResultForLlmContentResourceLink | ExternalToolTextResultForLlmContentResource diff --git a/python/test_rpc_generated.py b/python/test_rpc_generated.py index 06b920486e..5c52de8435 100644 --- a/python/test_rpc_generated.py +++ b/python/test_rpc_generated.py @@ -270,3 +270,38 @@ async def test_catalog_search_preserves_refusals_and_failures(payload, expected_ ) assert isinstance(result, expected_type) + + +@pytest.mark.asyncio +async def test_catalog_search_rejects_unknown_candidate_kinds(): + client = AsyncMock() + client.request = AsyncMock( + return_value={ + "kind": "succeeded", + "searchId": "search-unknown", + "candidates": [ + { + "kind": "future-kind", + "handle": "opaque:future/03-do-not-parse", + "rawCard": {"secret": "must-not-survive"}, + } + ], + "truncated": False, + "negotiated": { + "runtimeProtocolVersion": 1, + "grantedCapabilities": [], + }, + } + ) + api = ServerCatalogApi(client) + + with pytest.raises(ValueError, match="Unknown CatalogCandidate kind"): + await api.search( + CatalogSearchRequest( + contract=CatalogClientContract( + protocol_version=1, + required_capabilities=[], + ), + query="example", + ) + ) diff --git a/scripts/codegen/catalogue-conformance.ts b/scripts/codegen/catalogue-conformance.ts index fdbfd7fee4..bffd40f848 100644 --- a/scripts/codegen/catalogue-conformance.ts +++ b/scripts/codegen/catalogue-conformance.ts @@ -20,6 +20,11 @@ const sdkPackageLock = JSON.parse( ) as { packages?: Record }; const expectedPackageVersion = sdkPackageLock.packages?.["node_modules/@github/copilot"]?.version; +const javaCodegenPackageJson = JSON.parse( + await fs.readFile(path.join(REPO_ROOT, "java/scripts/codegen/package.json"), "utf8") +) as { dependencies?: Record }; +const javaCodegenPackageVersion = + javaCodegenPackageJson.dependencies?.["@github/copilot"]; const packageJson = JSON.parse( await fs.readFile(path.join(packageRoot, "package.json"), "utf8") ) as { version?: string }; @@ -51,6 +56,10 @@ assert( packageJson.version === expectedPackageVersion, `expected @github/copilot ${expectedPackageVersion}, received ${packageJson.version ?? "unknown"}` ); +assert( + javaCodegenPackageVersion === expectedPackageVersion, + `java/scripts/codegen must pin @github/copilot exactly to ${expectedPackageVersion}` +); assert( schema.server?.catalog?.search?.rpcMethod === "catalog.search", "catalog.search is missing" diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts index f609009320..46598c357c 100644 --- a/scripts/codegen/go.ts +++ b/scripts/codegen/go.ts @@ -1817,7 +1817,7 @@ function emitGoFlatDiscriminatedUnion( const unmarshalFuncName = goUnexportedFunctionName("unmarshal", typeName); const rawDataName = `Raw${typeName}${ctx.discriminatedUnionRawVariantSuffix ?? "Data"}`; - const hasRawVariant = discriminator.valueKind === "string"; + const hasRawVariant = discriminator.valueKind === "string" && typeName !== "CatalogCandidate"; const markerName = toGoUnexportedIdentifier(typeName); ctx.discriminatedUnions.set(typeName, { typeName, unmarshalFuncName }); @@ -1906,7 +1906,7 @@ function emitGoFlatDiscriminatedUnion( unmarshalLines.push(`\t\treturn &${rawDataName}{Discriminator: ${rawDiscExpr}, Raw: data}, nil`); } unmarshalLines.push(`\t}`); - if (discriminator.valueKind === "boolean") { + if (!hasRawVariant) { unmarshalLines.push(`\treturn nil, errors.New("data did not match any union variant for ${typeName}")`); } unmarshalLines.push(`}`); diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index 34002ceaf1..f14e578f87 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -508,9 +508,15 @@ function postProcessRefBasedDiscriminatedUnionsForPython( for (const m of actualDispatch) { dispatcherLines.push(` case ${pyDiscriminatorValueExpr(m.value)}: return ${m.typeName}.from_dict(obj)`); } - dispatcherLines.push( - ` case _: raise ValueError(f"Unknown ${actualAliasName} ${union.discriminatorProp}: {kind!r}")` - ); + if (actualAliasName.startsWith("Catalog")) { + dispatcherLines.push( + ` raise ValueError(f"Unknown ${actualAliasName} ${union.discriminatorProp}: {kind!r}")` + ); + } else { + dispatcherLines.push( + ` case _: raise ValueError(f"Unknown ${actualAliasName} ${union.discriminatorProp}: {kind!r}")` + ); + } code = `${code.trimEnd()}\n\n\n${aliasLine}\n\n\n${dispatcherLines.join("\n")}\n`; } @@ -1775,9 +1781,15 @@ function tryEmitPyRefBasedDiscriminatedUnion( ` case ${pyDiscriminatorValueExpr(m.value)}: return ${m.typeName}.from_dict(obj)` ); } - lines.push( - ` case _: raise ValueError(f"Unknown ${aliasName} ${discriminator.property}: {kind!r}")` - ); + if (aliasName.startsWith("Catalog")) { + lines.push( + ` raise ValueError(f"Unknown ${aliasName} ${discriminator.property}: {kind!r}")` + ); + } else { + lines.push( + ` case _: raise ValueError(f"Unknown ${aliasName} ${discriminator.property}: {kind!r}")` + ); + } ctx.classes.push(lines.join("\n")); } From 2b8365480133f65274ad255931af79d15e2995b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6khan=20Arkan?= Date: Fri, 4 Sep 2026 13:40:41 +0300 Subject: [PATCH 3/6] Generalise closed union codegen --- .../workflows/update-copilot-dependency.yml | 2 +- dotnet/src/Generated/Rpc.cs | 100 +- ... => DiscriminatedUnionConformanceTests.cs} | 82 +- ...> discriminated_union_conformance_test.go} | 62 +- go/rpc/zrpc.go | 10 + go/rpc/zrpc_encoding.go | 89 +- java/scripts/codegen/java.ts | 95 +- .../copilot/generated/rpc/McpInstallPlan.java | 2 +- .../generated/rpc/McpPlanEnumValueType.java | 33 + .../rpc/McpPlanPackageTransport.java | 33 + .../generated/rpc/McpPlanRemoteTransport.java | 37 + .../generated/rpc/McpPlanRequiredValue.java | 35 + .../rpc/McpPlanRequiredValueEnum.java | 94 ++ .../rpc/McpPlanRequiredValueScalar.java | 86 ++ .../generated/rpc/McpPlanScalarValueType.java | 39 + .../rpc/McpPlanSecretPlaceholder.java | 31 + .../generated/rpc/McpPlanTransportChoice.java | 35 + .../rpc/McpPlanTransportChoicePackage.java | 73 + .../rpc/McpPlanTransportChoiceRemote.java | 66 + .../generated/rpc/McpPlanValueCategory.java | 41 + .../generated/CatalogConformanceTest.java | 107 -- .../DiscriminatedUnionConformanceTest.java | 179 +++ nodejs/src/generated/rpc.ts | 1301 ++++++++++++++++- .../test/discriminated-union-codegen.test.ts | 140 ++ ...> discriminated-union-conformance.test.ts} | 54 +- python/copilot/generated/rpc.py | 40 +- python/test_rpc_generated.py | 96 +- ...> discriminated_union_conformance_test.rs} | 84 +- scripts/codegen/catalogue-conformance.ts | 108 -- scripts/codegen/csharp.ts | 33 +- scripts/codegen/go.ts | 32 +- scripts/codegen/package.json | 2 +- scripts/codegen/python.ts | 24 +- scripts/codegen/schema-conformance.ts | 138 ++ scripts/codegen/schema-unions.ts | 145 ++ scripts/codegen/typescript.ts | 313 +++- 36 files changed, 3379 insertions(+), 462 deletions(-) rename dotnet/test/Unit/{CatalogueConformanceTests.cs => DiscriminatedUnionConformanceTests.cs} (55%) rename go/rpc/{catalogue_conformance_test.go => discriminated_union_conformance_test.go} (63%) create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanEnumValueType.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanPackageTransport.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanRemoteTransport.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanRequiredValue.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanRequiredValueEnum.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanRequiredValueScalar.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanScalarValueType.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanSecretPlaceholder.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanTransportChoice.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanTransportChoicePackage.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanTransportChoiceRemote.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanValueCategory.java delete mode 100644 java/sdk/src/test/java/com/github/copilot/generated/CatalogConformanceTest.java create mode 100644 java/sdk/src/test/java/com/github/copilot/generated/DiscriminatedUnionConformanceTest.java create mode 100644 nodejs/test/discriminated-union-codegen.test.ts rename nodejs/test/{catalogue-conformance.test.ts => discriminated-union-conformance.test.ts} (73%) rename rust/tests/{catalogue_conformance_test.rs => discriminated_union_conformance_test.rs} (51%) delete mode 100644 scripts/codegen/catalogue-conformance.ts create mode 100644 scripts/codegen/schema-conformance.ts create mode 100644 scripts/codegen/schema-unions.ts diff --git a/.github/workflows/update-copilot-dependency.yml b/.github/workflows/update-copilot-dependency.yml index 2870ad27f7..7374dab863 100644 --- a/.github/workflows/update-copilot-dependency.yml +++ b/.github/workflows/update-copilot-dependency.yml @@ -91,7 +91,7 @@ jobs: env: VERSION: ${{ inputs.version }} working-directory: ./java/scripts/codegen - run: npm install "@github/copilot@$VERSION" + run: npm install --save-exact "@github/copilot@$VERSION" - name: Update Java POM CLI version property env: diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index 2c74ab4bde..d18bb91100 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -625,7 +625,8 @@ internal sealed class AccountGetQuotaRequest [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "type", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] + IgnoreUnrecognizedTypeDiscriminators = false, + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] [JsonDerivedType(typeof(AuthInfoHmac), "hmac")] [JsonDerivedType(typeof(AuthInfoEnv), "env")] [JsonDerivedType(typeof(AuthInfoToken), "token")] @@ -637,6 +638,7 @@ internal sealed class AccountGetQuotaRequest public partial class AuthInfo { /// The type discriminator. + [JsonRequired] [JsonPropertyName("type")] public virtual string Type { get; set; } = string.Empty; } @@ -1313,7 +1315,8 @@ internal sealed class McpDiscoverRequest [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] + IgnoreUnrecognizedTypeDiscriminators = false, + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] [JsonDerivedType(typeof(McpPlanInstallResultPlanned), "planned")] [JsonDerivedType(typeof(McpPlanInstallResultNegotiationRefused), "negotiation-refused")] [JsonDerivedType(typeof(McpPlanInstallResultHandleRejected), "handle-rejected")] @@ -1330,6 +1333,7 @@ internal sealed class McpDiscoverRequest public partial class McpPlanInstallResult { /// The type discriminator. + [JsonRequired] [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } @@ -1479,12 +1483,14 @@ public sealed class McpPlanTarget [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "installMethod", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] + IgnoreUnrecognizedTypeDiscriminators = false, + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] [JsonDerivedType(typeof(McpPlanTransportChoicePackage), "package")] [JsonDerivedType(typeof(McpPlanTransportChoiceRemote), "remote")] public partial class McpPlanTransportChoice { /// The type discriminator. + [JsonRequired] [JsonPropertyName("installMethod")] public virtual string InstallMethod { get; set; } = string.Empty; } @@ -1495,12 +1501,14 @@ public partial class McpPlanTransportChoice [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] + IgnoreUnrecognizedTypeDiscriminators = false, + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] [JsonDerivedType(typeof(McpPlanRequiredValueScalar), "scalar")] [JsonDerivedType(typeof(McpPlanRequiredValueEnum), "enum")] public partial class McpPlanRequiredValue { /// The type discriminator. + [JsonRequired] [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } @@ -2074,12 +2082,14 @@ public sealed class CatalogClientContract [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] + IgnoreUnrecognizedTypeDiscriminators = false, + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] [JsonDerivedType(typeof(McpPlanInstallSourceCandidate), "candidate")] [JsonDerivedType(typeof(McpPlanInstallSourceCard), "card")] public partial class McpPlanInstallSource { /// The type discriminator. + [JsonRequired] [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } @@ -2114,12 +2124,14 @@ public partial class McpPlanInstallSourceCandidate : McpPlanInstallSource [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] + IgnoreUnrecognizedTypeDiscriminators = false, + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] [JsonDerivedType(typeof(McpServerCardReferenceUrl), "url")] [JsonDerivedType(typeof(McpServerCardReferenceEmbedded), "embedded")] public partial class McpServerCardReference { /// The type discriminator. + [JsonRequired] [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } @@ -2347,7 +2359,8 @@ internal sealed class DiscoveredExtensionsDisableRequest [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] + IgnoreUnrecognizedTypeDiscriminators = false, + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] [JsonDerivedType(typeof(CatalogSearchResultSucceeded), "succeeded")] [JsonDerivedType(typeof(CatalogSearchResultNegotiationRefused), "negotiation-refused")] [JsonDerivedType(typeof(CatalogSearchResultUnsupportedKind), "unsupported-kind")] @@ -2362,6 +2375,7 @@ internal sealed class DiscoveredExtensionsDisableRequest public partial class CatalogSearchResult { /// The type discriminator. + [JsonRequired] [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } @@ -2372,12 +2386,14 @@ public partial class CatalogSearchResult [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] + IgnoreUnrecognizedTypeDiscriminators = false, + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] [JsonDerivedType(typeof(CatalogCandidateMcpServer), "mcp-server")] [JsonDerivedType(typeof(CatalogCandidateAiSkill), "ai-skill")] public partial class CatalogCandidate { /// The type discriminator. + [JsonRequired] [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } @@ -2407,12 +2423,14 @@ public sealed class CatalogMcpServerCandidateProvenance [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] + IgnoreUnrecognizedTypeDiscriminators = false, + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] [JsonDerivedType(typeof(CatalogCandidateSourceUrl), "url")] [JsonDerivedType(typeof(CatalogCandidateSourceEmbedded), "embedded")] public partial class CatalogCandidateSource { /// The type discriminator. + [JsonRequired] [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } @@ -4758,7 +4776,8 @@ internal sealed class SessionsGetBoardEntryCountRequest [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "state", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] + IgnoreUnrecognizedTypeDiscriminators = false, + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] [JsonDerivedType(typeof(RemoteControlStatusOff), "off")] [JsonDerivedType(typeof(RemoteControlStatusConnecting), "connecting")] [JsonDerivedType(typeof(RemoteControlStatusActive), "active")] @@ -4766,6 +4785,7 @@ internal sealed class SessionsGetBoardEntryCountRequest public partial class RemoteControlStatus { /// The type discriminator. + [JsonRequired] [JsonPropertyName("state")] public virtual string State { get; set; } = string.Empty; } @@ -5007,7 +5027,8 @@ internal sealed class ConfigureSessionExtensionsParams [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] + IgnoreUnrecognizedTypeDiscriminators = false, + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] [JsonDerivedType(typeof(AgentRegistrySpawnResultSpawned), "spawned")] [JsonDerivedType(typeof(AgentRegistrySpawnResultSpawnError), "spawn-error")] [JsonDerivedType(typeof(AgentRegistrySpawnResultRegistryTimeout), "registry-timeout")] @@ -5015,6 +5036,7 @@ internal sealed class ConfigureSessionExtensionsParams public partial class AgentRegistrySpawnResult { /// The type discriminator. + [JsonRequired] [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } @@ -5624,7 +5646,8 @@ public sealed class SessionSetCredentialsResult [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "type", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] + IgnoreUnrecognizedTypeDiscriminators = false, + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] [JsonDerivedType(typeof(SettableAuthInfoHmac), "hmac")] [JsonDerivedType(typeof(SettableAuthInfoEnv), "env")] [JsonDerivedType(typeof(SettableAuthInfoToken), "token")] @@ -5635,6 +5658,7 @@ public sealed class SessionSetCredentialsResult public partial class SettableAuthInfo { /// The type discriminator. + [JsonRequired] [JsonPropertyName("type")] public virtual string Type { get; set; } = string.Empty; } @@ -6050,12 +6074,14 @@ public sealed class DebugCollectLogsEntry [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] + IgnoreUnrecognizedTypeDiscriminators = false, + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] [JsonDerivedType(typeof(DebugCollectLogsDestinationArchive), "archive")] [JsonDerivedType(typeof(DebugCollectLogsDestinationDirectory), "directory")] public partial class DebugCollectLogsDestination { /// The type discriminator. + [JsonRequired] [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } @@ -6386,7 +6412,8 @@ internal sealed class CanvasProviderUnregisterRequest [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "type", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] + IgnoreUnrecognizedTypeDiscriminators = false, + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] [JsonDerivedType(typeof(FactoryRunFailureFactoryLimitReached), "factory_limit_reached")] [JsonDerivedType(typeof(FactoryRunFailureFactoryResumeDeclined), "factory_resume_declined")] [JsonDerivedType(typeof(FactoryRunFailureFactoryDurableFailure), "factory_durable_failure")] @@ -6395,6 +6422,7 @@ internal sealed class CanvasProviderUnregisterRequest public partial class FactoryRunFailure { /// The type discriminator. + [JsonRequired] [JsonPropertyName("type")] public virtual string Type { get; set; } = string.Empty; } @@ -8914,13 +8942,15 @@ internal sealed class TasksStartAgentRequest [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "type", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] + IgnoreUnrecognizedTypeDiscriminators = false, + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] [JsonDerivedType(typeof(TaskInfoAgent), "agent")] [JsonDerivedType(typeof(TaskInfoClient), "client")] [JsonDerivedType(typeof(TaskInfoShell), "shell")] public partial class TaskInfo { /// The type discriminator. + [JsonRequired] [JsonPropertyName("type")] public virtual string Type { get; set; } = string.Empty; } @@ -9404,7 +9434,8 @@ public sealed class TasksUpdateResult [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] + IgnoreUnrecognizedTypeDiscriminators = false, + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] [JsonDerivedType(typeof(TaskClientUpdateProgress), "progress")] [JsonDerivedType(typeof(TaskClientUpdateCompleted), "completed")] [JsonDerivedType(typeof(TaskClientUpdateFailed), "failed")] @@ -9412,6 +9443,7 @@ public sealed class TasksUpdateResult public partial class TaskClientUpdate { /// The type discriminator. + [JsonRequired] [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } @@ -9567,13 +9599,15 @@ internal sealed class SessionTasksWaitForPendingRequest [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "type", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] + IgnoreUnrecognizedTypeDiscriminators = false, + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] [JsonDerivedType(typeof(TaskProgressAgent), "agent")] [JsonDerivedType(typeof(TaskProgressClient), "client")] [JsonDerivedType(typeof(TaskProgressShell), "shell")] public partial class TaskProgress { /// The type discriminator. + [JsonRequired] [JsonPropertyName("type")] public virtual string Type { get; set; } = string.Empty; } @@ -12482,7 +12516,8 @@ internal sealed class SessionExtensionsReloadRequest [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "type", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] + IgnoreUnrecognizedTypeDiscriminators = false, + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] [JsonDerivedType(typeof(PushAttachmentFile), "file")] [JsonDerivedType(typeof(PushAttachmentDirectory), "directory")] [JsonDerivedType(typeof(PushAttachmentSelection), "selection")] @@ -12501,6 +12536,7 @@ internal sealed class SessionExtensionsReloadRequest public partial class PushAttachment { /// The type discriminator. + [JsonRequired] [JsonPropertyName("type")] public virtual string Type { get; set; } = string.Empty; } @@ -13221,7 +13257,8 @@ public sealed class ExternalToolTextResultForLlmBinaryResultsForLlm [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "type", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] + IgnoreUnrecognizedTypeDiscriminators = false, + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] [JsonDerivedType(typeof(ExternalToolTextResultForLlmContentText), "text")] [JsonDerivedType(typeof(ExternalToolTextResultForLlmContentTerminal), "terminal")] [JsonDerivedType(typeof(ExternalToolTextResultForLlmContentShellExit), "shell_exit")] @@ -13232,6 +13269,7 @@ public sealed class ExternalToolTextResultForLlmBinaryResultsForLlm public partial class ExternalToolTextResultForLlmContent { /// The type discriminator. + [JsonRequired] [JsonPropertyName("type")] public virtual string Type { get; set; } = string.Empty; } @@ -13824,7 +13862,8 @@ internal sealed class SessionCommandsListRequestWithSession [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] + IgnoreUnrecognizedTypeDiscriminators = false, + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] [JsonDerivedType(typeof(SlashCommandInvocationResultText), "text")] [JsonDerivedType(typeof(SlashCommandInvocationResultAgentPrompt), "agent-prompt")] [JsonDerivedType(typeof(SlashCommandInvocationResultCompleted), "completed")] @@ -13836,6 +13875,7 @@ internal sealed class SessionCommandsListRequestWithSession public partial class SlashCommandInvocationResult { /// The type discriminator. + [JsonRequired] [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } @@ -14832,7 +14872,8 @@ public sealed class PermissionDecisionContext [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] + IgnoreUnrecognizedTypeDiscriminators = false, + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] [JsonDerivedType(typeof(PermissionDecisionApproveOnce), "approve-once")] [JsonDerivedType(typeof(PermissionDecisionApproveForSession), "approve-for-session")] [JsonDerivedType(typeof(PermissionDecisionApproveForLocation), "approve-for-location")] @@ -14851,6 +14892,7 @@ public sealed class PermissionDecisionContext public partial class PermissionDecision { /// The type discriminator. + [JsonRequired] [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } @@ -14876,7 +14918,8 @@ public partial class PermissionDecisionApproveOnce : PermissionDecision [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] + IgnoreUnrecognizedTypeDiscriminators = false, + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] [JsonDerivedType(typeof(PermissionDecisionApproveForSessionApprovalCommands), "commands")] [JsonDerivedType(typeof(PermissionDecisionApproveForSessionApprovalRead), "read")] [JsonDerivedType(typeof(PermissionDecisionApproveForSessionApprovalWrite), "write")] @@ -14891,6 +14934,7 @@ public partial class PermissionDecisionApproveOnce : PermissionDecision public partial class PermissionDecisionApproveForSessionApproval { /// The type discriminator. + [JsonRequired] [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } @@ -15073,7 +15117,8 @@ public partial class PermissionDecisionApproveForSession : PermissionDecision [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] + IgnoreUnrecognizedTypeDiscriminators = false, + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] [JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalCommands), "commands")] [JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalRead), "read")] [JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalWrite), "write")] @@ -15088,6 +15133,7 @@ public partial class PermissionDecisionApproveForSession : PermissionDecision public partial class PermissionDecisionApproveForLocationApproval { /// The type discriminator. + [JsonRequired] [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } @@ -15863,7 +15909,8 @@ public sealed class PermissionsLocationsAddToolApprovalResult [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] + IgnoreUnrecognizedTypeDiscriminators = false, + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] [JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsCommands), "commands")] [JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsRead), "read")] [JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsWrite), "write")] @@ -15878,6 +15925,7 @@ public sealed class PermissionsLocationsAddToolApprovalResult public partial class PermissionsLocationsAddToolApprovalDetails { /// The type discriminator. + [JsonRequired] [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } @@ -18193,12 +18241,14 @@ internal sealed class SessionUsageGetMetricsRequest [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] + IgnoreUnrecognizedTypeDiscriminators = false, + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] [JsonDerivedType(typeof(SessionLimitPredictionResultAvailable), "available")] [JsonDerivedType(typeof(SessionLimitPredictionResultUnavailable), "unavailable")] public partial class SessionLimitPredictionResult { /// The type discriminator. + [JsonRequired] [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } diff --git a/dotnet/test/Unit/CatalogueConformanceTests.cs b/dotnet/test/Unit/DiscriminatedUnionConformanceTests.cs similarity index 55% rename from dotnet/test/Unit/CatalogueConformanceTests.cs rename to dotnet/test/Unit/DiscriminatedUnionConformanceTests.cs index 6249665c9e..d4332d9371 100644 --- a/dotnet/test/Unit/CatalogueConformanceTests.cs +++ b/dotnet/test/Unit/DiscriminatedUnionConformanceTests.cs @@ -3,11 +3,11 @@ using GitHub.Copilot.Rpc; using Xunit; -#pragma warning disable GHCP001 // The catalogue search schema is experimental in CLI 1.0.83-2. +#pragma warning disable GHCP001 // The catalogue search schema is experimental. namespace GitHub.Copilot.Test.Unit; -public class CatalogueConformanceTests +public class DiscriminatedUnionConformanceTests { private const string OpaqueMcpHandle = "opaque:mcp/01-do-not-parse"; private const string OpaqueSkillHandle = "opaque:skill/02-do-not-parse"; @@ -22,6 +22,7 @@ public void CatalogSearchResult_PreservesTypedCandidatesAndOpaqueHandles() const string json = """ { "kind": "succeeded", + "rawCard": { "secret": "must-not-survive" }, "searchId": "search-01", "candidates": [ { @@ -32,7 +33,7 @@ public void CatalogSearchResult_PreservesTypedCandidatesAndOpaqueHandles() "installability": "installable", "displayName": "Example MCP", "rawCard": { "secret": "must-not-survive" }, - "source": { "kind": "url", "url": "https://catalog.example/mcp.json" }, + "source": { "kind": "url", "url": "https://catalog.example/mcp.json", "rawCard": { "secret": "must-not-survive" } }, "provenance": { "authority": "catalog.example", "observedAt": "2026-09-02T11:00:00Z", @@ -47,7 +48,7 @@ public void CatalogSearchResult_PreservesTypedCandidatesAndOpaqueHandles() "installability": "not-installable-kind", "displayName": "Example skill", "rawCard": { "secret": "must-not-survive" }, - "source": { "kind": "embedded" }, + "source": { "kind": "embedded", "rawCard": { "secret": "must-not-survive" } }, "provenance": { "authority": "catalog.example", "observedAt": "2026-09-02T11:00:00Z", @@ -74,11 +75,13 @@ public void CatalogSearchResult_PreservesTypedCandidatesAndOpaqueHandles() using var encoded = JsonDocument.Parse(JsonSerializer.Serialize( result, SerializerOptions)); + Assert.False(encoded.RootElement.TryGetProperty("rawCard", out _)); foreach (var candidate in encoded.RootElement.GetProperty("candidates").EnumerateArray()) { Assert.False(candidate.TryGetProperty("card", out _)); Assert.False(candidate.TryGetProperty("cardData", out _)); Assert.False(candidate.TryGetProperty("rawCard", out _)); + Assert.False(candidate.GetProperty("source").TryGetProperty("rawCard", out _)); } } @@ -96,4 +99,75 @@ public void CatalogSearchResult_PreservesRefusalsAndFailures() SerializerOptions)); Assert.Equal(30, network.RetryAfterSeconds); } + + [Fact] + public void ClosedUnions_RejectUnknownAndMissingDiscriminators() + { + string[] invalidPayloads = + [ + """{"kind":"future-result","rawCard":{"secret":"must-not-survive"}}""", + """{"rawCard":{"secret":"must-not-survive"}}""", + """ + { + "kind":"succeeded", + "searchId":"search-invalid", + "candidates":[{"kind":"future-candidate","rawCard":{"secret":"must-not-survive"}}], + "truncated":false, + "negotiated":{"runtimeProtocolVersion":1,"grantedCapabilities":[]} + } + """, + """ + { + "kind":"succeeded", + "searchId":"search-invalid", + "candidates":[{"rawCard":{"secret":"must-not-survive"}}], + "truncated":false, + "negotiated":{"runtimeProtocolVersion":1,"grantedCapabilities":[]} + } + """, + """ + { + "kind":"succeeded", + "searchId":"search-invalid", + "candidates":[{ + "kind":"mcp-server", + "handle":"opaque:mcp/01-do-not-parse", + "handleExpiresAt":"2026-09-02T12:00:00Z", + "mediaType":"application/mcp-server-card+json", + "installability":"installable", + "displayName":"Example MCP", + "source":{"kind":"future-source","rawCard":{"secret":"must-not-survive"}}, + "provenance":{"authority":"catalog.example","observedAt":"2026-09-02T11:00:00Z","mediaType":"application/mcp-server-card+json"} + }], + "truncated":false, + "negotiated":{"runtimeProtocolVersion":1,"grantedCapabilities":[]} + } + """, + """ + { + "kind":"succeeded", + "searchId":"search-invalid", + "candidates":[{ + "kind":"mcp-server", + "handle":"opaque:mcp/01-do-not-parse", + "handleExpiresAt":"2026-09-02T12:00:00Z", + "mediaType":"application/mcp-server-card+json", + "installability":"installable", + "displayName":"Example MCP", + "source":{"rawCard":{"secret":"must-not-survive"}}, + "provenance":{"authority":"catalog.example","observedAt":"2026-09-02T11:00:00Z","mediaType":"application/mcp-server-card+json"} + }], + "truncated":false, + "negotiated":{"runtimeProtocolVersion":1,"grantedCapabilities":[]} + } + """, + ]; + + foreach (string json in invalidPayloads) + { + Exception? exception = Record.Exception(() => + JsonSerializer.Deserialize(json, SerializerOptions)); + Assert.True(exception is JsonException, $"Invalid closed union payload was accepted: {json}"); + } + } } diff --git a/go/rpc/catalogue_conformance_test.go b/go/rpc/discriminated_union_conformance_test.go similarity index 63% rename from go/rpc/catalogue_conformance_test.go rename to go/rpc/discriminated_union_conformance_test.go index 88585d3114..d05ba81754 100644 --- a/go/rpc/catalogue_conformance_test.go +++ b/go/rpc/discriminated_union_conformance_test.go @@ -10,9 +10,10 @@ const ( opaqueSkillHandle = "opaque:skill/02-do-not-parse" ) -func TestCatalogSearchResultPreservesCandidateSemantics(t *testing.T) { +func TestClosedDiscriminatedUnionPreservesKnownNestedVariants(t *testing.T) { result, err := unmarshalCatalogSearchResult([]byte(`{ "kind":"succeeded", + "rawCard":{"secret":"must-not-survive"}, "searchId":"search-01", "candidates":[ { @@ -23,7 +24,7 @@ func TestCatalogSearchResultPreservesCandidateSemantics(t *testing.T) { "installability":"installable", "displayName":"Example MCP", "rawCard":{"secret":"must-not-survive"}, - "source":{"kind":"url","url":"https://catalog.example/mcp.json"}, + "source":{"kind":"url","url":"https://catalog.example/mcp.json","rawCard":{"secret":"must-not-survive"}}, "provenance":{ "authority":"catalog.example", "observedAt":"2026-09-02T11:00:00Z", @@ -38,7 +39,7 @@ func TestCatalogSearchResultPreservesCandidateSemantics(t *testing.T) { "installability":"not-installable-kind", "displayName":"Example skill", "rawCard":{"secret":"must-not-survive"}, - "source":{"kind":"embedded"}, + "source":{"kind":"embedded","rawCard":{"secret":"must-not-survive"}}, "provenance":{ "authority":"catalog.example", "observedAt":"2026-09-02T11:00:00Z", @@ -86,6 +87,9 @@ func TestCatalogSearchResultPreservesCandidateSemantics(t *testing.T) { if err := json.Unmarshal(encoded, &wire); err != nil { t.Fatalf("decode catalogue wire result: %v", err) } + if _, exists := wire["rawCard"]; exists { + t.Fatalf("result leaked rawCard: %s", encoded) + } for _, candidate := range wire["candidates"].([]any) { fields := candidate.(map[string]any) for _, forbidden := range []string{"card", "cardData", "rawCard"} { @@ -93,10 +97,13 @@ func TestCatalogSearchResultPreservesCandidateSemantics(t *testing.T) { t.Fatalf("candidate leaked %q: %s", forbidden, encoded) } } + if _, exists := fields["source"].(map[string]any)["rawCard"]; exists { + t.Fatalf("candidate source leaked rawCard: %s", encoded) + } } } -func TestCatalogSearchResultPreservesRefusalsAndFailures(t *testing.T) { +func TestClosedDiscriminatedUnionPreservesRefusalsAndFailures(t *testing.T) { tests := []struct { name string payload string @@ -137,19 +144,38 @@ func TestCatalogSearchResultPreservesRefusalsAndFailures(t *testing.T) { } } -func TestCatalogSearchResultRejectsUnknownCandidateKinds(t *testing.T) { - _, err := unmarshalCatalogSearchResult([]byte(`{ - "kind":"succeeded", - "searchId":"search-unknown", - "candidates":[{ - "kind":"future-kind", - "handle":"opaque:future/03-do-not-parse", - "rawCard":{"secret":"must-not-survive"} - }], - "truncated":false, - "negotiated":{"runtimeProtocolVersion":1,"grantedCapabilities":[]} - }`)) - if err == nil { - t.Fatal("unknown catalogue candidate kind with rawCard must be rejected") +func TestClosedDiscriminatedUnionRejectsUnknownAndMissingDiscriminators(t *testing.T) { + validCandidatePrefix := `{ + "handle":"opaque:mcp/01-do-not-parse", + "handleExpiresAt":"2026-09-02T12:00:00Z", + "mediaType":"application/mcp-server-card+json", + "installability":"installable", + "displayName":"Example MCP", + "provenance":{ + "authority":"catalog.example", + "observedAt":"2026-09-02T11:00:00Z", + "mediaType":"application/mcp-server-card+json" + },` + searchPrefix := `{"kind":"succeeded","searchId":"search-invalid","candidates":[` + searchSuffix := `],"truncated":false,"negotiated":{"runtimeProtocolVersion":1,"grantedCapabilities":[]}}` + tests := map[string]string{ + "unknown outer discriminator": `{"kind":"future-result","rawCard":{"secret":"must-not-survive"}}`, + "missing outer discriminator": `{"rawCard":{"secret":"must-not-survive"}}`, + "unknown candidate discriminator": searchPrefix + validCandidatePrefix + + `"kind":"future-candidate","source":{"kind":"url","url":"https://catalog.example/mcp.json"},"rawCard":{"secret":"must-not-survive"}}` + searchSuffix, + "missing candidate discriminator": searchPrefix + validCandidatePrefix + + `"source":{"kind":"url","url":"https://catalog.example/mcp.json"},"rawCard":{"secret":"must-not-survive"}}` + searchSuffix, + "unknown nested discriminator": searchPrefix + validCandidatePrefix + + `"kind":"mcp-server","source":{"kind":"future-source","rawCard":{"secret":"must-not-survive"}}}` + searchSuffix, + "missing nested discriminator": searchPrefix + validCandidatePrefix + + `"kind":"mcp-server","source":{"rawCard":{"secret":"must-not-survive"}}}` + searchSuffix, + } + + for name, payload := range tests { + t.Run(name, func(t *testing.T) { + if _, err := unmarshalCatalogSearchResult([]byte(payload)); err == nil { + t.Fatal("invalid closed union payload must be rejected") + } + }) } } diff --git a/go/rpc/zrpc.go b/go/rpc/zrpc.go index 5d0f0638db..3c9d2d46de 100644 --- a/go/rpc/zrpc.go +++ b/go/rpc/zrpc.go @@ -1465,6 +1465,16 @@ type CatalogCandidate interface { Kind() CatalogCandidateKind } +type RawCatalogCandidateData struct { + Discriminator CatalogCandidateKind + Raw json.RawMessage +} + +func (RawCatalogCandidateData) catalogCandidate() {} +func (r RawCatalogCandidateData) Kind() CatalogCandidateKind { + return r.Discriminator +} + // An inert AI skill catalog result. AI skills are discovery-only and cannot be represented // as installable through this surface. // Experimental: CatalogAiSkillCandidate is part of an experimental API and may change or be diff --git a/go/rpc/zrpc_encoding.go b/go/rpc/zrpc_encoding.go index e08a9ffe9d..4133ff2e7e 100644 --- a/go/rpc/zrpc_encoding.go +++ b/go/rpc/zrpc_encoding.go @@ -69,9 +69,8 @@ func unmarshalAuthInfo(data []byte) (AuthInfo, error) { return nil, err } return &d, nil - default: - return &RawAuthInfoData{Discriminator: raw.Type, Raw: data}, nil } + return nil, errors.New("data did not match any union variant for AuthInfo") } func (r RawAuthInfoData) MarshalJSON() ([]byte, error) { @@ -272,9 +271,8 @@ func unmarshalAgentRegistrySpawnResult(data []byte) (AgentRegistrySpawnResult, e return nil, err } return &d, nil - default: - return &RawAgentRegistrySpawnResultData{Discriminator: raw.Kind, Raw: data}, nil } + return nil, errors.New("data did not match any union variant for AgentRegistrySpawnResult") } func (r RawAgentRegistrySpawnResultData) MarshalJSON() ([]byte, error) { @@ -435,9 +433,8 @@ func unmarshalAttachment(data []byte) (Attachment, error) { return nil, err } return &d, nil - default: - return &RawAttachmentData{Discriminator: raw.Type, Raw: data}, nil } + return nil, errors.New("data did not match any union variant for Attachment") } func (r RawAttachmentData) MarshalJSON() ([]byte, error) { @@ -700,6 +697,17 @@ func unmarshalCatalogCandidate(data []byte) (CatalogCandidate, error) { return nil, errors.New("data did not match any union variant for CatalogCandidate") } +func (r RawCatalogCandidateData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil + } + return json.Marshal(struct { + Kind CatalogCandidateKind `json:"kind"` + }{ + Kind: r.Discriminator, + }) +} + func unmarshalCatalogCandidateSource(data []byte) (CatalogCandidateSource, error) { if string(data) == "null" { return nil, nil @@ -725,9 +733,8 @@ func unmarshalCatalogCandidateSource(data []byte) (CatalogCandidateSource, error return nil, err } return &d, nil - default: - return &RawCatalogCandidateSourceData{Discriminator: raw.Kind, Raw: data}, nil } + return nil, errors.New("data did not match any union variant for CatalogCandidateSource") } func (r RawCatalogCandidateSourceData) MarshalJSON() ([]byte, error) { @@ -932,9 +939,8 @@ func unmarshalCatalogSearchResult(data []byte) (CatalogSearchResult, error) { return nil, err } return &d, nil - default: - return &RawCatalogSearchResultData{Discriminator: raw.Kind, Raw: data}, nil } + return nil, errors.New("data did not match any union variant for CatalogSearchResult") } func (r RawCatalogSearchResultData) MarshalJSON() ([]byte, error) { @@ -1195,9 +1201,8 @@ func unmarshalDebugCollectLogsDestination(data []byte) (DebugCollectLogsDestinat return nil, err } return &d, nil - default: - return &RawDebugCollectLogsDestinationData{Discriminator: raw.Kind, Raw: data}, nil } + return nil, errors.New("data did not match any union variant for DebugCollectLogsDestination") } func (r RawDebugCollectLogsDestinationData) MarshalJSON() ([]byte, error) { @@ -1342,9 +1347,8 @@ func unmarshalExternalToolTextResultForLlmContent(data []byte) (ExternalToolText return nil, err } return &d, nil - default: - return &RawExternalToolTextResultForLlmContentData{Discriminator: raw.Type, Raw: data}, nil } + return nil, errors.New("data did not match any union variant for ExternalToolTextResultForLlmContent") } func (r RawExternalToolTextResultForLlmContentData) MarshalJSON() ([]byte, error) { @@ -1606,9 +1610,8 @@ func unmarshalFactoryRunFailure(data []byte) (FactoryRunFailure, error) { return nil, err } return &d, nil - default: - return &RawFactoryRunFailureData{Discriminator: raw.Type, Raw: data}, nil } + return nil, errors.New("data did not match any union variant for FactoryRunFailure") } func (r RawFactoryRunFailureData) MarshalJSON() ([]byte, error) { @@ -2336,9 +2339,8 @@ func unmarshalMCPPlanTransportChoice(data []byte) (MCPPlanTransportChoice, error return nil, err } return &d, nil - default: - return &RawMCPPlanTransportChoiceData{Discriminator: raw.Transport, Raw: data}, nil } + return nil, errors.New("data did not match any union variant for MCPPlanTransportChoice") } func (r RawMCPPlanTransportChoiceData) MarshalJSON() ([]byte, error) { @@ -2377,9 +2379,8 @@ func unmarshalMCPPlanRequiredValue(data []byte) (MCPPlanRequiredValue, error) { return nil, err } return &d, nil - default: - return &RawMCPPlanRequiredValueData{Discriminator: raw.Kind, Raw: data}, nil } + return nil, errors.New("data did not match any union variant for MCPPlanRequiredValue") } func (r RawMCPPlanRequiredValueData) MarshalJSON() ([]byte, error) { @@ -2745,9 +2746,8 @@ func unmarshalMCPPlanInstallSource(data []byte) (MCPPlanInstallSource, error) { return nil, err } return &d, nil - default: - return &RawMCPPlanInstallSourceData{Discriminator: raw.Kind, Raw: data}, nil } + return nil, errors.New("data did not match any union variant for MCPPlanInstallSource") } func (r RawMCPPlanInstallSourceData) MarshalJSON() ([]byte, error) { @@ -2797,9 +2797,8 @@ func unmarshalMCPServerCardReference(data []byte) (MCPServerCardReference, error return nil, err } return &d, nil - default: - return &RawMCPServerCardReferenceData{Discriminator: raw.Kind, Raw: data}, nil } + return nil, errors.New("data did not match any union variant for MCPServerCardReference") } func (r RawMCPServerCardReferenceData) MarshalJSON() ([]byte, error) { @@ -2977,9 +2976,8 @@ func unmarshalMCPPlanInstallResult(data []byte) (MCPPlanInstallResult, error) { return nil, err } return &d, nil - default: - return &RawMCPPlanInstallResultData{Discriminator: raw.Kind, Raw: data}, nil } + return nil, errors.New("data did not match any union variant for MCPPlanInstallResult") } func (r RawMCPPlanInstallResultData) MarshalJSON() ([]byte, error) { @@ -3405,9 +3403,8 @@ func unmarshalPermissionDecision(data []byte) (PermissionDecision, error) { return nil, err } return &d, nil - default: - return &RawPermissionDecisionData{Discriminator: raw.Kind, Raw: data}, nil } + return nil, errors.New("data did not match any union variant for PermissionDecision") } func (r RawPermissionDecisionData) MarshalJSON() ([]byte, error) { @@ -3505,9 +3502,8 @@ func unmarshalUserToolSessionApproval(data []byte) (UserToolSessionApproval, err return nil, err } return &d, nil - default: - return &RawUserToolSessionApprovalData{Discriminator: raw.Kind, Raw: data}, nil } + return nil, errors.New("data did not match any union variant for UserToolSessionApproval") } func (r RawUserToolSessionApprovalData) MarshalJSON() ([]byte, error) { @@ -3770,9 +3766,8 @@ func unmarshalPermissionDecisionApproveForLocationApproval(data []byte) (Permiss return nil, err } return &d, nil - default: - return &RawPermissionDecisionApproveForLocationApprovalData{Discriminator: raw.Kind, Raw: data}, nil } + return nil, errors.New("data did not match any union variant for PermissionDecisionApproveForLocationApproval") } func (r RawPermissionDecisionApproveForLocationApprovalData) MarshalJSON() ([]byte, error) { @@ -4017,9 +4012,8 @@ func unmarshalPermissionDecisionApproveForSessionApproval(data []byte) (Permissi return nil, err } return &d, nil - default: - return &RawPermissionDecisionApproveForSessionApprovalData{Discriminator: raw.Kind, Raw: data}, nil } + return nil, errors.New("data did not match any union variant for PermissionDecisionApproveForSessionApproval") } func (r RawPermissionDecisionApproveForSessionApprovalData) MarshalJSON() ([]byte, error) { @@ -4396,9 +4390,8 @@ func unmarshalPermissionsLocationsAddToolApprovalDetails(data []byte) (Permissio return nil, err } return &d, nil - default: - return &RawPermissionsLocationsAddToolApprovalDetailsData{Discriminator: raw.Kind, Raw: data}, nil } + return nil, errors.New("data did not match any union variant for PermissionsLocationsAddToolApprovalDetails") } func (r RawPermissionsLocationsAddToolApprovalDetailsData) MarshalJSON() ([]byte, error) { @@ -4656,9 +4649,8 @@ func unmarshalPushAttachment(data []byte) (PushAttachment, error) { return nil, err } return &d, nil - default: - return &RawPushAttachmentData{Discriminator: raw.Type, Raw: data}, nil } + return nil, errors.New("data did not match any union variant for PushAttachment") } func (r RawPushAttachmentData) MarshalJSON() ([]byte, error) { @@ -4917,9 +4909,8 @@ func unmarshalRemoteControlStatus(data []byte) (RemoteControlStatus, error) { return nil, err } return &d, nil - default: - return &RawRemoteControlStatusData{Discriminator: raw.State, Raw: data}, nil } + return nil, errors.New("data did not match any union variant for RemoteControlStatus") } func (r RawRemoteControlStatusData) MarshalJSON() ([]byte, error) { @@ -5249,9 +5240,8 @@ func unmarshalSessionLimitPredictionResult(data []byte) (SessionLimitPredictionR return nil, err } return &d, nil - default: - return &RawSessionLimitPredictionResultData{Discriminator: raw.Kind, Raw: data}, nil } + return nil, errors.New("data did not match any union variant for SessionLimitPredictionResult") } func (r RawSessionLimitPredictionResultData) MarshalJSON() ([]byte, error) { @@ -5577,9 +5567,8 @@ func unmarshalSessionOpenParams(data []byte) (SessionOpenParams, error) { return nil, err } return &d, nil - default: - return &RawSessionOpenParamsData{Discriminator: raw.Kind, Raw: data}, nil } + return nil, errors.New("data did not match any union variant for SessionOpenParams") } func (r RawSessionOpenParamsData) MarshalJSON() ([]byte, error) { @@ -5725,9 +5714,8 @@ func unmarshalSettableAuthInfo(data []byte) (SettableAuthInfo, error) { return nil, err } return &d, nil - default: - return &RawSettableAuthInfoData{Discriminator: raw.Type, Raw: data}, nil } + return nil, errors.New("data did not match any union variant for SettableAuthInfo") } func (r RawSettableAuthInfoData) MarshalJSON() ([]byte, error) { @@ -5831,9 +5819,8 @@ func unmarshalSlashCommandInvocationResult(data []byte) (SlashCommandInvocationR return nil, err } return &d, nil - default: - return &RawSlashCommandInvocationResultData{Discriminator: raw.Kind, Raw: data}, nil } + return nil, errors.New("data did not match any union variant for SlashCommandInvocationResult") } func (r RawSlashCommandInvocationResultData) MarshalJSON() ([]byte, error) { @@ -5972,9 +5959,8 @@ func unmarshalTaskClientUpdate(data []byte) (TaskClientUpdate, error) { return nil, err } return &d, nil - default: - return &RawTaskClientUpdateData{Discriminator: raw.Kind, Raw: data}, nil } + return nil, errors.New("data did not match any union variant for TaskClientUpdate") } func (r RawTaskClientUpdateData) MarshalJSON() ([]byte, error) { @@ -6063,9 +6049,8 @@ func unmarshalTaskInfo(data []byte) (TaskInfo, error) { return nil, err } return &d, nil - default: - return &RawTaskInfoData{Discriminator: raw.Type, Raw: data}, nil } + return nil, errors.New("data did not match any union variant for TaskInfo") } func (r RawTaskInfoData) MarshalJSON() ([]byte, error) { diff --git a/java/scripts/codegen/java.ts b/java/scripts/codegen/java.ts index 96bdb72c18..9f4b1a2379 100644 --- a/java/scripts/codegen/java.ts +++ b/java/scripts/codegen/java.ts @@ -11,6 +11,7 @@ import fs from "fs/promises"; import type { JSONSchema7 } from "json-schema"; import path from "path"; import { fileURLToPath } from "url"; +import { analyseDiscriminatedUnionVariants } from "../../../scripts/codegen/schema-unions.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -333,34 +334,24 @@ interface DiscriminatorInfo { * A discriminator is a property with a `const` value that uniquely identifies each variant. */ function findDiscriminator(variants: JSONSchema7[]): DiscriminatorInfo | null { - if (variants.length === 0) return null; - const firstVariant = variants[0]; - if (!firstVariant.properties) return null; - - for (const [propName, propSchema] of Object.entries(firstVariant.properties).sort(([a], [b]) => a.localeCompare(b))) { - if (typeof propSchema !== "object") continue; - const schema = propSchema as JSONSchema7; - if (schema.const === undefined) continue; - - const mapping = new Map(); - let isValidDiscriminator = true; - - for (const variant of variants) { - if (!variant.properties) { isValidDiscriminator = false; break; } - const variantProp = variant.properties[propName]; - if (typeof variantProp !== "object") { isValidDiscriminator = false; break; } - const variantSchema = variantProp as JSONSchema7; - if (variantSchema.const === undefined) { isValidDiscriminator = false; break; } - const key = String(variantSchema.const); - if (mapping.has(key)) { isValidDiscriminator = false; break; } - mapping.set(key, { value: variantSchema.const, schema: variant }); - } - - if (isValidDiscriminator && mapping.size === variants.length) { - return { property: propName, mapping }; - } + const analysis = analyseDiscriminatedUnionVariants(variants); + if ( + !analysis || + analysis.variants.some((variant) => variant.discriminatorValues.length !== 1) || + analysis.mapping.some((entry) => entry.variants.length !== 1) + ) { + return null; } - return null; + + return { + property: analysis.property, + mapping: new Map( + analysis.mapping.map((entry) => [ + String(entry.value), + { value: entry.value, schema: entry.variants[0].schema }, + ]) + ), + }; } /** @@ -378,9 +369,13 @@ function resolveUnionVariants(variants: JSONSchema7[]): JSONSchema7[] { .filter((v) => v.type !== "null"); } -function collectPromotedNestedUnionTypes(root: unknown): void { +export function collectNestedDiscriminatedUnionTypeNames( + root: unknown, + definitions: Record +): Set { + const promotedTypes = new Set(); const visitedDefinitions = new Set(); - const visit = (node: unknown): void => { + const visit = (node: unknown, isRoot = false): void => { if (Array.isArray(node)) { for (const item of node) visit(item); return; @@ -392,15 +387,22 @@ function collectPromotedNestedUnionTypes(root: unknown): void { const name = schema.$ref.slice("#/definitions/".length); if (visitedDefinitions.has(name)) return; visitedDefinitions.add(name); - const definition = currentDefinitions[name]; + const definition = definitions[name]; if (definition) { const union = definition.anyOf ?? definition.oneOf; if ( union && Array.isArray(union) - && findDiscriminator(resolveUnionVariants(union as JSONSchema7[])) + && analyseDiscriminatedUnionVariants( + union as JSONSchema7[], + (variant) => { + if (!variant.$ref?.startsWith("#/definitions/")) return variant; + return definitions[variant.$ref.slice("#/definitions/".length)]; + } + ) + && !isRoot ) { - promotedNestedUnionTypes.add(name); + promotedTypes.add(name); } visit(definition); } @@ -411,7 +413,14 @@ function collectPromotedNestedUnionTypes(root: unknown): void { visit(value); } }; - visit(root); + visit(root, true); + return promotedTypes; +} + +function collectPromotedNestedUnionTypes(root: unknown): void { + for (const name of collectNestedDiscriminatedUnionTypeNames(root, currentDefinitions)) { + promotedNestedUnionTypes.add(name); + } } /** @@ -1456,8 +1465,14 @@ async function generateRpcTypes(schemaPath: string): Promise { for (const section of [schema.server, schema.session, schema.clientSession, schema.clientGlobal]) { if (!section) continue; for (const [, method] of collectRpcMethods(section)) { - if (method.rpcMethod === "catalog.search") { - collectPromotedNestedUnionTypes(method); + const result = resolveRef(method.result ?? undefined); + const union = result?.anyOf ?? result?.oneOf; + if ( + union + && Array.isArray(union) + && findDiscriminator(resolveUnionVariants(union as JSONSchema7[])) + ) { + collectPromotedNestedUnionTypes(method.result); } } } @@ -2461,7 +2476,9 @@ async function main(): Promise { console.log("\n✅ Java code generation complete!"); } -main().catch((err) => { - console.error("❌ Code generation failed:", err); - process.exit(1); -}); +if (process.argv[1] && path.resolve(process.argv[1]) === __filename) { + main().catch((err) => { + console.error("❌ Code generation failed:", err); + process.exit(1); + }); +} 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 7274ce2b7f..1979dbeee3 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/McpPlanEnumValueType.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanEnumValueType.java new file mode 100644 index 0000000000..2bed0a6521 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanEnumValueType.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 javax.annotation.processing.Generated; + +/** + * Discriminator for an enumerated required value + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum McpPlanEnumValueType { + /** The {@code enum} variant. */ + ENUM("enum"); + + private final String value; + McpPlanEnumValueType(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static McpPlanEnumValueType fromValue(String value) { + for (McpPlanEnumValueType v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown McpPlanEnumValueType value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanPackageTransport.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanPackageTransport.java new file mode 100644 index 0000000000..cae9f57b89 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanPackageTransport.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 javax.annotation.processing.Generated; + +/** + * Transport exposed by a locally launched package + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum McpPlanPackageTransport { + /** The {@code stdio} variant. */ + STDIO("stdio"); + + private final String value; + McpPlanPackageTransport(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static McpPlanPackageTransport fromValue(String value) { + for (McpPlanPackageTransport v : values()) { + if (v.value.equals(value)) return v; + } + 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/McpPlanRequiredValue.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanRequiredValue.java new file mode 100644 index 0000000000..52acb82611 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanRequiredValue.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; + +/** + * 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. + * + * @since 1.0.0 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = McpPlanRequiredValueScalar.class, name = "scalar"), + @JsonSubTypes.Type(value = McpPlanRequiredValueEnum.class, name = "enum") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public abstract class McpPlanRequiredValue { + + /** + * 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/McpPlanRequiredValueEnum.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanRequiredValueEnum.java new file mode 100644 index 0000000000..95eebb07b8 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanRequiredValueEnum.java @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package 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; + +/** + * One enumerated non-secret value a transport choice needs before it can be applied. The permitted values are structurally required. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class McpPlanRequiredValueEnum extends McpPlanRequiredValue { + + @JsonProperty("kind") + private final String kind = "enum"; + + @Override + public String getKind() { return kind; } + + /** Key the value is supplied under. Inert untrusted data. */ + @JsonProperty("key") + private String key; + + /** Where the value is applied when the server is launched. */ + @JsonProperty("category") + private McpPlanValueCategory category; + + /** Discriminator: the value must be one of `enumValues`. */ + @JsonProperty("valueType") + private McpPlanEnumValueType valueType; + + /** Whether the value must be present for the plan to be applicable. */ + @JsonProperty("required") + private Boolean required; + + /** 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. */ + @JsonProperty("defaultValue") + private String defaultValue; + + /** Human-readable label from the card. Inert untrusted text. */ + @JsonProperty("title") + private String title; + + /** Human-readable explanation from the card. Inert untrusted text. */ + @JsonProperty("description") + private String description; + + /** Non-empty permitted value set. Inert untrusted data. */ + @JsonProperty("enumValues") + private List enumValues; + + /** Whether the value may be supplied more than once. */ + @JsonProperty("isRepeated") + private Boolean isRepeated; + + public String getKey() { return key; } + public void setKey(String key) { this.key = key; } + + public McpPlanValueCategory getCategory() { return category; } + public void setCategory(McpPlanValueCategory category) { this.category = category; } + + public McpPlanEnumValueType getValueType() { return valueType; } + public void setValueType(McpPlanEnumValueType valueType) { this.valueType = valueType; } + + public Boolean getRequired() { return required; } + public void setRequired(Boolean required) { this.required = required; } + + public String getDefaultValue() { return defaultValue; } + public void setDefaultValue(String defaultValue) { this.defaultValue = defaultValue; } + + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public List getEnumValues() { return enumValues; } + public void setEnumValues(List enumValues) { this.enumValues = enumValues; } + + public Boolean getIsRepeated() { return isRepeated; } + public void setIsRepeated(Boolean isRepeated) { this.isRepeated = isRepeated; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanRequiredValueScalar.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanRequiredValueScalar.java new file mode 100644 index 0000000000..fba8ff2504 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanRequiredValueScalar.java @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package 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 non-secret scalar value a transport choice needs before it can be applied. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class McpPlanRequiredValueScalar extends McpPlanRequiredValue { + + @JsonProperty("kind") + private final String kind = "scalar"; + + @Override + public String getKind() { return kind; } + + /** Key the value is supplied under. Inert untrusted data. */ + @JsonProperty("key") + private String key; + + /** Where the value is applied when the server is launched. */ + @JsonProperty("category") + private McpPlanValueCategory category; + + /** Scalar type the value must conform to. */ + @JsonProperty("valueType") + private McpPlanScalarValueType valueType; + + /** Whether the value must be present for the plan to be applicable. */ + @JsonProperty("required") + private Boolean required; + + /** 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. */ + @JsonProperty("defaultValue") + private String defaultValue; + + /** Human-readable label from the card. Inert untrusted text. */ + @JsonProperty("title") + private String title; + + /** Human-readable explanation from the card. Inert untrusted text. */ + @JsonProperty("description") + private String description; + + /** Whether the value may be supplied more than once. */ + @JsonProperty("isRepeated") + private Boolean isRepeated; + + public String getKey() { return key; } + public void setKey(String key) { this.key = key; } + + public McpPlanValueCategory getCategory() { return category; } + public void setCategory(McpPlanValueCategory category) { this.category = category; } + + public McpPlanScalarValueType getValueType() { return valueType; } + public void setValueType(McpPlanScalarValueType valueType) { this.valueType = valueType; } + + public Boolean getRequired() { return required; } + public void setRequired(Boolean required) { this.required = required; } + + public String getDefaultValue() { return defaultValue; } + public void setDefaultValue(String defaultValue) { this.defaultValue = defaultValue; } + + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public Boolean getIsRepeated() { return isRepeated; } + public void setIsRepeated(Boolean isRepeated) { this.isRepeated = isRepeated; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanScalarValueType.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanScalarValueType.java new file mode 100644 index 0000000000..ca7620eff3 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanScalarValueType.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; + +/** + * Scalar type a required value must conform to + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum McpPlanScalarValueType { + /** The {@code string} variant. */ + STRING("string"), + /** The {@code number} variant. */ + NUMBER("number"), + /** The {@code boolean} variant. */ + BOOLEAN("boolean"), + /** The {@code path} variant. */ + PATH("path"); + + private final String value; + McpPlanScalarValueType(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static McpPlanScalarValueType fromValue(String value) { + for (McpPlanScalarValueType v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown McpPlanScalarValueType 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..23cd22ab74 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanTransportChoice.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; + +/** + * 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, 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 abstract class McpPlanTransportChoice { + + /** + * Returns the discriminator value for this variant. + * + * @return the installMethod discriminator + */ + public abstract String getInstallMethod(); +} 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..70cfa6d3ca --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanTransportChoicePackage.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; + +/** + * 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) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class McpPlanTransportChoicePackage extends McpPlanTransportChoice { + + @JsonProperty("installMethod") + private final String installMethod = "package"; + + @Override + public String getInstallMethod() { return installMethod; } + + /** Stable identifier for this choice within the plan, used to select it when the plan is applied. */ + @JsonProperty("choiceId") + private String choiceId; + + /** Local process transport this package choice would use. */ + @JsonProperty("transport") + private McpPlanPackageTransport transport; + + /** Packaging ecosystem, for example `oci` or `npm`. */ + @JsonProperty("packageType") + private String packageType; + + /** Package identifier. Inert untrusted data. */ + @JsonProperty("packageIdentifier") + private String packageIdentifier; + + /** Typed values this choice requires, excluding secrets. */ + @JsonProperty("requiredValues") + private List requiredValues; + + /** Secrets this choice requires, referenced by placeholder only. */ + @JsonProperty("secretPlaceholders") + private List secretPlaceholders; + + public String getChoiceId() { return choiceId; } + public void setChoiceId(String choiceId) { this.choiceId = choiceId; } + + public McpPlanPackageTransport getTransport() { return transport; } + public void setTransport(McpPlanPackageTransport transport) { this.transport = transport; } + + public String getPackageType() { return packageType; } + public void setPackageType(String packageType) { this.packageType = packageType; } + + public String getPackageIdentifier() { return packageIdentifier; } + public void setPackageIdentifier(String packageIdentifier) { this.packageIdentifier = packageIdentifier; } + + public List getRequiredValues() { return requiredValues; } + public void setRequiredValues(List requiredValues) { this.requiredValues = requiredValues; } + + public List getSecretPlaceholders() { return secretPlaceholders; } + public void setSecretPlaceholders(List secretPlaceholders) { this.secretPlaceholders = 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..8bf81185b7 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanTransportChoiceRemote.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; + +/** + * 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) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class McpPlanTransportChoiceRemote extends McpPlanTransportChoice { + + @JsonProperty("installMethod") + private final String installMethod = "remote"; + + @Override + public String getInstallMethod() { return installMethod; } + + /** Stable identifier for this choice within the plan, used to select it when the plan is applied. */ + @JsonProperty("choiceId") + private String choiceId; + + /** Endpoint transport this remote choice would use. */ + @JsonProperty("transport") + private McpPlanRemoteTransport transport; + + /** Endpoint URL. Inert untrusted data. */ + @JsonProperty("endpoint") + private String endpoint; + + /** Typed values this choice requires, excluding secrets. */ + @JsonProperty("requiredValues") + private List requiredValues; + + /** Secrets this choice requires, referenced by placeholder only. */ + @JsonProperty("secretPlaceholders") + private List secretPlaceholders; + + public String getChoiceId() { return choiceId; } + public void setChoiceId(String choiceId) { this.choiceId = choiceId; } + + public McpPlanRemoteTransport getTransport() { return transport; } + public void setTransport(McpPlanRemoteTransport transport) { this.transport = transport; } + + public String getEndpoint() { return endpoint; } + public void setEndpoint(String endpoint) { this.endpoint = endpoint; } + + public List getRequiredValues() { return requiredValues; } + public void setRequiredValues(List requiredValues) { this.requiredValues = requiredValues; } + + public List getSecretPlaceholders() { return secretPlaceholders; } + public void setSecretPlaceholders(List secretPlaceholders) { this.secretPlaceholders = secretPlaceholders; } +} 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/test/java/com/github/copilot/generated/CatalogConformanceTest.java b/java/sdk/src/test/java/com/github/copilot/generated/CatalogConformanceTest.java deleted file mode 100644 index 5e9e4fadbf..0000000000 --- a/java/sdk/src/test/java/com/github/copilot/generated/CatalogConformanceTest.java +++ /dev/null @@ -1,107 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -package com.github.copilot.generated; - -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 org.junit.jupiter.api.Test; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.copilot.generated.rpc.CatalogAiSkillCandidate; -import com.github.copilot.generated.rpc.CatalogAuthenticationRequiredError; -import com.github.copilot.generated.rpc.CatalogCandidateSourceEmbedded; -import com.github.copilot.generated.rpc.CatalogCandidateSourceUrl; -import com.github.copilot.generated.rpc.CatalogMcpServerCandidate; -import com.github.copilot.generated.rpc.CatalogNetworkFailureError; -import com.github.copilot.generated.rpc.CatalogSearchResult; -import com.github.copilot.generated.rpc.CatalogSearchSucceeded; - -class CatalogConformanceTest { - - private static final ObjectMapper MAPPER = new ObjectMapper(); - private static final String OPAQUE_MCP_HANDLE = "opaque:mcp/01-do-not-parse"; - private static final String OPAQUE_SKILL_HANDLE = "opaque:skill/02-do-not-parse"; - - @Test - void preservesTypedCandidatesAndOpaqueHandles() throws Exception { - var result = MAPPER.readValue(""" - { - "kind": "succeeded", - "searchId": "search-01", - "candidates": [ - { - "kind": "mcp-server", - "handle": "opaque:mcp/01-do-not-parse", - "handleExpiresAt": "2026-09-02T12:00:00Z", - "mediaType": "application/mcp-server-card+json", - "installability": "installable", - "displayName": "Example MCP", - "rawCard": { "secret": "must-not-survive" }, - "source": { "kind": "url", "url": "https://catalog.example/mcp.json" }, - "provenance": { - "authority": "catalog.example", - "observedAt": "2026-09-02T11:00:00Z", - "mediaType": "application/mcp-server-card+json" - } - }, - { - "kind": "ai-skill", - "handle": "opaque:skill/02-do-not-parse", - "handleExpiresAt": "2026-09-02T12:00:00Z", - "mediaType": "application/ai-skill", - "installability": "not-installable-kind", - "displayName": "Example skill", - "rawCard": { "secret": "must-not-survive" }, - "source": { "kind": "embedded" }, - "provenance": { - "authority": "catalog.example", - "observedAt": "2026-09-02T11:00:00Z", - "mediaType": "application/ai-skill" - } - } - ], - "truncated": false, - "negotiated": { - "runtimeProtocolVersion": 1, - "grantedCapabilities": ["mcp-server-card", "ai-skill-discovery"] - } - } - """, CatalogSearchResult.class); - - var success = assertInstanceOf(CatalogSearchSucceeded.class, result); - var mcp = assertInstanceOf(CatalogMcpServerCandidate.class, success.getCandidates().get(0)); - var skill = assertInstanceOf(CatalogAiSkillCandidate.class, success.getCandidates().get(1)); - assertEquals(OPAQUE_MCP_HANDLE, mcp.getHandle()); - assertEquals(OPAQUE_SKILL_HANDLE, skill.getHandle()); - assertInstanceOf(CatalogCandidateSourceUrl.class, mcp.getSource()); - assertInstanceOf(CatalogCandidateSourceEmbedded.class, skill.getSource()); - - JsonNode encoded = MAPPER.valueToTree(success); - for (JsonNode candidate : encoded.get("candidates")) { - assertFalse(candidate.has("card")); - assertFalse(candidate.has("cardData")); - assertFalse(candidate.has("rawCard")); - } - } - - @Test - void preservesRefusalsAndFailures() throws Exception { - var authentication = MAPPER.readValue(""" - {"kind":"authentication-required","reason":"no-credential","message":"Sign in is required."} - """, CatalogSearchResult.class); - assertInstanceOf(CatalogAuthenticationRequiredError.class, authentication); - - var network = assertInstanceOf(CatalogNetworkFailureError.class, - MAPPER.readValue( - """ - {"kind":"network-failure","reason":"timeout","retryAfterSeconds":30,"message":"The catalogue timed out."} - """, - CatalogSearchResult.class)); - assertEquals(30L, network.getRetryAfterSeconds()); - } -} diff --git a/java/sdk/src/test/java/com/github/copilot/generated/DiscriminatedUnionConformanceTest.java b/java/sdk/src/test/java/com/github/copilot/generated/DiscriminatedUnionConformanceTest.java new file mode 100644 index 0000000000..a7cfe90dda --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/generated/DiscriminatedUnionConformanceTest.java @@ -0,0 +1,179 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.generated; + +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.assertThrows; + +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.CatalogAiSkillCandidate; +import com.github.copilot.generated.rpc.CatalogAuthenticationRequiredError; +import com.github.copilot.generated.rpc.CatalogCandidateSourceEmbedded; +import com.github.copilot.generated.rpc.CatalogCandidateSourceUrl; +import com.github.copilot.generated.rpc.CatalogMcpServerCandidate; +import com.github.copilot.generated.rpc.CatalogNetworkFailureError; +import com.github.copilot.generated.rpc.CatalogSearchResult; +import com.github.copilot.generated.rpc.CatalogSearchSucceeded; + +class DiscriminatedUnionConformanceTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final String OPAQUE_MCP_HANDLE = "opaque:mcp/01-do-not-parse"; + private static final String OPAQUE_SKILL_HANDLE = "opaque:skill/02-do-not-parse"; + + @Test + void preservesTypedCandidatesAndOpaqueHandles() throws Exception { + var result = MAPPER.readValue( + """ + { + "kind": "succeeded", + "rawCard": { "secret": "must-not-survive" }, + "searchId": "search-01", + "candidates": [ + { + "kind": "mcp-server", + "handle": "opaque:mcp/01-do-not-parse", + "handleExpiresAt": "2026-09-02T12:00:00Z", + "mediaType": "application/mcp-server-card+json", + "installability": "installable", + "displayName": "Example MCP", + "rawCard": { "secret": "must-not-survive" }, + "source": { "kind": "url", "url": "https://catalog.example/mcp.json", "rawCard": { "secret": "must-not-survive" } }, + "provenance": { + "authority": "catalog.example", + "observedAt": "2026-09-02T11:00:00Z", + "mediaType": "application/mcp-server-card+json" + } + }, + { + "kind": "ai-skill", + "handle": "opaque:skill/02-do-not-parse", + "handleExpiresAt": "2026-09-02T12:00:00Z", + "mediaType": "application/ai-skill", + "installability": "not-installable-kind", + "displayName": "Example skill", + "rawCard": { "secret": "must-not-survive" }, + "source": { "kind": "embedded", "rawCard": { "secret": "must-not-survive" } }, + "provenance": { + "authority": "catalog.example", + "observedAt": "2026-09-02T11:00:00Z", + "mediaType": "application/ai-skill" + } + } + ], + "truncated": false, + "negotiated": { + "runtimeProtocolVersion": 1, + "grantedCapabilities": ["mcp-server-card", "ai-skill-discovery"] + } + } + """, + CatalogSearchResult.class); + + var success = assertInstanceOf(CatalogSearchSucceeded.class, result); + var mcp = assertInstanceOf(CatalogMcpServerCandidate.class, success.getCandidates().get(0)); + var skill = assertInstanceOf(CatalogAiSkillCandidate.class, success.getCandidates().get(1)); + assertEquals(OPAQUE_MCP_HANDLE, mcp.getHandle()); + assertEquals(OPAQUE_SKILL_HANDLE, skill.getHandle()); + assertInstanceOf(CatalogCandidateSourceUrl.class, mcp.getSource()); + assertInstanceOf(CatalogCandidateSourceEmbedded.class, skill.getSource()); + + JsonNode encoded = MAPPER.valueToTree(success); + assertFalse(encoded.has("rawCard")); + for (JsonNode candidate : encoded.get("candidates")) { + assertFalse(candidate.has("card")); + assertFalse(candidate.has("cardData")); + assertFalse(candidate.has("rawCard")); + assertFalse(candidate.get("source").has("rawCard")); + } + } + + @Test + void preservesRefusalsAndFailures() throws Exception { + var authentication = MAPPER.readValue(""" + {"kind":"authentication-required","reason":"no-credential","message":"Sign in is required."} + """, CatalogSearchResult.class); + assertInstanceOf(CatalogAuthenticationRequiredError.class, authentication); + + var network = assertInstanceOf(CatalogNetworkFailureError.class, + MAPPER.readValue( + """ + {"kind":"network-failure","reason":"timeout","retryAfterSeconds":30,"message":"The catalogue timed out."} + """, + CatalogSearchResult.class)); + assertEquals(30L, network.getRetryAfterSeconds()); + } + + @Test + void rejectsUnknownAndMissingDiscriminators() { + var invalidPayloads = new String[]{""" + {"kind":"future-result","rawCard":{"secret":"must-not-survive"}} + """, """ + {"rawCard":{"secret":"must-not-survive"}} + """, """ + { + "kind":"succeeded", + "searchId":"search-invalid", + "candidates":[{"kind":"future-candidate","rawCard":{"secret":"must-not-survive"}}], + "truncated":false, + "negotiated":{"runtimeProtocolVersion":1,"grantedCapabilities":[]} + } + """, """ + { + "kind":"succeeded", + "searchId":"search-invalid", + "candidates":[{"rawCard":{"secret":"must-not-survive"}}], + "truncated":false, + "negotiated":{"runtimeProtocolVersion":1,"grantedCapabilities":[]} + } + """, + """ + { + "kind":"succeeded", + "searchId":"search-invalid", + "candidates":[{ + "kind":"mcp-server", + "handle":"opaque:mcp/01-do-not-parse", + "handleExpiresAt":"2026-09-02T12:00:00Z", + "mediaType":"application/mcp-server-card+json", + "installability":"installable", + "displayName":"Example MCP", + "source":{"kind":"future-source","rawCard":{"secret":"must-not-survive"}}, + "provenance":{"authority":"catalog.example","observedAt":"2026-09-02T11:00:00Z","mediaType":"application/mcp-server-card+json"} + }], + "truncated":false, + "negotiated":{"runtimeProtocolVersion":1,"grantedCapabilities":[]} + } + """, + """ + { + "kind":"succeeded", + "searchId":"search-invalid", + "candidates":[{ + "kind":"mcp-server", + "handle":"opaque:mcp/01-do-not-parse", + "handleExpiresAt":"2026-09-02T12:00:00Z", + "mediaType":"application/mcp-server-card+json", + "installability":"installable", + "displayName":"Example MCP", + "source":{"rawCard":{"secret":"must-not-survive"}}, + "provenance":{"authority":"catalog.example","observedAt":"2026-09-02T11:00:00Z","mediaType":"application/mcp-server-card+json"} + }], + "truncated":false, + "negotiated":{"runtimeProtocolVersion":1,"grantedCapabilities":[]} + } + """}; + + for (var payload : invalidPayloads) { + assertThrows(JsonProcessingException.class, () -> MAPPER.readValue(payload, CatalogSearchResult.class)); + } + } +} diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index 62b864b6cb..e19ce924e1 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -23710,16 +23710,1271 @@ export interface SessionFsSqliteExistsRequest { sessionId: string; } -const FORBIDDEN_CATALOG_RESPONSE_FIELDS = new Set(["card", "cardData", "rawCard"]); +type RpcResultProjection = + | { kind: "ref"; name: string } + | { kind: "array"; items: RpcResultProjection | null } + | { kind: "object"; closed: boolean; properties: Record } + | { kind: "union"; discriminator: string; variants: Record }; -function sanitizeCatalogSearchResult(value: unknown): unknown { - if (Array.isArray(value)) return value.map(sanitizeCatalogSearchResult); - if (value === null || typeof value !== "object") return value; - return Object.fromEntries( - Object.entries(value) - .filter(([key]) => !FORBIDDEN_CATALOG_RESPONSE_FIELDS.has(key)) - .map(([key, child]) => [key, sanitizeCatalogSearchResult(child)]), - ); +const RPC_RESULT_PROJECTIONS: Record = { + "mcp.planInstall": { + "kind": "ref", + "name": "McpPlanInstallResult" + }, + "catalog.search": { + "kind": "ref", + "name": "CatalogSearchResult" + }, + "agentRegistry.spawn": { + "kind": "ref", + "name": "AgentRegistrySpawnResult" + }, + "session.gitHubAuth.login": { + "kind": "ref", + "name": "AuthInfo" + }, + "session.commands.invoke": { + "kind": "ref", + "name": "SlashCommandInvocationResult" + }, + "session.limitPrediction.predict": { + "kind": "ref", + "name": "SessionLimitPredictionResult" + } +}; + +const RPC_RESULT_PROJECTION_DEFINITIONS: Record = { + "McpPlanResourceIdentity": { + "kind": "object", + "closed": true, + "properties": { + "canonicalName": null, + "serverName": null, + "version": null, + "registryId": null + } + }, + "CardDigest": { + "kind": "object", + "closed": true, + "properties": { + "algorithm": null, + "value": null + } + }, + "McpPlanProvenance": { + "kind": "object", + "closed": true, + "properties": { + "authority": null, + "validatedAt": null, + "cardDigest": { + "kind": "ref", + "name": "CardDigest" + }, + "mediaType": null + } + }, + "McpPlanRequiredValueScalar": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "key": null, + "category": null, + "valueType": null, + "required": null, + "defaultValue": null, + "title": null, + "description": null, + "isRepeated": null + } + }, + "McpPlanRequiredValueEnum": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "key": null, + "category": null, + "valueType": null, + "required": null, + "defaultValue": null, + "title": null, + "description": null, + "enumValues": null, + "isRepeated": null + } + }, + "McpPlanRequiredValue": { + "kind": "union", + "discriminator": "kind", + "variants": { + "string:\"scalar\"": { + "kind": "ref", + "name": "McpPlanRequiredValueScalar" + }, + "string:\"enum\"": { + "kind": "ref", + "name": "McpPlanRequiredValueEnum" + } + } + }, + "McpPlanSecretPlaceholder": { + "kind": "object", + "closed": true, + "properties": { + "key": null, + "placeholder": null, + "title": null + } + }, + "McpPlanTransportChoicePackage": { + "kind": "object", + "closed": true, + "properties": { + "choiceId": null, + "transport": null, + "installMethod": null, + "packageType": null, + "packageIdentifier": null, + "requiredValues": { + "kind": "array", + "items": { + "kind": "ref", + "name": "McpPlanRequiredValue" + } + }, + "secretPlaceholders": { + "kind": "array", + "items": { + "kind": "ref", + "name": "McpPlanSecretPlaceholder" + } + } + } + }, + "McpPlanTransportChoiceRemote": { + "kind": "object", + "closed": true, + "properties": { + "choiceId": null, + "transport": null, + "installMethod": null, + "endpoint": null, + "requiredValues": { + "kind": "array", + "items": { + "kind": "ref", + "name": "McpPlanRequiredValue" + } + }, + "secretPlaceholders": { + "kind": "array", + "items": { + "kind": "ref", + "name": "McpPlanSecretPlaceholder" + } + } + } + }, + "McpPlanTransportChoice": { + "kind": "union", + "discriminator": "installMethod", + "variants": { + "string:\"package\"": { + "kind": "ref", + "name": "McpPlanTransportChoicePackage" + }, + "string:\"remote\"": { + "kind": "ref", + "name": "McpPlanTransportChoiceRemote" + } + } + }, + "McpPlanTarget": { + "kind": "object", + "closed": true, + "properties": { + "scope": null, + "configKey": null + } + }, + "McpPlanPolicyResult": { + "kind": "object", + "closed": true, + "properties": { + "decision": null, + "source": null, + "reason": null + } + }, + "McpPlanConfigurationChange": { + "kind": "object", + "closed": true, + "properties": { + "operation": null, + "scope": null, + "configKey": null, + "changedFields": null, + "secretReferences": null + } + }, + "McpInstallPlan": { + "kind": "object", + "closed": true, + "properties": { + "planHandle": null, + "planHandleExpiresAt": null, + "identity": { + "kind": "ref", + "name": "McpPlanResourceIdentity" + }, + "provenance": { + "kind": "ref", + "name": "McpPlanProvenance" + }, + "transportChoices": { + "kind": "array", + "items": { + "kind": "ref", + "name": "McpPlanTransportChoice" + } + }, + "recommendedTransportChoiceId": null, + "target": { + "kind": "ref", + "name": "McpPlanTarget" + }, + "policy": { + "kind": "ref", + "name": "McpPlanPolicyResult" + }, + "configurationChanges": { + "kind": "array", + "items": { + "kind": "ref", + "name": "McpPlanConfigurationChange" + } + }, + "reloadRequired": null, + "requiresInteractiveConfiguration": null + } + }, + "CatalogNegotiatedContract": { + "kind": "object", + "closed": true, + "properties": { + "runtimeProtocolVersion": null, + "grantedCapabilities": null + } + }, + "McpPlanInstallPlanned": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "plan": { + "kind": "ref", + "name": "McpInstallPlan" + }, + "negotiated": { + "kind": "ref", + "name": "CatalogNegotiatedContract" + } + } + }, + "CatalogNegotiationRefusedError": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "reason": null, + "runtimeProtocolVersion": null, + "minimumSupportedProtocolVersion": null, + "supportedCapabilities": null, + "unsupportedCapabilities": null, + "message": null + } + }, + "CatalogHandleRejectedError": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "handleType": null, + "reason": null, + "message": null + } + }, + "CatalogInvalidRequestError": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "field": null, + "message": null + } + }, + "CatalogAuthenticationRequiredError": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "reason": null, + "message": null + } + }, + "CatalogPolicyRejectedError": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "source": null, + "message": null + } + }, + "CatalogNetworkFailureError": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "reason": null, + "statusCode": null, + "retryAfterSeconds": null, + "message": null + } + }, + "CatalogUnsafeRetrievalError": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "reason": null, + "message": null + } + }, + "CatalogMalformedCardError": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "reason": null, + "mediaType": null, + "message": null + } + }, + "CatalogContractViolationError": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "reason": null, + "message": null + } + }, + "CatalogUnavailableTransportError": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "reason": null, + "message": null + } + }, + "CatalogNotInstallableError": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "reason": null, + "message": null + } + }, + "CatalogUnavailableError": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "reason": null, + "message": null + } + }, + "McpPlanInstallResult": { + "kind": "union", + "discriminator": "kind", + "variants": { + "string:\"planned\"": { + "kind": "ref", + "name": "McpPlanInstallPlanned" + }, + "string:\"negotiation-refused\"": { + "kind": "ref", + "name": "CatalogNegotiationRefusedError" + }, + "string:\"handle-rejected\"": { + "kind": "ref", + "name": "CatalogHandleRejectedError" + }, + "string:\"invalid-request\"": { + "kind": "ref", + "name": "CatalogInvalidRequestError" + }, + "string:\"authentication-required\"": { + "kind": "ref", + "name": "CatalogAuthenticationRequiredError" + }, + "string:\"policy-rejected\"": { + "kind": "ref", + "name": "CatalogPolicyRejectedError" + }, + "string:\"network-failure\"": { + "kind": "ref", + "name": "CatalogNetworkFailureError" + }, + "string:\"unsafe-retrieval\"": { + "kind": "ref", + "name": "CatalogUnsafeRetrievalError" + }, + "string:\"malformed-card\"": { + "kind": "ref", + "name": "CatalogMalformedCardError" + }, + "string:\"contract-violation\"": { + "kind": "ref", + "name": "CatalogContractViolationError" + }, + "string:\"unavailable-transport\"": { + "kind": "ref", + "name": "CatalogUnavailableTransportError" + }, + "string:\"not-installable\"": { + "kind": "ref", + "name": "CatalogNotInstallableError" + }, + "string:\"unavailable\"": { + "kind": "ref", + "name": "CatalogUnavailableError" + } + } + }, + "CatalogCandidateSourceUrl": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "url": null + } + }, + "CatalogCandidateSourceEmbedded": { + "kind": "object", + "closed": true, + "properties": { + "kind": null + } + }, + "CatalogCandidateSource": { + "kind": "union", + "discriminator": "kind", + "variants": { + "string:\"url\"": { + "kind": "ref", + "name": "CatalogCandidateSourceUrl" + }, + "string:\"embedded\"": { + "kind": "ref", + "name": "CatalogCandidateSourceEmbedded" + } + } + }, + "CatalogMcpServerCandidateProvenance": { + "kind": "object", + "closed": true, + "properties": { + "authority": null, + "observedAt": null, + "mediaType": null + } + }, + "CatalogMcpServerCandidate": { + "kind": "object", + "closed": true, + "properties": { + "handle": null, + "handleExpiresAt": null, + "kind": null, + "mediaType": null, + "installability": null, + "displayName": null, + "description": null, + "publisher": null, + "source": { + "kind": "ref", + "name": "CatalogCandidateSource" + }, + "provenance": { + "kind": "ref", + "name": "CatalogMcpServerCandidateProvenance" + } + } + }, + "CatalogAiSkillCandidateProvenance": { + "kind": "object", + "closed": true, + "properties": { + "authority": null, + "observedAt": null, + "mediaType": null + } + }, + "CatalogAiSkillCandidate": { + "kind": "object", + "closed": true, + "properties": { + "handle": null, + "handleExpiresAt": null, + "kind": null, + "mediaType": null, + "installability": null, + "displayName": null, + "description": null, + "publisher": null, + "source": { + "kind": "ref", + "name": "CatalogCandidateSource" + }, + "provenance": { + "kind": "ref", + "name": "CatalogAiSkillCandidateProvenance" + } + } + }, + "CatalogCandidate": { + "kind": "union", + "discriminator": "kind", + "variants": { + "string:\"mcp-server\"": { + "kind": "ref", + "name": "CatalogMcpServerCandidate" + }, + "string:\"ai-skill\"": { + "kind": "ref", + "name": "CatalogAiSkillCandidate" + } + } + }, + "CatalogSearchSucceeded": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "searchId": null, + "candidates": { + "kind": "array", + "items": { + "kind": "ref", + "name": "CatalogCandidate" + } + }, + "truncated": null, + "negotiated": { + "kind": "ref", + "name": "CatalogNegotiatedContract" + } + } + }, + "CatalogUnsupportedKindError": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "requestedKinds": null, + "supportedKinds": null, + "message": null + } + }, + "CatalogSearchResult": { + "kind": "union", + "discriminator": "kind", + "variants": { + "string:\"succeeded\"": { + "kind": "ref", + "name": "CatalogSearchSucceeded" + }, + "string:\"negotiation-refused\"": { + "kind": "ref", + "name": "CatalogNegotiationRefusedError" + }, + "string:\"unsupported-kind\"": { + "kind": "ref", + "name": "CatalogUnsupportedKindError" + }, + "string:\"invalid-request\"": { + "kind": "ref", + "name": "CatalogInvalidRequestError" + }, + "string:\"authentication-required\"": { + "kind": "ref", + "name": "CatalogAuthenticationRequiredError" + }, + "string:\"policy-rejected\"": { + "kind": "ref", + "name": "CatalogPolicyRejectedError" + }, + "string:\"network-failure\"": { + "kind": "ref", + "name": "CatalogNetworkFailureError" + }, + "string:\"unsafe-retrieval\"": { + "kind": "ref", + "name": "CatalogUnsafeRetrievalError" + }, + "string:\"malformed-card\"": { + "kind": "ref", + "name": "CatalogMalformedCardError" + }, + "string:\"contract-violation\"": { + "kind": "ref", + "name": "CatalogContractViolationError" + }, + "string:\"unavailable\"": { + "kind": "ref", + "name": "CatalogUnavailableError" + } + } + }, + "AgentRegistryLiveTargetEntry": { + "kind": "object", + "closed": true, + "properties": { + "schemaVersion": null, + "kind": null, + "pid": null, + "host": null, + "port": null, + "sessionId": null, + "sessionName": null, + "cwd": null, + "branch": null, + "model": null, + "status": null, + "attentionKind": null, + "statusRevision": null, + "lastTerminalEvent": null, + "startedAt": null, + "copilotVersion": null, + "lastSeenMs": null, + "token": null + } + }, + "AgentRegistryLogCapture": { + "kind": "object", + "closed": true, + "properties": { + "enabled": null, + "path": null, + "openError": null, + "openErrorReason": null + } + }, + "AgentRegistrySpawnSpawned": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "entry": { + "kind": "ref", + "name": "AgentRegistryLiveTargetEntry" + }, + "initialPromptSent": null, + "initialPromptError": null, + "logCapture": { + "kind": "ref", + "name": "AgentRegistryLogCapture" + } + } + }, + "AgentRegistrySpawnError": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "message": null, + "code": null + } + }, + "AgentRegistrySpawnRegistryTimeout": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "childPid": null, + "logCapture": { + "kind": "ref", + "name": "AgentRegistryLogCapture" + } + } + }, + "AgentRegistrySpawnValidationError": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "reason": null, + "field": null, + "message": null + } + }, + "AgentRegistrySpawnResult": { + "kind": "union", + "discriminator": "kind", + "variants": { + "string:\"spawned\"": { + "kind": "ref", + "name": "AgentRegistrySpawnSpawned" + }, + "string:\"spawn-error\"": { + "kind": "ref", + "name": "AgentRegistrySpawnError" + }, + "string:\"registry-timeout\"": { + "kind": "ref", + "name": "AgentRegistrySpawnRegistryTimeout" + }, + "string:\"validation-error\"": { + "kind": "ref", + "name": "AgentRegistrySpawnValidationError" + } + } + }, + "CopilotUserResponseEndpoints": { + "kind": "object", + "closed": true, + "properties": { + "api": null, + "origin-tracker": null, + "proxy": null, + "telemetry": null, + "exp": null + } + }, + "CopilotUserResponseQuotaSnapshotsChat": { + "kind": "object", + "closed": true, + "properties": { + "entitlement": null, + "overage_count": null, + "overage_permitted": null, + "percent_remaining": null, + "quota_id": null, + "quota_remaining": null, + "remaining": null, + "unlimited": null, + "timestamp_utc": null, + "has_quota": null, + "quota_reset_at": null, + "token_based_billing": null + } + }, + "CopilotUserResponseQuotaSnapshotsCompletions": { + "kind": "object", + "closed": true, + "properties": { + "entitlement": null, + "overage_count": null, + "overage_permitted": null, + "percent_remaining": null, + "quota_id": null, + "quota_remaining": null, + "remaining": null, + "unlimited": null, + "timestamp_utc": null, + "has_quota": null, + "quota_reset_at": null, + "token_based_billing": null + } + }, + "CopilotUserResponseQuotaSnapshotsPremiumInteractions": { + "kind": "object", + "closed": true, + "properties": { + "entitlement": null, + "overage_count": null, + "overage_permitted": null, + "percent_remaining": null, + "quota_id": null, + "quota_remaining": null, + "remaining": null, + "unlimited": null, + "timestamp_utc": null, + "has_quota": null, + "quota_reset_at": null, + "token_based_billing": null + } + }, + "CopilotUserResponseQuotaSnapshots": { + "kind": "object", + "closed": false, + "properties": { + "chat": { + "kind": "ref", + "name": "CopilotUserResponseQuotaSnapshotsChat" + }, + "completions": { + "kind": "ref", + "name": "CopilotUserResponseQuotaSnapshotsCompletions" + }, + "premium_interactions": { + "kind": "ref", + "name": "CopilotUserResponseQuotaSnapshotsPremiumInteractions" + } + } + }, + "CopilotUserResponse": { + "kind": "object", + "closed": true, + "properties": { + "login": null, + "access_type_sku": null, + "analytics_tracking_id": null, + "assigned_date": null, + "can_signup_for_limited": null, + "chat_enabled": null, + "copilot_plan": null, + "copilotignore_enabled": null, + "endpoints": { + "kind": "ref", + "name": "CopilotUserResponseEndpoints" + }, + "organization_login_list": null, + "organization_list": null, + "codex_agent_enabled": null, + "is_mcp_enabled": null, + "quota_reset_date": null, + "quota_snapshots": { + "kind": "ref", + "name": "CopilotUserResponseQuotaSnapshots" + }, + "restricted_telemetry": null, + "is_staff": null, + "te": null, + "token_based_billing": null, + "can_upgrade_plan": null, + "quota_reset_date_utc": null, + "limited_user_quotas": null, + "limited_user_reset_date": null, + "monthly_quotas": null, + "cloud_session_storage_enabled": null, + "cli_remote_control_enabled": null + } + }, + "HMACAuthInfo": { + "kind": "object", + "closed": true, + "properties": { + "type": null, + "host": null, + "hmac": null, + "copilotUser": { + "kind": "ref", + "name": "CopilotUserResponse" + } + } + }, + "EnvAuthInfo": { + "kind": "object", + "closed": true, + "properties": { + "type": null, + "host": null, + "login": null, + "token": null, + "envVar": null, + "copilotUser": { + "kind": "ref", + "name": "CopilotUserResponse" + } + } + }, + "TokenAuthInfo": { + "kind": "object", + "closed": true, + "properties": { + "type": null, + "host": null, + "token": null, + "registrationId": null, + "copilotUser": { + "kind": "ref", + "name": "CopilotUserResponse" + } + } + }, + "TokenProviderAuthInfo": { + "kind": "object", + "closed": true, + "properties": { + "type": null, + "host": null, + "registrationId": null, + "copilotUser": { + "kind": "ref", + "name": "CopilotUserResponse" + } + } + }, + "CopilotApiTokenAuthInfo": { + "kind": "object", + "closed": true, + "properties": { + "type": null, + "host": null, + "copilotUser": { + "kind": "ref", + "name": "CopilotUserResponse" + } + } + }, + "UserAuthInfo": { + "kind": "object", + "closed": true, + "properties": { + "type": null, + "host": null, + "login": null, + "copilotUser": { + "kind": "ref", + "name": "CopilotUserResponse" + } + } + }, + "GhCliAuthInfo": { + "kind": "object", + "closed": true, + "properties": { + "type": null, + "host": null, + "login": null, + "token": null, + "copilotUser": { + "kind": "ref", + "name": "CopilotUserResponse" + } + } + }, + "ApiKeyAuthInfo": { + "kind": "object", + "closed": true, + "properties": { + "type": null, + "apiKey": null, + "host": null, + "copilotUser": { + "kind": "ref", + "name": "CopilotUserResponse" + } + } + }, + "AuthInfo": { + "kind": "union", + "discriminator": "type", + "variants": { + "string:\"hmac\"": { + "kind": "ref", + "name": "HMACAuthInfo" + }, + "string:\"env\"": { + "kind": "ref", + "name": "EnvAuthInfo" + }, + "string:\"token\"": { + "kind": "ref", + "name": "TokenAuthInfo" + }, + "string:\"token-provider\"": { + "kind": "ref", + "name": "TokenProviderAuthInfo" + }, + "string:\"copilot-api-token\"": { + "kind": "ref", + "name": "CopilotApiTokenAuthInfo" + }, + "string:\"user\"": { + "kind": "ref", + "name": "UserAuthInfo" + }, + "string:\"gh-cli\"": { + "kind": "ref", + "name": "GhCliAuthInfo" + }, + "string:\"api-key\"": { + "kind": "ref", + "name": "ApiKeyAuthInfo" + } + } + }, + "SlashCommandTextResult": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "text": null, + "markdown": null, + "preserveAnsi": null, + "runtimeSettingsChanged": null + } + }, + "SlashCommandAgentPromptResult": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "prompt": null, + "displayPrompt": null, + "mode": null, + "notice": null, + "runtimeSettingsChanged": null + } + }, + "SlashCommandCompletedResult": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "message": null, + "mode": null, + "runtimeSettingsChanged": null + } + }, + "SlashCommandSelectSubcommandOption": { + "kind": "object", + "closed": true, + "properties": { + "name": null, + "description": null, + "group": null + } + }, + "SlashCommandSelectSubcommandResult": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "command": null, + "title": null, + "options": { + "kind": "array", + "items": { + "kind": "ref", + "name": "SlashCommandSelectSubcommandOption" + } + }, + "runtimeSettingsChanged": null + } + }, + "SlashCommandTimelineEntry": { + "kind": "object", + "closed": true, + "properties": { + "type": null, + "text": null, + "url": null, + "remediation": null + } + }, + "SlashCommandAddTimelineEntryResult": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "entry": { + "kind": "ref", + "name": "SlashCommandTimelineEntry" + }, + "prefillInput": null, + "runtimeSettingsChanged": null + } + }, + "SlashCommandModelPickerDialog": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "modelToEnable": null, + "scope": null, + "target": null + } + }, + "SlashCommandShowDialogResult": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "dialog": { + "kind": "ref", + "name": "SlashCommandModelPickerDialog" + }, + "runtimeSettingsChanged": null + } + }, + "SlashCommandSetModelResult": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "model": null, + "scope": null, + "warning": null, + "reasoningEffort": null, + "revertOnCancel": null, + "repoScope": null, + "runtimeSettingsChanged": null + } + }, + "SlashCommandSetPlanModelResult": { + "kind": "object", + "closed": true, + "properties": { + "kind": null, + "planModel": null, + "message": null, + "runtimeSettingsChanged": null + } + }, + "SlashCommandInvocationResult": { + "kind": "union", + "discriminator": "kind", + "variants": { + "string:\"text\"": { + "kind": "ref", + "name": "SlashCommandTextResult" + }, + "string:\"agent-prompt\"": { + "kind": "ref", + "name": "SlashCommandAgentPromptResult" + }, + "string:\"completed\"": { + "kind": "ref", + "name": "SlashCommandCompletedResult" + }, + "string:\"select-subcommand\"": { + "kind": "ref", + "name": "SlashCommandSelectSubcommandResult" + }, + "string:\"add-timeline-entry\"": { + "kind": "ref", + "name": "SlashCommandAddTimelineEntryResult" + }, + "string:\"show-dialog\"": { + "kind": "ref", + "name": "SlashCommandShowDialogResult" + }, + "string:\"set-model\"": { + "kind": "ref", + "name": "SlashCommandSetModelResult" + }, + "string:\"set-plan-model\"": { + "kind": "ref", + "name": "SlashCommandSetPlanModelResult" + } + } + }, + "SessionLimitPredictionTierOption": { + "kind": "object", + "closed": true, + "properties": { + "tier": null, + "cap": null + } + }, + "SessionLimitPredictionBaselineData": { + "kind": "object", + "closed": true, + "properties": { + "windowStart": null, + "windowEnd": null + } + }, + "SessionLimitPredictionDetails": { + "kind": "object", + "closed": true, + "properties": { + "clientType": null, + "modelId": null, + "source": null, + "sourceKey": null, + "family": null, + "tiers": { + "kind": "array", + "items": { + "kind": "ref", + "name": "SessionLimitPredictionTierOption" + } + }, + "baselineData": { + "kind": "ref", + "name": "SessionLimitPredictionBaselineData" + }, + "recommendedTier": null, + "recommendedCap": null + } + }, + "SessionLimitPredictionResult": { + "kind": "union", + "discriminator": "kind", + "variants": { + "string:\"available\"": { + "kind": "object", + "closed": true, + "properties": { + "prediction": { + "kind": "ref", + "name": "SessionLimitPredictionDetails" + }, + "kind": null + } + }, + "string:\"unavailable\"": { + "kind": "object", + "closed": true, + "properties": { + "reason": null, + "kind": null + } + } + } + } +}; + +function projectRpcResult(value: unknown, projection: RpcResultProjection, path = "$"): unknown { + if (projection.kind === "ref") { + const definition = RPC_RESULT_PROJECTION_DEFINITIONS[projection.name]; + if (!definition) throw new TypeError(`Missing RPC result projection for ${projection.name}`); + return projectRpcResult(value, definition, path); + } + if (projection.kind === "array") { + if (!Array.isArray(value)) throw new TypeError(`Invalid RPC result at ${path}: expected an array`); + return projection.items ? value.map((item, index) => projectRpcResult(item, projection.items!, `${path}[${index}]`)) : value; + } + if (value === null || typeof value !== "object" || Array.isArray(value)) { + throw new TypeError(`Invalid RPC result at ${path}: expected an object`); + } + const record = value as Record; + if (projection.kind === "union") { + const discriminator = record[projection.discriminator]; + const key = `${typeof discriminator}:${JSON.stringify(discriminator)}`; + const variant = projection.variants[key]; + if (!variant) { + throw new TypeError(`Invalid RPC result at ${path}: unknown or missing ${projection.discriminator} discriminator`); + } + return projectRpcResult(value, variant, path); + } + const result: Record = projection.closed ? {} : { ...record }; + for (const [name, child] of Object.entries(projection.properties)) { + if (!Object.hasOwn(record, name)) continue; + result[name] = child ? projectRpcResult(record[name], child, `${path}.${name}`) : record[name]; + } + return result; } /** Create typed server-scoped RPC methods (no session required). */ @@ -23904,7 +25159,10 @@ export function createServerRpc(connection: MessageConnection) { * @returns Outcome of an mcp.planInstall call: either a normalised plan, or one typed refusal. Nothing is written in either case. */ planInstall: async (params: McpPlanInstallRequest): Promise => - connection.sendRequest("mcp.planInstall", params), + projectRpcResult( + await connection.sendRequest("mcp.planInstall", params), + RPC_RESULT_PROJECTIONS["mcp.planInstall"], + ) as McpPlanInstallResult, }, /** @experimental */ extensions: { @@ -23947,8 +25205,9 @@ export function createServerRpc(connection: MessageConnection) { * @returns Outcome of a catalog.search call: either bounded inert candidates, or one typed refusal. Never a partial success. */ search: async (params: CatalogSearchRequest): Promise => - sanitizeCatalogSearchResult( + projectRpcResult( await connection.sendRequest("catalog.search", params), + RPC_RESULT_PROJECTIONS["catalog.search"], ) as CatalogSearchResult, }, /** @experimental */ @@ -24468,7 +25727,10 @@ export function createServerRpc(connection: MessageConnection) { * @returns Outcome of an agentRegistry.spawn call. */ spawn: async (params: AgentRegistrySpawnRequest): Promise => - connection.sendRequest("agentRegistry.spawn", params), + projectRpcResult( + await connection.sendRequest("agentRegistry.spawn", params), + RPC_RESULT_PROJECTIONS["agentRegistry.spawn"], + ) as AgentRegistrySpawnResult, }, }; } @@ -25775,7 +27037,10 @@ 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 }), + projectRpcResult( + await connection.sendRequest("session.commands.invoke", { sessionId, ...params }), + RPC_RESULT_PROJECTIONS["session.commands.invoke"], + ) as SlashCommandInvocationResult, /** * Reports completion of a pending client-handled slash command. * @@ -26473,7 +27738,10 @@ 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 }), + projectRpcResult( + await connection.sendRequest("session.limitPrediction.predict", { sessionId, ...params }), + RPC_RESULT_PROJECTIONS["session.limitPrediction.predict"], + ) as SessionLimitPredictionResult, }, /** @experimental */ remote: { @@ -26589,7 +27857,10 @@ 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 }), + projectRpcResult( + await connection.sendRequest("session.gitHubAuth.login", { sessionId, ...params }), + RPC_RESULT_PROJECTIONS["session.gitHubAuth.login"], + ) as AuthInfo, /** * Switches the session to another available authentication. * diff --git a/nodejs/test/discriminated-union-codegen.test.ts b/nodejs/test/discriminated-union-codegen.test.ts new file mode 100644 index 0000000000..592aab22d5 --- /dev/null +++ b/nodejs/test/discriminated-union-codegen.test.ts @@ -0,0 +1,140 @@ +import type { JSONSchema7 } from "json-schema"; +import { describe, expect, it } from "vitest"; + +import { collectNestedDiscriminatedUnionTypeNames } from "../../java/scripts/codegen/java.ts"; +import { + analyseDiscriminatedUnion, + schemaDiscriminatorValueKey, +} from "../../scripts/codegen/schema-unions.ts"; +import { createRpcResultProjectionBundle } from "../../scripts/codegen/typescript.ts"; +import type { DefinitionCollections } from "../../scripts/codegen/utils.ts"; + +const definitions: Record = { + SyntheticEnvelope: { + type: "object", + additionalProperties: false, + required: ["choices"], + properties: { + choices: { + type: "array", + items: { $ref: "#/definitions/SyntheticChoice" }, + }, + }, + }, + SyntheticChoice: { + anyOf: [{ $ref: "#/definitions/SyntheticAlpha" }, { $ref: "#/definitions/SyntheticBeta" }], + }, + SyntheticAlpha: { + type: "object", + additionalProperties: false, + required: ["kind", "source"], + properties: { + kind: { const: "alpha" }, + source: { $ref: "#/definitions/SyntheticSource" }, + }, + }, + SyntheticBeta: { + type: "object", + additionalProperties: false, + required: ["kind", "source"], + properties: { + kind: { const: "beta" }, + source: { $ref: "#/definitions/SyntheticSource" }, + }, + }, + SyntheticSource: { + oneOf: [ + { $ref: "#/definitions/SyntheticInlineSource" }, + { $ref: "#/definitions/SyntheticUrlSource" }, + ], + }, + SyntheticInlineSource: { + type: "object", + additionalProperties: false, + required: ["kind"], + properties: { + kind: { const: "inline" }, + }, + }, + SyntheticUrlSource: { + type: "object", + additionalProperties: false, + required: ["kind", "url"], + properties: { + kind: { const: "url" }, + url: { type: "string" }, + }, + }, +}; + +const collections: DefinitionCollections = { definitions, $defs: {} }; +const resolveVariant = (schema: JSONSchema7): JSONSchema7 | undefined => { + const name = schema.$ref?.match(/^#\/definitions\/([^/]+)$/)?.[1]; + return name ? definitions[name] : schema; +}; + +describe("schema-driven discriminated union codegen", () => { + it("classifies a closed synthetic union without relying on domain names", () => { + const analysis = analyseDiscriminatedUnion(definitions.SyntheticChoice, resolveVariant); + + expect(analysis?.property).toBe("kind"); + expect(analysis?.unknownVariantPolicy).toBe("reject"); + expect(analysis?.mapping.map(({ value }) => value)).toEqual(["alpha", "beta"]); + }); + + it("preserves fallback only when a variant explicitly permits extra properties", () => { + const openDefinitions = structuredClone(definitions); + openDefinitions.SyntheticBeta.additionalProperties = true; + const analysis = analyseDiscriminatedUnion(openDefinitions.SyntheticChoice, (schema) => { + const name = schema.$ref?.match(/^#\/definitions\/([^/]+)$/)?.[1]; + return name ? openDefinitions[name] : schema; + }); + + expect(analysis?.unknownVariantPolicy).toBe("preserve"); + }); + + it("builds nested runtime projections for an equivalent synthetic result", () => { + const projection = createRpcResultProjectionBundle( + { $ref: "#/definitions/SyntheticEnvelope" }, + collections + ); + + expect(projection?.root).toEqual({ + kind: "ref", + name: "SyntheticEnvelope", + }); + expect(projection?.definitions.SyntheticChoice).toMatchObject({ + kind: "union", + discriminator: "kind", + variants: { + [schemaDiscriminatorValueKey("alpha")]: { + kind: "ref", + name: "SyntheticAlpha", + }, + [schemaDiscriminatorValueKey("beta")]: { + kind: "ref", + name: "SyntheticBeta", + }, + }, + }); + expect(projection?.definitions.SyntheticSource).toMatchObject({ + kind: "union", + discriminator: "kind", + }); + }); + + it("promotes every nested synthetic union for Java generation", () => { + expect( + collectNestedDiscriminatedUnionTypeNames( + { $ref: "#/definitions/SyntheticEnvelope" }, + definitions + ) + ).toEqual(new Set(["SyntheticChoice", "SyntheticSource"])); + expect( + collectNestedDiscriminatedUnionTypeNames( + { $ref: "#/definitions/SyntheticChoice" }, + definitions + ) + ).toEqual(new Set(["SyntheticSource"])); + }); +}); diff --git a/nodejs/test/catalogue-conformance.test.ts b/nodejs/test/discriminated-union-conformance.test.ts similarity index 73% rename from nodejs/test/catalogue-conformance.test.ts rename to nodejs/test/discriminated-union-conformance.test.ts index 769bbd5c31..ce4330b825 100644 --- a/nodejs/test/catalogue-conformance.test.ts +++ b/nodejs/test/discriminated-union-conformance.test.ts @@ -63,16 +63,50 @@ function successWireResult(): unknown { if (result.kind !== "succeeded") throw new Error("Expected a successful search."); return { ...result, + rawCard: { secret: "must-not-survive" }, candidates: result.candidates.map((candidate) => ({ ...candidate, card: { secret: "must-not-survive" }, cardData: { secret: "must-not-survive" }, rawCard: { secret: "must-not-survive" }, + source: { + ...candidate.source, + rawCard: { secret: "must-not-survive" }, + }, })), }; } -describe("catalogue binding conformance", () => { +function invalidWireResult(kind: string): unknown { + const result = structuredClone(successWireResult()) as { + kind?: string; + candidates: Array<{ kind?: string; source: { kind?: string } }>; + }; + switch (kind) { + case "unknown-result": + result.kind = "future-result"; + return result; + case "missing-result": + delete result.kind; + return result; + case "unknown-candidate": + result.candidates[0].kind = "future-candidate"; + return result; + case "missing-candidate": + delete result.candidates[0].kind; + return result; + case "unknown-source": + result.candidates[0].source.kind = "future-source"; + return result; + case "missing-source": + delete result.candidates[0].source.kind; + return result; + default: + throw new Error(`Unknown invalid result fixture: ${kind}`); + } +} + +describe("closed discriminated union conformance", () => { it("transports typed candidates, refusals, and failures unchanged", async () => { const clientToServer = new PassThrough(); const serverToClient = new PassThrough(); @@ -105,6 +139,9 @@ describe("catalogue binding conformance", () => { message: "The catalogue timed out.", } satisfies CatalogSearchResult; } + if (params.query.startsWith("invalid:")) { + return invalidWireResult(params.query.slice("invalid:".length)); + } return successWireResult(); }); client.listen(); @@ -130,10 +167,12 @@ describe("catalogue binding conformance", () => { const encodedCandidates = JSON.parse(JSON.stringify(success)).candidates as Array< Record >; + expect(success).not.toHaveProperty("rawCard"); for (const candidate of encodedCandidates) { expect(candidate).not.toHaveProperty("card"); expect(candidate).not.toHaveProperty("cardData"); expect(candidate).not.toHaveProperty("rawCard"); + expect(candidate.source).not.toHaveProperty("rawCard"); } await expect( @@ -147,5 +186,18 @@ describe("catalogue binding conformance", () => { reason: "timeout", retryAfterSeconds: 30, }); + + for (const invalid of [ + "unknown-result", + "missing-result", + "unknown-candidate", + "missing-candidate", + "unknown-source", + "missing-source", + ]) { + await expect( + rpc.catalog.search({ ...request, query: `invalid:${invalid}` }) + ).rejects.toThrow(/unknown or missing kind discriminator/); + } }); }); diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index 52a07e576e..e0eb6984f1 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -40037,7 +40037,7 @@ def _load_AgentRegistrySpawnResult(obj: Any) -> "AgentRegistrySpawnResult": case "spawn-error": return AgentRegistrySpawnError.from_dict(obj) case "registry-timeout": return AgentRegistrySpawnRegistryTimeout.from_dict(obj) case "validation-error": return AgentRegistrySpawnValidationError.from_dict(obj) - case _: raise ValueError(f"Unknown AgentRegistrySpawnResult kind: {kind!r}") + 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 @@ -40054,7 +40054,7 @@ def _load_AuthInfo(obj: Any) -> "AuthInfo": case "user": return UserAuthInfo.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}") + raise ValueError(f"Unknown AuthInfo type: {kind!r}") # One inert catalog result, represented as an MCP server or discovery-only AI skill variant so kind, media type, provenance, and installability cannot contradict each other. CatalogCandidate = CatalogMCPServerCandidate | CatalogAISkillCandidate @@ -40112,7 +40112,7 @@ def _load_ExternalToolTextResultForLlmContent(obj: Any) -> "ExternalToolTextResu case "audio": return ExternalToolTextResultForLlmContentAudio.from_dict(obj) case "resource_link": return ExternalToolTextResultForLlmContentResourceLink.from_dict(obj) case "resource": return ExternalToolTextResultForLlmContentResource.from_dict(obj) - case _: raise ValueError(f"Unknown ExternalToolTextResultForLlmContent type: {kind!r}") + raise ValueError(f"Unknown ExternalToolTextResultForLlmContent type: {kind!r}") # Outcome of an mcp.planInstall call: either a normalised plan, or one typed refusal. Nothing is written in either case. MCPPlanInstallResult = MCPPlanInstallPlanned | CatalogNegotiationRefusedError | CatalogHandleRejectedError | CatalogInvalidRequestError | CatalogAuthenticationRequiredError | CatalogPolicyRejectedError | CatalogNetworkFailureError | CatalogUnsafeRetrievalError | CatalogMalformedCardError | CatalogContractViolationError | CatalogUnavailableTransportError | CatalogNotInstallableError | CatalogUnavailableError @@ -40134,7 +40134,7 @@ def _load_MCPPlanInstallResult(obj: Any) -> "MCPPlanInstallResult": case "unavailable-transport": return CatalogUnavailableTransportError.from_dict(obj) case "not-installable": return CatalogNotInstallableError.from_dict(obj) case "unavailable": return CatalogUnavailableError.from_dict(obj) - case _: raise ValueError(f"Unknown MCPPlanInstallResult kind: {kind!r}") + raise ValueError(f"Unknown MCPPlanInstallResult kind: {kind!r}") # What an install plan is computed from: a candidate handle from a previous search, or a card supplied directly. MCPPlanInstallSource = MCPPlanInstallSourceCandidate | MCPPlanInstallSourceCard @@ -40145,7 +40145,7 @@ def _load_MCPPlanInstallSource(obj: Any) -> "MCPPlanInstallSource": match kind: case "candidate": return MCPPlanInstallSourceCandidate.from_dict(obj) case "card": return MCPPlanInstallSourceCard.from_dict(obj) - case _: raise ValueError(f"Unknown MCPPlanInstallSource kind: {kind!r}") + raise ValueError(f"Unknown MCPPlanInstallSource kind: {kind!r}") # 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. MCPPlanRequiredValue = MCPPlanRequiredValueScalar | MCPPlanRequiredValueEnum @@ -40156,7 +40156,7 @@ def _load_MCPPlanRequiredValue(obj: Any) -> "MCPPlanRequiredValue": match kind: case "scalar": return MCPPlanRequiredValueScalar.from_dict(obj) case "enum": return MCPPlanRequiredValueEnum.from_dict(obj) - case _: raise ValueError(f"Unknown MCPPlanRequiredValue kind: {kind!r}") + raise ValueError(f"Unknown MCPPlanRequiredValue kind: {kind!r}") # 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. MCPPlanTransportChoice = MCPPlanTransportChoicePackage | MCPPlanTransportChoiceRemote @@ -40167,7 +40167,7 @@ def _load_MCPPlanTransportChoice(obj: Any) -> "MCPPlanTransportChoice": match kind: case "package": return MCPPlanTransportChoicePackage.from_dict(obj) case "remote": return MCPPlanTransportChoiceRemote.from_dict(obj) - case _: raise ValueError(f"Unknown MCPPlanTransportChoice installMethod: {kind!r}") + raise ValueError(f"Unknown MCPPlanTransportChoice installMethod: {kind!r}") # 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. MCPServerCardReference = MCPServerCardURL | MCPServerCardEmbedded @@ -40178,7 +40178,7 @@ def _load_MCPServerCardReference(obj: Any) -> "MCPServerCardReference": match kind: case "url": return MCPServerCardURL.from_dict(obj) case "embedded": return MCPServerCardEmbedded.from_dict(obj) - case _: raise ValueError(f"Unknown MCPServerCardReference kind: {kind!r}") + 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 @@ -40202,7 +40202,7 @@ def _load_PermissionDecision(obj: Any) -> "PermissionDecision": case "denied-interactively-by-user": return PermissionDecisionDeniedInteractivelyByUser.from_dict(obj) case "denied-by-content-exclusion-policy": return PermissionDecisionDeniedByContentExclusionPolicy.from_dict(obj) case "denied-by-permission-request-hook": return PermissionDecisionDeniedByPermissionRequestHook.from_dict(obj) - case _: raise ValueError(f"Unknown PermissionDecision kind: {kind!r}") + 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 @@ -40222,7 +40222,7 @@ def _load_PermissionDecisionApproveForLocationApproval(obj: Any) -> "PermissionD case "factory": return PermissionDecisionApproveForLocationApprovalFactory.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}") + 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 @@ -40242,7 +40242,7 @@ def _load_PermissionDecisionApproveForSessionApproval(obj: Any) -> "PermissionDe case "factory": return PermissionDecisionApproveForSessionApprovalFactory.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}") + 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 @@ -40262,7 +40262,7 @@ def _load_PermissionsLocationsAddToolApprovalDetails(obj: Any) -> "PermissionsLo case "factory": return PermissionsLocationsAddToolApprovalDetailsFactory.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}") + raise ValueError(f"Unknown PermissionsLocationsAddToolApprovalDetails kind: {kind!r}") # Attachment union accepted by push input, covering files, directories, GitHub objects, blobs, snippets, and extension context. PushAttachment = PushAttachmentFile | PushAttachmentDirectory | PushAttachmentSelection | PushAttachmentGitHubReference | PushAttachmentGitHubCommit | PushAttachmentGitHubRelease | PushAttachmentGitHubActionsJob | PushAttachmentGitHubRepository | PushAttachmentGitHubFileDiff | PushAttachmentGitHubTreeComparison | PushAttachmentGitHubURL | PushAttachmentGitHubFile | PushAttachmentGitHubSnippet | PushAttachmentBlob | ExtensionContextPushInput @@ -40286,7 +40286,7 @@ def _load_PushAttachment(obj: Any) -> "PushAttachment": case "github_snippet": return PushAttachmentGitHubSnippet.from_dict(obj) case "blob": return PushAttachmentBlob.from_dict(obj) case "extension_context": return ExtensionContextPushInput.from_dict(obj) - case _: raise ValueError(f"Unknown PushAttachment type: {kind!r}") + raise ValueError(f"Unknown PushAttachment type: {kind!r}") # Result of the queued command execution. QueuedCommandResult = QueuedCommandHandled | QueuedCommandNotHandled @@ -40297,7 +40297,7 @@ def _load_QueuedCommandResult(obj: Any) -> "QueuedCommandResult": match kind: case True: return QueuedCommandHandled.from_dict(obj) case False: return QueuedCommandNotHandled.from_dict(obj) - case _: raise ValueError(f"Unknown QueuedCommandResult handled: {kind!r}") + raise ValueError(f"Unknown QueuedCommandResult handled: {kind!r}") # State of the runtime-managed remote-control singleton. RemoteControlStatus = RemoteControlStatusOff | RemoteControlStatusConnecting | RemoteControlStatusActive | RemoteControlStatusError @@ -40310,7 +40310,7 @@ def _load_RemoteControlStatus(obj: Any) -> "RemoteControlStatus": case "connecting": return RemoteControlStatusConnecting.from_dict(obj) case "active": return RemoteControlStatusActive.from_dict(obj) case "error": return RemoteControlStatusError.from_dict(obj) - case _: raise ValueError(f"Unknown RemoteControlStatus state: {kind!r}") + raise ValueError(f"Unknown RemoteControlStatus state: {kind!r}") # Local or remote session metadata entry. Narrow on `isRemote` to access source-specific fields. SessionListEntry = LocalSessionMetadataValue | RemoteSessionMetadataValue @@ -40321,7 +40321,7 @@ def _load_SessionListEntry(obj: Any) -> "SessionListEntry": match kind: case False: return LocalSessionMetadataValue.from_dict(obj) case True: return RemoteSessionMetadataValue.from_dict(obj) - case _: raise ValueError(f"Unknown SessionListEntry isRemote: {kind!r}") + raise ValueError(f"Unknown SessionListEntry isRemote: {kind!r}") # Open a session by creating, resuming, attaching, connecting to a remote, or handing off. SessionOpenParams = SessionsOpenCreate | SessionsOpenResume | SessionsOpenResumeLast | SessionsOpenAttach | SessionsOpenRemote | SessionsOpenCloud | SessionsOpenHandoff @@ -40337,7 +40337,7 @@ def _load_SessionOpenParams(obj: Any) -> "SessionOpenParams": case "remote": return SessionsOpenRemote.from_dict(obj) case "cloud": return SessionsOpenCloud.from_dict(obj) case "handoff": return SessionsOpenHandoff.from_dict(obj) - case _: raise ValueError(f"Unknown SessionOpenParams kind: {kind!r}") + 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 @@ -40353,7 +40353,7 @@ def _load_SettableAuthInfo(obj: Any) -> "SettableAuthInfo": case "user": return UserAuthInfo.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}") + raise ValueError(f"Unknown SettableAuthInfo type: {kind!r}") # Result of invoking the slash command (text output, prompt to send to the agent, completion, or subcommand selection). SlashCommandInvocationResult = SlashCommandTextResult | SlashCommandAgentPromptResult | SlashCommandCompletedResult | SlashCommandSelectSubcommandResult | SlashCommandAddTimelineEntryResult | SlashCommandShowDialogResult | SlashCommandSetModelResult | SlashCommandSetPlanModelResult @@ -40370,7 +40370,7 @@ def _load_SlashCommandInvocationResult(obj: Any) -> "SlashCommandInvocationResul case "show-dialog": return SlashCommandShowDialogResult.from_dict(obj) case "set-model": return SlashCommandSetModelResult.from_dict(obj) case "set-plan-model": return SlashCommandSetPlanModelResult.from_dict(obj) - case _: raise ValueError(f"Unknown SlashCommandInvocationResult kind: {kind!r}") + raise ValueError(f"Unknown SlashCommandInvocationResult kind: {kind!r}") # Tracked task union returned by task APIs, containing an agent, client, or shell task. TaskInfo = TaskAgentInfo | TaskClientInfo | TaskShellInfo @@ -40382,7 +40382,7 @@ def _load_TaskInfo(obj: Any) -> "TaskInfo": case "agent": return TaskAgentInfo.from_dict(obj) case "client": return TaskClientInfo.from_dict(obj) case "shell": return TaskShellInfo.from_dict(obj) - case _: raise ValueError(f"Unknown TaskInfo type: {kind!r}") + raise ValueError(f"Unknown TaskInfo type: {kind!r}") AccountGetAllUsersResult = list diff --git a/python/test_rpc_generated.py b/python/test_rpc_generated.py index 5c52de8435..c59b518348 100644 --- a/python/test_rpc_generated.py +++ b/python/test_rpc_generated.py @@ -1,6 +1,7 @@ """Tests for generated RPC method behavior.""" import json +from typing import Any from unittest.mock import AsyncMock import pytest @@ -156,11 +157,12 @@ def test_queued_command_result_serializes_boolean_discriminator( @pytest.mark.asyncio -async def test_catalog_search_preserves_typed_candidates_and_opaque_handles(): +async def test_closed_union_preserves_typed_nested_variants_and_opaque_handles(): client = AsyncMock() client.request = AsyncMock( return_value={ "kind": "succeeded", + "rawCard": {"secret": "must-not-survive"}, "searchId": "search-01", "candidates": [ { @@ -174,6 +176,7 @@ async def test_catalog_search_preserves_typed_candidates_and_opaque_handles(): "source": { "kind": "url", "url": "https://catalog.example/mcp.json", + "rawCard": {"secret": "must-not-survive"}, }, "provenance": { "authority": "catalog.example", @@ -189,7 +192,10 @@ async def test_catalog_search_preserves_typed_candidates_and_opaque_handles(): "installability": "not-installable-kind", "displayName": "Example skill", "rawCard": {"secret": "must-not-survive"}, - "source": {"kind": "embedded"}, + "source": { + "kind": "embedded", + "rawCard": {"secret": "must-not-survive"}, + }, "provenance": { "authority": "catalog.example", "observedAt": "2026-09-02T11:00:00Z", @@ -227,8 +233,11 @@ async def test_catalog_search_preserves_typed_candidates_and_opaque_handles(): assert skill.handle == OPAQUE_SKILL_HANDLE assert isinstance(mcp.source, CatalogCandidateSourceURL) assert isinstance(skill.source, CatalogCandidateSourceEmbedded) - for candidate in result.to_dict()["candidates"]: + encoded = result.to_dict() + assert "rawCard" not in encoded + for candidate in encoded["candidates"]: assert {"card", "cardData", "rawCard"}.isdisjoint(candidate) + assert "rawCard" not in candidate["source"] @pytest.mark.asyncio @@ -254,7 +263,7 @@ async def test_catalog_search_preserves_typed_candidates_and_opaque_handles(): ), ], ) -async def test_catalog_search_preserves_refusals_and_failures(payload, expected_type): +async def test_closed_union_preserves_refusals_and_failures(payload, expected_type): client = AsyncMock() client.request = AsyncMock(return_value=payload) api = ServerCatalogApi(client) @@ -273,29 +282,68 @@ async def test_catalog_search_preserves_refusals_and_failures(payload, expected_ @pytest.mark.asyncio -async def test_catalog_search_rejects_unknown_candidate_kinds(): - client = AsyncMock() - client.request = AsyncMock( - return_value={ - "kind": "succeeded", - "searchId": "search-unknown", - "candidates": [ - { - "kind": "future-kind", - "handle": "opaque:future/03-do-not-parse", - "rawCard": {"secret": "must-not-survive"}, - } - ], - "truncated": False, - "negotiated": { - "runtimeProtocolVersion": 1, - "grantedCapabilities": [], - }, +@pytest.mark.parametrize( + "case", + [ + "unknown-result", + "missing-result", + "unknown-candidate", + "missing-candidate", + "unknown-source", + "missing-source", + ], +) +async def test_closed_union_rejects_unknown_and_missing_discriminators(case): + candidate: dict[str, Any] = { + "kind": "mcp-server", + "handle": OPAQUE_MCP_HANDLE, + "handleExpiresAt": "2026-09-02T12:00:00Z", + "mediaType": "application/mcp-server-card+json", + "installability": "installable", + "displayName": "Example MCP", + "source": { + "kind": "url", + "url": "https://catalog.example/mcp.json", + }, + "provenance": { + "authority": "catalog.example", + "observedAt": "2026-09-02T11:00:00Z", + "mediaType": "application/mcp-server-card+json", + }, + } + payload: dict[str, Any] = { + "kind": "succeeded", + "searchId": "search-invalid", + "candidates": [candidate], + "truncated": False, + "negotiated": { + "runtimeProtocolVersion": 1, + "grantedCapabilities": [], + }, + } + if case == "unknown-result": + payload = {"kind": "future-result", "rawCard": {"secret": "must-not-survive"}} + elif case == "missing-result": + payload = {"rawCard": {"secret": "must-not-survive"}} + elif case == "unknown-candidate": + candidate["kind"] = "future-candidate" + candidate["rawCard"] = {"secret": "must-not-survive"} + elif case == "missing-candidate": + del candidate["kind"] + candidate["rawCard"] = {"secret": "must-not-survive"} + elif case == "unknown-source": + candidate["source"] = { + "kind": "future-source", + "rawCard": {"secret": "must-not-survive"}, } - ) + elif case == "missing-source": + candidate["source"] = {"rawCard": {"secret": "must-not-survive"}} + + client = AsyncMock() + client.request = AsyncMock(return_value=payload) api = ServerCatalogApi(client) - with pytest.raises(ValueError, match="Unknown CatalogCandidate kind"): + with pytest.raises(ValueError, match="Unknown .* kind"): await api.search( CatalogSearchRequest( contract=CatalogClientContract( diff --git a/rust/tests/catalogue_conformance_test.rs b/rust/tests/discriminated_union_conformance_test.rs similarity index 51% rename from rust/tests/catalogue_conformance_test.rs rename to rust/tests/discriminated_union_conformance_test.rs index 035b5ebd66..1d05f9fb64 100644 --- a/rust/tests/catalogue_conformance_test.rs +++ b/rust/tests/discriminated_union_conformance_test.rs @@ -6,9 +6,10 @@ const OPAQUE_MCP_HANDLE: &str = "opaque:mcp/01-do-not-parse"; const OPAQUE_SKILL_HANDLE: &str = "opaque:skill/02-do-not-parse"; #[test] -fn catalog_search_result_preserves_candidate_semantics() { +fn closed_union_preserves_known_nested_variants() { let result: CatalogSearchResult = serde_json::from_value(serde_json::json!({ "kind": "succeeded", + "rawCard": {"secret": "must-not-survive"}, "searchId": "search-01", "candidates": [ { @@ -19,7 +20,11 @@ fn catalog_search_result_preserves_candidate_semantics() { "installability": "installable", "displayName": "Example MCP", "rawCard": {"secret": "must-not-survive"}, - "source": {"kind": "url", "url": "https://catalog.example/mcp.json"}, + "source": { + "kind": "url", + "url": "https://catalog.example/mcp.json", + "rawCard": {"secret": "must-not-survive"} + }, "provenance": { "authority": "catalog.example", "observedAt": "2026-09-02T11:00:00Z", @@ -34,7 +39,10 @@ fn catalog_search_result_preserves_candidate_semantics() { "installability": "not-installable-kind", "displayName": "Example skill", "rawCard": {"secret": "must-not-survive"}, - "source": {"kind": "embedded"}, + "source": { + "kind": "embedded", + "rawCard": {"secret": "must-not-survive"} + }, "provenance": { "authority": "catalog.example", "observedAt": "2026-09-02T11:00:00Z", @@ -65,6 +73,7 @@ fn catalog_search_result_preserves_candidate_semantics() { assert!(matches!(skill.source, CatalogCandidateSource::Embedded(_))); let wire = serde_json::to_value(&result).unwrap(); + assert!(wire.get("rawCard").is_none()); for candidate in wire["candidates"].as_array().unwrap() { let fields = candidate.as_object().unwrap(); for forbidden in ["card", "cardData", "rawCard"] { @@ -73,11 +82,12 @@ fn catalog_search_result_preserves_candidate_semantics() { "candidate leaked {forbidden}" ); } + assert!(candidate["source"].get("rawCard").is_none()); } } #[test] -fn catalog_search_result_preserves_refusals_and_failures() { +fn closed_union_preserves_refusals_and_failures() { let authentication: CatalogSearchResult = serde_json::from_value(serde_json::json!({ "kind": "authentication-required", "reason": "no-credential", @@ -101,3 +111,69 @@ fn catalog_search_result_preserves_refusals_and_failures() { }; assert_eq!(failure.retry_after_seconds, Some(30)); } + +#[test] +fn closed_union_rejects_unknown_and_missing_discriminators() { + let invalid_payloads = [ + serde_json::json!({"kind": "future-result", "rawCard": {"secret": "must-not-survive"}}), + serde_json::json!({"rawCard": {"secret": "must-not-survive"}}), + serde_json::json!({ + "kind": "succeeded", + "searchId": "search-invalid", + "candidates": [{"kind": "future-candidate", "rawCard": {"secret": "must-not-survive"}}], + "truncated": false, + "negotiated": {"runtimeProtocolVersion": 1, "grantedCapabilities": []} + }), + serde_json::json!({ + "kind": "succeeded", + "searchId": "search-invalid", + "candidates": [{"rawCard": {"secret": "must-not-survive"}}], + "truncated": false, + "negotiated": {"runtimeProtocolVersion": 1, "grantedCapabilities": []} + }), + serde_json::json!({ + "kind": "succeeded", + "searchId": "search-invalid", + "candidates": [{ + "kind": "mcp-server", + "handle": OPAQUE_MCP_HANDLE, + "handleExpiresAt": "2026-09-02T12:00:00Z", + "mediaType": "application/mcp-server-card+json", + "installability": "installable", + "displayName": "Example MCP", + "source": {"kind": "future-source", "rawCard": {"secret": "must-not-survive"}}, + "provenance": { + "authority": "catalog.example", + "observedAt": "2026-09-02T11:00:00Z", + "mediaType": "application/mcp-server-card+json" + } + }], + "truncated": false, + "negotiated": {"runtimeProtocolVersion": 1, "grantedCapabilities": []} + }), + serde_json::json!({ + "kind": "succeeded", + "searchId": "search-invalid", + "candidates": [{ + "kind": "mcp-server", + "handle": OPAQUE_MCP_HANDLE, + "handleExpiresAt": "2026-09-02T12:00:00Z", + "mediaType": "application/mcp-server-card+json", + "installability": "installable", + "displayName": "Example MCP", + "source": {"rawCard": {"secret": "must-not-survive"}}, + "provenance": { + "authority": "catalog.example", + "observedAt": "2026-09-02T11:00:00Z", + "mediaType": "application/mcp-server-card+json" + } + }], + "truncated": false, + "negotiated": {"runtimeProtocolVersion": 1, "grantedCapabilities": []} + }), + ]; + + for payload in invalid_payloads { + assert!(serde_json::from_value::(payload).is_err()); + } +} diff --git a/scripts/codegen/catalogue-conformance.ts b/scripts/codegen/catalogue-conformance.ts deleted file mode 100644 index bffd40f848..0000000000 --- a/scripts/codegen/catalogue-conformance.ts +++ /dev/null @@ -1,108 +0,0 @@ -import fs from "fs/promises"; -import path from "path"; - -import { getApiSchemaPath, REPO_ROOT } from "./utils.js"; - -const FORBIDDEN_CANDIDATE_FIELDS = ["card", "cardData", "rawCard"]; - -function assert(condition: unknown, message: string): asserts condition { - if (!condition) throw new Error(`Catalogue schema conformance failed: ${message}`); -} - -function referencedDefinitionNames(schema: { anyOf?: Array<{ $ref?: string }> }): string[] { - return (schema.anyOf ?? []).map((variant) => variant.$ref?.split("/").at(-1) ?? ""); -} - -const schemaPath = await getApiSchemaPath(); -const packageRoot = path.dirname(path.dirname(schemaPath)); -const sdkPackageLock = JSON.parse( - await fs.readFile(path.join(REPO_ROOT, "nodejs/package-lock.json"), "utf8") -) as { packages?: Record }; -const expectedPackageVersion = - sdkPackageLock.packages?.["node_modules/@github/copilot"]?.version; -const javaCodegenPackageJson = JSON.parse( - await fs.readFile(path.join(REPO_ROOT, "java/scripts/codegen/package.json"), "utf8") -) as { dependencies?: Record }; -const javaCodegenPackageVersion = - javaCodegenPackageJson.dependencies?.["@github/copilot"]; -const packageJson = JSON.parse( - await fs.readFile(path.join(packageRoot, "package.json"), "utf8") -) as { version?: string }; -const schema = JSON.parse(await fs.readFile(schemaPath, "utf8")) as { - definitions: Record< - string, - { - anyOf?: Array<{ $ref?: string }>; - properties?: Record; - } - >; - server?: { - catalog?: { - search?: { - rpcMethod?: string; - params?: { $ref?: string }; - result?: { $ref?: string }; - }; - }; - }; -}; - -assert( - expectedPackageVersion !== undefined - && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(expectedPackageVersion), - "nodejs/package-lock.json must pin @github/copilot to an exact version" -); -assert( - packageJson.version === expectedPackageVersion, - `expected @github/copilot ${expectedPackageVersion}, received ${packageJson.version ?? "unknown"}` -); -assert( - javaCodegenPackageVersion === expectedPackageVersion, - `java/scripts/codegen must pin @github/copilot exactly to ${expectedPackageVersion}` -); -assert( - schema.server?.catalog?.search?.rpcMethod === "catalog.search", - "catalog.search is missing" -); -assert( - schema.server.catalog.search.params?.$ref === "#/definitions/CatalogSearchRequest", - "catalog.search request is not typed" -); -assert( - schema.server.catalog.search.result?.$ref === "#/definitions/CatalogSearchResult", - "catalog.search result is not typed" -); - -const candidateVariants = referencedDefinitionNames(schema.definitions.CatalogCandidate); -assert( - candidateVariants.join(",") === "CatalogMcpServerCandidate,CatalogAiSkillCandidate", - `unexpected candidate variants: ${candidateVariants.join(", ")}` -); -const sourceVariants = referencedDefinitionNames(schema.definitions.CatalogCandidateSource); -assert( - sourceVariants.join(",") === "CatalogCandidateSourceUrl,CatalogCandidateSourceEmbedded", - `unexpected candidate source variants: ${sourceVariants.join(", ")}` -); - -for (const name of candidateVariants) { - const properties = schema.definitions[name]?.properties; - assert(properties?.handle?.type === "string", `${name}.handle must remain an opaque string`); - for (const field of FORBIDDEN_CANDIDATE_FIELDS) { - assert(!(field in properties), `${name} exposes forbidden raw card field ${field}`); - } -} - -const resultVariants = new Set( - referencedDefinitionNames(schema.definitions.CatalogSearchResult) -); -for (const name of [ - "CatalogSearchSucceeded", - "CatalogAuthenticationRequiredError", - "CatalogNetworkFailureError", - "CatalogContractViolationError", - "CatalogUnavailableError", -]) { - assert(resultVariants.has(name), `CatalogSearchResult is missing ${name}`); -} - -console.log(`Catalogue schema conformance: @github/copilot ${packageJson.version}`); diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index 00afec6000..e977626fc9 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -12,6 +12,10 @@ import path from "path"; import { fileURLToPath } from "url"; import { promisify } from "util"; import type { JSONSchema7 } from "json-schema"; +import { + analyseDiscriminatedUnionVariants, + type UnknownVariantPolicy, +} from "./schema-unions.js"; import { cloneSchemaForCodegen, fixNullableRequiredRefsInApiSchema, @@ -796,6 +800,7 @@ type PropertyTypeResolver = ( interface DiscriminatedUnionGenerationOptions { sealLeafTypes?: boolean; + unknownVariantPolicy?: UnknownVariantPolicy; } function isBooleanDiscriminator(discriminatorInfo: DiscriminatorInfo): boolean { @@ -916,9 +921,16 @@ function generatePolymorphicClasses( lines.push(...xmlDocCommentWithFallback(description, `Polymorphic base type discriminated by ${escapeXml(discriminatorProperty)}.`, "")); if (experimental) pushExperimentalAttribute(lines); + const unknownDerivedTypeHandling = + options.unknownVariantPolicy === "reject" + ? "FailSerialization" + : "FallBackToBaseType"; lines.push(`[JsonPolymorphic(`); lines.push(` TypeDiscriminatorPropertyName = "${discriminatorProperty}",`); - lines.push(` UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]`); + if (options.unknownVariantPolicy === "reject") { + lines.push(` IgnoreUnrecognizedTypeDiscriminators = false,`); + } + lines.push(` UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.${unknownDerivedTypeHandling})]`); for (const { value } of discriminatorInfo.mapping.values()) { const constValue = String(value); @@ -929,6 +941,9 @@ function generatePolymorphicClasses( lines.push(`public partial class ${renamedBase}`); lines.push(`{`); lines.push(` /// The type discriminator.`); + if (options.unknownVariantPolicy === "reject") { + lines.push(` [JsonRequired]`); + } lines.push(` [JsonPropertyName("${discriminatorProperty}")]`); lines.push(` public virtual string ${toPascalCase(discriminatorProperty)} { get; set; } = string.Empty;`); for (const propName of baseProperties) { @@ -1719,7 +1734,21 @@ function resolveRpcType(schema: JSONSchema7, isRequired: boolean, parentClassNam } return result; }; - const polymorphicCode = generateDiscriminatedUnionClass(baseClassName, discriminatorInfo, variants, rpcKnownTypes, nestedMap, rpcEnumOutput, schema.description, rpcPropertyResolver, isSchemaExperimental(schema) || experimentalRpcTypes.has(baseClassName)); + const polymorphicCode = generateDiscriminatedUnionClass( + baseClassName, + discriminatorInfo, + variants, + rpcKnownTypes, + nestedMap, + rpcEnumOutput, + schema.description, + rpcPropertyResolver, + isSchemaExperimental(schema) || experimentalRpcTypes.has(baseClassName), + { + unknownVariantPolicy: + analyseDiscriminatedUnionVariants(variants)?.unknownVariantPolicy, + } + ); classes.push(polymorphicCode); for (const nested of nestedMap.values()) classes.push(nested); } diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts index 46598c357c..664b0dbde4 100644 --- a/scripts/codegen/go.ts +++ b/scripts/codegen/go.ts @@ -13,6 +13,7 @@ import path from "path"; import { fileURLToPath } from "url"; import { promisify } from "util"; import wordwrap from "wordwrap"; +import { analyseDiscriminatedUnion, type UnknownVariantPolicy } from "./schema-unions.js"; import { addManagedApprovalRequiredToPermissionRequests, cloneSchemaForCodegen, @@ -530,6 +531,7 @@ interface GoDiscriminatorInfo { valueKind: GoDiscriminatorValueKind; mapping: Map; variants: GoDiscriminatedUnionVariant[]; + unknownVariantPolicy: UnknownVariantPolicy; } interface GoRequiredFieldDiscriminatorInfo { @@ -567,6 +569,7 @@ interface GoCodegenCtx { definitions?: DefinitionCollections; wrapComments?: boolean; discriminatedUnionRawVariantSuffix?: string; + applyClosedUnionUnknownVariantPolicy?: boolean; skipDefinitionTypeNames?: Set; encodingBlocks?: Set; unionVariantMarshalers?: Set; @@ -685,7 +688,13 @@ function findGoDiscriminator( } } if (valid && mapping.size > 0 && unionVariants.length === variants.length) { - return { property: propName, valueKind: firstDiscriminatorValues.kind, mapping, variants: unionVariants }; + return { + property: propName, + valueKind: firstDiscriminatorValues.kind, + mapping, + variants: unionVariants, + unknownVariantPolicy: "preserve", + }; } } return null; @@ -1111,6 +1120,7 @@ function registerGoExternalUnionUnmarshalers( definitions: externalDefinitions, wrapComments: ctx.wrapComments, discriminatedUnionRawVariantSuffix: ctx.discriminatedUnionRawVariantSuffix, + applyClosedUnionUnknownVariantPolicy: ctx.applyClosedUnionUnknownVariantPolicy, packageName: ctx.packageName, }; @@ -1817,7 +1827,10 @@ function emitGoFlatDiscriminatedUnion( const unmarshalFuncName = goUnexportedFunctionName("unmarshal", typeName); const rawDataName = `Raw${typeName}${ctx.discriminatedUnionRawVariantSuffix ?? "Data"}`; - const hasRawVariant = discriminator.valueKind === "string" && typeName !== "CatalogCandidate"; + // Preserve existing wrapper types for source compatibility; closed unions never decode into them. + const emitsRawVariant = discriminator.valueKind === "string"; + const acceptsRawVariant = + emitsRawVariant && discriminator.unknownVariantPolicy === "preserve"; const markerName = toGoUnexportedIdentifier(typeName); ctx.discriminatedUnions.set(typeName, { typeName, unmarshalFuncName }); @@ -1894,25 +1907,25 @@ function emitGoFlatDiscriminatedUnion( unmarshalLines.push(`\t\t\treturn &d, nil`); unmarshalLines.push(`\t\t}`); } - if (hasRawVariant) { + if (acceptsRawVariant) { unmarshalLines.push(`\t\treturn &${rawDataName}{Discriminator: ${rawDiscExpr}, Raw: data}, nil`); } else { unmarshalLines.push(`\t\treturn nil, errors.New("data did not match any union variant for ${typeName}")`); } } } - if (hasRawVariant) { + if (acceptsRawVariant) { unmarshalLines.push(`\tdefault:`); unmarshalLines.push(`\t\treturn &${rawDataName}{Discriminator: ${rawDiscExpr}, Raw: data}, nil`); } unmarshalLines.push(`\t}`); - if (!hasRawVariant) { + if (!acceptsRawVariant) { unmarshalLines.push(`\treturn nil, errors.New("data did not match any union variant for ${typeName}")`); } unmarshalLines.push(`}`); pushGoEncodingBlock(unmarshalLines, ctx); - if (hasRawVariant) { + if (emitsRawVariant) { lines.push(`type ${rawDataName} struct {`); lines.push(`\tDiscriminator ${discGoType}`); lines.push(`\tRaw json.RawMessage`); @@ -2800,6 +2813,12 @@ function planGoUnion(typeName: string, schema: JSONSchema7, ctx: GoCodegenCtx, i const description = (schema as JSONSchema7).description; const discriminator = findGoDiscriminator(members, ctx, typeName); if (discriminator) { + if (ctx.applyClosedUnionUnknownVariantPolicy) { + discriminator.unknownVariantPolicy = + analyseDiscriminatedUnion(schema, (variant) => + resolveGoUnionMember(variant, ctx.definitions) + )?.unknownVariantPolicy ?? "preserve"; + } return { kind: "discriminated", typeName, schema, description, discriminator }; } @@ -3099,6 +3118,7 @@ function generateGoRpcTypeCode(definitions: Record, definit discriminatedUnions: new Map(), generatedNames: new Set(), definitions: definitionCollections, + applyClosedUnionUnknownVariantPolicy: true, packageName: "rpc", }; ctx.skipDefinitionTypeNames = collectGoDiscriminatedUnionVariantDefinitionTypeNames(definitions, ctx); diff --git a/scripts/codegen/package.json b/scripts/codegen/package.json index ed4092891f..fb47410338 100644 --- a/scripts/codegen/package.json +++ b/scripts/codegen/package.json @@ -3,7 +3,7 @@ "private": true, "type": "module", "scripts": { - "conformance": "tsx catalogue-conformance.ts", + "conformance": "tsx schema-conformance.ts", "generate": "npm run conformance && tsx typescript.ts && tsx csharp.ts && tsx python.ts && tsx go.ts && tsx rust.ts", "generate:ts": "tsx typescript.ts", "generate:csharp": "tsx csharp.ts", diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index f14e578f87..94b92e318b 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -508,15 +508,9 @@ function postProcessRefBasedDiscriminatedUnionsForPython( for (const m of actualDispatch) { dispatcherLines.push(` case ${pyDiscriminatorValueExpr(m.value)}: return ${m.typeName}.from_dict(obj)`); } - if (actualAliasName.startsWith("Catalog")) { - dispatcherLines.push( - ` raise ValueError(f"Unknown ${actualAliasName} ${union.discriminatorProp}: {kind!r}")` - ); - } else { - dispatcherLines.push( - ` case _: raise ValueError(f"Unknown ${actualAliasName} ${union.discriminatorProp}: {kind!r}")` - ); - } + dispatcherLines.push( + ` raise ValueError(f"Unknown ${actualAliasName} ${union.discriminatorProp}: {kind!r}")` + ); code = `${code.trimEnd()}\n\n\n${aliasLine}\n\n\n${dispatcherLines.join("\n")}\n`; } @@ -1781,15 +1775,9 @@ function tryEmitPyRefBasedDiscriminatedUnion( ` case ${pyDiscriminatorValueExpr(m.value)}: return ${m.typeName}.from_dict(obj)` ); } - if (aliasName.startsWith("Catalog")) { - lines.push( - ` raise ValueError(f"Unknown ${aliasName} ${discriminator.property}: {kind!r}")` - ); - } else { - lines.push( - ` case _: raise ValueError(f"Unknown ${aliasName} ${discriminator.property}: {kind!r}")` - ); - } + lines.push( + ` case _: raise ValueError(f"Unknown ${aliasName} ${discriminator.property}: {kind!r}")` + ); ctx.classes.push(lines.join("\n")); } diff --git a/scripts/codegen/schema-conformance.ts b/scripts/codegen/schema-conformance.ts new file mode 100644 index 0000000000..6f2df8e6d1 --- /dev/null +++ b/scripts/codegen/schema-conformance.ts @@ -0,0 +1,138 @@ +import fs from "fs/promises"; +import type { JSONSchema7 } from "json-schema"; +import path from "path"; + +import { COPILOT_CLI_VERSION } from "../../nodejs/src/cliVersion.js"; +import { analyseDiscriminatedUnion } from "./schema-unions.js"; +import { getApiSchemaPath, REPO_ROOT } from "./utils.js"; + +const FORBIDDEN_CANDIDATE_FIELDS = ["card", "cardData", "rawCard"]; + +function assert(condition: unknown, message: string): asserts condition { + if (!condition) throw new Error(`Schema conformance failed: ${message}`); +} + +function referencedDefinitionNames(schema: JSONSchema7): string[] { + return (((schema.anyOf ?? schema.oneOf) as JSONSchema7[]) ?? []).map( + (variant) => variant.$ref?.split("/").at(-1) ?? "", + ); +} + +const schemaPath = await getApiSchemaPath(); +const packageRoot = path.dirname(path.dirname(schemaPath)); +const javaCodegenPackageJson = JSON.parse( + await fs.readFile( + path.join(REPO_ROOT, "java/scripts/codegen/package.json"), + "utf8", + ), +) as { dependencies?: Record }; +const javaCodegenPackageVersion = + javaCodegenPackageJson.dependencies?.["@github/copilot"]; +const packageJson = JSON.parse( + await fs.readFile(path.join(packageRoot, "package.json"), "utf8"), +) as { version?: string }; +const schema = JSON.parse(await fs.readFile(schemaPath, "utf8")) as { + definitions: Record; + server?: { + catalog?: { + search?: { + rpcMethod?: string; + params?: { $ref?: string }; + result?: { $ref?: string }; + }; + }; + }; +}; + +assert( + /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(COPILOT_CLI_VERSION), + "nodejs/src/cliVersion.ts must pin an exact Copilot CLI version", +); +assert( + packageJson.version === COPILOT_CLI_VERSION, + `expected Copilot CLI ${COPILOT_CLI_VERSION}, received ${packageJson.version ?? "unknown"}`, +); +assert( + javaCodegenPackageVersion === COPILOT_CLI_VERSION, + `java/scripts/codegen must pin @github/copilot exactly to ${COPILOT_CLI_VERSION}`, +); +assert( + schema.server?.catalog?.search?.rpcMethod === "catalog.search", + "catalog.search is missing", +); +assert( + schema.server.catalog.search.params?.$ref === + "#/definitions/CatalogSearchRequest", + "catalog.search request is not typed", +); +assert( + schema.server.catalog.search.result?.$ref === + "#/definitions/CatalogSearchResult", + "catalog.search result is not typed", +); + +const candidateVariants = referencedDefinitionNames( + schema.definitions.CatalogCandidate, +); +assert( + candidateVariants.join(",") === + "CatalogMcpServerCandidate,CatalogAiSkillCandidate", + `unexpected candidate variants: ${candidateVariants.join(", ")}`, +); +const sourceVariants = referencedDefinitionNames( + schema.definitions.CatalogCandidateSource, +); +assert( + sourceVariants.join(",") === + "CatalogCandidateSourceUrl,CatalogCandidateSourceEmbedded", + `unexpected candidate source variants: ${sourceVariants.join(", ")}`, +); + +const resolveVariant = (variant: JSONSchema7): JSONSchema7 | undefined => { + const name = variant.$ref?.match(/^#\/definitions\/([^/]+)$/)?.[1]; + return name ? schema.definitions[name] : variant; +}; +for (const name of [ + "CatalogCandidate", + "CatalogCandidateSource", + "CatalogSearchResult", +]) { + const analysis = analyseDiscriminatedUnion( + schema.definitions[name], + resolveVariant, + ); + assert(analysis !== undefined, `${name} must remain a discriminated union`); + assert( + analysis.unknownVariantPolicy === "reject", + `${name} variants must remain closed to unknown payload fields`, + ); +} + +for (const name of candidateVariants) { + const properties = schema.definitions[name]?.properties; + assert( + properties?.handle?.type === "string", + `${name}.handle must remain an opaque string`, + ); + for (const field of FORBIDDEN_CANDIDATE_FIELDS) { + assert( + !(field in properties), + `${name} exposes forbidden raw card field ${field}`, + ); + } +} + +const resultVariants = new Set( + referencedDefinitionNames(schema.definitions.CatalogSearchResult), +); +for (const name of [ + "CatalogSearchSucceeded", + "CatalogAuthenticationRequiredError", + "CatalogNetworkFailureError", + "CatalogContractViolationError", + "CatalogUnavailableError", +]) { + assert(resultVariants.has(name), `CatalogSearchResult is missing ${name}`); +} + +console.log(`Schema conformance: Copilot CLI ${packageJson.version}`); diff --git a/scripts/codegen/schema-unions.ts b/scripts/codegen/schema-unions.ts new file mode 100644 index 0000000000..4604e1c39a --- /dev/null +++ b/scripts/codegen/schema-unions.ts @@ -0,0 +1,145 @@ +import type { JSONSchema7 } from "json-schema"; + +export type SchemaDiscriminatorValue = string | number | boolean | null; +export type UnknownVariantPolicy = "preserve" | "reject"; + +export interface SchemaDiscriminatedUnionVariant { + source: JSONSchema7; + schema: JSONSchema7; + discriminatorValues: SchemaDiscriminatorValue[]; +} + +export interface SchemaDiscriminatorMapping { + value: SchemaDiscriminatorValue; + variants: SchemaDiscriminatedUnionVariant[]; +} + +export interface SchemaDiscriminatedUnion { + property: string; + variants: SchemaDiscriminatedUnionVariant[]; + mapping: SchemaDiscriminatorMapping[]; + unknownVariantPolicy: UnknownVariantPolicy; +} + +export type SchemaVariantResolver = ( + schema: JSONSchema7, +) => JSONSchema7 | undefined; + +function isDiscriminatorValue( + value: unknown, +): value is SchemaDiscriminatorValue { + return ( + value === null || ["string", "number", "boolean"].includes(typeof value) + ); +} + +function discriminatorValues( + schema: JSONSchema7, +): SchemaDiscriminatorValue[] | undefined { + if (isDiscriminatorValue(schema.const)) { + return [schema.const]; + } + if ( + Array.isArray(schema.enum) && + schema.enum.length > 0 && + schema.enum.every(isDiscriminatorValue) + ) { + return [...new Set(schema.enum)]; + } + return undefined; +} + +export function schemaDiscriminatorValueKey( + value: SchemaDiscriminatorValue, +): string { + return `${typeof value}:${JSON.stringify(value)}`; +} + +/** + * Derive discriminator and unknown-value handling from JSON Schema alone. + * + * A union is closed when every resolved variant rejects additional properties. + * Language emitters use that policy to reject unknown or missing discriminators + * while retaining their idiomatic generated representation. + */ +export function analyseDiscriminatedUnionVariants( + sources: JSONSchema7[], + resolveVariant: SchemaVariantResolver = (schema) => schema, +): SchemaDiscriminatedUnion | undefined { + if (sources.length < 2) return undefined; + + const resolved = sources.map((source) => resolveVariant(source)); + if (resolved.some((schema) => !schema?.properties)) return undefined; + + const schemas = resolved as JSONSchema7[]; + for (const property of Object.keys(schemas[0].properties ?? {}).sort()) { + const variants: SchemaDiscriminatedUnionVariant[] = []; + const mapping = new Map< + string, + { + value: SchemaDiscriminatorValue; + variants: SchemaDiscriminatedUnionVariant[]; + } + >(); + let valid = true; + + for (let index = 0; index < schemas.length; index++) { + const schema = schemas[index]; + const propertySchema = schema.properties?.[property]; + if ( + !(schema.required ?? []).includes(property) || + !propertySchema || + typeof propertySchema !== "object" + ) { + valid = false; + break; + } + + const values = discriminatorValues(propertySchema as JSONSchema7); + if (!values) { + valid = false; + break; + } + + const variant = { + source: sources[index], + schema, + discriminatorValues: values, + }; + variants.push(variant); + for (const value of values) { + const key = schemaDiscriminatorValueKey(value); + const entry = mapping.get(key) ?? { value, variants: [] }; + entry.variants.push(variant); + mapping.set(key, entry); + } + } + + if (valid && variants.length === schemas.length && mapping.size > 0) { + return { + property, + variants, + mapping: [...mapping.values()], + unknownVariantPolicy: schemas.every( + (schema) => schema.additionalProperties === false, + ) + ? "reject" + : "preserve", + }; + } + } + + return undefined; +} + +export function analyseDiscriminatedUnion( + schema: JSONSchema7, + resolveVariant: SchemaVariantResolver = (variant) => variant, +): SchemaDiscriminatedUnion | undefined { + const variants = schema.anyOf ?? schema.oneOf; + if (!Array.isArray(variants)) return undefined; + return analyseDiscriminatedUnionVariants( + variants as JSONSchema7[], + resolveVariant, + ); +} diff --git a/scripts/codegen/typescript.ts b/scripts/codegen/typescript.ts index d83f9b2e05..f5dbfe37f6 100644 --- a/scripts/codegen/typescript.ts +++ b/scripts/codegen/typescript.ts @@ -11,6 +11,10 @@ import type { JSONSchema7 } from "json-schema"; import { compile } from "json-schema-to-typescript"; import path from "path"; import { fileURLToPath } from "url"; +import { + analyseDiscriminatedUnion, + schemaDiscriminatorValueKey, +} from "./schema-unions.js"; import { getApiSchemaPath, fixNullableRequiredRefsInApiSchema, @@ -606,6 +610,200 @@ async function generateSessionEvents(schemaPath?: string): Promise { // ── RPC Types ─────────────────────────────────────────────────────────────── let rpcDefinitions: DefinitionCollections = { definitions: {}, $defs: {} }; +let rpcResultProjections = new Map(); +let rpcResultProjectionDefinitions = new Map(); + +export type RpcResultProjection = + | { kind: "ref"; name: string } + | { kind: "array"; items: RpcResultProjection | null } + | { + kind: "object"; + closed: boolean; + properties: Record; + } + | { + kind: "union"; + discriminator: string; + variants: Record; + }; + +interface RpcResultProjectionBuild { + projection: RpcResultProjection | null; + containsClosedUnion: boolean; +} + +export interface RpcResultProjectionBundle { + root: RpcResultProjection; + definitions: Record; +} + +function localDefinitionName(ref: string): string | undefined { + return ref.match(/^#\/(?:definitions|\$defs)\/([^/]+)$/)?.[1]; +} + +function buildRpcResultProjection( + schema: JSONSchema7, + definitions: DefinitionCollections, + projectionDefinitions: Map, + resolvingReferences = new Set() +): RpcResultProjectionBuild { + if (schema.$ref) { + const definitionName = localDefinitionName(schema.$ref); + if (!definitionName) { + return { projection: null, containsClosedUnion: false }; + } + const cached = projectionDefinitions.get(definitionName); + if (cached) { + return cached.projection + ? { + projection: { kind: "ref", name: definitionName }, + containsClosedUnion: cached.containsClosedUnion, + } + : cached; + } + if (resolvingReferences.has(definitionName)) { + return { + projection: { kind: "ref", name: definitionName }, + containsClosedUnion: false, + }; + } + const resolved = resolveSchema(schema, definitions); + if (!resolved) return { projection: null, containsClosedUnion: false }; + const nestedReferences = new Set(resolvingReferences); + nestedReferences.add(definitionName); + const built = buildRpcResultProjection( + resolved, + definitions, + projectionDefinitions, + nestedReferences + ); + projectionDefinitions.set(definitionName, built); + return built.projection + ? { + projection: { kind: "ref", name: definitionName }, + containsClosedUnion: built.containsClosedUnion, + } + : built; + } + + const discriminatedUnion = analyseDiscriminatedUnion( + schema, + (variant) => + resolveObjectSchema(variant, definitions) ?? + resolveSchema(variant, definitions) ?? + variant + ); + if ( + discriminatedUnion?.unknownVariantPolicy === "reject" && + discriminatedUnion.mapping.every((entry) => entry.variants.length === 1) + ) { + const variants: Record = {}; + for (const entry of discriminatedUnion.mapping) { + const variantProjection = buildRpcResultProjection( + entry.variants[0].source, + definitions, + projectionDefinitions, + new Set(resolvingReferences) + ).projection; + if (!variantProjection) { + return { projection: null, containsClosedUnion: false }; + } + variants[schemaDiscriminatorValueKey(entry.value)] = variantProjection; + } + return { + projection: { + kind: "union", + discriminator: discriminatedUnion.property, + variants, + }, + containsClosedUnion: true, + }; + } + + const unionMembers = schema.anyOf ?? schema.oneOf; + if (Array.isArray(unionMembers)) { + const nonNullMembers = (unionMembers as JSONSchema7[]).filter( + (member) => member.type !== "null" + ); + if (nonNullMembers.length === 1) { + return buildRpcResultProjection( + nonNullMembers[0], + definitions, + projectionDefinitions, + resolvingReferences + ); + } + } + + if (schema.type === "array" && schema.items && !Array.isArray(schema.items)) { + const item = buildRpcResultProjection( + schema.items as JSONSchema7, + definitions, + projectionDefinitions, + new Set(resolvingReferences) + ); + return { + projection: + item.projection || item.containsClosedUnion + ? { kind: "array", items: item.projection } + : null, + containsClosedUnion: item.containsClosedUnion, + }; + } + + const objectSchema = resolveObjectSchema(schema, definitions); + if ( + objectSchema && + (objectSchema.type === "object" || objectSchema.properties) && + !objectSchema.anyOf && + !objectSchema.oneOf + ) { + const properties: Record = {}; + let containsClosedUnion = false; + for (const [name, property] of Object.entries(objectSchema.properties ?? {})) { + if (!property || typeof property !== "object") { + properties[name] = null; + continue; + } + const child = buildRpcResultProjection( + property as JSONSchema7, + definitions, + projectionDefinitions, + new Set(resolvingReferences) + ); + properties[name] = child.projection; + containsClosedUnion ||= child.containsClosedUnion; + } + const closed = objectSchema.additionalProperties === false; + return { + projection: + closed || Object.values(properties).some((projection) => projection !== null) + ? { kind: "object", closed, properties } + : null, + containsClosedUnion, + }; + } + + return { projection: null, containsClosedUnion: false }; +} + +export function createRpcResultProjectionBundle( + schema: JSONSchema7 | null | undefined, + definitions: DefinitionCollections +): RpcResultProjectionBundle | undefined { + if (!schema) return undefined; + const projectionDefinitions = new Map(); + const result = buildRpcResultProjection(schema, definitions, projectionDefinitions); + if (!result.containsClosedUnion || !result.projection) return undefined; + return { + root: result.projection, + definitions: Object.fromEntries( + [...projectionDefinitions] + .filter(([, built]) => built.projection) + .map(([name, built]) => [name, built.projection!]) + ), + }; +} function withRootTitle(schema: JSONSchema7, title: string): JSONSchema7 { return { ...schema, title }; @@ -741,6 +939,27 @@ 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); + rpcResultProjections = new Map(); + rpcResultProjectionDefinitions = new Map(); + for (const method of rpcMethods) { + const resultSchema = getMethodResultSchema(method); + const resultUnion = resultSchema + ? analyseDiscriminatedUnion( + resultSchema, + (variant) => + resolveObjectSchema(variant, rpcDefinitions) ?? + resolveSchema(variant, rpcDefinitions) ?? + variant + ) + : undefined; + if (resultUnion?.unknownVariantPolicy !== "reject") continue; + const projection = createRpcResultProjectionBundle(method.result, rpcDefinitions); + if (!projection) continue; + rpcResultProjections.set(method.rpcMethod, projection.root); + for (const [name, definition] of Object.entries(projection.definitions)) { + rpcResultProjectionDefinitions.set(name, definition); + } + } const combinedSchema = withSharedDefinitions( { $schema: "http://json-schema.org/draft-07/schema#", @@ -884,21 +1103,81 @@ function hasInternalMethods(node: Record): boolean { return false; } + if (rpcResultProjections.size > 0) { + lines.push(`type RpcResultProjection =`); + lines.push(` | { kind: "ref"; name: string }`); + lines.push(` | { kind: "array"; items: RpcResultProjection | null }`); + lines.push( + ` | { kind: "object"; closed: boolean; properties: Record }` + ); + lines.push( + ` | { kind: "union"; discriminator: string; variants: Record };` + ); + lines.push(""); + lines.push( + `const RPC_RESULT_PROJECTIONS: Record = ${JSON.stringify(Object.fromEntries(rpcResultProjections), null, 4)};` + ); + lines.push(""); + lines.push( + `const RPC_RESULT_PROJECTION_DEFINITIONS: Record = ${JSON.stringify(Object.fromEntries(rpcResultProjectionDefinitions), null, 4)};` + ); + lines.push(""); + lines.push( + `function projectRpcResult(value: unknown, projection: RpcResultProjection, path = "$"): unknown {` + ); + lines.push(` if (projection.kind === "ref") {`); + lines.push( + ` const definition = RPC_RESULT_PROJECTION_DEFINITIONS[projection.name];` + ); + lines.push( + ` if (!definition) throw new TypeError(\`Missing RPC result projection for \${projection.name}\`);` + ); + lines.push(` return projectRpcResult(value, definition, path);`); + lines.push(` }`); + lines.push(` if (projection.kind === "array") {`); + lines.push( + ` if (!Array.isArray(value)) throw new TypeError(\`Invalid RPC result at \${path}: expected an array\`);` + ); + lines.push( + ` return projection.items ? value.map((item, index) => projectRpcResult(item, projection.items!, \`\${path}[\${index}]\`)) : value;` + ); + lines.push(` }`); + lines.push( + ` if (value === null || typeof value !== "object" || Array.isArray(value)) {` + ); + lines.push( + ` throw new TypeError(\`Invalid RPC result at \${path}: expected an object\`);` + ); + lines.push(` }`); + lines.push(` const record = value as Record;`); + lines.push(` if (projection.kind === "union") {`); + lines.push(` const discriminator = record[projection.discriminator];`); + lines.push( + ` const key = \`\${typeof discriminator}:\${JSON.stringify(discriminator)}\`;` + ); + lines.push(` const variant = projection.variants[key];`); + lines.push(` if (!variant) {`); + lines.push( + ` throw new TypeError(\`Invalid RPC result at \${path}: unknown or missing \${projection.discriminator} discriminator\`);` + ); + lines.push(` }`); + lines.push(` return projectRpcResult(value, variant, path);`); + lines.push(` }`); + lines.push( + ` const result: Record = projection.closed ? {} : { ...record };` + ); + lines.push(` for (const [name, child] of Object.entries(projection.properties)) {`); + lines.push(` if (!Object.hasOwn(record, name)) continue;`); + lines.push( + ` result[name] = child ? projectRpcResult(record[name], child, \`\${path}.\${name}\`) : record[name];` + ); + lines.push(` }`); + lines.push(` return result;`); + lines.push(`}`); + lines.push(""); + } + if (schema.server) { - if (collectRpcMethods(schema.server).some((method) => method.rpcMethod === "catalog.search")) { - lines.push(`const FORBIDDEN_CATALOG_RESPONSE_FIELDS = new Set(["card", "cardData", "rawCard"]);`); - lines.push(""); - lines.push(`function sanitizeCatalogSearchResult(value: unknown): unknown {`); - lines.push(` if (Array.isArray(value)) return value.map(sanitizeCatalogSearchResult);`); - lines.push(` if (value === null || typeof value !== "object") return value;`); - lines.push(` return Object.fromEntries(`); - lines.push(` Object.entries(value)`); - lines.push(` .filter(([key]) => !FORBIDDEN_CATALOG_RESPONSE_FIELDS.has(key))`); - lines.push(` .map(([key, child]) => [key, sanitizeCatalogSearchResult(child)]),`); - lines.push(` );`); - lines.push(`}`); - lines.push(""); - } lines.push(`/** Create typed server-scoped RPC methods (no session required). */`); lines.push(`export function createServerRpc(connection: MessageConnection) {`); lines.push(` return {`); @@ -1030,9 +1309,11 @@ function emitGroup( includeExperimental: (value as RpcMethod).stability === "experimental" && !parentExperimental, }); lines.push(`${indent}${key}: async (${sigParams.join(", ")}): Promise<${resultType}> =>`); - if (rpcMethod === "catalog.search") { - lines.push(`${indent} sanitizeCatalogSearchResult(`); + const resultProjection = rpcResultProjections.get(rpcMethod); + if (resultProjection) { + lines.push(`${indent} projectRpcResult(`); lines.push(`${indent} await connection.sendRequest("${rpcMethod}", ${bodyArg}),`); + lines.push(`${indent} RPC_RESULT_PROJECTIONS[${JSON.stringify(rpcMethod)}],`); lines.push(`${indent} ) as ${resultType},`); } else { lines.push(`${indent} connection.sendRequest("${rpcMethod}", ${bodyArg}),`); From c07135e1242ede39c9d957a2bf1432cfa4cf6d2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6khan=20Arkan?= Date: Fri, 4 Sep 2026 15:10:19 +0300 Subject: [PATCH 4/6] Limit closed union codegen scope --- dotnet/src/Generated/Rpc.cs | 88 +- go/rpc/zrpc_encoding.go | 72 +- java/scripts/codegen/java.ts | 13 +- .../copilot/generated/rpc/McpInstallPlan.java | 2 +- .../generated/rpc/McpPlanEnumValueType.java | 33 - .../rpc/McpPlanPackageTransport.java | 33 - .../generated/rpc/McpPlanRemoteTransport.java | 37 - .../generated/rpc/McpPlanRequiredValue.java | 35 - .../rpc/McpPlanRequiredValueEnum.java | 94 -- .../rpc/McpPlanRequiredValueScalar.java | 86 -- .../generated/rpc/McpPlanScalarValueType.java | 39 - .../rpc/McpPlanSecretPlaceholder.java | 31 - .../generated/rpc/McpPlanTransportChoice.java | 35 - .../rpc/McpPlanTransportChoicePackage.java | 73 - .../rpc/McpPlanTransportChoiceRemote.java | 66 - .../generated/rpc/McpPlanValueCategory.java | 41 - nodejs/src/generated/rpc.ts | 1226 ++--------------- .../test/discriminated-union-codegen.test.ts | 35 +- python/copilot/generated/rpc.py | 40 +- scripts/codegen/csharp.ts | 27 +- scripts/codegen/go.ts | 34 +- scripts/codegen/python.ts | 27 +- scripts/codegen/schema-conformance.ts | 42 +- scripts/codegen/schema-unions.ts | 104 ++ scripts/codegen/typescript.ts | 17 +- 25 files changed, 487 insertions(+), 1843 deletions(-) delete mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanEnumValueType.java delete mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanPackageTransport.java delete mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanRemoteTransport.java delete mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanRequiredValue.java delete mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanRequiredValueEnum.java delete mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanRequiredValueScalar.java delete mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanScalarValueType.java delete mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanSecretPlaceholder.java delete mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanTransportChoice.java delete mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanTransportChoicePackage.java delete mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanTransportChoiceRemote.java delete mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanValueCategory.java diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index d18bb91100..22fb609a95 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -625,8 +625,7 @@ internal sealed class AccountGetQuotaRequest [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "type", - IgnoreUnrecognizedTypeDiscriminators = false, - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonDerivedType(typeof(AuthInfoHmac), "hmac")] [JsonDerivedType(typeof(AuthInfoEnv), "env")] [JsonDerivedType(typeof(AuthInfoToken), "token")] @@ -638,7 +637,6 @@ internal sealed class AccountGetQuotaRequest public partial class AuthInfo { /// The type discriminator. - [JsonRequired] [JsonPropertyName("type")] public virtual string Type { get; set; } = string.Empty; } @@ -1315,8 +1313,7 @@ internal sealed class McpDiscoverRequest [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", - IgnoreUnrecognizedTypeDiscriminators = false, - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonDerivedType(typeof(McpPlanInstallResultPlanned), "planned")] [JsonDerivedType(typeof(McpPlanInstallResultNegotiationRefused), "negotiation-refused")] [JsonDerivedType(typeof(McpPlanInstallResultHandleRejected), "handle-rejected")] @@ -1333,7 +1330,6 @@ internal sealed class McpDiscoverRequest public partial class McpPlanInstallResult { /// The type discriminator. - [JsonRequired] [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } @@ -1483,14 +1479,12 @@ public sealed class McpPlanTarget [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "installMethod", - IgnoreUnrecognizedTypeDiscriminators = false, - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonDerivedType(typeof(McpPlanTransportChoicePackage), "package")] [JsonDerivedType(typeof(McpPlanTransportChoiceRemote), "remote")] public partial class McpPlanTransportChoice { /// The type discriminator. - [JsonRequired] [JsonPropertyName("installMethod")] public virtual string InstallMethod { get; set; } = string.Empty; } @@ -1501,14 +1495,12 @@ public partial class McpPlanTransportChoice [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", - IgnoreUnrecognizedTypeDiscriminators = false, - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonDerivedType(typeof(McpPlanRequiredValueScalar), "scalar")] [JsonDerivedType(typeof(McpPlanRequiredValueEnum), "enum")] public partial class McpPlanRequiredValue { /// The type discriminator. - [JsonRequired] [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } @@ -2082,14 +2074,12 @@ public sealed class CatalogClientContract [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", - IgnoreUnrecognizedTypeDiscriminators = false, - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonDerivedType(typeof(McpPlanInstallSourceCandidate), "candidate")] [JsonDerivedType(typeof(McpPlanInstallSourceCard), "card")] public partial class McpPlanInstallSource { /// The type discriminator. - [JsonRequired] [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } @@ -2124,14 +2114,12 @@ public partial class McpPlanInstallSourceCandidate : McpPlanInstallSource [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", - IgnoreUnrecognizedTypeDiscriminators = false, - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonDerivedType(typeof(McpServerCardReferenceUrl), "url")] [JsonDerivedType(typeof(McpServerCardReferenceEmbedded), "embedded")] public partial class McpServerCardReference { /// The type discriminator. - [JsonRequired] [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } @@ -4776,8 +4764,7 @@ internal sealed class SessionsGetBoardEntryCountRequest [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "state", - IgnoreUnrecognizedTypeDiscriminators = false, - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonDerivedType(typeof(RemoteControlStatusOff), "off")] [JsonDerivedType(typeof(RemoteControlStatusConnecting), "connecting")] [JsonDerivedType(typeof(RemoteControlStatusActive), "active")] @@ -4785,7 +4772,6 @@ internal sealed class SessionsGetBoardEntryCountRequest public partial class RemoteControlStatus { /// The type discriminator. - [JsonRequired] [JsonPropertyName("state")] public virtual string State { get; set; } = string.Empty; } @@ -5027,8 +5013,7 @@ internal sealed class ConfigureSessionExtensionsParams [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", - IgnoreUnrecognizedTypeDiscriminators = false, - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonDerivedType(typeof(AgentRegistrySpawnResultSpawned), "spawned")] [JsonDerivedType(typeof(AgentRegistrySpawnResultSpawnError), "spawn-error")] [JsonDerivedType(typeof(AgentRegistrySpawnResultRegistryTimeout), "registry-timeout")] @@ -5036,7 +5021,6 @@ internal sealed class ConfigureSessionExtensionsParams public partial class AgentRegistrySpawnResult { /// The type discriminator. - [JsonRequired] [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } @@ -5646,8 +5630,7 @@ public sealed class SessionSetCredentialsResult [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "type", - IgnoreUnrecognizedTypeDiscriminators = false, - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonDerivedType(typeof(SettableAuthInfoHmac), "hmac")] [JsonDerivedType(typeof(SettableAuthInfoEnv), "env")] [JsonDerivedType(typeof(SettableAuthInfoToken), "token")] @@ -5658,7 +5641,6 @@ public sealed class SessionSetCredentialsResult public partial class SettableAuthInfo { /// The type discriminator. - [JsonRequired] [JsonPropertyName("type")] public virtual string Type { get; set; } = string.Empty; } @@ -6074,14 +6056,12 @@ public sealed class DebugCollectLogsEntry [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", - IgnoreUnrecognizedTypeDiscriminators = false, - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonDerivedType(typeof(DebugCollectLogsDestinationArchive), "archive")] [JsonDerivedType(typeof(DebugCollectLogsDestinationDirectory), "directory")] public partial class DebugCollectLogsDestination { /// The type discriminator. - [JsonRequired] [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } @@ -6412,8 +6392,7 @@ internal sealed class CanvasProviderUnregisterRequest [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "type", - IgnoreUnrecognizedTypeDiscriminators = false, - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonDerivedType(typeof(FactoryRunFailureFactoryLimitReached), "factory_limit_reached")] [JsonDerivedType(typeof(FactoryRunFailureFactoryResumeDeclined), "factory_resume_declined")] [JsonDerivedType(typeof(FactoryRunFailureFactoryDurableFailure), "factory_durable_failure")] @@ -6422,7 +6401,6 @@ internal sealed class CanvasProviderUnregisterRequest public partial class FactoryRunFailure { /// The type discriminator. - [JsonRequired] [JsonPropertyName("type")] public virtual string Type { get; set; } = string.Empty; } @@ -8942,15 +8920,13 @@ internal sealed class TasksStartAgentRequest [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "type", - IgnoreUnrecognizedTypeDiscriminators = false, - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonDerivedType(typeof(TaskInfoAgent), "agent")] [JsonDerivedType(typeof(TaskInfoClient), "client")] [JsonDerivedType(typeof(TaskInfoShell), "shell")] public partial class TaskInfo { /// The type discriminator. - [JsonRequired] [JsonPropertyName("type")] public virtual string Type { get; set; } = string.Empty; } @@ -9434,8 +9410,7 @@ public sealed class TasksUpdateResult [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", - IgnoreUnrecognizedTypeDiscriminators = false, - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonDerivedType(typeof(TaskClientUpdateProgress), "progress")] [JsonDerivedType(typeof(TaskClientUpdateCompleted), "completed")] [JsonDerivedType(typeof(TaskClientUpdateFailed), "failed")] @@ -9443,7 +9418,6 @@ public sealed class TasksUpdateResult public partial class TaskClientUpdate { /// The type discriminator. - [JsonRequired] [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } @@ -9599,15 +9573,13 @@ internal sealed class SessionTasksWaitForPendingRequest [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "type", - IgnoreUnrecognizedTypeDiscriminators = false, - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonDerivedType(typeof(TaskProgressAgent), "agent")] [JsonDerivedType(typeof(TaskProgressClient), "client")] [JsonDerivedType(typeof(TaskProgressShell), "shell")] public partial class TaskProgress { /// The type discriminator. - [JsonRequired] [JsonPropertyName("type")] public virtual string Type { get; set; } = string.Empty; } @@ -12516,8 +12488,7 @@ internal sealed class SessionExtensionsReloadRequest [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "type", - IgnoreUnrecognizedTypeDiscriminators = false, - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonDerivedType(typeof(PushAttachmentFile), "file")] [JsonDerivedType(typeof(PushAttachmentDirectory), "directory")] [JsonDerivedType(typeof(PushAttachmentSelection), "selection")] @@ -12536,7 +12507,6 @@ internal sealed class SessionExtensionsReloadRequest public partial class PushAttachment { /// The type discriminator. - [JsonRequired] [JsonPropertyName("type")] public virtual string Type { get; set; } = string.Empty; } @@ -13257,8 +13227,7 @@ public sealed class ExternalToolTextResultForLlmBinaryResultsForLlm [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "type", - IgnoreUnrecognizedTypeDiscriminators = false, - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonDerivedType(typeof(ExternalToolTextResultForLlmContentText), "text")] [JsonDerivedType(typeof(ExternalToolTextResultForLlmContentTerminal), "terminal")] [JsonDerivedType(typeof(ExternalToolTextResultForLlmContentShellExit), "shell_exit")] @@ -13269,7 +13238,6 @@ public sealed class ExternalToolTextResultForLlmBinaryResultsForLlm public partial class ExternalToolTextResultForLlmContent { /// The type discriminator. - [JsonRequired] [JsonPropertyName("type")] public virtual string Type { get; set; } = string.Empty; } @@ -13862,8 +13830,7 @@ internal sealed class SessionCommandsListRequestWithSession [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", - IgnoreUnrecognizedTypeDiscriminators = false, - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonDerivedType(typeof(SlashCommandInvocationResultText), "text")] [JsonDerivedType(typeof(SlashCommandInvocationResultAgentPrompt), "agent-prompt")] [JsonDerivedType(typeof(SlashCommandInvocationResultCompleted), "completed")] @@ -13875,7 +13842,6 @@ internal sealed class SessionCommandsListRequestWithSession public partial class SlashCommandInvocationResult { /// The type discriminator. - [JsonRequired] [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } @@ -14872,8 +14838,7 @@ public sealed class PermissionDecisionContext [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", - IgnoreUnrecognizedTypeDiscriminators = false, - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonDerivedType(typeof(PermissionDecisionApproveOnce), "approve-once")] [JsonDerivedType(typeof(PermissionDecisionApproveForSession), "approve-for-session")] [JsonDerivedType(typeof(PermissionDecisionApproveForLocation), "approve-for-location")] @@ -14892,7 +14857,6 @@ public sealed class PermissionDecisionContext public partial class PermissionDecision { /// The type discriminator. - [JsonRequired] [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } @@ -14918,8 +14882,7 @@ public partial class PermissionDecisionApproveOnce : PermissionDecision [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", - IgnoreUnrecognizedTypeDiscriminators = false, - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonDerivedType(typeof(PermissionDecisionApproveForSessionApprovalCommands), "commands")] [JsonDerivedType(typeof(PermissionDecisionApproveForSessionApprovalRead), "read")] [JsonDerivedType(typeof(PermissionDecisionApproveForSessionApprovalWrite), "write")] @@ -14934,7 +14897,6 @@ public partial class PermissionDecisionApproveOnce : PermissionDecision public partial class PermissionDecisionApproveForSessionApproval { /// The type discriminator. - [JsonRequired] [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } @@ -15117,8 +15079,7 @@ public partial class PermissionDecisionApproveForSession : PermissionDecision [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", - IgnoreUnrecognizedTypeDiscriminators = false, - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalCommands), "commands")] [JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalRead), "read")] [JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalWrite), "write")] @@ -15133,7 +15094,6 @@ public partial class PermissionDecisionApproveForSession : PermissionDecision public partial class PermissionDecisionApproveForLocationApproval { /// The type discriminator. - [JsonRequired] [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } @@ -15909,8 +15869,7 @@ public sealed class PermissionsLocationsAddToolApprovalResult [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", - IgnoreUnrecognizedTypeDiscriminators = false, - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsCommands), "commands")] [JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsRead), "read")] [JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsWrite), "write")] @@ -15925,7 +15884,6 @@ public sealed class PermissionsLocationsAddToolApprovalResult public partial class PermissionsLocationsAddToolApprovalDetails { /// The type discriminator. - [JsonRequired] [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } @@ -18241,14 +18199,12 @@ internal sealed class SessionUsageGetMetricsRequest [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", - IgnoreUnrecognizedTypeDiscriminators = false, - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonDerivedType(typeof(SessionLimitPredictionResultAvailable), "available")] [JsonDerivedType(typeof(SessionLimitPredictionResultUnavailable), "unavailable")] public partial class SessionLimitPredictionResult { /// The type discriminator. - [JsonRequired] [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } diff --git a/go/rpc/zrpc_encoding.go b/go/rpc/zrpc_encoding.go index 4133ff2e7e..2d921c41dc 100644 --- a/go/rpc/zrpc_encoding.go +++ b/go/rpc/zrpc_encoding.go @@ -69,8 +69,9 @@ func unmarshalAuthInfo(data []byte) (AuthInfo, error) { return nil, err } return &d, nil + default: + return &RawAuthInfoData{Discriminator: raw.Type, Raw: data}, nil } - return nil, errors.New("data did not match any union variant for AuthInfo") } func (r RawAuthInfoData) MarshalJSON() ([]byte, error) { @@ -271,8 +272,9 @@ func unmarshalAgentRegistrySpawnResult(data []byte) (AgentRegistrySpawnResult, e return nil, err } return &d, nil + default: + return &RawAgentRegistrySpawnResultData{Discriminator: raw.Kind, Raw: data}, nil } - return nil, errors.New("data did not match any union variant for AgentRegistrySpawnResult") } func (r RawAgentRegistrySpawnResultData) MarshalJSON() ([]byte, error) { @@ -433,8 +435,9 @@ func unmarshalAttachment(data []byte) (Attachment, error) { return nil, err } return &d, nil + default: + return &RawAttachmentData{Discriminator: raw.Type, Raw: data}, nil } - return nil, errors.New("data did not match any union variant for Attachment") } func (r RawAttachmentData) MarshalJSON() ([]byte, error) { @@ -1201,8 +1204,9 @@ func unmarshalDebugCollectLogsDestination(data []byte) (DebugCollectLogsDestinat return nil, err } return &d, nil + default: + return &RawDebugCollectLogsDestinationData{Discriminator: raw.Kind, Raw: data}, nil } - return nil, errors.New("data did not match any union variant for DebugCollectLogsDestination") } func (r RawDebugCollectLogsDestinationData) MarshalJSON() ([]byte, error) { @@ -1347,8 +1351,9 @@ func unmarshalExternalToolTextResultForLlmContent(data []byte) (ExternalToolText return nil, err } return &d, nil + default: + return &RawExternalToolTextResultForLlmContentData{Discriminator: raw.Type, Raw: data}, nil } - return nil, errors.New("data did not match any union variant for ExternalToolTextResultForLlmContent") } func (r RawExternalToolTextResultForLlmContentData) MarshalJSON() ([]byte, error) { @@ -1610,8 +1615,9 @@ func unmarshalFactoryRunFailure(data []byte) (FactoryRunFailure, error) { return nil, err } return &d, nil + default: + return &RawFactoryRunFailureData{Discriminator: raw.Type, Raw: data}, nil } - return nil, errors.New("data did not match any union variant for FactoryRunFailure") } func (r RawFactoryRunFailureData) MarshalJSON() ([]byte, error) { @@ -2339,8 +2345,9 @@ func unmarshalMCPPlanTransportChoice(data []byte) (MCPPlanTransportChoice, error return nil, err } return &d, nil + default: + return &RawMCPPlanTransportChoiceData{Discriminator: raw.Transport, Raw: data}, nil } - return nil, errors.New("data did not match any union variant for MCPPlanTransportChoice") } func (r RawMCPPlanTransportChoiceData) MarshalJSON() ([]byte, error) { @@ -2379,8 +2386,9 @@ func unmarshalMCPPlanRequiredValue(data []byte) (MCPPlanRequiredValue, error) { return nil, err } return &d, nil + default: + return &RawMCPPlanRequiredValueData{Discriminator: raw.Kind, Raw: data}, nil } - return nil, errors.New("data did not match any union variant for MCPPlanRequiredValue") } func (r RawMCPPlanRequiredValueData) MarshalJSON() ([]byte, error) { @@ -2746,8 +2754,9 @@ func unmarshalMCPPlanInstallSource(data []byte) (MCPPlanInstallSource, error) { return nil, err } return &d, nil + default: + return &RawMCPPlanInstallSourceData{Discriminator: raw.Kind, Raw: data}, nil } - return nil, errors.New("data did not match any union variant for MCPPlanInstallSource") } func (r RawMCPPlanInstallSourceData) MarshalJSON() ([]byte, error) { @@ -2797,8 +2806,9 @@ func unmarshalMCPServerCardReference(data []byte) (MCPServerCardReference, error return nil, err } return &d, nil + default: + return &RawMCPServerCardReferenceData{Discriminator: raw.Kind, Raw: data}, nil } - return nil, errors.New("data did not match any union variant for MCPServerCardReference") } func (r RawMCPServerCardReferenceData) MarshalJSON() ([]byte, error) { @@ -2976,8 +2986,9 @@ func unmarshalMCPPlanInstallResult(data []byte) (MCPPlanInstallResult, error) { return nil, err } return &d, nil + default: + return &RawMCPPlanInstallResultData{Discriminator: raw.Kind, Raw: data}, nil } - return nil, errors.New("data did not match any union variant for MCPPlanInstallResult") } func (r RawMCPPlanInstallResultData) MarshalJSON() ([]byte, error) { @@ -3403,8 +3414,9 @@ func unmarshalPermissionDecision(data []byte) (PermissionDecision, error) { return nil, err } return &d, nil + default: + return &RawPermissionDecisionData{Discriminator: raw.Kind, Raw: data}, nil } - return nil, errors.New("data did not match any union variant for PermissionDecision") } func (r RawPermissionDecisionData) MarshalJSON() ([]byte, error) { @@ -3502,8 +3514,9 @@ func unmarshalUserToolSessionApproval(data []byte) (UserToolSessionApproval, err return nil, err } return &d, nil + default: + return &RawUserToolSessionApprovalData{Discriminator: raw.Kind, Raw: data}, nil } - return nil, errors.New("data did not match any union variant for UserToolSessionApproval") } func (r RawUserToolSessionApprovalData) MarshalJSON() ([]byte, error) { @@ -3766,8 +3779,9 @@ func unmarshalPermissionDecisionApproveForLocationApproval(data []byte) (Permiss return nil, err } return &d, nil + default: + return &RawPermissionDecisionApproveForLocationApprovalData{Discriminator: raw.Kind, Raw: data}, nil } - return nil, errors.New("data did not match any union variant for PermissionDecisionApproveForLocationApproval") } func (r RawPermissionDecisionApproveForLocationApprovalData) MarshalJSON() ([]byte, error) { @@ -4012,8 +4026,9 @@ func unmarshalPermissionDecisionApproveForSessionApproval(data []byte) (Permissi return nil, err } return &d, nil + default: + return &RawPermissionDecisionApproveForSessionApprovalData{Discriminator: raw.Kind, Raw: data}, nil } - return nil, errors.New("data did not match any union variant for PermissionDecisionApproveForSessionApproval") } func (r RawPermissionDecisionApproveForSessionApprovalData) MarshalJSON() ([]byte, error) { @@ -4390,8 +4405,9 @@ func unmarshalPermissionsLocationsAddToolApprovalDetails(data []byte) (Permissio return nil, err } return &d, nil + default: + return &RawPermissionsLocationsAddToolApprovalDetailsData{Discriminator: raw.Kind, Raw: data}, nil } - return nil, errors.New("data did not match any union variant for PermissionsLocationsAddToolApprovalDetails") } func (r RawPermissionsLocationsAddToolApprovalDetailsData) MarshalJSON() ([]byte, error) { @@ -4649,8 +4665,9 @@ func unmarshalPushAttachment(data []byte) (PushAttachment, error) { return nil, err } return &d, nil + default: + return &RawPushAttachmentData{Discriminator: raw.Type, Raw: data}, nil } - return nil, errors.New("data did not match any union variant for PushAttachment") } func (r RawPushAttachmentData) MarshalJSON() ([]byte, error) { @@ -4909,8 +4926,9 @@ func unmarshalRemoteControlStatus(data []byte) (RemoteControlStatus, error) { return nil, err } return &d, nil + default: + return &RawRemoteControlStatusData{Discriminator: raw.State, Raw: data}, nil } - return nil, errors.New("data did not match any union variant for RemoteControlStatus") } func (r RawRemoteControlStatusData) MarshalJSON() ([]byte, error) { @@ -5240,8 +5258,9 @@ func unmarshalSessionLimitPredictionResult(data []byte) (SessionLimitPredictionR return nil, err } return &d, nil + default: + return &RawSessionLimitPredictionResultData{Discriminator: raw.Kind, Raw: data}, nil } - return nil, errors.New("data did not match any union variant for SessionLimitPredictionResult") } func (r RawSessionLimitPredictionResultData) MarshalJSON() ([]byte, error) { @@ -5567,8 +5586,9 @@ func unmarshalSessionOpenParams(data []byte) (SessionOpenParams, error) { return nil, err } return &d, nil + default: + return &RawSessionOpenParamsData{Discriminator: raw.Kind, Raw: data}, nil } - return nil, errors.New("data did not match any union variant for SessionOpenParams") } func (r RawSessionOpenParamsData) MarshalJSON() ([]byte, error) { @@ -5714,8 +5734,9 @@ func unmarshalSettableAuthInfo(data []byte) (SettableAuthInfo, error) { return nil, err } return &d, nil + default: + return &RawSettableAuthInfoData{Discriminator: raw.Type, Raw: data}, nil } - return nil, errors.New("data did not match any union variant for SettableAuthInfo") } func (r RawSettableAuthInfoData) MarshalJSON() ([]byte, error) { @@ -5819,8 +5840,9 @@ func unmarshalSlashCommandInvocationResult(data []byte) (SlashCommandInvocationR return nil, err } return &d, nil + default: + return &RawSlashCommandInvocationResultData{Discriminator: raw.Kind, Raw: data}, nil } - return nil, errors.New("data did not match any union variant for SlashCommandInvocationResult") } func (r RawSlashCommandInvocationResultData) MarshalJSON() ([]byte, error) { @@ -5959,8 +5981,9 @@ func unmarshalTaskClientUpdate(data []byte) (TaskClientUpdate, error) { return nil, err } return &d, nil + default: + return &RawTaskClientUpdateData{Discriminator: raw.Kind, Raw: data}, nil } - return nil, errors.New("data did not match any union variant for TaskClientUpdate") } func (r RawTaskClientUpdateData) MarshalJSON() ([]byte, error) { @@ -6049,8 +6072,9 @@ func unmarshalTaskInfo(data []byte) (TaskInfo, error) { return nil, err } return &d, nil + default: + return &RawTaskInfoData{Discriminator: raw.Type, Raw: data}, nil } - return nil, errors.New("data did not match any union variant for TaskInfo") } func (r RawTaskInfoData) MarshalJSON() ([]byte, error) { diff --git a/java/scripts/codegen/java.ts b/java/scripts/codegen/java.ts index 9f4b1a2379..71b191d956 100644 --- a/java/scripts/codegen/java.ts +++ b/java/scripts/codegen/java.ts @@ -11,7 +11,10 @@ import fs from "fs/promises"; import type { JSONSchema7 } from "json-schema"; import path from "path"; import { fileURLToPath } from "url"; -import { analyseDiscriminatedUnionVariants } from "../../../scripts/codegen/schema-unions.js"; +import { + analyseDiscriminatedUnionVariants, + analyseNestedClosedUnionResult, +} from "../../../scripts/codegen/schema-unions.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -1465,13 +1468,7 @@ async function generateRpcTypes(schemaPath: string): Promise { for (const section of [schema.server, schema.session, schema.clientSession, schema.clientGlobal]) { if (!section) continue; for (const [, method] of collectRpcMethods(section)) { - const result = resolveRef(method.result ?? undefined); - const union = result?.anyOf ?? result?.oneOf; - if ( - union - && Array.isArray(union) - && findDiscriminator(resolveUnionVariants(union as JSONSchema7[])) - ) { + if (analyseNestedClosedUnionResult(method.result, currentDefinitions)) { collectPromotedNestedUnionTypes(method.result); } } 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 1979dbeee3..7274ce2b7f 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/McpPlanEnumValueType.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanEnumValueType.java deleted file mode 100644 index 2bed0a6521..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanEnumValueType.java +++ /dev/null @@ -1,33 +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 javax.annotation.processing.Generated; - -/** - * Discriminator for an enumerated required value - * - * @since 1.0.0 - */ -@javax.annotation.processing.Generated("copilot-sdk-codegen") -public enum McpPlanEnumValueType { - /** The {@code enum} variant. */ - ENUM("enum"); - - private final String value; - McpPlanEnumValueType(String value) { this.value = value; } - @com.fasterxml.jackson.annotation.JsonValue - public String getValue() { return value; } - @com.fasterxml.jackson.annotation.JsonCreator - public static McpPlanEnumValueType fromValue(String value) { - for (McpPlanEnumValueType v : values()) { - if (v.value.equals(value)) return v; - } - throw new IllegalArgumentException("Unknown McpPlanEnumValueType value: " + value); - } -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanPackageTransport.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanPackageTransport.java deleted file mode 100644 index cae9f57b89..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanPackageTransport.java +++ /dev/null @@ -1,33 +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 javax.annotation.processing.Generated; - -/** - * Transport exposed by a locally launched package - * - * @since 1.0.0 - */ -@javax.annotation.processing.Generated("copilot-sdk-codegen") -public enum McpPlanPackageTransport { - /** The {@code stdio} variant. */ - STDIO("stdio"); - - private final String value; - McpPlanPackageTransport(String value) { this.value = value; } - @com.fasterxml.jackson.annotation.JsonValue - public String getValue() { return value; } - @com.fasterxml.jackson.annotation.JsonCreator - public static McpPlanPackageTransport fromValue(String value) { - for (McpPlanPackageTransport v : values()) { - if (v.value.equals(value)) return v; - } - 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 deleted file mode 100644 index 40366197df..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanRemoteTransport.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 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/McpPlanRequiredValue.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanRequiredValue.java deleted file mode 100644 index 52acb82611..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanRequiredValue.java +++ /dev/null @@ -1,35 +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.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import javax.annotation.processing.Generated; - -/** - * 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. - * - * @since 1.0.0 - */ -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind", visible = true) -@JsonSubTypes({ - @JsonSubTypes.Type(value = McpPlanRequiredValueScalar.class, name = "scalar"), - @JsonSubTypes.Type(value = McpPlanRequiredValueEnum.class, name = "enum") -}) -@JsonIgnoreProperties(ignoreUnknown = true) -@javax.annotation.processing.Generated("copilot-sdk-codegen") -public abstract class McpPlanRequiredValue { - - /** - * 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/McpPlanRequiredValueEnum.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanRequiredValueEnum.java deleted file mode 100644 index 95eebb07b8..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanRequiredValueEnum.java +++ /dev/null @@ -1,94 +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 java.util.List; -import javax.annotation.processing.Generated; - -/** - * One enumerated non-secret value a transport choice needs before it can be applied. The permitted values are structurally required. - * - * @since 1.0.0 - */ -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonInclude(JsonInclude.Include.NON_NULL) -@javax.annotation.processing.Generated("copilot-sdk-codegen") -public final class McpPlanRequiredValueEnum extends McpPlanRequiredValue { - - @JsonProperty("kind") - private final String kind = "enum"; - - @Override - public String getKind() { return kind; } - - /** Key the value is supplied under. Inert untrusted data. */ - @JsonProperty("key") - private String key; - - /** Where the value is applied when the server is launched. */ - @JsonProperty("category") - private McpPlanValueCategory category; - - /** Discriminator: the value must be one of `enumValues`. */ - @JsonProperty("valueType") - private McpPlanEnumValueType valueType; - - /** Whether the value must be present for the plan to be applicable. */ - @JsonProperty("required") - private Boolean required; - - /** 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. */ - @JsonProperty("defaultValue") - private String defaultValue; - - /** Human-readable label from the card. Inert untrusted text. */ - @JsonProperty("title") - private String title; - - /** Human-readable explanation from the card. Inert untrusted text. */ - @JsonProperty("description") - private String description; - - /** Non-empty permitted value set. Inert untrusted data. */ - @JsonProperty("enumValues") - private List enumValues; - - /** Whether the value may be supplied more than once. */ - @JsonProperty("isRepeated") - private Boolean isRepeated; - - public String getKey() { return key; } - public void setKey(String key) { this.key = key; } - - public McpPlanValueCategory getCategory() { return category; } - public void setCategory(McpPlanValueCategory category) { this.category = category; } - - public McpPlanEnumValueType getValueType() { return valueType; } - public void setValueType(McpPlanEnumValueType valueType) { this.valueType = valueType; } - - public Boolean getRequired() { return required; } - public void setRequired(Boolean required) { this.required = required; } - - public String getDefaultValue() { return defaultValue; } - public void setDefaultValue(String defaultValue) { this.defaultValue = defaultValue; } - - public String getTitle() { return title; } - public void setTitle(String title) { this.title = title; } - - public String getDescription() { return description; } - public void setDescription(String description) { this.description = description; } - - public List getEnumValues() { return enumValues; } - public void setEnumValues(List enumValues) { this.enumValues = enumValues; } - - public Boolean getIsRepeated() { return isRepeated; } - public void setIsRepeated(Boolean isRepeated) { this.isRepeated = isRepeated; } -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanRequiredValueScalar.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanRequiredValueScalar.java deleted file mode 100644 index fba8ff2504..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanRequiredValueScalar.java +++ /dev/null @@ -1,86 +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; - -/** - * One non-secret scalar value a transport choice needs before it can be applied. - * - * @since 1.0.0 - */ -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonInclude(JsonInclude.Include.NON_NULL) -@javax.annotation.processing.Generated("copilot-sdk-codegen") -public final class McpPlanRequiredValueScalar extends McpPlanRequiredValue { - - @JsonProperty("kind") - private final String kind = "scalar"; - - @Override - public String getKind() { return kind; } - - /** Key the value is supplied under. Inert untrusted data. */ - @JsonProperty("key") - private String key; - - /** Where the value is applied when the server is launched. */ - @JsonProperty("category") - private McpPlanValueCategory category; - - /** Scalar type the value must conform to. */ - @JsonProperty("valueType") - private McpPlanScalarValueType valueType; - - /** Whether the value must be present for the plan to be applicable. */ - @JsonProperty("required") - private Boolean required; - - /** 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. */ - @JsonProperty("defaultValue") - private String defaultValue; - - /** Human-readable label from the card. Inert untrusted text. */ - @JsonProperty("title") - private String title; - - /** Human-readable explanation from the card. Inert untrusted text. */ - @JsonProperty("description") - private String description; - - /** Whether the value may be supplied more than once. */ - @JsonProperty("isRepeated") - private Boolean isRepeated; - - public String getKey() { return key; } - public void setKey(String key) { this.key = key; } - - public McpPlanValueCategory getCategory() { return category; } - public void setCategory(McpPlanValueCategory category) { this.category = category; } - - public McpPlanScalarValueType getValueType() { return valueType; } - public void setValueType(McpPlanScalarValueType valueType) { this.valueType = valueType; } - - public Boolean getRequired() { return required; } - public void setRequired(Boolean required) { this.required = required; } - - public String getDefaultValue() { return defaultValue; } - public void setDefaultValue(String defaultValue) { this.defaultValue = defaultValue; } - - public String getTitle() { return title; } - public void setTitle(String title) { this.title = title; } - - public String getDescription() { return description; } - public void setDescription(String description) { this.description = description; } - - public Boolean getIsRepeated() { return isRepeated; } - public void setIsRepeated(Boolean isRepeated) { this.isRepeated = isRepeated; } -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanScalarValueType.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanScalarValueType.java deleted file mode 100644 index ca7620eff3..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanScalarValueType.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 javax.annotation.processing.Generated; - -/** - * Scalar type a required value must conform to - * - * @since 1.0.0 - */ -@javax.annotation.processing.Generated("copilot-sdk-codegen") -public enum McpPlanScalarValueType { - /** The {@code string} variant. */ - STRING("string"), - /** The {@code number} variant. */ - NUMBER("number"), - /** The {@code boolean} variant. */ - BOOLEAN("boolean"), - /** The {@code path} variant. */ - PATH("path"); - - private final String value; - McpPlanScalarValueType(String value) { this.value = value; } - @com.fasterxml.jackson.annotation.JsonValue - public String getValue() { return value; } - @com.fasterxml.jackson.annotation.JsonCreator - public static McpPlanScalarValueType fromValue(String value) { - for (McpPlanScalarValueType v : values()) { - if (v.value.equals(value)) return v; - } - throw new IllegalArgumentException("Unknown McpPlanScalarValueType 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 deleted file mode 100644 index c54696caae..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanSecretPlaceholder.java +++ /dev/null @@ -1,31 +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; - -/** - * 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 deleted file mode 100644 index 23cd22ab74..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanTransportChoice.java +++ /dev/null @@ -1,35 +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.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, 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 abstract class McpPlanTransportChoice { - - /** - * Returns the discriminator value for this variant. - * - * @return the installMethod discriminator - */ - public abstract String getInstallMethod(); -} 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 deleted file mode 100644 index 70cfa6d3ca..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanTransportChoicePackage.java +++ /dev/null @@ -1,73 +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 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) -@javax.annotation.processing.Generated("copilot-sdk-codegen") -public final class McpPlanTransportChoicePackage extends McpPlanTransportChoice { - - @JsonProperty("installMethod") - private final String installMethod = "package"; - - @Override - public String getInstallMethod() { return installMethod; } - - /** Stable identifier for this choice within the plan, used to select it when the plan is applied. */ - @JsonProperty("choiceId") - private String choiceId; - - /** Local process transport this package choice would use. */ - @JsonProperty("transport") - private McpPlanPackageTransport transport; - - /** Packaging ecosystem, for example `oci` or `npm`. */ - @JsonProperty("packageType") - private String packageType; - - /** Package identifier. Inert untrusted data. */ - @JsonProperty("packageIdentifier") - private String packageIdentifier; - - /** Typed values this choice requires, excluding secrets. */ - @JsonProperty("requiredValues") - private List requiredValues; - - /** Secrets this choice requires, referenced by placeholder only. */ - @JsonProperty("secretPlaceholders") - private List secretPlaceholders; - - public String getChoiceId() { return choiceId; } - public void setChoiceId(String choiceId) { this.choiceId = choiceId; } - - public McpPlanPackageTransport getTransport() { return transport; } - public void setTransport(McpPlanPackageTransport transport) { this.transport = transport; } - - public String getPackageType() { return packageType; } - public void setPackageType(String packageType) { this.packageType = packageType; } - - public String getPackageIdentifier() { return packageIdentifier; } - public void setPackageIdentifier(String packageIdentifier) { this.packageIdentifier = packageIdentifier; } - - public List getRequiredValues() { return requiredValues; } - public void setRequiredValues(List requiredValues) { this.requiredValues = requiredValues; } - - public List getSecretPlaceholders() { return secretPlaceholders; } - public void setSecretPlaceholders(List secretPlaceholders) { this.secretPlaceholders = 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 deleted file mode 100644 index 8bf81185b7..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanTransportChoiceRemote.java +++ /dev/null @@ -1,66 +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 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) -@javax.annotation.processing.Generated("copilot-sdk-codegen") -public final class McpPlanTransportChoiceRemote extends McpPlanTransportChoice { - - @JsonProperty("installMethod") - private final String installMethod = "remote"; - - @Override - public String getInstallMethod() { return installMethod; } - - /** Stable identifier for this choice within the plan, used to select it when the plan is applied. */ - @JsonProperty("choiceId") - private String choiceId; - - /** Endpoint transport this remote choice would use. */ - @JsonProperty("transport") - private McpPlanRemoteTransport transport; - - /** Endpoint URL. Inert untrusted data. */ - @JsonProperty("endpoint") - private String endpoint; - - /** Typed values this choice requires, excluding secrets. */ - @JsonProperty("requiredValues") - private List requiredValues; - - /** Secrets this choice requires, referenced by placeholder only. */ - @JsonProperty("secretPlaceholders") - private List secretPlaceholders; - - public String getChoiceId() { return choiceId; } - public void setChoiceId(String choiceId) { this.choiceId = choiceId; } - - public McpPlanRemoteTransport getTransport() { return transport; } - public void setTransport(McpPlanRemoteTransport transport) { this.transport = transport; } - - public String getEndpoint() { return endpoint; } - public void setEndpoint(String endpoint) { this.endpoint = endpoint; } - - public List getRequiredValues() { return requiredValues; } - public void setRequiredValues(List requiredValues) { this.requiredValues = requiredValues; } - - public List getSecretPlaceholders() { return secretPlaceholders; } - public void setSecretPlaceholders(List secretPlaceholders) { this.secretPlaceholders = secretPlaceholders; } -} 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 deleted file mode 100644 index e94008ef7a..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanValueCategory.java +++ /dev/null @@ -1,41 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: api.schema.json - -package com.github.copilot.generated.rpc; - -import 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/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index e19ce924e1..e68046951b 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -23717,1230 +23717,291 @@ type RpcResultProjection = | { kind: "union"; discriminator: string; variants: Record }; const RPC_RESULT_PROJECTIONS: Record = { - "mcp.planInstall": { - "kind": "ref", - "name": "McpPlanInstallResult" - }, "catalog.search": { "kind": "ref", "name": "CatalogSearchResult" - }, - "agentRegistry.spawn": { - "kind": "ref", - "name": "AgentRegistrySpawnResult" - }, - "session.gitHubAuth.login": { - "kind": "ref", - "name": "AuthInfo" - }, - "session.commands.invoke": { - "kind": "ref", - "name": "SlashCommandInvocationResult" - }, - "session.limitPrediction.predict": { - "kind": "ref", - "name": "SessionLimitPredictionResult" } }; const RPC_RESULT_PROJECTION_DEFINITIONS: Record = { - "McpPlanResourceIdentity": { - "kind": "object", - "closed": true, - "properties": { - "canonicalName": null, - "serverName": null, - "version": null, - "registryId": null - } - }, - "CardDigest": { - "kind": "object", - "closed": true, - "properties": { - "algorithm": null, - "value": null - } - }, - "McpPlanProvenance": { - "kind": "object", - "closed": true, - "properties": { - "authority": null, - "validatedAt": null, - "cardDigest": { - "kind": "ref", - "name": "CardDigest" - }, - "mediaType": null - } - }, - "McpPlanRequiredValueScalar": { - "kind": "object", - "closed": true, - "properties": { - "kind": null, - "key": null, - "category": null, - "valueType": null, - "required": null, - "defaultValue": null, - "title": null, - "description": null, - "isRepeated": null - } - }, - "McpPlanRequiredValueEnum": { - "kind": "object", - "closed": true, - "properties": { - "kind": null, - "key": null, - "category": null, - "valueType": null, - "required": null, - "defaultValue": null, - "title": null, - "description": null, - "enumValues": null, - "isRepeated": null - } - }, - "McpPlanRequiredValue": { - "kind": "union", - "discriminator": "kind", - "variants": { - "string:\"scalar\"": { - "kind": "ref", - "name": "McpPlanRequiredValueScalar" - }, - "string:\"enum\"": { - "kind": "ref", - "name": "McpPlanRequiredValueEnum" - } - } - }, - "McpPlanSecretPlaceholder": { - "kind": "object", - "closed": true, - "properties": { - "key": null, - "placeholder": null, - "title": null - } - }, - "McpPlanTransportChoicePackage": { - "kind": "object", - "closed": true, - "properties": { - "choiceId": null, - "transport": null, - "installMethod": null, - "packageType": null, - "packageIdentifier": null, - "requiredValues": { - "kind": "array", - "items": { - "kind": "ref", - "name": "McpPlanRequiredValue" - } - }, - "secretPlaceholders": { - "kind": "array", - "items": { - "kind": "ref", - "name": "McpPlanSecretPlaceholder" - } - } - } - }, - "McpPlanTransportChoiceRemote": { - "kind": "object", - "closed": true, - "properties": { - "choiceId": null, - "transport": null, - "installMethod": null, - "endpoint": null, - "requiredValues": { - "kind": "array", - "items": { - "kind": "ref", - "name": "McpPlanRequiredValue" - } - }, - "secretPlaceholders": { - "kind": "array", - "items": { - "kind": "ref", - "name": "McpPlanSecretPlaceholder" - } - } - } - }, - "McpPlanTransportChoice": { - "kind": "union", - "discriminator": "installMethod", - "variants": { - "string:\"package\"": { - "kind": "ref", - "name": "McpPlanTransportChoicePackage" - }, - "string:\"remote\"": { - "kind": "ref", - "name": "McpPlanTransportChoiceRemote" - } - } - }, - "McpPlanTarget": { - "kind": "object", - "closed": true, - "properties": { - "scope": null, - "configKey": null - } - }, - "McpPlanPolicyResult": { - "kind": "object", - "closed": true, - "properties": { - "decision": null, - "source": null, - "reason": null - } - }, - "McpPlanConfigurationChange": { - "kind": "object", - "closed": true, - "properties": { - "operation": null, - "scope": null, - "configKey": null, - "changedFields": null, - "secretReferences": null - } - }, - "McpInstallPlan": { - "kind": "object", - "closed": true, - "properties": { - "planHandle": null, - "planHandleExpiresAt": null, - "identity": { - "kind": "ref", - "name": "McpPlanResourceIdentity" - }, - "provenance": { - "kind": "ref", - "name": "McpPlanProvenance" - }, - "transportChoices": { - "kind": "array", - "items": { - "kind": "ref", - "name": "McpPlanTransportChoice" - } - }, - "recommendedTransportChoiceId": null, - "target": { - "kind": "ref", - "name": "McpPlanTarget" - }, - "policy": { - "kind": "ref", - "name": "McpPlanPolicyResult" - }, - "configurationChanges": { - "kind": "array", - "items": { - "kind": "ref", - "name": "McpPlanConfigurationChange" - } - }, - "reloadRequired": null, - "requiresInteractiveConfiguration": null - } - }, - "CatalogNegotiatedContract": { - "kind": "object", - "closed": true, - "properties": { - "runtimeProtocolVersion": null, - "grantedCapabilities": null - } - }, - "McpPlanInstallPlanned": { - "kind": "object", - "closed": true, - "properties": { - "kind": null, - "plan": { - "kind": "ref", - "name": "McpInstallPlan" - }, - "negotiated": { - "kind": "ref", - "name": "CatalogNegotiatedContract" - } - } - }, - "CatalogNegotiationRefusedError": { - "kind": "object", - "closed": true, - "properties": { - "kind": null, - "reason": null, - "runtimeProtocolVersion": null, - "minimumSupportedProtocolVersion": null, - "supportedCapabilities": null, - "unsupportedCapabilities": null, - "message": null - } - }, - "CatalogHandleRejectedError": { - "kind": "object", - "closed": true, - "properties": { - "kind": null, - "handleType": null, - "reason": null, - "message": null - } - }, - "CatalogInvalidRequestError": { - "kind": "object", - "closed": true, - "properties": { - "kind": null, - "field": null, - "message": null - } - }, - "CatalogAuthenticationRequiredError": { - "kind": "object", - "closed": true, - "properties": { - "kind": null, - "reason": null, - "message": null - } - }, - "CatalogPolicyRejectedError": { - "kind": "object", - "closed": true, - "properties": { - "kind": null, - "source": null, - "message": null - } - }, - "CatalogNetworkFailureError": { - "kind": "object", - "closed": true, - "properties": { - "kind": null, - "reason": null, - "statusCode": null, - "retryAfterSeconds": null, - "message": null - } - }, - "CatalogUnsafeRetrievalError": { - "kind": "object", - "closed": true, - "properties": { - "kind": null, - "reason": null, - "message": null - } - }, - "CatalogMalformedCardError": { - "kind": "object", - "closed": true, - "properties": { - "kind": null, - "reason": null, - "mediaType": null, - "message": null - } - }, - "CatalogContractViolationError": { - "kind": "object", - "closed": true, - "properties": { - "kind": null, - "reason": null, - "message": null - } - }, - "CatalogUnavailableTransportError": { - "kind": "object", - "closed": true, - "properties": { - "kind": null, - "reason": null, - "message": null - } - }, - "CatalogNotInstallableError": { + "CatalogCandidateSourceUrl": { "kind": "object", "closed": true, "properties": { "kind": null, - "reason": null, - "message": null - } - }, - "CatalogUnavailableError": { - "kind": "object", - "closed": true, - "properties": { - "kind": null, - "reason": null, - "message": null - } - }, - "McpPlanInstallResult": { - "kind": "union", - "discriminator": "kind", - "variants": { - "string:\"planned\"": { - "kind": "ref", - "name": "McpPlanInstallPlanned" - }, - "string:\"negotiation-refused\"": { - "kind": "ref", - "name": "CatalogNegotiationRefusedError" - }, - "string:\"handle-rejected\"": { - "kind": "ref", - "name": "CatalogHandleRejectedError" - }, - "string:\"invalid-request\"": { - "kind": "ref", - "name": "CatalogInvalidRequestError" - }, - "string:\"authentication-required\"": { - "kind": "ref", - "name": "CatalogAuthenticationRequiredError" - }, - "string:\"policy-rejected\"": { - "kind": "ref", - "name": "CatalogPolicyRejectedError" - }, - "string:\"network-failure\"": { - "kind": "ref", - "name": "CatalogNetworkFailureError" - }, - "string:\"unsafe-retrieval\"": { - "kind": "ref", - "name": "CatalogUnsafeRetrievalError" - }, - "string:\"malformed-card\"": { - "kind": "ref", - "name": "CatalogMalformedCardError" - }, - "string:\"contract-violation\"": { - "kind": "ref", - "name": "CatalogContractViolationError" - }, - "string:\"unavailable-transport\"": { - "kind": "ref", - "name": "CatalogUnavailableTransportError" - }, - "string:\"not-installable\"": { - "kind": "ref", - "name": "CatalogNotInstallableError" - }, - "string:\"unavailable\"": { - "kind": "ref", - "name": "CatalogUnavailableError" - } - } - }, - "CatalogCandidateSourceUrl": { - "kind": "object", - "closed": true, - "properties": { - "kind": null, - "url": null - } - }, - "CatalogCandidateSourceEmbedded": { - "kind": "object", - "closed": true, - "properties": { - "kind": null - } - }, - "CatalogCandidateSource": { - "kind": "union", - "discriminator": "kind", - "variants": { - "string:\"url\"": { - "kind": "ref", - "name": "CatalogCandidateSourceUrl" - }, - "string:\"embedded\"": { - "kind": "ref", - "name": "CatalogCandidateSourceEmbedded" - } - } - }, - "CatalogMcpServerCandidateProvenance": { - "kind": "object", - "closed": true, - "properties": { - "authority": null, - "observedAt": null, - "mediaType": null - } - }, - "CatalogMcpServerCandidate": { - "kind": "object", - "closed": true, - "properties": { - "handle": null, - "handleExpiresAt": null, - "kind": null, - "mediaType": null, - "installability": null, - "displayName": null, - "description": null, - "publisher": null, - "source": { - "kind": "ref", - "name": "CatalogCandidateSource" - }, - "provenance": { - "kind": "ref", - "name": "CatalogMcpServerCandidateProvenance" - } - } - }, - "CatalogAiSkillCandidateProvenance": { - "kind": "object", - "closed": true, - "properties": { - "authority": null, - "observedAt": null, - "mediaType": null - } - }, - "CatalogAiSkillCandidate": { - "kind": "object", - "closed": true, - "properties": { - "handle": null, - "handleExpiresAt": null, - "kind": null, - "mediaType": null, - "installability": null, - "displayName": null, - "description": null, - "publisher": null, - "source": { - "kind": "ref", - "name": "CatalogCandidateSource" - }, - "provenance": { - "kind": "ref", - "name": "CatalogAiSkillCandidateProvenance" - } - } - }, - "CatalogCandidate": { - "kind": "union", - "discriminator": "kind", - "variants": { - "string:\"mcp-server\"": { - "kind": "ref", - "name": "CatalogMcpServerCandidate" - }, - "string:\"ai-skill\"": { - "kind": "ref", - "name": "CatalogAiSkillCandidate" - } - } - }, - "CatalogSearchSucceeded": { - "kind": "object", - "closed": true, - "properties": { - "kind": null, - "searchId": null, - "candidates": { - "kind": "array", - "items": { - "kind": "ref", - "name": "CatalogCandidate" - } - }, - "truncated": null, - "negotiated": { - "kind": "ref", - "name": "CatalogNegotiatedContract" - } - } - }, - "CatalogUnsupportedKindError": { - "kind": "object", - "closed": true, - "properties": { - "kind": null, - "requestedKinds": null, - "supportedKinds": null, - "message": null - } - }, - "CatalogSearchResult": { - "kind": "union", - "discriminator": "kind", - "variants": { - "string:\"succeeded\"": { - "kind": "ref", - "name": "CatalogSearchSucceeded" - }, - "string:\"negotiation-refused\"": { - "kind": "ref", - "name": "CatalogNegotiationRefusedError" - }, - "string:\"unsupported-kind\"": { - "kind": "ref", - "name": "CatalogUnsupportedKindError" - }, - "string:\"invalid-request\"": { - "kind": "ref", - "name": "CatalogInvalidRequestError" - }, - "string:\"authentication-required\"": { - "kind": "ref", - "name": "CatalogAuthenticationRequiredError" - }, - "string:\"policy-rejected\"": { - "kind": "ref", - "name": "CatalogPolicyRejectedError" - }, - "string:\"network-failure\"": { - "kind": "ref", - "name": "CatalogNetworkFailureError" - }, - "string:\"unsafe-retrieval\"": { - "kind": "ref", - "name": "CatalogUnsafeRetrievalError" - }, - "string:\"malformed-card\"": { - "kind": "ref", - "name": "CatalogMalformedCardError" - }, - "string:\"contract-violation\"": { - "kind": "ref", - "name": "CatalogContractViolationError" - }, - "string:\"unavailable\"": { - "kind": "ref", - "name": "CatalogUnavailableError" - } - } - }, - "AgentRegistryLiveTargetEntry": { - "kind": "object", - "closed": true, - "properties": { - "schemaVersion": null, - "kind": null, - "pid": null, - "host": null, - "port": null, - "sessionId": null, - "sessionName": null, - "cwd": null, - "branch": null, - "model": null, - "status": null, - "attentionKind": null, - "statusRevision": null, - "lastTerminalEvent": null, - "startedAt": null, - "copilotVersion": null, - "lastSeenMs": null, - "token": null - } - }, - "AgentRegistryLogCapture": { - "kind": "object", - "closed": true, - "properties": { - "enabled": null, - "path": null, - "openError": null, - "openErrorReason": null - } - }, - "AgentRegistrySpawnSpawned": { - "kind": "object", - "closed": true, - "properties": { - "kind": null, - "entry": { - "kind": "ref", - "name": "AgentRegistryLiveTargetEntry" - }, - "initialPromptSent": null, - "initialPromptError": null, - "logCapture": { - "kind": "ref", - "name": "AgentRegistryLogCapture" - } - } - }, - "AgentRegistrySpawnError": { - "kind": "object", - "closed": true, - "properties": { - "kind": null, - "message": null, - "code": null - } - }, - "AgentRegistrySpawnRegistryTimeout": { - "kind": "object", - "closed": true, - "properties": { - "kind": null, - "childPid": null, - "logCapture": { - "kind": "ref", - "name": "AgentRegistryLogCapture" - } + "url": null } }, - "AgentRegistrySpawnValidationError": { + "CatalogCandidateSourceEmbedded": { "kind": "object", "closed": true, "properties": { - "kind": null, - "reason": null, - "field": null, - "message": null + "kind": null } }, - "AgentRegistrySpawnResult": { + "CatalogCandidateSource": { "kind": "union", "discriminator": "kind", "variants": { - "string:\"spawned\"": { - "kind": "ref", - "name": "AgentRegistrySpawnSpawned" - }, - "string:\"spawn-error\"": { - "kind": "ref", - "name": "AgentRegistrySpawnError" - }, - "string:\"registry-timeout\"": { + "string:\"url\"": { "kind": "ref", - "name": "AgentRegistrySpawnRegistryTimeout" + "name": "CatalogCandidateSourceUrl" }, - "string:\"validation-error\"": { + "string:\"embedded\"": { "kind": "ref", - "name": "AgentRegistrySpawnValidationError" + "name": "CatalogCandidateSourceEmbedded" } } }, - "CopilotUserResponseEndpoints": { - "kind": "object", - "closed": true, - "properties": { - "api": null, - "origin-tracker": null, - "proxy": null, - "telemetry": null, - "exp": null - } - }, - "CopilotUserResponseQuotaSnapshotsChat": { - "kind": "object", - "closed": true, - "properties": { - "entitlement": null, - "overage_count": null, - "overage_permitted": null, - "percent_remaining": null, - "quota_id": null, - "quota_remaining": null, - "remaining": null, - "unlimited": null, - "timestamp_utc": null, - "has_quota": null, - "quota_reset_at": null, - "token_based_billing": null - } - }, - "CopilotUserResponseQuotaSnapshotsCompletions": { - "kind": "object", - "closed": true, - "properties": { - "entitlement": null, - "overage_count": null, - "overage_permitted": null, - "percent_remaining": null, - "quota_id": null, - "quota_remaining": null, - "remaining": null, - "unlimited": null, - "timestamp_utc": null, - "has_quota": null, - "quota_reset_at": null, - "token_based_billing": null - } - }, - "CopilotUserResponseQuotaSnapshotsPremiumInteractions": { + "CatalogMcpServerCandidateProvenance": { "kind": "object", "closed": true, "properties": { - "entitlement": null, - "overage_count": null, - "overage_permitted": null, - "percent_remaining": null, - "quota_id": null, - "quota_remaining": null, - "remaining": null, - "unlimited": null, - "timestamp_utc": null, - "has_quota": null, - "quota_reset_at": null, - "token_based_billing": null - } - }, - "CopilotUserResponseQuotaSnapshots": { - "kind": "object", - "closed": false, - "properties": { - "chat": { - "kind": "ref", - "name": "CopilotUserResponseQuotaSnapshotsChat" - }, - "completions": { - "kind": "ref", - "name": "CopilotUserResponseQuotaSnapshotsCompletions" - }, - "premium_interactions": { - "kind": "ref", - "name": "CopilotUserResponseQuotaSnapshotsPremiumInteractions" - } + "authority": null, + "observedAt": null, + "mediaType": null } }, - "CopilotUserResponse": { + "CatalogMcpServerCandidate": { "kind": "object", "closed": true, "properties": { - "login": null, - "access_type_sku": null, - "analytics_tracking_id": null, - "assigned_date": null, - "can_signup_for_limited": null, - "chat_enabled": null, - "copilot_plan": null, - "copilotignore_enabled": null, - "endpoints": { - "kind": "ref", - "name": "CopilotUserResponseEndpoints" - }, - "organization_login_list": null, - "organization_list": null, - "codex_agent_enabled": null, - "is_mcp_enabled": null, - "quota_reset_date": null, - "quota_snapshots": { + "handle": null, + "handleExpiresAt": null, + "kind": null, + "mediaType": null, + "installability": null, + "displayName": null, + "description": null, + "publisher": null, + "source": { "kind": "ref", - "name": "CopilotUserResponseQuotaSnapshots" + "name": "CatalogCandidateSource" }, - "restricted_telemetry": null, - "is_staff": null, - "te": null, - "token_based_billing": null, - "can_upgrade_plan": null, - "quota_reset_date_utc": null, - "limited_user_quotas": null, - "limited_user_reset_date": null, - "monthly_quotas": null, - "cloud_session_storage_enabled": null, - "cli_remote_control_enabled": null - } - }, - "HMACAuthInfo": { - "kind": "object", - "closed": true, - "properties": { - "type": null, - "host": null, - "hmac": null, - "copilotUser": { + "provenance": { "kind": "ref", - "name": "CopilotUserResponse" + "name": "CatalogMcpServerCandidateProvenance" } } }, - "EnvAuthInfo": { + "CatalogAiSkillCandidateProvenance": { "kind": "object", "closed": true, "properties": { - "type": null, - "host": null, - "login": null, - "token": null, - "envVar": null, - "copilotUser": { - "kind": "ref", - "name": "CopilotUserResponse" - } + "authority": null, + "observedAt": null, + "mediaType": null } }, - "TokenAuthInfo": { + "CatalogAiSkillCandidate": { "kind": "object", "closed": true, "properties": { - "type": null, - "host": null, - "token": null, - "registrationId": null, - "copilotUser": { + "handle": null, + "handleExpiresAt": null, + "kind": null, + "mediaType": null, + "installability": null, + "displayName": null, + "description": null, + "publisher": null, + "source": { "kind": "ref", - "name": "CopilotUserResponse" - } - } - }, - "TokenProviderAuthInfo": { - "kind": "object", - "closed": true, - "properties": { - "type": null, - "host": null, - "registrationId": null, - "copilotUser": { + "name": "CatalogCandidateSource" + }, + "provenance": { "kind": "ref", - "name": "CopilotUserResponse" + "name": "CatalogAiSkillCandidateProvenance" } } }, - "CopilotApiTokenAuthInfo": { - "kind": "object", - "closed": true, - "properties": { - "type": null, - "host": null, - "copilotUser": { + "CatalogCandidate": { + "kind": "union", + "discriminator": "kind", + "variants": { + "string:\"mcp-server\"": { "kind": "ref", - "name": "CopilotUserResponse" - } - } - }, - "UserAuthInfo": { - "kind": "object", - "closed": true, - "properties": { - "type": null, - "host": null, - "login": null, - "copilotUser": { + "name": "CatalogMcpServerCandidate" + }, + "string:\"ai-skill\"": { "kind": "ref", - "name": "CopilotUserResponse" + "name": "CatalogAiSkillCandidate" } } }, - "GhCliAuthInfo": { + "CatalogNegotiatedContract": { "kind": "object", "closed": true, "properties": { - "type": null, - "host": null, - "login": null, - "token": null, - "copilotUser": { - "kind": "ref", - "name": "CopilotUserResponse" - } + "runtimeProtocolVersion": null, + "grantedCapabilities": null } }, - "ApiKeyAuthInfo": { + "CatalogSearchSucceeded": { "kind": "object", "closed": true, "properties": { - "type": null, - "apiKey": null, - "host": null, - "copilotUser": { - "kind": "ref", - "name": "CopilotUserResponse" - } - } - }, - "AuthInfo": { - "kind": "union", - "discriminator": "type", - "variants": { - "string:\"hmac\"": { - "kind": "ref", - "name": "HMACAuthInfo" - }, - "string:\"env\"": { - "kind": "ref", - "name": "EnvAuthInfo" - }, - "string:\"token\"": { - "kind": "ref", - "name": "TokenAuthInfo" - }, - "string:\"token-provider\"": { - "kind": "ref", - "name": "TokenProviderAuthInfo" - }, - "string:\"copilot-api-token\"": { - "kind": "ref", - "name": "CopilotApiTokenAuthInfo" - }, - "string:\"user\"": { - "kind": "ref", - "name": "UserAuthInfo" - }, - "string:\"gh-cli\"": { - "kind": "ref", - "name": "GhCliAuthInfo" + "kind": null, + "searchId": null, + "candidates": { + "kind": "array", + "items": { + "kind": "ref", + "name": "CatalogCandidate" + } }, - "string:\"api-key\"": { + "truncated": null, + "negotiated": { "kind": "ref", - "name": "ApiKeyAuthInfo" + "name": "CatalogNegotiatedContract" } } }, - "SlashCommandTextResult": { + "CatalogNegotiationRefusedError": { "kind": "object", "closed": true, "properties": { "kind": null, - "text": null, - "markdown": null, - "preserveAnsi": null, - "runtimeSettingsChanged": null + "reason": null, + "runtimeProtocolVersion": null, + "minimumSupportedProtocolVersion": null, + "supportedCapabilities": null, + "unsupportedCapabilities": null, + "message": null } }, - "SlashCommandAgentPromptResult": { + "CatalogUnsupportedKindError": { "kind": "object", "closed": true, "properties": { "kind": null, - "prompt": null, - "displayPrompt": null, - "mode": null, - "notice": null, - "runtimeSettingsChanged": null + "requestedKinds": null, + "supportedKinds": null, + "message": null } }, - "SlashCommandCompletedResult": { + "CatalogInvalidRequestError": { "kind": "object", "closed": true, "properties": { "kind": null, - "message": null, - "mode": null, - "runtimeSettingsChanged": null - } - }, - "SlashCommandSelectSubcommandOption": { - "kind": "object", - "closed": true, - "properties": { - "name": null, - "description": null, - "group": null + "field": null, + "message": null } }, - "SlashCommandSelectSubcommandResult": { + "CatalogAuthenticationRequiredError": { "kind": "object", "closed": true, "properties": { "kind": null, - "command": null, - "title": null, - "options": { - "kind": "array", - "items": { - "kind": "ref", - "name": "SlashCommandSelectSubcommandOption" - } - }, - "runtimeSettingsChanged": null + "reason": null, + "message": null } }, - "SlashCommandTimelineEntry": { + "CatalogPolicyRejectedError": { "kind": "object", "closed": true, "properties": { - "type": null, - "text": null, - "url": null, - "remediation": null + "kind": null, + "source": null, + "message": null } }, - "SlashCommandAddTimelineEntryResult": { + "CatalogNetworkFailureError": { "kind": "object", "closed": true, "properties": { "kind": null, - "entry": { - "kind": "ref", - "name": "SlashCommandTimelineEntry" - }, - "prefillInput": null, - "runtimeSettingsChanged": null + "reason": null, + "statusCode": null, + "retryAfterSeconds": null, + "message": null } }, - "SlashCommandModelPickerDialog": { + "CatalogUnsafeRetrievalError": { "kind": "object", "closed": true, "properties": { "kind": null, - "modelToEnable": null, - "scope": null, - "target": null + "reason": null, + "message": null } }, - "SlashCommandShowDialogResult": { + "CatalogMalformedCardError": { "kind": "object", "closed": true, "properties": { "kind": null, - "dialog": { - "kind": "ref", - "name": "SlashCommandModelPickerDialog" - }, - "runtimeSettingsChanged": null + "reason": null, + "mediaType": null, + "message": null } }, - "SlashCommandSetModelResult": { + "CatalogContractViolationError": { "kind": "object", "closed": true, "properties": { "kind": null, - "model": null, - "scope": null, - "warning": null, - "reasoningEffort": null, - "revertOnCancel": null, - "repoScope": null, - "runtimeSettingsChanged": null + "reason": null, + "message": null } }, - "SlashCommandSetPlanModelResult": { + "CatalogUnavailableError": { "kind": "object", "closed": true, "properties": { "kind": null, - "planModel": null, - "message": null, - "runtimeSettingsChanged": null + "reason": null, + "message": null } }, - "SlashCommandInvocationResult": { + "CatalogSearchResult": { "kind": "union", "discriminator": "kind", "variants": { - "string:\"text\"": { + "string:\"succeeded\"": { "kind": "ref", - "name": "SlashCommandTextResult" + "name": "CatalogSearchSucceeded" }, - "string:\"agent-prompt\"": { + "string:\"negotiation-refused\"": { "kind": "ref", - "name": "SlashCommandAgentPromptResult" + "name": "CatalogNegotiationRefusedError" }, - "string:\"completed\"": { + "string:\"unsupported-kind\"": { "kind": "ref", - "name": "SlashCommandCompletedResult" + "name": "CatalogUnsupportedKindError" }, - "string:\"select-subcommand\"": { + "string:\"invalid-request\"": { "kind": "ref", - "name": "SlashCommandSelectSubcommandResult" + "name": "CatalogInvalidRequestError" }, - "string:\"add-timeline-entry\"": { + "string:\"authentication-required\"": { "kind": "ref", - "name": "SlashCommandAddTimelineEntryResult" + "name": "CatalogAuthenticationRequiredError" }, - "string:\"show-dialog\"": { + "string:\"policy-rejected\"": { "kind": "ref", - "name": "SlashCommandShowDialogResult" + "name": "CatalogPolicyRejectedError" }, - "string:\"set-model\"": { + "string:\"network-failure\"": { "kind": "ref", - "name": "SlashCommandSetModelResult" + "name": "CatalogNetworkFailureError" }, - "string:\"set-plan-model\"": { + "string:\"unsafe-retrieval\"": { "kind": "ref", - "name": "SlashCommandSetPlanModelResult" - } - } - }, - "SessionLimitPredictionTierOption": { - "kind": "object", - "closed": true, - "properties": { - "tier": null, - "cap": null - } - }, - "SessionLimitPredictionBaselineData": { - "kind": "object", - "closed": true, - "properties": { - "windowStart": null, - "windowEnd": null - } - }, - "SessionLimitPredictionDetails": { - "kind": "object", - "closed": true, - "properties": { - "clientType": null, - "modelId": null, - "source": null, - "sourceKey": null, - "family": null, - "tiers": { - "kind": "array", - "items": { - "kind": "ref", - "name": "SessionLimitPredictionTierOption" - } + "name": "CatalogUnsafeRetrievalError" }, - "baselineData": { + "string:\"malformed-card\"": { "kind": "ref", - "name": "SessionLimitPredictionBaselineData" + "name": "CatalogMalformedCardError" }, - "recommendedTier": null, - "recommendedCap": null - } - }, - "SessionLimitPredictionResult": { - "kind": "union", - "discriminator": "kind", - "variants": { - "string:\"available\"": { - "kind": "object", - "closed": true, - "properties": { - "prediction": { - "kind": "ref", - "name": "SessionLimitPredictionDetails" - }, - "kind": null - } + "string:\"contract-violation\"": { + "kind": "ref", + "name": "CatalogContractViolationError" }, "string:\"unavailable\"": { - "kind": "object", - "closed": true, - "properties": { - "reason": null, - "kind": null - } + "kind": "ref", + "name": "CatalogUnavailableError" } } } @@ -25159,10 +24220,7 @@ export function createServerRpc(connection: MessageConnection) { * @returns Outcome of an mcp.planInstall call: either a normalised plan, or one typed refusal. Nothing is written in either case. */ planInstall: async (params: McpPlanInstallRequest): Promise => - projectRpcResult( - await connection.sendRequest("mcp.planInstall", params), - RPC_RESULT_PROJECTIONS["mcp.planInstall"], - ) as McpPlanInstallResult, + connection.sendRequest("mcp.planInstall", params), }, /** @experimental */ extensions: { @@ -25727,10 +24785,7 @@ export function createServerRpc(connection: MessageConnection) { * @returns Outcome of an agentRegistry.spawn call. */ spawn: async (params: AgentRegistrySpawnRequest): Promise => - projectRpcResult( - await connection.sendRequest("agentRegistry.spawn", params), - RPC_RESULT_PROJECTIONS["agentRegistry.spawn"], - ) as AgentRegistrySpawnResult, + connection.sendRequest("agentRegistry.spawn", params), }, }; } @@ -27037,10 +26092,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 => - projectRpcResult( - await connection.sendRequest("session.commands.invoke", { sessionId, ...params }), - RPC_RESULT_PROJECTIONS["session.commands.invoke"], - ) as SlashCommandInvocationResult, + connection.sendRequest("session.commands.invoke", { sessionId, ...params }), /** * Reports completion of a pending client-handled slash command. * @@ -27738,10 +26790,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 => - projectRpcResult( - await connection.sendRequest("session.limitPrediction.predict", { sessionId, ...params }), - RPC_RESULT_PROJECTIONS["session.limitPrediction.predict"], - ) as SessionLimitPredictionResult, + connection.sendRequest("session.limitPrediction.predict", { sessionId, ...params }), }, /** @experimental */ remote: { @@ -27857,10 +26906,7 @@ 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 => - projectRpcResult( - await connection.sendRequest("session.gitHubAuth.login", { sessionId, ...params }), - RPC_RESULT_PROJECTIONS["session.gitHubAuth.login"], - ) as AuthInfo, + connection.sendRequest("session.gitHubAuth.login", { sessionId, ...params }), /** * Switches the session to another available authentication. * diff --git a/nodejs/test/discriminated-union-codegen.test.ts b/nodejs/test/discriminated-union-codegen.test.ts index 592aab22d5..fbda41991e 100644 --- a/nodejs/test/discriminated-union-codegen.test.ts +++ b/nodejs/test/discriminated-union-codegen.test.ts @@ -4,23 +4,40 @@ import { describe, expect, it } from "vitest"; import { collectNestedDiscriminatedUnionTypeNames } from "../../java/scripts/codegen/java.ts"; import { analyseDiscriminatedUnion, + analyseNestedClosedUnionResult, schemaDiscriminatorValueKey, } from "../../scripts/codegen/schema-unions.ts"; import { createRpcResultProjectionBundle } from "../../scripts/codegen/typescript.ts"; import type { DefinitionCollections } from "../../scripts/codegen/utils.ts"; const definitions: Record = { - SyntheticEnvelope: { + SyntheticResult: { + anyOf: [ + { $ref: "#/definitions/SyntheticSucceeded" }, + { $ref: "#/definitions/SyntheticFailed" }, + ], + }, + SyntheticSucceeded: { type: "object", additionalProperties: false, - required: ["choices"], + required: ["kind", "choices"], properties: { + kind: { const: "succeeded" }, choices: { type: "array", items: { $ref: "#/definitions/SyntheticChoice" }, }, }, }, + SyntheticFailed: { + type: "object", + additionalProperties: false, + required: ["kind", "message"], + properties: { + kind: { const: "failed" }, + message: { type: "string" }, + }, + }, SyntheticChoice: { anyOf: [{ $ref: "#/definitions/SyntheticAlpha" }, { $ref: "#/definitions/SyntheticBeta" }], }, @@ -95,13 +112,13 @@ describe("schema-driven discriminated union codegen", () => { it("builds nested runtime projections for an equivalent synthetic result", () => { const projection = createRpcResultProjectionBundle( - { $ref: "#/definitions/SyntheticEnvelope" }, + { $ref: "#/definitions/SyntheticResult" }, collections ); expect(projection?.root).toEqual({ kind: "ref", - name: "SyntheticEnvelope", + name: "SyntheticResult", }); expect(projection?.definitions.SyntheticChoice).toMatchObject({ kind: "union", @@ -124,9 +141,17 @@ describe("schema-driven discriminated union codegen", () => { }); it("promotes every nested synthetic union for Java generation", () => { + expect( + analyseNestedClosedUnionResult( + { $ref: "#/definitions/SyntheticResult" }, + definitions + )?.unionDefinitionNames + ).toEqual( + new Set(["SyntheticResult", "SyntheticChoice", "SyntheticSource"]) + ); expect( collectNestedDiscriminatedUnionTypeNames( - { $ref: "#/definitions/SyntheticEnvelope" }, + { $ref: "#/definitions/SyntheticResult" }, definitions ) ).toEqual(new Set(["SyntheticChoice", "SyntheticSource"])); diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index e0eb6984f1..52a07e576e 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -40037,7 +40037,7 @@ def _load_AgentRegistrySpawnResult(obj: Any) -> "AgentRegistrySpawnResult": case "spawn-error": return AgentRegistrySpawnError.from_dict(obj) case "registry-timeout": return AgentRegistrySpawnRegistryTimeout.from_dict(obj) case "validation-error": return AgentRegistrySpawnValidationError.from_dict(obj) - raise ValueError(f"Unknown AgentRegistrySpawnResult kind: {kind!r}") + 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 @@ -40054,7 +40054,7 @@ def _load_AuthInfo(obj: Any) -> "AuthInfo": case "user": return UserAuthInfo.from_dict(obj) case "gh-cli": return GhCLIAuthInfo.from_dict(obj) case "api-key": return APIKeyAuthInfo.from_dict(obj) - raise ValueError(f"Unknown AuthInfo type: {kind!r}") + case _: raise ValueError(f"Unknown AuthInfo type: {kind!r}") # One inert catalog result, represented as an MCP server or discovery-only AI skill variant so kind, media type, provenance, and installability cannot contradict each other. CatalogCandidate = CatalogMCPServerCandidate | CatalogAISkillCandidate @@ -40112,7 +40112,7 @@ def _load_ExternalToolTextResultForLlmContent(obj: Any) -> "ExternalToolTextResu case "audio": return ExternalToolTextResultForLlmContentAudio.from_dict(obj) case "resource_link": return ExternalToolTextResultForLlmContentResourceLink.from_dict(obj) case "resource": return ExternalToolTextResultForLlmContentResource.from_dict(obj) - raise ValueError(f"Unknown ExternalToolTextResultForLlmContent type: {kind!r}") + case _: raise ValueError(f"Unknown ExternalToolTextResultForLlmContent type: {kind!r}") # Outcome of an mcp.planInstall call: either a normalised plan, or one typed refusal. Nothing is written in either case. MCPPlanInstallResult = MCPPlanInstallPlanned | CatalogNegotiationRefusedError | CatalogHandleRejectedError | CatalogInvalidRequestError | CatalogAuthenticationRequiredError | CatalogPolicyRejectedError | CatalogNetworkFailureError | CatalogUnsafeRetrievalError | CatalogMalformedCardError | CatalogContractViolationError | CatalogUnavailableTransportError | CatalogNotInstallableError | CatalogUnavailableError @@ -40134,7 +40134,7 @@ def _load_MCPPlanInstallResult(obj: Any) -> "MCPPlanInstallResult": case "unavailable-transport": return CatalogUnavailableTransportError.from_dict(obj) case "not-installable": return CatalogNotInstallableError.from_dict(obj) case "unavailable": return CatalogUnavailableError.from_dict(obj) - raise ValueError(f"Unknown MCPPlanInstallResult kind: {kind!r}") + case _: raise ValueError(f"Unknown MCPPlanInstallResult kind: {kind!r}") # What an install plan is computed from: a candidate handle from a previous search, or a card supplied directly. MCPPlanInstallSource = MCPPlanInstallSourceCandidate | MCPPlanInstallSourceCard @@ -40145,7 +40145,7 @@ def _load_MCPPlanInstallSource(obj: Any) -> "MCPPlanInstallSource": match kind: case "candidate": return MCPPlanInstallSourceCandidate.from_dict(obj) case "card": return MCPPlanInstallSourceCard.from_dict(obj) - raise ValueError(f"Unknown MCPPlanInstallSource kind: {kind!r}") + case _: raise ValueError(f"Unknown MCPPlanInstallSource kind: {kind!r}") # 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. MCPPlanRequiredValue = MCPPlanRequiredValueScalar | MCPPlanRequiredValueEnum @@ -40156,7 +40156,7 @@ def _load_MCPPlanRequiredValue(obj: Any) -> "MCPPlanRequiredValue": match kind: case "scalar": return MCPPlanRequiredValueScalar.from_dict(obj) case "enum": return MCPPlanRequiredValueEnum.from_dict(obj) - raise ValueError(f"Unknown MCPPlanRequiredValue kind: {kind!r}") + case _: raise ValueError(f"Unknown MCPPlanRequiredValue kind: {kind!r}") # 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. MCPPlanTransportChoice = MCPPlanTransportChoicePackage | MCPPlanTransportChoiceRemote @@ -40167,7 +40167,7 @@ def _load_MCPPlanTransportChoice(obj: Any) -> "MCPPlanTransportChoice": match kind: case "package": return MCPPlanTransportChoicePackage.from_dict(obj) case "remote": return MCPPlanTransportChoiceRemote.from_dict(obj) - raise ValueError(f"Unknown MCPPlanTransportChoice installMethod: {kind!r}") + case _: raise ValueError(f"Unknown MCPPlanTransportChoice installMethod: {kind!r}") # 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. MCPServerCardReference = MCPServerCardURL | MCPServerCardEmbedded @@ -40178,7 +40178,7 @@ def _load_MCPServerCardReference(obj: Any) -> "MCPServerCardReference": match kind: case "url": return MCPServerCardURL.from_dict(obj) case "embedded": return MCPServerCardEmbedded.from_dict(obj) - raise ValueError(f"Unknown MCPServerCardReference kind: {kind!r}") + 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 @@ -40202,7 +40202,7 @@ def _load_PermissionDecision(obj: Any) -> "PermissionDecision": case "denied-interactively-by-user": return PermissionDecisionDeniedInteractivelyByUser.from_dict(obj) case "denied-by-content-exclusion-policy": return PermissionDecisionDeniedByContentExclusionPolicy.from_dict(obj) case "denied-by-permission-request-hook": return PermissionDecisionDeniedByPermissionRequestHook.from_dict(obj) - raise ValueError(f"Unknown PermissionDecision kind: {kind!r}") + 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 @@ -40222,7 +40222,7 @@ def _load_PermissionDecisionApproveForLocationApproval(obj: Any) -> "PermissionD case "factory": return PermissionDecisionApproveForLocationApprovalFactory.from_dict(obj) case "extension-permission-access": return PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess.from_dict(obj) case "extension-env-access": return PermissionDecisionApproveForLocationApprovalExtensionEnvAccess.from_dict(obj) - raise ValueError(f"Unknown PermissionDecisionApproveForLocationApproval kind: {kind!r}") + 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 @@ -40242,7 +40242,7 @@ def _load_PermissionDecisionApproveForSessionApproval(obj: Any) -> "PermissionDe case "factory": return PermissionDecisionApproveForSessionApprovalFactory.from_dict(obj) case "extension-permission-access": return PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess.from_dict(obj) case "extension-env-access": return PermissionDecisionApproveForSessionApprovalExtensionEnvAccess.from_dict(obj) - raise ValueError(f"Unknown PermissionDecisionApproveForSessionApproval kind: {kind!r}") + 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 @@ -40262,7 +40262,7 @@ def _load_PermissionsLocationsAddToolApprovalDetails(obj: Any) -> "PermissionsLo case "factory": return PermissionsLocationsAddToolApprovalDetailsFactory.from_dict(obj) case "extension-permission-access": return PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess.from_dict(obj) case "extension-env-access": return PermissionsLocationsAddToolApprovalDetailsExtensionEnvAccess.from_dict(obj) - raise ValueError(f"Unknown PermissionsLocationsAddToolApprovalDetails kind: {kind!r}") + case _: raise ValueError(f"Unknown PermissionsLocationsAddToolApprovalDetails kind: {kind!r}") # Attachment union accepted by push input, covering files, directories, GitHub objects, blobs, snippets, and extension context. PushAttachment = PushAttachmentFile | PushAttachmentDirectory | PushAttachmentSelection | PushAttachmentGitHubReference | PushAttachmentGitHubCommit | PushAttachmentGitHubRelease | PushAttachmentGitHubActionsJob | PushAttachmentGitHubRepository | PushAttachmentGitHubFileDiff | PushAttachmentGitHubTreeComparison | PushAttachmentGitHubURL | PushAttachmentGitHubFile | PushAttachmentGitHubSnippet | PushAttachmentBlob | ExtensionContextPushInput @@ -40286,7 +40286,7 @@ def _load_PushAttachment(obj: Any) -> "PushAttachment": case "github_snippet": return PushAttachmentGitHubSnippet.from_dict(obj) case "blob": return PushAttachmentBlob.from_dict(obj) case "extension_context": return ExtensionContextPushInput.from_dict(obj) - raise ValueError(f"Unknown PushAttachment type: {kind!r}") + case _: raise ValueError(f"Unknown PushAttachment type: {kind!r}") # Result of the queued command execution. QueuedCommandResult = QueuedCommandHandled | QueuedCommandNotHandled @@ -40297,7 +40297,7 @@ def _load_QueuedCommandResult(obj: Any) -> "QueuedCommandResult": match kind: case True: return QueuedCommandHandled.from_dict(obj) case False: return QueuedCommandNotHandled.from_dict(obj) - raise ValueError(f"Unknown QueuedCommandResult handled: {kind!r}") + case _: raise ValueError(f"Unknown QueuedCommandResult handled: {kind!r}") # State of the runtime-managed remote-control singleton. RemoteControlStatus = RemoteControlStatusOff | RemoteControlStatusConnecting | RemoteControlStatusActive | RemoteControlStatusError @@ -40310,7 +40310,7 @@ def _load_RemoteControlStatus(obj: Any) -> "RemoteControlStatus": case "connecting": return RemoteControlStatusConnecting.from_dict(obj) case "active": return RemoteControlStatusActive.from_dict(obj) case "error": return RemoteControlStatusError.from_dict(obj) - raise ValueError(f"Unknown RemoteControlStatus state: {kind!r}") + case _: raise ValueError(f"Unknown RemoteControlStatus state: {kind!r}") # Local or remote session metadata entry. Narrow on `isRemote` to access source-specific fields. SessionListEntry = LocalSessionMetadataValue | RemoteSessionMetadataValue @@ -40321,7 +40321,7 @@ def _load_SessionListEntry(obj: Any) -> "SessionListEntry": match kind: case False: return LocalSessionMetadataValue.from_dict(obj) case True: return RemoteSessionMetadataValue.from_dict(obj) - raise ValueError(f"Unknown SessionListEntry isRemote: {kind!r}") + case _: raise ValueError(f"Unknown SessionListEntry isRemote: {kind!r}") # Open a session by creating, resuming, attaching, connecting to a remote, or handing off. SessionOpenParams = SessionsOpenCreate | SessionsOpenResume | SessionsOpenResumeLast | SessionsOpenAttach | SessionsOpenRemote | SessionsOpenCloud | SessionsOpenHandoff @@ -40337,7 +40337,7 @@ def _load_SessionOpenParams(obj: Any) -> "SessionOpenParams": case "remote": return SessionsOpenRemote.from_dict(obj) case "cloud": return SessionsOpenCloud.from_dict(obj) case "handoff": return SessionsOpenHandoff.from_dict(obj) - raise ValueError(f"Unknown SessionOpenParams kind: {kind!r}") + 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 @@ -40353,7 +40353,7 @@ def _load_SettableAuthInfo(obj: Any) -> "SettableAuthInfo": case "user": return UserAuthInfo.from_dict(obj) case "gh-cli": return GhCLIAuthInfo.from_dict(obj) case "api-key": return APIKeyAuthInfo.from_dict(obj) - raise ValueError(f"Unknown SettableAuthInfo type: {kind!r}") + case _: raise ValueError(f"Unknown SettableAuthInfo type: {kind!r}") # Result of invoking the slash command (text output, prompt to send to the agent, completion, or subcommand selection). SlashCommandInvocationResult = SlashCommandTextResult | SlashCommandAgentPromptResult | SlashCommandCompletedResult | SlashCommandSelectSubcommandResult | SlashCommandAddTimelineEntryResult | SlashCommandShowDialogResult | SlashCommandSetModelResult | SlashCommandSetPlanModelResult @@ -40370,7 +40370,7 @@ def _load_SlashCommandInvocationResult(obj: Any) -> "SlashCommandInvocationResul case "show-dialog": return SlashCommandShowDialogResult.from_dict(obj) case "set-model": return SlashCommandSetModelResult.from_dict(obj) case "set-plan-model": return SlashCommandSetPlanModelResult.from_dict(obj) - raise ValueError(f"Unknown SlashCommandInvocationResult kind: {kind!r}") + case _: raise ValueError(f"Unknown SlashCommandInvocationResult kind: {kind!r}") # Tracked task union returned by task APIs, containing an agent, client, or shell task. TaskInfo = TaskAgentInfo | TaskClientInfo | TaskShellInfo @@ -40382,7 +40382,7 @@ def _load_TaskInfo(obj: Any) -> "TaskInfo": case "agent": return TaskAgentInfo.from_dict(obj) case "client": return TaskClientInfo.from_dict(obj) case "shell": return TaskShellInfo.from_dict(obj) - raise ValueError(f"Unknown TaskInfo type: {kind!r}") + case _: raise ValueError(f"Unknown TaskInfo type: {kind!r}") AccountGetAllUsersResult = list diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index e977626fc9..76f90306f1 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -14,6 +14,7 @@ import { promisify } from "util"; import type { JSONSchema7 } from "json-schema"; import { analyseDiscriminatedUnionVariants, + analyseNestedClosedUnionResult, type UnknownVariantPolicy, } from "./schema-unions.js"; import { @@ -1600,6 +1601,7 @@ let rpcKnownTypes = new Map(); let rpcEnumOutput: string[] = []; let externalRpcValueTypes = new Set(); let rpcRootJsonSerializableTypes = new Set(); +let rpcRejectUnknownUnionTypeNames = new Set(); /** Schema definitions available during RPC generation (for $ref resolution). */ let rpcDefinitions: DefinitionCollections = { definitions: {}, $defs: {} }; @@ -1745,8 +1747,9 @@ function resolveRpcType(schema: JSONSchema7, isRequired: boolean, parentClassNam rpcPropertyResolver, isSchemaExperimental(schema) || experimentalRpcTypes.has(baseClassName), { - unknownVariantPolicy: - analyseDiscriminatedUnionVariants(variants)?.unknownVariantPolicy, + unknownVariantPolicy: rpcRejectUnknownUnionTypeNames.has(baseClassName) + ? analyseDiscriminatedUnionVariants(variants)?.unknownVariantPolicy + : undefined, } ); classes.push(polymorphicCode); @@ -2654,6 +2657,26 @@ function generateRpcCode( ...collectRpcMethods(schema.clientSession || {}), ...collectRpcMethods(schema.clientGlobal || {}), ]; + const schemaDefinitions = { + ...Object.fromEntries( + Object.entries(rpcDefinitions.$defs ?? {}).filter( + ([, value]) => typeof value === "object" && value !== null + ) + ) as Record, + ...Object.fromEntries( + Object.entries(rpcDefinitions.definitions ?? {}).filter( + ([, value]) => typeof value === "object" && value !== null + ) + ) as Record, + }; + rpcRejectUnknownUnionTypeNames = new Set(); + for (const method of allMethods) { + const analysis = analyseNestedClosedUnionResult(method.result, schemaDefinitions); + if (!analysis) continue; + for (const name of analysis.unionDefinitionNames) { + rpcRejectUnknownUnionTypeNames.add(typeToClassName(name)); + } + } for (const name of collectRpcMethodReferencedDefinitionNames( allMethods.filter((method) => method.stability !== "experimental"), rpcDefinitions diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts index 664b0dbde4..021e41ad89 100644 --- a/scripts/codegen/go.ts +++ b/scripts/codegen/go.ts @@ -13,7 +13,11 @@ import path from "path"; import { fileURLToPath } from "url"; import { promisify } from "util"; import wordwrap from "wordwrap"; -import { analyseDiscriminatedUnion, type UnknownVariantPolicy } from "./schema-unions.js"; +import { + analyseDiscriminatedUnion, + analyseNestedClosedUnionResult, + type UnknownVariantPolicy, +} from "./schema-unions.js"; import { addManagedApprovalRequiredToPermissionRequests, cloneSchemaForCodegen, @@ -569,7 +573,7 @@ interface GoCodegenCtx { definitions?: DefinitionCollections; wrapComments?: boolean; discriminatedUnionRawVariantSuffix?: string; - applyClosedUnionUnknownVariantPolicy?: boolean; + rejectUnknownUnionTypeNames?: Set; skipDefinitionTypeNames?: Set; encodingBlocks?: Set; unionVariantMarshalers?: Set; @@ -1120,7 +1124,7 @@ function registerGoExternalUnionUnmarshalers( definitions: externalDefinitions, wrapComments: ctx.wrapComments, discriminatedUnionRawVariantSuffix: ctx.discriminatedUnionRawVariantSuffix, - applyClosedUnionUnknownVariantPolicy: ctx.applyClosedUnionUnknownVariantPolicy, + rejectUnknownUnionTypeNames: ctx.rejectUnknownUnionTypeNames, packageName: ctx.packageName, }; @@ -2813,7 +2817,7 @@ function planGoUnion(typeName: string, schema: JSONSchema7, ctx: GoCodegenCtx, i const description = (schema as JSONSchema7).description; const discriminator = findGoDiscriminator(members, ctx, typeName); if (discriminator) { - if (ctx.applyClosedUnionUnknownVariantPolicy) { + if (ctx.rejectUnknownUnionTypeNames?.has(typeName)) { discriminator.unknownVariantPolicy = analyseDiscriminatedUnion(schema, (variant) => resolveGoUnionMember(variant, ctx.definitions) @@ -3109,7 +3113,11 @@ function goGeneratedEncodingFileCode(schemaFileName: string, packageName: string return wrapComments ? wrapGeneratedGoComments(code) : code; } -function generateGoRpcTypeCode(definitions: Record, definitionCollections: DefinitionCollections): GoGeneratedTypeCode { +function generateGoRpcTypeCode( + definitions: Record, + definitionCollections: DefinitionCollections, + rejectUnknownUnionTypeNames: Set +): GoGeneratedTypeCode { const ctx: GoCodegenCtx = { structs: [], encoding: [], @@ -3118,7 +3126,7 @@ function generateGoRpcTypeCode(definitions: Record, definit discriminatedUnions: new Map(), generatedNames: new Set(), definitions: definitionCollections, - applyClosedUnionUnknownVariantPolicy: true, + rejectUnknownUnionTypeNames, packageName: "rpc", }; ctx.skipDefinitionTypeNames = collectGoDiscriminatedUnionVariantDefinitionTypeNames(definitions, ctx); @@ -3849,6 +3857,14 @@ async function generateRpc(schemaPath?: string): Promise { Object.entries(rpcDefinitions.definitions ?? {}).filter(([, value]) => typeof value === "object" && value !== null) ) as Record, }; + const rejectUnknownUnionTypeNames = new Set(); + for (const method of allMethods) { + const analysis = analyseNestedClosedUnionResult(method.result, allDefinitions); + if (!analysis) continue; + for (const name of analysis.unionDefinitionNames) { + rejectUnknownUnionTypeNames.add(goDefinitionName(name)); + } + } for (const method of allMethods) { const resultSchema = getMethodResultSchema(method); @@ -3905,7 +3921,11 @@ async function generateRpc(schemaPath?: string): Promise { rpcDefinitions = allDefinitionCollections; // Strip trailing whitespace from generated output (gofmt requirement) - const generatedRpcCode = generateGoRpcTypeCode(allDefinitions, allDefinitionCollections); + const generatedRpcCode = generateGoRpcTypeCode( + allDefinitions, + allDefinitionCollections, + rejectUnknownUnionTypeNames + ); let generatedTypeCode = stripTrailingGoWhitespace(generatedRpcCode.typeCode); const generatedEncodingCode = stripTrailingGoWhitespace(generatedRpcCode.encodingCode); diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index 94b92e318b..4357791b7a 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -10,6 +10,7 @@ import fs from "fs/promises"; import path from "path"; import type { JSONSchema7, JSONSchema7Definition } from "json-schema"; import { fileURLToPath } from "url"; +import { analyseNestedClosedUnionResult } from "./schema-unions.js"; import { addManagedApprovalRequiredToPermissionRequests, cloneSchemaForCodegen, @@ -353,7 +354,8 @@ interface ResolvedRefBasedUnion { function postProcessRefBasedDiscriminatedUnionsForPython( code: string, definitions: Record, - definitionCollections: DefinitionCollections + definitionCollections: DefinitionCollections, + explicitFailureUnionNames: ReadonlySet ): { code: string; unions: ResolvedRefBasedUnion[] } { interface UnionInfo { aliasName: string; @@ -508,9 +510,15 @@ function postProcessRefBasedDiscriminatedUnionsForPython( for (const m of actualDispatch) { dispatcherLines.push(` case ${pyDiscriminatorValueExpr(m.value)}: return ${m.typeName}.from_dict(obj)`); } - dispatcherLines.push( - ` raise ValueError(f"Unknown ${actualAliasName} ${union.discriminatorProp}: {kind!r}")` - ); + if (explicitFailureUnionNames.has(union.aliasName)) { + dispatcherLines.push( + ` raise ValueError(f"Unknown ${actualAliasName} ${union.discriminatorProp}: {kind!r}")` + ); + } else { + dispatcherLines.push( + ` case _: raise ValueError(f"Unknown ${actualAliasName} ${union.discriminatorProp}: {kind!r}")` + ); + } code = `${code.trimEnd()}\n\n\n${aliasLine}\n\n\n${dispatcherLines.join("\n")}\n`; } @@ -3058,6 +3066,14 @@ async function generateRpc(schemaPath?: string, sessionEventsSchema?: JSONSchema } const allDefinitions = combinedSchema.definitions! as Record; + const explicitFailureUnionNames = new Set(); + for (const method of allMethods) { + const analysis = analyseNestedClosedUnionResult(method.result, allDefinitions); + if (!analysis) continue; + for (const name of analysis.unionDefinitionNames) { + explicitFailureUnionNames.add(name); + } + } preservePythonRpcStringDateFields(allDefinitions); const allDefinitionCollections: DefinitionCollections = { definitions: { ...(combinedSchema.$defs ?? {}), ...allDefinitions }, @@ -3138,7 +3154,8 @@ async function generateRpc(schemaPath?: string, sessionEventsSchema?: JSONSchema const { code: typesCodeAfterUnions, unions: refBasedUnions } = postProcessRefBasedDiscriminatedUnionsForPython( typesCode, allDefinitions, - allDefinitionCollections + allDefinitionCollections, + explicitFailureUnionNames ); typesCode = typesCodeAfterUnions; typesCode = modernizePython(typesCode); diff --git a/scripts/codegen/schema-conformance.ts b/scripts/codegen/schema-conformance.ts index 6f2df8e6d1..e2d9e3b6be 100644 --- a/scripts/codegen/schema-conformance.ts +++ b/scripts/codegen/schema-conformance.ts @@ -3,7 +3,10 @@ import type { JSONSchema7 } from "json-schema"; import path from "path"; import { COPILOT_CLI_VERSION } from "../../nodejs/src/cliVersion.js"; -import { analyseDiscriminatedUnion } from "./schema-unions.js"; +import { + analyseDiscriminatedUnion, + analyseNestedClosedUnionResult, +} from "./schema-unions.js"; import { getApiSchemaPath, REPO_ROOT } from "./utils.js"; const FORBIDDEN_CANDIDATE_FIELDS = ["card", "cardData", "rawCard"]; @@ -18,6 +21,19 @@ function referencedDefinitionNames(schema: JSONSchema7): string[] { ); } +function collectRpcMethods( + node: unknown, +): Array<{ rpcMethod: string; result?: JSONSchema7 }> { + if (!node || typeof node !== "object") return []; + if ( + "rpcMethod" in node && + typeof (node as { rpcMethod?: unknown }).rpcMethod === "string" + ) { + return [node as { rpcMethod: string; result?: JSONSchema7 }]; + } + return Object.values(node).flatMap(collectRpcMethods); +} + const schemaPath = await getApiSchemaPath(); const packageRoot = path.dirname(path.dirname(schemaPath)); const javaCodegenPackageJson = JSON.parse( @@ -108,6 +124,30 @@ for (const name of [ ); } +const selectedNestedUnionMethods = collectRpcMethods(schema) + .map((method) => ({ + method, + analysis: analyseNestedClosedUnionResult( + method.result, + schema.definitions, + ), + })) + .filter((entry) => entry.analysis !== undefined); +assert( + selectedNestedUnionMethods.map(({ method }) => method.rpcMethod).join(",") === + "catalog.search", + `unexpected nested-union result methods: ${selectedNestedUnionMethods + .map(({ method }) => method.rpcMethod) + .join(", ")}`, +); +assert( + [...selectedNestedUnionMethods[0].analysis!.unionDefinitionNames] + .sort() + .join(",") === + "CatalogCandidate,CatalogCandidateSource,CatalogSearchResult", + "nested-union policy graph must remain limited to the proven catalogue result unions", +); + for (const name of candidateVariants) { const properties = schema.definitions[name]?.properties; assert( diff --git a/scripts/codegen/schema-unions.ts b/scripts/codegen/schema-unions.ts index 4604e1c39a..142d1f09e8 100644 --- a/scripts/codegen/schema-unions.ts +++ b/scripts/codegen/schema-unions.ts @@ -21,6 +21,11 @@ export interface SchemaDiscriminatedUnion { unknownVariantPolicy: UnknownVariantPolicy; } +export interface NestedClosedUnionResult { + rootDefinitionName?: string; + unionDefinitionNames: Set; +} + export type SchemaVariantResolver = ( schema: JSONSchema7, ) => JSONSchema7 | undefined; @@ -143,3 +148,102 @@ export function analyseDiscriminatedUnion( resolveVariant, ); } + +function localDefinitionName(schema: JSONSchema7): string | undefined { + return schema.$ref?.match(/^#\/(?:definitions|\$defs)\/([^/]+)$/)?.[1]; +} + +function resolveLocalSchema( + schema: JSONSchema7, + definitions: Record, +): JSONSchema7 | undefined { + const name = localDefinitionName(schema); + return name ? definitions[name] : schema; +} + +/** + * Select the narrow nested-union shape that requires promoted list elements: + * a closed discriminated result whose variant directly owns an array of another + * closed discriminated union. Nested closed unions below those list elements + * are included in the same policy graph. + */ +export function analyseNestedClosedUnionResult( + root: JSONSchema7 | null | undefined, + definitions: Record, +): NestedClosedUnionResult | undefined { + if (!root) return undefined; + const resolveVariant = (schema: JSONSchema7): JSONSchema7 | undefined => + resolveLocalSchema(schema, definitions); + const resolvedRoot = resolveVariant(root); + if (!resolvedRoot) return undefined; + const rootUnion = analyseDiscriminatedUnion(resolvedRoot, resolveVariant); + if (rootUnion?.unknownVariantPolicy !== "reject") return undefined; + + const nestedArrayItems: JSONSchema7[] = []; + for (const variant of rootUnion.variants) { + for (const property of Object.values(variant.schema.properties ?? {})) { + if (!property || typeof property !== "object") continue; + const resolvedProperty = resolveVariant(property as JSONSchema7); + if ( + resolvedProperty?.type !== "array" || + !resolvedProperty.items || + Array.isArray(resolvedProperty.items) + ) { + continue; + } + const items = resolvedProperty.items as JSONSchema7; + const resolvedItems = resolveVariant(items); + if ( + resolvedItems && + analyseDiscriminatedUnion(resolvedItems, resolveVariant) + ?.unknownVariantPolicy === "reject" + ) { + nestedArrayItems.push(items); + } + } + } + if (nestedArrayItems.length === 0) return undefined; + + const unionDefinitionNames = new Set(); + const rootDefinitionName = localDefinitionName(root); + if (rootDefinitionName) unionDefinitionNames.add(rootDefinitionName); + const visitedDefinitions = new Set(); + const visit = (schema: JSONSchema7): void => { + const definitionName = localDefinitionName(schema); + if (definitionName) { + if (visitedDefinitions.has(definitionName)) return; + visitedDefinitions.add(definitionName); + const definition = definitions[definitionName]; + if (!definition) return; + if ( + analyseDiscriminatedUnion(definition, resolveVariant) + ?.unknownVariantPolicy === "reject" + ) { + unionDefinitionNames.add(definitionName); + } + visit(definition); + return; + } + + for (const property of Object.values(schema.properties ?? {})) { + if (property && typeof property === "object") { + visit(property as JSONSchema7); + } + } + if (schema.items && !Array.isArray(schema.items)) { + visit(schema.items as JSONSchema7); + } + for (const branch of [ + ...(schema.anyOf ?? []), + ...(schema.oneOf ?? []), + ...(schema.allOf ?? []), + ]) { + if (branch && typeof branch === "object") { + visit(branch as JSONSchema7); + } + } + }; + for (const items of nestedArrayItems) visit(items); + + return { rootDefinitionName, unionDefinitionNames }; +} diff --git a/scripts/codegen/typescript.ts b/scripts/codegen/typescript.ts index f5dbfe37f6..75313755e1 100644 --- a/scripts/codegen/typescript.ts +++ b/scripts/codegen/typescript.ts @@ -13,6 +13,7 @@ import path from "path"; import { fileURLToPath } from "url"; import { analyseDiscriminatedUnion, + analyseNestedClosedUnionResult, schemaDiscriminatorValueKey, } from "./schema-unions.js"; import { @@ -941,18 +942,12 @@ import type { MessageConnection } from "vscode-jsonrpc/node.js"; rpcDefinitions = collectDefinitionCollections(schema as Record); rpcResultProjections = new Map(); rpcResultProjectionDefinitions = new Map(); + const schemaDefinitions = { + ...(rpcDefinitions.$defs as Record), + ...(rpcDefinitions.definitions as Record), + }; for (const method of rpcMethods) { - const resultSchema = getMethodResultSchema(method); - const resultUnion = resultSchema - ? analyseDiscriminatedUnion( - resultSchema, - (variant) => - resolveObjectSchema(variant, rpcDefinitions) ?? - resolveSchema(variant, rpcDefinitions) ?? - variant - ) - : undefined; - if (resultUnion?.unknownVariantPolicy !== "reject") continue; + if (!analyseNestedClosedUnionResult(method.result, schemaDefinitions)) continue; const projection = createRpcResultProjectionBundle(method.result, rpcDefinitions); if (!projection) continue; rpcResultProjections.set(method.rpcMethod, projection.root); From 6f8e8173fd1399f334262f8ef2ab918601ba65c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6khan=20Arkan?= Date: Fri, 4 Sep 2026 15:23:26 +0300 Subject: [PATCH 5/6] Format union codegen test --- nodejs/test/discriminated-union-codegen.test.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/nodejs/test/discriminated-union-codegen.test.ts b/nodejs/test/discriminated-union-codegen.test.ts index fbda41991e..204e4ace47 100644 --- a/nodejs/test/discriminated-union-codegen.test.ts +++ b/nodejs/test/discriminated-union-codegen.test.ts @@ -142,13 +142,9 @@ describe("schema-driven discriminated union codegen", () => { it("promotes every nested synthetic union for Java generation", () => { expect( - analyseNestedClosedUnionResult( - { $ref: "#/definitions/SyntheticResult" }, - definitions - )?.unionDefinitionNames - ).toEqual( - new Set(["SyntheticResult", "SyntheticChoice", "SyntheticSource"]) - ); + analyseNestedClosedUnionResult({ $ref: "#/definitions/SyntheticResult" }, definitions) + ?.unionDefinitionNames + ).toEqual(new Set(["SyntheticResult", "SyntheticChoice", "SyntheticSource"])); expect( collectNestedDiscriminatedUnionTypeNames( { $ref: "#/definitions/SyntheticResult" }, From 3a0524e51f7566aeb273e3f2fbcc7eadc5921fba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6khan=20Arkan?= Date: Fri, 4 Sep 2026 15:51:51 +0300 Subject: [PATCH 6/6] Reduce union projection metadata --- nodejs/src/generated/rpc.ts | 41 +++-------------------------------- scripts/codegen/typescript.ts | 15 ++++++++----- 2 files changed, 13 insertions(+), 43 deletions(-) diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index e68046951b..043c77272e 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -23753,15 +23753,6 @@ const RPC_RESULT_PROJECTION_DEFINITIONS: Record = { } } }, - "CatalogMcpServerCandidateProvenance": { - "kind": "object", - "closed": true, - "properties": { - "authority": null, - "observedAt": null, - "mediaType": null - } - }, "CatalogMcpServerCandidate": { "kind": "object", "closed": true, @@ -23778,19 +23769,7 @@ const RPC_RESULT_PROJECTION_DEFINITIONS: Record = { "kind": "ref", "name": "CatalogCandidateSource" }, - "provenance": { - "kind": "ref", - "name": "CatalogMcpServerCandidateProvenance" - } - } - }, - "CatalogAiSkillCandidateProvenance": { - "kind": "object", - "closed": true, - "properties": { - "authority": null, - "observedAt": null, - "mediaType": null + "provenance": null } }, "CatalogAiSkillCandidate": { @@ -23809,10 +23788,7 @@ const RPC_RESULT_PROJECTION_DEFINITIONS: Record = { "kind": "ref", "name": "CatalogCandidateSource" }, - "provenance": { - "kind": "ref", - "name": "CatalogAiSkillCandidateProvenance" - } + "provenance": null } }, "CatalogCandidate": { @@ -23829,14 +23805,6 @@ const RPC_RESULT_PROJECTION_DEFINITIONS: Record = { } } }, - "CatalogNegotiatedContract": { - "kind": "object", - "closed": true, - "properties": { - "runtimeProtocolVersion": null, - "grantedCapabilities": null - } - }, "CatalogSearchSucceeded": { "kind": "object", "closed": true, @@ -23851,10 +23819,7 @@ const RPC_RESULT_PROJECTION_DEFINITIONS: Record = { } }, "truncated": null, - "negotiated": { - "kind": "ref", - "name": "CatalogNegotiatedContract" - } + "negotiated": null } }, "CatalogNegotiationRefusedError": { diff --git a/scripts/codegen/typescript.ts b/scripts/codegen/typescript.ts index 75313755e1..c608ae640f 100644 --- a/scripts/codegen/typescript.ts +++ b/scripts/codegen/typescript.ts @@ -646,7 +646,8 @@ function buildRpcResultProjection( schema: JSONSchema7, definitions: DefinitionCollections, projectionDefinitions: Map, - resolvingReferences = new Set() + resolvingReferences = new Set(), + projectClosedObject = false ): RpcResultProjectionBuild { if (schema.$ref) { const definitionName = localDefinitionName(schema.$ref); @@ -676,7 +677,8 @@ function buildRpcResultProjection( resolved, definitions, projectionDefinitions, - nestedReferences + nestedReferences, + projectClosedObject ); projectionDefinitions.set(definitionName, built); return built.projection @@ -704,7 +706,8 @@ function buildRpcResultProjection( entry.variants[0].source, definitions, projectionDefinitions, - new Set(resolvingReferences) + new Set(resolvingReferences), + true ).projection; if (!variantProjection) { return { projection: null, containsClosedUnion: false }; @@ -775,10 +778,12 @@ function buildRpcResultProjection( properties[name] = child.projection; containsClosedUnion ||= child.containsClosedUnion; } - const closed = objectSchema.additionalProperties === false; + const closed = + projectClosedObject && objectSchema.additionalProperties === false; return { projection: - closed || Object.values(properties).some((projection) => projection !== null) + projectClosedObject || + Object.values(properties).some((projection) => projection !== null) ? { kind: "object", closed, properties } : null, containsClosedUnion,