agentshim 0.8.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.8.0 → agentshim-0.10.0}/CHANGELOG.md +54 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/PKG-INFO +2 -1
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/__init__.py +5 -1
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/agent.py +49 -6
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/core/__init__.py +4 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/core/profile.py +36 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/core/provider.py +11 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/claude/provider.py +34 -0
- agentshim-0.10.0/agentshim/providers/codex/mcp_scope.py +93 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/codex/provider.py +25 -0
- agentshim-0.10.0/agentshim/providers/codex/skills.py +86 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/pyproject.toml +4 -2
- agentshim-0.8.0/agentshim/providers/codex/skills.py +0 -27
- {agentshim-0.8.0 → agentshim-0.10.0}/.gitignore +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/README.md +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/core/_files.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/core/env.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/core/errors.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/core/events.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/core/mcp.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/core/pricing.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/core/schema.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/core/skills.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/core/stream.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/core/turn.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/core/usage.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/execution/__init__.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/execution/executor.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/execution/host.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/execution/transform.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/__init__.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/claude/__init__.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/claude/events.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/claude/hooks/__init__.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/claude/hooks/confine_reads.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/claude/parser.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/claude/sandbox.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/claude/scripted.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/claude/user_hooks.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/codex/__init__.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/codex/_toml.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/codex/events.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/codex/parser.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/codex/rules.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/codex/sandbox.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/codex/scripted.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/copilot/__init__.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/copilot/events.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/copilot/parser.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/copilot/provider.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/copilot/scripted.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/gemini/__init__.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/gemini/events.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/gemini/parser.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/gemini/provider.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/gemini/scripted.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/opencode/__init__.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/opencode/events.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/opencode/parser.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/opencode/provider.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/providers/opencode/scripted.py +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/py.typed +0 -0
- {agentshim-0.8.0 → agentshim-0.10.0}/agentshim/testing/__init__.py +0 -0
|
@@ -1,5 +1,59 @@
|
|
|
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
|
+
|
|
36
|
+
## 0.9.0 (2026-10-03)
|
|
37
|
+
|
|
38
|
+
Skill isolation. Additive: the default scope keeps today's behaviour.
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- `SkillScope` (`ALL`, `PROJECT`) and `start_session(skill_scope=...)` /
|
|
43
|
+
`run(skill_scope=...)`. `PROJECT` offers only the workspace's skills and
|
|
44
|
+
the CLI's built-in ones, not the user's personal skills or plugins.
|
|
45
|
+
- `ProviderProfile.skill_scopes` declares the scopes a provider enforces;
|
|
46
|
+
asking for another raises `ProviderCapabilityError` from `start_session`.
|
|
47
|
+
- `ArgvContext.skill_scope` carries the session's scope to `build_argv`.
|
|
48
|
+
- Claude Code: `PROJECT` passes `--setting-sources project,local`, which also
|
|
49
|
+
skips user settings and `~/.claude/CLAUDE.md`; credentials still work.
|
|
50
|
+
- Codex: `PROJECT` sets `features.plugins=false` and disables every user
|
|
51
|
+
`SKILL.md` (`$CODEX_HOME/skills` except `.system`, `~/.agents/skills`)
|
|
52
|
+
through `skills.config`.
|
|
53
|
+
- Gemini, opencode and Copilot support `ALL` only.
|
|
54
|
+
- Live e2e: `SkillScope.PROJECT` hides a skill seeded in a relocated user
|
|
55
|
+
home while the workspace skill stays offered (`tests/e2e/test_skills_e2e.py`).
|
|
56
|
+
|
|
3
57
|
## 0.8.0 (2026-10-03)
|
|
4
58
|
|
|
5
59
|
Skill observability. Additive: a caller that ignores the new events and the
|
|
@@ -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,
|
|
@@ -50,6 +51,7 @@ from .core import (
|
|
|
50
51
|
SessionResumeError,
|
|
51
52
|
SessionStarted,
|
|
52
53
|
SkillInvoked,
|
|
54
|
+
SkillScope,
|
|
53
55
|
SkillsDiscovered,
|
|
54
56
|
SkillSignal,
|
|
55
57
|
SkillSummary,
|
|
@@ -96,7 +98,7 @@ from .providers.copilot import CopilotProvider
|
|
|
96
98
|
from .providers.gemini import GeminiProvider
|
|
97
99
|
from .providers.opencode import OpencodeProvider
|
|
98
100
|
|
|
99
|
-
__version__ = "0.
|
|
101
|
+
__version__ = "0.10.0"
|
|
100
102
|
|
|
101
103
|
__all__ = [
|
|
102
104
|
"AgentEvent",
|
|
@@ -133,6 +135,7 @@ __all__ = [
|
|
|
133
135
|
"McpConfigError",
|
|
134
136
|
"McpInstallation",
|
|
135
137
|
"McpMechanism",
|
|
138
|
+
"McpScope",
|
|
136
139
|
"McpServer",
|
|
137
140
|
"McpTransport",
|
|
138
141
|
"ModelPricing",
|
|
@@ -159,6 +162,7 @@ __all__ = [
|
|
|
159
162
|
"SessionResumeError",
|
|
160
163
|
"SessionStarted",
|
|
161
164
|
"SkillInvoked",
|
|
165
|
+
"SkillScope",
|
|
162
166
|
"SkillSignal",
|
|
163
167
|
"SkillSummary",
|
|
164
168
|
"SkillTracker",
|
|
@@ -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
|
|
@@ -114,9 +120,22 @@ class CliAgent:
|
|
|
114
120
|
cwd: str | None = None,
|
|
115
121
|
timeout: float | None = None,
|
|
116
122
|
session_id: str | None = None,
|
|
123
|
+
skill_scope: SkillScope = SkillScope.ALL,
|
|
124
|
+
mcp_scope: McpScope = McpScope.ALL,
|
|
117
125
|
) -> AgentSession:
|
|
118
|
-
"""Open a conversation whose turns resume one another.
|
|
119
|
-
|
|
126
|
+
"""Open a conversation whose turns resume one another.
|
|
127
|
+
|
|
128
|
+
``skill_scope`` and ``mcp_scope`` are fixed for the conversation (see
|
|
129
|
+
``AgentSession``).
|
|
130
|
+
"""
|
|
131
|
+
return AgentSession(
|
|
132
|
+
self,
|
|
133
|
+
cwd=cwd,
|
|
134
|
+
timeout=timeout,
|
|
135
|
+
session_id=session_id,
|
|
136
|
+
skill_scope=skill_scope,
|
|
137
|
+
mcp_scope=mcp_scope,
|
|
138
|
+
)
|
|
120
139
|
|
|
121
140
|
def run(
|
|
122
141
|
self,
|
|
@@ -124,9 +143,14 @@ class CliAgent:
|
|
|
124
143
|
*,
|
|
125
144
|
cwd: str | None = None,
|
|
126
145
|
timeout: float | None = None,
|
|
146
|
+
skill_scope: SkillScope = SkillScope.ALL,
|
|
147
|
+
mcp_scope: McpScope = McpScope.ALL,
|
|
127
148
|
) -> TurnResult:
|
|
128
149
|
"""Run one turn in a throwaway session."""
|
|
129
|
-
|
|
150
|
+
session = self.start_session(
|
|
151
|
+
cwd=cwd, timeout=timeout, skill_scope=skill_scope, mcp_scope=mcp_scope
|
|
152
|
+
)
|
|
153
|
+
return session.turn(request)
|
|
130
154
|
|
|
131
155
|
|
|
132
156
|
def _discard(message: str) -> None:
|
|
@@ -136,13 +160,15 @@ def _discard(message: str) -> None:
|
|
|
136
160
|
class AgentSession:
|
|
137
161
|
"""A provider conversation. One turn at a time; ``cancel`` is thread-safe."""
|
|
138
162
|
|
|
139
|
-
def __init__(
|
|
163
|
+
def __init__( # noqa: PLR0913 - one keyword per independent session option
|
|
140
164
|
self,
|
|
141
165
|
agent: CliAgent,
|
|
142
166
|
*,
|
|
143
167
|
cwd: str | None = None,
|
|
144
168
|
timeout: float | None = None,
|
|
145
169
|
session_id: str | None = None,
|
|
170
|
+
skill_scope: SkillScope = SkillScope.ALL,
|
|
171
|
+
mcp_scope: McpScope = McpScope.ALL,
|
|
146
172
|
) -> None:
|
|
147
173
|
"""Bind a conversation to one agent, with per-turn defaults.
|
|
148
174
|
|
|
@@ -150,8 +176,22 @@ class AgentSession:
|
|
|
150
176
|
successive turns resume one another. ``cwd`` and ``timeout`` are
|
|
151
177
|
defaults an individual ``TurnRequest`` may override. Passing
|
|
152
178
|
``session_id`` adopts a conversation the provider already has, so the
|
|
153
|
-
first turn resumes rather than starts fresh.
|
|
179
|
+
first turn resumes rather than starts fresh. ``skill_scope`` limits
|
|
180
|
+
which skills every turn's CLI may discover; a scope the provider's
|
|
181
|
+
``profile.skill_scopes`` does not list raises
|
|
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.
|
|
154
186
|
"""
|
|
187
|
+
if skill_scope not in agent.profile.skill_scopes:
|
|
188
|
+
msg = f"{agent.profile.name} cannot limit skills to scope {skill_scope.value!r}"
|
|
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)
|
|
193
|
+
self.skill_scope = skill_scope
|
|
194
|
+
self.mcp_scope = mcp_scope
|
|
155
195
|
self._agent = agent
|
|
156
196
|
self._cwd = cwd
|
|
157
197
|
self._timeout = timeout
|
|
@@ -260,6 +300,9 @@ class AgentSession:
|
|
|
260
300
|
mcp_argv=installation.argv,
|
|
261
301
|
extra_args=req.extra_args,
|
|
262
302
|
cwd=cwd,
|
|
303
|
+
skill_scope=self.skill_scope,
|
|
304
|
+
mcp_scope=self.mcp_scope,
|
|
305
|
+
mcp_servers=tuple(req.mcp_servers),
|
|
263
306
|
)
|
|
264
307
|
)
|
|
265
308
|
command = CommandRequest(argv=argv, stdin=req.prompt, cwd=cwd, env=env, timeout=timeout)
|
|
@@ -55,9 +55,11 @@ 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,
|
|
62
|
+
SkillScope,
|
|
61
63
|
SkillSignal,
|
|
62
64
|
)
|
|
63
65
|
from .provider import ArgvContext, McpInstallation, ParsedTurn, Provider, StreamParser
|
|
@@ -87,6 +89,7 @@ __all__ = [
|
|
|
87
89
|
"McpConfigError",
|
|
88
90
|
"McpInstallation",
|
|
89
91
|
"McpMechanism",
|
|
92
|
+
"McpScope",
|
|
90
93
|
"McpServer",
|
|
91
94
|
"McpTransport",
|
|
92
95
|
"ModelPricing",
|
|
@@ -110,6 +113,7 @@ __all__ = [
|
|
|
110
113
|
"SessionResumeError",
|
|
111
114
|
"SessionStarted",
|
|
112
115
|
"SkillInvoked",
|
|
116
|
+
"SkillScope",
|
|
113
117
|
"SkillSignal",
|
|
114
118
|
"SkillSummary",
|
|
115
119
|
"SkillTracker",
|
|
@@ -61,6 +61,38 @@ class SkillSignal(Enum):
|
|
|
61
61
|
INFERRED = "inferred"
|
|
62
62
|
|
|
63
63
|
|
|
64
|
+
class SkillScope(Enum):
|
|
65
|
+
"""Which skills a session's CLI may discover.
|
|
66
|
+
|
|
67
|
+
``ALL``: whatever the CLI finds by default, including the user's own
|
|
68
|
+
skills and installed plugins. ``PROJECT``: only the skills under the
|
|
69
|
+
session's working directory (the profile's ``skill_dirs``) plus the
|
|
70
|
+
CLI's built-in ones, so the user running agentshim does not change what
|
|
71
|
+
the agent is offered. A provider declares the scopes it can enforce in
|
|
72
|
+
``ProviderProfile.skill_scopes``; asking for another one is an error,
|
|
73
|
+
never a silent ``ALL``.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
ALL = "all"
|
|
77
|
+
PROJECT = "project"
|
|
78
|
+
|
|
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
|
+
|
|
64
96
|
def _no_container_env() -> Mapping[str, str]:
|
|
65
97
|
"""Empty read-only default: a frozen spec must not carry a mutable one.
|
|
66
98
|
|
|
@@ -109,3 +141,7 @@ class ProviderProfile:
|
|
|
109
141
|
skill_discovery: SkillSignal = SkillSignal.NONE
|
|
110
142
|
#: Whether the stream reveals skill loads (``SkillInvoked``).
|
|
111
143
|
skill_invocation: SkillSignal = SkillSignal.NONE
|
|
144
|
+
#: The ``SkillScope`` values a session on this provider may request.
|
|
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,6 +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 McpScope, SkillScope
|
|
8
9
|
from .usage import ProviderUsage
|
|
9
10
|
|
|
10
11
|
if TYPE_CHECKING:
|
|
@@ -37,6 +38,16 @@ class ArgvContext:
|
|
|
37
38
|
#: The directory the CLI will run in, or ``None`` for the executor's own.
|
|
38
39
|
#: Argv never carries it; a provider reads it to validate paths against it.
|
|
39
40
|
cwd: str | None = None
|
|
41
|
+
#: Which skills the CLI may discover; the session has already checked
|
|
42
|
+
#: that the provider's profile supports it.
|
|
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] = ()
|
|
40
51
|
|
|
41
52
|
|
|
42
53
|
@dataclass(frozen=True)
|
|
@@ -9,9 +9,11 @@ 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,
|
|
16
|
+
SkillScope,
|
|
15
17
|
SkillSignal,
|
|
16
18
|
)
|
|
17
19
|
|
|
@@ -31,6 +33,23 @@ if TYPE_CHECKING:
|
|
|
31
33
|
MCP_CONFIG_FILENAME = ".mcp.json"
|
|
32
34
|
MCP_SERVER_KEY = "mcpServers"
|
|
33
35
|
|
|
36
|
+
#: ``SkillScope.PROJECT``: load settings only from the workspace's
|
|
37
|
+
#: ``.claude/settings.json`` and ``.claude/settings.local.json`` (plus
|
|
38
|
+
#: ``--settings`` and managed policy, which always apply). Leaving out the
|
|
39
|
+
#: ``user`` source drops ``~/.claude/skills``, the user's plugins (with their
|
|
40
|
+
#: skills, hooks and MCP servers), ``~/.claude/CLAUDE.md`` and the user's
|
|
41
|
+
#: settings (default model, permissions, hooks, ``env``, ``apiKeyHelper``).
|
|
42
|
+
#: Credentials are not settings: OAuth in ``.credentials.json`` and the
|
|
43
|
+
#: ``auth_env_vars`` keep working. Claude Code's built-in skills and claude.ai
|
|
44
|
+
#: account connectors are not user settings and stay.
|
|
45
|
+
PROJECT_SETTING_SOURCES = ("--setting-sources", "project,local")
|
|
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
|
+
|
|
34
53
|
PROFILE = ProviderProfile(
|
|
35
54
|
name="claude",
|
|
36
55
|
display_name="Claude Code",
|
|
@@ -54,6 +73,8 @@ PROFILE = ProviderProfile(
|
|
|
54
73
|
# ``system/init`` lists ``skills``; the ``Skill`` tool call names one.
|
|
55
74
|
skill_discovery=SkillSignal.STRUCTURED,
|
|
56
75
|
skill_invocation=SkillSignal.STRUCTURED,
|
|
76
|
+
skill_scopes=frozenset({SkillScope.ALL, SkillScope.PROJECT}),
|
|
77
|
+
mcp_scopes=frozenset({McpScope.ALL, McpScope.SESSION}),
|
|
57
78
|
container_install=(
|
|
58
79
|
"apt-get update && apt-get install -y --no-install-recommends curl ca-certificates",
|
|
59
80
|
"curl -fsSL https://claude.ai/install.sh | bash",
|
|
@@ -117,6 +138,8 @@ class ClaudeProvider:
|
|
|
117
138
|
"stream-json",
|
|
118
139
|
"--verbose",
|
|
119
140
|
]
|
|
141
|
+
if ctx.skill_scope is SkillScope.PROJECT:
|
|
142
|
+
argv += PROJECT_SETTING_SOURCES
|
|
120
143
|
if ctx.model:
|
|
121
144
|
argv += ["--model", ctx.model]
|
|
122
145
|
if ctx.reasoning_effort:
|
|
@@ -126,6 +149,8 @@ class ClaudeProvider:
|
|
|
126
149
|
settings = build_settings(self.sandbox, hooks=self.hooks)
|
|
127
150
|
if settings:
|
|
128
151
|
argv += ["--settings", json.dumps(settings)]
|
|
152
|
+
if ctx.mcp_scope is McpScope.SESSION:
|
|
153
|
+
argv += _session_mcp_argv(ctx.mcp_servers)
|
|
129
154
|
argv += list(ctx.mcp_argv)
|
|
130
155
|
argv += list(ctx.extra_args)
|
|
131
156
|
return argv
|
|
@@ -178,6 +203,15 @@ def _resumed_session_id(argv: Sequence[str]) -> str | None:
|
|
|
178
203
|
return args[index + 1]
|
|
179
204
|
|
|
180
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
|
+
|
|
181
215
|
def mcp_entry(server: McpServer) -> dict[str, Any]:
|
|
182
216
|
"""Render one server the way ``.mcp.json`` describes it."""
|
|
183
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,16 +10,20 @@ 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,
|
|
17
|
+
SkillScope,
|
|
16
18
|
SkillSignal,
|
|
17
19
|
)
|
|
18
20
|
|
|
19
21
|
from ._toml import toml_array, toml_str, unescape_toml
|
|
22
|
+
from .mcp_scope import session_scope_overrides
|
|
20
23
|
from .parser import CodexStreamParser
|
|
21
24
|
from .rules import RULES_FILENAME
|
|
22
25
|
from .sandbox import CodexSandboxConfig, resolve_sandbox, sandbox_overrides
|
|
26
|
+
from .skills import project_scope_overrides
|
|
23
27
|
|
|
24
28
|
if TYPE_CHECKING:
|
|
25
29
|
from collections.abc import Callable, Mapping, Sequence
|
|
@@ -79,6 +83,8 @@ PROFILE = ProviderProfile(
|
|
|
79
83
|
# ``exec --json`` lists no skills; a load is inferred from a shell read
|
|
80
84
|
# of a ``SKILL.md`` (``skills.py``).
|
|
81
85
|
skill_invocation=SkillSignal.INFERRED,
|
|
86
|
+
skill_scopes=frozenset({SkillScope.ALL, SkillScope.PROJECT}),
|
|
87
|
+
mcp_scopes=frozenset({McpScope.ALL, McpScope.SESSION}),
|
|
82
88
|
container_install=(_NODE_INSTALL, _CODEX_INSTALL),
|
|
83
89
|
# Documented Codex CLI variable that relocates ~/.codex (``codex --help``:
|
|
84
90
|
# "Layer $CODEX_HOME/<name>.config.toml on top of the base user config";
|
|
@@ -130,6 +136,7 @@ class CodexProvider:
|
|
|
130
136
|
if ctx.model:
|
|
131
137
|
argv += ["--model", ctx.model]
|
|
132
138
|
argv += _shell_path_config(ctx.env)
|
|
139
|
+
argv += _scope_overrides(ctx)
|
|
133
140
|
if ctx.reasoning_effort:
|
|
134
141
|
argv += ["--config", f"model_reasoning_effort={toml_str(ctx.reasoning_effort)}"]
|
|
135
142
|
argv += list(ctx.mcp_argv)
|
|
@@ -265,6 +272,21 @@ def _shell_path_config(env: Mapping[str, str]) -> list[str]:
|
|
|
265
272
|
return ["--config", f"shell_environment_policy.set.PATH={toml_str(path)}"]
|
|
266
273
|
|
|
267
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
|
+
|
|
268
290
|
def _server_flags(server: McpServer) -> list[str]:
|
|
269
291
|
"""Render one MCP server as dotted-path TOML overrides.
|
|
270
292
|
|
|
@@ -333,6 +355,9 @@ def parse_mcp_servers(argv: Sequence[str]) -> dict[str, dict[str, Any]]:
|
|
|
333
355
|
servers: dict[str, dict[str, Any]] = {}
|
|
334
356
|
for key, field, raw_value in _mcp_config_overrides(argv):
|
|
335
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}
|
|
336
361
|
for entry in servers.values():
|
|
337
362
|
if "url" not in entry:
|
|
338
363
|
entry.setdefault("args", [])
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Codex skills: recognize a load in a shell command, and hide the user's.
|
|
2
|
+
|
|
3
|
+
Codex has no skill event in ``codex exec --json``: it injects a list of
|
|
4
|
+
skills into the prompt and the model loads one by reading its ``SKILL.md``
|
|
5
|
+
with a shell command (``sed -n '1,240p' .../.agents/skills/<name>/SKILL.md``).
|
|
6
|
+
That read is the only trace of the load in the stream, so it is inferred
|
|
7
|
+
here and nowhere else.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
from agentshim.core.events import SkillInvoked
|
|
17
|
+
|
|
18
|
+
from ._toml import toml_str
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from collections.abc import Mapping
|
|
22
|
+
|
|
23
|
+
#: A path ending in ``skills/[<group>/...]<name>/SKILL.md``, as one shell word.
|
|
24
|
+
_SKILL_FILE = re.compile(
|
|
25
|
+
r"(?<![^\s'\"=])(?P<path>(?:[^\s'\"]*/)?skills/(?:[^\s'\"/]+/)*?(?P<name>[^\s'\"/]+)/SKILL\.md)(?![^\s'\";|&)])"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def skill_reads(command: str, tool_id: str | None) -> list[SkillInvoked]:
|
|
30
|
+
"""Return one ``SkillInvoked`` per ``SKILL.md`` the command names, in order."""
|
|
31
|
+
return [
|
|
32
|
+
SkillInvoked(name=match["name"], source_path=match["path"], tool_id=tool_id)
|
|
33
|
+
for match in _SKILL_FILE.finditer(command)
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
#: Codex's own bundled skills, which it re-extracts into every home.
|
|
38
|
+
_SYSTEM_SKILLS = ".system"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def user_skill_files(env: Mapping[str, str]) -> list[str]:
|
|
42
|
+
"""Every user-installed ``SKILL.md`` the CLI run with *env* would offer.
|
|
43
|
+
|
|
44
|
+
Codex reads user skills from ``$CODEX_HOME/skills`` (default
|
|
45
|
+
``~/.codex/skills``) and ``~/.agents/skills``, beside the workspace's own
|
|
46
|
+
``.agents/skills``. ``$CODEX_HOME/skills/.system`` holds Codex's bundled
|
|
47
|
+
skills and is not the user's. Paths are listed as found and, when a
|
|
48
|
+
symlink is involved, also resolved, since either spelling may be the one
|
|
49
|
+
Codex matches against. The scan runs where agentshim runs, so a CLI in a
|
|
50
|
+
container is only covered when the container shares this home.
|
|
51
|
+
"""
|
|
52
|
+
home = env.get("HOME") or os.path.expanduser("~") # noqa: PTH111 - a str contract, not a Path
|
|
53
|
+
codex_home = env.get("CODEX_HOME") or os.path.join(home, ".codex") # noqa: PTH118
|
|
54
|
+
roots = [os.path.join(codex_home, "skills"), os.path.join(home, ".agents", "skills")] # noqa: PTH118
|
|
55
|
+
found: list[str] = []
|
|
56
|
+
for root in roots:
|
|
57
|
+
for directory, subdirs, files in os.walk(root, followlinks=True):
|
|
58
|
+
if directory == root and _SYSTEM_SKILLS in subdirs:
|
|
59
|
+
subdirs.remove(_SYSTEM_SKILLS)
|
|
60
|
+
subdirs.sort()
|
|
61
|
+
if "SKILL.md" in files:
|
|
62
|
+
path = os.path.join(directory, "SKILL.md") # noqa: PTH118
|
|
63
|
+
found.append(path)
|
|
64
|
+
resolved = os.path.realpath(path)
|
|
65
|
+
if resolved != path:
|
|
66
|
+
found.append(resolved)
|
|
67
|
+
return list(dict.fromkeys(found))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def project_scope_overrides(env: Mapping[str, str]) -> list[str]:
|
|
71
|
+
"""``--config`` flags that leave Codex only the workspace's skills.
|
|
72
|
+
|
|
73
|
+
Plugins are switched off as a feature (their skills, MCP servers and
|
|
74
|
+
apps go with them), and each user skill is disabled by path through
|
|
75
|
+
``skills.config``. The override replaces any ``skills.config`` in the
|
|
76
|
+
user's ``config.toml``, which is user policy this scope excludes anyway.
|
|
77
|
+
"""
|
|
78
|
+
entries = ", ".join(
|
|
79
|
+
f"{{path = {toml_str(path)}, enabled = false}}" for path in user_skill_files(env)
|
|
80
|
+
)
|
|
81
|
+
return [
|
|
82
|
+
"--config",
|
|
83
|
+
"features.plugins=false",
|
|
84
|
+
"--config",
|
|
85
|
+
f"skills.config=[{entries}]",
|
|
86
|
+
]
|
|
@@ -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 = [
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
"""Recognize a Codex skill load in a shell command.
|
|
2
|
-
|
|
3
|
-
Codex has no skill event in ``codex exec --json``: it injects a list of
|
|
4
|
-
skills into the prompt and the model loads one by reading its ``SKILL.md``
|
|
5
|
-
with a shell command (``sed -n '1,240p' .../.agents/skills/<name>/SKILL.md``).
|
|
6
|
-
That read is the only trace of the load in the stream, so it is inferred
|
|
7
|
-
here and nowhere else.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
import re
|
|
13
|
-
|
|
14
|
-
from agentshim.core.events import SkillInvoked
|
|
15
|
-
|
|
16
|
-
#: A path ending in ``skills/[<group>/...]<name>/SKILL.md``, as one shell word.
|
|
17
|
-
_SKILL_FILE = re.compile(
|
|
18
|
-
r"(?<![^\s'\"=])(?P<path>(?:[^\s'\"]*/)?skills/(?:[^\s'\"/]+/)*?(?P<name>[^\s'\"/]+)/SKILL\.md)(?![^\s'\";|&)])"
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def skill_reads(command: str, tool_id: str | None) -> list[SkillInvoked]:
|
|
23
|
-
"""Return one ``SkillInvoked`` per ``SKILL.md`` the command names, in order."""
|
|
24
|
-
return [
|
|
25
|
-
SkillInvoked(name=match["name"], source_path=match["path"], tool_id=tool_id)
|
|
26
|
-
for match in _SKILL_FILE.finditer(command)
|
|
27
|
-
]
|
|
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
|