orchid-cli 1.0.2__tar.gz → 1.0.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {orchid_cli-1.0.2 → orchid_cli-1.0.4}/AGENTS.md +43 -0
  2. orchid_cli-1.0.4/CHANGELOG.md +58 -0
  3. {orchid_cli-1.0.2 → orchid_cli-1.0.4}/PKG-INFO +69 -4
  4. {orchid_cli-1.0.2 → orchid_cli-1.0.4}/README.md +67 -2
  5. orchid_cli-1.0.4/orchid_cli/auth/__init__.py +15 -0
  6. orchid_cli-1.0.4/orchid_cli/auth/config.py +148 -0
  7. orchid_cli-1.0.4/orchid_cli/auth/flow.py +228 -0
  8. orchid_cli-1.0.4/orchid_cli/auth/middleware.py +156 -0
  9. orchid_cli-1.0.4/orchid_cli/auth/token_store.py +103 -0
  10. {orchid_cli-1.0.2 → orchid_cli-1.0.4}/orchid_cli/bootstrap.py +6 -2
  11. orchid_cli-1.0.4/orchid_cli/commands/auth.py +175 -0
  12. {orchid_cli-1.0.2 → orchid_cli-1.0.4}/orchid_cli/commands/chat.py +34 -32
  13. {orchid_cli-1.0.2 → orchid_cli-1.0.4}/orchid_cli/commands/skill.py +39 -1
  14. {orchid_cli-1.0.2 → orchid_cli-1.0.4}/orchid_cli/main.py +2 -1
  15. {orchid_cli-1.0.2 → orchid_cli-1.0.4}/pyproject.toml +2 -2
  16. {orchid_cli-1.0.2 → orchid_cli-1.0.4}/tests/conftest.py +0 -1
  17. orchid_cli-1.0.4/tests/test_auth.py +349 -0
  18. {orchid_cli-1.0.2 → orchid_cli-1.0.4}/tests/test_commands_chat.py +14 -11
  19. orchid_cli-1.0.2/CHANGELOG.md +0 -27
  20. {orchid_cli-1.0.2 → orchid_cli-1.0.4}/.editorconfig +0 -0
  21. {orchid_cli-1.0.2 → orchid_cli-1.0.4}/.github/workflows/ci.yml +0 -0
  22. {orchid_cli-1.0.2 → orchid_cli-1.0.4}/.gitignore +0 -0
  23. {orchid_cli-1.0.2 → orchid_cli-1.0.4}/.gitlint +0 -0
  24. {orchid_cli-1.0.2 → orchid_cli-1.0.4}/.pre-commit-config.yaml +0 -0
  25. {orchid_cli-1.0.2 → orchid_cli-1.0.4}/CLAUDE.md +0 -0
  26. {orchid_cli-1.0.2 → orchid_cli-1.0.4}/CONTRIBUTING.md +0 -0
  27. {orchid_cli-1.0.2 → orchid_cli-1.0.4}/LICENSE +0 -0
  28. {orchid_cli-1.0.2 → orchid_cli-1.0.4}/icon.svg +0 -0
  29. {orchid_cli-1.0.2 → orchid_cli-1.0.4}/orchid_cli/__init__.py +0 -0
  30. {orchid_cli-1.0.2 → orchid_cli-1.0.4}/orchid_cli/commands/__init__.py +0 -0
  31. {orchid_cli-1.0.2 → orchid_cli-1.0.4}/orchid_cli/commands/config.py +0 -0
  32. {orchid_cli-1.0.2 → orchid_cli-1.0.4}/orchid_cli/commands/index.py +0 -0
  33. {orchid_cli-1.0.2 → orchid_cli-1.0.4}/tests/fixtures/__init__.py +0 -0
  34. {orchid_cli-1.0.2 → orchid_cli-1.0.4}/tests/fixtures/fake_tools_dates.py +0 -0
  35. {orchid_cli-1.0.2 → orchid_cli-1.0.4}/tests/fixtures/fake_tools_math.py +0 -0
  36. {orchid_cli-1.0.2 → orchid_cli-1.0.4}/tests/test_bootstrap.py +0 -0
  37. {orchid_cli-1.0.2 → orchid_cli-1.0.4}/tests/test_commands_config.py +0 -0
  38. {orchid_cli-1.0.2 → orchid_cli-1.0.4}/tests/test_commands_skill.py +0 -0
