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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentskills-http
3
- Version: 0.2.0
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(headers=headers, params=params)
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(f"HTTP request failed: {url}") from exc
287
+ raise AgentSkillsError("HTTP request failed") from exc
217
288
  if resp.status_code == 404:
218
- raise SkillNotFoundError(f"Not found: {url}")
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 for {url}") from exc
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(f"HTTP request failed: {url}") from exc
311
+ raise AgentSkillsError("HTTP request failed") from exc
236
312
  if resp.status_code == 404:
237
- raise ResourceNotFoundError(f"Not found: {url}")
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 for {url}") from exc
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
- url = f"{self._base_url}/{quote(skill_id)}/SKILL.md"
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
- url = f"{self._base_url}/{quote(skill_id)}/{subdir}/{quote(name)}"
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.0"
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"]