Skip to content
Merged
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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!`, `<send!`, `events`)
- **Event Types** - All session events (`:assistant.message`, `:assistant.message_delta`, etc.)
- **Streaming** - How to handle incremental responses
- **Advanced Usage** - Tools, system messages, permissions, multiple sessions
- **Advanced Usage** - Tools, system messages, permissions (deny-by-default), multiple sessions

## Examples

Expand Down
37 changes: 32 additions & 5 deletions doc/reference/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ Create a client and session together, ensuring both are cleaned up on exit.
| `:provider` | map | Provider config for BYOK (see [BYOK docs](../auth/byok.md)). Required key: `:base-url`. Optional: `:provider-type` (`:openai`/`:azure`/`:anthropic`), `:wire-api` (`:completions`/`:responses`), `:api-key`, `:bearer-token`, `:azure-options` |
| `:mcp-servers` | map | MCP server configs keyed by server ID (see [MCP docs](../mcp/overview.md)). Local servers: `:mcp-command`, `:mcp-args`, `:mcp-tools`. Remote servers: `:mcp-server-type` (`:http`/`:sse`), `:mcp-url`, `:mcp-tools` |
| `:custom-agents` | vector | Custom agent configs |
| `:on-permission-request` | fn | Permission handler function |
| `:on-permission-request` | fn | Permission handler function. Without a handler, all permissions are denied (deny-by-default). Use `copilot/approve-all` to approve everything. |
| `:streaming?` | boolean | Enable streaming deltas |
| `:config-dir` | string | Override config directory for CLI |
| `:skill-directories` | vector | Additional skill directories to load |
Expand Down Expand Up @@ -1140,10 +1140,22 @@ Sessions emit `:session.compaction_start` and `:session.compaction_complete` eve

### Permission Handling

When the CLI needs approval (e.g., shell or file write), 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 <your-map>}`:
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 <your-map>}`:

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
Expand All @@ -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`
Expand Down
15 changes: 10 additions & 5 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}))
```

---
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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"
```

Expand All @@ -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
Expand Down
11 changes: 7 additions & 4 deletions examples/helpers_query.clj
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,21 @@
(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."
"What is the capital of France? Just the city."
"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
Expand All @@ -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})]
(<!! (go-loop []
(when-let [event (<! ch)]
(handle-event event)
Expand Down
2 changes: 2 additions & 0 deletions examples/mcp_local_server.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
7 changes: 3 additions & 4 deletions examples/metadata_api.clj
Original file line number Diff line number Diff line change
Expand Up @@ -53,21 +53,20 @@

;; 4. Model switching within a session
(println "\n4. Dynamic Model Switching:")
(copilot/with-session [session client {}]
;; Query with default model
(copilot/with-session [session client {:model "gpt-5.2"}]
;; Query with gpt-5.2
(println " Query: 'What is 2+2? Answer briefly.'")
(println (str " Response: " (h/query "What is 2+2? Answer briefly." :session session)))

;; Try model introspection (requires CLI support)
(try
(let [current (copilot/get-current-model session)]
(println (str "\n Current model: " current))
(copilot/switch-model! session "gpt-4o")
(copilot/switch-model! session "gpt-5.2")
(println (str " Switched to: " (copilot/get-current-model session)))
Comment on lines +56 to 66
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section is labeled “Dynamic Model Switching”, but the session is created with :model "gpt-5.2" and then switch-model! is called with the same model, so it doesn't actually demonstrate switching. Either start the session without an explicit model and switch to gpt-5.2, or switch to a different known-good model while keeping the initial model pinned for the earlier queries.

Copilot uses AI. Check for mistakes.
(println " Query: 'What was my previous question?'")
(println (str " Response: " (h/query "What was my previous question?" :session session))))
(catch Exception e
(println (str "\n Model switching skipped: " (.getMessage e)))))))

(println "\n=== Demo Complete ==="))

2 changes: 1 addition & 1 deletion examples/multi_agent.clj
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
(let [session (<! (copilot/<create-session
client
{:system-message {:mode :append :content researcher-prompt}
:model "gpt-4.1"}))]
:model "gpt-5.2"}))]
(if (instance? Throwable session)
{:topic topic :findings (str "Error: " (ex-message session))}
(let [answer (<! (copilot/<send! session {:prompt (str "Research: " topic)}))]
Expand Down
5 changes: 5 additions & 0 deletions src/github/copilot_sdk.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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)
26 changes: 23 additions & 3 deletions src/github/copilot_sdk/client.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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 (<! (session/handle-permission-request! client session-id permission-request))]
(log/debug "Permission response for session " session-id ": " result)
{:result result})))
Expand Down Expand Up @@ -503,6 +503,26 @@
sdk-protocol-version ", but server reports version " server-version)
{:expected sdk-protocol-version :actual server-version})))))

