nexus-coding-agent 0.1.0__py3-none-any.whl
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.
- nexus/__init__.py +4 -0
- nexus/application/__init__.py +2 -0
- nexus/application/approval_service.py +130 -0
- nexus/application/diff_service.py +177 -0
- nexus/application/event_publisher.py +180 -0
- nexus/application/execution_context.py +29 -0
- nexus/application/execution_ledger.py +126 -0
- nexus/application/model_call_context.py +29 -0
- nexus/application/observed_model_gateway.py +194 -0
- nexus/application/plan_approval_service.py +250 -0
- nexus/application/planning.py +668 -0
- nexus/application/runtime.py +624 -0
- nexus/application/session_service.py +411 -0
- nexus/application/structured_output.py +96 -0
- nexus/application/telemetry.py +396 -0
- nexus/application/telemetry_subscriber.py +216 -0
- nexus/application/tool_runtime.py +496 -0
- nexus/application/tracing_dispatcher.py +283 -0
- nexus/application/validation.py +293 -0
- nexus/config/__init__.py +17 -0
- nexus/config/loader.py +253 -0
- nexus/config/models.py +310 -0
- nexus/context/__init__.py +6 -0
- nexus/context/budget.py +24 -0
- nexus/context/builder.py +92 -0
- nexus/context/chunking.py +65 -0
- nexus/context/explorer.py +246 -0
- nexus/context/integration.py +63 -0
- nexus/context/manager.py +328 -0
- nexus/context/retrieval.py +261 -0
- nexus/domain/__init__.py +2 -0
- nexus/domain/agent_decision.py +113 -0
- nexus/domain/agent_state.py +49 -0
- nexus/domain/approvals.py +64 -0
- nexus/domain/context.py +108 -0
- nexus/domain/exploration.py +86 -0
- nexus/domain/mcp.py +51 -0
- nexus/domain/model.py +173 -0
- nexus/domain/observability.py +326 -0
- nexus/domain/persistence.py +122 -0
- nexus/domain/planning.py +329 -0
- nexus/domain/ports/__init__.py +61 -0
- nexus/domain/ports/agent_decision.py +9 -0
- nexus/domain/ports/approval_unit_of_work.py +27 -0
- nexus/domain/ports/checkpoint_provider.py +11 -0
- nexus/domain/ports/context.py +71 -0
- nexus/domain/ports/events.py +21 -0
- nexus/domain/ports/graph_runtime.py +30 -0
- nexus/domain/ports/mcp.py +23 -0
- nexus/domain/ports/model_gateway.py +21 -0
- nexus/domain/ports/model_input_budget.py +15 -0
- nexus/domain/ports/planning.py +62 -0
- nexus/domain/ports/repositories.py +54 -0
- nexus/domain/ports/repository_context.py +18 -0
- nexus/domain/ports/session_unit_of_work.py +33 -0
- nexus/domain/ports/skills.py +41 -0
- nexus/domain/ports/tooling.py +39 -0
- nexus/domain/ports/tracer.py +13 -0
- nexus/domain/ports/validation.py +32 -0
- nexus/domain/runtime_events.py +445 -0
- nexus/domain/skills.py +129 -0
- nexus/domain/tooling.py +163 -0
- nexus/domain/validation.py +161 -0
- nexus/errors/__init__.py +82 -0
- nexus/evaluation/__init__.py +7 -0
- nexus/evaluation/assertions.py +4 -0
- nexus/evaluation/evaluator.py +263 -0
- nexus/evaluation/fixtures.py +36 -0
- nexus/evaluation/harness.py +606 -0
- nexus/evaluation/loader.py +415 -0
- nexus/evaluation/metrics.py +27 -0
- nexus/evaluation/models.py +362 -0
- nexus/evaluation/report.py +43 -0
- nexus/evaluation/runner.py +480 -0
- nexus/infrastructure/__init__.py +2 -0
- nexus/infrastructure/asyncio_compat.py +13 -0
- nexus/infrastructure/bootstrap/__init__.py +17 -0
- nexus/infrastructure/bootstrap/composition.py +663 -0
- nexus/infrastructure/checkpoint/__init__.py +5 -0
- nexus/infrastructure/checkpoint/postgres.py +62 -0
- nexus/infrastructure/database.py +39 -0
- nexus/infrastructure/embedding.py +61 -0
- nexus/infrastructure/graph/__init__.py +6 -0
- nexus/infrastructure/graph/day4_runtime.py +966 -0
- nexus/infrastructure/graph/langgraph_runtime.py +172 -0
- nexus/infrastructure/mcp/__init__.py +5 -0
- nexus/infrastructure/mcp/manager.py +380 -0
- nexus/infrastructure/model_gateway/__init__.py +6 -0
- nexus/infrastructure/model_gateway/openai_compatible.py +345 -0
- nexus/infrastructure/observability/__init__.py +7 -0
- nexus/infrastructure/observability/console.py +31 -0
- nexus/infrastructure/observability/langsmith.py +236 -0
- nexus/infrastructure/observability/structured_logger.py +70 -0
- nexus/infrastructure/persistence/__init__.py +13 -0
- nexus/infrastructure/persistence/approval_unit_of_work.py +74 -0
- nexus/infrastructure/persistence/models.py +146 -0
- nexus/infrastructure/persistence/repositories.py +326 -0
- nexus/infrastructure/persistence/unit_of_work.py +82 -0
- nexus/infrastructure/repository_files.py +117 -0
- nexus/infrastructure/sandbox/__init__.py +5 -0
- nexus/infrastructure/sandbox/local_process.py +353 -0
- nexus/infrastructure/semantic.py +276 -0
- nexus/infrastructure/semantic_models.py +56 -0
- nexus/interfaces/__init__.py +2 -0
- nexus/interfaces/cli/__init__.py +2 -0
- nexus/interfaces/cli/__main__.py +6 -0
- nexus/interfaces/cli/app.py +266 -0
- nexus/interfaces/cli/renderer.py +118 -0
- nexus/security/__init__.py +14 -0
- nexus/security/approval_policies.py +73 -0
- nexus/security/command_policy.py +117 -0
- nexus/security/executables.py +107 -0
- nexus/security/git_commands.py +65 -0
- nexus/security/workspace.py +93 -0
- nexus/skills/__init__.py +7 -0
- nexus/skills/builtin/__init__.py +1 -0
- nexus/skills/builtin/debug-python/SKILL.md +26 -0
- nexus/skills/builtin/review-repository/SKILL.md +26 -0
- nexus/skills/builtin/write-tests/SKILL.md +26 -0
- nexus/skills/loader.py +439 -0
- nexus/skills/registry.py +146 -0
- nexus/skills/selector.py +152 -0
- nexus/tools/__init__.py +6 -0
- nexus/tools/editing.py +312 -0
- nexus/tools/mcp.py +160 -0
- nexus/tools/native.py +654 -0
- nexus/tools/registry.py +27 -0
- nexus_coding_agent-0.1.0.dist-info/METADATA +142 -0
- nexus_coding_agent-0.1.0.dist-info/RECORD +131 -0
- nexus_coding_agent-0.1.0.dist-info/WHEEL +4 -0
- nexus_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
nexus/skills/loader.py
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"""Bounded metadata-first loader for local and packaged Skill documents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import errno
|
|
6
|
+
import hashlib
|
|
7
|
+
import io
|
|
8
|
+
import tomllib
|
|
9
|
+
import zipfile
|
|
10
|
+
from contextvars import ContextVar
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from importlib.resources import files
|
|
13
|
+
from importlib.resources.abc import Traversable
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import IO
|
|
16
|
+
|
|
17
|
+
from nexus.domain.skills import SkillLocation, SkillMetadata, SkillSource
|
|
18
|
+
from nexus.errors import ContextError
|
|
19
|
+
|
|
20
|
+
MAX_SKILL_FILE_BYTES = 262_144
|
|
21
|
+
MAX_FRONT_MATTER_BYTES = 16_384
|
|
22
|
+
_REQUIRED_KEYS = {"id", "name", "description", "usage_scenario", "version"}
|
|
23
|
+
_ALLOWED_KEYS = {*_REQUIRED_KEYS, "priority"}
|
|
24
|
+
_REQUIRED_HEADINGS = (
|
|
25
|
+
"## Execution Principles",
|
|
26
|
+
"## Recommended Tools",
|
|
27
|
+
"## Workflow",
|
|
28
|
+
"## Constraints",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True, slots=True)
|
|
33
|
+
class _Snapshot:
|
|
34
|
+
size: int
|
|
35
|
+
modified_ns: int | None
|
|
36
|
+
front_hash: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _VersionInvalid(ValueError):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class FileSkillLoader:
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
repository_root: Path,
|
|
47
|
+
user_root: Path,
|
|
48
|
+
*,
|
|
49
|
+
builtin_package: str = "nexus.skills.builtin",
|
|
50
|
+
) -> None:
|
|
51
|
+
self._roots = {
|
|
52
|
+
SkillSource.REPOSITORY: repository_root,
|
|
53
|
+
SkillSource.USER_GLOBAL: user_root,
|
|
54
|
+
}
|
|
55
|
+
self._builtin_package = builtin_package
|
|
56
|
+
self._snapshots: ContextVar[
|
|
57
|
+
dict[int, tuple[SkillMetadata, _Snapshot]] | None
|
|
58
|
+
] = ContextVar("skill_loader_snapshots", default=None)
|
|
59
|
+
|
|
60
|
+
async def load_metadata(self, location: SkillLocation) -> SkillMetadata:
|
|
61
|
+
try:
|
|
62
|
+
before = self._document_state(location)
|
|
63
|
+
with self._open(location) as document:
|
|
64
|
+
front_bytes = _read_front_matter(document)
|
|
65
|
+
after = self._document_state(location)
|
|
66
|
+
if before != after:
|
|
67
|
+
raise ValueError("Skill document changed during metadata scan.")
|
|
68
|
+
size, modified_ns = after
|
|
69
|
+
metadata = _metadata(location, front_bytes)
|
|
70
|
+
except ContextError:
|
|
71
|
+
raise
|
|
72
|
+
except _VersionInvalid as exc:
|
|
73
|
+
raise ContextError(
|
|
74
|
+
f"Skill version is invalid for {location.source.value}:"
|
|
75
|
+
f"{location.relative_path}.",
|
|
76
|
+
code="SKILL_VERSION_INVALID",
|
|
77
|
+
) from exc
|
|
78
|
+
except (OSError, UnicodeDecodeError, tomllib.TOMLDecodeError, ValueError) as exc:
|
|
79
|
+
raise ContextError(
|
|
80
|
+
f"Skill metadata is invalid for {location.source.value}:"
|
|
81
|
+
f"{location.relative_path}.",
|
|
82
|
+
code="SKILL_METADATA_INVALID",
|
|
83
|
+
) from exc
|
|
84
|
+
snapshot = _Snapshot(size, modified_ns, hashlib.sha256(front_bytes).hexdigest())
|
|
85
|
+
snapshots = dict(self._snapshots.get() or {})
|
|
86
|
+
snapshots[id(metadata)] = (metadata, snapshot)
|
|
87
|
+
self._snapshots.set(snapshots)
|
|
88
|
+
return metadata
|
|
89
|
+
|
|
90
|
+
async def load_body(self, metadata: SkillMetadata) -> str:
|
|
91
|
+
snapshots = self._snapshots.get()
|
|
92
|
+
stored = None if snapshots is None else snapshots.get(id(metadata))
|
|
93
|
+
if stored is None or stored[0] is not metadata:
|
|
94
|
+
raise ContextError(
|
|
95
|
+
"Selected Skill metadata did not originate from the current metadata scan.",
|
|
96
|
+
code="SKILL_BODY_LOAD_FAILED",
|
|
97
|
+
)
|
|
98
|
+
snapshot = stored[1]
|
|
99
|
+
try:
|
|
100
|
+
size, modified_ns = self._document_state(metadata.location, selected=True)
|
|
101
|
+
if size != snapshot.size or modified_ns != snapshot.modified_ns:
|
|
102
|
+
raise ContextError(
|
|
103
|
+
"Selected Skill changed after metadata selection.",
|
|
104
|
+
code="SKILL_BODY_LOAD_FAILED",
|
|
105
|
+
)
|
|
106
|
+
with self._open(metadata.location, selected=True) as document:
|
|
107
|
+
content = document.read(MAX_SKILL_FILE_BYTES + 1)
|
|
108
|
+
if len(content) > MAX_SKILL_FILE_BYTES:
|
|
109
|
+
raise ContextError(
|
|
110
|
+
"Selected Skill exceeds the fixed document size limit.",
|
|
111
|
+
code="SKILL_BODY_LOAD_FAILED",
|
|
112
|
+
)
|
|
113
|
+
if self._document_state(metadata.location, selected=True) != (size, modified_ns):
|
|
114
|
+
raise ContextError(
|
|
115
|
+
"Selected Skill changed while its body was being loaded.",
|
|
116
|
+
code="SKILL_BODY_LOAD_FAILED",
|
|
117
|
+
)
|
|
118
|
+
stream = io.BytesIO(content)
|
|
119
|
+
front_bytes = _read_front_matter(stream)
|
|
120
|
+
if hashlib.sha256(front_bytes).hexdigest() != snapshot.front_hash:
|
|
121
|
+
raise ContextError(
|
|
122
|
+
"Selected Skill front matter changed after metadata selection.",
|
|
123
|
+
code="SKILL_BODY_LOAD_FAILED",
|
|
124
|
+
)
|
|
125
|
+
if _metadata(metadata.location, front_bytes) != metadata:
|
|
126
|
+
raise ContextError(
|
|
127
|
+
"Selected Skill metadata changed after selection.",
|
|
128
|
+
code="SKILL_BODY_LOAD_FAILED",
|
|
129
|
+
)
|
|
130
|
+
body = content[stream.tell() :].decode("utf-8")
|
|
131
|
+
except ContextError:
|
|
132
|
+
raise
|
|
133
|
+
except (OSError, UnicodeDecodeError, tomllib.TOMLDecodeError, ValueError) as exc:
|
|
134
|
+
raise ContextError(
|
|
135
|
+
"Selected Skill body could not be loaded safely.",
|
|
136
|
+
code="SKILL_BODY_LOAD_FAILED",
|
|
137
|
+
retryable=isinstance(exc, OSError) and _is_transient_io(exc),
|
|
138
|
+
) from exc
|
|
139
|
+
finally:
|
|
140
|
+
self._discard_snapshot(metadata)
|
|
141
|
+
normalized = body.replace("\r\n", "\n").replace("\r", "\n").strip() + "\n"
|
|
142
|
+
_validate_body(normalized)
|
|
143
|
+
return normalized
|
|
144
|
+
|
|
145
|
+
def _reset_snapshots(self) -> None:
|
|
146
|
+
self._snapshots.set(None)
|
|
147
|
+
|
|
148
|
+
def _discard_snapshot(self, metadata: SkillMetadata) -> None:
|
|
149
|
+
snapshots = self._snapshots.get()
|
|
150
|
+
if snapshots is None:
|
|
151
|
+
return
|
|
152
|
+
stored = snapshots.get(id(metadata))
|
|
153
|
+
if stored is None or stored[0] is not metadata:
|
|
154
|
+
return
|
|
155
|
+
remaining = dict(snapshots)
|
|
156
|
+
del remaining[id(metadata)]
|
|
157
|
+
self._snapshots.set(remaining or None)
|
|
158
|
+
|
|
159
|
+
def _open(self, location: SkillLocation, *, selected: bool = False) -> IO[bytes]:
|
|
160
|
+
if location.source is SkillSource.BUILTIN:
|
|
161
|
+
resource = self._builtin_resource(location, selected=selected)
|
|
162
|
+
try:
|
|
163
|
+
return resource.open("rb")
|
|
164
|
+
except OSError as exc:
|
|
165
|
+
if selected:
|
|
166
|
+
raise ContextError(
|
|
167
|
+
"Selected builtin Skill document is unreadable.",
|
|
168
|
+
code="SKILL_BODY_LOAD_FAILED",
|
|
169
|
+
retryable=_is_transient_io(exc),
|
|
170
|
+
) from exc
|
|
171
|
+
raise ContextError(
|
|
172
|
+
"Builtin Skill document is unreadable.",
|
|
173
|
+
code="SKILL_METADATA_INVALID",
|
|
174
|
+
) from exc
|
|
175
|
+
document = self._filesystem_document(location, selected=selected)
|
|
176
|
+
try:
|
|
177
|
+
return document.open("rb")
|
|
178
|
+
except OSError as exc:
|
|
179
|
+
if selected:
|
|
180
|
+
raise ContextError(
|
|
181
|
+
"Selected Skill document is unreadable.",
|
|
182
|
+
code="SKILL_BODY_LOAD_FAILED",
|
|
183
|
+
retryable=_is_transient_io(exc),
|
|
184
|
+
) from exc
|
|
185
|
+
raise
|
|
186
|
+
|
|
187
|
+
def _document_state(
|
|
188
|
+
self,
|
|
189
|
+
location: SkillLocation,
|
|
190
|
+
*,
|
|
191
|
+
selected: bool = False,
|
|
192
|
+
) -> tuple[int, int | None]:
|
|
193
|
+
if location.source is SkillSource.BUILTIN:
|
|
194
|
+
resource = self._builtin_resource(location, selected=selected)
|
|
195
|
+
try:
|
|
196
|
+
if isinstance(resource, Path):
|
|
197
|
+
stat = resource.stat()
|
|
198
|
+
_validate_size(stat.st_size)
|
|
199
|
+
return stat.st_size, stat.st_mtime_ns
|
|
200
|
+
if isinstance(resource, zipfile.Path):
|
|
201
|
+
size = resource.root.getinfo(resource.at).file_size
|
|
202
|
+
else:
|
|
203
|
+
raise ValueError(
|
|
204
|
+
"Builtin Skill resource size cannot be determined without body I/O."
|
|
205
|
+
)
|
|
206
|
+
except OSError as exc:
|
|
207
|
+
if selected:
|
|
208
|
+
raise ContextError(
|
|
209
|
+
"Selected builtin Skill document is unreadable.",
|
|
210
|
+
code="SKILL_BODY_LOAD_FAILED",
|
|
211
|
+
retryable=_is_transient_io(exc),
|
|
212
|
+
) from exc
|
|
213
|
+
raise
|
|
214
|
+
_validate_size(size)
|
|
215
|
+
return size, None
|
|
216
|
+
try:
|
|
217
|
+
stat = self._filesystem_document(location, selected=selected).stat()
|
|
218
|
+
_validate_size(stat.st_size)
|
|
219
|
+
return stat.st_size, stat.st_mtime_ns
|
|
220
|
+
except OSError as exc:
|
|
221
|
+
if selected:
|
|
222
|
+
raise ContextError(
|
|
223
|
+
"Selected Skill document is unreadable.",
|
|
224
|
+
code="SKILL_BODY_LOAD_FAILED",
|
|
225
|
+
retryable=_is_transient_io(exc),
|
|
226
|
+
) from exc
|
|
227
|
+
raise
|
|
228
|
+
|
|
229
|
+
def _filesystem_document(
|
|
230
|
+
self,
|
|
231
|
+
location: SkillLocation,
|
|
232
|
+
*,
|
|
233
|
+
selected: bool = False,
|
|
234
|
+
) -> Path:
|
|
235
|
+
root = self._roots[location.source]
|
|
236
|
+
try:
|
|
237
|
+
canonical_root = root.resolve(strict=True)
|
|
238
|
+
except OSError as exc:
|
|
239
|
+
if selected:
|
|
240
|
+
raise ContextError(
|
|
241
|
+
"Selected Skill source root is unavailable.",
|
|
242
|
+
code="SKILL_BODY_LOAD_FAILED",
|
|
243
|
+
retryable=_is_transient_io(exc),
|
|
244
|
+
) from exc
|
|
245
|
+
raise ContextError(
|
|
246
|
+
f"Skill path is unsafe for {location.source.value}:"
|
|
247
|
+
f"{location.relative_path}.",
|
|
248
|
+
code="SKILL_PATH_UNSAFE",
|
|
249
|
+
) from exc
|
|
250
|
+
try:
|
|
251
|
+
document = canonical_root.joinpath(*location.relative_path.split("/")).resolve(
|
|
252
|
+
strict=True
|
|
253
|
+
)
|
|
254
|
+
except OSError as exc:
|
|
255
|
+
if selected:
|
|
256
|
+
raise ContextError(
|
|
257
|
+
"Selected Skill document is unavailable.",
|
|
258
|
+
code="SKILL_BODY_LOAD_FAILED",
|
|
259
|
+
retryable=_is_transient_io(exc),
|
|
260
|
+
) from exc
|
|
261
|
+
raise ContextError(
|
|
262
|
+
f"Skill path is unsafe for {location.source.value}:"
|
|
263
|
+
f"{location.relative_path}.",
|
|
264
|
+
code="SKILL_PATH_UNSAFE",
|
|
265
|
+
) from exc
|
|
266
|
+
try:
|
|
267
|
+
document.relative_to(canonical_root)
|
|
268
|
+
except ValueError as exc:
|
|
269
|
+
raise ContextError(
|
|
270
|
+
f"Skill path is unsafe for {location.source.value}:{location.relative_path}.",
|
|
271
|
+
code="SKILL_PATH_UNSAFE",
|
|
272
|
+
) from exc
|
|
273
|
+
if not document.is_file():
|
|
274
|
+
raise ContextError(
|
|
275
|
+
f"Skill path is not a regular file for {location.source.value}:"
|
|
276
|
+
f"{location.relative_path}.",
|
|
277
|
+
code="SKILL_PATH_UNSAFE",
|
|
278
|
+
)
|
|
279
|
+
return document
|
|
280
|
+
|
|
281
|
+
def _builtin_resource(
|
|
282
|
+
self,
|
|
283
|
+
location: SkillLocation,
|
|
284
|
+
*,
|
|
285
|
+
selected: bool = False,
|
|
286
|
+
) -> Traversable:
|
|
287
|
+
try:
|
|
288
|
+
root = files(self._builtin_package)
|
|
289
|
+
resource = root.joinpath(*location.relative_path.split("/"))
|
|
290
|
+
if isinstance(root, Path) and isinstance(resource, Path):
|
|
291
|
+
canonical_root = root.resolve(strict=True)
|
|
292
|
+
try:
|
|
293
|
+
canonical_resource = resource.resolve(strict=True)
|
|
294
|
+
except OSError as exc:
|
|
295
|
+
if selected:
|
|
296
|
+
raise ContextError(
|
|
297
|
+
"Selected builtin Skill document is unavailable.",
|
|
298
|
+
code="SKILL_BODY_LOAD_FAILED",
|
|
299
|
+
retryable=_is_transient_io(exc),
|
|
300
|
+
) from exc
|
|
301
|
+
raise
|
|
302
|
+
canonical_resource.relative_to(canonical_root)
|
|
303
|
+
resource = canonical_resource
|
|
304
|
+
if not resource.is_file():
|
|
305
|
+
if selected:
|
|
306
|
+
raise ContextError(
|
|
307
|
+
"Selected builtin Skill document is unavailable.",
|
|
308
|
+
code="SKILL_BODY_LOAD_FAILED",
|
|
309
|
+
)
|
|
310
|
+
raise ValueError("Builtin Skill is not a regular package resource.")
|
|
311
|
+
return resource
|
|
312
|
+
except ContextError:
|
|
313
|
+
raise
|
|
314
|
+
except ModuleNotFoundError as exc:
|
|
315
|
+
if selected:
|
|
316
|
+
raise ContextError(
|
|
317
|
+
"Selected builtin Skill package is unavailable.",
|
|
318
|
+
code="SKILL_BODY_LOAD_FAILED",
|
|
319
|
+
) from exc
|
|
320
|
+
raise ContextError(
|
|
321
|
+
f"Builtin Skill path is unsafe: {location.relative_path}.",
|
|
322
|
+
code="SKILL_PATH_UNSAFE",
|
|
323
|
+
) from exc
|
|
324
|
+
except (OSError, ValueError) as exc:
|
|
325
|
+
raise ContextError(
|
|
326
|
+
f"Builtin Skill path is unsafe: {location.relative_path}.",
|
|
327
|
+
code="SKILL_PATH_UNSAFE",
|
|
328
|
+
) from exc
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _read_front_matter(document: IO[bytes]) -> bytes:
|
|
332
|
+
consumed = 0
|
|
333
|
+
first = document.readline(MAX_FRONT_MATTER_BYTES + 1)
|
|
334
|
+
consumed += len(first)
|
|
335
|
+
if first.startswith(b"\xef\xbb\xbf"):
|
|
336
|
+
first = first[3:]
|
|
337
|
+
if _line_value(first) != b"+++":
|
|
338
|
+
raise ValueError("Skill front matter opening delimiter is missing.")
|
|
339
|
+
front_lines: list[bytes] = []
|
|
340
|
+
while consumed <= MAX_FRONT_MATTER_BYTES:
|
|
341
|
+
line = document.readline(MAX_FRONT_MATTER_BYTES + 1 - consumed)
|
|
342
|
+
consumed += len(line)
|
|
343
|
+
if not line:
|
|
344
|
+
raise ValueError("Skill front matter closing delimiter is missing.")
|
|
345
|
+
if consumed > MAX_FRONT_MATTER_BYTES:
|
|
346
|
+
break
|
|
347
|
+
if _line_value(line) == b"+++":
|
|
348
|
+
return b"".join(front_lines)
|
|
349
|
+
front_lines.append(line)
|
|
350
|
+
raise ValueError("Skill front matter exceeds the fixed size limit.")
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _line_value(line: bytes) -> bytes:
|
|
354
|
+
return line.removesuffix(b"\n").removesuffix(b"\r")
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _metadata(location: SkillLocation, front_bytes: bytes) -> SkillMetadata:
|
|
358
|
+
document = tomllib.loads(front_bytes.decode("utf-8"))
|
|
359
|
+
if set(document) != _ALLOWED_KEYS or not _REQUIRED_KEYS <= set(document):
|
|
360
|
+
raise ValueError("Skill front matter keys are invalid.")
|
|
361
|
+
skill_id = document["id"]
|
|
362
|
+
name = document["name"]
|
|
363
|
+
description = document["description"]
|
|
364
|
+
usage_scenario = document["usage_scenario"]
|
|
365
|
+
priority = document.get("priority", 0)
|
|
366
|
+
version = document["version"]
|
|
367
|
+
if not isinstance(version, str):
|
|
368
|
+
raise _VersionInvalid("Skill version must be a string.")
|
|
369
|
+
if (
|
|
370
|
+
not isinstance(skill_id, str)
|
|
371
|
+
or not isinstance(name, str)
|
|
372
|
+
or not isinstance(description, str)
|
|
373
|
+
or not isinstance(usage_scenario, str)
|
|
374
|
+
or isinstance(priority, bool)
|
|
375
|
+
or not isinstance(priority, int)
|
|
376
|
+
):
|
|
377
|
+
raise ValueError("Skill front matter field types are invalid.")
|
|
378
|
+
try:
|
|
379
|
+
return SkillMetadata(
|
|
380
|
+
skill_id=skill_id,
|
|
381
|
+
name=name,
|
|
382
|
+
description=description,
|
|
383
|
+
usage_scenario=usage_scenario,
|
|
384
|
+
priority=priority,
|
|
385
|
+
version=version,
|
|
386
|
+
source=location.source,
|
|
387
|
+
location=location,
|
|
388
|
+
)
|
|
389
|
+
except ValueError as exc:
|
|
390
|
+
if "SemVer" in str(exc):
|
|
391
|
+
raise _VersionInvalid(str(exc)) from exc
|
|
392
|
+
raise
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _validate_size(size: int) -> None:
|
|
396
|
+
if size > MAX_SKILL_FILE_BYTES:
|
|
397
|
+
raise ValueError("Skill document exceeds the fixed size limit.")
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _is_transient_io(error: OSError) -> bool:
|
|
401
|
+
return error.errno in {
|
|
402
|
+
errno.EINTR,
|
|
403
|
+
errno.EAGAIN,
|
|
404
|
+
errno.ENFILE,
|
|
405
|
+
errno.EMFILE,
|
|
406
|
+
errno.ETIMEDOUT,
|
|
407
|
+
} or getattr(error, "winerror", None) in {32, 33}
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _validate_body(body: str) -> None:
|
|
411
|
+
if not body.strip() or any(line == "+++" for line in body.splitlines()):
|
|
412
|
+
raise ContextError("Selected Skill body is invalid.", code="SKILL_BODY_INVALID")
|
|
413
|
+
headings: list[tuple[int, str]] = []
|
|
414
|
+
fence: str | None = None
|
|
415
|
+
lines = body.splitlines()
|
|
416
|
+
for index, line in enumerate(lines):
|
|
417
|
+
stripped = line.lstrip()
|
|
418
|
+
if stripped.startswith(("```", "~~~")):
|
|
419
|
+
marker = stripped[:3]
|
|
420
|
+
fence = None if fence == marker else marker if fence is None else fence
|
|
421
|
+
continue
|
|
422
|
+
if fence is None and line.startswith("## "):
|
|
423
|
+
headings.append((index, line))
|
|
424
|
+
for heading in _REQUIRED_HEADINGS:
|
|
425
|
+
if sum(value == heading for _, value in headings) != 1:
|
|
426
|
+
raise ContextError("Selected Skill body is invalid.", code="SKILL_BODY_INVALID")
|
|
427
|
+
positions = [
|
|
428
|
+
next(index for index, value in headings if value == heading)
|
|
429
|
+
for heading in _REQUIRED_HEADINGS
|
|
430
|
+
]
|
|
431
|
+
if positions != sorted(positions) or any(
|
|
432
|
+
value not in _REQUIRED_HEADINGS and index < positions[-1]
|
|
433
|
+
for index, value in headings
|
|
434
|
+
):
|
|
435
|
+
raise ContextError("Selected Skill body is invalid.", code="SKILL_BODY_INVALID")
|
|
436
|
+
for offset, position in enumerate(positions):
|
|
437
|
+
end = positions[offset + 1] if offset + 1 < len(positions) else len(lines)
|
|
438
|
+
if not "\n".join(lines[position + 1 : end]).strip():
|
|
439
|
+
raise ContextError("Selected Skill body is invalid.", code="SKILL_BODY_INVALID")
|
nexus/skills/registry.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Deterministic metadata catalog and post-selection Skill resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextvars import ContextVar
|
|
6
|
+
from importlib.resources import files
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from nexus.domain.ports.skills import SkillLoader
|
|
10
|
+
from nexus.domain.skills import (
|
|
11
|
+
SelectedSkill,
|
|
12
|
+
SkillLocation,
|
|
13
|
+
SkillMetadata,
|
|
14
|
+
SkillSelectionResult,
|
|
15
|
+
SkillSource,
|
|
16
|
+
)
|
|
17
|
+
from nexus.errors import ContextError
|
|
18
|
+
|
|
19
|
+
_PRECEDENCE = {
|
|
20
|
+
SkillSource.REPOSITORY: 0,
|
|
21
|
+
SkillSource.USER_GLOBAL: 1,
|
|
22
|
+
SkillSource.BUILTIN: 2,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DefaultSkillRegistry:
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
loader: SkillLoader,
|
|
30
|
+
repository_root: Path,
|
|
31
|
+
user_root: Path,
|
|
32
|
+
*,
|
|
33
|
+
builtin_package: str = "nexus.skills.builtin",
|
|
34
|
+
) -> None:
|
|
35
|
+
self._loader = loader
|
|
36
|
+
self._roots = {
|
|
37
|
+
SkillSource.REPOSITORY: repository_root,
|
|
38
|
+
SkillSource.USER_GLOBAL: user_root,
|
|
39
|
+
}
|
|
40
|
+
self._builtin_package = builtin_package
|
|
41
|
+
self._metadata: ContextVar[tuple[SkillMetadata, ...]] = ContextVar(
|
|
42
|
+
"skill_registry_metadata",
|
|
43
|
+
default=(),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
async def scan_metadata(self) -> tuple[SkillMetadata, ...]:
|
|
47
|
+
self._reset_loader_snapshots()
|
|
48
|
+
result: list[SkillMetadata] = []
|
|
49
|
+
for source in (
|
|
50
|
+
SkillSource.REPOSITORY,
|
|
51
|
+
SkillSource.USER_GLOBAL,
|
|
52
|
+
SkillSource.BUILTIN,
|
|
53
|
+
):
|
|
54
|
+
for location in self._locations(source):
|
|
55
|
+
result.append(await self._loader.load_metadata(location))
|
|
56
|
+
seen: set[tuple[SkillSource, str]] = set()
|
|
57
|
+
for metadata in result:
|
|
58
|
+
identity = (metadata.source, metadata.skill_id)
|
|
59
|
+
if identity in seen:
|
|
60
|
+
raise ContextError(
|
|
61
|
+
f"Duplicate Skill identity in {metadata.source.value}:"
|
|
62
|
+
f"{metadata.skill_id}.",
|
|
63
|
+
code="SKILL_ID_DUPLICATE",
|
|
64
|
+
)
|
|
65
|
+
seen.add(identity)
|
|
66
|
+
catalog = tuple(result)
|
|
67
|
+
self._metadata.set(catalog)
|
|
68
|
+
return catalog
|
|
69
|
+
|
|
70
|
+
def resolve_selected(
|
|
71
|
+
self,
|
|
72
|
+
selection: SkillSelectionResult,
|
|
73
|
+
) -> tuple[SkillMetadata, ...]:
|
|
74
|
+
grouped: dict[str, list[SkillMetadata]] = {}
|
|
75
|
+
for metadata in self._metadata.get():
|
|
76
|
+
grouped.setdefault(metadata.skill_id, []).append(metadata)
|
|
77
|
+
resolved: list[SkillMetadata] = []
|
|
78
|
+
for skill_id in selection.selected_skill_ids:
|
|
79
|
+
variants = grouped.get(skill_id)
|
|
80
|
+
if not variants:
|
|
81
|
+
raise ContextError(
|
|
82
|
+
"Skill selection referenced an unknown identity.",
|
|
83
|
+
code="SKILL_SELECTION_FAILED",
|
|
84
|
+
retryable=True,
|
|
85
|
+
)
|
|
86
|
+
resolved.append(min(variants, key=lambda value: _PRECEDENCE[value.source]))
|
|
87
|
+
return tuple(resolved)
|
|
88
|
+
|
|
89
|
+
async def load_selected(
|
|
90
|
+
self,
|
|
91
|
+
selection: SkillSelectionResult,
|
|
92
|
+
) -> tuple[SelectedSkill, ...]:
|
|
93
|
+
try:
|
|
94
|
+
selected: list[SelectedSkill] = []
|
|
95
|
+
for metadata in self.resolve_selected(selection):
|
|
96
|
+
selected.append(
|
|
97
|
+
SelectedSkill(
|
|
98
|
+
metadata,
|
|
99
|
+
await self._loader.load_body(metadata),
|
|
100
|
+
selection.selection_reason_summary,
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
return tuple(selected)
|
|
104
|
+
finally:
|
|
105
|
+
self._reset_loader_snapshots()
|
|
106
|
+
|
|
107
|
+
def _reset_loader_snapshots(self) -> None:
|
|
108
|
+
reset = getattr(self._loader, "_reset_snapshots", None)
|
|
109
|
+
if callable(reset):
|
|
110
|
+
reset()
|
|
111
|
+
|
|
112
|
+
def _locations(self, source: SkillSource) -> tuple[SkillLocation, ...]:
|
|
113
|
+
if source is SkillSource.BUILTIN:
|
|
114
|
+
try:
|
|
115
|
+
root = files(self._builtin_package)
|
|
116
|
+
names = sorted(
|
|
117
|
+
item.name
|
|
118
|
+
for item in root.iterdir()
|
|
119
|
+
if item.is_dir() and item.name != "__pycache__"
|
|
120
|
+
)
|
|
121
|
+
except (ModuleNotFoundError, OSError) as exc:
|
|
122
|
+
raise ContextError(
|
|
123
|
+
"Builtin Skill root is unreadable.",
|
|
124
|
+
code="SKILL_PATH_UNSAFE",
|
|
125
|
+
) from exc
|
|
126
|
+
else:
|
|
127
|
+
root_path = self._roots[source]
|
|
128
|
+
if not root_path.exists():
|
|
129
|
+
return ()
|
|
130
|
+
try:
|
|
131
|
+
canonical_root = root_path.resolve(strict=True)
|
|
132
|
+
if not canonical_root.is_dir():
|
|
133
|
+
raise OSError("Skill root is not a directory.")
|
|
134
|
+
names = sorted(item.name for item in canonical_root.iterdir() if item.is_dir())
|
|
135
|
+
except OSError as exc:
|
|
136
|
+
raise ContextError(
|
|
137
|
+
f"Skill root is unsafe for {source.value}.",
|
|
138
|
+
code="SKILL_PATH_UNSAFE",
|
|
139
|
+
) from exc
|
|
140
|
+
try:
|
|
141
|
+
return tuple(SkillLocation(source, f"{name}/SKILL.md") for name in names)
|
|
142
|
+
except ValueError as exc:
|
|
143
|
+
raise ContextError(
|
|
144
|
+
f"Skill directory name is invalid for {source.value}.",
|
|
145
|
+
code="SKILL_METADATA_INVALID",
|
|
146
|
+
) from exc
|