agentskills-http 0.2.0__tar.gz → 0.2.1__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.2.0 → agentskills_http-0.2.1}/PKG-INFO +36 -5
- {agentskills_http-0.2.0 → agentskills_http-0.2.1}/README.md +32 -1
- {agentskills_http-0.2.0 → agentskills_http-0.2.1}/agentskills_http/static.py +95 -12
- {agentskills_http-0.2.0 → agentskills_http-0.2.1}/pyproject.toml +4 -4
- {agentskills_http-0.2.0 → agentskills_http-0.2.1}/agentskills_http/__init__.py +0 -0
- {agentskills_http-0.2.0 → agentskills_http-0.2.1}/agentskills_http/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentskills-http
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
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,9 +13,9 @@ 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.1.0)
|
|
17
|
-
Requires-Dist: httpx (>=0.27)
|
|
18
|
-
Requires-Dist: pyyaml (>=6.0)
|
|
16
|
+
Requires-Dist: agentskills-core (>=0.1.0,<1.0)
|
|
17
|
+
Requires-Dist: httpx (>=0.27,<1.0)
|
|
18
|
+
Requires-Dist: pyyaml (>=6.0,<7.0)
|
|
19
19
|
Project-URL: Homepage, https://agentskills.io
|
|
20
20
|
Project-URL: Repository, https://github.com/pratikxpanda/agentskills-sdk
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
@@ -93,7 +93,18 @@ provider = HTTPStaticFileSkillProvider("https://cdn.example.com/skills", client=
|
|
|
93
93
|
|
|
94
94
|
## API
|
|
95
95
|
|
|
96
|
-
### `HTTPStaticFileSkillProvider(base_url, *, client=None, headers=None)`
|
|
96
|
+
### `HTTPStaticFileSkillProvider(base_url, *, client=None, headers=None, params=None, require_tls=False, max_response_bytes=10_485_760)`
|
|
97
|
+
|
|
98
|
+
| Parameter | Type | Default | Description |
|
|
99
|
+
| --- | --- | --- | --- |
|
|
100
|
+
| `base_url` | `str` | — | Root URL where the skill tree is hosted |
|
|
101
|
+
| `client` | `AsyncClient \| None` | `None` | Pre-configured httpx client (caller manages lifecycle) |
|
|
102
|
+
| `headers` | `dict \| None` | `None` | Extra headers sent with every request |
|
|
103
|
+
| `params` | `dict \| None` | `None` | Query parameters appended to every request |
|
|
104
|
+
| `require_tls` | `bool` | `False` | Reject `http://` URLs with `ValueError` |
|
|
105
|
+
| `max_response_bytes` | `int` | `10_485_760` | Maximum allowed response size in bytes |
|
|
106
|
+
|
|
107
|
+
> **Note:** `client` and `headers`/`params` are mutually exclusive. Configure headers and params on the client directly when providing your own.
|
|
97
108
|
|
|
98
109
|
| Method | Returns | Description |
|
|
99
110
|
| --- | --- | --- |
|
|
@@ -117,6 +128,26 @@ Supports `async with` for automatic cleanup.
|
|
|
117
128
|
|
|
118
129
|
All exceptions inherit from `AgentSkillsError`.
|
|
119
130
|
|
|
131
|
+
## Security
|
|
132
|
+
|
|
133
|
+
- **Input validation** — Skill IDs and resource names are validated against a safe-character pattern (`^[a-zA-Z0-9][a-zA-Z0-9._-]*$`) to prevent path-traversal and injection attacks.
|
|
134
|
+
- **TLS warnings** — A `UserWarning` is emitted when `base_url` uses unencrypted HTTP. Set `require_tls=True` to reject HTTP URLs entirely.
|
|
135
|
+
- **Redirect protection** — The internally-created HTTP client does not follow redirects by default, preventing open-redirect SSRF.
|
|
136
|
+
- **Timeouts** — Default 30-second timeout on all HTTP requests.
|
|
137
|
+
- **Response size limits** — Responses exceeding 10 MB (default) are rejected before processing. Configure via `max_response_bytes`.
|
|
138
|
+
- **Error-message sanitization** — Error messages omit URLs and include only status codes and generic descriptions, preventing internal URL leakage.
|
|
139
|
+
|
|
140
|
+
For the full security policy, see [SECURITY.md](../../../SECURITY.md).
|
|
141
|
+
|
|
142
|
+
## Deployment Considerations
|
|
143
|
+
|
|
144
|
+
- **Rate limiting** — The SDK does not enforce rate limits on MCP tool
|
|
145
|
+
calls or HTTP requests. Deploy behind a reverse proxy or API gateway
|
|
146
|
+
that provides rate limiting in production environments.
|
|
147
|
+
- **Credential management** — Do not store secrets (API keys, SAS
|
|
148
|
+
tokens, Authorization headers) in config files committed to version
|
|
149
|
+
control. Use environment variables or a secret manager instead.
|
|
150
|
+
|
|
120
151
|
## License
|
|
121
152
|
|
|
122
153
|
MIT
|
|
@@ -71,7 +71,18 @@ provider = HTTPStaticFileSkillProvider("https://cdn.example.com/skills", client=
|
|
|
71
71
|
|
|
72
72
|
## API
|
|
73
73
|
|
|
74
|
-
### `HTTPStaticFileSkillProvider(base_url, *, client=None, headers=None)`
|
|
74
|
+
### `HTTPStaticFileSkillProvider(base_url, *, client=None, headers=None, params=None, require_tls=False, max_response_bytes=10_485_760)`
|
|
75
|
+
|
|
76
|
+
| Parameter | Type | Default | Description |
|
|
77
|
+
| --- | --- | --- | --- |
|
|
78
|
+
| `base_url` | `str` | — | Root URL where the skill tree is hosted |
|
|
79
|
+
| `client` | `AsyncClient \| None` | `None` | Pre-configured httpx client (caller manages lifecycle) |
|
|
80
|
+
| `headers` | `dict \| None` | `None` | Extra headers sent with every request |
|
|
81
|
+
| `params` | `dict \| None` | `None` | Query parameters appended to every request |
|
|
82
|
+
| `require_tls` | `bool` | `False` | Reject `http://` URLs with `ValueError` |
|
|
83
|
+
| `max_response_bytes` | `int` | `10_485_760` | Maximum allowed response size in bytes |
|
|
84
|
+
|
|
85
|
+
> **Note:** `client` and `headers`/`params` are mutually exclusive. Configure headers and params on the client directly when providing your own.
|
|
75
86
|
|
|
76
87
|
| Method | Returns | Description |
|
|
77
88
|
| --- | --- | --- |
|
|
@@ -95,6 +106,26 @@ Supports `async with` for automatic cleanup.
|
|
|
95
106
|
|
|
96
107
|
All exceptions inherit from `AgentSkillsError`.
|
|
97
108
|
|
|
109
|
+
## Security
|
|
110
|
+
|
|
111
|
+
- **Input validation** — Skill IDs and resource names are validated against a safe-character pattern (`^[a-zA-Z0-9][a-zA-Z0-9._-]*$`) to prevent path-traversal and injection attacks.
|
|
112
|
+
- **TLS warnings** — A `UserWarning` is emitted when `base_url` uses unencrypted HTTP. Set `require_tls=True` to reject HTTP URLs entirely.
|
|
113
|
+
- **Redirect protection** — The internally-created HTTP client does not follow redirects by default, preventing open-redirect SSRF.
|
|
114
|
+
- **Timeouts** — Default 30-second timeout on all HTTP requests.
|
|
115
|
+
- **Response size limits** — Responses exceeding 10 MB (default) are rejected before processing. Configure via `max_response_bytes`.
|
|
116
|
+
- **Error-message sanitization** — Error messages omit URLs and include only status codes and generic descriptions, preventing internal URL leakage.
|
|
117
|
+
|
|
118
|
+
For the full security policy, see [SECURITY.md](../../../SECURITY.md).
|
|
119
|
+
|
|
120
|
+
## Deployment Considerations
|
|
121
|
+
|
|
122
|
+
- **Rate limiting** — The SDK does not enforce rate limits on MCP tool
|
|
123
|
+
calls or HTTP requests. Deploy behind a reverse proxy or API gateway
|
|
124
|
+
that provides rate limiting in production environments.
|
|
125
|
+
- **Credential management** — Do not store secrets (API keys, SAS
|
|
126
|
+
tokens, Authorization headers) in config files committed to version
|
|
127
|
+
control. Use environment variables or a secret manager instead.
|
|
128
|
+
|
|
98
129
|
## License
|
|
99
130
|
|
|
100
131
|
MIT
|
|
@@ -28,8 +28,11 @@ for non-blocking HTTP requests.
|
|
|
28
28
|
|
|
29
29
|
from __future__ import annotations
|
|
30
30
|
|
|
31
|
+
import logging
|
|
32
|
+
import re
|
|
33
|
+
import warnings
|
|
31
34
|
from typing import Any
|
|
32
|
-
from urllib.parse import quote
|
|
35
|
+
from urllib.parse import quote, urlparse
|
|
33
36
|
|
|
34
37
|
import httpx
|
|
35
38
|
|
|
@@ -41,6 +44,20 @@ from agentskills_core import (
|
|
|
41
44
|
split_frontmatter,
|
|
42
45
|
)
|
|
43
46
|
|
|
47
|
+
_logger = logging.getLogger(__name__)
|
|
48
|
+
|
|
49
|
+
# Input validation: identifiers (skill_id, resource name) must be safe
|
|
50
|
+
# URL path segments. Allows alphanumeric, hyphens, dots, underscores.
|
|
51
|
+
# Must start with an alphanumeric character. No path separators or
|
|
52
|
+
# traversal sequences (e.g. ``../``).
|
|
53
|
+
_SAFE_IDENTIFIER_RE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9._-]*$")
|
|
54
|
+
|
|
55
|
+
#: Default maximum HTTP response size in bytes (10 MB).
|
|
56
|
+
DEFAULT_MAX_RESPONSE_BYTES: int = 10 * 1024 * 1024
|
|
57
|
+
|
|
58
|
+
#: Default HTTP request timeout in seconds.
|
|
59
|
+
DEFAULT_TIMEOUT_SECONDS: float = 30.0
|
|
60
|
+
|
|
44
61
|
|
|
45
62
|
class HTTPStaticFileSkillProvider(SkillProvider):
|
|
46
63
|
"""Skill provider backed by a static HTTP file host.
|
|
@@ -60,10 +77,20 @@ class HTTPStaticFileSkillProvider(SkillProvider):
|
|
|
60
77
|
slash is stripped automatically.
|
|
61
78
|
client: Optional pre-configured :class:`httpx.AsyncClient`.
|
|
62
79
|
When provided, the caller is responsible for closing it.
|
|
80
|
+
The provider will still enforce *max_response_bytes* but
|
|
81
|
+
will **not** override the client's timeout or redirect
|
|
82
|
+
settings.
|
|
63
83
|
headers: Optional extra headers sent with every request (e.g.
|
|
64
84
|
``Authorization``).
|
|
65
85
|
params: Optional query parameters appended to every request
|
|
66
86
|
(e.g. SAS tokens for Azure Blob Storage).
|
|
87
|
+
require_tls: If ``True``, reject ``http://`` base URLs with
|
|
88
|
+
a :class:`ValueError`. Defaults to ``False``, which
|
|
89
|
+
allows HTTP but emits a :class:`UserWarning`.
|
|
90
|
+
max_response_bytes: Maximum allowed response size in bytes.
|
|
91
|
+
Responses exceeding this limit raise
|
|
92
|
+
:class:`~agentskills_core.AgentSkillsError`. Defaults to
|
|
93
|
+
10 MB.
|
|
67
94
|
|
|
68
95
|
Example::
|
|
69
96
|
|
|
@@ -83,15 +110,40 @@ class HTTPStaticFileSkillProvider(SkillProvider):
|
|
|
83
110
|
client: httpx.AsyncClient | None = None,
|
|
84
111
|
headers: dict[str, str] | None = None,
|
|
85
112
|
params: dict[str, str] | None = None,
|
|
113
|
+
require_tls: bool = False,
|
|
114
|
+
max_response_bytes: int = DEFAULT_MAX_RESPONSE_BYTES,
|
|
86
115
|
) -> None:
|
|
87
116
|
if client is not None and (headers is not None or params is not None):
|
|
88
117
|
raise ValueError(
|
|
89
118
|
"Cannot specify both 'client' and 'headers'/'params'. "
|
|
90
119
|
"Configure headers and params on the client directly."
|
|
91
120
|
)
|
|
121
|
+
|
|
122
|
+
# TLS enforcement
|
|
123
|
+
parsed = urlparse(base_url)
|
|
124
|
+
if parsed.scheme == "http":
|
|
125
|
+
if require_tls:
|
|
126
|
+
raise ValueError(
|
|
127
|
+
"require_tls is enabled but base_url uses plain HTTP. "
|
|
128
|
+
"Use an HTTPS URL or set require_tls=False."
|
|
129
|
+
)
|
|
130
|
+
warnings.warn(
|
|
131
|
+
"base_url uses unencrypted HTTP. "
|
|
132
|
+
"Skill content fetched over HTTP is vulnerable to "
|
|
133
|
+
"man-in-the-middle attacks. Use HTTPS in production.",
|
|
134
|
+
UserWarning,
|
|
135
|
+
stacklevel=2,
|
|
136
|
+
)
|
|
137
|
+
|
|
92
138
|
self._base_url = base_url.rstrip("/")
|
|
139
|
+
self._max_response_bytes = max_response_bytes
|
|
93
140
|
self._owns_client = client is None
|
|
94
|
-
self._client = client or httpx.AsyncClient(
|
|
141
|
+
self._client = client or httpx.AsyncClient(
|
|
142
|
+
headers=headers,
|
|
143
|
+
params=params,
|
|
144
|
+
timeout=httpx.Timeout(DEFAULT_TIMEOUT_SECONDS),
|
|
145
|
+
follow_redirects=False,
|
|
146
|
+
)
|
|
95
147
|
|
|
96
148
|
async def aclose(self) -> None:
|
|
97
149
|
"""Close the underlying HTTP client if it is owned by this provider."""
|
|
@@ -104,6 +156,24 @@ class HTTPStaticFileSkillProvider(SkillProvider):
|
|
|
104
156
|
async def __aexit__(self, *exc: object) -> None:
|
|
105
157
|
await self.aclose()
|
|
106
158
|
|
|
159
|
+
# ------------------------------------------------------------------
|
|
160
|
+
# Input validation
|
|
161
|
+
# ------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
@staticmethod
|
|
164
|
+
def _validate_identifier(value: str, label: str) -> None:
|
|
165
|
+
"""Raise :class:`ValueError` if *value* is not a safe URL path segment.
|
|
166
|
+
|
|
167
|
+
Prevents path-traversal attacks (e.g. ``../``) and other
|
|
168
|
+
injection via ``skill_id`` or resource ``name``.
|
|
169
|
+
"""
|
|
170
|
+
if not _SAFE_IDENTIFIER_RE.match(value):
|
|
171
|
+
raise ValueError(
|
|
172
|
+
f"Invalid {label}: {value!r} — must start with an "
|
|
173
|
+
f"alphanumeric character and contain only alphanumeric "
|
|
174
|
+
f"characters, hyphens, dots, and underscores"
|
|
175
|
+
)
|
|
176
|
+
|
|
107
177
|
# ------------------------------------------------------------------
|
|
108
178
|
# Metadata & body
|
|
109
179
|
# ------------------------------------------------------------------
|
|
@@ -208,18 +278,23 @@ class HTTPStaticFileSkillProvider(SkillProvider):
|
|
|
208
278
|
|
|
209
279
|
Raises:
|
|
210
280
|
SkillNotFoundError: On 404.
|
|
211
|
-
AgentSkillsError: On other HTTP or connection errors
|
|
281
|
+
AgentSkillsError: On other HTTP or connection errors,
|
|
282
|
+
or if the response exceeds *max_response_bytes*.
|
|
212
283
|
"""
|
|
213
284
|
try:
|
|
214
285
|
resp = await self._client.get(url)
|
|
215
286
|
except httpx.HTTPError as exc:
|
|
216
|
-
raise AgentSkillsError(
|
|
287
|
+
raise AgentSkillsError("HTTP request failed") from exc
|
|
217
288
|
if resp.status_code == 404:
|
|
218
|
-
raise SkillNotFoundError(
|
|
289
|
+
raise SkillNotFoundError("Skill content not found")
|
|
219
290
|
try:
|
|
220
291
|
resp.raise_for_status()
|
|
221
292
|
except httpx.HTTPStatusError as exc:
|
|
222
|
-
raise AgentSkillsError(f"HTTP {resp.status_code} error
|
|
293
|
+
raise AgentSkillsError(f"HTTP {resp.status_code} error") from exc
|
|
294
|
+
if len(resp.content) > self._max_response_bytes:
|
|
295
|
+
raise AgentSkillsError(
|
|
296
|
+
f"Response exceeds maximum size ({self._max_response_bytes} bytes)"
|
|
297
|
+
)
|
|
223
298
|
return resp.text
|
|
224
299
|
|
|
225
300
|
async def _get_bytes(self, url: str) -> bytes:
|
|
@@ -227,26 +302,34 @@ class HTTPStaticFileSkillProvider(SkillProvider):
|
|
|
227
302
|
|
|
228
303
|
Raises:
|
|
229
304
|
ResourceNotFoundError: On 404.
|
|
230
|
-
AgentSkillsError: On other HTTP or connection errors
|
|
305
|
+
AgentSkillsError: On other HTTP or connection errors,
|
|
306
|
+
or if the response exceeds *max_response_bytes*.
|
|
231
307
|
"""
|
|
232
308
|
try:
|
|
233
309
|
resp = await self._client.get(url)
|
|
234
310
|
except httpx.HTTPError as exc:
|
|
235
|
-
raise AgentSkillsError(
|
|
311
|
+
raise AgentSkillsError("HTTP request failed") from exc
|
|
236
312
|
if resp.status_code == 404:
|
|
237
|
-
raise ResourceNotFoundError(
|
|
313
|
+
raise ResourceNotFoundError("Resource not found")
|
|
238
314
|
try:
|
|
239
315
|
resp.raise_for_status()
|
|
240
316
|
except httpx.HTTPStatusError as exc:
|
|
241
|
-
raise AgentSkillsError(f"HTTP {resp.status_code} error
|
|
317
|
+
raise AgentSkillsError(f"HTTP {resp.status_code} error") from exc
|
|
318
|
+
if len(resp.content) > self._max_response_bytes:
|
|
319
|
+
raise AgentSkillsError(
|
|
320
|
+
f"Response exceeds maximum size ({self._max_response_bytes} bytes)"
|
|
321
|
+
)
|
|
242
322
|
return resp.content
|
|
243
323
|
|
|
244
324
|
async def _get_skill_md(self, skill_id: str) -> str:
|
|
245
325
|
"""Fetch the full text of a skill's ``SKILL.md``."""
|
|
246
|
-
|
|
326
|
+
self._validate_identifier(skill_id, "skill_id")
|
|
327
|
+
url = f"{self._base_url}/{quote(skill_id, safe='')}/SKILL.md"
|
|
247
328
|
return await self._get_text(url)
|
|
248
329
|
|
|
249
330
|
async def _get_resource(self, skill_id: str, subdir: str, name: str) -> bytes:
|
|
250
331
|
"""Fetch a single resource file from a skill subdirectory."""
|
|
251
|
-
|
|
332
|
+
self._validate_identifier(skill_id, "skill_id")
|
|
333
|
+
self._validate_identifier(name, "resource name")
|
|
334
|
+
url = f"{self._base_url}/{quote(skill_id, safe='')}/{subdir}/{quote(name, safe='')}"
|
|
252
335
|
return await self._get_bytes(url)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "agentskills-http"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.1"
|
|
4
4
|
description = "HTTP-based skill providers for the Agent Skills format (https://agentskills.io)"
|
|
5
5
|
license = "MIT"
|
|
6
6
|
authors = ["Pratik Panda"]
|
|
@@ -18,9 +18,9 @@ classifiers = [
|
|
|
18
18
|
|
|
19
19
|
[tool.poetry.dependencies]
|
|
20
20
|
python = ">=3.12,<4.0"
|
|
21
|
-
agentskills-core = ">=0.1.0"
|
|
22
|
-
httpx = ">=0.27"
|
|
23
|
-
pyyaml = ">=6.0"
|
|
21
|
+
agentskills-core = ">=0.1.0,<1.0"
|
|
22
|
+
httpx = ">=0.27,<1.0"
|
|
23
|
+
pyyaml = ">=6.0,<7.0"
|
|
24
24
|
|
|
25
25
|
[build-system]
|
|
26
26
|
requires = ["poetry-core"]
|
|
File without changes
|
|
File without changes
|