Skip to content

Commit b079ec3

Browse files
Hide internal RPC methods from generated public API surface
The schema can now flag methods and types as internal. The codegen splits internal RPC methods into parallel structures so they don't appear on the public client API: - TypeScript: createInternalServerRpc / createInternalSessionRpc factories alongside the existing public ones; client.ts wires connect() through a private internalRpc getter. - C#: ConnectAsync and ConnectResult are emitted with the internal access modifier (real assembly-boundary access control). - Python: parallel InternalServerRpc / InternalSessionRpc classes with ':meta private:' docstrings. - Go: parallel InternalServerRpc / InternalSessionRpc types with their own unexported backing struct and NewInternalServerRpc constructor. - Internal type definitions get a per-language doc-comment marker. - New filterNodeByVisibility() helper in scripts/codegen/utils.ts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0961bfa commit b079ec3

10 files changed

Lines changed: 281 additions & 46 deletions

File tree

dotnet/src/Generated/Rpc.cs

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

go/rpc/generated_rpc.go

Lines changed: 30 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nodejs/src/client.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
StreamMessageReader,
2727
StreamMessageWriter,
2828
} from "vscode-jsonrpc/node.js";
29-
import { createServerRpc, registerClientSessionApiHandlers } from "./generated/rpc.js";
29+
import { createServerRpc, createInternalServerRpc, registerClientSessionApiHandlers } from "./generated/rpc.js";
3030
import { getSdkProtocolVersion } from "./sdkProtocolVersion.js";
3131
import { CopilotSession, NO_RESULT_PERMISSION_V2_ERROR } from "./session.js";
3232
import { createSessionFsAdapter } from "./sessionFsProvider.js";
@@ -246,6 +246,7 @@ export class CopilotClient {
246246
Set<(event: SessionLifecycleEvent) => void>
247247
> = new Map();
248248
private _rpc: ReturnType<typeof createServerRpc> | null = null;
249+
private _internalRpc: ReturnType<typeof createInternalServerRpc> | null = null;
249250
private processExitPromise: Promise<never> | null = null; // Rejects when CLI process exits
250251
private negotiatedProtocolVersion: number | null = null;
251252
/** Connection-level session filesystem config, set via constructor option. */
@@ -265,6 +266,20 @@ export class CopilotClient {
265266
return this._rpc;
266267
}
267268

269+
/**
270+
* Internal RPC surface (e.g. handshake helpers). Not part of the public API.
271+
* @internal
272+
*/
273+
private get internalRpc(): ReturnType<typeof createInternalServerRpc> {
274+
if (!this.connection) {
275+
throw new Error("Client is not connected. Call start() first.");
276+
}
277+
if (!this._internalRpc) {
278+
this._internalRpc = createInternalServerRpc(this.connection);
279+
}
280+
return this._internalRpc;
281+
}
282+
268283
/**
269284
* Creates a new CopilotClient instance.
270285
*
@@ -1099,7 +1114,7 @@ export class CopilotClient {
10991114

11001115
let serverVersion: number | undefined;
11011116
try {
1102-
const result = await raceAgainstExit(this.rpc.connect({ token: this.effectiveConnectionToken }));
1117+
const result = await raceAgainstExit(this.internalRpc.connect({ token: this.effectiveConnectionToken }));
11031118
serverVersion = result.protocolVersion;
11041119
} catch (err) {
11051120
if (err instanceof ResponseError && err.code === ErrorCodes.MethodNotFound) {

nodejs/src/generated/rpc.ts

Lines changed: 14 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

python/copilot/generated/rpc.py

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/codegen/csharp.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -977,6 +977,16 @@ function emitRpcClass(
977977
resolveObjectSchema(schema, rpcDefinitions) ??
978978
resolveSchema(schema, rpcDefinitions) ??
979979
schema;
980+
// Visibility is driven by the JSON Schema definition itself (set via
981+
// `.asInternal()` on the originating Zod schema). The runtime schema
982+
// generator enforces that no public method references an internal type,
983+
// so it's safe to upgrade callers' default to internal here.
984+
if (
985+
(schema as Record<string, unknown>).visibility === "internal" ||
986+
(effectiveSchema as Record<string, unknown>).visibility === "internal"
987+
) {
988+
visibility = "internal";
989+
}
980990
const schemaKey = stableStringify(effectiveSchema);
981991
const existingSchema = emittedRpcClassSchemas.get(className);
982992
if (existingSchema) {
@@ -1163,13 +1173,15 @@ function emitServerInstanceMethod(
11631173
groupDeprecated: boolean
11641174
): void {
11651175
const methodName = toPascalCase(name);
1176+
const isInternal = method.visibility === "internal";
1177+
const methodVisibility = isInternal ? "internal" : "public";
11661178
const resultSchema = getMethodResultSchema(method);
11671179
let resultClassName = !isVoidSchema(resultSchema) ? resultTypeName(method) : "";
11681180
if (!isVoidSchema(resultSchema) && method.stability === "experimental") {
11691181
experimentalRpcTypes.add(resultClassName);
11701182
}
11711183
if (isObjectSchema(resultSchema)) {
1172-
const resultClass = emitRpcClass(resultClassName, resultSchema!, "public", classes);
1184+
const resultClass = emitRpcClass(resultClassName, resultSchema!, methodVisibility, classes);
11731185
if (resultClass) classes.push(resultClass);
11741186
} else if (!isVoidSchema(resultSchema)) {
11751187
resultClassName = emitNonObjectResultType(resultClassName, resultSchema!, classes);
@@ -1221,7 +1233,7 @@ function emitServerInstanceMethod(
12211233
sigParams.push("CancellationToken cancellationToken = default");
12221234

12231235
const taskType = !isVoidSchema(resultSchema) ? `Task<${resultClassName}>` : "Task";
1224-
lines.push(`${indent}public async ${taskType} ${methodName}Async(${sigParams.join(", ")})`);
1236+
lines.push(`${indent}${methodVisibility} async ${taskType} ${methodName}Async(${sigParams.join(", ")})`);
12251237
lines.push(`${indent}{`);
12261238
if (requestClassName && bodyAssignments.length > 0) {
12271239
lines.push(`${indent} var request = new ${requestClassName} { ${bodyAssignments.join(", ")} };`);
@@ -1269,13 +1281,15 @@ function emitSessionRpcClasses(node: Record<string, unknown>, classes: string[])
12691281

12701282
function emitSessionMethod(key: string, method: RpcMethod, lines: string[], classes: string[], indent: string, groupExperimental: boolean, groupDeprecated: boolean): void {
12711283
const methodName = toPascalCase(key);
1284+
const isInternal = method.visibility === "internal";
1285+
const methodVisibility = isInternal ? "internal" : "public";
12721286
const resultSchema = getMethodResultSchema(method);
12731287
let resultClassName = !isVoidSchema(resultSchema) ? resultTypeName(method) : "";
12741288
if (!isVoidSchema(resultSchema) && method.stability === "experimental") {
12751289
experimentalRpcTypes.add(resultClassName);
12761290
}
12771291
if (isObjectSchema(resultSchema)) {
1278-
const resultClass = emitRpcClass(resultClassName, resultSchema!, "public", classes);
1292+
const resultClass = emitRpcClass(resultClassName, resultSchema!, methodVisibility, classes);
12791293
if (resultClass) classes.push(resultClass);
12801294
} else if (!isVoidSchema(resultSchema)) {
12811295
resultClassName = emitNonObjectResultType(resultClassName, resultSchema!, classes);
@@ -1321,7 +1335,7 @@ function emitSessionMethod(key: string, method: RpcMethod, lines: string[], clas
13211335
sigParams.push("CancellationToken cancellationToken = default");
13221336

13231337
const taskType = !isVoidSchema(resultSchema) ? `Task<${resultClassName}>` : "Task";
1324-
lines.push(`${indent}public async ${taskType} ${methodName}Async(${sigParams.join(", ")})`);
1338+
lines.push(`${indent}${methodVisibility} async ${taskType} ${methodName}Async(${sigParams.join(", ")})`);
13251339
lines.push(`${indent}{`, `${indent} var request = new ${requestClassName} { ${bodyAssignments.join(", ")} };`);
13261340
if (!isVoidSchema(resultSchema)) {
13271341
lines.push(`${indent} return await CopilotClient.InvokeRpcAsync<${resultClassName}>(_rpc, "${method.rpcMethod}", [request], cancellationToken);`, `${indent}}`);

scripts/codegen/go.ts

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { FetchingJSONSchemaStore, InputData, JSONSchemaInput, quicktype } from "
1313
import { promisify } from "util";
1414
import {
1515
cloneSchemaForCodegen,
16+
filterNodeByVisibility,
1617
fixNullableRequiredRefsInApiSchema,
1718
getApiSchemaPath,
1819
getRpcSchemaTypeName,
@@ -1161,6 +1162,21 @@ async function generateRpc(schemaPath?: string): Promise<void> {
11611162
`// Deprecated: ${typeName} is deprecated and will be removed in a future version.\n$1`
11621163
);
11631164
}
1165+
1166+
// Annotate internal data types (driven by the JSON Schema definition's
1167+
// `visibility: "internal"` flag, set via `.asInternal()` on the Zod source).
1168+
const internalTypeNames = new Set<string>();
1169+
for (const [name, def] of Object.entries(allDefinitions)) {
1170+
if (def && typeof def === "object" && (def as Record<string, unknown>).visibility === "internal") {
1171+
internalTypeNames.add(name);
1172+
}
1173+
}
1174+
for (const typeName of internalTypeNames) {
1175+
qtCode = qtCode.replace(
1176+
new RegExp(`^(type ${typeName} struct)`, "m"),
1177+
`// Internal: ${typeName} is an internal SDK API and is not part of the public surface.\n$1`
1178+
);
1179+
}
11641180
// Remove trailing blank lines from quicktype output before appending
11651181
qtCode = qtCode.replace(/\n+$/, "");
11661182
// Replace interface{} with any (quicktype emits the pre-1.18 form)
@@ -1196,12 +1212,18 @@ async function generateRpc(schemaPath?: string): Promise<void> {
11961212

11971213
// Emit ServerRpc
11981214
if (schema.server) {
1199-
emitRpcWrapper(lines, schema.server, false, resolveType, fieldNames);
1215+
const publicNode = filterNodeByVisibility(schema.server, "public");
1216+
if (publicNode) emitRpcWrapper(lines, publicNode, false, resolveType, fieldNames, "");
1217+
const internalNode = filterNodeByVisibility(schema.server, "internal");
1218+
if (internalNode) emitRpcWrapper(lines, internalNode, false, resolveType, fieldNames, "Internal");
12001219
}
12011220

12021221
// Emit SessionRpc
12031222
if (schema.session) {
1204-
emitRpcWrapper(lines, schema.session, true, resolveType, fieldNames);
1223+
const publicNode = filterNodeByVisibility(schema.session, "public");
1224+
if (publicNode) emitRpcWrapper(lines, publicNode, true, resolveType, fieldNames, "");
1225+
const internalNode = filterNodeByVisibility(schema.session, "internal");
1226+
if (internalNode) emitRpcWrapper(lines, internalNode, true, resolveType, fieldNames, "Internal");
12051227
}
12061228

12071229
if (schema.clientSession) {
@@ -1257,13 +1279,17 @@ function emitApiGroup(
12571279
}
12581280
}
12591281

1260-
function emitRpcWrapper(lines: string[], node: Record<string, unknown>, isSession: boolean, resolveType: (name: string) => string, fieldNames: Map<string, Map<string, string>>): void {
1282+
function emitRpcWrapper(lines: string[], node: Record<string, unknown>, isSession: boolean, resolveType: (name: string) => string, fieldNames: Map<string, Map<string, string>>, classPrefix: string = ""): void {
12611283
const groups = Object.entries(node).filter(([, v]) => typeof v === "object" && v !== null && !isRpcMethod(v));
12621284
const topLevelMethods = Object.entries(node).filter(([, v]) => isRpcMethod(v));
12631285

1264-
const wrapperName = isSession ? "SessionRpc" : "ServerRpc";
1286+
const wrapperName = classPrefix + (isSession ? "SessionRpc" : "ServerRpc");
12651287
const apiSuffix = "Api";
1266-
const serviceName = isSession ? "sessionApi" : "serverApi";
1288+
// Lowercase the prefix so the unexported service struct stays unexported in Go.
1289+
const prefixLower = classPrefix ? classPrefix.charAt(0).toLowerCase() + classPrefix.slice(1) : "";
1290+
const serviceName = prefixLower
1291+
? prefixLower + (isSession ? "SessionApi" : "ServerApi")
1292+
: (isSession ? "sessionApi" : "serverApi");
12671293

12681294
// Emit the common service struct (unexported, shared by all API groups via type cast)
12691295
lines.push(`type ${serviceName} struct {`);
@@ -1274,7 +1300,7 @@ function emitRpcWrapper(lines: string[], node: Record<string, unknown>, isSessio
12741300

12751301
// Emit API types for groups
12761302
for (const [groupName, groupNode] of groups) {
1277-
const prefix = isSession ? "" : "Server";
1303+
const prefix = classPrefix + (isSession ? "" : "Server");
12781304
const apiName = prefix + toPascalCase(groupName) + apiSuffix;
12791305
const groupExperimental = isNodeFullyExperimental(groupNode as Record<string, unknown>);
12801306
const groupDeprecated = isNodeFullyDeprecated(groupNode as Record<string, unknown>);
@@ -1288,12 +1314,14 @@ function emitRpcWrapper(lines: string[], node: Record<string, unknown>, isSessio
12881314
const pad = (name: string) => name.padEnd(maxFieldLen);
12891315

12901316
// Emit wrapper struct
1291-
lines.push(`// ${wrapperName} provides typed ${isSession ? "session" : "server"}-scoped RPC methods.`);
1317+
lines.push(classPrefix === "Internal"
1318+
? `// ${wrapperName} provides internal SDK ${isSession ? "session" : "server"}-scoped RPC methods (handshake helpers etc.). Not part of the public API.`
1319+
: `// ${wrapperName} provides typed ${isSession ? "session" : "server"}-scoped RPC methods.`);
12921320
lines.push(`type ${wrapperName} struct {`);
12931321
lines.push(`\t${pad("common")} ${serviceName} // Reuse a single struct instead of allocating one for each service on the heap.`);
12941322
lines.push(``);
12951323
for (const [groupName] of groups) {
1296-
const prefix = isSession ? "" : "Server";
1324+
const prefix = classPrefix + (isSession ? "" : "Server");
12971325
lines.push(`\t${pad(toPascalCase(groupName))} *${prefix}${toPascalCase(groupName)}${apiSuffix}`);
12981326
}
12991327
lines.push(`}`);
@@ -1315,7 +1343,7 @@ function emitRpcWrapper(lines: string[], node: Record<string, unknown>, isSessio
13151343
lines.push(`\tr.common = ${serviceName}{client: client}`);
13161344
}
13171345
for (const [groupName] of groups) {
1318-
const prefix = isSession ? "" : "Server";
1346+
const prefix = classPrefix + (isSession ? "" : "Server");
13191347
lines.push(`\tr.${toPascalCase(groupName)} = (*${prefix}${toPascalCase(groupName)}${apiSuffix})(&r.common)`);
13201348
}
13211349
lines.push(`\treturn r`);
@@ -1348,6 +1376,9 @@ function emitMethod(lines: string[], receiver: string, name: string, method: Rpc
13481376
if (method.stability === "experimental" && !groupExperimental) {
13491377
lines.push(`// Experimental: ${methodName} is an experimental API and may change or be removed in future versions.`);
13501378
}
1379+
if (method.visibility === "internal") {
1380+
lines.push(`// Internal: ${methodName} is part of the SDK's internal handshake/plumbing; external callers should not use it.`);
1381+
}
13511382
const sig = hasParams
13521383
? `func (a *${receiver}) ${methodName}(ctx context.Context, params *${paramsType}) (*${resultType}, error)`
13531384
: `func (a *${receiver}) ${methodName}(ctx context.Context) (*${resultType}, error)`;

0 commit comments

Comments
 (0)