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.
- {orchid_cli-1.0.2 → orchid_cli-1.0.4}/AGENTS.md +43 -0
- orchid_cli-1.0.4/CHANGELOG.md +58 -0
- {orchid_cli-1.0.2 → orchid_cli-1.0.4}/PKG-INFO +69 -4
- {orchid_cli-1.0.2 → orchid_cli-1.0.4}/README.md +67 -2
- orchid_cli-1.0.4/orchid_cli/auth/__init__.py +15 -0
- orchid_cli-1.0.4/orchid_cli/auth/config.py +148 -0
- orchid_cli-1.0.4/orchid_cli/auth/flow.py +228 -0
- orchid_cli-1.0.4/orchid_cli/auth/middleware.py +156 -0
- orchid_cli-1.0.4/orchid_cli/auth/token_store.py +103 -0
- {orchid_cli-1.0.2 → orchid_cli-1.0.4}/orchid_cli/bootstrap.py +6 -2
- orchid_cli-1.0.4/orchid_cli/commands/auth.py +175 -0
- {orchid_cli-1.0.2 → orchid_cli-1.0.4}/orchid_cli/commands/chat.py +34 -32
- {orchid_cli-1.0.2 → orchid_cli-1.0.4}/orchid_cli/commands/skill.py +39 -1
- {orchid_cli-1.0.2 → orchid_cli-1.0.4}/orchid_cli/main.py +2 -1
- {orchid_cli-1.0.2 → orchid_cli-1.0.4}/pyproject.toml +2 -2
- {orchid_cli-1.0.2 → orchid_cli-1.0.4}/tests/conftest.py +0 -1
- orchid_cli-1.0.4/tests/test_auth.py +349 -0
- {orchid_cli-1.0.2 → orchid_cli-1.0.4}/tests/test_commands_chat.py +14 -11
- orchid_cli-1.0.2/CHANGELOG.md +0 -27
- {orchid_cli-1.0.2 → orchid_cli-1.0.4}/.editorconfig +0 -0
- {orchid_cli-1.0.2 → orchid_cli-1.0.4}/.github/workflows/ci.yml +0 -0
- {orchid_cli-1.0.2 → orchid_cli-1.0.4}/.gitignore +0 -0
- {orchid_cli-1.0.2 → orchid_cli-1.0.4}/.gitlint +0 -0
- {orchid_cli-1.0.2 → orchid_cli-1.0.4}/.pre-commit-config.yaml +0 -0
- {orchid_cli-1.0.2 → orchid_cli-1.0.4}/CLAUDE.md +0 -0
- {orchid_cli-1.0.2 → orchid_cli-1.0.4}/CONTRIBUTING.md +0 -0
- {orchid_cli-1.0.2 → orchid_cli-1.0.4}/LICENSE +0 -0
- {orchid_cli-1.0.2 → orchid_cli-1.0.4}/icon.svg +0 -0
- {orchid_cli-1.0.2 → orchid_cli-1.0.4}/orchid_cli/__init__.py +0 -0
- {orchid_cli-1.0.2 → orchid_cli-1.0.4}/orchid_cli/commands/__init__.py +0 -0
- {orchid_cli-1.0.2 → orchid_cli-1.0.4}/orchid_cli/commands/config.py +0 -0
- {orchid_cli-1.0.2 → orchid_cli-1.0.4}/orchid_cli/commands/index.py +0 -0
- {orchid_cli-1.0.2 → orchid_cli-1.0.4}/tests/fixtures/__init__.py +0 -0
- {orchid_cli-1.0.2 → orchid_cli-1.0.4}/tests/fixtures/fake_tools_dates.py +0 -0
- {orchid_cli-1.0.2 → orchid_cli-1.0.4}/tests/fixtures/fake_tools_math.py +0 -0
- {orchid_cli-1.0.2 → orchid_cli-1.0.4}/tests/test_bootstrap.py +0 -0
- {orchid_cli-1.0.2 → orchid_cli-1.0.4}/tests/test_commands_config.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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)
|