Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions nodejs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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";
Expand All @@ -866,16 +866,23 @@ 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";

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
Expand Down
15 changes: 9 additions & 6 deletions nodejs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -109,6 +110,8 @@ export type {
NamedProviderConfig,
PermissionHandler,
PermissionRequest,
PermissionRequestedData,
PermissionRequestedEvent,
PermissionRequestResult,
ProviderConfig,
ProviderModelConfig,
Expand Down
36 changes: 31 additions & 5 deletions nodejs/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -35,7 +38,9 @@ export type {
ModelBillingTokenPrices,
ModelBillingTokenPricesLongContext,
} from "./generated/rpc.js";
export type SessionEvent = GeneratedSessionEvent;
export type SessionEvent =
| Exclude<GeneratedSessionEvent, { type: "permission.requested" }>
| PermissionRequestedEvent;
export type { ReasoningSummary } from "./generated/session-events.js";
export type { SessionFsProvider } from "./sessionFsProvider.js";
export { createSessionFsAdapter } from "./sessionFsProvider.js";
Expand Down Expand Up @@ -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;
Comment thread
joshspicer marked this conversation as resolved.
};

import type { PermissionDecisionRequest } from "./generated/rpc.js";
export type PermissionRequestedData = Omit<
GeneratedPermissionRequestedData,
"permissionRequest"
> & {
permissionRequest: PermissionRequest;
};

export type PermissionRequestedEvent = Omit<GeneratedPermissionRequestedEvent, "data"> & {
data: PermissionRequestedData;
};

/**
* Permission decision result returned from a {@link PermissionHandler}.
Expand All @@ -1116,7 +1138,11 @@ export type PermissionHandler = (
invocation: { sessionId: string }
) => Promise<PermissionRequestResult> | 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 => ({
Expand Down
19 changes: 19 additions & 0 deletions nodejs/test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,25 @@ async function stopClient(client: CopilotClient): Promise<void> {
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();
Expand Down
47 changes: 47 additions & 0 deletions nodejs/test/session-event-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -80,6 +83,11 @@ type _AssistantMessageEventStaysAlignedWithSessionEventUnion = _AssertEqual<
Extract<SessionEvent, { type: "assistant.message" }>
>;
const _assistantMessageEventAlignmentCheck: _AssistantMessageEventStaysAlignedWithSessionEventUnion = true;
type _PermissionRequestedEventStaysAlignedWithSessionEventUnion = _AssertEqual<
PermissionRequestedEvent,
Extract<SessionEvent, { type: "permission.requested" }>
>;
const _permissionRequestedEventAlignmentCheck: _PermissionRequestedEventStaysAlignedWithSessionEventUnion = true;

describe("Session event type exports (#1156)", () => {
it("exposes the headline ToolExecutionStartData type with a usable shape", () => {
Expand All @@ -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",
Expand Down Expand Up @@ -160,6 +205,7 @@ describe("Session event type exports (#1156)", () => {
assertImportable<ToolExecutionProgressData>();
assertImportable<ToolExecutionStartData>();
assertImportable<UserMessageData>();
assertImportable<PermissionRequestedData>();

assertImportable<AssistantMessageEvent>();
assertImportable<ErrorEvent>();
Expand All @@ -169,6 +215,7 @@ describe("Session event type exports (#1156)", () => {
assertImportable<ToolExecutionCompleteEvent>();
assertImportable<ToolExecutionStartEvent>();
assertImportable<UserMessageEvent>();
assertImportable<PermissionRequestedEvent>();

// Supporting auxiliary types referenced by the *Data shapes — these
// must round-trip through the package root too, otherwise consumers
Expand Down
Loading