Skip to content
Merged
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. This change

## [Unreleased]

### Added (documentation)
- 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).
- Updated BYOK limitations to link to the Managed Identity workaround instead of listing it as fully unsupported.
- Added Azure Managed Identity guide to `doc/auth/index.md` and `doc/index.md`.

### Added (upstream PR #512 sync)
- `examples/file_attachments.clj` — Demonstrates sending file attachments with prompts using `:attachments` in message options.
- `examples/session_resume.clj` — Demonstrates session resume: create session, send secret word, resume by ID, verify context preserved.
- `examples/infinite_sessions.clj` — Demonstrates infinite sessions with context compaction thresholds for long conversations.
- `examples/lifecycle_hooks.clj` — Demonstrates all 6 lifecycle hooks: session start/end, pre/post tool use, user prompt submitted, error occurred.
- `examples/reasoning_effort.clj` — Demonstrates the `:reasoning-effort` session config option.

## [0.1.28.0] - 2026-02-27
### Changed (upstream PR #554 sync)
- **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).
Expand Down
124 changes: 124 additions & 0 deletions doc/auth/azure-managed-identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Azure Managed Identity with BYOK

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.

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.

## How It Works

Azure AI Foundry's OpenAI-compatible endpoint accepts bearer tokens from Entra ID in place of static API keys. The pattern is:

1. Use `DefaultAzureCredential` to obtain a token for the `https://cognitiveservices.azure.com/.default` scope
2. Pass the token as `:bearer-token` in the BYOK provider config
3. Refresh the token before it expires (tokens are typically valid for ~1 hour)

## Clojure Example

### Prerequisites

Add the Azure Identity SDK to your `deps.edn`:

```clojure
;; deps.edn
{:deps {com.azure/azure-identity {:mvn/version "1.15.4"}
io.github.niclasgustafsson/copilot-sdk-clojure {:mvn/version "RELEASE"}}}
```

### Basic Usage

<!-- docs-validate: skip -->
```clojure
(require '[github.copilot-sdk :as copilot])
(require '[github.copilot-sdk.helpers :as h])

(import '[com.azure.identity DefaultAzureCredentialBuilder]
'[com.azure.core.credential TokenRequestContext])

(def cognitive-services-scope "https://cognitiveservices.azure.com/.default")

(defn get-azure-token
"Obtain a short-lived bearer token from Entra ID."
[]
(let [credential (.build (DefaultAzureCredentialBuilder.))
context (doto (TokenRequestContext.)
(.addScopes (into-array String [cognitive-services-scope])))]
(.getToken credential context)))

(def foundry-url (System/getenv "AZURE_AI_FOUNDRY_RESOURCE_URL"))

(copilot/with-client-session [session
{:model "gpt-4.1"
:provider {:provider-type :openai
:base-url (str foundry-url "/openai/v1/")
:bearer-token (.getToken (get-azure-token))
:wire-api :responses}}]
(println (h/query "Hello from Managed Identity!" :session session)))
```

### Token Refresh for Long-Running Applications

Bearer tokens expire (typically after ~1 hour). For servers or long-running agents, refresh the token before creating each session:

<!-- docs-validate: skip -->
```clojure
(require '[github.copilot-sdk :as copilot])
(require '[github.copilot-sdk.helpers :as h])

(import '[com.azure.identity DefaultAzureCredentialBuilder]
'[com.azure.core.credential TokenRequestContext])

(def cognitive-services-scope "https://cognitiveservices.azure.com/.default")
(def credential (.build (DefaultAzureCredentialBuilder.)))
(def context (doto (TokenRequestContext.)
(.addScopes (into-array String [cognitive-services-scope]))))

(defn fresh-provider-config
"Build a provider config with a freshly obtained bearer token."
[foundry-url]
(let [token (.getToken credential context)]
{:provider-type :openai
:base-url (str foundry-url "/openai/v1/")
:bearer-token (.getToken token)
:wire-api :responses}))

(def foundry-url (System/getenv "AZURE_AI_FOUNDRY_RESOURCE_URL"))

;; Each session gets a fresh token
(copilot/with-client [client {}]
(dotimes [_ 3]
(copilot/with-session [session client
{:model "gpt-4.1"
:provider (fresh-provider-config foundry-url)}]
(println (h/query "Hello!" :session session)))))
```