@@ -11,7 +11,13 @@ orchid-cli/
11
11
  orchid_cli/
12
12
  main.py Typer entry point — registers sub-commands
13
13
  bootstrap.py Shared startup: load config, build graph, init SQLite storage
14
+ auth/ OAuth 2.0 authentication (Authorization Code + PKCE)
15
+ config.py OAuth provider settings from orchid.yml (OIDC discovery)
16
+ flow.py PKCE flow: browser login, localhost callback, code exchange
17
+ token_store.py Secure token persistence (~/.orchid/tokens.json)
18
+ middleware.py Token refresh + AuthContext builder
14
19
  commands/
20
+ auth.py login, logout, status subcommands
15
21
  chat.py Full CRUD: create, list, delete, history, send, interactive, rename, share
16
22
  config.py validate command (checks agents.yaml)
17
23
  index.py seed command (batch-index RAG data)
@@ -45,9 +51,18 @@ orchid-cli/
45
51
 
46
52
  6. **Config resolution:** CLI args > env vars > `orchid.yml` > hardcoded defaults.
47
53
 
54
+ 7. **OAuth auth is self-contained in `auth/`.** The `auth/` subpackage handles the full OAuth 2.0 Authorization Code + PKCE flow. No OAuth logic in `chat.py`, `bootstrap.py`, or any other module. Chat commands call `get_auth_context(config_path)` which returns either a real OAuth-backed `AuthContext` or the dev fallback — callers don't know or care which.
55
+
56
+ 8. **Token storage at `~/.orchid/tokens.json`.** Permissions set to `0o600` (owner-only). Tokens are keyed by `client_id`, supporting multiple providers. Refresh tokens are used automatically when the access token expires.
57
+
48
58
  ## Commands
49
59
 
50
60
  ```bash
61
+ # Authentication (OAuth 2.0)
62
+ orchid auth login --config <path> # Opens browser for OAuth login
63
+ orchid auth status --config <path> # Show current auth state
64
+ orchid auth logout --config <path> # Clear stored tokens
65
+
51
66
  # Chat operations
52
67
  orchid chat create --config <path> # Create new chat session
53
68
  orchid chat list --config <path> # List all chats
@@ -96,6 +111,32 @@ Chat ID prefix matching is supported (type first few chars of UUID).
96
111
  | Storage class | `orchid_ai.persistence.sqlite.SQLiteChatStorage` | `CHAT_STORAGE_CLASS` |
97
112
  | Storage DSN | `~/.orchid/chats.db` | `CHAT_DB_DSN` |
98
113
 
114
+ ## OAuth Configuration
115
+
116
+ OAuth is configured via the `auth.cli` section in `orchid.yml`. When absent or when `auth.dev_bypass: true`, the CLI uses a dummy dev token (backward compatible).
117
+
118
+ ```yaml
119
+ auth:
120
+ dev_bypass: false
121
+ identity_resolver_class: myapp.identity.Resolver # optional — enriches AuthContext
122
+ domain: platform.example.com # optional — passed to resolver
123
+
124
+ cli:
125
+ client_id: my-cli-app
126
+ scopes: openid api
127
+
128
+ # Option A: OIDC auto-discovery (recommended)
129
+ issuer: https://auth.example.com
130
+
131
+ # Option B: Explicit endpoints
132
+ # authorization_endpoint: https://auth.example.com/oauth2/authorize
133
+ # token_endpoint: https://auth.example.com/oauth2/token
134
+ ```
135
+
136
+ **Flow:** `orchid auth login` opens the browser → user authenticates → callback on `localhost` → code exchanged for tokens → stored at `~/.orchid/tokens.json`. All subsequent `orchid chat` commands use the stored token automatically, refreshing it when expired.
137
+
138
+ **Auth resolution order in chat commands:** stored OAuth token → refresh if expired → identity resolver (optional) → fallback to dev token.
139
+
99
140
  ## Running
100
141
 
101
142
  ```bash
