agentshim 0.15.7__tar.gz → 0.16.0__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.
- {agentshim-0.15.7 → agentshim-0.16.0}/CHANGELOG.md +20 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/PKG-INFO +1 -1
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/__init__.py +21 -1
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/core/__init__.py +22 -0
- agentshim-0.16.0/agentshim/core/auth.py +335 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/core/profile.py +11 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/core/status.py +6 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/probe.py +1 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/claude/probe.py +21 -2
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/claude/provider.py +17 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/codex/probe.py +10 -1
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/codex/provider.py +22 -1
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/copilot/provider.py +8 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/gemini/provider.py +6 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/pyproject.toml +1 -1
- {agentshim-0.15.7 → agentshim-0.16.0}/.gitignore +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/LICENSE +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/README.md +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/agent.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/core/_files.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/core/checkpoints.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/core/clock.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/core/conversation.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/core/env.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/core/errors.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/core/events.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/core/home.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/core/ids.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/core/mcp.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/core/permissions.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/core/pricing.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/core/provider.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/core/schema.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/core/session_policy.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/core/skills.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/core/stream.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/core/turn.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/core/usage.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/execution/__init__.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/execution/confinement.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/execution/docker.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/execution/executor.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/execution/host.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/execution/process.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/execution/transform.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/oneshot.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/__init__.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/claude/__init__.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/claude/events.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/claude/failures.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/claude/hooks/__init__.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/claude/hooks/confine_reads.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/claude/parser.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/claude/rate_limits.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/claude/sandbox.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/claude/scripted.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/claude/stream_argv.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/claude/stream_transport.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/claude/user_hooks.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/codex/__init__.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/codex/_toml.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/codex/app_server/__init__.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/codex/app_server/_wire.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/codex/app_server/approvals.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/codex/app_server/channel.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/codex/app_server/errors.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/codex/app_server/items.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/codex/app_server/mcp.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/codex/app_server/permissions.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/codex/app_server/profile.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/codex/app_server/protocol.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/codex/app_server/rate_limits.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/codex/app_server/transport.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/codex/app_server/usage.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/codex/events.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/codex/failures.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/codex/mcp_scope.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/codex/parser.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/codex/rules.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/codex/sandbox.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/codex/scripted.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/codex/skills.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/copilot/__init__.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/copilot/events.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/copilot/parser.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/copilot/probe.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/copilot/scripted.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/gemini/__init__.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/gemini/events.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/gemini/parser.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/gemini/probe.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/gemini/scripted.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/opencode/__init__.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/opencode/events.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/opencode/parser.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/opencode/probe.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/opencode/provider.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/providers/opencode/scripted.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/py.typed +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/runtime.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/session.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/testing/__init__.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/testing/claude_stream.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/testing/clock.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/testing/codex_app_server.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/testing/confinement.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/testing/contracts.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/testing/conversation.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/testing/probe.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/agentshim/testing/process.py +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/tests/fixtures/claude_stream/README.md +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/tests/fixtures/codex/README.md +0 -0
- {agentshim-0.15.7 → agentshim-0.16.0}/tests/fixtures/codex_app_server/README.md +0 -0
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.0 (2026-10-14)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `plan_auth(profile, AuthPolicy, env=, home=)` chooses the one credential a CLI logs in
|
|
8
|
+
with and returns an `AuthPlan`: the exact variables and files to hand it, and the names
|
|
9
|
+
of the credentials it withheld. `AuthPolicy` names the allowed `CredentialKind`s
|
|
10
|
+
(`API_KEY`, `SUBSCRIPTION`) and an optional endpoint; with no endpoint, a base URL in
|
|
11
|
+
the environment is withheld. `AuthUnavailableError` names what is missing.
|
|
12
|
+
- `ProviderProfile.credential_sources` (`EnvCredential`, `FileCredential`, in the CLI's
|
|
13
|
+
precedence order) and `ProviderProfile.endpoint` (`EndpointSpec`) declare each CLI's
|
|
14
|
+
credentials as data. opencode declares none, so no policy can be enforced for it.
|
|
15
|
+
- `ProviderStatus.credential`: the kind of credential the CLI reports it is using (Claude
|
|
16
|
+
from `claude auth status`'s `authMethod`, Codex from `codex login status`), or `None`.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- `auth_env_vars` lists `CLAUDE_CODE_OAUTH_TOKEN` (claude) and `CODEX_API_KEY` (codex),
|
|
21
|
+
which the CLIs and their readiness probes already honoured.
|
|
22
|
+
|
|
3
23
|
## 0.15.7 (2026-10-09)
|
|
4
24
|
|
|
5
25
|
### Added
|
|
@@ -15,7 +15,10 @@ from .core import (
|
|
|
15
15
|
ApprovalPolicy,
|
|
16
16
|
ArgvContext,
|
|
17
17
|
AssistantText,
|
|
18
|
+
AuthPlan,
|
|
19
|
+
AuthPolicy,
|
|
18
20
|
AuthState,
|
|
21
|
+
AuthUnavailableError,
|
|
19
22
|
Checkpoint,
|
|
20
23
|
CheckpointStore,
|
|
21
24
|
CliCheckError,
|
|
@@ -32,8 +35,13 @@ from .core import (
|
|
|
32
35
|
ContinuityError,
|
|
33
36
|
Conversation,
|
|
34
37
|
ConversationSpec,
|
|
38
|
+
CredentialKind,
|
|
39
|
+
CredentialSource,
|
|
40
|
+
EndpointSpec,
|
|
41
|
+
EnvCredential,
|
|
35
42
|
EventHandlerBase,
|
|
36
43
|
FailureKind,
|
|
44
|
+
FileCredential,
|
|
37
45
|
FlagsInstallation,
|
|
38
46
|
HttpMcpServer,
|
|
39
47
|
IdAllocator,
|
|
@@ -86,6 +94,7 @@ from .core import (
|
|
|
86
94
|
SkillSignal,
|
|
87
95
|
SkillSummary,
|
|
88
96
|
SkillTracker,
|
|
97
|
+
StagedCredentialFile,
|
|
89
98
|
Stderr,
|
|
90
99
|
StdioMcpServer,
|
|
91
100
|
SteerableConversation,
|
|
@@ -120,6 +129,7 @@ from .core import (
|
|
|
120
129
|
normalize,
|
|
121
130
|
normalized_usage,
|
|
122
131
|
parse_json_object,
|
|
132
|
+
plan_auth,
|
|
123
133
|
prepare_config_home,
|
|
124
134
|
price_for,
|
|
125
135
|
)
|
|
@@ -156,7 +166,7 @@ from .providers.opencode import OpencodeProvider
|
|
|
156
166
|
from .runtime import Agent
|
|
157
167
|
from .session import Session, Turn, TurnTicket, map_mcp_servers
|
|
158
168
|
|
|
159
|
-
__version__ = "0.
|
|
169
|
+
__version__ = "0.16.0"
|
|
160
170
|
|
|
161
171
|
__all__ = [
|
|
162
172
|
"Agent",
|
|
@@ -168,7 +178,10 @@ __all__ = [
|
|
|
168
178
|
"ApprovalPolicy",
|
|
169
179
|
"ArgvContext",
|
|
170
180
|
"AssistantText",
|
|
181
|
+
"AuthPlan",
|
|
182
|
+
"AuthPolicy",
|
|
171
183
|
"AuthState",
|
|
184
|
+
"AuthUnavailableError",
|
|
172
185
|
"CallbackCommandStreamSink",
|
|
173
186
|
"Checkpoint",
|
|
174
187
|
"CheckpointStore",
|
|
@@ -200,9 +213,14 @@ __all__ = [
|
|
|
200
213
|
"Conversation",
|
|
201
214
|
"ConversationSpec",
|
|
202
215
|
"CopilotProvider",
|
|
216
|
+
"CredentialKind",
|
|
217
|
+
"CredentialSource",
|
|
203
218
|
"DockerExecConfinement",
|
|
219
|
+
"EndpointSpec",
|
|
220
|
+
"EnvCredential",
|
|
204
221
|
"EventHandlerBase",
|
|
205
222
|
"FailureKind",
|
|
223
|
+
"FileCredential",
|
|
206
224
|
"FlagsInstallation",
|
|
207
225
|
"GeminiProvider",
|
|
208
226
|
"HostCommandExecutor",
|
|
@@ -267,6 +285,7 @@ __all__ = [
|
|
|
267
285
|
"SkillTracker",
|
|
268
286
|
"SkillsDiscovered",
|
|
269
287
|
"SpawnRequest",
|
|
288
|
+
"StagedCredentialFile",
|
|
270
289
|
"Stderr",
|
|
271
290
|
"StderrLine",
|
|
272
291
|
"StdioMcpServer",
|
|
@@ -310,6 +329,7 @@ __all__ = [
|
|
|
310
329
|
"normalize",
|
|
311
330
|
"normalized_usage",
|
|
312
331
|
"parse_json_object",
|
|
332
|
+
"plan_auth",
|
|
313
333
|
"prepare_config_home",
|
|
314
334
|
"price_for",
|
|
315
335
|
"probe_provider",
|
|
@@ -7,6 +7,18 @@ this package may not do.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
from .auth import (
|
|
11
|
+
AuthPlan,
|
|
12
|
+
AuthPolicy,
|
|
13
|
+
AuthUnavailableError,
|
|
14
|
+
CredentialKind,
|
|
15
|
+
CredentialSource,
|
|
16
|
+
EndpointSpec,
|
|
17
|
+
EnvCredential,
|
|
18
|
+
FileCredential,
|
|
19
|
+
StagedCredentialFile,
|
|
20
|
+
plan_auth,
|
|
21
|
+
)
|
|
10
22
|
from .checkpoints import Checkpoint, CheckpointStore, InMemoryCheckpointStore
|
|
11
23
|
from .clock import Clock, StopSignal, SystemClock
|
|
12
24
|
from .conversation import (
|
|
@@ -116,7 +128,10 @@ __all__ = [
|
|
|
116
128
|
"ApprovalPolicy",
|
|
117
129
|
"ArgvContext",
|
|
118
130
|
"AssistantText",
|
|
131
|
+
"AuthPlan",
|
|
132
|
+
"AuthPolicy",
|
|
119
133
|
"AuthState",
|
|
134
|
+
"AuthUnavailableError",
|
|
120
135
|
"Checkpoint",
|
|
121
136
|
"CheckpointStore",
|
|
122
137
|
"CliCheckError",
|
|
@@ -133,8 +148,13 @@ __all__ = [
|
|
|
133
148
|
"ContinuityError",
|
|
134
149
|
"Conversation",
|
|
135
150
|
"ConversationSpec",
|
|
151
|
+
"CredentialKind",
|
|
152
|
+
"CredentialSource",
|
|
153
|
+
"EndpointSpec",
|
|
154
|
+
"EnvCredential",
|
|
136
155
|
"EventHandlerBase",
|
|
137
156
|
"FailureKind",
|
|
157
|
+
"FileCredential",
|
|
138
158
|
"FlagsInstallation",
|
|
139
159
|
"HttpMcpServer",
|
|
140
160
|
"IdAllocator",
|
|
@@ -187,6 +207,7 @@ __all__ = [
|
|
|
187
207
|
"SkillSummary",
|
|
188
208
|
"SkillTracker",
|
|
189
209
|
"SkillsDiscovered",
|
|
210
|
+
"StagedCredentialFile",
|
|
190
211
|
"Stderr",
|
|
191
212
|
"StdioMcpServer",
|
|
192
213
|
"SteerConsumed",
|
|
@@ -221,6 +242,7 @@ __all__ = [
|
|
|
221
242
|
"normalize",
|
|
222
243
|
"normalized_usage",
|
|
223
244
|
"parse_json_object",
|
|
245
|
+
"plan_auth",
|
|
224
246
|
"prepare_config_home",
|
|
225
247
|
"price_for",
|
|
226
248
|
]
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""Credential policy: which credential a CLI may log in with, and what to hand it.
|
|
2
|
+
|
|
3
|
+
A CLI can log in several ways, and it picks one by its own precedence rules
|
|
4
|
+
when more than one is present: a pay-per-token key in the environment, a
|
|
5
|
+
subscription login in a file under its state root, a token that a proxy
|
|
6
|
+
issued. A caller that must control which one is used (bill an API key, never
|
|
7
|
+
the operator's subscription) cannot do it by setting one variable: whatever
|
|
8
|
+
else the CLI can see may win. It has to hand the CLI exactly one credential
|
|
9
|
+
and nothing else.
|
|
10
|
+
|
|
11
|
+
Each provider declares its credentials as data (``ProviderProfile.
|
|
12
|
+
credential_sources``, in the CLI's own precedence order, and
|
|
13
|
+
``ProviderProfile.endpoint``). ``plan_auth`` turns that, a caller's
|
|
14
|
+
``AuthPolicy`` and the host's environment and home into an ``AuthPlan``: the
|
|
15
|
+
exact environment variables and files to deliver. The caller delivers the
|
|
16
|
+
plan and nothing more of the provider's ``auth_env_vars`` or ``auth_files``;
|
|
17
|
+
the readiness probe then reports which kind of credential the CLI says it is
|
|
18
|
+
using (``ProviderStatus.credential``), so the caller can confirm it.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from enum import Enum
|
|
26
|
+
from types import MappingProxyType
|
|
27
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
28
|
+
from urllib.parse import urlsplit
|
|
29
|
+
|
|
30
|
+
from .errors import AgentShimError
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from collections.abc import Callable, Mapping
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
from .profile import ProviderProfile
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CredentialKind(str, Enum):
|
|
40
|
+
"""What a credential bills and how it lives.
|
|
41
|
+
|
|
42
|
+
``API_KEY`` is a static secret read from the environment or a key file: a
|
|
43
|
+
vendor API key, or a token a proxy or gateway issued. ``SUBSCRIPTION`` is a
|
|
44
|
+
personal or team plan login (Claude.ai, ChatGPT, a Google or GitHub
|
|
45
|
+
account), usually an OAuth token the CLI refreshes.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
API_KEY = "api_key"
|
|
49
|
+
SUBSCRIPTION = "subscription"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class EnvCredential:
|
|
54
|
+
"""A credential the CLI reads from one environment variable."""
|
|
55
|
+
|
|
56
|
+
name: str
|
|
57
|
+
kind: CredentialKind
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class FileCredential:
|
|
62
|
+
"""A credential the CLI reads from a home-relative file.
|
|
63
|
+
|
|
64
|
+
The file's kind is either fixed (``kind``) or read from its contents
|
|
65
|
+
(``classify``, which returns ``None`` for contents it does not recognise);
|
|
66
|
+
exactly one is set. The path is also one of the profile's
|
|
67
|
+
``credential_files``.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
path: str
|
|
71
|
+
kind: CredentialKind | None = None
|
|
72
|
+
classify: Callable[[bytes], CredentialKind | None] | None = field(default=None, compare=False)
|
|
73
|
+
|
|
74
|
+
def __post_init__(self) -> None:
|
|
75
|
+
"""Require exactly one of ``kind`` and ``classify``."""
|
|
76
|
+
if (self.kind is None) == (self.classify is None):
|
|
77
|
+
msg = f"FileCredential {self.path!r} needs exactly one of kind and classify"
|
|
78
|
+
raise ValueError(msg)
|
|
79
|
+
|
|
80
|
+
def kind_of(self, contents: bytes) -> CredentialKind | None:
|
|
81
|
+
"""The kind of credential *contents* hold, or ``None`` if unrecognised."""
|
|
82
|
+
if self.kind is not None:
|
|
83
|
+
return self.kind
|
|
84
|
+
assert self.classify is not None # noqa: S101 - __post_init__ guarantees it
|
|
85
|
+
return self.classify(contents)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
CredentialSource = EnvCredential | FileCredential
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class EndpointSpec:
|
|
93
|
+
"""How a CLI is pointed at an API endpoint other than its vendor's.
|
|
94
|
+
|
|
95
|
+
``base_url_env`` takes the endpoint URL. ``companion_env`` are variables
|
|
96
|
+
that only make sense with a custom endpoint (extra headers a gateway
|
|
97
|
+
needs); they are forwarded from the host only when the policy names an
|
|
98
|
+
endpoint. ``credentials`` are the kinds the CLI sends to a custom endpoint.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
base_url_env: str
|
|
102
|
+
companion_env: tuple[str, ...] = ()
|
|
103
|
+
credentials: frozenset[CredentialKind] = frozenset({CredentialKind.API_KEY})
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass(frozen=True)
|
|
107
|
+
class AuthPolicy:
|
|
108
|
+
"""Which credentials a caller allows, and the endpoint to use.
|
|
109
|
+
|
|
110
|
+
``endpoint`` is ``None`` for the vendor's own endpoint: a base URL left in
|
|
111
|
+
the host environment is then withheld, not forwarded. A URL sends every
|
|
112
|
+
request there.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
credentials: frozenset[CredentialKind]
|
|
116
|
+
endpoint: str | None = None
|
|
117
|
+
|
|
118
|
+
def __post_init__(self) -> None:
|
|
119
|
+
"""Reject an empty credential set and an endpoint that is not an http(s) URL."""
|
|
120
|
+
if not self.credentials:
|
|
121
|
+
msg = "AuthPolicy.credentials must allow at least one CredentialKind"
|
|
122
|
+
raise ValueError(msg)
|
|
123
|
+
if self.endpoint is not None:
|
|
124
|
+
parts = urlsplit(self.endpoint)
|
|
125
|
+
if parts.scheme not in {"http", "https"} or not parts.hostname:
|
|
126
|
+
msg = (
|
|
127
|
+
f"AuthPolicy.endpoint must be an http(s) URL with a host, got {self.endpoint!r}"
|
|
128
|
+
)
|
|
129
|
+
raise ValueError(msg)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass(frozen=True)
|
|
133
|
+
class StagedCredentialFile:
|
|
134
|
+
"""A host credential file the CLI must see at ``home_path`` under its home.
|
|
135
|
+
|
|
136
|
+
``refreshable`` files hold a login the CLI rewrites when it refreshes the
|
|
137
|
+
token; share those writable so the refreshed token reaches the host.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
host_path: Path
|
|
141
|
+
home_path: str
|
|
142
|
+
refreshable: bool
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass(frozen=True)
|
|
146
|
+
class AuthPlan:
|
|
147
|
+
"""Exactly what to hand a CLI so it logs in with one allowed credential.
|
|
148
|
+
|
|
149
|
+
``env`` and ``files`` are the whole delivery: a caller that also forwards
|
|
150
|
+
any other of the profile's ``auth_env_vars`` or ``auth_files`` (settings
|
|
151
|
+
files included, which can carry credentials of their own) defeats the
|
|
152
|
+
plan. ``source`` names the chosen credential (a variable name or a
|
|
153
|
+
home-relative path); ``withheld`` names, never values, the credentials and
|
|
154
|
+
endpoint settings present on the host that the plan leaves out, for a log.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
provider: str
|
|
158
|
+
credential: CredentialKind
|
|
159
|
+
source: str
|
|
160
|
+
endpoint: str | None
|
|
161
|
+
env: Mapping[str, str]
|
|
162
|
+
files: tuple[StagedCredentialFile, ...]
|
|
163
|
+
withheld: tuple[str, ...]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class AuthUnavailableError(AgentShimError):
|
|
167
|
+
"""No credential the policy allows is present, or the policy cannot be met.
|
|
168
|
+
|
|
169
|
+
The message names what each allowed kind would need, and never a value.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def plan_auth(
|
|
174
|
+
profile: ProviderProfile,
|
|
175
|
+
policy: AuthPolicy,
|
|
176
|
+
*,
|
|
177
|
+
env: Mapping[str, str],
|
|
178
|
+
home: Path,
|
|
179
|
+
) -> AuthPlan:
|
|
180
|
+
"""Choose the credential *profile*'s CLI will log in with under *policy*.
|
|
181
|
+
|
|
182
|
+
Walks the profile's ``credential_sources`` in the CLI's precedence order
|
|
183
|
+
and takes the first one present on the host (a non-blank variable in
|
|
184
|
+
*env*, or an existing file under *home* whose contents classify) whose kind
|
|
185
|
+
the policy allows. Only that credential is delivered, so the CLI cannot
|
|
186
|
+
prefer another.
|
|
187
|
+
|
|
188
|
+
Raises:
|
|
189
|
+
AuthUnavailableError: the profile declares no credential sources, the
|
|
190
|
+
policy names an endpoint the CLI cannot take or that accepts none
|
|
191
|
+
of the allowed kinds, or no allowed credential is present.
|
|
192
|
+
"""
|
|
193
|
+
allowed = _allowed_kinds(profile, policy)
|
|
194
|
+
chosen, kind, present = _choose(profile, allowed, env, home)
|
|
195
|
+
delivered_env: dict[str, str] = {}
|
|
196
|
+
files: tuple[StagedCredentialFile, ...] = ()
|
|
197
|
+
if isinstance(chosen, EnvCredential):
|
|
198
|
+
delivered_env[chosen.name] = env[chosen.name]
|
|
199
|
+
else:
|
|
200
|
+
refreshable = kind is CredentialKind.SUBSCRIPTION
|
|
201
|
+
files = (StagedCredentialFile(home / chosen.path, chosen.path, refreshable),)
|
|
202
|
+
if policy.endpoint is not None and profile.endpoint is not None:
|
|
203
|
+
delivered_env[profile.endpoint.base_url_env] = policy.endpoint
|
|
204
|
+
delivered_env.update(
|
|
205
|
+
{name: env[name] for name in profile.endpoint.companion_env if _set(env, name)}
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
delivered = set(delivered_env) | {staged.home_path for staged in files}
|
|
209
|
+
on_host = {name for name in _host_auth_names(profile) if _set(env, name)}
|
|
210
|
+
on_host |= {path for path in profile.auth_files if (home / path).is_file()}
|
|
211
|
+
on_host |= set(present)
|
|
212
|
+
return AuthPlan(
|
|
213
|
+
provider=profile.name,
|
|
214
|
+
credential=kind,
|
|
215
|
+
source=_name(chosen),
|
|
216
|
+
endpoint=policy.endpoint,
|
|
217
|
+
env=MappingProxyType(delivered_env),
|
|
218
|
+
files=files,
|
|
219
|
+
withheld=tuple(sorted(on_host - delivered)),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def classify_json_field(
|
|
224
|
+
key: str, kinds: Mapping[str, CredentialKind]
|
|
225
|
+
) -> Callable[[bytes], CredentialKind | None]:
|
|
226
|
+
"""A ``FileCredential.classify`` that maps a top-level JSON string field to a kind.
|
|
227
|
+
|
|
228
|
+
Contents that are not a JSON object, lack *key*, or hold a value not in
|
|
229
|
+
*kinds* classify as ``None``.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
def classify(contents: bytes) -> CredentialKind | None:
|
|
233
|
+
try:
|
|
234
|
+
data: Any = json.loads(contents)
|
|
235
|
+
except ValueError:
|
|
236
|
+
return None
|
|
237
|
+
if not isinstance(data, dict):
|
|
238
|
+
return None
|
|
239
|
+
value = cast("dict[str, Any]", data).get(key)
|
|
240
|
+
return kinds.get(value) if isinstance(value, str) else None
|
|
241
|
+
|
|
242
|
+
return classify
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _allowed_kinds(profile: ProviderProfile, policy: AuthPolicy) -> frozenset[CredentialKind]:
|
|
246
|
+
"""The kinds *policy* allows that *profile*'s CLI can use with the policy's endpoint."""
|
|
247
|
+
if not profile.credential_sources:
|
|
248
|
+
msg = (
|
|
249
|
+
f"{profile.name} declares no credential sources agentshim can classify, "
|
|
250
|
+
"so no credential policy can be enforced for it"
|
|
251
|
+
)
|
|
252
|
+
raise AuthUnavailableError(msg)
|
|
253
|
+
if policy.endpoint is None:
|
|
254
|
+
return policy.credentials
|
|
255
|
+
if profile.endpoint is None:
|
|
256
|
+
msg = f"{profile.name} cannot be pointed at another endpoint ({policy.endpoint})"
|
|
257
|
+
raise AuthUnavailableError(msg)
|
|
258
|
+
allowed = policy.credentials & profile.endpoint.credentials
|
|
259
|
+
if not allowed:
|
|
260
|
+
msg = (
|
|
261
|
+
f"{profile.name} sends only {_kinds(profile.endpoint.credentials)} credentials "
|
|
262
|
+
f"to a custom endpoint, and the policy allows {_kinds(policy.credentials)}"
|
|
263
|
+
)
|
|
264
|
+
raise AuthUnavailableError(msg)
|
|
265
|
+
return allowed
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _choose(
|
|
269
|
+
profile: ProviderProfile,
|
|
270
|
+
allowed: frozenset[CredentialKind],
|
|
271
|
+
env: Mapping[str, str],
|
|
272
|
+
home: Path,
|
|
273
|
+
) -> tuple[CredentialSource, CredentialKind, list[str]]:
|
|
274
|
+
"""The first present allowed source, its kind, and every present source's name."""
|
|
275
|
+
chosen: tuple[CredentialSource, CredentialKind] | None = None
|
|
276
|
+
present: list[str] = []
|
|
277
|
+
for source in profile.credential_sources:
|
|
278
|
+
kind = _present_kind(source, env, home)
|
|
279
|
+
if kind is None:
|
|
280
|
+
continue
|
|
281
|
+
present.append(_name(source))
|
|
282
|
+
if chosen is None and kind in allowed:
|
|
283
|
+
chosen = (source, kind)
|
|
284
|
+
if chosen is None:
|
|
285
|
+
raise AuthUnavailableError(_unavailable(profile, allowed, present))
|
|
286
|
+
return chosen[0], chosen[1], present
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _present_kind(
|
|
290
|
+
source: CredentialSource, env: Mapping[str, str], home: Path
|
|
291
|
+
) -> CredentialKind | None:
|
|
292
|
+
if isinstance(source, EnvCredential):
|
|
293
|
+
return source.kind if _set(env, source.name) else None
|
|
294
|
+
path = home / source.path
|
|
295
|
+
try:
|
|
296
|
+
contents = path.read_bytes()
|
|
297
|
+
except OSError:
|
|
298
|
+
return None
|
|
299
|
+
return source.kind_of(contents)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _set(env: Mapping[str, str], name: str) -> bool:
|
|
303
|
+
value = env.get(name)
|
|
304
|
+
return bool(value and value.strip())
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _name(source: CredentialSource) -> str:
|
|
308
|
+
return source.name if isinstance(source, EnvCredential) else source.path
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _host_auth_names(profile: ProviderProfile) -> set[str]:
|
|
312
|
+
names = set(profile.auth_env_vars)
|
|
313
|
+
if profile.endpoint is not None:
|
|
314
|
+
names.add(profile.endpoint.base_url_env)
|
|
315
|
+
names.update(profile.endpoint.companion_env)
|
|
316
|
+
return names
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _kinds(kinds: frozenset[CredentialKind]) -> str:
|
|
320
|
+
return ", ".join(sorted(kind.value for kind in kinds))
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _unavailable(
|
|
324
|
+
profile: ProviderProfile, allowed: frozenset[CredentialKind], present: list[str]
|
|
325
|
+
) -> str:
|
|
326
|
+
wanted = [
|
|
327
|
+
_name(source)
|
|
328
|
+
for source in profile.credential_sources
|
|
329
|
+
if source.kind is None or source.kind in allowed
|
|
330
|
+
]
|
|
331
|
+
found = f"; present but not allowed: {', '.join(present)}" if present else ""
|
|
332
|
+
return (
|
|
333
|
+
f"no {_kinds(allowed)} credential for {profile.name} is available: "
|
|
334
|
+
f"provide one of {', '.join(wanted)}{found}"
|
|
335
|
+
)
|
|
@@ -16,6 +16,8 @@ from .permissions import NativeMode
|
|
|
16
16
|
if TYPE_CHECKING:
|
|
17
17
|
from collections.abc import Mapping
|
|
18
18
|
|
|
19
|
+
from .auth import CredentialSource, EndpointSpec
|
|
20
|
+
|
|
19
21
|
|
|
20
22
|
class McpMechanism(Enum):
|
|
21
23
|
"""How a provider is told about MCP servers."""
|
|
@@ -220,3 +222,12 @@ class ProviderProfile:
|
|
|
220
222
|
#: turn runs. A provider that does not (every one-shot transport) rejects
|
|
221
223
|
#: it with ``ProviderCapabilityError`` instead of delaying the message.
|
|
222
224
|
supports_steer: bool = False
|
|
225
|
+
#: The credentials the CLI can log in with, in the CLI's own precedence
|
|
226
|
+
#: order (0.16.0+). ``plan_auth`` walks them to choose the one credential
|
|
227
|
+
#: a caller hands the CLI. Every variable is in ``auth_env_vars`` and every
|
|
228
|
+
#: file in ``credential_files``. Empty means the CLI's credentials cannot
|
|
229
|
+
#: be classified, so no credential policy can be enforced for it.
|
|
230
|
+
credential_sources: tuple[CredentialSource, ...] = ()
|
|
231
|
+
#: How the CLI is pointed at another API endpoint, or ``None`` if it
|
|
232
|
+
#: cannot be (0.16.0+).
|
|
233
|
+
endpoint: EndpointSpec | None = None
|
|
@@ -16,6 +16,8 @@ from typing import TYPE_CHECKING
|
|
|
16
16
|
if TYPE_CHECKING:
|
|
17
17
|
from collections.abc import Callable, Mapping
|
|
18
18
|
|
|
19
|
+
from .auth import CredentialKind
|
|
20
|
+
|
|
19
21
|
|
|
20
22
|
class AuthState(str, Enum):
|
|
21
23
|
"""Whether the provider CLI is authenticated, as far as its own tooling can say.
|
|
@@ -45,6 +47,9 @@ class ProviderStatus:
|
|
|
45
47
|
version: str | None
|
|
46
48
|
auth: AuthState
|
|
47
49
|
auth_detail: str
|
|
50
|
+
#: The kind of credential the CLI says it is logged in with (0.16.0+), or
|
|
51
|
+
#: ``None`` when it did not say or could not be read unambiguously.
|
|
52
|
+
credential: CredentialKind | None = None
|
|
48
53
|
|
|
49
54
|
|
|
50
55
|
@dataclass(frozen=True)
|
|
@@ -67,6 +72,7 @@ class AuthCheck:
|
|
|
67
72
|
|
|
68
73
|
state: AuthState
|
|
69
74
|
detail: str
|
|
75
|
+
credential: CredentialKind | None = None
|
|
70
76
|
|
|
71
77
|
|
|
72
78
|
@dataclass(frozen=True)
|
|
@@ -4,6 +4,12 @@ The command prints a JSON object (``loggedIn``, ``authMethod``, ...) and exits
|
|
|
4
4
|
1 when logged out. It reads local credentials and the environment
|
|
5
5
|
(``ANTHROPIC_API_KEY``, ``CLAUDE_CODE_OAUTH_TOKEN``) only; it makes no model
|
|
6
6
|
request.
|
|
7
|
+
|
|
8
|
+
``authMethod`` names the credential in use (Claude Code 2.1): ``claude.ai``
|
|
9
|
+
for the stored subscription login, ``api_key`` for ``ANTHROPIC_API_KEY``, and
|
|
10
|
+
``oauth_token`` for both ``ANTHROPIC_AUTH_TOKEN`` (a static token, often a
|
|
11
|
+
gateway's) and ``CLAUDE_CODE_OAUTH_TOKEN`` (a subscription token). Only the
|
|
12
|
+
environment the command ran with tells those last two apart.
|
|
7
13
|
"""
|
|
8
14
|
|
|
9
15
|
from __future__ import annotations
|
|
@@ -11,6 +17,7 @@ from __future__ import annotations
|
|
|
11
17
|
import json
|
|
12
18
|
from typing import TYPE_CHECKING, Any, cast
|
|
13
19
|
|
|
20
|
+
from agentshim.core.auth import CredentialKind
|
|
14
21
|
from agentshim.core.status import AuthCheck, AuthProbe, AuthState, ProbeOutput, ProbeSpec
|
|
15
22
|
|
|
16
23
|
if TYPE_CHECKING:
|
|
@@ -21,7 +28,6 @@ LOGIN_HINT = "run `claude auth login` or set ANTHROPIC_API_KEY"
|
|
|
21
28
|
|
|
22
29
|
def read_auth(output: ProbeOutput, env: Mapping[str, str]) -> AuthCheck:
|
|
23
30
|
"""Read ``claude auth status``'s JSON."""
|
|
24
|
-
del env
|
|
25
31
|
try:
|
|
26
32
|
data: Any = json.loads(output.stdout)
|
|
27
33
|
except ValueError:
|
|
@@ -33,12 +39,25 @@ def read_auth(output: ProbeOutput, env: Mapping[str, str]) -> AuthCheck:
|
|
|
33
39
|
if logged_in is True:
|
|
34
40
|
method = status.get("authMethod")
|
|
35
41
|
how = f" ({method})" if isinstance(method, str) and method != "none" else ""
|
|
36
|
-
return AuthCheck(AuthState.KNOWN_OK, f"logged in{how}")
|
|
42
|
+
return AuthCheck(AuthState.KNOWN_OK, f"logged in{how}", _credential(method, env))
|
|
37
43
|
if logged_in is False:
|
|
38
44
|
return AuthCheck(AuthState.FAILED, f"claude is not logged in; {LOGIN_HINT}")
|
|
39
45
|
return AuthCheck(AuthState.UNKNOWN, _unrecognised(output))
|
|
40
46
|
|
|
41
47
|
|
|
48
|
+
def _credential(method: object, env: Mapping[str, str]) -> CredentialKind | None:
|
|
49
|
+
if method == "claude.ai":
|
|
50
|
+
return CredentialKind.SUBSCRIPTION
|
|
51
|
+
if method == "api_key":
|
|
52
|
+
return CredentialKind.API_KEY
|
|
53
|
+
if method == "oauth_token":
|
|
54
|
+
bearer = bool(env.get("ANTHROPIC_AUTH_TOKEN", "").strip())
|
|
55
|
+
subscription = bool(env.get("CLAUDE_CODE_OAUTH_TOKEN", "").strip())
|
|
56
|
+
if bearer != subscription:
|
|
57
|
+
return CredentialKind.API_KEY if bearer else CredentialKind.SUBSCRIPTION
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
42
61
|
def _unrecognised(output: ProbeOutput) -> str:
|
|
43
62
|
return f"unrecognised `claude auth status` output (exit {output.returncode})"
|
|
44
63
|
|
|
@@ -5,6 +5,12 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
from typing import TYPE_CHECKING, Any
|
|
7
7
|
|
|
8
|
+
from agentshim.core.auth import (
|
|
9
|
+
CredentialKind,
|
|
10
|
+
EndpointSpec,
|
|
11
|
+
EnvCredential,
|
|
12
|
+
FileCredential,
|
|
13
|
+
)
|
|
8
14
|
from agentshim.core.errors import FailureKind, ProviderCapabilityError, SessionResumeError
|
|
9
15
|
from agentshim.core.mcp import HttpMcpServer, NoopInstallation, StdioMcpServer, install_config_file
|
|
10
16
|
from agentshim.core.profile import (
|
|
@@ -75,6 +81,7 @@ PROFILE = ProviderProfile(
|
|
|
75
81
|
auth_env_vars=(
|
|
76
82
|
"ANTHROPIC_AUTH_TOKEN",
|
|
77
83
|
"ANTHROPIC_API_KEY",
|
|
84
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
78
85
|
"ANTHROPIC_BASE_URL",
|
|
79
86
|
"ANTHROPIC_CUSTOM_HEADERS",
|
|
80
87
|
),
|
|
@@ -103,6 +110,16 @@ PROFILE = ProviderProfile(
|
|
|
103
110
|
".claude.json",
|
|
104
111
|
),
|
|
105
112
|
credential_files=(".claude/.credentials.json",),
|
|
113
|
+
# Precedence as ``claude auth status`` reports it: a bearer token beats an
|
|
114
|
+
# API key, which beats a subscription token (``claude setup-token``),
|
|
115
|
+
# which beats the stored Claude.ai login.
|
|
116
|
+
credential_sources=(
|
|
117
|
+
EnvCredential("ANTHROPIC_AUTH_TOKEN", CredentialKind.API_KEY),
|
|
118
|
+
EnvCredential("ANTHROPIC_API_KEY", CredentialKind.API_KEY),
|
|
119
|
+
EnvCredential("CLAUDE_CODE_OAUTH_TOKEN", CredentialKind.SUBSCRIPTION),
|
|
120
|
+
FileCredential(".claude/.credentials.json", kind=CredentialKind.SUBSCRIPTION),
|
|
121
|
+
),
|
|
122
|
+
endpoint=EndpointSpec("ANTHROPIC_BASE_URL", companion_env=("ANTHROPIC_CUSTOM_HEADERS",)),
|
|
106
123
|
mcp_config_file=MCP_CONFIG_FILENAME,
|
|
107
124
|
)
|
|
108
125
|
|
|
@@ -10,6 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
from typing import TYPE_CHECKING
|
|
12
12
|
|
|
13
|
+
from agentshim.core.auth import CredentialKind
|
|
13
14
|
from agentshim.core.status import AuthCheck, AuthProbe, AuthState, ProbeOutput, ProbeSpec
|
|
14
15
|
|
|
15
16
|
if TYPE_CHECKING:
|
|
@@ -22,12 +23,20 @@ LOGIN_HINT = "run `codex login`"
|
|
|
22
23
|
_API_KEY_ENV = ("CODEX_API_KEY", "OPENAI_API_KEY")
|
|
23
24
|
|
|
24
25
|
|
|
26
|
+
def _credential(line: str) -> CredentialKind | None:
|
|
27
|
+
if line.startswith("Logged in using ChatGPT"):
|
|
28
|
+
return CredentialKind.SUBSCRIPTION
|
|
29
|
+
if line.startswith("Logged in using an API key"):
|
|
30
|
+
return CredentialKind.API_KEY
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
25
34
|
def read_auth(output: ProbeOutput, env: Mapping[str, str]) -> AuthCheck:
|
|
26
35
|
"""Read ``codex login status``'s text."""
|
|
27
36
|
lines = [line.strip() for line in output.text.splitlines() if line.strip()]
|
|
28
37
|
for line in lines:
|
|
29
38
|
if line.startswith("Logged in"):
|
|
30
|
-
return AuthCheck(AuthState.KNOWN_OK, line)
|
|
39
|
+
return AuthCheck(AuthState.KNOWN_OK, line, _credential(line))
|
|
31
40
|
if any(line.startswith("Not logged in") for line in lines):
|
|
32
41
|
if any(env.get(key) for key in _API_KEY_ENV):
|
|
33
42
|
return AuthCheck(
|
|
@@ -6,6 +6,13 @@ import os
|
|
|
6
6
|
import re
|
|
7
7
|
from typing import TYPE_CHECKING, Any
|
|
8
8
|
|
|
9
|
+
from agentshim.core.auth import (
|
|
10
|
+
CredentialKind,
|
|
11
|
+
EndpointSpec,
|
|
12
|
+
EnvCredential,
|
|
13
|
+
FileCredential,
|
|
14
|
+
classify_json_field,
|
|
15
|
+
)
|
|
9
16
|
from agentshim.core.errors import ProviderCapabilityError, SessionResumeError
|
|
10
17
|
from agentshim.core.home import state_root
|
|
11
18
|
from agentshim.core.mcp import FlagsInstallation, HttpMcpServer, NoopInstallation
|
|
@@ -81,7 +88,7 @@ PROFILE = ProviderProfile(
|
|
|
81
88
|
"Library/Application Support/com.openai.codex",
|
|
82
89
|
"Library/Caches/codex",
|
|
83
90
|
),
|
|
84
|
-
auth_env_vars=("OPENAI_API_KEY", "OPENAI_BASE_URL"),
|
|
91
|
+
auth_env_vars=("CODEX_API_KEY", "OPENAI_API_KEY", "OPENAI_BASE_URL"),
|
|
85
92
|
skill_dirs=(".agents/skills",),
|
|
86
93
|
# ``exec --json`` lists no skills; a load is inferred from a shell read
|
|
87
94
|
# of a ``SKILL.md`` (``skills.py``).
|
|
@@ -102,6 +109,20 @@ PROFILE = ProviderProfile(
|
|
|
102
109
|
# not needlessly expose it to an agent process.
|
|
103
110
|
auth_files=(".codex/auth.json",),
|
|
104
111
|
credential_files=(".codex/auth.json",),
|
|
112
|
+
# ``codex exec`` takes either key in place of a stored login. auth.json
|
|
113
|
+
# records which login it holds (``codex login`` vs ``--with-api-key``).
|
|
114
|
+
credential_sources=(
|
|
115
|
+
EnvCredential("CODEX_API_KEY", CredentialKind.API_KEY),
|
|
116
|
+
EnvCredential("OPENAI_API_KEY", CredentialKind.API_KEY),
|
|
117
|
+
FileCredential(
|
|
118
|
+
".codex/auth.json",
|
|
119
|
+
classify=classify_json_field(
|
|
120
|
+
"auth_mode",
|
|
121
|
+
{"chatgpt": CredentialKind.SUBSCRIPTION, "apikey": CredentialKind.API_KEY},
|
|
122
|
+
),
|
|
123
|
+
),
|
|
124
|
+
),
|
|
125
|
+
endpoint=EndpointSpec("OPENAI_BASE_URL"),
|
|
105
126
|
# Every turn writes a rollout under sessions/, and `resume` reads it back.
|
|
106
127
|
# The rest of the state directory (worktrees, caches, logs) is not needed
|
|
107
128
|
# to log in or resume.
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
from typing import TYPE_CHECKING, Any
|
|
7
7
|
|
|
8
|
+
from agentshim.core.auth import CredentialKind, EnvCredential, FileCredential
|
|
8
9
|
from agentshim.core.errors import ProviderCapabilityError
|
|
9
10
|
from agentshim.core.mcp import FlagsInstallation, HttpMcpServer, NoopInstallation
|
|
10
11
|
from agentshim.core.profile import McpMechanism, OutputSchemaStyle, ProviderProfile
|
|
@@ -61,6 +62,13 @@ PROFILE = ProviderProfile(
|
|
|
61
62
|
# config.json carries the logged-in users and, without a keychain, the
|
|
62
63
|
# tokens; the CLI rewrites it in place on login and refresh.
|
|
63
64
|
credential_files=(".copilot/config.json",),
|
|
65
|
+
# Every Copilot credential is a GitHub login tied to a Copilot plan.
|
|
66
|
+
credential_sources=(
|
|
67
|
+
EnvCredential("COPILOT_GITHUB_TOKEN", CredentialKind.SUBSCRIPTION),
|
|
68
|
+
EnvCredential("GH_TOKEN", CredentialKind.SUBSCRIPTION),
|
|
69
|
+
EnvCredential("GITHUB_TOKEN", CredentialKind.SUBSCRIPTION),
|
|
70
|
+
FileCredential(".copilot/config.json", kind=CredentialKind.SUBSCRIPTION),
|
|
71
|
+
),
|
|
64
72
|
# CLI_FLAGS: Copilot takes MCP servers as one --additional-mcp-config
|
|
65
73
|
# flag, never a file.
|
|
66
74
|
mcp_config_file=None,
|
|
@@ -18,6 +18,7 @@ from __future__ import annotations
|
|
|
18
18
|
|
|
19
19
|
from typing import TYPE_CHECKING, Any
|
|
20
20
|
|
|
21
|
+
from agentshim.core.auth import CredentialKind, EnvCredential, FileCredential
|
|
21
22
|
from agentshim.core.errors import ProviderCapabilityError, SessionResumeError
|
|
22
23
|
from agentshim.core.mcp import (
|
|
23
24
|
HttpMcpServer,
|
|
@@ -81,6 +82,11 @@ PROFILE = ProviderProfile(
|
|
|
81
82
|
".gemini/.env",
|
|
82
83
|
),
|
|
83
84
|
credential_files=(".gemini/oauth_creds.json",),
|
|
85
|
+
credential_sources=(
|
|
86
|
+
EnvCredential("GEMINI_API_KEY", CredentialKind.API_KEY),
|
|
87
|
+
EnvCredential("GOOGLE_API_KEY", CredentialKind.API_KEY),
|
|
88
|
+
FileCredential(".gemini/oauth_creds.json", kind=CredentialKind.SUBSCRIPTION),
|
|
89
|
+
),
|
|
84
90
|
mcp_config_file=MCP_CONFIG_FILENAME,
|
|
85
91
|
)
|
|
86
92
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|