Skip to content

Commit 3cbf9e4

Browse files
authored
Merge pull request #36 from copilot-community-sdk/port-upstream-examples-and-docs
Port upstream examples (PR #512) and Azure Managed Identity docs (PR #498)
2 parents cd0a6c8 + f2baca6 commit 3cbf9e4

12 files changed

Lines changed: 487 additions & 5 deletions

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. This change
33

44
## [Unreleased]
55

6+
### Added (documentation)
7+
- Azure Managed Identity BYOK guide (`doc/auth/azure-managed-identity.md`): shows how to use `DefaultAzureCredential` with short-lived bearer tokens for Azure AI Foundry, with Clojure examples for basic usage and token refresh (upstream PR #498).
8+
- Updated BYOK limitations to link to the Managed Identity workaround instead of listing it as fully unsupported.
9+
- Added Azure Managed Identity guide to `doc/auth/index.md` and `doc/index.md`.
10+
11+
### Added (upstream PR #512 sync)
12+
- `examples/file_attachments.clj` — Demonstrates sending file attachments with prompts using `:attachments` in message options.
13+
- `examples/session_resume.clj` — Demonstrates session resume: create session, send secret word, resume by ID, verify context preserved.
14+
- `examples/infinite_sessions.clj` — Demonstrates infinite sessions with context compaction thresholds for long conversations.
15+
- `examples/lifecycle_hooks.clj` — Demonstrates all 6 lifecycle hooks: session start/end, pre/post tool use, user prompt submitted, error occurred.
16+
- `examples/reasoning_effort.clj` — Demonstrates the `:reasoning-effort` session config option.
17+
618
## [0.1.28.0] - 2026-02-27
719
### Changed (upstream PR #554 sync)
820
- **BREAKING**: `:on-permission-request` is now **required** when calling `create-session`, `resume-session`, `<create-session`, and `<resume-session`. Calls without a handler throw `ExceptionInfo` with a descriptive message. This matches upstream Node.js SDK where `onPermissionRequest` is required in `SessionConfig` and `ResumeSessionConfig` (upstream PR #554).

doc/auth/azure-managed-identity.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# Azure Managed Identity with BYOK
2+
3+
The Copilot SDK's [BYOK mode](./byok.md) accepts static API keys, but Azure deployments often use **Managed Identity** (Entra ID) instead of long-lived keys. Since the SDK does not natively support Entra ID authentication, you can obtain a short-lived bearer token and pass it via the `:bearer-token` provider config field.
4+
5+
This guide shows how to use `DefaultAzureCredential` from the [Azure Identity SDK](https://learn.microsoft.com/java/api/overview/azure/identity-readme) to authenticate with Azure AI Foundry models through the Copilot SDK.
6+
7+
## How It Works
8+
9+
Azure AI Foundry's OpenAI-compatible endpoint accepts bearer tokens from Entra ID in place of static API keys. The pattern is:
10+
11+
1. Use `DefaultAzureCredential` to obtain a token for the `https://cognitiveservices.azure.com/.default` scope
12+
2. Pass the token as `:bearer-token` in the BYOK provider config
13+
3. Refresh the token before it expires (tokens are typically valid for ~1 hour)
14+
15+
## Clojure Example
16+
17+
### Prerequisites
18+
19+
Add the Azure Identity SDK to your `deps.edn`:
20+
21+
```clojure
22+
;; deps.edn
23+
{:deps {com.azure/azure-identity {:mvn/version "1.15.4"}
24+
io.github.copilot-community-sdk/copilot-sdk-clojure {:mvn/version "LATEST"}}}
25+
```
26+
27+
### Basic Usage
28+
29+
<!-- docs-validate: skip -->
30+
```clojure
31+
(require '[github.copilot-sdk :as copilot])
32+
(require '[github.copilot-sdk.helpers :as h])
33+
34+
(import '[com.azure.identity DefaultAzureCredentialBuilder]
35+
'[com.azure.core.credential TokenRequestContext])
36+
37+
(def cognitive-services-scope "https://cognitiveservices.azure.com/.default")
38+
39+
(defn get-azure-token
40+
"Obtain a short-lived bearer token string from Entra ID."
41+
[]
42+
(let [credential (.build (DefaultAzureCredentialBuilder.))
43+
context (doto (TokenRequestContext.)
44+
(.addScopes (into-array String [cognitive-services-scope])))]
45+
(-> (.getToken credential context)
46+
(.block)
47+
(.getToken))))
48+
49+
(def foundry-url (System/getenv "AZURE_AI_FOUNDRY_RESOURCE_URL"))
50+
51+
(copilot/with-client-session [session
52+
{:model "gpt-4.1"
53+
:provider {:provider-type :openai
54+
:base-url (str foundry-url "/openai/v1/")
55+
:bearer-token (get-azure-token)
56+
:wire-api :responses}}]
57+
(println (h/query "Hello from Managed Identity!" :session session)))
58+
```
59+
60+
### Token Refresh for Long-Running Applications
61+
62+
Bearer tokens expire (typically after ~1 hour). For servers or long-running agents, refresh the token before creating each session:
63+
64+
<!-- docs-validate: skip -->
65+
```clojure
66+
(require '[github.copilot-sdk :as copilot])
67+
(require '[github.copilot-sdk.helpers :as h])
68+
69+
(import '[com.azure.identity DefaultAzureCredentialBuilder]
70+
'[com.azure.core.credential TokenRequestContext])
71+
72+
(def cognitive-services-scope "https://cognitiveservices.azure.com/.default")
73+
(def credential (.build (DefaultAzureCredentialBuilder.)))
74+
(def context (doto (TokenRequestContext.)
75+
(.addScopes (into-array String [cognitive-services-scope]))))
76+
77+
(defn fresh-provider-config
78+
"Build a provider config with a freshly obtained bearer token."
79+
[foundry-url]
80+
(let [token (-> (.getToken credential context)
81+
(.block)
82+
(.getToken))]
83+
{:provider-type :openai
84+
:base-url (str foundry-url "/openai/v1/")
85+
:bearer-token token
86+
:wire-api :responses}))
87+
88+
(def foundry-url (System/getenv "AZURE_AI_FOUNDRY_RESOURCE_URL"))
89+
90+
;; Each session gets a fresh token
91+
(copilot/with-client [client {}]
92+
(dotimes [_ 3]
93+
(copilot/with-session [session client
94+
{:model "gpt-4.1"
95+
:provider (fresh-provider-config foundry-url)}]
96+
(println (h/query "Hello!" :session session)))))
97+
```
98+
99+
## Environment Configuration
100+
101+
| Variable | Description | Example |
102+
|----------|-------------|---------|
103+
| `AZURE_AI_FOUNDRY_RESOURCE_URL` | Your Azure AI Foundry resource URL | `https://myresource.openai.azure.com` |
104+
105+
No API key environment variable is needed — authentication is handled by `DefaultAzureCredential`, which automatically supports:
106+
107+
- **Managed Identity** (system-assigned or user-assigned) — for Azure-hosted apps
108+
- **Azure CLI** (`az login`) — for local development
109+
- **Environment variables** (`AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_CLIENT_SECRET`) — for service principals
110+
- **Workload Identity** — for Kubernetes
111+
112+
See the [DefaultAzureCredential documentation](https://learn.microsoft.com/java/api/com.azure.identity.defaultazurecredential) for the full credential chain.
113+
114+
## When to Use This Pattern
115+
116+
| Scenario | Recommendation |
117+
|----------|----------------|
118+
| Azure-hosted app with Managed Identity | ✅ Use this pattern |
119+
| App with existing Azure AD service principal | ✅ Use this pattern |
120+
| Local development with `az login` | ✅ Use this pattern |
121+
| Non-Azure environment with static API key | Use [standard BYOK](./byok.md) |
122+
| GitHub Copilot subscription available | Use [GitHub auth](./index.md#github-signed-in-user) |
123+
124+
## See Also
125+
126+
- [BYOK Setup Guide](./byok.md) — Static API key configuration
127+
- [Authentication Overview](./index.md) — All authentication methods
128+
- [Azure Identity documentation](https://learn.microsoft.com/java/api/overview/azure/identity-readme)

doc/auth/byok.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,13 +167,14 @@ Some providers require bearer token authentication instead of API keys:
167167

168168
### Identity Limitations
169169

170-
BYOK authentication is **key-based only**. The following are NOT supported:
170+
BYOK authentication uses **static credentials that you supply** (API keys or bearer tokens); it does not natively perform Entra ID, OIDC, or managed identity flows. However, you can use `DefaultAzureCredential` to obtain a short-lived bearer token and pass it via `:bearer-token`. See the [Azure Managed Identity workaround](./azure-managed-identity.md) for details.
171+
172+
The following identity flows are NOT natively supported (you must handle them yourself and pass the resulting credential to BYOK):
171173

172174
- ❌ Microsoft Entra ID (Azure AD) managed identities or service principals
173175
- ❌ Third-party identity providers (OIDC, SAML, etc.)
174-
- ❌ Azure Managed Identity
175176

176-
You must use an API key or bearer token that you manage yourself.
177+
You must provide and manage the API key or bearer token that BYOK uses.
177178

178179
### Feature Limitations
179180

doc/auth/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,5 +134,6 @@ To prevent the SDK from automatically using stored credentials or `gh` CLI auth:
134134
## Next Steps
135135

136136
- [BYOK Documentation](./byok.md) — Use your own API keys
137+
- [Azure Managed Identity](./azure-managed-identity.md) — Azure BYOK without static API keys
137138
- [Getting Started Guide](../getting-started.md) — Build your first Copilot-powered app
138139
- [MCP Servers](../mcp/overview.md) — Connect to external tools

doc/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Clojure SDK for programmatic control of the GitHub Copilot CLI via JSON-RPC.
1111

1212
- [Authentication](auth/index.md) — GitHub auth, OAuth, environment variables, priority order
1313
- [BYOK Providers](auth/byok.md) — Bring Your Own Key for OpenAI, Azure, Anthropic, Ollama
14+
- [Azure Managed Identity](auth/azure-managed-identity.md) — Azure BYOK with Managed Identity (no API keys)
1415
- [MCP Servers](mcp/overview.md) — Model Context Protocol server integration
1516
- [MCP Debugging](mcp/debugging.md) — Troubleshooting MCP connections
1617

examples/README.md

Lines changed: 148 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,30 @@ clojure -A:examples -X byok-provider/run :provider-name '"ollama"'
6767
# MCP local server (requires npx/Node.js)
6868
clojure -A:examples -X mcp-local-server/run
6969
clojure -A:examples -X mcp-local-server/run-with-custom-tools
70+
71+
# File attachments
72+
clojure -A:examples -X file-attachments/run
73+
74+
# Session resume
75+
clojure -A:examples -X session-resume/run
76+
77+
# Infinite sessions (context compaction)
78+
clojure -A:examples -X infinite-sessions/run
79+
80+
# Lifecycle hooks
81+
clojure -A:examples -X lifecycle-hooks/run
82+
83+
# Reasoning effort
84+
clojure -A:examples -X reasoning-effort/run
7085
```
7186

7287
Or run all examples:
7388
```bash
7489
./run-all-examples.sh
7590
```
7691

77-
> **Note:** `run-all-examples.sh` runs the core examples (1–9) that need only the Copilot CLI.
78-
> Example 10 (BYOK) and Example 11 (MCP) require external dependencies (API keys, Node.js) and must be run manually.
92+
> **Note:** `run-all-examples.sh` runs 14 examples that need only the Copilot CLI (examples 1–9 and 12–16).
93+
> Examples 10 (BYOK) and 11 (MCP) require external dependencies (API keys, Node.js) and must be run manually.
7994
8095
With a custom CLI path:
8196
```bash
@@ -584,6 +599,137 @@ See [doc/mcp/overview.md](../doc/mcp/overview.md) for full MCP documentation.
584599

585600
---
586601

602+
## Example 12: File Attachments (`file_attachments.clj`)
603+
604+
**Difficulty:** Beginner
605+
**Concepts:** File attachments, message options
606+
607+
Attach files to a prompt so the model can analyze their contents.
608+
609+
### What It Demonstrates
610+
611+
- Sending `:attachments` in message options with `send-and-wait!`
612+
- File attachment type: `{:type :file :path "/absolute/path"}`
613+
- Resolving relative paths to absolute with `java.io.File`
614+
615+
### Usage
616+
617+
```bash
618+
# Attach and analyze deps.edn (default)
619+
clojure -A:examples -X file-attachments/run
620+
621+
# Attach a different file
622+
clojure -A:examples -X file-attachments/run :file-path '"README.md"'
623+
```
624+
625+
---
626+
627+
## Example 13: Session Resume (`session_resume.clj`)
628+
629+
**Difficulty:** Intermediate
630+
**Concepts:** Session persistence, session resume, multi-session lifecycle
631+
632+
Resume a previous session by ID to continue a conversation with preserved context.
633+
634+
### What It Demonstrates
635+
636+
- Creating a session and sending a message to store context
637+
- Retrieving the session ID from the session map
638+
- Resuming a session with `copilot/resume-session`
639+
- Verifying context is preserved across resume
640+
- Manual session lifecycle with `with-client` (ensures `stop!`/session cleanup), `create-session`, `resume-session`
641+
642+
### Usage
643+
644+
```bash
645+
# Default: remembers "PINEAPPLE"
646+
clojure -A:examples -X session-resume/run
647+
648+
# Custom secret word
649+
clojure -A:examples -X session-resume/run :secret-word '"MANGO"'
650+
```
651+
652+
---
653+
654+
## Example 14: Infinite Sessions (`infinite_sessions.clj`)
655+
656+
**Difficulty:** Intermediate
657+
**Concepts:** Infinite sessions, context compaction, long conversations
658+
659+
Enable infinite sessions so the SDK automatically compacts older messages when the context window fills up.
660+
661+
### What It Demonstrates
662+
663+
- Configuring `:infinite-sessions` with compaction thresholds
664+
- `:background-compaction-threshold` — when background compaction starts (80%)
665+
- `:buffer-exhaustion-threshold` — when urgent compaction triggers (95%)
666+
- Sending multiple prompts in a long-running session
667+
668+
### Usage
669+
670+
```bash
671+
clojure -A:examples -X infinite-sessions/run
672+
673+
# Custom prompts
674+
clojure -A:examples -X infinite-sessions/run :prompts '["What is Clojure?" "Who created it?" "When?"]'
675+
```
676+
677+
---
678+
679+
## Example 15: Lifecycle Hooks (`lifecycle_hooks.clj`)
680+
681+
**Difficulty:** Intermediate
682+
**Concepts:** Hooks, callbacks, tool use monitoring
683+
684+
Register callbacks for session lifecycle events: start/end, tool use, prompts, and errors.
685+
686+
### What It Demonstrates
687+
688+
- Configuring `:hooks` in session config with all 6 hook types
689+
- `:on-session-start` — fires when session begins
690+
- `:on-session-end` — fires when session ends
691+
- `:on-pre-tool-use` — fires before a tool runs (return `{:approved true}` to allow)
692+
- `:on-post-tool-use` — fires after a tool completes
693+
- `:on-user-prompt-submitted` — fires when user sends a prompt
694+
- `:on-error-occurred` — fires on errors
695+
- Collecting and summarizing hook events
696+
697+
### Usage
698+
699+
```bash
700+
clojure -A:examples -X lifecycle-hooks/run
701+
702+
# Custom prompt
703+
clojure -A:examples -X lifecycle-hooks/run :prompt '"List all .md files using glob"'
704+
```
705+
706+
---
707+
708+
## Example 16: Reasoning Effort (`reasoning_effort.clj`)
709+
710+
**Difficulty:** Beginner
711+
**Concepts:** Reasoning effort, model configuration
712+
713+
Control how much reasoning the model applies with the `:reasoning-effort` option.
714+
715+
### What It Demonstrates
716+
717+
- Setting `:reasoning-effort` in session config
718+
- Valid values: `"low"`, `"medium"`, `"high"`, `"xhigh"`
719+
- Lower effort produces faster, more concise responses
720+
721+
### Usage
722+
723+
```bash
724+
# Default: low reasoning effort
725+
clojure -A:examples -X reasoning-effort/run
726+
727+
# Higher reasoning effort
728+
clojure -A:examples -X reasoning-effort/run :effort '"high"'
729+
```
730+
731+
---
732+
587733
## Clojure vs JavaScript Comparison
588734

589735
Here's how common patterns compare between the Clojure and JavaScript SDKs:

examples/file_attachments.clj

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
(ns file-attachments
2+
"Demonstrates sending file attachments with a prompt.
3+
Attaches the project's deps.edn file and asks the model to analyze it."
4+
(:require [github.copilot-sdk :as copilot]))
5+
6+
;; See examples/README.md for usage
7+
8+
(def defaults
9+
{:prompt "Summarize the dependencies in the attached file in 2-3 sentences."
10+
:file-path "deps.edn"})
11+
12+
(defn run
13+
[{:keys [prompt file-path]
14+
:or {prompt (:prompt defaults) file-path (:file-path defaults)}}]
15+
(let [abs-path (.getAbsolutePath (java.io.File. file-path))]
16+
(copilot/with-client-session [session {:on-permission-request copilot/approve-all
17+
:model "claude-haiku-4.5"}]
18+
(println "📎 Attaching:" abs-path)
19+
(println "Q:" prompt)
20+
(let [response (copilot/send-and-wait!
21+
session
22+
{:prompt prompt
23+
:attachments [{:type :file :path abs-path}]})]
24+
(println "🤖:" (get-in response [:data :content]))))))

0 commit comments

Comments
 (0)