agentskills-http 0.2.3__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.4.0/PKG-INFO +244 -0
- agentskills_http-0.4.0/README.md +221 -0
- {agentskills_http-0.2.3 → agentskills_http-0.4.0}/agentskills_http/__init__.py +16 -16
- agentskills_http-0.4.0/agentskills_http/static.py +739 -0
- {agentskills_http-0.2.3 → agentskills_http-0.4.0}/pyproject.toml +28 -27
- agentskills_http-0.2.3/PKG-INFO +0 -153
- agentskills_http-0.2.3/README.md +0 -131
- agentskills_http-0.2.3/agentskills_http/static.py +0 -359
- {agentskills_http-0.2.3 → agentskills_http-0.4.0}/agentskills_http/py.typed +0 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentskills-http
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: HTTP-based skill providers for the Agent Skills format (https://agentskills.io)
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Pratik Panda
|
|
7
|
+
Requires-Python: >=3.12,<4.0
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
16
|
+
Requires-Dist: agentskills-core (>=0.4.0,<1.0)
|
|
17
|
+
Requires-Dist: httpx (>=0.27,<1.0)
|
|
18
|
+
Requires-Dist: pyyaml (>=6.0,<7.0)
|
|
19
|
+
Project-URL: Homepage, https://agentskills.io
|
|
20
|
+
Project-URL: Repository, https://github.com/pratikxpanda/agentskills-sdk
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# agentskills-http
|
|
24
|
+
|
|
25
|
+
[](https://pypi.org/project/agentskills-http/)
|
|
26
|
+
[](https://pypi.org/project/agentskills-http/)
|
|
27
|
+
[](https://github.com/pratikxpanda/agentskills-sdk/blob/main/LICENSE)
|
|
28
|
+
|
|
29
|
+
> HTTP static-file skill provider for the [Agent Skills SDK](https://github.com/pratikxpanda/agentskills-sdk).
|
|
30
|
+
|
|
31
|
+
Serves [Agent Skills](https://agentskills.io) from any static HTTP file host - S3, Azure Blob, CDN, GitHub Pages, Nginx, etc. Expects the same directory-tree layout as the filesystem provider, served over HTTP.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install agentskills-http
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Requires Python 3.12 or newer. Installs `agentskills-core`, `httpx`, and `pyyaml` as dependencies.
|
|
40
|
+
|
|
41
|
+
## Expected URL Layout
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
https://cdn.example.com/skills/
|
|
45
|
+
├── incident-response/
|
|
46
|
+
│ ├── SKILL.md
|
|
47
|
+
│ ├── references/severity-levels.md
|
|
48
|
+
│ ├── scripts/page-oncall.sh
|
|
49
|
+
│ └── assets/flowchart.mermaid
|
|
50
|
+
└── another-skill/
|
|
51
|
+
└── SKILL.md
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from agentskills_core import SkillRegistry
|
|
58
|
+
from agentskills_http import HTTPStaticFileSkillProvider
|
|
59
|
+
|
|
60
|
+
async with HTTPStaticFileSkillProvider("https://cdn.example.com/skills") as provider:
|
|
61
|
+
registry = SkillRegistry()
|
|
62
|
+
await registry.register("incident-response", provider)
|
|
63
|
+
|
|
64
|
+
skill = registry.get_skill("incident-response")
|
|
65
|
+
meta = await skill.get_metadata()
|
|
66
|
+
body = await skill.get_body()
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Custom Headers
|
|
70
|
+
|
|
71
|
+
Pass authentication or other headers:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from agentskills_http import HTTPStaticFileSkillProvider
|
|
75
|
+
|
|
76
|
+
provider = HTTPStaticFileSkillProvider(
|
|
77
|
+
"https://cdn.example.com/skills",
|
|
78
|
+
headers={"Authorization": "Bearer <token>"},
|
|
79
|
+
)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Bring Your Own Client
|
|
83
|
+
|
|
84
|
+
Supply a pre-configured `httpx.AsyncClient` for full control over timeouts, proxies, etc.:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
import httpx
|
|
88
|
+
from agentskills_http import HTTPStaticFileSkillProvider
|
|
89
|
+
|
|
90
|
+
client = httpx.AsyncClient(timeout=30, headers={"Authorization": "Bearer <token>"})
|
|
91
|
+
provider = HTTPStaticFileSkillProvider("https://cdn.example.com/skills", client=client)
|
|
92
|
+
# caller is responsible for closing the client
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
> **Note:** `client` and `headers` are mutually exclusive. Configure headers on the client directly when providing your own.
|
|
96
|
+
|
|
97
|
+
## API
|
|
98
|
+
|
|
99
|
+
### `HTTPStaticFileSkillProvider(base_url, *, client=None, headers=None, params=None, require_tls=False, max_response_bytes=10_485_760, revalidate=False)`
|
|
100
|
+
|
|
101
|
+
| Parameter | Type | Default | Description |
|
|
102
|
+
| --- | --- | --- | --- |
|
|
103
|
+
| `base_url` | `str` | - | Root URL where the skill tree is hosted |
|
|
104
|
+
| `client` | `AsyncClient \| None` | `None` | Pre-configured httpx client (caller manages lifecycle) |
|
|
105
|
+
| `headers` | `dict \| None` | `None` | Extra headers sent with every request |
|
|
106
|
+
| `params` | `dict \| None` | `None` | Query parameters appended to every request |
|
|
107
|
+
| `require_tls` | `bool` | `False` | Reject `http://` URLs with `ValueError` |
|
|
108
|
+
| `max_response_bytes` | `int` | `10_485_760` | Maximum allowed response size in bytes |
|
|
109
|
+
| `revalidate` | `bool` | `False` | Re-check cached `SKILL.md` on every access with `If-None-Match` / `If-Modified-Since` |
|
|
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` |
|
|
112
|
+
| `timeout` | `float` | `30.0` | Request timeout in seconds (ignored when you supply `client`) |
|
|
113
|
+
| `max_retries` | `int` | `2` | Retries after the initial attempt, for retryable failures only |
|
|
114
|
+
| `retry_backoff` | `float` | `0.5` | Base delay in seconds for exponential backoff |
|
|
115
|
+
| `max_retry_delay` | `float` | `30.0` | Ceiling on any single backoff sleep |
|
|
116
|
+
|
|
117
|
+
> **Note:** `client` and `headers`/`params` are mutually exclusive. Configure headers and params on the client directly when providing your own.
|
|
118
|
+
|
|
119
|
+
| Method | Returns | Description |
|
|
120
|
+
| --- | --- | --- |
|
|
121
|
+
| `get_metadata(skill_id)` | `dict[str, Any]` | Parsed YAML frontmatter from `SKILL.md` |
|
|
122
|
+
| `get_body(skill_id)` | `str` | Markdown body after the frontmatter |
|
|
123
|
+
| `get_script(skill_id, name)` | `bytes` | Raw script content |
|
|
124
|
+
| `get_asset(skill_id, name)` | `bytes` | Raw asset content |
|
|
125
|
+
| `get_reference(skill_id, name)` | `bytes` | Raw reference content |
|
|
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`) |
|
|
128
|
+
| `invalidate(skill_id=None)` | `None` | Drop cached `SKILL.md` content for one skill, or all skills |
|
|
129
|
+
| `aclose()` | `None` | Close the HTTP client (if owned by the provider) |
|
|
130
|
+
|
|
131
|
+
Supports `async with` for automatic cleanup.
|
|
132
|
+
|
|
133
|
+
## Resource Discovery
|
|
134
|
+
|
|
135
|
+
A static file host cannot be enumerated: there is no portable directory listing over plain HTTP. By default this provider therefore reports that it *cannot* list resources — `list_resources()` raises `ResourceListingNotSupportedError` — rather than returning an empty mapping that would look like a skill with no resources.
|
|
136
|
+
|
|
137
|
+
If you control the host, publish a small manifest at `{base_url}/{skill_id}/index.json`:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"references": ["severity-levels.md"],
|
|
142
|
+
"scripts": ["page-oncall.sh"],
|
|
143
|
+
"assets": ["flowchart.mermaid"]
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Then opt in:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
provider = HTTPStaticFileSkillProvider(BASE, resource_manifest=True)
|
|
151
|
+
listing = await provider.list_resources("incident-response")
|
|
152
|
+
```
|
|
153
|
+
|
|
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.
|
|
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
|
+
|
|
177
|
+
## Caching
|
|
178
|
+
|
|
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.
|
|
180
|
+
|
|
181
|
+
By default the cache is served until you call `invalidate()`. If your host serves mutable skills and the process is long-lived, opt into conditional revalidation instead:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
provider = HTTPStaticFileSkillProvider(BASE, revalidate=True)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
That sends `If-None-Match` / `If-Modified-Since` on every access and reuses the cached body on `304`. It costs one cheap round-trip per access, so prefer the default plus an explicit `invalidate()` when you control publishing.
|
|
188
|
+
|
|
189
|
+
## Error Handling
|
|
190
|
+
|
|
191
|
+
| Scenario | Exception | Retried |
|
|
192
|
+
| --- | --- | --- |
|
|
193
|
+
| `404` / `410` on `SKILL.md` | `SkillNotFoundError` | No |
|
|
194
|
+
| `404` / `410` on a resource | `ResourceNotFoundError` | No |
|
|
195
|
+
| `5xx`, `408`, `425`, `429` | `SkillUnavailableError` | Yes |
|
|
196
|
+
| Timeouts, connection and protocol errors | `SkillUnavailableError` | Yes |
|
|
197
|
+
| `401` / `403` | `AgentSkillsError` | No |
|
|
198
|
+
| Other `4xx`, oversized responses | `AgentSkillsError` | No |
|
|
199
|
+
|
|
200
|
+
All exceptions inherit from `AgentSkillsError`.
|
|
201
|
+
|
|
202
|
+
The split between `SkillNotFoundError` and `SkillUnavailableError` is the point of the taxonomy: a `503` means the skill may well exist and the same request could succeed in a moment, whereas a `404` means it is gone. Collapsing both into "not found" turns a retryable blip into a permanent-looking failure, and nothing downstream can tell the difference.
|
|
203
|
+
|
|
204
|
+
### Retries
|
|
205
|
+
|
|
206
|
+
Retryable failures are retried with exponential backoff and full jitter:
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
provider = HTTPStaticFileSkillProvider(
|
|
210
|
+
BASE,
|
|
211
|
+
max_retries=2, # attempts after the first; 0 disables
|
|
212
|
+
retry_backoff=0.5, # base delay in seconds
|
|
213
|
+
max_retry_delay=30.0, # ceiling on any single sleep
|
|
214
|
+
)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Jitter matters because a registry builds its catalog concurrently — without it, every skill fetch would retry in lockstep and hit the recovering server as one wave.
|
|
218
|
+
|
|
219
|
+
`Retry-After` is honoured in both the delay-seconds and HTTP-date forms. If the server asks for longer than `max_retry_delay`, the request is **not** retried: blocking a request path for minutes is worse than failing fast. The advised delay is still available to the caller as `SkillUnavailableError.retry_after`, so a scheduler can act on it.
|
|
220
|
+
|
|
221
|
+
## Security
|
|
222
|
+
|
|
223
|
+
- **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.
|
|
224
|
+
- **TLS warnings** - A `UserWarning` is emitted when `base_url` uses unencrypted HTTP. Set `require_tls=True` to reject HTTP URLs entirely.
|
|
225
|
+
- **Redirect protection** - The internally-created HTTP client does not follow redirects by default, preventing open-redirect SSRF.
|
|
226
|
+
- **Timeouts** - Default 30-second timeout on all HTTP requests. Configure via `timeout`.
|
|
227
|
+
- **Response size limits** - Responses exceeding 10 MB (default) are rejected before processing. Configure via `max_response_bytes`.
|
|
228
|
+
- **Error-message sanitization** - Messages carry the status code and the path *relative to `base_url`* — never the host, never a query string. The underlying `httpx` exception is deliberately **not** chained (`from None`), because `httpx.HTTPStatusError` renders the full request URL including its query string, which is exactly where SAS tokens and signed-URL signatures live. Chaining it leaked credentials into every traceback.
|
|
229
|
+
|
|
230
|
+
For the full security policy, see [SECURITY.md](https://github.com/pratikxpanda/agentskills-sdk/blob/main/SECURITY.md).
|
|
231
|
+
|
|
232
|
+
## Deployment Considerations
|
|
233
|
+
|
|
234
|
+
- **Rate limiting** - The SDK does not enforce rate limits on MCP tool
|
|
235
|
+
calls or HTTP requests. Deploy behind a reverse proxy or API gateway
|
|
236
|
+
that provides rate limiting in production environments.
|
|
237
|
+
- **Credential management** - Do not store secrets (API keys, SAS
|
|
238
|
+
tokens, Authorization headers) in config files committed to version
|
|
239
|
+
control. Use environment variables or a secret manager instead.
|
|
240
|
+
|
|
241
|
+
## License
|
|
242
|
+
|
|
243
|
+
MIT
|
|
244
|
+
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# agentskills-http
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/agentskills-http/)
|
|
4
|
+
[](https://pypi.org/project/agentskills-http/)
|
|
5
|
+
[](https://github.com/pratikxpanda/agentskills-sdk/blob/main/LICENSE)
|
|
6
|
+
|
|
7
|
+
> HTTP static-file skill provider for the [Agent Skills SDK](https://github.com/pratikxpanda/agentskills-sdk).
|
|
8
|
+
|
|
9
|
+
Serves [Agent Skills](https://agentskills.io) from any static HTTP file host - S3, Azure Blob, CDN, GitHub Pages, Nginx, etc. Expects the same directory-tree layout as the filesystem provider, served over HTTP.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install agentskills-http
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Requires Python 3.12 or newer. Installs `agentskills-core`, `httpx`, and `pyyaml` as dependencies.
|
|
18
|
+
|
|
19
|
+
## Expected URL Layout
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
https://cdn.example.com/skills/
|
|
23
|
+
├── incident-response/
|
|
24
|
+
│ ├── SKILL.md
|
|
25
|
+
│ ├── references/severity-levels.md
|
|
26
|
+
│ ├── scripts/page-oncall.sh
|
|
27
|
+
│ └── assets/flowchart.mermaid
|
|
28
|
+
└── another-skill/
|
|
29
|
+
└── SKILL.md
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from agentskills_core import SkillRegistry
|
|
36
|
+
from agentskills_http import HTTPStaticFileSkillProvider
|
|
37
|
+
|
|
38
|
+
async with HTTPStaticFileSkillProvider("https://cdn.example.com/skills") as provider:
|
|
39
|
+
registry = SkillRegistry()
|
|
40
|
+
await registry.register("incident-response", provider)
|
|
41
|
+
|
|
42
|
+
skill = registry.get_skill("incident-response")
|
|
43
|
+
meta = await skill.get_metadata()
|
|
44
|
+
body = await skill.get_body()
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Custom Headers
|
|
48
|
+
|
|
49
|
+
Pass authentication or other headers:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from agentskills_http import HTTPStaticFileSkillProvider
|
|
53
|
+
|
|
54
|
+
provider = HTTPStaticFileSkillProvider(
|
|
55
|
+
"https://cdn.example.com/skills",
|
|
56
|
+
headers={"Authorization": "Bearer <token>"},
|
|
57
|
+
)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Bring Your Own Client
|
|
61
|
+
|
|
62
|
+
Supply a pre-configured `httpx.AsyncClient` for full control over timeouts, proxies, etc.:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import httpx
|
|
66
|
+
from agentskills_http import HTTPStaticFileSkillProvider
|
|
67
|
+
|
|
68
|
+
client = httpx.AsyncClient(timeout=30, headers={"Authorization": "Bearer <token>"})
|
|
69
|
+
provider = HTTPStaticFileSkillProvider("https://cdn.example.com/skills", client=client)
|
|
70
|
+
# caller is responsible for closing the client
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
> **Note:** `client` and `headers` are mutually exclusive. Configure headers on the client directly when providing your own.
|
|
74
|
+
|
|
75
|
+
## API
|
|
76
|
+
|
|
77
|
+
### `HTTPStaticFileSkillProvider(base_url, *, client=None, headers=None, params=None, require_tls=False, max_response_bytes=10_485_760, revalidate=False)`
|
|
78
|
+
|
|
79
|
+
| Parameter | Type | Default | Description |
|
|
80
|
+
| --- | --- | --- | --- |
|
|
81
|
+
| `base_url` | `str` | - | Root URL where the skill tree is hosted |
|
|
82
|
+
| `client` | `AsyncClient \| None` | `None` | Pre-configured httpx client (caller manages lifecycle) |
|
|
83
|
+
| `headers` | `dict \| None` | `None` | Extra headers sent with every request |
|
|
84
|
+
| `params` | `dict \| None` | `None` | Query parameters appended to every request |
|
|
85
|
+
| `require_tls` | `bool` | `False` | Reject `http://` URLs with `ValueError` |
|
|
86
|
+
| `max_response_bytes` | `int` | `10_485_760` | Maximum allowed response size in bytes |
|
|
87
|
+
| `revalidate` | `bool` | `False` | Re-check cached `SKILL.md` on every access with `If-None-Match` / `If-Modified-Since` |
|
|
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` |
|
|
90
|
+
| `timeout` | `float` | `30.0` | Request timeout in seconds (ignored when you supply `client`) |
|
|
91
|
+
| `max_retries` | `int` | `2` | Retries after the initial attempt, for retryable failures only |
|
|
92
|
+
| `retry_backoff` | `float` | `0.5` | Base delay in seconds for exponential backoff |
|
|
93
|
+
| `max_retry_delay` | `float` | `30.0` | Ceiling on any single backoff sleep |
|
|
94
|
+
|
|
95
|
+
> **Note:** `client` and `headers`/`params` are mutually exclusive. Configure headers and params on the client directly when providing your own.
|
|
96
|
+
|
|
97
|
+
| Method | Returns | Description |
|
|
98
|
+
| --- | --- | --- |
|
|
99
|
+
| `get_metadata(skill_id)` | `dict[str, Any]` | Parsed YAML frontmatter from `SKILL.md` |
|
|
100
|
+
| `get_body(skill_id)` | `str` | Markdown body after the frontmatter |
|
|
101
|
+
| `get_script(skill_id, name)` | `bytes` | Raw script content |
|
|
102
|
+
| `get_asset(skill_id, name)` | `bytes` | Raw asset content |
|
|
103
|
+
| `get_reference(skill_id, name)` | `bytes` | Raw reference content |
|
|
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`) |
|
|
106
|
+
| `invalidate(skill_id=None)` | `None` | Drop cached `SKILL.md` content for one skill, or all skills |
|
|
107
|
+
| `aclose()` | `None` | Close the HTTP client (if owned by the provider) |
|
|
108
|
+
|
|
109
|
+
Supports `async with` for automatic cleanup.
|
|
110
|
+
|
|
111
|
+
## Resource Discovery
|
|
112
|
+
|
|
113
|
+
A static file host cannot be enumerated: there is no portable directory listing over plain HTTP. By default this provider therefore reports that it *cannot* list resources — `list_resources()` raises `ResourceListingNotSupportedError` — rather than returning an empty mapping that would look like a skill with no resources.
|
|
114
|
+
|
|
115
|
+
If you control the host, publish a small manifest at `{base_url}/{skill_id}/index.json`:
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"references": ["severity-levels.md"],
|
|
120
|
+
"scripts": ["page-oncall.sh"],
|
|
121
|
+
"assets": ["flowchart.mermaid"]
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Then opt in:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
provider = HTTPStaticFileSkillProvider(BASE, resource_manifest=True)
|
|
129
|
+
listing = await provider.list_resources("incident-response")
|
|
130
|
+
```
|
|
131
|
+
|
|
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.
|
|
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
|
+
|
|
155
|
+
## Caching
|
|
156
|
+
|
|
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.
|
|
158
|
+
|
|
159
|
+
By default the cache is served until you call `invalidate()`. If your host serves mutable skills and the process is long-lived, opt into conditional revalidation instead:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
provider = HTTPStaticFileSkillProvider(BASE, revalidate=True)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
That sends `If-None-Match` / `If-Modified-Since` on every access and reuses the cached body on `304`. It costs one cheap round-trip per access, so prefer the default plus an explicit `invalidate()` when you control publishing.
|
|
166
|
+
|
|
167
|
+
## Error Handling
|
|
168
|
+
|
|
169
|
+
| Scenario | Exception | Retried |
|
|
170
|
+
| --- | --- | --- |
|
|
171
|
+
| `404` / `410` on `SKILL.md` | `SkillNotFoundError` | No |
|
|
172
|
+
| `404` / `410` on a resource | `ResourceNotFoundError` | No |
|
|
173
|
+
| `5xx`, `408`, `425`, `429` | `SkillUnavailableError` | Yes |
|
|
174
|
+
| Timeouts, connection and protocol errors | `SkillUnavailableError` | Yes |
|
|
175
|
+
| `401` / `403` | `AgentSkillsError` | No |
|
|
176
|
+
| Other `4xx`, oversized responses | `AgentSkillsError` | No |
|
|
177
|
+
|
|
178
|
+
All exceptions inherit from `AgentSkillsError`.
|
|
179
|
+
|
|
180
|
+
The split between `SkillNotFoundError` and `SkillUnavailableError` is the point of the taxonomy: a `503` means the skill may well exist and the same request could succeed in a moment, whereas a `404` means it is gone. Collapsing both into "not found" turns a retryable blip into a permanent-looking failure, and nothing downstream can tell the difference.
|
|
181
|
+
|
|
182
|
+
### Retries
|
|
183
|
+
|
|
184
|
+
Retryable failures are retried with exponential backoff and full jitter:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
provider = HTTPStaticFileSkillProvider(
|
|
188
|
+
BASE,
|
|
189
|
+
max_retries=2, # attempts after the first; 0 disables
|
|
190
|
+
retry_backoff=0.5, # base delay in seconds
|
|
191
|
+
max_retry_delay=30.0, # ceiling on any single sleep
|
|
192
|
+
)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Jitter matters because a registry builds its catalog concurrently — without it, every skill fetch would retry in lockstep and hit the recovering server as one wave.
|
|
196
|
+
|
|
197
|
+
`Retry-After` is honoured in both the delay-seconds and HTTP-date forms. If the server asks for longer than `max_retry_delay`, the request is **not** retried: blocking a request path for minutes is worse than failing fast. The advised delay is still available to the caller as `SkillUnavailableError.retry_after`, so a scheduler can act on it.
|
|
198
|
+
|
|
199
|
+
## Security
|
|
200
|
+
|
|
201
|
+
- **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.
|
|
202
|
+
- **TLS warnings** - A `UserWarning` is emitted when `base_url` uses unencrypted HTTP. Set `require_tls=True` to reject HTTP URLs entirely.
|
|
203
|
+
- **Redirect protection** - The internally-created HTTP client does not follow redirects by default, preventing open-redirect SSRF.
|
|
204
|
+
- **Timeouts** - Default 30-second timeout on all HTTP requests. Configure via `timeout`.
|
|
205
|
+
- **Response size limits** - Responses exceeding 10 MB (default) are rejected before processing. Configure via `max_response_bytes`.
|
|
206
|
+
- **Error-message sanitization** - Messages carry the status code and the path *relative to `base_url`* — never the host, never a query string. The underlying `httpx` exception is deliberately **not** chained (`from None`), because `httpx.HTTPStatusError` renders the full request URL including its query string, which is exactly where SAS tokens and signed-URL signatures live. Chaining it leaked credentials into every traceback.
|
|
207
|
+
|
|
208
|
+
For the full security policy, see [SECURITY.md](https://github.com/pratikxpanda/agentskills-sdk/blob/main/SECURITY.md).
|
|
209
|
+
|
|
210
|
+
## Deployment Considerations
|
|
211
|
+
|
|
212
|
+
- **Rate limiting** - The SDK does not enforce rate limits on MCP tool
|
|
213
|
+
calls or HTTP requests. Deploy behind a reverse proxy or API gateway
|
|
214
|
+
that provides rate limiting in production environments.
|
|
215
|
+
- **Credential management** - Do not store secrets (API keys, SAS
|
|
216
|
+
tokens, Authorization headers) in config files committed to version
|
|
217
|
+
control. Use environment variables or a secret manager instead.
|
|
218
|
+
|
|
219
|
+
## License
|
|
220
|
+
|
|
221
|
+
MIT
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
"""HTTP-based skill providers for the Agent Skills format.
|
|
2
|
-
|
|
3
|
-
This package provides :class:`HTTPStaticFileSkillProvider`, a concrete
|
|
4
|
-
implementation of :class:`~agentskills_core.SkillProvider` that fetches
|
|
5
|
-
`Agent Skills <https://agentskills.io>`_ from a static HTTP file host
|
|
6
|
-
(S3, Azure Blob Storage, CDN, GitHub Pages, or any web server serving
|
|
7
|
-
raw files).
|
|
8
|
-
|
|
9
|
-
Install::
|
|
10
|
-
|
|
11
|
-
pip install agentskills-http
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
from agentskills_http.static import HTTPStaticFileSkillProvider
|
|
15
|
-
|
|
16
|
-
__all__ = ["HTTPStaticFileSkillProvider"]
|
|
1
|
+
"""HTTP-based skill providers for the Agent Skills format.
|
|
2
|
+
|
|
3
|
+
This package provides :class:`HTTPStaticFileSkillProvider`, a concrete
|
|
4
|
+
implementation of :class:`~agentskills_core.SkillProvider` that fetches
|
|
5
|
+
`Agent Skills <https://agentskills.io>`_ from a static HTTP file host
|
|
6
|
+
(S3, Azure Blob Storage, CDN, GitHub Pages, or any web server serving
|
|
7
|
+
raw files).
|
|
8
|
+
|
|
9
|
+
Install::
|
|
10
|
+
|
|
11
|
+
pip install agentskills-http
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from agentskills_http.static import HTTPStaticFileSkillProvider
|
|
15
|
+
|
|
16
|
+
__all__ = ["HTTPStaticFileSkillProvider"]
|