## Environment Configuration

| Variable | Description | Example |
|----------|-------------|---------|
| `AZURE_AI_FOUNDRY_RESOURCE_URL` | Your Azure AI Foundry resource URL | `https://myresource.openai.azure.com` |

No API key environment variable is needed — authentication is handled by `DefaultAzureCredential`, which automatically supports:

- **Managed Identity** (system-assigned or user-assigned) — for Azure-hosted apps
- **Azure CLI** (`az login`) — for local development
- **Environment variables** (`AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_CLIENT_SECRET`) — for service principals
- **Workload Identity** — for Kubernetes

See the [DefaultAzureCredential documentation](https://learn.microsoft.com/java/api/com.azure.identity.defaultazurecredential) for the full credential chain.

## When to Use This Pattern

| Scenario | Recommendation |
|----------|----------------|
| Azure-hosted app with Managed Identity | ✅ Use this pattern |
| App with existing Azure AD service principal | ✅ Use this pattern |
| Local development with `az login` | ✅ Use this pattern |
| Non-Azure environment with static API key | Use [standard BYOK](./byok.md) |
| GitHub Copilot subscription available | Use [GitHub auth](./index.md#github-signed-in-user) |

## See Also

- [BYOK Setup Guide](./byok.md) — Static API key configuration
- [Authentication Overview](./index.md) — All authentication methods
- [Azure Identity documentation](https://learn.microsoft.com/java/api/overview/azure/identity-readme)
5 changes: 3 additions & 2 deletions doc/auth/byok.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,12 @@ Some providers require bearer token authentication instead of API keys:

### Identity Limitations

BYOK authentication is **key-based only**. The following are NOT supported:
BYOK authentication is **key-based only** — no native Entra ID, OIDC, or managed identity support. 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.

The following are NOT natively supported:

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

You must use an API key or bearer token that you manage yourself.

Expand Down
1 change: 1 addition & 0 deletions doc/auth/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,5 +134,6 @@ To prevent the SDK from automatically using stored credentials or `gh` CLI auth:
## Next Steps

- [BYOK Documentation](./byok.md) — Use your own API keys
- [Azure Managed Identity](./azure-managed-identity.md) — Azure BYOK without static API keys
- [Getting Started Guide](../getting-started.md) — Build your first Copilot-powered app
- [MCP Servers](../mcp/overview.md) — Connect to external tools
1 change: 1 addition & 0 deletions doc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Clojure SDK for programmatic control of the GitHub Copilot CLI via JSON-RPC.

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

Expand Down
150 changes: 148 additions & 2 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,30 @@ clojure -A:examples -X byok-provider/run :provider-name '"ollama"'
# MCP local server (requires npx/Node.js)
clojure -A:examples -X mcp-local-server/run
clojure -A:examples -X mcp-local-server/run-with-custom-tools

# File attachments
clojure -A:examples -X file-attachments/run

# Session resume
clojure -A:examples -X session-resume/run

# Infinite sessions (context compaction)
clojure -A:examples -X infinite-sessions/run

# Lifecycle hooks
clojure -A:examples -X lifecycle-hooks/run

# Reasoning effort
clojure -A:examples -X reasoning-effort/run
```

Or run all examples:
```bash
./run-all-examples.sh
```

> **Note:** `run-all-examples.sh` runs the core examples (1–9) that need only the Copilot CLI.
> Example 10 (BYOK) and Example 11 (MCP) require external dependencies (API keys, Node.js) and must be run manually.
> **Note:** `run-all-examples.sh` runs 14 examples that need only the Copilot CLI (examples 1–9 and 12–16).
> Examples 10 (BYOK) and 11 (MCP) require external dependencies (API keys, Node.js) and must be run manually.

With a custom CLI path:
```bash
Expand Down Expand Up @@ -584,6 +599,137 @@ See [doc/mcp/overview.md](../doc/mcp/overview.md) for full MCP documentation.

---

## Example 12: File Attachments (`file_attachments.clj`)

**Difficulty:** Beginner
**Concepts:** File attachments, message options

Attach files to a prompt so the model can analyze their contents.

### What It Demonstrates

- Sending `:attachments` in message options with `send-and-wait!`
- File attachment type: `{:type :file :path "/absolute/path"}`
- Resolving relative paths to absolute with `java.io.File`

### Usage

```bash
# Attach and analyze deps.edn (default)
clojure -A:examples -X file-attachments/run

# Attach a different file
clojure -A:examples -X file-attachments/run :file-path '"README.md"'
```

---

## Example 13: Session Resume (`session_resume.clj`)

**Difficulty:** Intermediate
**Concepts:** Session persistence, session resume, multi-session lifecycle

Resume a previous session by ID to continue a conversation with preserved context.

### What It Demonstrates

- Creating a session and sending a message to store context
- Retrieving the session ID from the session map
- Resuming a session with `copilot/resume-session`
- Verifying context is preserved across resume
- Manual session lifecycle with `with-client`, `create-session`, `destroy!`

### Usage

```bash
# Default: remembers "PINEAPPLE"
clojure -A:examples -X session-resume/run

# Custom secret word
clojure -A:examples -X session-resume/run :secret-word '"MANGO"'
```

---

## Example 14: Infinite Sessions (`infinite_sessions.clj`)

**Difficulty:** Intermediate
**Concepts:** Infinite sessions, context compaction, long conversations

Enable infinite sessions so the SDK automatically compacts older messages when the context window fills up.

### What It Demonstrates

- Configuring `:infinite-sessions` with compaction thresholds
- `:background-compaction-threshold` — when background compaction starts (80%)
- `:buffer-exhaustion-threshold` — when urgent compaction triggers (95%)
- Sending multiple prompts in a long-running session

### Usage

```bash
clojure -A:examples -X infinite-sessions/run

# Custom prompts
clojure -A:examples -X infinite-sessions/run :prompts '["What is Clojure?" "Who created it?" "When?"]'
```

---

## Example 15: Lifecycle Hooks (`lifecycle_hooks.clj`)

**Difficulty:** Intermediate
**Concepts:** Hooks, callbacks, tool use monitoring

Register callbacks for session lifecycle events: start/end, tool use, prompts, and errors.

### What It Demonstrates

- Configuring `:hooks` in session config with all 6 hook types
- `:on-session-start` — fires when session begins
- `:on-session-end` — fires when session ends
- `:on-pre-tool-use` — fires before a tool runs (return `{:approved true}` to allow)
- `:on-post-tool-use` — fires after a tool completes
- `:on-user-prompt-submitted` — fires when user sends a prompt
- `:on-error-occurred` — fires on errors
- Collecting and summarizing hook events

### Usage

```bash
clojure -A:examples -X lifecycle-hooks/run

# Custom prompt
clojure -A:examples -X lifecycle-hooks/run :prompt '"List all .md files using glob"'
```

---

## Example 16: Reasoning Effort (`reasoning_effort.clj`)

**Difficulty:** Beginner
**Concepts:** Reasoning effort, model configuration

Control how much reasoning the model applies with the `:reasoning-effort` option.

### What It Demonstrates

- Setting `:reasoning-effort` in session config
- Valid values: `"low"`, `"medium"`, `"high"`, `"xhigh"`
- Lower effort produces faster, more concise responses

### Usage

```bash
# Default: low reasoning effort
clojure -A:examples -X reasoning-effort/run

# Higher reasoning effort
clojure -A:examples -X reasoning-effort/run :effort '"high"'
```

---

## Clojure vs JavaScript Comparison

Here's how common patterns compare between the Clojure and JavaScript SDKs:
Expand Down
24 changes: 24 additions & 0 deletions examples/file_attachments.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
(ns file-attachments
"Demonstrates sending file attachments with a prompt.
Attaches the project's deps.edn file and asks the model to analyze it."
(:require [github.copilot-sdk :as copilot]))

;; See examples/README.md for usage

(def defaults
{:prompt "Summarize the dependencies in the attached file in 2-3 sentences."
:file-path "deps.edn"})

(defn run
[{:keys [prompt file-path]
:or {prompt (:prompt defaults) file-path (:file-path defaults)}}]
(let [abs-path (.getAbsolutePath (java.io.File. file-path))]
(copilot/with-client-session [session {:on-permission-request copilot/approve-all
:model "claude-haiku-4.5"}]
(println "📎 Attaching:" abs-path)
(println "Q:" prompt)
(let [response (copilot/send-and-wait!
session
{:prompt prompt
:attachments [{:type :file :path abs-path}]})]
(println "🤖:" (get-in response [:data :content]))))))
Loading