From e91f3caa787702ae255da66cd1a59bf8d932f7f3 Mon Sep 17 00:00:00 2001 From: Karl Krukow Date: Thu, 19 Feb 2026 13:40:26 +0100 Subject: [PATCH 1/2] fix: use keywords for permission denial :kind values Default permission-denial responses in session.clj and client.clj returned :kind as a string ("denied-no-approval-rule-...") while the spec (::permission-result-kind) and approve-all both use keywords. This inconsistency meant users inspecting responses in Clojure saw mixed types depending on whether a handler was configured. Changes: - session.clj: 3 denial paths now return keyword :kind - client.clj: unknown-session denial now returns keyword :kind - integration_test.clj: updated assertion to expect keyword Also includes upstream PR #509 sync (deny-by-default permissions): - requestPermission always true on wire - approve-all convenience handler - Integration tests for permission model - Documentation and changelog updates Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 13 +++ README.md | 2 +- doc/reference/API.md | 37 ++++++- examples/README.md | 15 ++- examples/mcp_local_server.clj | 2 + src/github/copilot_sdk.clj | 5 + src/github/copilot_sdk/client.clj | 26 ++++- src/github/copilot_sdk/instrument.clj | 7 ++ src/github/copilot_sdk/session.clj | 20 ++-- test/github/copilot_sdk/integration_test.clj | 100 +++++++++++++++++++ test/github/copilot_sdk/mock_server.clj | 18 ++++ 11 files changed, 221 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3baf630..b89e585b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,19 @@ All notable changes to this project will be documented in this file. This change ## [Unreleased] +### Changed (upstream PR #509 sync) +- **BREAKING**: Deny all permissions by default — `requestPermission` is now always `true` on the wire, and permission requests are denied when no `:on-permission-request` handler is configured. Previously, omitting the handler meant the CLI never asked for permission. To restore the old behavior, pass `:on-permission-request copilot/approve-all` in your session config. + +### Added (upstream PR #509 sync) +- `approve-all` — convenience permission handler that approves all requests (`copilot/approve-all`). Equivalent to the upstream Node.js SDK `approveAll` export. Use as `:on-permission-request copilot/approve-all` in session config. +- Integration tests for deny-by-default permission model: wire format assertions, `approve-all` behavior, handler dispatch with/without handler, custom selective handler + +### Changed +- MCP local server example now passes `:on-permission-request copilot/approve-all` (required for MCP tool execution under deny-by-default) + +### Fixed +- Permission denial result `:kind` now consistently uses keywords (not strings) in default handler responses, matching specs and `approve-all` behavior + ## [0.1.25.1] - 2026-02-18 ### Fixed - Release pipeline: GPG signing now fails fast with a clear error when no key is available, instead of silently producing unsigned artifacts that Maven Central rejects diff --git a/README.md b/README.md index 20e50a32..5d2a41dc 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ See [doc/reference/API.md](./doc/reference/API.md) for the complete API referenc - **CopilotSession** - Session methods (`send!`, `send-and-wait!`, `}`: +The SDK uses a **deny-by-default** permission model. All permission requests +(file writes, shell commands, URL fetches, etc.) are denied unless your +session config provides an `:on-permission-request` handler. + +Use `approve-all` to opt into approving everything: + +```clojure +(def session (copilot/create-session client + {:on-permission-request copilot/approve-all})) +``` + +For fine-grained control, provide your own handler. When the CLI needs +approval, it sends a JSON-RPC `permission.request` to the SDK. Your +`:on-permission-request` callback must return a map compatible with the +permission result payload; the SDK wraps this into the JSON-RPC response +as `{:result }`: The `permission_bash.clj` example demonstrates both an allowed and a denied shell command and prints the full permission request payload so you can inspect @@ -1164,6 +1176,21 @@ fields like `:full-command-text`, `:commands`, and `:possible-paths`. {:kind :denied-interactively-by-user :feedback "Not allowed"} ``` +#### `approve-all` + +```clojure +(copilot/approve-all request ctx) +``` + +A convenience permission handler that approves all permission requests. +Equivalent to the upstream Node.js SDK `approveAll` export. + +Pass as the `:on-permission-request` value in session config: + +```clojure +(copilot/create-session client {:on-permission-request copilot/approve-all}) +``` + ### User Input Handling When the agent needs input from the user (via `ask_user` tool), the `:on-user-input-request` diff --git a/examples/README.md b/examples/README.md index 0c3fac0f..d51984da 100644 --- a/examples/README.md +++ b/examples/README.md @@ -166,7 +166,7 @@ clojure -A:examples -X helpers-query/run-multi :questions '["What is Rust?" "Wha (require '[github.copilot-sdk.helpers :as h]) ;; Simplest possible query - just get the answer -(h/query "What is 2+2?") +(h/query "What is 2+2?" :session {:model "gpt-5.2"}) ;; => "4" ;; With options @@ -180,7 +180,7 @@ clojure -A:examples -X helpers-query/run-multi :questions '["What is Rust?" "Wha (flush)) (defmethod handle-event :copilot/assistant.message [_] (println)) -(run! handle-event (h/query-seq! "Tell me a joke" :session {:streaming? true})) +(run! handle-event (h/query-seq! "Tell me a joke" :session {:model "gpt-5.2" :streaming? true})) ``` --- @@ -367,7 +367,11 @@ clojure -A:examples -X metadata-api/run ## Example 7: Permission Handling (`permission_bash.clj`) **Difficulty:** Intermediate -**Concepts:** permission requests, bash tool, approval callback +**Concepts:** permission requests, bash tool, approval callback, deny-by-default + +The SDK uses a **deny-by-default** permission model — all permission requests are +denied unless an `:on-permission-request` handler is provided. Use `copilot/approve-all` +for blanket approval, or provide a custom handler for fine-grained control. Shows how to: - handle `permission.request` via `:on-permission-request` @@ -550,6 +554,7 @@ Shows how to integrate MCP (Model Context Protocol) servers to extend the assist - Configuring `:mcp-servers` with a local stdio server - Using the `@modelcontextprotocol/server-filesystem` MCP server - Combining MCP server tools with custom tools +- Using `copilot/approve-all` to permit MCP tool execution (deny-by-default) ### Prerequisites @@ -603,7 +608,7 @@ await client.start(); **Clojure:** ```clojure (require '[github.copilot-sdk.helpers :as h]) -(h/query "What is 2+2?") +(h/query "What is 2+2?" :session {:model "gpt-5.2"}) ;; => "4" ``` @@ -625,7 +630,7 @@ session.on((event) => { (defmethod handle-event :copilot/assistant.message [{{:keys [content]} :data}] (println content)) -(run! handle-event (h/query-seq! "Hello" :session {:streaming? true})) +(run! handle-event (h/query-seq! "Hello" :session {:model "gpt-5.2" :streaming? true})) ``` ### Tool Definition diff --git a/examples/mcp_local_server.clj b/examples/mcp_local_server.clj index d72a567a..88a0c09e 100644 --- a/examples/mcp_local_server.clj +++ b/examples/mcp_local_server.clj @@ -24,6 +24,7 @@ (println (str "MCP Filesystem Server — allowed directory: " allowed-dir)) (println) (let [session-config {:model "gpt-5.2" + :on-permission-request copilot/approve-all :mcp-servers {"filesystem" {:mcp-command "npx" @@ -55,6 +56,7 @@ (str "Summary: " (subs text 0 (min 100 (count text))) "...")))}) session-config {:model "gpt-5.2" :tools [summary-tool] + :on-permission-request copilot/approve-all :mcp-servers {"filesystem" {:mcp-command "npx" diff --git a/src/github/copilot_sdk.clj b/src/github/copilot_sdk.clj index 2f971169..1f081e38 100644 --- a/src/github/copilot_sdk.clj +++ b/src/github/copilot_sdk.clj @@ -821,3 +821,8 @@ (def result-failure tools/result-failure) (def result-denied tools/result-denied) (def result-rejected tools/result-rejected) + +;; Re-export permission helpers +(def approve-all + "Permission handler that approves all requests. See `github.copilot-sdk.client/approve-all`." + client/approve-all) diff --git a/src/github/copilot_sdk/client.clj b/src/github/copilot_sdk/client.clj index 985699f2..588c811c 100644 --- a/src/github/copilot_sdk/client.clj +++ b/src/github/copilot_sdk/client.clj @@ -386,7 +386,7 @@ "permission.request" (let [{:keys [session-id permission-request]} params] (if-not (get-in @(:state client) [:sessions session-id]) - {:result {:kind "denied-no-approval-rule-and-could-not-request-from-user"}} + {:result {:kind :denied-no-approval-rule-and-could-not-request-from-user}} (let [result ( Date: Thu, 19 Feb 2026 13:40:35 +0100 Subject: [PATCH 2/2] chore: pin all examples to gpt-5.2 model Examples that relied on the CLI default model or used gpt-4.1/gpt-4o now explicitly specify gpt-5.2 for consistent behavior. - helpers_query.clj: add session-config with model, pass to all - metadata_api.clj: with-session and switch-model! now use gpt-5.2 - multi_agent.clj: researcher sessions changed from gpt-4.1 to gpt-5.2 BYOK provider examples (claude-sonnet-4, llama3) are intentionally unchanged as they demonstrate provider-specific models. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- examples/helpers_query.clj | 11 +++++++---- examples/metadata_api.clj | 7 +++---- examples/multi_agent.clj | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/examples/helpers_query.clj b/examples/helpers_query.clj index 3996b98e..cdc88c0d 100644 --- a/examples/helpers_query.clj +++ b/examples/helpers_query.clj @@ -8,10 +8,13 @@ (def defaults {:prompt "What is the capital of Japan? Answer in one sentence."}) +(def session-config + {:model "gpt-5.2"}) + (defn run [{:keys [prompt] :or {prompt (:prompt defaults)}}] (println "Query:" prompt) - (println "🤖:" (h/query prompt))) + (println "🤖:" (h/query prompt :session session-config))) (defn run-multi [{:keys [questions] :or {questions ["What is 2+2? Just the number." @@ -19,7 +22,7 @@ "Who wrote Hamlet? Just the name."]}}] (doseq [q questions] (println "Q:" q) - (println "A:" (h/query q)) + (println "A:" (h/query q :session session-config)) (println))) ;; Define a multimethod for handling events by type @@ -34,13 +37,13 @@ [{:keys [prompt] :or {prompt "Explain the concept of immutability in 2-3 sentences."}}] (println "Query:" prompt) (println) - (run! handle-event (h/query-seq! prompt :session {:streaming? true}))) + (run! handle-event (h/query-seq! prompt :session {:model "gpt-5.2" :streaming? true}))) (defn run-async [{:keys [prompt] :or {prompt "Tell me a short joke."}}] (println "Query:" prompt) (println) - (let [ch (h/query-chan prompt :session {:streaming? true})] + (let [ch (h/query-chan prompt :session {:model "gpt-5.2" :streaming? true})] (