Skip to content

Commit c4c57b3

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 dbac2d8 commit c4c57b3

8 files changed

Lines changed: 242 additions & 35 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.

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
*
@@ -1100,7 +1115,7 @@ export class CopilotClient {
11001115

11011116
let serverVersion: number | undefined;
11021117
try {
1103-
const result = await raceAgainstExit(this.rpc.connect({ token: this.effectiveConnectionToken }));
1118+
const result = await raceAgainstExit(this.internalRpc.connect({ token: this.effectiveConnectionToken }));
11041119
serverVersion = result.protocolVersion;
11051120
} catch (err) {
11061121
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.

scripts/codegen/csharp.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -983,6 +983,16 @@ function emitRpcClass(
983983
resolveObjectSchema(schema, rpcDefinitions) ??
984984
resolveSchema(schema, rpcDefinitions) ??
985985
schema;
986+
// Visibility is driven by the JSON Schema definition itself (set via
987+
// `.asInternal()` on the originating Zod schema). The runtime schema
988+
// generator enforces that no public method references an internal type,
989+
// so it's safe to upgrade callers' default to internal here.
990+
if (
991+
(schema as Record<string, unknown>).visibility === "internal" ||
992+
(effectiveSchema as Record<string, unknown>).visibility === "internal"
993+
) {
994+
visibility = "internal";
995+
}
986996
const schemaKey = stableStringify(effectiveSchema);
987997
const existingSchema = emittedRpcClassSchemas.get(className);
988998
if (existingSchema) {
@@ -1169,13 +1179,15 @@ function emitServerInstanceMethod(
11691179
groupDeprecated: boolean
11701180
): void {
11711181
const methodName = toPascalCase(name);
1182+
const isInternal = method.visibility === "internal";
1183+
const methodVisibility = isInternal ? "internal" : "public";
11721184
const resultSchema = getMethodResultSchema(method);
11731185
let resultClassName = !isVoidSchema(resultSchema) ? resultTypeName(method) : "";
11741186
if (!isVoidSchema(resultSchema) && method.stability === "experimental") {
11751187
experimentalRpcTypes.add(resultClassName);
11761188
}
11771189
if (isObjectSchema(resultSchema)) {
1178-
const resultClass = emitRpcClass(resultClassName, resultSchema!, "public", classes);
1190+
const resultClass = emitRpcClass(resultClassName, resultSchema!, methodVisibility, classes);
11791191
if (resultClass) classes.push(resultClass);
11801192
} else if (!isVoidSchema(resultSchema)) {
11811193
resultClassName = emitNonObjectResultType(resultClassName, resultSchema!, classes);
@@ -1227,7 +1239,7 @@ function emitServerInstanceMethod(
12271239
sigParams.push("CancellationToken cancellationToken = default");
12281240

12291241
const taskType = !isVoidSchema(resultSchema) ? `Task<${resultClassName}>` : "Task";
1230-
lines.push(`${indent}public async ${taskType} ${methodName}Async(${sigParams.join(", ")})`);
1242+
lines.push(`${indent}${methodVisibility} async ${taskType} ${methodName}Async(${sigParams.join(", ")})`);
12311243
lines.push(`${indent}{`);
12321244
if (requestClassName && bodyAssignments.length > 0) {
12331245
lines.push(`${indent} var request = new ${requestClassName} { ${bodyAssignments.join(", ")} };`);
@@ -1275,13 +1287,15 @@ function emitSessionRpcClasses(node: Record<string, unknown>, classes: string[])
12751287

12761288
function emitSessionMethod(key: string, method: RpcMethod, lines: string[], classes: string[], indent: string, groupExperimental: boolean, groupDeprecated: boolean): void {
12771289
const methodName = toPascalCase(key);
1290+
const isInternal = method.visibility === "internal";
1291+
const methodVisibility = isInternal ? "internal" : "public";
12781292
const resultSchema = getMethodResultSchema(method);
12791293
let resultClassName = !isVoidSchema(resultSchema) ? resultTypeName(method) : "";
12801294
if (!isVoidSchema(resultSchema) && method.stability === "experimental") {
12811295
experimentalRpcTypes.add(resultClassName);
12821296
}
12831297
if (isObjectSchema(resultSchema)) {
1284-
const resultClass = emitRpcClass(resultClassName, resultSchema!, "public", classes);
1298+
const resultClass = emitRpcClass(resultClassName, resultSchema!, methodVisibility, classes);
12851299
if (resultClass) classes.push(resultClass);
12861300
} else if (!isVoidSchema(resultSchema)) {
12871301
resultClassName = emitNonObjectResultType(resultClassName, resultSchema!, classes);
@@ -1327,7 +1341,7 @@ function emitSessionMethod(key: string, method: RpcMethod, lines: string[], clas
13271341
sigParams.push("CancellationToken cancellationToken = default");
13281342

13291343
const taskType = !isVoidSchema(resultSchema) ? `Task<${resultClassName}>` : "Task";
1330-
lines.push(`${indent}public async ${taskType} ${methodName}Async(${sigParams.join(", ")})`);
1344+
lines.push(`${indent}${methodVisibility} async ${taskType} ${methodName}Async(${sigParams.join(", ")})`);
13311345
lines.push(`${indent}{`, `${indent} var request = new ${requestClassName} { ${bodyAssignments.join(", ")} };`);
13321346
if (!isVoidSchema(resultSchema)) {
13331347
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)