;; ---------------------------------------------------------------------------
;; Permission helpers
;; ---------------------------------------------------------------------------

(defn approve-all
"Permission handler that approves all permission requests.

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 (equivalent to the
upstream `approveAll` export):

(copilot/create-session client {:on-permission-request copilot/approve-all})

For fine-grained control, provide your own handler function instead."
[_request _ctx]
{:kind :approved})

(defn start!
"Start the CLI server and establish connection.
Blocks until connected or throws on error.
Expand Down Expand Up @@ -937,7 +957,7 @@
(:available-tools config) (assoc :available-tools (:available-tools config))
(:excluded-tools config) (assoc :excluded-tools (:excluded-tools config))
wire-provider (assoc :provider wire-provider)
true (assoc :request-permission (boolean (:on-permission-request config)))
true (assoc :request-permission true)
(:streaming? config) (assoc :streaming (:streaming? config))
wire-mcp-servers (assoc :mcp-servers wire-mcp-servers)
wire-custom-agents (assoc :custom-agents wire-custom-agents)
Expand Down Expand Up @@ -984,7 +1004,7 @@
(:available-tools config) (assoc :available-tools (:available-tools config))
(:excluded-tools config) (assoc :excluded-tools (:excluded-tools config))
wire-provider (assoc :provider wire-provider)
true (assoc :request-permission (boolean (:on-permission-request config)))
true (assoc :request-permission true)
(:streaming? config) (assoc :streaming (:streaming? config))
wire-mcp-servers (assoc :mcp-servers wire-mcp-servers)
wire-custom-agents (assoc :custom-agents wire-custom-agents)
Expand Down
7 changes: 7 additions & 0 deletions src/github/copilot_sdk/instrument.clj
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@
:args (s/cat :client ::specs/client)
:ret nil?)

(s/fdef github.copilot-sdk.client/approve-all
:args (s/cat :request ::specs/permission-request
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new approve-all fdef requires :request to conform to ::specs/permission-request, which currently requires keyword :permission-kind. However, wire-level permission requests may carry string kinds (and tests in this PR construct them as strings). Either relax the fdef to accept the wire shape as well, or ensure permission requests are normalized to keyword kinds before calling the handler; otherwise instrumentation can fail when users pass github.copilot-sdk.client/approve-all as :on-permission-request.

Suggested change
:args (s/cat :request ::specs/permission-request
:args (s/cat :request (s/or :normalized ::specs/permission-request
:wire map?)

Copilot uses AI. Check for mistakes.
:ctx map?)
:ret ::specs/permission-result)

(s/fdef github.copilot-sdk.client/stop!
:args (s/cat :client ::specs/client)
:ret (s/coll-of any?))
Expand Down Expand Up @@ -238,6 +243,7 @@
(stest/instrument '[github.copilot-sdk.client/client
github.copilot-sdk.client/state
github.copilot-sdk.client/start!
github.copilot-sdk.client/approve-all
github.copilot-sdk.client/stop!
github.copilot-sdk.client/force-stop!
github.copilot-sdk.client/ping
Expand Down Expand Up @@ -284,6 +290,7 @@
(stest/unstrument '[github.copilot-sdk.client/client
github.copilot-sdk.client/state
github.copilot-sdk.client/start!
github.copilot-sdk.client/approve-all
github.copilot-sdk.client/stop!
github.copilot-sdk.client/force-stop!
github.copilot-sdk.client/ping
Expand Down
20 changes: 10 additions & 10 deletions src/github/copilot_sdk/session.clj
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@
(fn []
(let [handler (:permission-handler (session-state client session-id))]
(if-not handler
{:result {:kind "denied-no-approval-rule-and-could-not-request-from-user"}}
{:result {:kind :denied-no-approval-rule-and-could-not-request-from-user}}
(try
(let [result (handler request {:session-id session-id})
;; If handler returns a channel, await it
Expand All @@ -176,16 +176,16 @@

(and (map? result) (contains? result :result)
(map? (:result result)) (contains? (:result result) :kind))
result
result

:else
(do
(log/warn "Invalid permission response for session " session-id ": " result)
{:result {:kind "denied-no-approval-rule-and-could-not-request-from-user"}})))
(catch Exception e
(log/error "Permission handler error for session " session-id ": " (ex-message e))
{:result {:kind "denied-no-approval-rule-and-could-not-request-from-user"}})))))
:io))
:else
(do
(log/warn "Invalid permission response for session " session-id ": " result)
{:result {:kind :denied-no-approval-rule-and-could-not-request-from-user}})))
(catch Exception e
(log/error "Permission handler error for session " session-id ": " (ex-message e))
{:result {:kind :denied-no-approval-rule-and-could-not-request-from-user}})))))
:io))

(defn handle-user-input-request!
"Handle an incoming user input request (ask_user). Returns a channel with the result.
Expand Down
Loading