agentshim 0.9.0__tar.gz → 0.10.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.9.0 → agentshim-0.10.0}/CHANGELOG.md +33 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/PKG-INFO +2 -1
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/__init__.py +3 -1
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/agent.py +32 -6
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/core/__init__.py +2 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/core/profile.py +18 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/core/provider.py +8 -1
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/claude/provider.py +19 -0
- agentshim-0.10.0/agentshim/providers/codex/mcp_scope.py +93 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/codex/provider.py +22 -2
- {agentshim-0.9.0 → agentshim-0.10.0}/pyproject.toml +4 -2
- {agentshim-0.9.0 → agentshim-0.10.0}/.gitignore +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/README.md +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/core/_files.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/core/env.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/core/errors.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/core/events.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/core/mcp.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/core/pricing.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/core/schema.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/core/skills.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/core/stream.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/core/turn.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/core/usage.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/execution/__init__.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/execution/executor.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/execution/host.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/execution/transform.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/__init__.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/claude/__init__.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/claude/events.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/claude/hooks/__init__.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/claude/hooks/confine_reads.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/claude/parser.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/claude/sandbox.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/claude/scripted.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/claude/user_hooks.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/codex/__init__.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/codex/_toml.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/codex/events.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/codex/parser.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/codex/rules.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/codex/sandbox.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/codex/scripted.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/codex/skills.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/copilot/__init__.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/copilot/events.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/copilot/parser.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/copilot/provider.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/copilot/scripted.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/gemini/__init__.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/gemini/events.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/gemini/parser.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/gemini/provider.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/gemini/scripted.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/opencode/__init__.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/opencode/events.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/opencode/parser.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/opencode/provider.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/opencode/scripted.py +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/py.typed +0 -0
- {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/testing/__init__.py +0 -0
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.10.0 (2026-10-03)
|
|
4
|
+
|
|
5
|
+
MCP isolation. Additive: the default scope keeps today's behaviour. Adds a
|
|
6
|
+
runtime dependency on `tomli` for Python 3.10 only (Codex config parsing).
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- `McpScope` (`ALL`, `SESSION`) and `start_session(mcp_scope=...)` /
|
|
11
|
+
`run(mcp_scope=...)`. `SESSION` connects the CLI only to the servers in
|
|
12
|
+
`TurnRequest.mcp_servers`; a turn given none sees no MCP server. It is a
|
|
13
|
+
separate option from `SkillScope` because a provider may enforce one and not
|
|
14
|
+
the other; each is refused independently.
|
|
15
|
+
- `ProviderProfile.mcp_scopes` declares the scopes a provider enforces;
|
|
16
|
+
asking for another raises `ProviderCapabilityError` from `start_session`.
|
|
17
|
+
- `ArgvContext.mcp_scope` and `ArgvContext.mcp_servers` carry the scope and
|
|
18
|
+
the turn's servers to `build_argv`.
|
|
19
|
+
- Claude Code: `SESSION` passes `--strict-mcp-config` plus the turn's servers
|
|
20
|
+
inline as `--mcp-config`. This drops user and project (`.mcp.json`) servers,
|
|
21
|
+
plugin servers and claude.ai account connectors.
|
|
22
|
+
- Codex: `SESSION` sets `enabled = false` on every `mcp_servers` entry found
|
|
23
|
+
in `$CODEX_HOME/config.toml`, `/etc/codex/config.toml` and
|
|
24
|
+
`<cwd>/.codex/config.toml` that the turn was not given, and sets
|
|
25
|
+
`features.plugins=false` and `features.apps=false` (plugin servers and the
|
|
26
|
+
account's ChatGPT apps).
|
|
27
|
+
- Gemini, opencode and Copilot support `ALL` only.
|
|
28
|
+
- Live e2e: a server seeded into the CLI's default configuration is reachable
|
|
29
|
+
under `ALL` and not under `SESSION` (`tests/e2e/test_mcp_scope_e2e.py`).
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- `parse_mcp_servers` ignores `enabled=false` overrides: they disable a
|
|
34
|
+
configured server and do not define one.
|
|
35
|
+
|
|
3
36
|
## 0.9.0 (2026-10-03)
|
|
4
37
|
|
|
5
38
|
Skill isolation. Additive: the default scope keeps today's behaviour.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agentshim
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: Provider-agnostic coding-agent CLI shims
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: tomli>=2; python_full_version < '3.11'
|
|
6
7
|
Provides-Extra: test
|
|
7
8
|
Requires-Dist: pytest>=8.0.0; extra == 'test'
|
|
8
9
|
Description-Content-Type: text/markdown
|
|
@@ -27,6 +27,7 @@ from .core import (
|
|
|
27
27
|
McpConfigError,
|
|
28
28
|
McpInstallation,
|
|
29
29
|
McpMechanism,
|
|
30
|
+
McpScope,
|
|
30
31
|
McpServer,
|
|
31
32
|
McpTransport,
|
|
32
33
|
ModelPricing,
|
|
@@ -97,7 +98,7 @@ from .providers.copilot import CopilotProvider
|
|
|
97
98
|
from .providers.gemini import GeminiProvider
|
|
98
99
|
from .providers.opencode import OpencodeProvider
|
|
99
100
|
|
|
100
|
-
__version__ = "0.
|
|
101
|
+
__version__ = "0.10.0"
|
|
101
102
|
|
|
102
103
|
__all__ = [
|
|
103
104
|
"AgentEvent",
|
|
@@ -134,6 +135,7 @@ __all__ = [
|
|
|
134
135
|
"McpConfigError",
|
|
135
136
|
"McpInstallation",
|
|
136
137
|
"McpMechanism",
|
|
138
|
+
"McpScope",
|
|
137
139
|
"McpServer",
|
|
138
140
|
"McpTransport",
|
|
139
141
|
"ModelPricing",
|
|
@@ -23,7 +23,13 @@ from agentshim.core.errors import (
|
|
|
23
23
|
SessionResumeError,
|
|
24
24
|
)
|
|
25
25
|
from agentshim.core.events import RunFinished, RunStarted, compose_event_handlers
|
|
26
|
-
from agentshim.core.profile import
|
|
26
|
+
from agentshim.core.profile import (
|
|
27
|
+
McpMechanism,
|
|
28
|
+
McpScope,
|
|
29
|
+
OutputSchemaStyle,
|
|
30
|
+
SchemaDialect,
|
|
31
|
+
SkillScope,
|
|
32
|
+
)
|
|
27
33
|
from agentshim.core.provider import ArgvContext
|
|
28
34
|
from agentshim.core.schema import compact_json, dialect_problems, materialize
|
|
29
35
|
from agentshim.core.skills import SkillTracker
|
|
@@ -115,13 +121,20 @@ class CliAgent:
|
|
|
115
121
|
timeout: float | None = None,
|
|
116
122
|
session_id: str | None = None,
|
|
117
123
|
skill_scope: SkillScope = SkillScope.ALL,
|
|
124
|
+
mcp_scope: McpScope = McpScope.ALL,
|
|
118
125
|
) -> AgentSession:
|
|
119
126
|
"""Open a conversation whose turns resume one another.
|
|
120
127
|
|
|
121
|
-
``skill_scope``
|
|
128
|
+
``skill_scope`` and ``mcp_scope`` are fixed for the conversation (see
|
|
129
|
+
``AgentSession``).
|
|
122
130
|
"""
|
|
123
131
|
return AgentSession(
|
|
124
|
-
self,
|
|
132
|
+
self,
|
|
133
|
+
cwd=cwd,
|
|
134
|
+
timeout=timeout,
|
|
135
|
+
session_id=session_id,
|
|
136
|
+
skill_scope=skill_scope,
|
|
137
|
+
mcp_scope=mcp_scope,
|
|
125
138
|
)
|
|
126
139
|
|
|
127
140
|
def run(
|
|
@@ -131,9 +144,12 @@ class CliAgent:
|
|
|
131
144
|
cwd: str | None = None,
|
|
132
145
|
timeout: float | None = None,
|
|
133
146
|
skill_scope: SkillScope = SkillScope.ALL,
|
|
147
|
+
mcp_scope: McpScope = McpScope.ALL,
|
|
134
148
|
) -> TurnResult:
|
|
135
149
|
"""Run one turn in a throwaway session."""
|
|
136
|
-
session = self.start_session(
|
|
150
|
+
session = self.start_session(
|
|
151
|
+
cwd=cwd, timeout=timeout, skill_scope=skill_scope, mcp_scope=mcp_scope
|
|
152
|
+
)
|
|
137
153
|
return session.turn(request)
|
|
138
154
|
|
|
139
155
|
|
|
@@ -144,7 +160,7 @@ def _discard(message: str) -> None:
|
|
|
144
160
|
class AgentSession:
|
|
145
161
|
"""A provider conversation. One turn at a time; ``cancel`` is thread-safe."""
|
|
146
162
|
|
|
147
|
-
def __init__(
|
|
163
|
+
def __init__( # noqa: PLR0913 - one keyword per independent session option
|
|
148
164
|
self,
|
|
149
165
|
agent: CliAgent,
|
|
150
166
|
*,
|
|
@@ -152,6 +168,7 @@ class AgentSession:
|
|
|
152
168
|
timeout: float | None = None,
|
|
153
169
|
session_id: str | None = None,
|
|
154
170
|
skill_scope: SkillScope = SkillScope.ALL,
|
|
171
|
+
mcp_scope: McpScope = McpScope.ALL,
|
|
155
172
|
) -> None:
|
|
156
173
|
"""Bind a conversation to one agent, with per-turn defaults.
|
|
157
174
|
|
|
@@ -162,12 +179,19 @@ class AgentSession:
|
|
|
162
179
|
first turn resumes rather than starts fresh. ``skill_scope`` limits
|
|
163
180
|
which skills every turn's CLI may discover; a scope the provider's
|
|
164
181
|
``profile.skill_scopes`` does not list raises
|
|
165
|
-
``ProviderCapabilityError`` here, before any turn runs.
|
|
182
|
+
``ProviderCapabilityError`` here, before any turn runs. ``mcp_scope``
|
|
183
|
+
limits which MCP servers every turn's CLI may connect to
|
|
184
|
+
(``McpScope.SESSION``: only those in ``TurnRequest.mcp_servers``) and
|
|
185
|
+
is checked against ``profile.mcp_scopes`` the same way.
|
|
166
186
|
"""
|
|
167
187
|
if skill_scope not in agent.profile.skill_scopes:
|
|
168
188
|
msg = f"{agent.profile.name} cannot limit skills to scope {skill_scope.value!r}"
|
|
169
189
|
raise ProviderCapabilityError(msg)
|
|
190
|
+
if mcp_scope not in agent.profile.mcp_scopes:
|
|
191
|
+
msg = f"{agent.profile.name} cannot limit MCP servers to scope {mcp_scope.value!r}"
|
|
192
|
+
raise ProviderCapabilityError(msg)
|
|
170
193
|
self.skill_scope = skill_scope
|
|
194
|
+
self.mcp_scope = mcp_scope
|
|
171
195
|
self._agent = agent
|
|
172
196
|
self._cwd = cwd
|
|
173
197
|
self._timeout = timeout
|
|
@@ -277,6 +301,8 @@ class AgentSession:
|
|
|
277
301
|
extra_args=req.extra_args,
|
|
278
302
|
cwd=cwd,
|
|
279
303
|
skill_scope=self.skill_scope,
|
|
304
|
+
mcp_scope=self.mcp_scope,
|
|
305
|
+
mcp_servers=tuple(req.mcp_servers),
|
|
280
306
|
)
|
|
281
307
|
)
|
|
282
308
|
command = CommandRequest(argv=argv, stdin=req.prompt, cwd=cwd, env=env, timeout=timeout)
|
|
@@ -55,6 +55,7 @@ from .mcp import (
|
|
|
55
55
|
from .pricing import ModelPricing, PricingTable, cost_usd, default_pricing, price_for
|
|
56
56
|
from .profile import (
|
|
57
57
|
McpMechanism,
|
|
58
|
+
McpScope,
|
|
58
59
|
OutputSchemaStyle,
|
|
59
60
|
ProviderProfile,
|
|
60
61
|
SchemaDialect,
|
|
@@ -88,6 +89,7 @@ __all__ = [
|
|
|
88
89
|
"McpConfigError",
|
|
89
90
|
"McpInstallation",
|
|
90
91
|
"McpMechanism",
|
|
92
|
+
"McpScope",
|
|
91
93
|
"McpServer",
|
|
92
94
|
"McpTransport",
|
|
93
95
|
"ModelPricing",
|
|
@@ -77,6 +77,22 @@ class SkillScope(Enum):
|
|
|
77
77
|
PROJECT = "project"
|
|
78
78
|
|
|
79
79
|
|
|
80
|
+
class McpScope(Enum):
|
|
81
|
+
"""Which MCP servers a session's CLI may connect to.
|
|
82
|
+
|
|
83
|
+
``ALL``: whatever the CLI finds by default, including the user's and the
|
|
84
|
+
project's own MCP configuration, plugin servers and account-level
|
|
85
|
+
connectors. ``SESSION``: only the servers the session was given through
|
|
86
|
+
``TurnRequest.mcp_servers``; a session given none sees no MCP server at
|
|
87
|
+
all. A provider declares the scopes it can enforce in
|
|
88
|
+
``ProviderProfile.mcp_scopes``; asking for another one is an error, never
|
|
89
|
+
a silent ``ALL``.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
ALL = "all"
|
|
93
|
+
SESSION = "session"
|
|
94
|
+
|
|
95
|
+
|
|
80
96
|
def _no_container_env() -> Mapping[str, str]:
|
|
81
97
|
"""Empty read-only default: a frozen spec must not carry a mutable one.
|
|
82
98
|
|
|
@@ -127,3 +143,5 @@ class ProviderProfile:
|
|
|
127
143
|
skill_invocation: SkillSignal = SkillSignal.NONE
|
|
128
144
|
#: The ``SkillScope`` values a session on this provider may request.
|
|
129
145
|
skill_scopes: frozenset[SkillScope] = frozenset({SkillScope.ALL})
|
|
146
|
+
#: The ``McpScope`` values a session on this provider may request.
|
|
147
|
+
mcp_scopes: frozenset[McpScope] = frozenset({McpScope.ALL})
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
6
|
from typing import TYPE_CHECKING, Any, Protocol
|
|
7
7
|
|
|
8
|
-
from .profile import SkillScope
|
|
8
|
+
from .profile import McpScope, SkillScope
|
|
9
9
|
from .usage import ProviderUsage
|
|
10
10
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
@@ -41,6 +41,13 @@ class ArgvContext:
|
|
|
41
41
|
#: Which skills the CLI may discover; the session has already checked
|
|
42
42
|
#: that the provider's profile supports it.
|
|
43
43
|
skill_scope: SkillScope = SkillScope.ALL
|
|
44
|
+
#: Which MCP servers the CLI may connect to; the session has already
|
|
45
|
+
#: checked that the provider's profile supports it.
|
|
46
|
+
mcp_scope: McpScope = McpScope.ALL
|
|
47
|
+
#: The servers the turn was given (the ones ``install_mcp`` installed).
|
|
48
|
+
#: A provider enforcing ``McpScope.SESSION`` renders them itself when its
|
|
49
|
+
#: installed config file is not enough to exclude the user's own.
|
|
50
|
+
mcp_servers: Sequence[McpServer] = ()
|
|
44
51
|
|
|
45
52
|
|
|
46
53
|
@dataclass(frozen=True)
|
|
@@ -9,6 +9,7 @@ from agentshim.core.errors import ProviderCapabilityError, SessionResumeError
|
|
|
9
9
|
from agentshim.core.mcp import HttpMcpServer, NoopInstallation, StdioMcpServer, install_config_file
|
|
10
10
|
from agentshim.core.profile import (
|
|
11
11
|
McpMechanism,
|
|
12
|
+
McpScope,
|
|
12
13
|
OutputSchemaStyle,
|
|
13
14
|
ProviderProfile,
|
|
14
15
|
SchemaDialect,
|
|
@@ -43,6 +44,12 @@ MCP_SERVER_KEY = "mcpServers"
|
|
|
43
44
|
#: account connectors are not user settings and stay.
|
|
44
45
|
PROJECT_SETTING_SOURCES = ("--setting-sources", "project,local")
|
|
45
46
|
|
|
47
|
+
#: ``McpScope.SESSION``: ``--strict-mcp-config`` makes ``--mcp-config`` the
|
|
48
|
+
#: only source of MCP servers. It drops the user's and the project's
|
|
49
|
+
#: (``.mcp.json``) servers, plugin servers and claude.ai account connectors,
|
|
50
|
+
#: so the servers the turn was given are passed inline as ``--mcp-config``.
|
|
51
|
+
STRICT_MCP_FLAG = "--strict-mcp-config"
|
|
52
|
+
|
|
46
53
|
PROFILE = ProviderProfile(
|
|
47
54
|
name="claude",
|
|
48
55
|
display_name="Claude Code",
|
|
@@ -67,6 +74,7 @@ PROFILE = ProviderProfile(
|
|
|
67
74
|
skill_discovery=SkillSignal.STRUCTURED,
|
|
68
75
|
skill_invocation=SkillSignal.STRUCTURED,
|
|
69
76
|
skill_scopes=frozenset({SkillScope.ALL, SkillScope.PROJECT}),
|
|
77
|
+
mcp_scopes=frozenset({McpScope.ALL, McpScope.SESSION}),
|
|
70
78
|
container_install=(
|
|
71
79
|
"apt-get update && apt-get install -y --no-install-recommends curl ca-certificates",
|
|
72
80
|
"curl -fsSL https://claude.ai/install.sh | bash",
|
|
@@ -141,6 +149,8 @@ class ClaudeProvider:
|
|
|
141
149
|
settings = build_settings(self.sandbox, hooks=self.hooks)
|
|
142
150
|
if settings:
|
|
143
151
|
argv += ["--settings", json.dumps(settings)]
|
|
152
|
+
if ctx.mcp_scope is McpScope.SESSION:
|
|
153
|
+
argv += _session_mcp_argv(ctx.mcp_servers)
|
|
144
154
|
argv += list(ctx.mcp_argv)
|
|
145
155
|
argv += list(ctx.extra_args)
|
|
146
156
|
return argv
|
|
@@ -193,6 +203,15 @@ def _resumed_session_id(argv: Sequence[str]) -> str | None:
|
|
|
193
203
|
return args[index + 1]
|
|
194
204
|
|
|
195
205
|
|
|
206
|
+
def _session_mcp_argv(servers: Sequence[McpServer]) -> list[str]:
|
|
207
|
+
"""Flags that leave Claude Code exactly *servers* and nothing else."""
|
|
208
|
+
argv = [STRICT_MCP_FLAG]
|
|
209
|
+
if servers:
|
|
210
|
+
config = {MCP_SERVER_KEY: {server.name: mcp_entry(server) for server in servers}}
|
|
211
|
+
argv += ["--mcp-config", json.dumps(config)]
|
|
212
|
+
return argv
|
|
213
|
+
|
|
214
|
+
|
|
196
215
|
def mcp_entry(server: McpServer) -> dict[str, Any]:
|
|
197
216
|
"""Render one server the way ``.mcp.json`` describes it."""
|
|
198
217
|
if server.startup_timeout_s is not None:
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Codex MCP servers: hide every one the session was not given.
|
|
2
|
+
|
|
3
|
+
Codex merges MCP servers from several layers: ``$CODEX_HOME/config.toml``,
|
|
4
|
+
system config, a trusted workspace's ``.codex/config.toml``, installed
|
|
5
|
+
plugins, and the account's ChatGPT apps (the ``codex_apps`` server). A
|
|
6
|
+
``--config mcp_servers={}`` override does not replace the merged table, so
|
|
7
|
+
each configured server is switched off by name (``enabled = false``), and
|
|
8
|
+
plugins and apps are switched off as features.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
16
|
+
|
|
17
|
+
from ._toml import toml_str
|
|
18
|
+
|
|
19
|
+
if sys.version_info >= (3, 11):
|
|
20
|
+
import tomllib
|
|
21
|
+
else: # pragma: no cover - exercised on the 3.10 CI leg
|
|
22
|
+
import tomli as tomllib # pyright: ignore[reportMissingImports]
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from collections.abc import Collection, Iterable, Mapping
|
|
26
|
+
|
|
27
|
+
_SYSTEM_CONFIG = "/etc/codex/config.toml"
|
|
28
|
+
_BARE_KEY = frozenset("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-")
|
|
29
|
+
|
|
30
|
+
#: Features that bring in MCP servers on their own: plugins (each may bundle
|
|
31
|
+
#: servers) and apps (the account's ChatGPT connectors).
|
|
32
|
+
_MCP_FEATURES = ("plugins", "apps")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def configured_server_keys(env: Mapping[str, str], cwd: str | None) -> list[str]:
|
|
36
|
+
"""Every ``mcp_servers`` key the config files Codex would read define.
|
|
37
|
+
|
|
38
|
+
Reads the user's ``$CODEX_HOME/config.toml`` (default ``~/.codex``), the
|
|
39
|
+
system config, and ``<cwd>/.codex/config.toml``. A file that is missing or
|
|
40
|
+
not valid TOML contributes nothing (Codex reports a broken config itself).
|
|
41
|
+
The scan runs where agentshim runs, so a CLI in a container is only
|
|
42
|
+
covered when the container shares these files.
|
|
43
|
+
"""
|
|
44
|
+
home = env.get("HOME") or os.path.expanduser("~") # noqa: PTH111 - a str contract, not a Path
|
|
45
|
+
codex_home = env.get("CODEX_HOME") or os.path.join(home, ".codex") # noqa: PTH118
|
|
46
|
+
paths = [os.path.join(codex_home, "config.toml"), _SYSTEM_CONFIG] # noqa: PTH118
|
|
47
|
+
if cwd is not None:
|
|
48
|
+
paths.append(os.path.join(cwd, ".codex", "config.toml")) # noqa: PTH118
|
|
49
|
+
keys: list[str] = []
|
|
50
|
+
for path in paths:
|
|
51
|
+
keys += _server_keys(path)
|
|
52
|
+
return list(dict.fromkeys(keys))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _server_keys(path: str) -> Iterable[str]:
|
|
56
|
+
config = _read_toml(path)
|
|
57
|
+
servers: object = config.get("mcp_servers")
|
|
58
|
+
if not isinstance(servers, dict):
|
|
59
|
+
return []
|
|
60
|
+
return [str(key) for key in cast("dict[object, object]", servers)]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _read_toml(path: str) -> dict[str, Any]:
|
|
64
|
+
try:
|
|
65
|
+
with open(path, "rb") as handle: # noqa: PTH123 - a str contract, not a Path
|
|
66
|
+
# pyright analyzes the 3.10 leg, where the optional ``tomli`` has no
|
|
67
|
+
# stubs in the 3.12 environment: the types are known at runtime.
|
|
68
|
+
return tomllib.load(handle) # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
|
|
69
|
+
except (OSError, tomllib.TOMLDecodeError): # pyright: ignore[reportUnknownMemberType]
|
|
70
|
+
return {}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _table_key(key: str) -> str:
|
|
74
|
+
return key if key and set(key) <= _BARE_KEY else toml_str(key)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def session_scope_overrides(
|
|
78
|
+
env: Mapping[str, str], cwd: str | None, given_keys: Collection[str]
|
|
79
|
+
) -> list[str]:
|
|
80
|
+
"""``--config`` flags that leave Codex only the servers in *given_keys*.
|
|
81
|
+
|
|
82
|
+
*given_keys* are the ``mcp_servers`` keys the turn's own overrides define;
|
|
83
|
+
they are not disabled. A given server that shares a key with a configured
|
|
84
|
+
one is merged field by field with it by Codex, so give session servers
|
|
85
|
+
names the user's config does not use.
|
|
86
|
+
"""
|
|
87
|
+
flags: list[str] = []
|
|
88
|
+
for key in configured_server_keys(env, cwd):
|
|
89
|
+
if key not in given_keys:
|
|
90
|
+
flags += ["--config", f"mcp_servers.{_table_key(key)}.enabled=false"]
|
|
91
|
+
for feature in _MCP_FEATURES:
|
|
92
|
+
flags += ["--config", f"features.{feature}=false"]
|
|
93
|
+
return flags
|
|
@@ -10,6 +10,7 @@ from agentshim.core.errors import ProviderCapabilityError, SessionResumeError
|
|
|
10
10
|
from agentshim.core.mcp import FlagsInstallation, HttpMcpServer, NoopInstallation
|
|
11
11
|
from agentshim.core.profile import (
|
|
12
12
|
McpMechanism,
|
|
13
|
+
McpScope,
|
|
13
14
|
OutputSchemaStyle,
|
|
14
15
|
ProviderProfile,
|
|
15
16
|
SchemaDialect,
|
|
@@ -18,6 +19,7 @@ from agentshim.core.profile import (
|
|
|
18
19
|
)
|
|
19
20
|
|
|
20
21
|
from ._toml import toml_array, toml_str, unescape_toml
|
|
22
|
+
from .mcp_scope import session_scope_overrides
|
|
21
23
|
from .parser import CodexStreamParser
|
|
22
24
|
from .rules import RULES_FILENAME
|
|
23
25
|
from .sandbox import CodexSandboxConfig, resolve_sandbox, sandbox_overrides
|
|
@@ -82,6 +84,7 @@ PROFILE = ProviderProfile(
|
|
|
82
84
|
# of a ``SKILL.md`` (``skills.py``).
|
|
83
85
|
skill_invocation=SkillSignal.INFERRED,
|
|
84
86
|
skill_scopes=frozenset({SkillScope.ALL, SkillScope.PROJECT}),
|
|
87
|
+
mcp_scopes=frozenset({McpScope.ALL, McpScope.SESSION}),
|
|
85
88
|
container_install=(_NODE_INSTALL, _CODEX_INSTALL),
|
|
86
89
|
# Documented Codex CLI variable that relocates ~/.codex (``codex --help``:
|
|
87
90
|
# "Layer $CODEX_HOME/<name>.config.toml on top of the base user config";
|
|
@@ -133,8 +136,7 @@ class CodexProvider:
|
|
|
133
136
|
if ctx.model:
|
|
134
137
|
argv += ["--model", ctx.model]
|
|
135
138
|
argv += _shell_path_config(ctx.env)
|
|
136
|
-
|
|
137
|
-
argv += project_scope_overrides(ctx.env)
|
|
139
|
+
argv += _scope_overrides(ctx)
|
|
138
140
|
if ctx.reasoning_effort:
|
|
139
141
|
argv += ["--config", f"model_reasoning_effort={toml_str(ctx.reasoning_effort)}"]
|
|
140
142
|
argv += list(ctx.mcp_argv)
|
|
@@ -270,6 +272,21 @@ def _shell_path_config(env: Mapping[str, str]) -> list[str]:
|
|
|
270
272
|
return ["--config", f"shell_environment_policy.set.PATH={toml_str(path)}"]
|
|
271
273
|
|
|
272
274
|
|
|
275
|
+
def _scope_overrides(ctx: ArgvContext) -> list[str]:
|
|
276
|
+
"""The ``--config`` flags the session's skill and MCP scopes call for.
|
|
277
|
+
|
|
278
|
+
Both scopes switch ``features.plugins`` off; the flag is emitted once.
|
|
279
|
+
"""
|
|
280
|
+
flags: list[str] = []
|
|
281
|
+
if ctx.skill_scope is SkillScope.PROJECT:
|
|
282
|
+
flags += project_scope_overrides(ctx.env)
|
|
283
|
+
if ctx.mcp_scope is McpScope.SESSION:
|
|
284
|
+
given = {server.name.replace("-", "_") for server in ctx.mcp_servers}
|
|
285
|
+
flags += session_scope_overrides(ctx.env, ctx.cwd, given)
|
|
286
|
+
pairs = [flags[i : i + 2] for i in range(0, len(flags), 2)]
|
|
287
|
+
return [item for pair in dict.fromkeys(map(tuple, pairs)) for item in pair]
|
|
288
|
+
|
|
289
|
+
|
|
273
290
|
def _server_flags(server: McpServer) -> list[str]:
|
|
274
291
|
"""Render one MCP server as dotted-path TOML overrides.
|
|
275
292
|
|
|
@@ -338,6 +355,9 @@ def parse_mcp_servers(argv: Sequence[str]) -> dict[str, dict[str, Any]]:
|
|
|
338
355
|
servers: dict[str, dict[str, Any]] = {}
|
|
339
356
|
for key, field, raw_value in _mcp_config_overrides(argv):
|
|
340
357
|
_apply_mcp_override(servers.setdefault(key, {}), field, raw_value)
|
|
358
|
+
# ``McpScope.SESSION`` also emits ``enabled=false`` overrides for servers
|
|
359
|
+
# the user configured; those define no server, so they are not reported.
|
|
360
|
+
servers = {key: entry for key, entry in servers.items() if "command" in entry or "url" in entry}
|
|
341
361
|
for entry in servers.values():
|
|
342
362
|
if "url" not in entry:
|
|
343
363
|
entry.setdefault("args", [])
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agentshim"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.10.0"
|
|
4
4
|
description = "Provider-agnostic coding-agent CLI shims"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
7
|
-
dependencies = [
|
|
7
|
+
dependencies = [
|
|
8
|
+
"tomli>=2 ; python_full_version < '3.11'",
|
|
9
|
+
]
|
|
8
10
|
|
|
9
11
|
[project.optional-dependencies]
|
|
10
12
|
test = [
|
|
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
|