agentshim 0.12.0__tar.gz → 0.13.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.12.0 → agentshim-0.13.0}/CHANGELOG.md +19 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/PKG-INFO +1 -1
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/__init__.py +5 -1
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/agent.py +24 -5
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/core/__init__.py +4 -0
- agentshim-0.13.0/agentshim/core/home.py +90 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/core/profile.py +27 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/core/provider.py +4 -1
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/claude/provider.py +14 -1
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/codex/provider.py +36 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/pyproject.toml +1 -1
- {agentshim-0.12.0 → agentshim-0.13.0}/.gitignore +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/README.md +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/core/_files.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/core/env.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/core/errors.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/core/events.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/core/mcp.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/core/pricing.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/core/schema.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/core/skills.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/core/stream.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/core/turn.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/core/usage.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/execution/__init__.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/execution/executor.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/execution/host.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/execution/transform.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/__init__.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/claude/__init__.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/claude/events.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/claude/failures.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/claude/hooks/__init__.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/claude/hooks/confine_reads.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/claude/parser.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/claude/sandbox.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/claude/scripted.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/claude/user_hooks.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/codex/__init__.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/codex/_toml.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/codex/events.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/codex/failures.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/codex/mcp_scope.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/codex/parser.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/codex/rules.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/codex/sandbox.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/codex/scripted.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/codex/skills.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/copilot/__init__.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/copilot/events.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/copilot/parser.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/copilot/provider.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/copilot/scripted.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/gemini/__init__.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/gemini/events.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/gemini/parser.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/gemini/provider.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/gemini/scripted.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/opencode/__init__.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/opencode/events.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/opencode/parser.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/opencode/provider.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/providers/opencode/scripted.py +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/py.typed +0 -0
- {agentshim-0.12.0 → agentshim-0.13.0}/agentshim/testing/__init__.py +0 -0
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.0 (2026-10-03)
|
|
4
|
+
|
|
5
|
+
A session can leave out the user's own CLI configuration. Additive.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `ConfigScope` (`ALL`, `PROJECT`) as a session option on `start_session` and
|
|
10
|
+
`run`, `ProviderProfile.config_scopes`, and `ArgvContext.config_scope`.
|
|
11
|
+
`PROJECT` keeps the user's settings, hooks, global instructions, notify
|
|
12
|
+
commands, profiles and memory out of every turn. Claude Code:
|
|
13
|
+
`--setting-sources project,local` and `autoMemoryEnabled=false`. Codex: a
|
|
14
|
+
dedicated `CODEX_HOME` plus `--ignore-user-config --disable memories`;
|
|
15
|
+
`build_argv` refuses the scope in the user's own home. Copilot, Gemini and
|
|
16
|
+
opencode refuse it.
|
|
17
|
+
- `ProviderProfile.config_home_files` and `prepare_config_home`, which build
|
|
18
|
+
that dedicated home holding only the login and return the environment that
|
|
19
|
+
points the CLI at it. The login is a symlink to the user's own `auth.json`,
|
|
20
|
+
so a refresh token Codex rotates in either home stays valid in both.
|
|
21
|
+
|
|
3
22
|
## 0.12.0 (2026-10-03)
|
|
4
23
|
|
|
5
24
|
A turn whose output never matched its schema says so. Additive except that
|
|
@@ -19,6 +19,7 @@ from .core import (
|
|
|
19
19
|
CliTimeoutError,
|
|
20
20
|
CompositeEventHandler,
|
|
21
21
|
ConfigFileInstallation,
|
|
22
|
+
ConfigScope,
|
|
22
23
|
ConsoleEventHandler,
|
|
23
24
|
EventHandlerBase,
|
|
24
25
|
FailureKind,
|
|
@@ -79,6 +80,7 @@ from .core import (
|
|
|
79
80
|
normalize,
|
|
80
81
|
normalized_usage,
|
|
81
82
|
parse_json_object,
|
|
83
|
+
prepare_config_home,
|
|
82
84
|
price_for,
|
|
83
85
|
)
|
|
84
86
|
from .execution import (
|
|
@@ -99,7 +101,7 @@ from .providers.copilot import CopilotProvider
|
|
|
99
101
|
from .providers.gemini import GeminiProvider
|
|
100
102
|
from .providers.opencode import OpencodeProvider
|
|
101
103
|
|
|
102
|
-
__version__ = "0.
|
|
104
|
+
__version__ = "0.13.0"
|
|
103
105
|
|
|
104
106
|
__all__ = [
|
|
105
107
|
"AgentEvent",
|
|
@@ -125,6 +127,7 @@ __all__ = [
|
|
|
125
127
|
"CommandStreamSink",
|
|
126
128
|
"CompositeEventHandler",
|
|
127
129
|
"ConfigFileInstallation",
|
|
130
|
+
"ConfigScope",
|
|
128
131
|
"ConsoleEventHandler",
|
|
129
132
|
"CopilotProvider",
|
|
130
133
|
"EventHandlerBase",
|
|
@@ -194,6 +197,7 @@ __all__ = [
|
|
|
194
197
|
"normalize",
|
|
195
198
|
"normalized_usage",
|
|
196
199
|
"parse_json_object",
|
|
200
|
+
"prepare_config_home",
|
|
197
201
|
"price_for",
|
|
198
202
|
"provider_names",
|
|
199
203
|
]
|
|
@@ -24,6 +24,7 @@ from agentshim.core.errors import (
|
|
|
24
24
|
)
|
|
25
25
|
from agentshim.core.events import RunFinished, RunStarted, compose_event_handlers
|
|
26
26
|
from agentshim.core.profile import (
|
|
27
|
+
ConfigScope,
|
|
27
28
|
McpMechanism,
|
|
28
29
|
McpScope,
|
|
29
30
|
OutputSchemaStyle,
|
|
@@ -114,7 +115,7 @@ class CliAgent:
|
|
|
114
115
|
self.executor.check_binary(self.binary_path, self.env, timeout=check_timeout)
|
|
115
116
|
self.log(f"{self.profile.display_name} ready at {self.binary_path}")
|
|
116
117
|
|
|
117
|
-
def start_session(
|
|
118
|
+
def start_session( # noqa: PLR0913 - one keyword per independent session option
|
|
118
119
|
self,
|
|
119
120
|
*,
|
|
120
121
|
cwd: str | None = None,
|
|
@@ -122,11 +123,12 @@ class CliAgent:
|
|
|
122
123
|
session_id: str | None = None,
|
|
123
124
|
skill_scope: SkillScope = SkillScope.ALL,
|
|
124
125
|
mcp_scope: McpScope = McpScope.ALL,
|
|
126
|
+
config_scope: ConfigScope = ConfigScope.ALL,
|
|
125
127
|
) -> AgentSession:
|
|
126
128
|
"""Open a conversation whose turns resume one another.
|
|
127
129
|
|
|
128
|
-
``skill_scope`` and ``
|
|
129
|
-
``AgentSession``).
|
|
130
|
+
``skill_scope``, ``mcp_scope`` and ``config_scope`` are fixed for the
|
|
131
|
+
conversation (see ``AgentSession``).
|
|
130
132
|
"""
|
|
131
133
|
return AgentSession(
|
|
132
134
|
self,
|
|
@@ -135,9 +137,10 @@ class CliAgent:
|
|
|
135
137
|
session_id=session_id,
|
|
136
138
|
skill_scope=skill_scope,
|
|
137
139
|
mcp_scope=mcp_scope,
|
|
140
|
+
config_scope=config_scope,
|
|
138
141
|
)
|
|
139
142
|
|
|
140
|
-
def run(
|
|
143
|
+
def run( # noqa: PLR0913 - one keyword per independent session option
|
|
141
144
|
self,
|
|
142
145
|
request: TurnRequest | str,
|
|
143
146
|
*,
|
|
@@ -145,10 +148,15 @@ class CliAgent:
|
|
|
145
148
|
timeout: float | None = None,
|
|
146
149
|
skill_scope: SkillScope = SkillScope.ALL,
|
|
147
150
|
mcp_scope: McpScope = McpScope.ALL,
|
|
151
|
+
config_scope: ConfigScope = ConfigScope.ALL,
|
|
148
152
|
) -> TurnResult:
|
|
149
153
|
"""Run one turn in a throwaway session."""
|
|
150
154
|
session = self.start_session(
|
|
151
|
-
cwd=cwd,
|
|
155
|
+
cwd=cwd,
|
|
156
|
+
timeout=timeout,
|
|
157
|
+
skill_scope=skill_scope,
|
|
158
|
+
mcp_scope=mcp_scope,
|
|
159
|
+
config_scope=config_scope,
|
|
152
160
|
)
|
|
153
161
|
return session.turn(request)
|
|
154
162
|
|
|
@@ -169,6 +177,7 @@ class AgentSession:
|
|
|
169
177
|
session_id: str | None = None,
|
|
170
178
|
skill_scope: SkillScope = SkillScope.ALL,
|
|
171
179
|
mcp_scope: McpScope = McpScope.ALL,
|
|
180
|
+
config_scope: ConfigScope = ConfigScope.ALL,
|
|
172
181
|
) -> None:
|
|
173
182
|
"""Bind a conversation to one agent, with per-turn defaults.
|
|
174
183
|
|
|
@@ -183,6 +192,11 @@ class AgentSession:
|
|
|
183
192
|
limits which MCP servers every turn's CLI may connect to
|
|
184
193
|
(``McpScope.SESSION``: only those in ``TurnRequest.mcp_servers``) and
|
|
185
194
|
is checked against ``profile.mcp_scopes`` the same way.
|
|
195
|
+
``config_scope`` limits which of the user's own CLI configuration
|
|
196
|
+
(settings, hooks, global instructions, memory) every turn loads, and
|
|
197
|
+
is checked against ``profile.config_scopes``; a provider with
|
|
198
|
+
``profile.config_home_files`` additionally needs its state root
|
|
199
|
+
pointed at a home from ``prepare_config_home``.
|
|
186
200
|
"""
|
|
187
201
|
if skill_scope not in agent.profile.skill_scopes:
|
|
188
202
|
msg = f"{agent.profile.name} cannot limit skills to scope {skill_scope.value!r}"
|
|
@@ -190,7 +204,11 @@ class AgentSession:
|
|
|
190
204
|
if mcp_scope not in agent.profile.mcp_scopes:
|
|
191
205
|
msg = f"{agent.profile.name} cannot limit MCP servers to scope {mcp_scope.value!r}"
|
|
192
206
|
raise ProviderCapabilityError(msg)
|
|
207
|
+
if config_scope not in agent.profile.config_scopes:
|
|
208
|
+
msg = f"{agent.profile.name} cannot limit user configuration to scope {config_scope.value!r}"
|
|
209
|
+
raise ProviderCapabilityError(msg)
|
|
193
210
|
self.skill_scope = skill_scope
|
|
211
|
+
self.config_scope = config_scope
|
|
194
212
|
self.mcp_scope = mcp_scope
|
|
195
213
|
self._agent = agent
|
|
196
214
|
self._cwd = cwd
|
|
@@ -302,6 +320,7 @@ class AgentSession:
|
|
|
302
320
|
cwd=cwd,
|
|
303
321
|
skill_scope=self.skill_scope,
|
|
304
322
|
mcp_scope=self.mcp_scope,
|
|
323
|
+
config_scope=self.config_scope,
|
|
305
324
|
mcp_servers=tuple(req.mcp_servers),
|
|
306
325
|
)
|
|
307
326
|
)
|
|
@@ -43,6 +43,7 @@ from .events import (
|
|
|
43
43
|
UsageReport,
|
|
44
44
|
compose_event_handlers,
|
|
45
45
|
)
|
|
46
|
+
from .home import prepare_config_home
|
|
46
47
|
from .mcp import (
|
|
47
48
|
ConfigFileInstallation,
|
|
48
49
|
FlagsInstallation,
|
|
@@ -55,6 +56,7 @@ from .mcp import (
|
|
|
55
56
|
)
|
|
56
57
|
from .pricing import ModelPricing, PricingTable, cost_usd, default_pricing, price_for
|
|
57
58
|
from .profile import (
|
|
59
|
+
ConfigScope,
|
|
58
60
|
McpMechanism,
|
|
59
61
|
McpScope,
|
|
60
62
|
OutputSchemaStyle,
|
|
@@ -82,6 +84,7 @@ __all__ = [
|
|
|
82
84
|
"CliTimeoutError",
|
|
83
85
|
"CompositeEventHandler",
|
|
84
86
|
"ConfigFileInstallation",
|
|
87
|
+
"ConfigScope",
|
|
85
88
|
"ConsoleEventHandler",
|
|
86
89
|
"EventHandlerBase",
|
|
87
90
|
"FailureKind",
|
|
@@ -142,5 +145,6 @@ __all__ = [
|
|
|
142
145
|
"normalize",
|
|
143
146
|
"normalized_usage",
|
|
144
147
|
"parse_json_object",
|
|
148
|
+
"prepare_config_home",
|
|
145
149
|
"price_for",
|
|
146
150
|
]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Dedicated CLI homes for ``ConfigScope.PROJECT``.
|
|
2
|
+
|
|
3
|
+
Some CLIs read the user's global instructions, hooks and memory from their
|
|
4
|
+
state root (Codex: ``$CODEX_HOME/AGENTS.md``, ``hooks.json``, ``memories/``)
|
|
5
|
+
with no flag to skip them. The only way to keep them out is to run the CLI
|
|
6
|
+
against another state root that holds nothing but the credentials. That root
|
|
7
|
+
is the caller's to own, because the CLI keeps its conversations there too: a
|
|
8
|
+
session resumes only from the home it started in.
|
|
9
|
+
|
|
10
|
+
The credentials are linked, not copied. Codex rotates its OAuth refresh token
|
|
11
|
+
on refresh (the server rejects a reused one with ``refresh_token_reused``) and
|
|
12
|
+
saves ``auth.json`` by opening it with ``O_TRUNC`` and writing in place
|
|
13
|
+
(codex-rs ``login/src/auth/storage.rs``, ``FileAuthStorage::save``), which
|
|
14
|
+
follows a symlink. A copy would strand one of the two logins after the first
|
|
15
|
+
refresh; a link keeps a single file that every process rereads before it
|
|
16
|
+
refreshes (``AuthManager::refresh_token``'s guarded reload).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import TYPE_CHECKING
|
|
24
|
+
|
|
25
|
+
from ._files import real_path
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from collections.abc import Mapping
|
|
29
|
+
|
|
30
|
+
from .profile import ProviderProfile
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def state_root(profile: ProviderProfile, env: Mapping[str, str]) -> Path | None:
|
|
34
|
+
"""The state root the CLI run with *env* reads, or ``None`` if it has none.
|
|
35
|
+
|
|
36
|
+
``$<state_root_env>`` when set, else ``$HOME/<state_dirs[0]>``.
|
|
37
|
+
"""
|
|
38
|
+
if profile.state_root_env and env.get(profile.state_root_env):
|
|
39
|
+
return Path(env[profile.state_root_env]).expanduser()
|
|
40
|
+
if not profile.state_dirs:
|
|
41
|
+
return None
|
|
42
|
+
home = env.get("HOME") or os.path.expanduser("~") # noqa: PTH111 - a str contract, not a Path
|
|
43
|
+
return Path(home) / profile.state_dirs[0]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def prepare_config_home(
|
|
47
|
+
profile: ProviderProfile, home: Path, env: Mapping[str, str]
|
|
48
|
+
) -> dict[str, str]:
|
|
49
|
+
"""Make *home* a state root holding only the credentials, and point at it.
|
|
50
|
+
|
|
51
|
+
Links each of ``profile.config_home_files`` that exists in the state root
|
|
52
|
+
*env* selects into *home* as a symlink to that file's real path (replacing
|
|
53
|
+
whatever was there, such as a copy from an older agentshim), so a token
|
|
54
|
+
the CLI refreshes in either home is the one both read next. Returns the
|
|
55
|
+
environment overrides that make the CLI use *home*. A sandbox that runs
|
|
56
|
+
the CLI must expose the link's target read-write. Everything else in *home* (conversations, caches) is the
|
|
57
|
+
CLI's own and is left alone. A provider with no ``config_home_files``
|
|
58
|
+
isolates by flags alone: nothing is written and the result is empty.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
ValueError: *home* is not absolute, or is the state root *env*
|
|
62
|
+
already selects, which would isolate nothing.
|
|
63
|
+
"""
|
|
64
|
+
if not profile.config_home_files:
|
|
65
|
+
return {}
|
|
66
|
+
if not home.is_absolute():
|
|
67
|
+
msg = f"{profile.name} config home must be an absolute path, got {home}"
|
|
68
|
+
raise ValueError(msg)
|
|
69
|
+
assert profile.state_root_env is not None, "config_home_files needs a state_root_env" # noqa: S101 - profile invariant
|
|
70
|
+
source = state_root(profile, env)
|
|
71
|
+
if source is not None and real_path(source) == real_path(home):
|
|
72
|
+
msg = f"{profile.name} config home {home} is the user's own state root"
|
|
73
|
+
raise ValueError(msg)
|
|
74
|
+
home.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
for name in profile.config_home_files:
|
|
76
|
+
origin = None if source is None else source / name
|
|
77
|
+
if origin is not None and origin.is_file():
|
|
78
|
+
_link(home / name, real_path(origin))
|
|
79
|
+
return {profile.state_root_env: str(home)}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _link(link: Path, target: Path) -> None:
|
|
83
|
+
"""Point *link* at *target*, atomically replacing any file already there."""
|
|
84
|
+
if link.is_symlink() and link.readlink() == target:
|
|
85
|
+
return
|
|
86
|
+
link.parent.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
staged = link.with_name(f".{link.name}.{os.getpid()}.link")
|
|
88
|
+
staged.unlink(missing_ok=True)
|
|
89
|
+
staged.symlink_to(target)
|
|
90
|
+
staged.replace(link)
|
|
@@ -93,6 +93,26 @@ class McpScope(Enum):
|
|
|
93
93
|
SESSION = "session"
|
|
94
94
|
|
|
95
95
|
|
|
96
|
+
class ConfigScope(Enum):
|
|
97
|
+
"""Which of the user's own CLI configuration a session loads.
|
|
98
|
+
|
|
99
|
+
``ALL``: whatever the CLI loads by default: the user's settings, hooks,
|
|
100
|
+
global instructions (``~/.claude/CLAUDE.md``, ``$CODEX_HOME/AGENTS.md``),
|
|
101
|
+
notify commands, profiles and memory. ``PROJECT``: none of that; the
|
|
102
|
+
session sees the workspace's own instructions and settings plus what
|
|
103
|
+
agentshim passes for the turn, so the user running agentshim does not
|
|
104
|
+
change what the agent does. Credentials keep working. A provider that
|
|
105
|
+
lists a ``ProviderProfile.config_home_files`` can only isolate through a
|
|
106
|
+
dedicated home: ``prepare_config_home`` builds it and returns the
|
|
107
|
+
environment that points the CLI at it. A provider declares the scopes it
|
|
108
|
+
can enforce in ``ProviderProfile.config_scopes``; asking for another one
|
|
109
|
+
is an error, never a silent ``ALL``.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
ALL = "all"
|
|
113
|
+
PROJECT = "project"
|
|
114
|
+
|
|
115
|
+
|
|
96
116
|
def _no_container_env() -> Mapping[str, str]:
|
|
97
117
|
"""Empty read-only default: a frozen spec must not carry a mutable one.
|
|
98
118
|
|
|
@@ -145,3 +165,10 @@ class ProviderProfile:
|
|
|
145
165
|
skill_scopes: frozenset[SkillScope] = frozenset({SkillScope.ALL})
|
|
146
166
|
#: The ``McpScope`` values a session on this provider may request.
|
|
147
167
|
mcp_scopes: frozenset[McpScope] = frozenset({McpScope.ALL})
|
|
168
|
+
#: The ``ConfigScope`` values a session on this provider may request.
|
|
169
|
+
config_scopes: frozenset[ConfigScope] = frozenset({ConfigScope.ALL})
|
|
170
|
+
#: Files, relative to the state root (``state_dirs[0]``, or
|
|
171
|
+
#: ``$<state_root_env>``), that ``prepare_config_home`` copies into a
|
|
172
|
+
#: dedicated home for ``ConfigScope.PROJECT``. Empty when the provider
|
|
173
|
+
#: isolates by flags alone and needs no home.
|
|
174
|
+
config_home_files: tuple[str, ...] = ()
|
|
@@ -6,7 +6,7 @@ from dataclasses import dataclass, field
|
|
|
6
6
|
from typing import TYPE_CHECKING, Any, Protocol
|
|
7
7
|
|
|
8
8
|
from .errors import FailureKind
|
|
9
|
-
from .profile import McpScope, SkillScope
|
|
9
|
+
from .profile import ConfigScope, McpScope, SkillScope
|
|
10
10
|
from .usage import ProviderUsage
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
@@ -49,6 +49,9 @@ class ArgvContext:
|
|
|
49
49
|
#: A provider enforcing ``McpScope.SESSION`` renders them itself when its
|
|
50
50
|
#: installed config file is not enough to exclude the user's own.
|
|
51
51
|
mcp_servers: Sequence[McpServer] = ()
|
|
52
|
+
#: Which of the user's own CLI configuration the CLI may load; the
|
|
53
|
+
#: session has already checked that the provider's profile supports it.
|
|
54
|
+
config_scope: ConfigScope = ConfigScope.ALL
|
|
52
55
|
|
|
53
56
|
|
|
54
57
|
@dataclass(frozen=True)
|
|
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
8
8
|
from agentshim.core.errors import FailureKind, ProviderCapabilityError, SessionResumeError
|
|
9
9
|
from agentshim.core.mcp import HttpMcpServer, NoopInstallation, StdioMcpServer, install_config_file
|
|
10
10
|
from agentshim.core.profile import (
|
|
11
|
+
ConfigScope,
|
|
11
12
|
McpMechanism,
|
|
12
13
|
McpScope,
|
|
13
14
|
OutputSchemaStyle,
|
|
@@ -44,6 +45,14 @@ MCP_SERVER_KEY = "mcpServers"
|
|
|
44
45
|
#: account connectors are not user settings and stay.
|
|
45
46
|
PROJECT_SETTING_SOURCES = ("--setting-sources", "project,local")
|
|
46
47
|
|
|
48
|
+
#: ``ConfigScope.PROJECT``: the same ``--setting-sources`` drops the user's
|
|
49
|
+
#: settings (hooks, ``env``, permissions, default model), ``~/.claude/CLAUDE.md``
|
|
50
|
+
#: and plugins; auto-memory is not a setting source and is switched off in the
|
|
51
|
+
#: turn's ``--settings`` instead, so ``~/.claude/projects/*/memory`` is neither
|
|
52
|
+
#: read nor written. Built-in skills, the workspace's ``CLAUDE.md``,
|
|
53
|
+
#: ``.claude/`` settings and skills, and credentials keep working.
|
|
54
|
+
NO_AUTO_MEMORY_SETTINGS = {"autoMemoryEnabled": False}
|
|
55
|
+
|
|
47
56
|
#: ``McpScope.SESSION``: ``--strict-mcp-config`` makes ``--mcp-config`` the
|
|
48
57
|
#: only source of MCP servers. It drops the user's and the project's
|
|
49
58
|
#: (``.mcp.json``) servers, plugin servers and claude.ai account connectors,
|
|
@@ -75,6 +84,7 @@ PROFILE = ProviderProfile(
|
|
|
75
84
|
skill_invocation=SkillSignal.STRUCTURED,
|
|
76
85
|
skill_scopes=frozenset({SkillScope.ALL, SkillScope.PROJECT}),
|
|
77
86
|
mcp_scopes=frozenset({McpScope.ALL, McpScope.SESSION}),
|
|
87
|
+
config_scopes=frozenset({ConfigScope.ALL, ConfigScope.PROJECT}),
|
|
78
88
|
container_install=(
|
|
79
89
|
"apt-get update && apt-get install -y --no-install-recommends curl ca-certificates",
|
|
80
90
|
"curl -fsSL https://claude.ai/install.sh | bash",
|
|
@@ -138,7 +148,8 @@ class ClaudeProvider:
|
|
|
138
148
|
"stream-json",
|
|
139
149
|
"--verbose",
|
|
140
150
|
]
|
|
141
|
-
|
|
151
|
+
isolate_config = ctx.config_scope is ConfigScope.PROJECT
|
|
152
|
+
if ctx.skill_scope is SkillScope.PROJECT or isolate_config:
|
|
142
153
|
argv += PROJECT_SETTING_SOURCES
|
|
143
154
|
if ctx.model:
|
|
144
155
|
argv += ["--model", ctx.model]
|
|
@@ -147,6 +158,8 @@ class ClaudeProvider:
|
|
|
147
158
|
if ctx.schema_inline:
|
|
148
159
|
argv += ["--json-schema", ctx.schema_inline]
|
|
149
160
|
settings = build_settings(self.sandbox, hooks=self.hooks)
|
|
161
|
+
if isolate_config:
|
|
162
|
+
settings.update(NO_AUTO_MEMORY_SETTINGS)
|
|
150
163
|
if settings:
|
|
151
164
|
argv += ["--settings", json.dumps(settings)]
|
|
152
165
|
if ctx.mcp_scope is McpScope.SESSION:
|
|
@@ -7,8 +7,10 @@ import re
|
|
|
7
7
|
from typing import TYPE_CHECKING, Any
|
|
8
8
|
|
|
9
9
|
from agentshim.core.errors import ProviderCapabilityError, SessionResumeError
|
|
10
|
+
from agentshim.core.home import state_root
|
|
10
11
|
from agentshim.core.mcp import FlagsInstallation, HttpMcpServer, NoopInstallation
|
|
11
12
|
from agentshim.core.profile import (
|
|
13
|
+
ConfigScope,
|
|
12
14
|
McpMechanism,
|
|
13
15
|
McpScope,
|
|
14
16
|
OutputSchemaStyle,
|
|
@@ -85,6 +87,10 @@ PROFILE = ProviderProfile(
|
|
|
85
87
|
skill_invocation=SkillSignal.INFERRED,
|
|
86
88
|
skill_scopes=frozenset({SkillScope.ALL, SkillScope.PROJECT}),
|
|
87
89
|
mcp_scopes=frozenset({McpScope.ALL, McpScope.SESSION}),
|
|
90
|
+
config_scopes=frozenset({ConfigScope.ALL, ConfigScope.PROJECT}),
|
|
91
|
+
# Codex reads AGENTS.md, hooks.json and memories/ from $CODEX_HOME with no
|
|
92
|
+
# flag to skip them, so ConfigScope.PROJECT needs a home of its own.
|
|
93
|
+
config_home_files=("auth.json",),
|
|
88
94
|
container_install=(_NODE_INSTALL, _CODEX_INSTALL),
|
|
89
95
|
# Documented Codex CLI variable that relocates ~/.codex (``codex --help``:
|
|
90
96
|
# "Layer $CODEX_HOME/<name>.config.toml on top of the base user config";
|
|
@@ -105,6 +111,13 @@ BYPASS_FLAG = "--dangerously-bypass-approvals-and-sandbox"
|
|
|
105
111
|
#: Keeps user and project ``.rules`` files out of a sandboxed turn.
|
|
106
112
|
IGNORE_RULES_FLAG = "--ignore-rules"
|
|
107
113
|
|
|
114
|
+
#: ``ConfigScope.PROJECT``: ``$CODEX_HOME/config.toml`` (profiles, notify,
|
|
115
|
+
#: ``developer_instructions``, project trust) is not loaded, and the memories
|
|
116
|
+
#: Codex would otherwise write and reread across sessions in the home stay
|
|
117
|
+
#: off. The global ``AGENTS.md``, ``hooks.json`` and existing memories are
|
|
118
|
+
#: kept out by the dedicated home itself (``prepare_config_home``).
|
|
119
|
+
PROJECT_CONFIG_FLAGS = ("--ignore-user-config", "--disable", "memories")
|
|
120
|
+
|
|
108
121
|
|
|
109
122
|
class CodexProvider:
|
|
110
123
|
"""Codex (``codex exec --json``). ``sandbox`` is a provider option."""
|
|
@@ -136,6 +149,9 @@ class CodexProvider:
|
|
|
136
149
|
if ctx.model:
|
|
137
150
|
argv += ["--model", ctx.model]
|
|
138
151
|
argv += _shell_path_config(ctx.env)
|
|
152
|
+
if ctx.config_scope is ConfigScope.PROJECT:
|
|
153
|
+
_check_config_home(ctx)
|
|
154
|
+
argv += PROJECT_CONFIG_FLAGS
|
|
139
155
|
argv += _scope_overrides(ctx)
|
|
140
156
|
if ctx.reasoning_effort:
|
|
141
157
|
argv += ["--config", f"model_reasoning_effort={toml_str(ctx.reasoning_effort)}"]
|
|
@@ -250,6 +266,26 @@ def _writable_dirs(config: CodexSandboxConfig, ctx: ArgvContext) -> list[str]:
|
|
|
250
266
|
return dirs
|
|
251
267
|
|
|
252
268
|
|
|
269
|
+
def _check_config_home(ctx: ArgvContext) -> None:
|
|
270
|
+
"""Refuse ``ConfigScope.PROJECT`` unless the turn runs in a home of its own.
|
|
271
|
+
|
|
272
|
+
The user's global ``AGENTS.md``, hooks and memories live in the state root
|
|
273
|
+
and no flag skips them, so a turn still pointed at the user's root would
|
|
274
|
+
load them while claiming to be isolated.
|
|
275
|
+
"""
|
|
276
|
+
home = ctx.env.get("CODEX_HOME")
|
|
277
|
+
if not home or not os.path.isabs(home): # noqa: PTH117 - a str contract, not a Path
|
|
278
|
+
msg = (
|
|
279
|
+
"ConfigScope.PROJECT needs CODEX_HOME set to the absolute path of a "
|
|
280
|
+
f"dedicated home prepared with prepare_config_home (got {home!r})"
|
|
281
|
+
)
|
|
282
|
+
raise ProviderCapabilityError(msg)
|
|
283
|
+
user_root = state_root(PROFILE, {key: v for key, v in ctx.env.items() if key != "CODEX_HOME"})
|
|
284
|
+
if user_root is not None and os.path.realpath(home) == os.path.realpath(user_root):
|
|
285
|
+
msg = f"ConfigScope.PROJECT needs a dedicated CODEX_HOME, not the user's own {home}"
|
|
286
|
+
raise ProviderCapabilityError(msg)
|
|
287
|
+
|
|
288
|
+
|
|
253
289
|
def _is_within(path: str, directory: str) -> bool:
|
|
254
290
|
resolved, parent = os.path.realpath(path), os.path.realpath(directory)
|
|
255
291
|
return resolved == parent or resolved.startswith(parent.rstrip(os.sep) + os.sep)
|
|
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
|