agentshim 0.16.0__tar.gz → 0.18.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.16.0 → agentshim-0.18.0}/CHANGELOG.md +41 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/PKG-INFO +1 -1
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/__init__.py +21 -1
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/core/__init__.py +16 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/core/auth.py +270 -11
- agentshim-0.18.0/agentshim/core/models.py +247 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/core/profile.py +20 -4
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/claude/provider.py +34 -3
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/codex/provider.py +11 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/gemini/provider.py +18 -1
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/opencode/provider.py +3 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/pyproject.toml +1 -1
- {agentshim-0.16.0 → agentshim-0.18.0}/.gitignore +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/LICENSE +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/README.md +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/agent.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/core/_files.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/core/checkpoints.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/core/clock.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/core/conversation.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/core/env.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/core/errors.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/core/events.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/core/home.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/core/ids.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/core/mcp.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/core/permissions.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/core/pricing.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/core/provider.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/core/schema.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/core/session_policy.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/core/skills.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/core/status.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/core/stream.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/core/turn.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/core/usage.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/execution/__init__.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/execution/confinement.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/execution/docker.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/execution/executor.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/execution/host.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/execution/process.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/execution/transform.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/oneshot.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/probe.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/__init__.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/claude/__init__.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/claude/events.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/claude/failures.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/claude/hooks/__init__.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/claude/hooks/confine_reads.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/claude/parser.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/claude/probe.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/claude/rate_limits.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/claude/sandbox.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/claude/scripted.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/claude/stream_argv.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/claude/stream_transport.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/claude/user_hooks.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/codex/__init__.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/codex/_toml.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/codex/app_server/__init__.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/codex/app_server/_wire.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/codex/app_server/approvals.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/codex/app_server/channel.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/codex/app_server/errors.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/codex/app_server/items.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/codex/app_server/mcp.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/codex/app_server/permissions.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/codex/app_server/profile.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/codex/app_server/protocol.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/codex/app_server/rate_limits.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/codex/app_server/transport.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/codex/app_server/usage.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/codex/events.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/codex/failures.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/codex/mcp_scope.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/codex/parser.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/codex/probe.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/codex/rules.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/codex/sandbox.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/codex/scripted.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/codex/skills.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/copilot/__init__.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/copilot/events.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/copilot/parser.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/copilot/probe.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/copilot/provider.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/copilot/scripted.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/gemini/__init__.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/gemini/events.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/gemini/parser.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/gemini/probe.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/gemini/scripted.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/opencode/__init__.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/opencode/events.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/opencode/parser.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/opencode/probe.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/providers/opencode/scripted.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/py.typed +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/runtime.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/session.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/testing/__init__.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/testing/claude_stream.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/testing/clock.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/testing/codex_app_server.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/testing/confinement.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/testing/contracts.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/testing/conversation.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/testing/probe.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/agentshim/testing/process.py +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/tests/fixtures/claude_stream/README.md +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/tests/fixtures/codex/README.md +0 -0
- {agentshim-0.16.0 → agentshim-0.18.0}/tests/fixtures/codex_app_server/README.md +0 -0
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.18.0 (2026-10-16)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `context_window(provider, model)` and `model_spec(provider, model)`: a model's context
|
|
8
|
+
window in tokens from a static, versioned table (`ModelSpec`, `ModelTable`,
|
|
9
|
+
`default_models()`), each entry citing the vendor page it was read from. Covers the
|
|
10
|
+
Claude 5 family (`claude-fable-5-1`, `claude-opus-5-5`, `claude-sonnet-5-5`,
|
|
11
|
+
`claude-haiku-5-5`, all 1M) and current OpenAI models. An unknown model, or a
|
|
12
|
+
bracketed variant, returns `None`; the lookup never matches by prefix.
|
|
13
|
+
- `ProviderProfile.models`: the model ids a provider's CLI is known to run, the default
|
|
14
|
+
first, and `ProviderProfile.default_model`. Claude, Codex and opencode declare theirs.
|
|
15
|
+
|
|
16
|
+
## 0.17.0 (2026-10-15)
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `host_auth_plan(profile, env=, home=)` returns the `AuthPlan` that reproduces how the
|
|
21
|
+
CLI logs in by itself on a host: the first present credential source of any kind, plus
|
|
22
|
+
the host's endpoint variables exactly as set, so a confined copy given only the plan
|
|
23
|
+
sends the same credential to the same place. A host base URL is reproduced, not
|
|
24
|
+
validated, and a credential goes to the host's endpoint whatever its kind (Claude Code
|
|
25
|
+
sends `CLAUDE_CODE_OAUTH_TOKEN` to a custom `ANTHROPIC_BASE_URL`). It raises
|
|
26
|
+
`HostAuthUnplannableError`, whose `reason` (`HostAuthGap`) is `UNCLASSIFIED`,
|
|
27
|
+
`NO_CREDENTIAL`, `EXTERNAL_CREDENTIAL` or `SETTINGS_ENV`, when no plan can reproduce
|
|
28
|
+
the host's login.
|
|
29
|
+
- `ExternalCredential`, a `CredentialSource` for a way the CLI logs in that no plan can
|
|
30
|
+
hand over, detected by variables, paths or settings keys. Claude declares a cloud
|
|
31
|
+
provider (`CLAUDE_CODE_USE_BEDROCK`, `_VERTEX`, `_FOUNDRY`), `apiKeyHelper`, and
|
|
32
|
+
Anthropic profiles at their documented places in its precedence; Gemini declares an
|
|
33
|
+
auth type selected in settings or by variable, and `.gemini/.env`.
|
|
34
|
+
- `ProviderProfile.settings` (`SettingsSpec`): the settings files a CLI reads at start
|
|
35
|
+
that can change how it logs in, and the key of the block it applies over its
|
|
36
|
+
environment (Claude's `env`).
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- `CredentialSource` is now `EnvCredential | FileCredential | ExternalCredential`;
|
|
41
|
+
code that matched on two variants must handle the third. `plan_auth` never chooses an
|
|
42
|
+
`ExternalCredential` and names a present one in `AuthPlan.withheld`.
|
|
43
|
+
|
|
3
44
|
## 0.16.0 (2026-10-14)
|
|
4
45
|
|
|
5
46
|
### Added
|
|
@@ -40,9 +40,12 @@ from .core import (
|
|
|
40
40
|
EndpointSpec,
|
|
41
41
|
EnvCredential,
|
|
42
42
|
EventHandlerBase,
|
|
43
|
+
ExternalCredential,
|
|
43
44
|
FailureKind,
|
|
44
45
|
FileCredential,
|
|
45
46
|
FlagsInstallation,
|
|
47
|
+
HostAuthGap,
|
|
48
|
+
HostAuthUnplannableError,
|
|
46
49
|
HttpMcpServer,
|
|
47
50
|
IdAllocator,
|
|
48
51
|
InMemoryCheckpointStore,
|
|
@@ -54,6 +57,8 @@ from .core import (
|
|
|
54
57
|
McpServer,
|
|
55
58
|
McpTransport,
|
|
56
59
|
ModelPricing,
|
|
60
|
+
ModelSpec,
|
|
61
|
+
ModelTable,
|
|
57
62
|
NativeMode,
|
|
58
63
|
NativePermissions,
|
|
59
64
|
NoopInstallation,
|
|
@@ -88,6 +93,7 @@ from .core import (
|
|
|
88
93
|
SessionStarted,
|
|
89
94
|
SessionState,
|
|
90
95
|
SessionStateError,
|
|
96
|
+
SettingsSpec,
|
|
91
97
|
SkillInvoked,
|
|
92
98
|
SkillScope,
|
|
93
99
|
SkillsDiscovered,
|
|
@@ -120,12 +126,16 @@ from .core import (
|
|
|
120
126
|
UsageReport,
|
|
121
127
|
compact_json,
|
|
122
128
|
compose_event_handlers,
|
|
129
|
+
context_window,
|
|
123
130
|
cost_usd,
|
|
131
|
+
default_models,
|
|
124
132
|
default_pricing,
|
|
125
133
|
dialect_problems,
|
|
134
|
+
host_auth_plan,
|
|
126
135
|
install_config_file,
|
|
127
136
|
interactive_env,
|
|
128
137
|
materialize,
|
|
138
|
+
model_spec,
|
|
129
139
|
normalize,
|
|
130
140
|
normalized_usage,
|
|
131
141
|
parse_json_object,
|
|
@@ -166,7 +176,7 @@ from .providers.opencode import OpencodeProvider
|
|
|
166
176
|
from .runtime import Agent
|
|
167
177
|
from .session import Session, Turn, TurnTicket, map_mcp_servers
|
|
168
178
|
|
|
169
|
-
__version__ = "0.
|
|
179
|
+
__version__ = "0.18.0"
|
|
170
180
|
|
|
171
181
|
__all__ = [
|
|
172
182
|
"Agent",
|
|
@@ -219,10 +229,13 @@ __all__ = [
|
|
|
219
229
|
"EndpointSpec",
|
|
220
230
|
"EnvCredential",
|
|
221
231
|
"EventHandlerBase",
|
|
232
|
+
"ExternalCredential",
|
|
222
233
|
"FailureKind",
|
|
223
234
|
"FileCredential",
|
|
224
235
|
"FlagsInstallation",
|
|
225
236
|
"GeminiProvider",
|
|
237
|
+
"HostAuthGap",
|
|
238
|
+
"HostAuthUnplannableError",
|
|
226
239
|
"HostCommandExecutor",
|
|
227
240
|
"HostProcess",
|
|
228
241
|
"HttpMcpServer",
|
|
@@ -236,6 +249,8 @@ __all__ = [
|
|
|
236
249
|
"McpServer",
|
|
237
250
|
"McpTransport",
|
|
238
251
|
"ModelPricing",
|
|
252
|
+
"ModelSpec",
|
|
253
|
+
"ModelTable",
|
|
239
254
|
"NativeMode",
|
|
240
255
|
"NativePermissions",
|
|
241
256
|
"NoRunningTurnError",
|
|
@@ -278,6 +293,7 @@ __all__ = [
|
|
|
278
293
|
"SessionStarted",
|
|
279
294
|
"SessionState",
|
|
280
295
|
"SessionStateError",
|
|
296
|
+
"SettingsSpec",
|
|
281
297
|
"SkillInvoked",
|
|
282
298
|
"SkillScope",
|
|
283
299
|
"SkillSignal",
|
|
@@ -318,14 +334,18 @@ __all__ = [
|
|
|
318
334
|
"compact_json",
|
|
319
335
|
"compose_event_handlers",
|
|
320
336
|
"confine",
|
|
337
|
+
"context_window",
|
|
321
338
|
"cost_usd",
|
|
339
|
+
"default_models",
|
|
322
340
|
"default_pricing",
|
|
323
341
|
"dialect_problems",
|
|
324
342
|
"get_provider",
|
|
343
|
+
"host_auth_plan",
|
|
325
344
|
"install_config_file",
|
|
326
345
|
"interactive_env",
|
|
327
346
|
"map_mcp_servers",
|
|
328
347
|
"materialize",
|
|
348
|
+
"model_spec",
|
|
329
349
|
"normalize",
|
|
330
350
|
"normalized_usage",
|
|
331
351
|
"parse_json_object",
|
|
@@ -15,8 +15,13 @@ from .auth import (
|
|
|
15
15
|
CredentialSource,
|
|
16
16
|
EndpointSpec,
|
|
17
17
|
EnvCredential,
|
|
18
|
+
ExternalCredential,
|
|
18
19
|
FileCredential,
|
|
20
|
+
HostAuthGap,
|
|
21
|
+
HostAuthUnplannableError,
|
|
22
|
+
SettingsSpec,
|
|
19
23
|
StagedCredentialFile,
|
|
24
|
+
host_auth_plan,
|
|
20
25
|
plan_auth,
|
|
21
26
|
)
|
|
22
27
|
from .checkpoints import Checkpoint, CheckpointStore, InMemoryCheckpointStore
|
|
@@ -90,6 +95,7 @@ from .mcp import (
|
|
|
90
95
|
StdioMcpServer,
|
|
91
96
|
install_config_file,
|
|
92
97
|
)
|
|
98
|
+
from .models import ModelSpec, ModelTable, context_window, default_models, model_spec
|
|
93
99
|
from .permissions import ApprovalPolicy, NativeMode, NativePermissions
|
|
94
100
|
from .pricing import ModelPricing, PricingTable, cost_usd, default_pricing, price_for
|
|
95
101
|
from .profile import (
|
|
@@ -153,9 +159,12 @@ __all__ = [
|
|
|
153
159
|
"EndpointSpec",
|
|
154
160
|
"EnvCredential",
|
|
155
161
|
"EventHandlerBase",
|
|
162
|
+
"ExternalCredential",
|
|
156
163
|
"FailureKind",
|
|
157
164
|
"FileCredential",
|
|
158
165
|
"FlagsInstallation",
|
|
166
|
+
"HostAuthGap",
|
|
167
|
+
"HostAuthUnplannableError",
|
|
159
168
|
"HttpMcpServer",
|
|
160
169
|
"IdAllocator",
|
|
161
170
|
"InMemoryCheckpointStore",
|
|
@@ -167,6 +176,8 @@ __all__ = [
|
|
|
167
176
|
"McpServer",
|
|
168
177
|
"McpTransport",
|
|
169
178
|
"ModelPricing",
|
|
179
|
+
"ModelSpec",
|
|
180
|
+
"ModelTable",
|
|
170
181
|
"NativeMode",
|
|
171
182
|
"NativePermissions",
|
|
172
183
|
"NoRunningTurnError",
|
|
@@ -201,6 +212,7 @@ __all__ = [
|
|
|
201
212
|
"SessionStarted",
|
|
202
213
|
"SessionState",
|
|
203
214
|
"SessionStateError",
|
|
215
|
+
"SettingsSpec",
|
|
204
216
|
"SkillInvoked",
|
|
205
217
|
"SkillScope",
|
|
206
218
|
"SkillSignal",
|
|
@@ -233,12 +245,16 @@ __all__ = [
|
|
|
233
245
|
"UsageReport",
|
|
234
246
|
"compact_json",
|
|
235
247
|
"compose_event_handlers",
|
|
248
|
+
"context_window",
|
|
236
249
|
"cost_usd",
|
|
250
|
+
"default_models",
|
|
237
251
|
"default_pricing",
|
|
238
252
|
"dialect_problems",
|
|
253
|
+
"host_auth_plan",
|
|
239
254
|
"install_config_file",
|
|
240
255
|
"interactive_env",
|
|
241
256
|
"materialize",
|
|
257
|
+
"model_spec",
|
|
242
258
|
"normalize",
|
|
243
259
|
"normalized_usage",
|
|
244
260
|
"parse_json_object",
|
|
@@ -16,6 +16,12 @@ exact environment variables and files to deliver. The caller delivers the
|
|
|
16
16
|
plan and nothing more of the provider's ``auth_env_vars`` or ``auth_files``;
|
|
17
17
|
the readiness probe then reports which kind of credential the CLI says it is
|
|
18
18
|
using (``ProviderStatus.credential``), so the caller can confirm it.
|
|
19
|
+
|
|
20
|
+
A caller with no policy of its own, that wants a confined copy of the CLI to
|
|
21
|
+
log in exactly as the CLI would on the host, asks ``host_auth_plan`` instead:
|
|
22
|
+
it applies the CLI's own precedence with every kind allowed and the host's
|
|
23
|
+
endpoint as it is, and refuses (``HostAuthUnplannableError``) when the host
|
|
24
|
+
CLI would log in some way a plan cannot reproduce.
|
|
19
25
|
"""
|
|
20
26
|
|
|
21
27
|
from __future__ import annotations
|
|
@@ -85,7 +91,49 @@ class FileCredential:
|
|
|
85
91
|
return self.classify(contents)
|
|
86
92
|
|
|
87
93
|
|
|
88
|
-
|
|
94
|
+
@dataclass(frozen=True)
|
|
95
|
+
class ExternalCredential:
|
|
96
|
+
"""A way the CLI logs in that no plan can hand to another copy of it.
|
|
97
|
+
|
|
98
|
+
A cloud provider selected by a variable (whose own credentials live in
|
|
99
|
+
other tools' state), a helper command named in a settings file, a
|
|
100
|
+
credential store outside the CLI's state root. It is declared at its place
|
|
101
|
+
in the precedence order so ``host_auth_plan`` knows when the host CLI would
|
|
102
|
+
use it; ``plan_auth`` never chooses it. It is present when any variable in
|
|
103
|
+
``env`` is set, any home-relative path in ``paths`` exists, or any of the
|
|
104
|
+
profile's ``settings`` files has one of ``settings_keys`` at its top level.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
label: str
|
|
108
|
+
env: tuple[str, ...] = ()
|
|
109
|
+
paths: tuple[str, ...] = ()
|
|
110
|
+
settings_keys: tuple[str, ...] = ()
|
|
111
|
+
|
|
112
|
+
def __post_init__(self) -> None:
|
|
113
|
+
"""Require some way to detect the credential."""
|
|
114
|
+
if not (self.env or self.paths or self.settings_keys):
|
|
115
|
+
msg = f"ExternalCredential {self.label!r} needs an env, paths or settings_keys entry"
|
|
116
|
+
raise ValueError(msg)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
CredentialSource = EnvCredential | FileCredential | ExternalCredential
|
|
120
|
+
#: The sources a plan can hand over.
|
|
121
|
+
_DeliverableSource = EnvCredential | FileCredential
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(frozen=True)
|
|
125
|
+
class SettingsSpec:
|
|
126
|
+
"""The JSON settings files a CLI reads from its home when it starts.
|
|
127
|
+
|
|
128
|
+
``files`` are home-relative paths. ``env_key`` names a top-level object
|
|
129
|
+
whose string entries the CLI applies over its own environment (Claude
|
|
130
|
+
Code's ``env``), or ``None`` when the CLI has no such block. Settings can
|
|
131
|
+
change which credential or endpoint the CLI uses, so ``host_auth_plan``
|
|
132
|
+
reads them; neither plan function delivers them.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
files: tuple[str, ...]
|
|
136
|
+
env_key: str | None = None
|
|
89
137
|
|
|
90
138
|
|
|
91
139
|
@dataclass(frozen=True)
|
|
@@ -170,6 +218,37 @@ class AuthUnavailableError(AgentShimError):
|
|
|
170
218
|
"""
|
|
171
219
|
|
|
172
220
|
|
|
221
|
+
class HostAuthGap(str, Enum):
|
|
222
|
+
"""Why ``host_auth_plan`` cannot reproduce how the host CLI logs in.
|
|
223
|
+
|
|
224
|
+
``UNCLASSIFIED``: the profile declares no credential sources. ``NO_CREDENTIAL``:
|
|
225
|
+
none of the declared sources is present, so the CLI either cannot log in
|
|
226
|
+
or does it some way agentshim does not know. ``EXTERNAL_CREDENTIAL``: the
|
|
227
|
+
CLI would use an ``ExternalCredential``. ``SETTINGS_ENV``: a settings file
|
|
228
|
+
sets a credential or endpoint variable of its own, so the environment the
|
|
229
|
+
CLI logs in with is not the caller's.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
UNCLASSIFIED = "unclassified"
|
|
233
|
+
NO_CREDENTIAL = "no_credential"
|
|
234
|
+
EXTERNAL_CREDENTIAL = "external_credential"
|
|
235
|
+
SETTINGS_ENV = "settings_env"
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class HostAuthUnplannableError(AgentShimError):
|
|
239
|
+
"""The host CLI's login cannot be reproduced by a plan; ``reason`` says why.
|
|
240
|
+
|
|
241
|
+
The message names the variables or files involved, never a value. A caller
|
|
242
|
+
that still wants the CLI logged in elsewhere has only the host's whole auth
|
|
243
|
+
state (``auth_env_vars`` and ``auth_files``) to hand over.
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
def __init__(self, reason: HostAuthGap, message: str) -> None:
|
|
247
|
+
"""Record *reason* beside the message."""
|
|
248
|
+
super().__init__(message)
|
|
249
|
+
self.reason = reason
|
|
250
|
+
|
|
251
|
+
|
|
173
252
|
def plan_auth(
|
|
174
253
|
profile: ProviderProfile,
|
|
175
254
|
policy: AuthPolicy,
|
|
@@ -205,10 +284,6 @@ def plan_auth(
|
|
|
205
284
|
{name: env[name] for name in profile.endpoint.companion_env if _set(env, name)}
|
|
206
285
|
)
|
|
207
286
|
|
|
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
287
|
return AuthPlan(
|
|
213
288
|
provider=profile.name,
|
|
214
289
|
credential=kind,
|
|
@@ -216,7 +291,61 @@ def plan_auth(
|
|
|
216
291
|
endpoint=policy.endpoint,
|
|
217
292
|
env=MappingProxyType(delivered_env),
|
|
218
293
|
files=files,
|
|
219
|
-
withheld=
|
|
294
|
+
withheld=_withheld(profile, env, home, present, _delivered(delivered_env, files)),
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def host_auth_plan(profile: ProviderProfile, *, env: Mapping[str, str], home: Path) -> AuthPlan:
|
|
299
|
+
"""Say how *profile*'s CLI logs in by itself on a host, as a plan a confined copy can use.
|
|
300
|
+
|
|
301
|
+
The CLI's own choice: the first present source in ``credential_sources``,
|
|
302
|
+
of any kind, at whatever endpoint *env* points it to. The plan delivers
|
|
303
|
+
that credential, and the host's endpoint variables (``EndpointSpec``
|
|
304
|
+
``base_url_env`` and ``companion_env``) exactly as they are set, so a
|
|
305
|
+
confined copy given only the plan sends the same credential to the same
|
|
306
|
+
place. ``AuthPlan.endpoint`` is the host's base URL, or ``None`` for the
|
|
307
|
+
vendor's endpoint.
|
|
308
|
+
|
|
309
|
+
The host's base URL is reproduced, not validated: a value the CLI cannot
|
|
310
|
+
use fails the copy the same way it fails the host CLI (Claude Code fails
|
|
311
|
+
every request with "Invalid URL" and never falls back to its vendor). The
|
|
312
|
+
credential's kind is not checked against ``EndpointSpec.credentials``
|
|
313
|
+
either: what the CLI does with a credential and a custom endpoint is the
|
|
314
|
+
CLI's behaviour, and the copy reproduces it.
|
|
315
|
+
|
|
316
|
+
Raises:
|
|
317
|
+
HostAuthUnplannableError: the profile declares no credential sources,
|
|
318
|
+
none is present, the CLI would log in with an
|
|
319
|
+
``ExternalCredential``, or a settings file sets a credential or
|
|
320
|
+
endpoint variable; ``reason`` is the matching ``HostAuthGap``.
|
|
321
|
+
"""
|
|
322
|
+
if not profile.credential_sources:
|
|
323
|
+
msg = f"{profile.name} declares no credential sources agentshim can classify"
|
|
324
|
+
raise HostAuthUnplannableError(HostAuthGap.UNCLASSIFIED, msg)
|
|
325
|
+
settings = _read_settings(profile, home)
|
|
326
|
+
_refuse_settings_env(profile, settings)
|
|
327
|
+
chosen, kind = _host_choice(profile, env, home, settings)
|
|
328
|
+
delivered_env: dict[str, str] = {}
|
|
329
|
+
files: tuple[StagedCredentialFile, ...] = ()
|
|
330
|
+
if isinstance(chosen, EnvCredential):
|
|
331
|
+
delivered_env[chosen.name] = env[chosen.name]
|
|
332
|
+
else:
|
|
333
|
+
refreshable = kind is CredentialKind.SUBSCRIPTION
|
|
334
|
+
files = (StagedCredentialFile(home / chosen.path, chosen.path, refreshable),)
|
|
335
|
+
endpoint: str | None = None
|
|
336
|
+
if profile.endpoint is not None:
|
|
337
|
+
names = (profile.endpoint.base_url_env, *profile.endpoint.companion_env)
|
|
338
|
+
delivered_env.update({name: env[name] for name in names if _set(env, name)})
|
|
339
|
+
endpoint = delivered_env.get(profile.endpoint.base_url_env)
|
|
340
|
+
present = _present_names(profile, env, home, settings)
|
|
341
|
+
return AuthPlan(
|
|
342
|
+
provider=profile.name,
|
|
343
|
+
credential=kind,
|
|
344
|
+
source=_name(chosen),
|
|
345
|
+
endpoint=endpoint,
|
|
346
|
+
env=MappingProxyType(delivered_env),
|
|
347
|
+
files=files,
|
|
348
|
+
withheld=_withheld(profile, env, home, present, _delivered(delivered_env, files)),
|
|
220
349
|
)
|
|
221
350
|
|
|
222
351
|
|
|
@@ -270,11 +399,14 @@ def _choose(
|
|
|
270
399
|
allowed: frozenset[CredentialKind],
|
|
271
400
|
env: Mapping[str, str],
|
|
272
401
|
home: Path,
|
|
273
|
-
) -> tuple[
|
|
402
|
+
) -> tuple[_DeliverableSource, CredentialKind, list[str]]:
|
|
274
403
|
"""The first present allowed source, its kind, and every present source's name."""
|
|
275
|
-
chosen: tuple[
|
|
404
|
+
chosen: tuple[_DeliverableSource, CredentialKind] | None = None
|
|
276
405
|
present: list[str] = []
|
|
277
406
|
for source in profile.credential_sources:
|
|
407
|
+
if isinstance(source, ExternalCredential):
|
|
408
|
+
present += _external_found(profile, source, env, home)
|
|
409
|
+
continue
|
|
278
410
|
kind = _present_kind(source, env, home)
|
|
279
411
|
if kind is None:
|
|
280
412
|
continue
|
|
@@ -286,8 +418,134 @@ def _choose(
|
|
|
286
418
|
return chosen[0], chosen[1], present
|
|
287
419
|
|
|
288
420
|
|
|
421
|
+
def _host_choice(
|
|
422
|
+
profile: ProviderProfile,
|
|
423
|
+
env: Mapping[str, str],
|
|
424
|
+
home: Path,
|
|
425
|
+
settings: Mapping[str, dict[str, Any]],
|
|
426
|
+
) -> tuple[_DeliverableSource, CredentialKind]:
|
|
427
|
+
"""The source the CLI itself would log in with: the first present one, of any kind."""
|
|
428
|
+
for source in profile.credential_sources:
|
|
429
|
+
if isinstance(source, ExternalCredential):
|
|
430
|
+
found = _external_found(profile, source, env, home, settings)
|
|
431
|
+
if found:
|
|
432
|
+
msg = (
|
|
433
|
+
f"{profile.name} would log in with {source.label} "
|
|
434
|
+
f"({', '.join(found)}), which no plan can hand to another copy of it"
|
|
435
|
+
)
|
|
436
|
+
raise HostAuthUnplannableError(HostAuthGap.EXTERNAL_CREDENTIAL, msg)
|
|
437
|
+
continue
|
|
438
|
+
kind = _present_kind(source, env, home)
|
|
439
|
+
if kind is not None:
|
|
440
|
+
return source, kind
|
|
441
|
+
wanted = ", ".join(
|
|
442
|
+
_name(source)
|
|
443
|
+
for source in profile.credential_sources
|
|
444
|
+
if not isinstance(source, ExternalCredential)
|
|
445
|
+
)
|
|
446
|
+
msg = f"no credential {profile.name} logs in with is present: none of {wanted}"
|
|
447
|
+
raise HostAuthUnplannableError(HostAuthGap.NO_CREDENTIAL, msg)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _read_settings(profile: ProviderProfile, home: Path) -> dict[str, dict[str, Any]]:
|
|
451
|
+
"""Each of *profile*'s settings files that holds a JSON object, by path."""
|
|
452
|
+
out: dict[str, dict[str, Any]] = {}
|
|
453
|
+
for path in profile.settings.files if profile.settings is not None else ():
|
|
454
|
+
try:
|
|
455
|
+
data: Any = json.loads((home / path).read_bytes())
|
|
456
|
+
except (OSError, ValueError):
|
|
457
|
+
continue
|
|
458
|
+
if isinstance(data, dict):
|
|
459
|
+
out[path] = cast("dict[str, Any]", data)
|
|
460
|
+
return out
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _refuse_settings_env(profile: ProviderProfile, settings: Mapping[str, dict[str, Any]]) -> None:
|
|
464
|
+
"""Raise when a settings file's env block sets a variable the CLI logs in with."""
|
|
465
|
+
key = profile.settings.env_key if profile.settings is not None else None
|
|
466
|
+
if key is None:
|
|
467
|
+
return
|
|
468
|
+
names = _host_auth_names(profile) | _credential_env_names(profile)
|
|
469
|
+
for path, data in settings.items():
|
|
470
|
+
block: Any = data.get(key)
|
|
471
|
+
if not isinstance(block, dict):
|
|
472
|
+
continue
|
|
473
|
+
overridden = sorted(names & set(cast("dict[str, Any]", block)))
|
|
474
|
+
if overridden:
|
|
475
|
+
msg = (
|
|
476
|
+
f"{path} sets {', '.join(overridden)} in its {key!r} block, so "
|
|
477
|
+
f"{profile.name} does not log in with the caller's environment"
|
|
478
|
+
)
|
|
479
|
+
raise HostAuthUnplannableError(HostAuthGap.SETTINGS_ENV, msg)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _external_found(
|
|
483
|
+
profile: ProviderProfile,
|
|
484
|
+
source: ExternalCredential,
|
|
485
|
+
env: Mapping[str, str],
|
|
486
|
+
home: Path,
|
|
487
|
+
settings: Mapping[str, dict[str, Any]] | None = None,
|
|
488
|
+
) -> list[str]:
|
|
489
|
+
"""What of *source* is on the host: set variables, existing paths, settings keys."""
|
|
490
|
+
found = [name for name in source.env if _set(env, name)]
|
|
491
|
+
found += [path for path in source.paths if (home / path).exists()]
|
|
492
|
+
if source.settings_keys:
|
|
493
|
+
loaded = _read_settings(profile, home) if settings is None else settings
|
|
494
|
+
found += [
|
|
495
|
+
f"{key} in {path}"
|
|
496
|
+
for path, data in loaded.items()
|
|
497
|
+
for key in source.settings_keys
|
|
498
|
+
if key in data
|
|
499
|
+
]
|
|
500
|
+
return found
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _present_names(
|
|
504
|
+
profile: ProviderProfile,
|
|
505
|
+
env: Mapping[str, str],
|
|
506
|
+
home: Path,
|
|
507
|
+
settings: Mapping[str, dict[str, Any]],
|
|
508
|
+
) -> list[str]:
|
|
509
|
+
"""The names of every declared source present on the host."""
|
|
510
|
+
names: list[str] = []
|
|
511
|
+
for source in profile.credential_sources:
|
|
512
|
+
if isinstance(source, ExternalCredential):
|
|
513
|
+
names += _external_found(profile, source, env, home, settings)
|
|
514
|
+
elif _present_kind(source, env, home) is not None:
|
|
515
|
+
names.append(_name(source))
|
|
516
|
+
return names
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _withheld(
|
|
520
|
+
profile: ProviderProfile,
|
|
521
|
+
env: Mapping[str, str],
|
|
522
|
+
home: Path,
|
|
523
|
+
present: list[str],
|
|
524
|
+
delivered: set[str],
|
|
525
|
+
) -> tuple[str, ...]:
|
|
526
|
+
"""The names of the host's auth state a plan delivering *delivered* leaves out."""
|
|
527
|
+
on_host = {name for name in _host_auth_names(profile) if _set(env, name)}
|
|
528
|
+
on_host |= {path for path in profile.auth_files if (home / path).is_file()}
|
|
529
|
+
on_host |= set(present)
|
|
530
|
+
return tuple(sorted(on_host - delivered))
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _delivered(env: Mapping[str, str], files: tuple[StagedCredentialFile, ...]) -> set[str]:
|
|
534
|
+
return set(env) | {staged.home_path for staged in files}
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def _credential_env_names(profile: ProviderProfile) -> set[str]:
|
|
538
|
+
names: set[str] = set()
|
|
539
|
+
for source in profile.credential_sources:
|
|
540
|
+
if isinstance(source, EnvCredential):
|
|
541
|
+
names.add(source.name)
|
|
542
|
+
elif isinstance(source, ExternalCredential):
|
|
543
|
+
names.update(source.env)
|
|
544
|
+
return names
|
|
545
|
+
|
|
546
|
+
|
|
289
547
|
def _present_kind(
|
|
290
|
-
source:
|
|
548
|
+
source: _DeliverableSource, env: Mapping[str, str], home: Path
|
|
291
549
|
) -> CredentialKind | None:
|
|
292
550
|
if isinstance(source, EnvCredential):
|
|
293
551
|
return source.kind if _set(env, source.name) else None
|
|
@@ -304,7 +562,7 @@ def _set(env: Mapping[str, str], name: str) -> bool:
|
|
|
304
562
|
return bool(value and value.strip())
|
|
305
563
|
|
|
306
564
|
|
|
307
|
-
def _name(source:
|
|
565
|
+
def _name(source: _DeliverableSource) -> str:
|
|
308
566
|
return source.name if isinstance(source, EnvCredential) else source.path
|
|
309
567
|
|
|
310
568
|
|
|
@@ -326,7 +584,8 @@ def _unavailable(
|
|
|
326
584
|
wanted = [
|
|
327
585
|
_name(source)
|
|
328
586
|
for source in profile.credential_sources
|
|
329
|
-
if
|
|
587
|
+
if not isinstance(source, ExternalCredential)
|
|
588
|
+
and (source.kind is None or source.kind in allowed)
|
|
330
589
|
]
|
|
331
590
|
found = f"; present but not allowed: {', '.join(present)}" if present else ""
|
|
332
591
|
return (
|