From 03184d26a5602e3eda848aeaf6c2ed66957190dd Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:29:33 -0700 Subject: [PATCH 1/5] Expose managed approval requirement on permission requests Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 18f1bbc1-6001-43e2-b293-724505087f6a --- nodejs/src/types.ts | 10 ++++++++-- nodejs/test/session-event-types.test.ts | 12 ++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 5f89ca3bed..ce558190be 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -1097,9 +1097,15 @@ export type SystemMessageConfig = * 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. */ -export type { PermissionRequest } from "./generated/session-events.js"; -import type { PermissionRequest } from "./generated/session-events.js"; +import type { PermissionRequest as GeneratedPermissionRequest } from "./generated/session-events.js"; +export type PermissionRequest = GeneratedPermissionRequest & { + readonly managedApprovalRequired?: boolean; +}; import type { PermissionDecisionRequest } from "./generated/rpc.js"; diff --git a/nodejs/test/session-event-types.test.ts b/nodejs/test/session-event-types.test.ts index 37b49f8a48..9d32fc2eae 100644 --- a/nodejs/test/session-event-types.test.ts +++ b/nodejs/test/session-event-types.test.ts @@ -18,6 +18,7 @@ import { describe, expect, it } from "vitest"; import type { // The aggregate union; must still resolve via the package root. SessionEvent, + PermissionRequest, // *Data payload types from the v0.3.0 generated session-event schema. AssistantMessageData, @@ -103,6 +104,17 @@ describe("Session event type exports (#1156)", () => { expect(data.turnId).toBe("turn-1"); }); + it("exposes whether managed policy requires explicit user approval", () => { + const request: PermissionRequest = { + kind: "read", + path: "/workspace/file.txt", + intention: "Read a file", + managedApprovalRequired: true, + }; + + expect(request.managedApprovalRequired).toBe(true); + }); + it("wraps ToolExecutionStartData inside the exported ToolExecutionStartEvent", () => { const event: ToolExecutionStartEvent = { id: "evt-1", From e5417bd35727d015c97babc7dc992d407c5873dd Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:30:51 -0700 Subject: [PATCH 2/5] docs: align managed selector name with Domain Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: c7f00b84-b0a7-4cdf-aca9-ffd49737f26e --- nodejs/src/types.ts | 3 ++- nodejs/test/session-event-types.test.ts | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index ce558190be..f6e5c81dc1 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -1100,7 +1100,8 @@ export type SystemMessageConfig = * * `managedApprovalRequired` indicates that managed policy requires an explicit * user decision. Hosts should bypass automatic approval and present their - * normal confirmation UI. + * normal confirmation UI. The runtime currently emits it for managed Shell, + * Read, Edit, and Domain selector asks. */ import type { PermissionRequest as GeneratedPermissionRequest } from "./generated/session-events.js"; export type PermissionRequest = GeneratedPermissionRequest & { diff --git a/nodejs/test/session-event-types.test.ts b/nodejs/test/session-event-types.test.ts index 9d32fc2eae..4f68f7d633 100644 --- a/nodejs/test/session-event-types.test.ts +++ b/nodejs/test/session-event-types.test.ts @@ -104,11 +104,11 @@ describe("Session event type exports (#1156)", () => { expect(data.turnId).toBe("turn-1"); }); - it("exposes whether managed policy requires explicit user approval", () => { + it("exposes explicit user approval metadata for managed Domain requests", () => { const request: PermissionRequest = { - kind: "read", - path: "/workspace/file.txt", - intention: "Read a file", + kind: "url", + url: "https://api.example.com/data", + intention: "Fetch domain data", managedApprovalRequired: true, }; From c942cf1447ecaff1cd97eb0374459af25f6b4662 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:35:31 -0700 Subject: [PATCH 3/5] Fix managed permission approval surfaces Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- nodejs/src/index.ts | 15 ++++++----- nodejs/src/types.ts | 27 ++++++++++++++++--- nodejs/test/client.test.ts | 19 ++++++++++++++ nodejs/test/session-event-types.test.ts | 35 +++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 10 deletions(-) diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 352afc6a94..e9702148d8 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 f6e5c81dc1..2a6d3949ad 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,6 +1097,8 @@ 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 @@ -1103,12 +1110,20 @@ export type SystemMessageConfig = * normal confirmation UI. The runtime currently emits it for managed Shell, * Read, Edit, and Domain selector asks. */ -import type { PermissionRequest as GeneratedPermissionRequest } 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}. @@ -1123,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 77149bc4b9..4805b1148a 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 4f68f7d633..c5e00833d0 100644 --- a/nodejs/test/session-event-types.test.ts +++ b/nodejs/test/session-event-types.test.ts @@ -19,6 +19,8 @@ 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, @@ -81,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", () => { @@ -115,6 +122,32 @@ describe("Session event type exports (#1156)", () => { 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", @@ -172,6 +205,7 @@ describe("Session event type exports (#1156)", () => { assertImportable(); assertImportable(); assertImportable(); + assertImportable(); assertImportable(); assertImportable(); @@ -181,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 From 0da1c5995fa4fffe3aecffc94221d6eb21ded841 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Tue, 28 Jul 2026 15:40:10 -0700 Subject: [PATCH 4/5] docs: clarify managed approval behavior Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- nodejs/README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/nodejs/README.md b/nodejs/README.md index e591c7dbc6..6fd2b3d790 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,6 +866,8 @@ 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: From dea81193b798980086df96cfef3e968c5750bd68 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Tue, 28 Jul 2026 17:41:48 -0700 Subject: [PATCH 5/5] docs: guard managed custom approvals Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- nodejs/README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/nodejs/README.md b/nodejs/README.md index 6fd2b3d790..b85c3f22b9 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -870,7 +870,7 @@ For requests with `managedApprovalRequired: true`, `approveAll` returns `{ kind: ### 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"; @@ -878,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