agentshim 0.8.0__tar.gz → 0.9.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.9.0}/CHANGELOG.md +21 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/PKG-INFO +1 -1
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/__init__.py +3 -1
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/agent.py +22 -5
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/core/__init__.py +2 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/core/profile.py +18 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/core/provider.py +4 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/claude/provider.py +15 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/codex/provider.py +5 -0
- agentshim-0.9.0/agentshim/providers/codex/skills.py +86 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/pyproject.toml +1 -1
- agentshim-0.8.0/agentshim/providers/codex/skills.py +0 -27
- {agentshim-0.8.0 → agentshim-0.9.0}/.gitignore +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/README.md +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/core/_files.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/core/env.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/core/errors.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/core/events.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/core/mcp.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/core/pricing.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/core/schema.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/core/skills.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/core/stream.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/core/turn.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/core/usage.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/execution/__init__.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/execution/executor.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/execution/host.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/execution/transform.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/__init__.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/claude/__init__.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/claude/events.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/claude/hooks/__init__.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/claude/hooks/confine_reads.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/claude/parser.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/claude/sandbox.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/claude/scripted.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/claude/user_hooks.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/codex/__init__.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/codex/_toml.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/codex/events.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/codex/parser.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/codex/rules.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/codex/sandbox.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/codex/scripted.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/copilot/__init__.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/copilot/events.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/copilot/parser.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/copilot/provider.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/copilot/scripted.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/gemini/__init__.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/gemini/events.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/gemini/parser.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/gemini/provider.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/gemini/scripted.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/opencode/__init__.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/opencode/events.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/opencode/parser.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/opencode/provider.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/providers/opencode/scripted.py +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/py.typed +0 -0
- {agentshim-0.8.0 → agentshim-0.9.0}/agentshim/testing/__init__.py +0 -0
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0 (2026-10-03)
|
|
4
|
+
|
|
5
|
+
Skill isolation. Additive: the default scope keeps today's behaviour.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `SkillScope` (`ALL`, `PROJECT`) and `start_session(skill_scope=...)` /
|
|
10
|
+
`run(skill_scope=...)`. `PROJECT` offers only the workspace's skills and
|
|
11
|
+
the CLI's built-in ones, not the user's personal skills or plugins.
|
|
12
|
+
- `ProviderProfile.skill_scopes` declares the scopes a provider enforces;
|
|
13
|
+
asking for another raises `ProviderCapabilityError` from `start_session`.
|
|
14
|
+
- `ArgvContext.skill_scope` carries the session's scope to `build_argv`.
|
|
15
|
+
- Claude Code: `PROJECT` passes `--setting-sources project,local`, which also
|
|
16
|
+
skips user settings and `~/.claude/CLAUDE.md`; credentials still work.
|
|
17
|
+
- Codex: `PROJECT` sets `features.plugins=false` and disables every user
|
|
18
|
+
`SKILL.md` (`$CODEX_HOME/skills` except `.system`, `~/.agents/skills`)
|
|
19
|
+
through `skills.config`.
|
|
20
|
+
- Gemini, opencode and Copilot support `ALL` only.
|
|
21
|
+
- Live e2e: `SkillScope.PROJECT` hides a skill seeded in a relocated user
|
|
22
|
+
home while the workspace skill stays offered (`tests/e2e/test_skills_e2e.py`).
|
|
23
|
+
|
|
3
24
|
## 0.8.0 (2026-10-03)
|
|
4
25
|
|
|
5
26
|
Skill observability. Additive: a caller that ignores the new events and the
|
|
@@ -50,6 +50,7 @@ from .core import (
|
|
|
50
50
|
SessionResumeError,
|
|
51
51
|
SessionStarted,
|
|
52
52
|
SkillInvoked,
|
|
53
|
+
SkillScope,
|
|
53
54
|
SkillsDiscovered,
|
|
54
55
|
SkillSignal,
|
|
55
56
|
SkillSummary,
|
|
@@ -96,7 +97,7 @@ from .providers.copilot import CopilotProvider
|
|
|
96
97
|
from .providers.gemini import GeminiProvider
|
|
97
98
|
from .providers.opencode import OpencodeProvider
|
|
98
99
|
|
|
99
|
-
__version__ = "0.
|
|
100
|
+
__version__ = "0.9.0"
|
|
100
101
|
|
|
101
102
|
__all__ = [
|
|
102
103
|
"AgentEvent",
|
|
@@ -159,6 +160,7 @@ __all__ = [
|
|
|
159
160
|
"SessionResumeError",
|
|
160
161
|
"SessionStarted",
|
|
161
162
|
"SkillInvoked",
|
|
163
|
+
"SkillScope",
|
|
162
164
|
"SkillSignal",
|
|
163
165
|
"SkillSummary",
|
|
164
166
|
"SkillTracker",
|
|
@@ -23,7 +23,7 @@ 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 McpMechanism, OutputSchemaStyle, SchemaDialect
|
|
26
|
+
from agentshim.core.profile import McpMechanism, OutputSchemaStyle, SchemaDialect, SkillScope
|
|
27
27
|
from agentshim.core.provider import ArgvContext
|
|
28
28
|
from agentshim.core.schema import compact_json, dialect_problems, materialize
|
|
29
29
|
from agentshim.core.skills import SkillTracker
|
|
@@ -114,9 +114,15 @@ class CliAgent:
|
|
|
114
114
|
cwd: str | None = None,
|
|
115
115
|
timeout: float | None = None,
|
|
116
116
|
session_id: str | None = None,
|
|
117
|
+
skill_scope: SkillScope = SkillScope.ALL,
|
|
117
118
|
) -> AgentSession:
|
|
118
|
-
"""Open a conversation whose turns resume one another.
|
|
119
|
-
|
|
119
|
+
"""Open a conversation whose turns resume one another.
|
|
120
|
+
|
|
121
|
+
``skill_scope`` is fixed for the conversation (see ``AgentSession``).
|
|
122
|
+
"""
|
|
123
|
+
return AgentSession(
|
|
124
|
+
self, cwd=cwd, timeout=timeout, session_id=session_id, skill_scope=skill_scope
|
|
125
|
+
)
|
|
120
126
|
|
|
121
127
|
def run(
|
|
122
128
|
self,
|
|
@@ -124,9 +130,11 @@ class CliAgent:
|
|
|
124
130
|
*,
|
|
125
131
|
cwd: str | None = None,
|
|
126
132
|
timeout: float | None = None,
|
|
133
|
+
skill_scope: SkillScope = SkillScope.ALL,
|
|
127
134
|
) -> TurnResult:
|
|
128
135
|
"""Run one turn in a throwaway session."""
|
|
129
|
-
|
|
136
|
+
session = self.start_session(cwd=cwd, timeout=timeout, skill_scope=skill_scope)
|
|
137
|
+
return session.turn(request)
|
|
130
138
|
|
|
131
139
|
|
|
132
140
|
def _discard(message: str) -> None:
|
|
@@ -143,6 +151,7 @@ class AgentSession:
|
|
|
143
151
|
cwd: str | None = None,
|
|
144
152
|
timeout: float | None = None,
|
|
145
153
|
session_id: str | None = None,
|
|
154
|
+
skill_scope: SkillScope = SkillScope.ALL,
|
|
146
155
|
) -> None:
|
|
147
156
|
"""Bind a conversation to one agent, with per-turn defaults.
|
|
148
157
|
|
|
@@ -150,8 +159,15 @@ class AgentSession:
|
|
|
150
159
|
successive turns resume one another. ``cwd`` and ``timeout`` are
|
|
151
160
|
defaults an individual ``TurnRequest`` may override. Passing
|
|
152
161
|
``session_id`` adopts a conversation the provider already has, so the
|
|
153
|
-
first turn resumes rather than starts fresh.
|
|
162
|
+
first turn resumes rather than starts fresh. ``skill_scope`` limits
|
|
163
|
+
which skills every turn's CLI may discover; a scope the provider's
|
|
164
|
+
``profile.skill_scopes`` does not list raises
|
|
165
|
+
``ProviderCapabilityError`` here, before any turn runs.
|
|
154
166
|
"""
|
|
167
|
+
if skill_scope not in agent.profile.skill_scopes:
|
|
168
|
+
msg = f"{agent.profile.name} cannot limit skills to scope {skill_scope.value!r}"
|
|
169
|
+
raise ProviderCapabilityError(msg)
|
|
170
|
+
self.skill_scope = skill_scope
|
|
155
171
|
self._agent = agent
|
|
156
172
|
self._cwd = cwd
|
|
157
173
|
self._timeout = timeout
|
|
@@ -260,6 +276,7 @@ class AgentSession:
|
|
|
260
276
|
mcp_argv=installation.argv,
|
|
261
277
|
extra_args=req.extra_args,
|
|
262
278
|
cwd=cwd,
|
|
279
|
+
skill_scope=self.skill_scope,
|
|
263
280
|
)
|
|
264
281
|
)
|
|
265
282
|
command = CommandRequest(argv=argv, stdin=req.prompt, cwd=cwd, env=env, timeout=timeout)
|
|
@@ -58,6 +58,7 @@ from .profile import (
|
|
|
58
58
|
OutputSchemaStyle,
|
|
59
59
|
ProviderProfile,
|
|
60
60
|
SchemaDialect,
|
|
61
|
+
SkillScope,
|
|
61
62
|
SkillSignal,
|
|
62
63
|
)
|
|
63
64
|
from .provider import ArgvContext, McpInstallation, ParsedTurn, Provider, StreamParser
|
|
@@ -110,6 +111,7 @@ __all__ = [
|
|
|
110
111
|
"SessionResumeError",
|
|
111
112
|
"SessionStarted",
|
|
112
113
|
"SkillInvoked",
|
|
114
|
+
"SkillScope",
|
|
113
115
|
"SkillSignal",
|
|
114
116
|
"SkillSummary",
|
|
115
117
|
"SkillTracker",
|
|
@@ -61,6 +61,22 @@ 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
|
+
|
|
64
80
|
def _no_container_env() -> Mapping[str, str]:
|
|
65
81
|
"""Empty read-only default: a frozen spec must not carry a mutable one.
|
|
66
82
|
|
|
@@ -109,3 +125,5 @@ class ProviderProfile:
|
|
|
109
125
|
skill_discovery: SkillSignal = SkillSignal.NONE
|
|
110
126
|
#: Whether the stream reveals skill loads (``SkillInvoked``).
|
|
111
127
|
skill_invocation: SkillSignal = SkillSignal.NONE
|
|
128
|
+
#: The ``SkillScope`` values a session on this provider may request.
|
|
129
|
+
skill_scopes: frozenset[SkillScope] = frozenset({SkillScope.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 SkillScope
|
|
8
9
|
from .usage import ProviderUsage
|
|
9
10
|
|
|
10
11
|
if TYPE_CHECKING:
|
|
@@ -37,6 +38,9 @@ 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
|
|
40
44
|
|
|
41
45
|
|
|
42
46
|
@dataclass(frozen=True)
|
|
@@ -12,6 +12,7 @@ from agentshim.core.profile import (
|
|
|
12
12
|
OutputSchemaStyle,
|
|
13
13
|
ProviderProfile,
|
|
14
14
|
SchemaDialect,
|
|
15
|
+
SkillScope,
|
|
15
16
|
SkillSignal,
|
|
16
17
|
)
|
|
17
18
|
|
|
@@ -31,6 +32,17 @@ if TYPE_CHECKING:
|
|
|
31
32
|
MCP_CONFIG_FILENAME = ".mcp.json"
|
|
32
33
|
MCP_SERVER_KEY = "mcpServers"
|
|
33
34
|
|
|
35
|
+
#: ``SkillScope.PROJECT``: load settings only from the workspace's
|
|
36
|
+
#: ``.claude/settings.json`` and ``.claude/settings.local.json`` (plus
|
|
37
|
+
#: ``--settings`` and managed policy, which always apply). Leaving out the
|
|
38
|
+
#: ``user`` source drops ``~/.claude/skills``, the user's plugins (with their
|
|
39
|
+
#: skills, hooks and MCP servers), ``~/.claude/CLAUDE.md`` and the user's
|
|
40
|
+
#: settings (default model, permissions, hooks, ``env``, ``apiKeyHelper``).
|
|
41
|
+
#: Credentials are not settings: OAuth in ``.credentials.json`` and the
|
|
42
|
+
#: ``auth_env_vars`` keep working. Claude Code's built-in skills and claude.ai
|
|
43
|
+
#: account connectors are not user settings and stay.
|
|
44
|
+
PROJECT_SETTING_SOURCES = ("--setting-sources", "project,local")
|
|
45
|
+
|
|
34
46
|
PROFILE = ProviderProfile(
|
|
35
47
|
name="claude",
|
|
36
48
|
display_name="Claude Code",
|
|
@@ -54,6 +66,7 @@ PROFILE = ProviderProfile(
|
|
|
54
66
|
# ``system/init`` lists ``skills``; the ``Skill`` tool call names one.
|
|
55
67
|
skill_discovery=SkillSignal.STRUCTURED,
|
|
56
68
|
skill_invocation=SkillSignal.STRUCTURED,
|
|
69
|
+
skill_scopes=frozenset({SkillScope.ALL, SkillScope.PROJECT}),
|
|
57
70
|
container_install=(
|
|
58
71
|
"apt-get update && apt-get install -y --no-install-recommends curl ca-certificates",
|
|
59
72
|
"curl -fsSL https://claude.ai/install.sh | bash",
|
|
@@ -117,6 +130,8 @@ class ClaudeProvider:
|
|
|
117
130
|
"stream-json",
|
|
118
131
|
"--verbose",
|
|
119
132
|
]
|
|
133
|
+
if ctx.skill_scope is SkillScope.PROJECT:
|
|
134
|
+
argv += PROJECT_SETTING_SOURCES
|
|
120
135
|
if ctx.model:
|
|
121
136
|
argv += ["--model", ctx.model]
|
|
122
137
|
if ctx.reasoning_effort:
|
|
@@ -13,6 +13,7 @@ from agentshim.core.profile import (
|
|
|
13
13
|
OutputSchemaStyle,
|
|
14
14
|
ProviderProfile,
|
|
15
15
|
SchemaDialect,
|
|
16
|
+
SkillScope,
|
|
16
17
|
SkillSignal,
|
|
17
18
|
)
|
|
18
19
|
|
|
@@ -20,6 +21,7 @@ from ._toml import toml_array, toml_str, unescape_toml
|
|
|
20
21
|
from .parser import CodexStreamParser
|
|
21
22
|
from .rules import RULES_FILENAME
|
|
22
23
|
from .sandbox import CodexSandboxConfig, resolve_sandbox, sandbox_overrides
|
|
24
|
+
from .skills import project_scope_overrides
|
|
23
25
|
|
|
24
26
|
if TYPE_CHECKING:
|
|
25
27
|
from collections.abc import Callable, Mapping, Sequence
|
|
@@ -79,6 +81,7 @@ PROFILE = ProviderProfile(
|
|
|
79
81
|
# ``exec --json`` lists no skills; a load is inferred from a shell read
|
|
80
82
|
# of a ``SKILL.md`` (``skills.py``).
|
|
81
83
|
skill_invocation=SkillSignal.INFERRED,
|
|
84
|
+
skill_scopes=frozenset({SkillScope.ALL, SkillScope.PROJECT}),
|
|
82
85
|
container_install=(_NODE_INSTALL, _CODEX_INSTALL),
|
|
83
86
|
# Documented Codex CLI variable that relocates ~/.codex (``codex --help``:
|
|
84
87
|
# "Layer $CODEX_HOME/<name>.config.toml on top of the base user config";
|
|
@@ -130,6 +133,8 @@ class CodexProvider:
|
|
|
130
133
|
if ctx.model:
|
|
131
134
|
argv += ["--model", ctx.model]
|
|
132
135
|
argv += _shell_path_config(ctx.env)
|
|
136
|
+
if ctx.skill_scope is SkillScope.PROJECT:
|
|
137
|
+
argv += project_scope_overrides(ctx.env)
|
|
133
138
|
if ctx.reasoning_effort:
|
|
134
139
|
argv += ["--config", f"model_reasoning_effort={toml_str(ctx.reasoning_effort)}"]
|
|
135
140
|
argv += list(ctx.mcp_argv)
|
|
@@ -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,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
|