@@ -128,3 +169,5 @@ Requires Ollama running on host with models: `llama3.2`, `nomic-embed-text`.
128
169
  - `bootstrap()` sets `ORCHID_CONFIG` as an env var so the orchid library can find the YAML. Don't remove this.
129
170
  - Chat persistence auto-creates `~/.orchid/chats.db` on first run. The directory is created automatically.
130
171
  - Embedding dimension mismatch (768 vs 1536 vs 3072) causes silent retrieval failures. Switching models requires re-indexing Qdrant.
172
+ - Running `orchid chat` against a config with OAuth-protected MCP servers without `orchid auth login` first — the CLI falls back to the dev token, and MCP servers return 401. Always run `orchid auth login -c <config>` first.
173
+ - Token file permissions — `~/.orchid/tokens.json` should be `0o600` (owner-only). The CLI sets this automatically, but manual edits or copies may loosen permissions.
@@ -0,0 +1,58 @@
1
+ # CHANGELOG
2
+
3
+ <!-- version list -->
4
+
5
+ ## v1.0.4 (2026-04-14)
6
+
7
+ ### Bug Fixes
8
+
9
+ - **auth**: Improve token expiration messaging and log formatting
10
+ ([`7ef8e5b`](https://github.com/gadz82/orchid-cli/commit/7ef8e5b4bd33ade45b8ac07634c7b84e02808a5b))
11
+
12
+ - **auth**: Integrate OAuth 2.0 PKCE flow and token storage into CLI auth commands
13
+ ([`e2df0b8`](https://github.com/gadz82/orchid-cli/commit/e2df0b8b261350382b94c8359abadab0007fdc6c))
14
+
15
+
16
+ ## v1.0.3 (2026-04-14)
17
+
18
+ ### Bug Fixes
19
+
20
+ - Built-in tools args propagation fix.
21
+ ([`b12dd65`](https://github.com/gadz82/orchid-cli/commit/b12dd652fc61b35d5cc89a1d5df9dcc6fe841533))
22
+
23
+ - Built-in tools parameter declarations in config.
24
+ ([`ce62103`](https://github.com/gadz82/orchid-cli/commit/ce621034f4ad307dd8203ab98b4eac4cc604969b))
25
+
26
+ - Fixing ruff errors and updated dependency
27
+ ([`30bbf37`](https://github.com/gadz82/orchid-cli/commit/30bbf3701d30aa422d86a2906a8546377b026248))
28
+
29
+ - Implement multi-turn LLM tool loop package version update
30
+ ([`97e0a1a`](https://github.com/gadz82/orchid-cli/commit/97e0a1a22d47ce4ad2387a68f9b588b8c81641f9))
31
+
32
+ - Tools result context injection.
33
+ ([`fbdfdef`](https://github.com/gadz82/orchid-cli/commit/fbdfdef54da0e8a2df947b12fcfb8220cf488daf))
34
+
35
+
36
+ ## v1.0.2 (2026-04-13)
37
+
38
+ ### Bug Fixes
39
+
40
+ - Removing external dependencies and improving error handling and final outcome for the user.
41
+ ([`2ebc6ef`](https://github.com/gadz82/orchid-cli/commit/2ebc6efb8f96e047a3dc649710967d4aece64ab4))
42
+
43
+
44
+ ## v1.0.1 (2026-04-13)
45
+
46
+ ### Bug Fixes
47
+
48
+ - Orchid-cli package description and readme update.
49
+ ([`5055129`](https://github.com/gadz82/orchid-cli/commit/505512906fc9b6d88752940dd1c9b24a2eec8f0d))
50
+
51
+
52
+ ## v1.0.0 (2026-04-13)
53
+
54
+ - Initial Release
55
+
56
+ ## v1.0.0 (2026-04-10)
57
+
58
+ - Initial Release
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orchid-cli
3
- Version: 1.0.2
3
+ Version: 1.0.4
4
4
  Summary: Orchid CLI — command-line interface for the Orchid agent framework
5
5
  Project-URL: Homepage, https://github.com/gadz82/orchid
6
6
  Project-URL: Repository, https://github.com/gadz82/orchid
@@ -26,7 +26,7 @@ Classifier: Typing :: Typed
26
26
  Requires-Python: >=3.11
27
27
  Requires-Dist: httpx>=0.28.0
28
28
  Requires-Dist: langchain-core>=0.3.0
29
- Requires-Dist: orchid-ai>=1.2.0
29
+ Requires-Dist: orchid-ai>=1.2.13
30
30
  Requires-Dist: pydantic-settings>=2.7.0
31
31
  Requires-Dist: pyyaml>=6.0
32
32
  Requires-Dist: rich>=13.0
@@ -64,6 +64,9 @@ The `orchid` command is available after installation.
64
64
  # Validate config:
65
65
  orchid config validate agents.yaml
66
66
 
67
+ # Authenticate (required for configs with OAuth-protected MCP servers):
68
+ orchid auth login -c orchid.yml
69
+
67
70
  # Start an interactive chat session:
68
71
  orchid chat interactive -c orchid.yml
69
72
 
@@ -74,6 +77,21 @@ orchid chat send <chat_id> "Hello!" -c orchid.yml
74
77
 
75
78
  ## Commands
76
79
 
80
+ ### Authentication
81
+
82
+ ```bash
83
+ # Log in via OAuth (opens browser, exchanges code for tokens)
84
+ orchid auth login -c orchid.yml
85
+
86
+ # Check current auth status (token expiry, tenant, user)
87
+ orchid auth status -c orchid.yml
88
+
89
+ # Clear stored tokens
90
+ orchid auth logout -c orchid.yml
91
+ ```
92
+
93
+ Authentication is required when MCP servers or tools need a real Bearer token. When OAuth is not configured (`auth.dev_bypass: true` or no `auth.cli` section), a dev fallback token is used automatically.
94
+
77
95
  ### Chat Management
78
96
 
79
97
  ```bash
@@ -193,6 +211,17 @@ llm:
193
211
  model: ollama/llama3.2
194
212
  agents:
195
213
  config_path: agents.yaml
214
+ auth:
215
+ dev_bypass: false # set true to skip OAuth entirely
216
+ identity_resolver_class: myapp.identity.Resolver # optional
217
+ domain: platform.example.com # optional
218
+ cli:
219
+ client_id: my-cli-app
220
+ scopes: openid api
221
+ issuer: https://auth.example.com # OIDC auto-discovery
222
+ # OR explicit endpoints:
223
+ # authorization_endpoint: https://auth.example.com/oauth2/authorize
224
+ # token_endpoint: https://auth.example.com/oauth2/token
196
225
  rag:
197
226
  vector_backend: null # no Qdrant needed for basic usage
198
227
  storage:
@@ -208,8 +237,38 @@ storage:
208
237
  | Vector backend | `qdrant` | `VECTOR_BACKEND` |
209
238
  | Storage class | `orchid.persistence.sqlite.SQLiteChatStorage` | `CHAT_STORAGE_CLASS` |
210
239
  | Storage DSN | `~/.orchid/chats.db` | `CHAT_DB_DSN` |
240
+ | Token storage | `~/.orchid/tokens.json` | -- |
241
+
242
+ Chat data is stored in SQLite at `~/.orchid/chats.db` by default. OAuth tokens are stored at `~/.orchid/tokens.json` with owner-only permissions (`0o600`). Both directories are created automatically on first use.
243
+
244
+ ## Authentication
245
+
246
+ The CLI supports **OAuth 2.0 Authorization Code + PKCE** for authenticating with external services. This is a generic, provider-agnostic flow that works with any standard OAuth 2.0 / OIDC provider (Okta, Auth0, Keycloak, etc.).
247
+
248
+ ### How It Works
249
+
250
+ 1. `orchid auth login` opens the system browser to the provider's authorization page
251
+ 2. User authenticates and consents
252
+ 3. Provider redirects to a temporary `localhost` callback server
253
+ 4. CLI exchanges the authorization code for access + refresh tokens (with PKCE verification)
254
+ 5. Tokens are stored at `~/.orchid/tokens.json`
255
+ 6. All subsequent `orchid chat` commands use the stored token automatically
256
+
257
+ ### OIDC Discovery
258
+
259
+ When `issuer` is set in the config, the CLI fetches `{issuer}/.well-known/openid-configuration` to auto-discover `authorization_endpoint` and `token_endpoint`. This is the recommended approach -- you only need the issuer URL.
260
+
261
+ ### Token Refresh
262
+
263
+ When the access token expires and a refresh token is available, the CLI refreshes automatically before sending the request. If refresh fails, you'll be prompted to run `orchid auth login` again.
264
+
265
+ ### Identity Resolution
266
+
267
+ When `identity_resolver_class` is configured, the CLI calls the resolver after login to populate `tenant_key` and `user_id` from the OAuth token. These identity fields are cached in the token file so subsequent commands don't need the resolver. See the [orchid IdentityResolver ABC](../orchid/orchid_ai/core/identity.py) for the interface.
268
+
269
+ ### Dev Fallback
211
270
 
212
- Chat data is stored in SQLite at `~/.orchid/chats.db` by default. The directory is created automatically on first run.
271
+ When `auth.dev_bypass: true` or `auth.cli` is absent, the CLI uses a dummy token (`cli-token`, tenant=`cli`, user=`cli-user`). This is fully backward compatible -- existing configs without OAuth continue to work unchanged.
213
272
 
214
273
  ## Prerequisites
215
274
 
@@ -222,14 +281,20 @@ Chat data is stored in SQLite at `~/.orchid/chats.db` by default. The directory
222
281
  orchid_cli/
223
282
  main.py Typer entry point -- registers sub-commands
224
283
  bootstrap.py Shared startup: load config, build graph, init storage
284
+ auth/ OAuth 2.0 authentication (self-contained)
285
+ config.py Provider settings from orchid.yml + OIDC discovery
286
+ flow.py Authorization Code + PKCE flow (browser, localhost callback)
287
+ token_store.py Secure token persistence (~/.orchid/tokens.json)
288
+ middleware.py Token refresh + AuthContext builder
225
289
  commands/
290
+ auth.py login, logout, status subcommands
226
291
  chat.py Full CRUD + messaging + interactive mode
227
292
  config.py Validate agents.yaml
228
293
  index.py Seed RAG vector store
229
294
  skill.py Generate Claude Code skills from agents.yaml
230
295
  ```
231
296
 
232
- The CLI is a thin layer that calls `orchid` SDK functions and displays results via Rich.
297
+ The CLI is a thin layer that calls `orchid` SDK functions and displays results via Rich. The `auth/` subpackage is fully self-contained -- no OAuth logic leaks into chat commands or bootstrap.
233
298
 
234
299
  ## Development
235
300
 
@@ -22,6 +22,9 @@ The `orchid` command is available after installation.
22
22
  # Validate config:
23
23
  orchid config validate agents.yaml
24
24
 
25
+ # Authenticate (required for configs with OAuth-protected MCP servers):
26
+ orchid auth login -c orchid.yml
27
+
25
28
  # Start an interactive chat session:
26
29
  orchid chat interactive -c orchid.yml
27
30
 
@@ -32,6 +35,21 @@ orchid chat send <chat_id> "Hello!" -c orchid.yml
32
35
 
33
36
  ## Commands
34
37
 
38
+ ### Authentication
39
+
40
+ ```bash
41
+ # Log in via OAuth (opens browser, exchanges code for tokens)
42
+ orchid auth login -c orchid.yml
43
+
44
+ # Check current auth status (token expiry, tenant, user)
45
+ orchid auth status -c orchid.yml
46
+
47
+ # Clear stored tokens
48
+ orchid auth logout -c orchid.yml
49
+ ```
50
+
51
+ Authentication is required when MCP servers or tools need a real Bearer token. When OAuth is not configured (`auth.dev_bypass: true` or no `auth.cli` section), a dev fallback token is used automatically.
52
+
35
53
  ### Chat Management
36
54
 
37
55
  ```bash
@@ -151,6 +169,17 @@ llm:
151
169
  model: ollama/llama3.2
152
170
  agents:
153
171
  config_path: agents.yaml
172
+ auth:
173
+ dev_bypass: false # set true to skip OAuth entirely
174
+ identity_resolver_class: myapp.identity.Resolver # optional
175
+ domain: platform.example.com # optional
176
+ cli:
177
+ client_id: my-cli-app
178
+ scopes: openid api
179
+ issuer: https://auth.example.com # OIDC auto-discovery
180
+ # OR explicit endpoints:
181
+ # authorization_endpoint: https://auth.example.com/oauth2/authorize
182
+ # token_endpoint: https://auth.example.com/oauth2/token
154
183
  rag:
155
184
  vector_backend: null # no Qdrant needed for basic usage
156
185
  storage:
@@ -166,8 +195,38 @@ storage:
166
195
  | Vector backend | `qdrant` | `VECTOR_BACKEND` |
167
196
  | Storage class | `orchid.persistence.sqlite.SQLiteChatStorage` | `CHAT_STORAGE_CLASS` |
168
197
  | Storage DSN | `~/.orchid/chats.db` | `CHAT_DB_DSN` |
198
+ | Token storage | `~/.orchid/tokens.json` | -- |
199
+
200
+ Chat data is stored in SQLite at `~/.orchid/chats.db` by default. OAuth tokens are stored at `~/.orchid/tokens.json` with owner-only permissions (`0o600`). Both directories are created automatically on first use.
201
+
202
+ ## Authentication
203
+
204
+ The CLI supports **OAuth 2.0 Authorization Code + PKCE** for authenticating with external services. This is a generic, provider-agnostic flow that works with any standard OAuth 2.0 / OIDC provider (Okta, Auth0, Keycloak, etc.).
205
+
206
+ ### How It Works
207
+
208
+ 1. `orchid auth login` opens the system browser to the provider's authorization page
209
+ 2. User authenticates and consents
210
+ 3. Provider redirects to a temporary `localhost` callback server
211
+ 4. CLI exchanges the authorization code for access + refresh tokens (with PKCE verification)
212
+ 5. Tokens are stored at `~/.orchid/tokens.json`
213
+ 6. All subsequent `orchid chat` commands use the stored token automatically
214
+
215
+ ### OIDC Discovery
216
+
217
+ When `issuer` is set in the config, the CLI fetches `{issuer}/.well-known/openid-configuration` to auto-discover `authorization_endpoint` and `token_endpoint`. This is the recommended approach -- you only need the issuer URL.
218
+
219
+ ### Token Refresh
220
+
221
+ When the access token expires and a refresh token is available, the CLI refreshes automatically before sending the request. If refresh fails, you'll be prompted to run `orchid auth login` again.
222
+
223
+ ### Identity Resolution
224
+
225
+ When `identity_resolver_class` is configured, the CLI calls the resolver after login to populate `tenant_key` and `user_id` from the OAuth token. These identity fields are cached in the token file so subsequent commands don't need the resolver. See the [orchid IdentityResolver ABC](../orchid/orchid_ai/core/identity.py) for the interface.
226
+
227
+ ### Dev Fallback
169
228
 
170
- Chat data is stored in SQLite at `~/.orchid/chats.db` by default. The directory is created automatically on first run.
229
+ When `auth.dev_bypass: true` or `auth.cli` is absent, the CLI uses a dummy token (`cli-token`, tenant=`cli`, user=`cli-user`). This is fully backward compatible -- existing configs without OAuth continue to work unchanged.
171
230
 
172
231
  ## Prerequisites
173
232
 
@@ -180,14 +239,20 @@ Chat data is stored in SQLite at `~/.orchid/chats.db` by default. The directory
180
239
  orchid_cli/
181
240
  main.py Typer entry point -- registers sub-commands
182
241
  bootstrap.py Shared startup: load config, build graph, init storage
242
+ auth/ OAuth 2.0 authentication (self-contained)
243
+ config.py Provider settings from orchid.yml + OIDC discovery
244
+ flow.py Authorization Code + PKCE flow (browser, localhost callback)
245
+ token_store.py Secure token persistence (~/.orchid/tokens.json)
246
+ middleware.py Token refresh + AuthContext builder
183
247
  commands/
248
+ auth.py login, logout, status subcommands
184
249
  chat.py Full CRUD + messaging + interactive mode
185
250
  config.py Validate agents.yaml
186
251
  index.py Seed RAG vector store
187
252
  skill.py Generate Claude Code skills from agents.yaml
188
253
  ```
189
254
 
190
- The CLI is a thin layer that calls `orchid` SDK functions and displays results via Rich.
255
+ The CLI is a thin layer that calls `orchid` SDK functions and displays results via Rich. The `auth/` subpackage is fully self-contained -- no OAuth logic leaks into chat commands or bootstrap.
191
256
 
192
257
  ## Development
193
258
 
@@ -0,0 +1,15 @@
1
+ """
2
+ OAuth authentication for orchid-cli.
3
+
4
+ Provides generic OAuth 2.0 Authorization Code + PKCE flow,
5
+ token storage, and automatic refresh. Works with any standard
6
+ OAuth 2.0 / OIDC provider.
7
+
8
+ Usage::
9
+
10
+ from orchid_cli.auth.middleware import get_auth_context
11
+
12
+ auth = await get_auth_context(config_path="orchid.yml")
13
+ """
14
+
15
+ from __future__ import annotations
@@ -0,0 +1,148 @@
1
+ """
2
+ OAuth provider configuration — parsed from the ``auth.cli`` section of orchid.yml.
3
+
4
+ Supports two modes:
5
+ 1. **OIDC discovery** — set ``issuer`` and endpoints are auto-discovered
6
+ via ``{issuer}/.well-known/openid-configuration``.
7
+ 2. **Explicit endpoints** — set ``authorization_endpoint`` and
8
+ ``token_endpoint`` directly.
9
+
10
+ When ``auth.dev_bypass: true`` or ``auth.cli`` is absent, the CLI falls
11
+ back to the legacy hardcoded dummy token (no OAuth).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import logging
17
+ from dataclasses import dataclass
18
+
19
+ import httpx
20
+ import yaml
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class OAuthProviderConfig:
27
+ """Resolved OAuth provider settings — ready to use."""
28
+
29
+ client_id: str
30
+ authorization_endpoint: str
31
+ token_endpoint: str
32
+ scopes: str = "openid"
33
+ # Optional OIDC issuer (informational after discovery).
34
+ issuer: str = ""
35
+ # Optional identity resolver class (for enriching AuthContext).
36
+ identity_resolver_class: str = ""
37
+ # Optional domain (passed to IdentityResolver.resolve).
38
+ domain: str = ""
39
+
40
+
41
+ def load_oauth_config(config_path: str) -> OAuthProviderConfig | None:
42
+ """Load OAuth config from the ``auth.cli`` section of orchid.yml.
43
+
44
+ Returns ``None`` when:
45
+ - ``auth.dev_bypass`` is truthy, OR
46
+ - ``auth.cli`` section is absent or incomplete.
47
+ """
48
+ if not config_path:
49
+ return None
50
+
51
+ try:
52
+ with open(config_path) as f:
53
+ data = yaml.safe_load(f) or {}
54
+ except FileNotFoundError:
55
+ logger.warning("[CLI Auth] Config file %s not found", config_path)
56
+ return None
57
+
58
+ auth_section = data.get("auth", {})
59
+ if not isinstance(auth_section, dict):
60
+ return None
61
+
62
+ # dev_bypass means no OAuth
63
+ if _is_truthy(auth_section.get("dev_bypass", False)):
64
+ return None
65
+
66
+ cli_section = auth_section.get("cli", {})
67
+ if not isinstance(cli_section, dict) or not cli_section:
68
+ return None
69
+
70
+ client_id = cli_section.get("client_id", "")
71
+ if not client_id:
72
+ logger.warning("[CLI Auth] auth.cli.client_id is required for OAuth")
73
+ return None
74
+
75
+ scopes = cli_section.get("scopes", "openid")
76
+ issuer = cli_section.get("issuer", "")
77
+ auth_endpoint = cli_section.get("authorization_endpoint", "")
78
+ token_endpoint = cli_section.get("token_endpoint", "")
79
+
80
+ # Carry forward top-level auth fields for identity resolution.
81
+ identity_resolver_class = auth_section.get("identity_resolver_class", "")
82
+ domain = auth_section.get("domain", "")
83
+
84
+ return OAuthProviderConfig(
85
+ client_id=client_id,
86
+ authorization_endpoint=auth_endpoint,
87
+ token_endpoint=token_endpoint,
88
+ scopes=scopes,
89
+ issuer=issuer,
90
+ identity_resolver_class=identity_resolver_class,
91
+ domain=domain,
92
+ )
93
+
94
+
95
+ async def discover_oidc_endpoints(config: OAuthProviderConfig) -> OAuthProviderConfig:
96
+ """Resolve endpoints via OIDC discovery if ``issuer`` is set but endpoints are missing.
97
+
98
+ Fetches ``{issuer}/.well-known/openid-configuration`` and extracts
99
+ ``authorization_endpoint`` and ``token_endpoint``.
100
+
101
+ If both endpoints are already set, returns ``config`` unchanged.
102
+ """
103
+ if config.authorization_endpoint and config.token_endpoint:
104
+ return config
105
+
106
+ if not config.issuer:
107
+ raise ValueError(
108
+ "OAuth config requires either 'issuer' (for OIDC discovery) "
109
+ "or explicit 'authorization_endpoint' + 'token_endpoint'."
110
+ )
111
+
112
+ discovery_url = f"{config.issuer.rstrip('/')}/.well-known/openid-configuration"
113
+ logger.info("[CLI Auth] Discovering OIDC endpoints from %s", discovery_url)
114
+
115
+ async with httpx.AsyncClient(timeout=15) as client:
116
+ resp = await client.get(discovery_url)
117
+ resp.raise_for_status()
118
+ metadata = resp.json()
119
+
120
+ auth_ep = config.authorization_endpoint or metadata.get("authorization_endpoint", "")
121
+ token_ep = config.token_endpoint or metadata.get("token_endpoint", "")
122
+
123
+ if not auth_ep or not token_ep:
124
+ raise ValueError(
125
+ f"OIDC discovery at {discovery_url} did not provide "
126
+ f"authorization_endpoint ({auth_ep!r}) or token_endpoint ({token_ep!r})."
127
+ )
128
+
129
+ logger.info("[CLI Auth] Discovered: authorization=%s, token=%s", auth_ep, token_ep)
130
+
131
+ return OAuthProviderConfig(
132
+ client_id=config.client_id,
133
+ authorization_endpoint=auth_ep,
134
+ token_endpoint=token_ep,
135
+ scopes=config.scopes,
136
+ issuer=config.issuer,
137
+ identity_resolver_class=config.identity_resolver_class,
138
+ domain=config.domain,
139
+ )
140
+
141
+
142
+ def _is_truthy(value: object) -> bool:
143
+ """Interpret YAML booleans and strings as truthy."""
144
+ if isinstance(value, bool):
145
+ return value
146
+ if isinstance(value, str):
147
+ return value.lower() in ("true", "1", "yes")
148
+ return bool(value)