agentskills-http 0.3.0__tar.gz → 0.4.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.
- {agentskills_http-0.3.0 → agentskills_http-0.4.0}/PKG-INFO +25 -2
- {agentskills_http-0.3.0 → agentskills_http-0.4.0}/README.md +23 -0
- {agentskills_http-0.3.0 → agentskills_http-0.4.0}/agentskills_http/static.py +132 -43
- {agentskills_http-0.3.0 → agentskills_http-0.4.0}/pyproject.toml +2 -2
- {agentskills_http-0.3.0 → agentskills_http-0.4.0}/agentskills_http/__init__.py +0 -0
- {agentskills_http-0.3.0 → agentskills_http-0.4.0}/agentskills_http/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentskills-http
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: HTTP-based skill providers for the Agent Skills format (https://agentskills.io)
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Pratik Panda
|
|
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.13
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.14
|
|
15
15
|
Classifier: Topic :: Software Development :: Libraries
|
|
16
|
-
Requires-Dist: agentskills-core (>=0.
|
|
16
|
+
Requires-Dist: agentskills-core (>=0.4.0,<1.0)
|
|
17
17
|
Requires-Dist: httpx (>=0.27,<1.0)
|
|
18
18
|
Requires-Dist: pyyaml (>=6.0,<7.0)
|
|
19
19
|
Project-URL: Homepage, https://agentskills.io
|
|
@@ -108,6 +108,7 @@ provider = HTTPStaticFileSkillProvider("https://cdn.example.com/skills", client=
|
|
|
108
108
|
| `max_response_bytes` | `int` | `10_485_760` | Maximum allowed response size in bytes |
|
|
109
109
|
| `revalidate` | `bool` | `False` | Re-check cached `SKILL.md` on every access with `If-None-Match` / `If-Modified-Since` |
|
|
110
110
|
| `resource_manifest` | `bool` | `False` | Enable `list_resources()` by reading a per-skill `index.json` |
|
|
111
|
+
| `skill_manifest` | `bool` | `False` | Enable `discover()` by reading a root `index.json` |
|
|
111
112
|
| `timeout` | `float` | `30.0` | Request timeout in seconds (ignored when you supply `client`) |
|
|
112
113
|
| `max_retries` | `int` | `2` | Retries after the initial attempt, for retryable failures only |
|
|
113
114
|
| `retry_backoff` | `float` | `0.5` | Base delay in seconds for exponential backoff |
|
|
@@ -123,6 +124,7 @@ provider = HTTPStaticFileSkillProvider("https://cdn.example.com/skills", client=
|
|
|
123
124
|
| `get_asset(skill_id, name)` | `bytes` | Raw asset content |
|
|
124
125
|
| `get_reference(skill_id, name)` | `bytes` | Raw reference content |
|
|
125
126
|
| `list_resources(skill_id)` | `dict[str, list[str]]` | Resource names from `index.json` (requires `resource_manifest=True`) |
|
|
127
|
+
| `discover()` | `list[str]` | Skill IDs from the root `index.json` (requires `skill_manifest=True`) |
|
|
126
128
|
| `invalidate(skill_id=None)` | `None` | Drop cached `SKILL.md` content for one skill, or all skills |
|
|
127
129
|
| `aclose()` | `None` | Close the HTTP client (if owned by the provider) |
|
|
128
130
|
|
|
@@ -151,6 +153,27 @@ listing = await provider.list_resources("incident-response")
|
|
|
151
153
|
|
|
152
154
|
Missing categories default to empty lists. A manifest is host-supplied data whose entries are later interpolated into URLs, so names failing the identifier-safety check are dropped. If a given skill has no `index.json`, `list_resources()` raises `ResourceListingNotSupportedError` for that skill — again, not an empty result.
|
|
153
155
|
|
|
156
|
+
## Skill Discovery
|
|
157
|
+
|
|
158
|
+
The same problem one level up: nothing on a static host says which skills exist. Publish the same
|
|
159
|
+
file at the root, listing skills instead of resources:
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{ "skills": ["incident-response", "api-style-guide"] }
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Then opt in and register the whole host at once:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
async with HTTPStaticFileSkillProvider(BASE, skill_manifest=True) as provider:
|
|
169
|
+
await registry.register_all(provider)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
One filename and one shape — an object mapping a category to a list of names — at two depths,
|
|
173
|
+
rather than two manifest formats to keep in step. Unsafe and duplicate IDs are dropped as above.
|
|
174
|
+
Without `skill_manifest=True`, or when the root publishes no manifest, `discover()` raises
|
|
175
|
+
`DiscoveryNotSupportedError`.
|
|
176
|
+
|
|
154
177
|
## Caching
|
|
155
178
|
|
|
156
179
|
`SKILL.md` responses are cached per provider instance. Without it a single skill costs up to five round-trips per agent session — twice during registration, once per catalog build, and again on each tool call. Scripts, assets and references are not cached.
|
|
@@ -86,6 +86,7 @@ provider = HTTPStaticFileSkillProvider("https://cdn.example.com/skills", client=
|
|
|
86
86
|
| `max_response_bytes` | `int` | `10_485_760` | Maximum allowed response size in bytes |
|
|
87
87
|
| `revalidate` | `bool` | `False` | Re-check cached `SKILL.md` on every access with `If-None-Match` / `If-Modified-Since` |
|
|
88
88
|
| `resource_manifest` | `bool` | `False` | Enable `list_resources()` by reading a per-skill `index.json` |
|
|
89
|
+
| `skill_manifest` | `bool` | `False` | Enable `discover()` by reading a root `index.json` |
|
|
89
90
|
| `timeout` | `float` | `30.0` | Request timeout in seconds (ignored when you supply `client`) |
|
|
90
91
|
| `max_retries` | `int` | `2` | Retries after the initial attempt, for retryable failures only |
|
|
91
92
|
| `retry_backoff` | `float` | `0.5` | Base delay in seconds for exponential backoff |
|
|
@@ -101,6 +102,7 @@ provider = HTTPStaticFileSkillProvider("https://cdn.example.com/skills", client=
|
|
|
101
102
|
| `get_asset(skill_id, name)` | `bytes` | Raw asset content |
|
|
102
103
|
| `get_reference(skill_id, name)` | `bytes` | Raw reference content |
|
|
103
104
|
| `list_resources(skill_id)` | `dict[str, list[str]]` | Resource names from `index.json` (requires `resource_manifest=True`) |
|
|
105
|
+
| `discover()` | `list[str]` | Skill IDs from the root `index.json` (requires `skill_manifest=True`) |
|
|
104
106
|
| `invalidate(skill_id=None)` | `None` | Drop cached `SKILL.md` content for one skill, or all skills |
|
|
105
107
|
| `aclose()` | `None` | Close the HTTP client (if owned by the provider) |
|
|
106
108
|
|
|
@@ -129,6 +131,27 @@ listing = await provider.list_resources("incident-response")
|
|
|
129
131
|
|
|
130
132
|
Missing categories default to empty lists. A manifest is host-supplied data whose entries are later interpolated into URLs, so names failing the identifier-safety check are dropped. If a given skill has no `index.json`, `list_resources()` raises `ResourceListingNotSupportedError` for that skill — again, not an empty result.
|
|
131
133
|
|
|
134
|
+
## Skill Discovery
|
|
135
|
+
|
|
136
|
+
The same problem one level up: nothing on a static host says which skills exist. Publish the same
|
|
137
|
+
file at the root, listing skills instead of resources:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{ "skills": ["incident-response", "api-style-guide"] }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Then opt in and register the whole host at once:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
async with HTTPStaticFileSkillProvider(BASE, skill_manifest=True) as provider:
|
|
147
|
+
await registry.register_all(provider)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
One filename and one shape — an object mapping a category to a list of names — at two depths,
|
|
151
|
+
rather than two manifest formats to keep in step. Unsafe and duplicate IDs are dropped as above.
|
|
152
|
+
Without `skill_manifest=True`, or when the root publishes no manifest, `discover()` raises
|
|
153
|
+
`DiscoveryNotSupportedError`.
|
|
154
|
+
|
|
132
155
|
## Caching
|
|
133
156
|
|
|
134
157
|
`SKILL.md` responses are cached per provider instance. Without it a single skill costs up to five round-trips per agent session — twice during registration, once per catalog build, and again on each tool call. Scripts, assets and references are not cached.
|
|
@@ -16,11 +16,15 @@ Expected URL layout::
|
|
|
16
16
|
└── another-skill/
|
|
17
17
|
└── SKILL.md
|
|
18
18
|
|
|
19
|
-
The provider is a pure content accessor —
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
The provider is a pure content accessor by default — a plain static host
|
|
20
|
+
cannot be enumerated, so registration is explicit via
|
|
21
|
+
:meth:`SkillRegistry.register <agentskills_core.SkillRegistry.register>` and
|
|
22
|
+
resource names come from the skill body. A host that publishes
|
|
23
|
+
``index.json`` manifests can enable both optional capabilities: a manifest
|
|
24
|
+
at ``{base_url}/index.json`` lists the skills, and one at
|
|
25
|
+
``{base_url}/{skill_id}/index.json`` lists that skill's resources. Same
|
|
26
|
+
filename, same shape — a JSON object mapping a category to a list of names
|
|
27
|
+
— at two depths, rather than two manifest formats to keep in step.
|
|
24
28
|
|
|
25
29
|
All methods are ``async`` and use `httpx <https://www.python-httpx.org/>`_
|
|
26
30
|
for non-blocking HTTP requests.
|
|
@@ -44,6 +48,7 @@ import httpx
|
|
|
44
48
|
from agentskills_core import (
|
|
45
49
|
RESOURCE_KINDS,
|
|
46
50
|
AgentSkillsError,
|
|
51
|
+
DiscoveryNotSupportedError,
|
|
47
52
|
ResourceListingNotSupportedError,
|
|
48
53
|
ResourceNotFoundError,
|
|
49
54
|
SkillNotFoundError,
|
|
@@ -83,8 +88,12 @@ _NOT_FOUND_STATUS_CODES: frozenset[int] = frozenset({404, 410})
|
|
|
83
88
|
#: Non-5xx statuses worth retrying.
|
|
84
89
|
_RETRYABLE_STATUS_CODES: frozenset[int] = frozenset({408, 425, 429})
|
|
85
90
|
|
|
86
|
-
#:
|
|
87
|
-
|
|
91
|
+
#: Manifest filename. Served at the root it lists skills; served inside a
|
|
92
|
+
#: skill it lists that skill's resources.
|
|
93
|
+
MANIFEST_NAME: str = "index.json"
|
|
94
|
+
|
|
95
|
+
#: Manifest key holding skill IDs in a root manifest.
|
|
96
|
+
SKILLS_MANIFEST_KEY: str = "skills"
|
|
88
97
|
|
|
89
98
|
|
|
90
99
|
def _parse_retry_after(value: str | None) -> float | None:
|
|
@@ -120,7 +129,9 @@ class HTTPStaticFileSkillProvider(SkillProvider):
|
|
|
120
129
|
GitHub Pages, etc.) that hosts skill files at predictable URL paths.
|
|
121
130
|
Resource names (scripts, assets, references) are discovered by the
|
|
122
131
|
agent from the skill body, or from an optional per-skill
|
|
123
|
-
``index.json`` manifest -- see *resource_manifest*.
|
|
132
|
+
``index.json`` manifest -- see *resource_manifest*. The set of
|
|
133
|
+
skills is likewise unknowable over plain HTTP unless the host
|
|
134
|
+
publishes a root ``index.json`` -- see *skill_manifest*.
|
|
124
135
|
|
|
125
136
|
The provider owns an :class:`httpx.AsyncClient` for connection
|
|
126
137
|
pooling. If you supply your own client the provider will use it
|
|
@@ -158,6 +169,13 @@ class HTTPStaticFileSkillProvider(SkillProvider):
|
|
|
158
169
|
plain static host cannot be enumerated and claiming
|
|
159
170
|
otherwise would make missing manifests look like skills with
|
|
160
171
|
no resources.
|
|
172
|
+
skill_manifest: Set ``True`` if the host publishes a root
|
|
173
|
+
``index.json`` with a ``"skills"`` list. Enables
|
|
174
|
+
:meth:`discover`, and so
|
|
175
|
+
:meth:`SkillRegistry.register_all
|
|
176
|
+
<agentskills_core.SkillRegistry.register_all>`. Defaults to
|
|
177
|
+
``False`` for the same reason: without a manifest, "no
|
|
178
|
+
skills found" would be a lie.
|
|
161
179
|
timeout: Request timeout in seconds. Ignored when you supply
|
|
162
180
|
your own *client*.
|
|
163
181
|
max_retries: Retries after the initial attempt, for retryable
|
|
@@ -199,6 +217,7 @@ class HTTPStaticFileSkillProvider(SkillProvider):
|
|
|
199
217
|
max_response_bytes: int = DEFAULT_MAX_RESPONSE_BYTES,
|
|
200
218
|
revalidate: bool = False,
|
|
201
219
|
resource_manifest: bool = False,
|
|
220
|
+
skill_manifest: bool = False,
|
|
202
221
|
timeout: float = DEFAULT_TIMEOUT_SECONDS,
|
|
203
222
|
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
204
223
|
retry_backoff: float = DEFAULT_RETRY_BACKOFF_SECONDS,
|
|
@@ -237,6 +256,7 @@ class HTTPStaticFileSkillProvider(SkillProvider):
|
|
|
237
256
|
self._revalidate = revalidate
|
|
238
257
|
self._skill_md_cache: dict[str, _CachedSkillMd] = {}
|
|
239
258
|
self.supports_resource_listing = resource_manifest
|
|
259
|
+
self.supports_discovery = skill_manifest
|
|
240
260
|
self._max_retries = max_retries
|
|
241
261
|
self._retry_backoff = retry_backoff
|
|
242
262
|
self._max_retry_delay = max_retry_delay
|
|
@@ -277,14 +297,20 @@ class HTTPStaticFileSkillProvider(SkillProvider):
|
|
|
277
297
|
# ------------------------------------------------------------------
|
|
278
298
|
|
|
279
299
|
@staticmethod
|
|
280
|
-
def _validate_identifier(value: str, label: str) -> None:
|
|
281
|
-
"""Raise
|
|
300
|
+
def _validate_identifier(value: str, label: str, error: type[AgentSkillsError]) -> None:
|
|
301
|
+
"""Raise *error* if *value* is not a safe URL path segment.
|
|
282
302
|
|
|
283
303
|
Prevents path-traversal attacks (e.g. ``../``) and other
|
|
284
304
|
injection via ``skill_id`` or resource ``name``.
|
|
305
|
+
|
|
306
|
+
*error* is the exception the calling method documents — a
|
|
307
|
+
rejected identifier is indistinguishable from a missing one from
|
|
308
|
+
the caller's side, and raising something outside the
|
|
309
|
+
:class:`~agentskills_core.AgentSkillsError` hierarchy would force
|
|
310
|
+
callers to special-case this provider.
|
|
285
311
|
"""
|
|
286
312
|
if not _SAFE_IDENTIFIER_RE.match(value):
|
|
287
|
-
raise
|
|
313
|
+
raise error(
|
|
288
314
|
f"Invalid {label}: {value!r} — must start with an "
|
|
289
315
|
f"alphanumeric character and contain only alphanumeric "
|
|
290
316
|
f"characters, hyphens, dots, and underscores"
|
|
@@ -413,58 +439,121 @@ class HTTPStaticFileSkillProvider(SkillProvider):
|
|
|
413
439
|
ResourceListingNotSupportedError: If the provider was built
|
|
414
440
|
without ``resource_manifest=True``, or the skill has no
|
|
415
441
|
published manifest.
|
|
442
|
+
SkillNotFoundError: If the skill itself does not exist.
|
|
416
443
|
AgentSkillsError: If the manifest is not a JSON object.
|
|
417
444
|
"""
|
|
418
445
|
if not self.supports_resource_listing:
|
|
419
446
|
raise ResourceListingNotSupportedError(
|
|
420
447
|
"This provider was not configured with a resource manifest. "
|
|
421
448
|
"A static HTTP host cannot be enumerated. Pass "
|
|
422
|
-
"resource_manifest=True if the host publishes "
|
|
423
|
-
|
|
424
|
-
"names from the skill body."
|
|
449
|
+
f"resource_manifest=True if the host publishes {MANIFEST_NAME} "
|
|
450
|
+
"per skill, otherwise take resource names from the skill body."
|
|
425
451
|
)
|
|
426
452
|
|
|
427
|
-
self._validate_identifier(skill_id, "skill_id")
|
|
428
|
-
url = f"{self._base_url}/{quote(skill_id, safe='')}/{
|
|
453
|
+
self._validate_identifier(skill_id, "skill_id", SkillNotFoundError)
|
|
454
|
+
url = f"{self._base_url}/{quote(skill_id, safe='')}/{MANIFEST_NAME}"
|
|
455
|
+
subject = f"Resource manifest for skill {skill_id!r}"
|
|
429
456
|
try:
|
|
430
|
-
|
|
457
|
+
manifest = await self._fetch_manifest(url, subject)
|
|
431
458
|
except ResourceNotFoundError as exc:
|
|
459
|
+
# A 404 here means either "this skill publishes no manifest" or
|
|
460
|
+
# "there is no such skill", and only SKILL.md can tell them
|
|
461
|
+
# apart. The extra request is on the failure path, and its
|
|
462
|
+
# result is cached for the caller who asks next.
|
|
463
|
+
await self._get_skill_md(skill_id)
|
|
432
464
|
raise ResourceListingNotSupportedError(
|
|
433
|
-
f"No {
|
|
465
|
+
f"No {MANIFEST_NAME} manifest published for skill "
|
|
434
466
|
f"{skill_id!r}. Take resource names from the skill body instead."
|
|
435
467
|
) from exc
|
|
436
468
|
|
|
437
|
-
|
|
438
|
-
manifest = json.loads(raw)
|
|
439
|
-
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
440
|
-
raise AgentSkillsError(
|
|
441
|
-
f"Resource manifest for skill {skill_id!r} is not valid JSON"
|
|
442
|
-
) from exc
|
|
469
|
+
return {kind: self._manifest_names(manifest, kind, subject) for kind in RESOURCE_KINDS}
|
|
443
470
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
)
|
|
471
|
+
# ------------------------------------------------------------------
|
|
472
|
+
# Skill discovery
|
|
473
|
+
# ------------------------------------------------------------------
|
|
448
474
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
475
|
+
async def discover(self) -> list[str]:
|
|
476
|
+
"""List the host's skills from the root ``index.json`` manifest.
|
|
477
|
+
|
|
478
|
+
Requires ``skill_manifest=True``. The manifest is a JSON object
|
|
479
|
+
at ``{base_url}/index.json`` with a ``"skills"`` list::
|
|
480
|
+
|
|
481
|
+
{"skills": ["incident-response", "api-style-guide"]}
|
|
482
|
+
|
|
483
|
+
It is the same file format as the per-skill resource manifest,
|
|
484
|
+
one level up, so a host publishing both has one shape to
|
|
485
|
+
generate rather than two. Other keys are ignored, and entries
|
|
486
|
+
that are not valid skill IDs are dropped -- a manifest is
|
|
487
|
+
host-supplied data and an ID is later interpolated into a URL.
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
Sorted skill IDs. Empty when the manifest lists none.
|
|
491
|
+
|
|
492
|
+
Raises:
|
|
493
|
+
DiscoveryNotSupportedError: If the provider was built
|
|
494
|
+
without ``skill_manifest=True``, or the host publishes
|
|
495
|
+
no root manifest.
|
|
496
|
+
AgentSkillsError: If the manifest is not a JSON object.
|
|
497
|
+
"""
|
|
498
|
+
if not self.supports_discovery:
|
|
499
|
+
raise DiscoveryNotSupportedError(
|
|
500
|
+
"This provider was not configured with a skill manifest. "
|
|
501
|
+
"A static HTTP host cannot be enumerated. Pass "
|
|
502
|
+
f"skill_manifest=True if the host publishes a root {MANIFEST_NAME}, "
|
|
503
|
+
"otherwise register skills explicitly."
|
|
460
504
|
)
|
|
461
505
|
|
|
462
|
-
|
|
506
|
+
url = f"{self._base_url}/{MANIFEST_NAME}"
|
|
507
|
+
subject = "Skill manifest"
|
|
508
|
+
try:
|
|
509
|
+
manifest = await self._fetch_manifest(url, subject)
|
|
510
|
+
except ResourceNotFoundError as exc:
|
|
511
|
+
raise DiscoveryNotSupportedError(
|
|
512
|
+
f"No {MANIFEST_NAME} manifest published at {self._describe(url)}. "
|
|
513
|
+
"Register skills explicitly instead."
|
|
514
|
+
) from exc
|
|
515
|
+
|
|
516
|
+
skill_ids = self._manifest_names(manifest, SKILLS_MANIFEST_KEY, subject)
|
|
517
|
+
_logger.debug("Discovered %d skills from %s", len(skill_ids), self._describe(url))
|
|
518
|
+
return skill_ids
|
|
463
519
|
|
|
464
520
|
# ------------------------------------------------------------------
|
|
465
521
|
# Internal helpers
|
|
466
522
|
# ------------------------------------------------------------------
|
|
467
523
|
|
|
524
|
+
async def _fetch_manifest(self, url: str, subject: str) -> dict[str, Any]:
|
|
525
|
+
"""Fetch and parse an ``index.json`` manifest.
|
|
526
|
+
|
|
527
|
+
Raises:
|
|
528
|
+
ResourceNotFoundError: If no manifest is published there.
|
|
529
|
+
Callers translate this into the "cannot enumerate"
|
|
530
|
+
error their own capability documents.
|
|
531
|
+
AgentSkillsError: If the manifest is not a JSON object.
|
|
532
|
+
"""
|
|
533
|
+
raw = await self._get_bytes(url)
|
|
534
|
+
try:
|
|
535
|
+
manifest = json.loads(raw)
|
|
536
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
537
|
+
raise AgentSkillsError(f"{subject} is not valid JSON") from exc
|
|
538
|
+
if not isinstance(manifest, dict):
|
|
539
|
+
raise AgentSkillsError(f"{subject} must be a JSON object")
|
|
540
|
+
return manifest
|
|
541
|
+
|
|
542
|
+
@staticmethod
|
|
543
|
+
def _manifest_names(manifest: dict[str, Any], key: str, subject: str) -> list[str]:
|
|
544
|
+
"""Return the safe, sorted, de-duplicated names under *key*.
|
|
545
|
+
|
|
546
|
+
A missing key means an empty list. Unsafe entries are dropped
|
|
547
|
+
rather than raising: one bad name in a host-supplied manifest
|
|
548
|
+
should not make the whole listing unavailable.
|
|
549
|
+
"""
|
|
550
|
+
entries = manifest.get(key) or []
|
|
551
|
+
if not isinstance(entries, list):
|
|
552
|
+
raise AgentSkillsError(f"{subject} has a non-list value for {key!r}")
|
|
553
|
+
return sorted(
|
|
554
|
+
{name for name in entries if isinstance(name, str) and _SAFE_IDENTIFIER_RE.match(name)}
|
|
555
|
+
)
|
|
556
|
+
|
|
468
557
|
def _describe(self, url: str) -> str:
|
|
469
558
|
"""Describe *url* for an error message or log record without leaking secrets.
|
|
470
559
|
|
|
@@ -611,7 +700,7 @@ class HTTPStaticFileSkillProvider(SkillProvider):
|
|
|
611
700
|
|
|
612
701
|
async def _get_skill_md(self, skill_id: str) -> str:
|
|
613
702
|
"""Fetch a skill's ``SKILL.md``, serving from cache when possible."""
|
|
614
|
-
self._validate_identifier(skill_id, "skill_id")
|
|
703
|
+
self._validate_identifier(skill_id, "skill_id", SkillNotFoundError)
|
|
615
704
|
url = f"{self._base_url}/{quote(skill_id, safe='')}/SKILL.md"
|
|
616
705
|
|
|
617
706
|
cached = self._skill_md_cache.get(skill_id)
|
|
@@ -644,7 +733,7 @@ class HTTPStaticFileSkillProvider(SkillProvider):
|
|
|
644
733
|
|
|
645
734
|
async def _get_resource(self, skill_id: str, subdir: str, name: str) -> bytes:
|
|
646
735
|
"""Fetch a single resource file from a skill subdirectory."""
|
|
647
|
-
self._validate_identifier(skill_id, "skill_id")
|
|
648
|
-
self._validate_identifier(name, "resource name")
|
|
736
|
+
self._validate_identifier(skill_id, "skill_id", SkillNotFoundError)
|
|
737
|
+
self._validate_identifier(name, "resource name", ResourceNotFoundError)
|
|
649
738
|
url = f"{self._base_url}/{quote(skill_id, safe='')}/{subdir}/{quote(name, safe='')}"
|
|
650
739
|
return await self._get_bytes(url)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "agentskills-http"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "HTTP-based skill providers for the Agent Skills format (https://agentskills.io)"
|
|
5
5
|
license = "MIT"
|
|
6
6
|
authors = ["Pratik Panda"]
|
|
@@ -19,7 +19,7 @@ classifiers = [
|
|
|
19
19
|
|
|
20
20
|
[tool.poetry.dependencies]
|
|
21
21
|
python = ">=3.12,<4.0"
|
|
22
|
-
agentskills-core = ">=0.
|
|
22
|
+
agentskills-core = ">=0.4.0,<1.0"
|
|
23
23
|
httpx = ">=0.27,<1.0"
|
|
24
24
|
pyyaml = ">=6.0,<7.0"
|
|
25
25
|
|
|
File without changes
|
|
File without changes
|