diff --git a/nodejs/README.md b/nodejs/README.md index e591c7dbc..b85c3f22b 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -36,7 +36,7 @@ import { CopilotClient, approveAll } from "@github/copilot-sdk"; const client = new CopilotClient(); await client.start(); -// Create a session (onPermissionRequest is optional; approveAll allows every tool) +// approveAll approves ordinary requests; managed requests still require a human decision. const session = await client.createSession({ model: "gpt-5", onPermissionRequest: approveAll, @@ -130,7 +130,7 @@ Create a new conversation session. - `systemMessage?: SystemMessageConfig` - System message customization (see below) - `infiniteSessions?: InfiniteSessionConfig` - Configure automatic context compaction (see below) - `provider?: ProviderConfig` - Custom API provider configuration (BYOK - Bring Your Own Key). See [Custom Providers](#custom-providers) section. -- `onPermissionRequest?: PermissionHandler` - Optional handler called before each tool execution to approve or deny it. When omitted, permission requests are emitted as events and left pending for manual resolution. Use `approveAll` to allow everything, or provide a custom function for fine-grained control. See [Permission Handling](#permission-handling) section. +- `onPermissionRequest?: PermissionHandler` - Optional handler called before each tool execution to approve or deny it. When omitted, permission requests are emitted as events and left pending for manual resolution. Use `approveAll` to approve ordinary requests automatically; requests with `managedApprovalRequired: true` remain pending for explicit resolution through a human-facing host flow. Provide a custom function for other fine-grained control. See [Permission Handling](#permission-handling) section. - `onUserInputRequest?: UserInputHandler` - Handler for user input requests from the agent. Enables the `ask_user` tool. See [User Input Requests](#user-input-requests) section. - `onElicitationRequest?: ElicitationHandler` - Handler for elicitation requests dispatched by the server. Enables this client to present form-based UI dialogs on behalf of the agent or other session participants. See [Elicitation Requests](#elicitation-requests) section. - `hooks?: SessionHooks` - Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section. @@ -855,7 +855,7 @@ An `onPermissionRequest` handler is optional when you create or resume a session ### Approve All (simplest) -Use the built-in `approveAll` helper to allow every tool call without any checks: +Use the built-in `approveAll` helper to approve ordinary permission requests automatically: ```typescript import { CopilotClient, approveAll } from "@github/copilot-sdk"; @@ -866,9 +866,11 @@ const session = await client.createSession({ }); ``` +For requests with `managedApprovalRequired: true`, `approveAll` returns `{ kind: "no-result" }`. The request remains pending and the host must present a human-facing confirmation flow to resolve it explicitly. + ### Custom Permission Handler -Provide your own function to inspect each request and apply custom logic: +Provide your own function to inspect each request and apply custom logic. Check `managedApprovalRequired` before any automatic approval: ```typescript import type { PermissionRequest, PermissionRequestResult } from "@github/copilot-sdk"; @@ -876,6 +878,11 @@ import type { PermissionRequest, PermissionRequestResult } from "@github/copilot const session = await client.createSession({ model: "gpt-5", onPermissionRequest: (request: PermissionRequest, invocation): PermissionRequestResult => { + if (request.managedApprovalRequired === true) { + // Leave the request pending for the host's human-facing confirmation flow. + return { kind: "no-result" }; + } + // request.kind — what type of operation is being requested: // "shell" — executing a shell command // "write" — writing or editing a file diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 352afc6a9..e9702148d 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -39,13 +39,14 @@ export { // consumers can import them directly from "@github/copilot-sdk" instead of // reaching into the package's internal dist layout. See issue #1156. // -// Three names from this file are also explicitly exported elsewhere in this +// Five names from this file are also explicitly exported elsewhere in this // module — `SessionEvent` (re-exported below from `./types.js`), -// `PermissionRequest` (re-exported below from `./types.js`), and -// `AssistantMessageEvent` (re-exported above from `./session.js`). Per the -// ECMAScript module spec, the explicit named re-exports shadow the names -// arriving via `export type *`, so the hand-authored public API surface for -// those three identifiers is preserved unchanged. +// `PermissionRequest` (re-exported below from `./types.js`), +// `PermissionRequestedData`/`PermissionRequestedEvent` (also re-exported below +// from `./types.js`), and `AssistantMessageEvent` (re-exported above from +// `./session.js`). Per the ECMAScript module spec, the explicit named re-exports +// shadow the names arriving via `export type *`, so the hand-authored public API +// surface for those five identifiers is preserved unchanged. export type * from "./generated/session-events.js"; export type { CommandContext, @@ -109,6 +110,8 @@ export type { NamedProviderConfig, PermissionHandler, PermissionRequest, + PermissionRequestedData, + PermissionRequestedEvent, PermissionRequestResult, ProviderConfig, ProviderModelConfig, diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 5f89ca3be..2a6d3949a 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -11,6 +11,9 @@ import type { Canvas } from "./canvas.js"; import type { SessionFsProvider } from "./sessionFsProvider.js"; import type { CopilotRequestHandler } from "./copilotRequestHandler.js"; import type { + PermissionRequest as GeneratedPermissionRequest, + PermissionRequestedData as GeneratedPermissionRequestedData, + PermissionRequestedEvent as GeneratedPermissionRequestedEvent, ReasoningSummary, SessionLimitsConfig, SessionEvent as GeneratedSessionEvent, @@ -35,7 +38,9 @@ export type { ModelBillingTokenPrices, ModelBillingTokenPricesLongContext, } from "./generated/rpc.js"; -export type SessionEvent = GeneratedSessionEvent; +export type SessionEvent = + | Exclude + | PermissionRequestedEvent; export type { ReasoningSummary } from "./generated/session-events.js"; export type { SessionFsProvider } from "./sessionFsProvider.js"; export { createSessionFsAdapter } from "./sessionFsProvider.js"; @@ -1092,16 +1097,33 @@ export type SystemMessageConfig = | SystemMessageReplaceConfig | SystemMessageCustomizeConfig; +import type { PermissionDecisionRequest } from "./generated/rpc.js"; + /** * Permission request types from the server. This is the generated * discriminated union from the runtime schema — switch on `kind` to * access the variant-specific fields (e.g. shell `commands`, write * `fileName`/`diff`, mcp `toolName`/`args`). + * + * `managedApprovalRequired` indicates that managed policy requires an explicit + * user decision. Hosts should bypass automatic approval and present their + * normal confirmation UI. The runtime currently emits it for managed Shell, + * Read, Edit, and Domain selector asks. */ -export type { PermissionRequest } from "./generated/session-events.js"; -import type { PermissionRequest } from "./generated/session-events.js"; +export type PermissionRequest = GeneratedPermissionRequest & { + readonly managedApprovalRequired?: boolean; +}; -import type { PermissionDecisionRequest } from "./generated/rpc.js"; +export type PermissionRequestedData = Omit< + GeneratedPermissionRequestedData, + "permissionRequest" +> & { + permissionRequest: PermissionRequest; +}; + +export type PermissionRequestedEvent = Omit & { + data: PermissionRequestedData; +}; /** * Permission decision result returned from a {@link PermissionHandler}. @@ -1116,7 +1138,11 @@ export type PermissionHandler = ( invocation: { sessionId: string } ) => Promise | PermissionRequestResult; -export const approveAll: PermissionHandler = () => ({ kind: "approve-once" }); +/** + * Approves permission requests unless managed policy requires an explicit human decision. + */ +export const approveAll: PermissionHandler = (request) => + request.managedApprovalRequired ? { kind: "no-result" } : { kind: "approve-once" }; export const defaultJoinSessionPermissionHandler: PermissionHandler = (): PermissionRequestResult => ({ diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 77149bc4b..4805b1148 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -19,6 +19,25 @@ async function stopClient(client: CopilotClient): Promise { await client.stop(); } +describe("approveAll", () => { + const request = { + kind: "url" as const, + url: "https://api.example.com/data", + intention: "Fetch domain data", + }; + const invocation = { sessionId: "session-1" }; + + it("approves ordinary permission requests", () => { + expect(approveAll(request, invocation)).toEqual({ kind: "approve-once" }); + }); + + it("leaves managed permission requests pending for human approval", () => { + expect(approveAll({ ...request, managedApprovalRequired: true }, invocation)).toEqual({ + kind: "no-result", + }); + }); +}); + describe("CopilotClient", () => { it("disposes the stdio connection when child stdin emits an error", async () => { const client = new CopilotClient(); diff --git a/nodejs/test/session-event-types.test.ts b/nodejs/test/session-event-types.test.ts index 37b49f8a4..c5e00833d 100644 --- a/nodejs/test/session-event-types.test.ts +++ b/nodejs/test/session-event-types.test.ts @@ -18,6 +18,9 @@ import { describe, expect, it } from "vitest"; import type { // The aggregate union; must still resolve via the package root. SessionEvent, + PermissionRequest, + PermissionRequestedData, + PermissionRequestedEvent, // *Data payload types from the v0.3.0 generated session-event schema. AssistantMessageData, @@ -80,6 +83,11 @@ type _AssistantMessageEventStaysAlignedWithSessionEventUnion = _AssertEqual< Extract >; const _assistantMessageEventAlignmentCheck: _AssistantMessageEventStaysAlignedWithSessionEventUnion = true; +type _PermissionRequestedEventStaysAlignedWithSessionEventUnion = _AssertEqual< + PermissionRequestedEvent, + Extract +>; +const _permissionRequestedEventAlignmentCheck: _PermissionRequestedEventStaysAlignedWithSessionEventUnion = true; describe("Session event type exports (#1156)", () => { it("exposes the headline ToolExecutionStartData type with a usable shape", () => { @@ -103,6 +111,43 @@ describe("Session event type exports (#1156)", () => { expect(data.turnId).toBe("turn-1"); }); + it("exposes explicit user approval metadata for managed Domain requests", () => { + const request: PermissionRequest = { + kind: "url", + url: "https://api.example.com/data", + intention: "Fetch domain data", + managedApprovalRequired: true, + }; + + expect(request.managedApprovalRequired).toBe(true); + }); + + it("exposes managed approval metadata through permission event types", () => { + const data: PermissionRequestedData = { + permissionRequest: { + kind: "url", + url: "https://api.example.com/data", + intention: "Fetch domain data", + managedApprovalRequired: true, + }, + requestId: "permission-1", + }; + const event: SessionEvent = { + id: "evt-permission-1", + parentId: null, + timestamp: "2026-01-01T00:00:00.000Z", + type: "permission.requested", + data, + }; + + if (event.type !== "permission.requested") { + throw new Error("expected permission.requested narrowing"); + } + + const permissionEvent: PermissionRequestedEvent = event; + expect(permissionEvent.data.permissionRequest.managedApprovalRequired).toBe(true); + }); + it("wraps ToolExecutionStartData inside the exported ToolExecutionStartEvent", () => { const event: ToolExecutionStartEvent = { id: "evt-1", @@ -160,6 +205,7 @@ describe("Session event type exports (#1156)", () => { assertImportable(); assertImportable(); assertImportable(); + assertImportable(); assertImportable(); assertImportable(); @@ -169,6 +215,7 @@ describe("Session event type exports (#1156)", () => { assertImportable(); assertImportable(); assertImportable(); + assertImportable(); // Supporting auxiliary types referenced by the *Data shapes — these // must round-trip through the package root too, otherwise consumers