diff --git a/docs/features/custom-agents.md b/docs/features/custom-agents.md index c36b856a4..71fd9b4b1 100644 --- a/docs/features/custom-agents.md +++ b/docs/features/custom-agents.md @@ -54,7 +54,7 @@ const session = await client.createSession({ prompt: "You are a code editor. Make minimal, surgical changes to files as requested.", }, ], - onPermissionRequest: async () => ({ kind: "approved" }), + onPermissionRequest: async () => ({ kind: "approve-once" }), }); ``` @@ -71,7 +71,7 @@ client = CopilotClient() await client.start() session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approved"), + on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), model="gpt-4.1", custom_agents=[ { @@ -284,7 +284,7 @@ const session = await client.createSession({ skills: ["markdown-lint"], }, ], - onPermissionRequest: async () => ({ kind: "approved" }), + onPermissionRequest: async () => ({ kind: "approve-once" }), }); ``` diff --git a/docs/features/hooks.md b/docs/features/hooks.md index d8684d202..db9ad72ec 100644 --- a/docs/features/hooks.md +++ b/docs/features/hooks.md @@ -50,7 +50,7 @@ const session = await client.createSession({ onPostToolUse: async (input, invocation) => { /* ... */ }, // ... add only the hooks you need }, - onPermissionRequest: async () => ({ kind: "approved" }), + onPermissionRequest: async () => ({ kind: "approve-once" }), }); ``` @@ -61,12 +61,13 @@ const session = await client.createSession({ ```python from copilot import CopilotClient +from copilot.session import PermissionRequestResult client = CopilotClient() await client.start() session = await client.create_session( - on_permission_request=lambda req, inv: {"kind": "approved"}, + on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), hooks={ "on_session_start": on_session_start, "on_pre_tool_use": on_pre_tool_use, @@ -113,7 +114,7 @@ func main() { OnPostToolUse: onPostToolUse, }, OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: "approved"}, nil + return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil }, }) _ = session @@ -133,7 +134,7 @@ session, err := client.CreateSession(ctx, &copilot.SessionConfig{ // ... add only the hooks you need }, OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: "approved"}, nil + return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil }, }) ``` @@ -251,7 +252,7 @@ const session = await client.createSession({ return { permissionDecision: "allow" }; }, }, - onPermissionRequest: async () => ({ kind: "approved" }), + onPermissionRequest: async () => ({ kind: "approve-once" }), }); ``` @@ -261,6 +262,8 @@ const session = await client.createSession({ Python ```python +from copilot.session import PermissionRequestResult + READ_ONLY_TOOLS = ["read_file", "glob", "grep", "view"] async def on_pre_tool_use(input_data, invocation): @@ -273,7 +276,7 @@ async def on_pre_tool_use(input_data, invocation): return {"permissionDecision": "allow"} session = await client.create_session( - on_permission_request=lambda req, inv: {"kind": "approved"}, + on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), hooks={"on_pre_tool_use": on_pre_tool_use}, ) ``` @@ -463,7 +466,7 @@ const session = await client.createSession({ return { permissionDecision: "allow" }; }, }, - onPermissionRequest: async () => ({ kind: "approved" }), + onPermissionRequest: async () => ({ kind: "approve-once" }), }); ``` @@ -481,7 +484,7 @@ const session = await client.createSession({ return { permissionDecision: "allow" }; }, }, - onPermissionRequest: async () => ({ kind: "approved" }), + onPermissionRequest: async () => ({ kind: "approve-once" }), }); ``` @@ -563,7 +566,7 @@ const session = await client.createSession({ return null; }, }, - onPermissionRequest: async () => ({ kind: "approved" }), + onPermissionRequest: async () => ({ kind: "approve-once" }), }); ``` @@ -575,6 +578,7 @@ const session = await client.createSession({ ```python import json, aiofiles +from copilot.session import PermissionRequestResult audit_log = [] @@ -626,7 +630,7 @@ async def on_session_end(input_data, invocation): return None session = await client.create_session( - on_permission_request=lambda req, inv: {"kind": "approved"}, + on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), hooks={ "on_session_start": on_session_start, "on_user_prompt_submitted": on_user_prompt_submitted, @@ -661,7 +665,7 @@ const session = await client.createSession({ : null; }, }, - onPermissionRequest: async () => ({ kind: "approved" }), + onPermissionRequest: async () => ({ kind: "approve-once" }), }); ``` @@ -694,7 +698,7 @@ const session = await client.createSession({ return null; }, }, - onPermissionRequest: async () => ({ kind: "approved" }), + onPermissionRequest: async () => ({ kind: "approve-once" }), }); ``` @@ -705,6 +709,7 @@ const session = await client.createSession({ ```python import subprocess +from copilot.session import PermissionRequestResult async def on_session_end(input_data, invocation): sid = invocation["session_id"][:8] @@ -723,7 +728,7 @@ async def on_error_occurred(input_data, invocation): return None session = await client.create_session( - on_permission_request=lambda req, inv: {"kind": "approved"}, + on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), hooks={ "on_session_end": on_session_end, "on_error_occurred": on_error_occurred, @@ -750,7 +755,7 @@ const session = await client.createSession({ return null; }, }, - onPermissionRequest: async () => ({ kind: "approved" }), + onPermissionRequest: async () => ({ kind: "approve-once" }), }); ``` @@ -774,7 +779,7 @@ const session = await client.createSession({ return null; }, }, - onPermissionRequest: async () => ({ kind: "approved" }), + onPermissionRequest: async () => ({ kind: "approve-once" }), }); ``` @@ -800,7 +805,7 @@ const session = await client.createSession({ }; }, }, - onPermissionRequest: async () => ({ kind: "approved" }), + onPermissionRequest: async () => ({ kind: "approve-once" }), }); ``` @@ -826,7 +831,7 @@ const session = await client.createSession({ return null; }, }, - onPermissionRequest: async () => ({ kind: "approved" }), + onPermissionRequest: async () => ({ kind: "approve-once" }), }); ``` @@ -850,7 +855,7 @@ const session = await client.createSession({ return null; }, }, - onPermissionRequest: async () => ({ kind: "approved" }), + onPermissionRequest: async () => ({ kind: "approve-once" }), }); ``` @@ -871,7 +876,7 @@ const session = await client.createSession({ }; }, }, - onPermissionRequest: async () => ({ kind: "approved" }), + onPermissionRequest: async () => ({ kind: "approve-once" }), }); ``` @@ -917,7 +922,7 @@ const session = await client.createSession({ return null; }, }, - onPermissionRequest: async () => ({ kind: "approved" }), + onPermissionRequest: async () => ({ kind: "approve-once" }), }); ``` @@ -927,6 +932,8 @@ const session = await client.createSession({ Python ```python +from copilot.session import PermissionRequestResult + session_metrics = {} async def on_session_start(input_data, invocation): @@ -956,7 +963,7 @@ async def on_session_end(input_data, invocation): return None session = await client.create_session( - on_permission_request=lambda req, inv: {"kind": "approved"}, + on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), hooks={ "on_session_start": on_session_start, "on_user_prompt_submitted": on_user_prompt_submitted, @@ -999,7 +1006,7 @@ const session = await client.createSession({ return null; }, }, - onPermissionRequest: async () => ({ kind: "approved" }), + onPermissionRequest: async () => ({ kind: "approve-once" }), }); ``` diff --git a/docs/features/image-input.md b/docs/features/image-input.md index 6a25b312e..286414c91 100644 --- a/docs/features/image-input.md +++ b/docs/features/image-input.md @@ -48,7 +48,7 @@ await client.start(); const session = await client.createSession({ model: "gpt-4.1", - onPermissionRequest: async () => ({ kind: "approved" }), + onPermissionRequest: async () => ({ kind: "approve-once" }), }); await session.send({ @@ -75,7 +75,7 @@ client = CopilotClient() await client.start() session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approved"), + on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), model="gpt-4.1", ) @@ -263,7 +263,7 @@ await client.start(); const session = await client.createSession({ model: "gpt-4.1", - onPermissionRequest: async () => ({ kind: "approved" }), + onPermissionRequest: async () => ({ kind: "approve-once" }), }); const base64ImageData = "..."; // your base64-encoded image @@ -293,7 +293,7 @@ client = CopilotClient() await client.start() session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approved"), + on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), model="gpt-4.1", ) diff --git a/docs/features/skills.md b/docs/features/skills.md index 2d89d62a8..6db0d60f3 100644 --- a/docs/features/skills.md +++ b/docs/features/skills.md @@ -29,7 +29,7 @@ const session = await client.createSession({ "./skills/code-review", "./skills/documentation", ], - onPermissionRequest: async () => ({ kind: "approved" }), + onPermissionRequest: async () => ({ kind: "approve-once" }), }); // Copilot now has access to skills in those directories @@ -50,7 +50,7 @@ async def main(): await client.start() session = await client.create_session( - on_permission_request=lambda req, inv: {"kind": "approved"}, + on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), model="gpt-4.1", skill_directories=[ "./skills/code-review", @@ -375,7 +375,7 @@ const session = await client.createSession({ prompt: "Focus on OWASP Top 10 vulnerabilities", skills: ["security-scan", "dependency-check"], }], - onPermissionRequest: async () => ({ kind: "approved" }), + onPermissionRequest: async () => ({ kind: "approve-once" }), }); ``` > [!NOTE] @@ -396,7 +396,7 @@ const session = await client.createSession({ tools: ["*"], }, }, - onPermissionRequest: async () => ({ kind: "approved" }), + onPermissionRequest: async () => ({ kind: "approve-once" }), }); ``` diff --git a/docs/features/steering-and-queueing.md b/docs/features/steering-and-queueing.md index e7f67457c..7858a7d3f 100644 --- a/docs/features/steering-and-queueing.md +++ b/docs/features/steering-and-queueing.md @@ -48,7 +48,7 @@ await client.start(); const session = await client.createSession({ model: "gpt-4.1", - onPermissionRequest: async () => ({ kind: "approved" }), + onPermissionRequest: async () => ({ kind: "approve-once" }), }); // Start a long-running task @@ -77,7 +77,7 @@ async def main(): await client.start() session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approved"), + on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), model="gpt-4.1", ) @@ -235,7 +235,7 @@ await client.start(); const session = await client.createSession({ model: "gpt-4.1", - onPermissionRequest: async () => ({ kind: "approved" }), + onPermissionRequest: async () => ({ kind: "approve-once" }), }); // Send an initial task @@ -269,7 +269,7 @@ async def main(): await client.start() session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approved"), + on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), model="gpt-4.1", ) @@ -476,7 +476,7 @@ You can use both patterns together in a single session. Steering affects the cur ```typescript const session = await client.createSession({ model: "gpt-4.1", - onPermissionRequest: async () => ({ kind: "approved" }), + onPermissionRequest: async () => ({ kind: "approve-once" }), }); // Start a task @@ -502,7 +502,7 @@ await session.send({ ```python session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approved"), + on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), model="gpt-4.1", ) diff --git a/docs/getting-started.md b/docs/getting-started.md index 4ee6bd298..182bd7a52 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -671,7 +671,7 @@ from copilot.session import PermissionRequestResult client = CopilotClient() -session = await client.create_session(on_permission_request=lambda req, inv: PermissionRequestResult(kind="approved")) +session = await client.create_session(on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once")) # Subscribe to all events unsubscribe = session.on(lambda event: print(f"Event: {event.type}")) diff --git a/docs/integrations/microsoft-agent-framework.md b/docs/integrations/microsoft-agent-framework.md index 2f9f1966a..8fea9aa07 100644 --- a/docs/integrations/microsoft-agent-framework.md +++ b/docs/integrations/microsoft-agent-framework.md @@ -217,7 +217,7 @@ const client = new CopilotClient(); const session = await client.createSession({ model: "gpt-4.1", tools: [getWeather], - onPermissionRequest: async () => ({ kind: "approved" }), + onPermissionRequest: async () => ({ kind: "approve-once" }), }); await session.sendAndWait({ prompt: "What's the weather like in Seattle?" }); @@ -521,7 +521,7 @@ const client = new CopilotClient(); const session = await client.createSession({ model: "gpt-4.1", streaming: true, - onPermissionRequest: async () => ({ kind: "approved" }), + onPermissionRequest: async () => ({ kind: "approve-once" }), }); session.on("assistant.message_delta", (event) => { @@ -600,7 +600,7 @@ import { CopilotClient } from "@github/copilot-sdk"; const client = new CopilotClient(); const session = await client.createSession({ model: "gpt-4.1", - onPermissionRequest: async () => ({ kind: "approved" }), + onPermissionRequest: async () => ({ kind: "approve-once" }), }); const response = await session.sendAndWait({ prompt: "Explain this code" }); ``` diff --git a/dotnet/README.md b/dotnet/README.md index 6b76f3913..d5be3ab80 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -758,7 +758,7 @@ var session = await client.CreateSessionAsync(new SessionConfig if (request.Kind == "shell") { // Deny shell commands - return new PermissionRequestResult { Kind = PermissionRequestResultKind.DeniedInteractivelyByUser }; + return new PermissionRequestResult { Kind = PermissionRequestResultKind.Rejected }; } return new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }; @@ -768,13 +768,16 @@ var session = await client.CreateSessionAsync(new SessionConfig ### Permission Result Kinds -| Value | Meaning | -| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `PermissionRequestResultKind.Approved` | Allow the tool to run | -| `PermissionRequestResultKind.DeniedInteractivelyByUser` | User explicitly denied the request | -| `PermissionRequestResultKind.DeniedCouldNotRequestFromUser` | No approval rule matched and user could not be asked | -| `PermissionRequestResultKind.DeniedByRules` | Denied by a policy rule | -| `PermissionRequestResultKind.NoResult` | Leave the permission request unanswered (the SDK returns without calling the RPC). Not allowed for protocol v2 permission requests (will be rejected). | +The `Kind` property must be one of the canonical `PermissionRequestResultKind` values. Approval decisions are present-tense — they describe the decision to apply, not the past-tense outcome reported back on `permission.completed` session events. + +| Value | Wire value | Meaning | +| ------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `PermissionRequestResultKind.Approved` | `"approve-once"` | Allow this single request | +| `PermissionRequestResultKind.Rejected` | `"reject"` | Deny the request | +| `PermissionRequestResultKind.UserNotAvailable` | `"user-not-available"` | Deny the request because no user is available to confirm it | +| `PermissionRequestResultKind.NoResult` | `"no-result"` | Leave the permission request unanswered (the SDK returns without calling the RPC). Not allowed for protocol v2 permission requests (will be rejected). | + +> The past-tense names `PermissionRequestResultKind.DeniedInteractivelyByUser`, `PermissionRequestResultKind.DeniedCouldNotRequestFromUser`, and `PermissionRequestResultKind.DeniedByRules` remain as `[Obsolete]` aliases for backward compatibility — prefer the canonical members above in new code. ### Resuming Sessions diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 0775280e8..a4651bb4a 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -613,13 +613,13 @@ public override void Write(Utf8JsonWriter writer, PermissionRequestResultKind va public class PermissionRequestResult { /// - /// Permission decision kind. + /// Permission decision kind. Use the static members of + /// to construct values. Valid kinds are: /// - /// "approved" — the operation is allowed. - /// "denied-by-rules" — denied by configured permission rules. - /// "denied-interactively-by-user" — the user explicitly denied the request. - /// "denied-no-approval-rule-and-could-not-request-from-user" — no rule matched and user approval was unavailable. - /// "no-result" — leave the pending permission request unanswered. + /// "approve-once" () — allow this single request. + /// "reject" () — deny the request. + /// "user-not-available" () — deny because no user is available to confirm. + /// "no-result" () — leave the pending request unanswered (protocol v1 only; rejected by protocol v2 servers). /// /// [JsonPropertyName("kind")] diff --git a/go/README.md b/go/README.md index 29760064c..be54e2152 100644 --- a/go/README.md +++ b/go/README.md @@ -606,7 +606,7 @@ session, err := client.CreateSession(context.Background(), &copilot.SessionConfi if request.Kind == copilot.KindShell { // Deny shell commands - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindDeniedInteractivelyByUser}, nil + return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindRejected}, nil } return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil @@ -616,13 +616,16 @@ session, err := client.CreateSession(context.Background(), &copilot.SessionConfi ### Permission Result Kinds -| Constant | Meaning | -| ---------------------------------------------------------- | --------------------------------------------------------------------------------------- | -| `PermissionRequestResultKindApproved` | Allow the tool to run | -| `PermissionRequestResultKindDeniedInteractivelyByUser` | User explicitly denied the request | -| `PermissionRequestResultKindDeniedCouldNotRequestFromUser` | No approval rule matched and user could not be asked | -| `PermissionRequestResultKindDeniedByRules` | Denied by a policy rule | -| `PermissionRequestResultKindNoResult` | Leave the permission request unanswered (protocol v1 only; not allowed for protocol v2) | +The `Kind` field must be one of the canonical `PermissionRequestResultKind` constants. Approval decisions are present-tense — they describe the decision to apply, not the past-tense outcome reported back on `permission.completed` session events. + +| Constant | Wire value | Meaning | +| --------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------- | +| `PermissionRequestResultKindApproved` | `"approve-once"` | Allow this single request | +| `PermissionRequestResultKindRejected` | `"reject"` | Deny the request | +| `PermissionRequestResultKindUserNotAvailable` | `"user-not-available"` | Deny the request because no user is available to confirm it | +| `PermissionRequestResultKindNoResult` | `"no-result"` | Leave the permission request unanswered (protocol v1 only; rejected by protocol v2 servers) | + +> The past-tense names `PermissionRequestResultKindDeniedInteractivelyByUser`, `PermissionRequestResultKindDeniedCouldNotRequestFromUser`, and `PermissionRequestResultKindDeniedByRules` remain as deprecated aliases for backward compatibility — prefer the canonical constants above in new code. ### Resuming Sessions diff --git a/nodejs/README.md b/nodejs/README.md index a8ada97ed..ce75b58f2 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -843,25 +843,28 @@ const session = await client.createSession({ // request.fullCommandText — full shell command (for shell) if (request.kind === "shell") { - // Deny shell commands - return { kind: "denied-interactively-by-user" }; + // Deny shell commands, optionally telling the model why + return { kind: "reject", feedback: "Shell commands are not allowed." }; } - return { kind: "approved" }; + return { kind: "approve-once" }; }, }); ``` ### Permission Result Kinds -| Kind | Meaning | -| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------- | -| `"approved"` | Allow the tool to run | -| `"denied-interactively-by-user"` | User explicitly denied the request | -| `"denied-no-approval-rule-and-could-not-request-from-user"` | No approval rule matched and user could not be asked | -| `"denied-by-rules"` | Denied by a policy rule | -| `"denied-by-content-exclusion-policy"` | Denied due to a content exclusion policy | -| `"no-result"` | Leave the request unanswered (only valid with protocol v1; rejected by protocol v2 servers) | +The handler must return one of the `PermissionDecision` shapes (or `{ kind: "no-result" }`). Approval scopes are present-tense — they describe the decision to apply, not the outcome reported back on session events: + +| Kind | Meaning | Extra fields | +| ------------------------ | --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | +| `"approve-once"` | Allow this single request | — | +| `"approve-for-session"` | Allow this request and remember the approval for the rest of the session | `approval?` (rule to remember), `domain?` (for URL approvals) | +| `"approve-for-location"` | Allow this request and persist the approval for this project location (git root or cwd) | `approval` (rule to persist), `locationKey` (location to persist under) | +| `"approve-permanently"` | Allow this request and persist the approval across sessions (currently used for URL domains) | `domain` (URL domain to approve) | +| `"reject"` | Deny the request | `feedback?` (optional string surfaced to the agent) | +| `"user-not-available"` | Deny the request because no user is available to confirm it | — | +| `"no-result"` | Leave the request unanswered (only valid with protocol v1; rejected by protocol v2 servers) | — | ### Resuming Sessions diff --git a/nodejs/docs/examples.md b/nodejs/docs/examples.md index a3483d8d4..c4b8acb1c 100644 --- a/nodejs/docs/examples.md +++ b/nodejs/docs/examples.md @@ -561,12 +561,12 @@ const session = await joinSession({ onPermissionRequest: async (request) => { if (request.kind === "shell") { // request.fullCommandText has the shell command - return { kind: "approved" }; + return { kind: "approve-once" }; } if (request.kind === "write") { - return { kind: "approved" }; + return { kind: "approve-once" }; } - return { kind: "denied-by-rules" }; + return { kind: "reject" }; }, }); ``` diff --git a/python/README.md b/python/README.md index 79f97cf04..ddf7b632f 100644 --- a/python/README.md +++ b/python/README.md @@ -587,9 +587,9 @@ def on_permission_request( if request.kind.value == "shell": # Deny shell commands - return PermissionRequestResult(kind="denied-interactively-by-user") + return PermissionRequestResult(kind="reject") - return PermissionRequestResult(kind="approved") + return PermissionRequestResult(kind="approve-once") session = await client.create_session( on_permission_request=on_permission_request, @@ -605,19 +605,19 @@ async def on_permission_request( ) -> PermissionRequestResult: # Simulate an async approval check (e.g., prompting a user over a network) await asyncio.sleep(0) - return PermissionRequestResult(kind="approved") + return PermissionRequestResult(kind="approve-once") ``` ### Permission Result Kinds -| `kind` value | Meaning | -| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------- | -| `"approved"` | Allow the tool to run | -| `"denied-interactively-by-user"` | User explicitly denied the request | -| `"denied-no-approval-rule-and-could-not-request-from-user"` | No approval rule matched and user could not be asked (default when no kind is specified) | -| `"denied-by-rules"` | Denied by a policy rule | -| `"denied-by-content-exclusion-policy"` | Denied due to a content exclusion policy | -| `"no-result"` | Leave the request unanswered (not allowed for protocol v2 permission requests) | +The handler must return a `PermissionRequestResult` with one of the kinds declared by the `PermissionRequestResultKind` type. Approval decisions are present-tense — they describe the decision to apply, not the past-tense outcome reported back on `permission.completed` session events. + +| `kind` value | Meaning | +| ---------------------- | ------------------------------------------------------------------------------------------- | +| `"approve-once"` | Allow this single request | +| `"reject"` | Deny the request | +| `"user-not-available"` | Deny the request because no user is available to confirm it (the default) | +| `"no-result"` | Leave the request unanswered (only valid with protocol v1; rejected by protocol v2 servers) | ### Resuming Sessions diff --git a/test/scenarios/callbacks/hooks/python/main.py b/test/scenarios/callbacks/hooks/python/main.py index dbfceb22a..ba224ef24 100644 --- a/test/scenarios/callbacks/hooks/python/main.py +++ b/test/scenarios/callbacks/hooks/python/main.py @@ -2,13 +2,14 @@ import os from copilot import CopilotClient from copilot.client import SubprocessConfig +from copilot.session import PermissionRequestResult hook_log: list[str] = [] async def auto_approve_permission(request, invocation): - return {"kind": "approved"} + return PermissionRequestResult(kind="approve-once") async def on_session_start(input_data, invocation): diff --git a/test/scenarios/callbacks/hooks/typescript/src/index.ts b/test/scenarios/callbacks/hooks/typescript/src/index.ts index 2a5cde585..4ecd7ec33 100644 --- a/test/scenarios/callbacks/hooks/typescript/src/index.ts +++ b/test/scenarios/callbacks/hooks/typescript/src/index.ts @@ -11,7 +11,7 @@ async function main() { try { const session = await client.createSession({ model: "claude-haiku-4.5", - onPermissionRequest: async () => ({ kind: "approved" as const }), + onPermissionRequest: async () => ({ kind: "approve-once" as const }), hooks: { onSessionStart: async () => { hookLog.push("onSessionStart"); diff --git a/test/scenarios/callbacks/permissions/README.md b/test/scenarios/callbacks/permissions/README.md index 19945235f..2fcb7a67c 100644 --- a/test/scenarios/callbacks/permissions/README.md +++ b/test/scenarios/callbacks/permissions/README.md @@ -12,7 +12,7 @@ This pattern is the foundation for: 1. **Enable `onPermissionRequest` handler** on the session config 2. **Track which tools requested permission** in a log array -3. **Approve all permission requests** (return `kind: "approved"`) +3. **Approve all permission requests** (return `kind: "approve-once"`) 4. **Send a prompt that triggers tool use** (e.g., listing files via glob) 5. **Print the permission log** showing which tools were approved @@ -29,12 +29,12 @@ This pattern is the foundation for: | Option | Value | Effect | |--------|-------|--------| -| `onPermissionRequest` | Log + approve | Records tool name, returns `approved` | +| `onPermissionRequest` | Log + approve | Records tool name, returns `approve-once` | | `hooks.onPreToolUse` | Auto-allow | No tool confirmation prompts | ## Key Insight -The `onPermissionRequest` handler gives the integrator full control over which tools the agent can execute. By inspecting the request (tool name, arguments), you can implement allow/deny lists, require human approval for dangerous operations, or log every action for compliance. Returning `{ kind: "denied" }` blocks the tool from running. +The `onPermissionRequest` handler gives the integrator full control over which tools the agent can execute. By inspecting the request (tool name, arguments), you can implement allow/deny lists, require human approval for dangerous operations, or log every action for compliance. Returning `{ kind: "reject" }` blocks the tool from running. ## Run diff --git a/test/scenarios/callbacks/permissions/python/main.py b/test/scenarios/callbacks/permissions/python/main.py index de788e5fb..677ca58d0 100644 --- a/test/scenarios/callbacks/permissions/python/main.py +++ b/test/scenarios/callbacks/permissions/python/main.py @@ -2,6 +2,7 @@ import os from copilot import CopilotClient from copilot.client import SubprocessConfig +from copilot.session import PermissionRequestResult # Track which tools requested permission permission_log: list[str] = [] @@ -9,7 +10,7 @@ async def log_permission(request, invocation): permission_log.append(f"approved:{request.tool_name}") - return {"kind": "approved"} + return PermissionRequestResult(kind="approve-once") async def auto_approve_tool(input_data, invocation): diff --git a/test/scenarios/callbacks/permissions/typescript/src/index.ts b/test/scenarios/callbacks/permissions/typescript/src/index.ts index 6a163bc27..8e72fc08b 100644 --- a/test/scenarios/callbacks/permissions/typescript/src/index.ts +++ b/test/scenarios/callbacks/permissions/typescript/src/index.ts @@ -15,7 +15,7 @@ async function main() { model: "claude-haiku-4.5", onPermissionRequest: async (request) => { permissionLog.push(`approved:${request.toolName}`); - return { kind: "approved" as const }; + return { kind: "approve-once" as const }; }, hooks: { onPreToolUse: async () => ({ permissionDecision: "allow" as const }), diff --git a/test/scenarios/callbacks/user-input/python/main.py b/test/scenarios/callbacks/user-input/python/main.py index 0c23e6b15..07a7eb40e 100644 --- a/test/scenarios/callbacks/user-input/python/main.py +++ b/test/scenarios/callbacks/user-input/python/main.py @@ -2,13 +2,14 @@ import os from copilot import CopilotClient from copilot.client import SubprocessConfig +from copilot.session import PermissionRequestResult input_log: list[str] = [] async def auto_approve_permission(request, invocation): - return {"kind": "approved"} + return PermissionRequestResult(kind="approve-once") async def auto_approve_tool(input_data, invocation): diff --git a/test/scenarios/callbacks/user-input/typescript/src/index.ts b/test/scenarios/callbacks/user-input/typescript/src/index.ts index 5964ce6c1..915008b68 100644 --- a/test/scenarios/callbacks/user-input/typescript/src/index.ts +++ b/test/scenarios/callbacks/user-input/typescript/src/index.ts @@ -11,7 +11,7 @@ async function main() { try { const session = await client.createSession({ model: "claude-haiku-4.5", - onPermissionRequest: async () => ({ kind: "approved" as const }), + onPermissionRequest: async () => ({ kind: "approve-once" as const }), onUserInputRequest: async (request) => { inputLog.push(`question: ${request.question}`); return { answer: "Paris", wasFreeform: true }; diff --git a/test/scenarios/tools/skills/python/main.py b/test/scenarios/tools/skills/python/main.py index 3ec9fb2ee..a6d6bf2c0 100644 --- a/test/scenarios/tools/skills/python/main.py +++ b/test/scenarios/tools/skills/python/main.py @@ -4,6 +4,7 @@ from copilot import CopilotClient from copilot.client import SubprocessConfig +from copilot.session import PermissionRequestResult async def main(): @@ -16,7 +17,7 @@ async def main(): skills_dir = str(Path(__file__).resolve().parent.parent / "sample-skills") session = await client.create_session( - on_permission_request=lambda _, __: {"kind": "approved"}, + on_permission_request=lambda _, __: PermissionRequestResult(kind="approve-once"), model="claude-haiku-4.5", skill_directories=[skills_dir], hooks={ diff --git a/test/scenarios/tools/skills/typescript/src/index.ts b/test/scenarios/tools/skills/typescript/src/index.ts index de7f13568..36447d975 100644 --- a/test/scenarios/tools/skills/typescript/src/index.ts +++ b/test/scenarios/tools/skills/typescript/src/index.ts @@ -16,7 +16,7 @@ async function main() { const session = await client.createSession({ model: "claude-haiku-4.5", skillDirectories: [skillsDir], - onPermissionRequest: async () => ({ kind: "approved" as const }), + onPermissionRequest: async () => ({ kind: "approve-once" as const }), hooks: { onPreToolUse: async () => ({ permissionDecision: "allow" as const }), }, diff --git a/test/scenarios/tools/virtual-filesystem/python/main.py b/test/scenarios/tools/virtual-filesystem/python/main.py index f7635c6c6..57b197509 100644 --- a/test/scenarios/tools/virtual-filesystem/python/main.py +++ b/test/scenarios/tools/virtual-filesystem/python/main.py @@ -2,6 +2,7 @@ import os from copilot import CopilotClient, define_tool from copilot.client import SubprocessConfig +from copilot.session import PermissionRequestResult from pydantic import BaseModel, Field # In-memory virtual filesystem @@ -39,7 +40,7 @@ def list_files() -> str: async def auto_approve_permission(request, invocation): - return {"kind": "approved"} + return PermissionRequestResult(kind="approve-once") async def auto_approve_tool(input_data, invocation): diff --git a/test/scenarios/tools/virtual-filesystem/typescript/src/index.ts b/test/scenarios/tools/virtual-filesystem/typescript/src/index.ts index 4f7dadfd6..fa146da83 100644 --- a/test/scenarios/tools/virtual-filesystem/typescript/src/index.ts +++ b/test/scenarios/tools/virtual-filesystem/typescript/src/index.ts @@ -51,7 +51,7 @@ async function main() { // Remove all built-in tools — only our custom virtual FS tools are available availableTools: [], tools: [createFile, readFile, listFiles], - onPermissionRequest: async () => ({ kind: "approved" as const }), + onPermissionRequest: async () => ({ kind: "approve-once" as const }), hooks: { onPreToolUse: async () => ({ permissionDecision: "allow" as const }), },