From 6cbd2b155d0b0ae21a67f903bc3c87b0d6ea9174 Mon Sep 17 00:00:00 2001 From: Karl Krukow Date: Fri, 13 Mar 2026 11:50:55 +0100 Subject: [PATCH 1/2] feat: add no-result permission outcome for extensions (upstream PR #802) Extensions can attach to sessions without actively answering permission requests by returning {:kind :no-result} from their :on-permission-request handler. - v3 protocol: skip handlePendingPermissionRequest RPC call - v2 protocol: propagate as JSON-RPC internal error (-32603) - Handle both direct {:kind :no-result} and wrapped {:result {:kind :no-result}} - Add :no-result to ::permission-result-kind spec - Add tests for both direct and wrapped no-result on v2 path - Update API.md and CHANGELOG.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 2 ++ doc/reference/API.md | 3 ++ src/github/copilot_sdk/client.clj | 33 +++++++++++++------- src/github/copilot_sdk/session.clj | 14 ++++++++- src/github/copilot_sdk/specs.clj | 3 +- test/github/copilot_sdk/integration_test.clj | 30 ++++++++++++++++++ 6 files changed, 72 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76acaca..05a7a37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/). ## [Unreleased] +### Added +- `:no-result` permission outcome — extensions can attach to sessions without actively answering permission requests by returning `{:kind :no-result}` from their `:on-permission-request` handler. On v3 protocol, the `handlePendingPermissionRequest` RPC is skipped; on v2, an error is propagated to the CLI (upstream PR #802). ## [0.1.32.0] - 2026-03-12 ### Added (upstream sync) diff --git a/doc/reference/API.md b/doc/reference/API.md index 246382a..673683c 100644 --- a/doc/reference/API.md +++ b/doc/reference/API.md @@ -1318,6 +1318,9 @@ fields like `:full-command-text`, `:commands`, and `:possible-paths`. ;; Deny after user interaction (optional feedback) {:kind :denied-interactively-by-user :feedback "Not allowed"} + +;; Extension declines to answer (another handler may respond) +{:kind :no-result} ``` #### `approve-all` diff --git a/src/github/copilot_sdk/client.clj b/src/github/copilot_sdk/client.clj index 633e217..2bf0aec 100644 --- a/src/github/copilot_sdk/client.clj +++ b/src/github/copilot_sdk/client.clj @@ -247,7 +247,9 @@ (defn- handle-v3-permission-requested! "Handle v3 permission.requested broadcast event. Calls the session's permission handler and responds via the - session.permissions.handlePendingPermissionRequest RPC method." + session.permissions.handlePendingPermissionRequest RPC method. + When the handler returns :no-result, the RPC call is skipped + so the extension does not answer this permission request." [client session-id event] (let [data (:data event) request-id (:request-id data) @@ -257,13 +259,15 @@ (try (let [perm-response ( Date: Fri, 13 Mar 2026 12:20:07 +0100 Subject: [PATCH 2/2] fix: update docstring and add v3 no-result test (PR review feedback) - Clarify handle-permission-request! docstring for v2 vs v3 behavior - Add integration test for v3 permission.requested no-result path - Add integration test for v3 permission.requested approved path - Add handlePendingPermissionRequest handler to mock server Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/github/copilot_sdk/session.clj | 7 ++- test/github/copilot_sdk/integration_test.clj | 63 ++++++++++++++++++++ test/github/copilot_sdk/mock_server.clj | 1 + 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/github/copilot_sdk/session.clj b/src/github/copilot_sdk/session.clj index f4ce5e3..92c1e62 100644 --- a/src/github/copilot_sdk/session.clj +++ b/src/github/copilot_sdk/session.clj @@ -198,8 +198,11 @@ (defn handle-permission-request! "Handle an incoming permission request. Returns a channel with the result. When the handler returns `{:kind :no-result}`, the result is - `{:result :no-result}` — callers must check for this sentinel and - skip responding to the CLI (extensions that don't answer permissions)." + `{:result :no-result}` — callers must check for this sentinel: + - **v3 (broadcast path):** skip the `handlePendingPermissionRequest` RPC + entirely so the extension does not answer this permission request. + - **v2 (request-handler path):** propagate as a JSON-RPC internal error + (code -32603) so the CLI knows the request was not handled." [client session-id request] (async/thread-call (fn [] diff --git a/test/github/copilot_sdk/integration_test.clj b/test/github/copilot_sdk/integration_test.clj index 39501e0..7500b4a 100644 --- a/test/github/copilot_sdk/integration_test.clj +++ b/test/github/copilot_sdk/integration_test.clj @@ -938,6 +938,69 @@ (is (= -32603 (get-in response [:error :code])) "wrapped no-result on v2 should also produce a -32603 error")))) +(deftest test-permission-no-result-v3 + (testing "v3 no-result skips handlePendingPermissionRequest RPC" + (let [requests (atom []) + _ (mock/set-request-hook! *mock-server* + (fn [method params] + (swap! requests conj {:method method :params params}))) + session (sdk/create-session *test-client* + {:on-permission-request + (fn [_request _ctx] + {:kind :no-result})}) + session-id (sdk/session-id session)] + ;; Force protocol v3 so the broadcast path is active + (swap! (:state *test-client*) assoc :negotiated-protocol-version 3) + ;; Reset captured requests after session creation + (reset! requests []) + ;; Inject a v3 permission.requested broadcast event + (mock/send-session-event! *mock-server* session-id + "permission.requested" + {:requestId "perm-req-1" + :permissionRequest {:permissionKind "shell" + :fullCommandText "echo test"}}) + ;; Allow async handler to process + (Thread/sleep 500) + ;; The handler returned no-result — no handlePendingPermissionRequest RPC + (is (empty? (filter #(= "session.permissions.handlePendingPermissionRequest" + (:method %)) + @requests)) + "no-result should skip the handlePendingPermissionRequest RPC")))) + +(deftest test-permission-approved-v3 + (testing "v3 approved handler sends handlePendingPermissionRequest RPC" + (let [requests (atom []) + rpc-latch (java.util.concurrent.CountDownLatch. 1) + _ (mock/set-request-hook! *mock-server* + (fn [method params] + (swap! requests conj {:method method :params params}) + (when (= "session.permissions.handlePendingPermissionRequest" method) + (.countDown rpc-latch)))) + session (sdk/create-session *test-client* + {:on-permission-request sdk/approve-all}) + session-id (sdk/session-id session)] + ;; Force protocol v3 + (swap! (:state *test-client*) assoc :negotiated-protocol-version 3) + ;; Reset captured requests after session creation + (reset! requests []) + ;; Inject a v3 permission.requested broadcast event + (mock/send-session-event! *mock-server* session-id + "permission.requested" + {:requestId "perm-req-2" + :permissionRequest {:permissionKind "shell" + :fullCommandText "echo test"}}) + ;; Wait for the RPC to arrive (up to 5 seconds) + (.await rpc-latch 5 java.util.concurrent.TimeUnit/SECONDS) + ;; The handler approved — should send handlePendingPermissionRequest RPC + (let [perm-rpcs (filter #(= "session.permissions.handlePendingPermissionRequest" + (:method %)) + @requests)] + (is (= 1 (count perm-rpcs)) + "approved result should send handlePendingPermissionRequest RPC") + (when (seq perm-rpcs) + (is (= "perm-req-2" (:requestId (:params (first perm-rpcs)))) + "RPC should include the correct request-id")))))) + ;; ----------------------------------------------------------------------------- ;; Last Session ID Tests ;; ----------------------------------------------------------------------------- diff --git a/test/github/copilot_sdk/mock_server.clj b/test/github/copilot_sdk/mock_server.clj index f6386b2..592d941 100644 --- a/test/github/copilot_sdk/mock_server.clj +++ b/test/github/copilot_sdk/mock_server.clj @@ -300,6 +300,7 @@ "session.model.getCurrent" (handle-session-model-get-current server params) "session.model.switchTo" (handle-session-model-switch-to server params) "session.log" (handle-session-log server params) + "session.permissions.handlePendingPermissionRequest" {:ok true} (throw (ex-info "Method not found" {:code -32601 :method method})))] {:jsonrpc "2.0" :id (:id msg)