agent-plugins 0.2.0__tar.gz → 0.2.2__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.
- agent_plugins-0.2.2/PKG-INFO +124 -0
- agent_plugins-0.2.2/README.md +103 -0
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/pyproject.toml +3 -3
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/pyproject.toml.orig +3 -3
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/src/agent_plugins/_build/plan.py +66 -8
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/src/agent_plugins/_build/sdist.py +2 -2
- agent_plugins-0.2.2/src/agent_plugins/_build/wheel.py +239 -0
- agent_plugins-0.2.2/src/agent_plugins/_build/wheel_archive.py +292 -0
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/src/agent_plugins/_discovery.py +9 -1
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/src/agent_plugins/_files.py +1 -1
- agent_plugins-0.2.2/src/agent_plugins/_mcp.py +198 -0
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/src/agent_plugins/_plugin.py +9 -2
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/src/agent_plugins/_schema/json.py +14 -1
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/src/agent_plugins/_schema/lazy.py +2 -1
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/src/agent_plugins/_schema/manifest.py +10 -1
- agent_plugins-0.2.2/src/agent_plugins/_schema/mcp.py +144 -0
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/src/agent_plugins/_schema/skill.py +5 -3
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/src/agent_plugins/_skill.py +7 -3
- agent_plugins-0.2.0/PKG-INFO +0 -206
- agent_plugins-0.2.0/README.md +0 -185
- agent_plugins-0.2.0/src/agent_plugins/_build/wheel.py +0 -541
- agent_plugins-0.2.0/src/agent_plugins/_schema/mcp.py +0 -283
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/.agent-plugin/plugin.json +0 -0
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/.agent-plugin/skills/agent-plugins/SKILL.md +0 -0
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/.agent-plugin/skills/agent-plugins/agents/openai.yaml +0 -0
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/LICENSE +0 -0
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/src/agent_plugins/__init__.py +0 -0
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/src/agent_plugins/__main__.py +0 -0
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/src/agent_plugins/_build/__init__.py +0 -0
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/src/agent_plugins/_build/backend.py +0 -0
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/src/agent_plugins/_cli.py +0 -0
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/src/agent_plugins/_errors.py +0 -0
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/src/agent_plugins/_marker.py +0 -0
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/src/agent_plugins/_schema/__init__.py +0 -0
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/src/agent_plugins/_schema/errors.py +0 -0
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/src/agent_plugins/_schema/models.py +0 -0
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/src/agent_plugins/_schema/v1/__init__.py +0 -0
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/src/agent_plugins/_schema/v1/manifest.py +0 -0
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/src/agent_plugins/_schema/v1/mcp.py +0 -0
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/src/agent_plugins/_tree.py +0 -0
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/src/agent_plugins/build/__init__.py +0 -0
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/src/agent_plugins/build/hatchling.py +0 -0
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/src/agent_plugins/build/uv_build.py +0 -0
- {agent_plugins-0.2.0 → agent_plugins-0.2.2}/src/agent_plugins/py.typed +0 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-plugins
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Ship Agent Plugins with Python packages and inspect their installed files
|
|
5
|
+
Author: Péter Ferenc Gyarmati
|
|
6
|
+
Author-email: Péter Ferenc Gyarmati <dev.petergy@gmail.com>
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Dist: tomli>=1.0.3 ; python_full_version < '3.11'
|
|
16
|
+
Requires-Python: >=3.10, <3.15
|
|
17
|
+
Project-URL: Documentation, https://peter-gy.github.io/agent-plugins/
|
|
18
|
+
Project-URL: Issues, https://github.com/peter-gy/agent-plugins/issues
|
|
19
|
+
Project-URL: Source, https://github.com/peter-gy/agent-plugins
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
<p align="center">
|
|
23
|
+
<picture>
|
|
24
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/peter-gy/agent-plugins/main/docs/public/brand/agent-plugins-lockup-horizontal-dark.svg">
|
|
25
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/peter-gy/agent-plugins/main/docs/public/brand/agent-plugins-lockup-horizontal-light.svg">
|
|
26
|
+
<img alt="agent-plugins" src="https://raw.githubusercontent.com/peter-gy/agent-plugins/main/docs/public/brand/agent-plugins-lockup-horizontal-light.svg" width="430">
|
|
27
|
+
</picture>
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
<p align="center">
|
|
31
|
+
Ship Agent Plugins with Python packages.
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
<p align="center">
|
|
35
|
+
<a href="https://peter-gy.github.io/agent-plugins/"><strong>Documentation</strong></a> ·
|
|
36
|
+
<a href="https://pypi.org/project/agent-plugins/"><strong>PyPI</strong></a> ·
|
|
37
|
+
<a href="https://agent-plugins.org/"><strong>Agent Plugins format</strong></a>
|
|
38
|
+
</p>
|
|
39
|
+
|
|
40
|
+
<p align="center">
|
|
41
|
+
<a href="https://pypi.org/project/agent-plugins/"><img alt="PyPI version" src="https://img.shields.io/pypi/v/agent-plugins"></a>
|
|
42
|
+
<a href="https://pypi.org/project/agent-plugins/"><img alt="Supported Python versions" src="https://img.shields.io/pypi/pyversions/agent-plugins"></a>
|
|
43
|
+
<a href="https://github.com/peter-gy/agent-plugins/actions/workflows/ci.yml"><img alt="CI status" src="https://github.com/peter-gy/agent-plugins/actions/workflows/ci.yml/badge.svg"></a>
|
|
44
|
+
<a href="https://github.com/peter-gy/agent-plugins/blob/main/LICENSE"><img alt="Apache-2.0 license" src="https://img.shields.io/pypi/l/agent-plugins"></a>
|
|
45
|
+
</p>
|
|
46
|
+
|
|
47
|
+
`agent-plugins` ships a Python library and its agent integrations in one package. An agent with Python execution can read its packaged instructions, then use the library in the same environment. Library code, skills, tool configuration, and resources share one release.
|
|
48
|
+
|
|
49
|
+
The [Agent Plugins format](https://agent-plugins.org/) defines the directory: a manifest, [Agent Skills](https://agentskills.io/specification) for instructions and resources, [Model Context Protocol (MCP)](https://modelcontextprotocol.io/specification) server configuration for tools, and client extensions. This library packages that directory and makes it discoverable through Python metadata. Agent clients choose which components to activate.
|
|
50
|
+
|
|
51
|
+
## Package your plugin
|
|
52
|
+
|
|
53
|
+
Keep `plugin.json` and `skills/` beside your code. For a project using the [uv build backend](https://docs.astral.sh/uv/concepts/build-backend/), configure `pyproject.toml`:
|
|
54
|
+
|
|
55
|
+
```toml
|
|
56
|
+
[build-system]
|
|
57
|
+
requires = ["agent-plugins", "uv_build"]
|
|
58
|
+
build-backend = "agent_plugins.build.uv_build"
|
|
59
|
+
|
|
60
|
+
[tool.agent-plugins]
|
|
61
|
+
root = "."
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Build with the [uv package manager](https://docs.astral.sh/uv/):
|
|
65
|
+
|
|
66
|
+
```console
|
|
67
|
+
uv build
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The [quickstart](https://peter-gy.github.io/agent-plugins/guide/getting-started) creates a complete project, builds it, and locates the installed plugin. Use the [Hatchling adapter](https://peter-gy.github.io/agent-plugins/guide/build-backends#hatchling) for Hatchling projects, or [attach a prebuilt wheel](https://peter-gy.github.io/agent-plugins/guide/attach-wheel) when another tool owns the build:
|
|
71
|
+
|
|
72
|
+
```console
|
|
73
|
+
agent-plugins attach-wheel dist/my_project-0.1.0-py3-none-any.whl --project .
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Attachment updates the wheel in place. Pass `--output-dir` to preserve the input.
|
|
77
|
+
|
|
78
|
+
## Inspect an installed plugin
|
|
79
|
+
|
|
80
|
+
Install `agent-plugins` in the environment you want to inspect. The package includes its own Agent Skill:
|
|
81
|
+
|
|
82
|
+
```console
|
|
83
|
+
pip install agent-plugins
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
import agent_plugins as ap
|
|
88
|
+
|
|
89
|
+
plugin = ap.locate("agent-plugins")
|
|
90
|
+
skill = plugin.skill("agent-plugins")
|
|
91
|
+
|
|
92
|
+
print(skill.source)
|
|
93
|
+
print(skill.file("SKILL.md"))
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Pass your library's distribution name to `locate()` to inspect its plugin. Use [`Plugin.from_project()`](https://peter-gy.github.io/agent-plugins/guide/inspect-project) to inspect the selected source files before building.
|
|
97
|
+
|
|
98
|
+
## Documentation
|
|
99
|
+
|
|
100
|
+
- [Get started](https://peter-gy.github.io/agent-plugins/guide/getting-started): package, install, and locate a plugin.
|
|
101
|
+
- [How packaging works](https://peter-gy.github.io/agent-plugins/guide/artifact-lifecycle): wheels, source distributions, and editable installs.
|
|
102
|
+
- [Integrate](https://peter-gy.github.io/agent-plugins/guide/inspect-installed): read skills, inspect files, and resolve MCP configuration.
|
|
103
|
+
- [Python API](https://peter-gy.github.io/agent-plugins/reference/python-api) · [CLI](https://peter-gy.github.io/agent-plugins/reference/cli) · [Configuration](https://peter-gy.github.io/agent-plugins/reference/pyproject)
|
|
104
|
+
|
|
105
|
+
<details>
|
|
106
|
+
<summary>Packaging lifecycle</summary>
|
|
107
|
+
|
|
108
|
+
<p align="center">
|
|
109
|
+
<picture>
|
|
110
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/peter-gy/agent-plugins/main/docs/public/brand/agent-plugins-core-model-dark.svg">
|
|
111
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/peter-gy/agent-plugins/main/docs/public/brand/agent-plugins-core-model-light.svg">
|
|
112
|
+
<img alt="Core model: an authored Python project builds into one wheel that installs the library beside its version-matched Agent Plugin, which locate() returns as a plugin handle" src="https://raw.githubusercontent.com/peter-gy/agent-plugins/main/docs/public/brand/agent-plugins-core-model-light.svg" width="300">
|
|
113
|
+
</picture>
|
|
114
|
+
</p>
|
|
115
|
+
|
|
116
|
+
</details>
|
|
117
|
+
|
|
118
|
+
## Development
|
|
119
|
+
|
|
120
|
+
See [development_docs/](https://github.com/peter-gy/agent-plugins/tree/main/development_docs) for setup, architecture, checks, and releases. Serve the documentation locally with `pnpm --dir docs dev`.
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
[Apache-2.0](https://github.com/peter-gy/agent-plugins/blob/main/LICENSE).
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/peter-gy/agent-plugins/main/docs/public/brand/agent-plugins-lockup-horizontal-dark.svg">
|
|
4
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/peter-gy/agent-plugins/main/docs/public/brand/agent-plugins-lockup-horizontal-light.svg">
|
|
5
|
+
<img alt="agent-plugins" src="https://raw.githubusercontent.com/peter-gy/agent-plugins/main/docs/public/brand/agent-plugins-lockup-horizontal-light.svg" width="430">
|
|
6
|
+
</picture>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
Ship Agent Plugins with Python packages.
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
<p align="center">
|
|
14
|
+
<a href="https://peter-gy.github.io/agent-plugins/"><strong>Documentation</strong></a> ·
|
|
15
|
+
<a href="https://pypi.org/project/agent-plugins/"><strong>PyPI</strong></a> ·
|
|
16
|
+
<a href="https://agent-plugins.org/"><strong>Agent Plugins format</strong></a>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<a href="https://pypi.org/project/agent-plugins/"><img alt="PyPI version" src="https://img.shields.io/pypi/v/agent-plugins"></a>
|
|
21
|
+
<a href="https://pypi.org/project/agent-plugins/"><img alt="Supported Python versions" src="https://img.shields.io/pypi/pyversions/agent-plugins"></a>
|
|
22
|
+
<a href="https://github.com/peter-gy/agent-plugins/actions/workflows/ci.yml"><img alt="CI status" src="https://github.com/peter-gy/agent-plugins/actions/workflows/ci.yml/badge.svg"></a>
|
|
23
|
+
<a href="https://github.com/peter-gy/agent-plugins/blob/main/LICENSE"><img alt="Apache-2.0 license" src="https://img.shields.io/pypi/l/agent-plugins"></a>
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
`agent-plugins` ships a Python library and its agent integrations in one package. An agent with Python execution can read its packaged instructions, then use the library in the same environment. Library code, skills, tool configuration, and resources share one release.
|
|
27
|
+
|
|
28
|
+
The [Agent Plugins format](https://agent-plugins.org/) defines the directory: a manifest, [Agent Skills](https://agentskills.io/specification) for instructions and resources, [Model Context Protocol (MCP)](https://modelcontextprotocol.io/specification) server configuration for tools, and client extensions. This library packages that directory and makes it discoverable through Python metadata. Agent clients choose which components to activate.
|
|
29
|
+
|
|
30
|
+
## Package your plugin
|
|
31
|
+
|
|
32
|
+
Keep `plugin.json` and `skills/` beside your code. For a project using the [uv build backend](https://docs.astral.sh/uv/concepts/build-backend/), configure `pyproject.toml`:
|
|
33
|
+
|
|
34
|
+
```toml
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["agent-plugins", "uv_build"]
|
|
37
|
+
build-backend = "agent_plugins.build.uv_build"
|
|
38
|
+
|
|
39
|
+
[tool.agent-plugins]
|
|
40
|
+
root = "."
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Build with the [uv package manager](https://docs.astral.sh/uv/):
|
|
44
|
+
|
|
45
|
+
```console
|
|
46
|
+
uv build
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The [quickstart](https://peter-gy.github.io/agent-plugins/guide/getting-started) creates a complete project, builds it, and locates the installed plugin. Use the [Hatchling adapter](https://peter-gy.github.io/agent-plugins/guide/build-backends#hatchling) for Hatchling projects, or [attach a prebuilt wheel](https://peter-gy.github.io/agent-plugins/guide/attach-wheel) when another tool owns the build:
|
|
50
|
+
|
|
51
|
+
```console
|
|
52
|
+
agent-plugins attach-wheel dist/my_project-0.1.0-py3-none-any.whl --project .
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Attachment updates the wheel in place. Pass `--output-dir` to preserve the input.
|
|
56
|
+
|
|
57
|
+
## Inspect an installed plugin
|
|
58
|
+
|
|
59
|
+
Install `agent-plugins` in the environment you want to inspect. The package includes its own Agent Skill:
|
|
60
|
+
|
|
61
|
+
```console
|
|
62
|
+
pip install agent-plugins
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import agent_plugins as ap
|
|
67
|
+
|
|
68
|
+
plugin = ap.locate("agent-plugins")
|
|
69
|
+
skill = plugin.skill("agent-plugins")
|
|
70
|
+
|
|
71
|
+
print(skill.source)
|
|
72
|
+
print(skill.file("SKILL.md"))
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Pass your library's distribution name to `locate()` to inspect its plugin. Use [`Plugin.from_project()`](https://peter-gy.github.io/agent-plugins/guide/inspect-project) to inspect the selected source files before building.
|
|
76
|
+
|
|
77
|
+
## Documentation
|
|
78
|
+
|
|
79
|
+
- [Get started](https://peter-gy.github.io/agent-plugins/guide/getting-started): package, install, and locate a plugin.
|
|
80
|
+
- [How packaging works](https://peter-gy.github.io/agent-plugins/guide/artifact-lifecycle): wheels, source distributions, and editable installs.
|
|
81
|
+
- [Integrate](https://peter-gy.github.io/agent-plugins/guide/inspect-installed): read skills, inspect files, and resolve MCP configuration.
|
|
82
|
+
- [Python API](https://peter-gy.github.io/agent-plugins/reference/python-api) · [CLI](https://peter-gy.github.io/agent-plugins/reference/cli) · [Configuration](https://peter-gy.github.io/agent-plugins/reference/pyproject)
|
|
83
|
+
|
|
84
|
+
<details>
|
|
85
|
+
<summary>Packaging lifecycle</summary>
|
|
86
|
+
|
|
87
|
+
<p align="center">
|
|
88
|
+
<picture>
|
|
89
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/peter-gy/agent-plugins/main/docs/public/brand/agent-plugins-core-model-dark.svg">
|
|
90
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/peter-gy/agent-plugins/main/docs/public/brand/agent-plugins-core-model-light.svg">
|
|
91
|
+
<img alt="Core model: an authored Python project builds into one wheel that installs the library beside its version-matched Agent Plugin, which locate() returns as a plugin handle" src="https://raw.githubusercontent.com/peter-gy/agent-plugins/main/docs/public/brand/agent-plugins-core-model-light.svg" width="300">
|
|
92
|
+
</picture>
|
|
93
|
+
</p>
|
|
94
|
+
|
|
95
|
+
</details>
|
|
96
|
+
|
|
97
|
+
## Development
|
|
98
|
+
|
|
99
|
+
See [development_docs/](https://github.com/peter-gy/agent-plugins/tree/main/development_docs) for setup, architecture, checks, and releases. Serve the documentation locally with `pnpm --dir docs dev`.
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
[Apache-2.0](https://github.com/peter-gy/agent-plugins/blob/main/LICENSE).
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agent-plugins"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.2"
|
|
4
4
|
description = "Ship Agent Plugins with Python packages and inspect their installed files"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
7
7
|
license-files = ["LICENSE"]
|
|
8
8
|
requires-python = ">=3.10,<3.15"
|
|
9
|
-
dependencies = ["tomli
|
|
9
|
+
dependencies = ["tomli>=1.0.3; python_version < '3.11'"]
|
|
10
10
|
classifiers = [
|
|
11
11
|
"Programming Language :: Python :: 3.10",
|
|
12
12
|
"Programming Language :: Python :: 3.11",
|
|
@@ -30,7 +30,7 @@ agent-plugins = "agent_plugins._cli:main"
|
|
|
30
30
|
|
|
31
31
|
[build-system]
|
|
32
32
|
requires = [
|
|
33
|
-
"tomli
|
|
33
|
+
"tomli>=1.0.3; python_version < '3.11'",
|
|
34
34
|
"uv_build==0.12.2",
|
|
35
35
|
]
|
|
36
36
|
build-backend = "agent_plugins.build.uv_build"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agent-plugins"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.2"
|
|
4
4
|
description = "Ship Agent Plugins with Python packages and inspect their installed files"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -10,7 +10,7 @@ authors = [
|
|
|
10
10
|
]
|
|
11
11
|
requires-python = ">=3.10,<3.15"
|
|
12
12
|
dependencies = [
|
|
13
|
-
"tomli
|
|
13
|
+
"tomli>=1.0.3; python_version < '3.11'",
|
|
14
14
|
]
|
|
15
15
|
classifiers = [
|
|
16
16
|
"Programming Language :: Python :: 3.10",
|
|
@@ -31,7 +31,7 @@ agent-plugins = "agent_plugins._cli:main"
|
|
|
31
31
|
|
|
32
32
|
[build-system]
|
|
33
33
|
requires = [
|
|
34
|
-
"tomli
|
|
34
|
+
"tomli>=1.0.3; python_version < '3.11'",
|
|
35
35
|
"uv_build==0.12.2",
|
|
36
36
|
]
|
|
37
37
|
build-backend = "agent_plugins.build.uv_build"
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import sys
|
|
6
6
|
from dataclasses import dataclass
|
|
7
|
-
from pathlib import Path, PurePosixPath
|
|
7
|
+
from pathlib import Path, PurePosixPath, PureWindowsPath
|
|
8
8
|
from typing import cast
|
|
9
9
|
|
|
10
10
|
if sys.version_info >= (3, 11):
|
|
@@ -38,7 +38,7 @@ def build_plan(project: str | Path = ".") -> BuildPlan:
|
|
|
38
38
|
"""Load project configuration and return its complete plugin file plan."""
|
|
39
39
|
try:
|
|
40
40
|
project_path = Path(project).resolve()
|
|
41
|
-
except (OSError, RuntimeError) as error:
|
|
41
|
+
except (OSError, RuntimeError, ValueError) as error:
|
|
42
42
|
raise AgentPluginError(
|
|
43
43
|
f"Project path cannot be resolved: {project}. Check the path and retry."
|
|
44
44
|
) from error
|
|
@@ -63,10 +63,11 @@ def build_plan(project: str | Path = ".") -> BuildPlan:
|
|
|
63
63
|
|
|
64
64
|
staged = project_path / STAGED_ROOT
|
|
65
65
|
configured = project_path / root_value
|
|
66
|
-
|
|
66
|
+
is_staged = (staged / "plugin.json").is_file()
|
|
67
|
+
source_root = staged if is_staged else configured
|
|
67
68
|
try:
|
|
68
69
|
root = source_root.resolve(strict=True)
|
|
69
|
-
except (OSError, RuntimeError) as error:
|
|
70
|
+
except (OSError, RuntimeError, ValueError) as error:
|
|
70
71
|
raise AgentPluginError(
|
|
71
72
|
f"Agent Plugin root cannot be resolved: {source_root}"
|
|
72
73
|
) from error
|
|
@@ -75,10 +76,13 @@ def build_plan(project: str | Path = ".") -> BuildPlan:
|
|
|
75
76
|
|
|
76
77
|
files: dict[PurePosixPath, Path] = {}
|
|
77
78
|
_add_file(files, root, root / "plugin.json", required=True)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
if is_staged:
|
|
80
|
+
_add_tree(files, root, root)
|
|
81
|
+
else:
|
|
82
|
+
_add_tree(files, root, root / "skills")
|
|
83
|
+
_add_file(files, root, root / "mcp.json", required=False)
|
|
84
|
+
for pattern in include:
|
|
85
|
+
_add_pattern(files, root, pattern)
|
|
82
86
|
|
|
83
87
|
mappings = tuple(
|
|
84
88
|
FileMapping(source=source, target=target)
|
|
@@ -87,6 +91,58 @@ def build_plan(project: str | Path = ".") -> BuildPlan:
|
|
|
87
91
|
return BuildPlan(project=project_path, root=root, files=mappings)
|
|
88
92
|
|
|
89
93
|
|
|
94
|
+
def validate_plan(plan: BuildPlan) -> tuple[PurePosixPath, ...]:
|
|
95
|
+
"""Check that a plan can produce a discoverable plugin payload."""
|
|
96
|
+
files: list[PurePosixPath] = []
|
|
97
|
+
seen: set[PurePosixPath] = set()
|
|
98
|
+
for mapping in plan.files:
|
|
99
|
+
target = mapping.target
|
|
100
|
+
value = target.as_posix()
|
|
101
|
+
if (
|
|
102
|
+
target.is_absolute()
|
|
103
|
+
or PureWindowsPath(value).drive
|
|
104
|
+
or not target.parts
|
|
105
|
+
or ".." in target.parts
|
|
106
|
+
or "\\" in value
|
|
107
|
+
or "\x00" in value
|
|
108
|
+
):
|
|
109
|
+
raise AgentPluginError(
|
|
110
|
+
f"Plugin target must stay within the plugin root: {value!r}"
|
|
111
|
+
)
|
|
112
|
+
if target in seen:
|
|
113
|
+
raise AgentPluginError(f"Plugin plan contains a duplicate target: {value}")
|
|
114
|
+
collision = next(
|
|
115
|
+
(
|
|
116
|
+
existing
|
|
117
|
+
for existing in seen
|
|
118
|
+
if existing in target.parents or target in existing.parents
|
|
119
|
+
),
|
|
120
|
+
None,
|
|
121
|
+
)
|
|
122
|
+
if collision is not None:
|
|
123
|
+
raise AgentPluginError(
|
|
124
|
+
"Plugin plan contains file-directory target collisions: "
|
|
125
|
+
f"{collision.as_posix()} and {value}"
|
|
126
|
+
)
|
|
127
|
+
seen.add(target)
|
|
128
|
+
try:
|
|
129
|
+
source = mapping.source.resolve(strict=True)
|
|
130
|
+
except (OSError, RuntimeError, ValueError) as error:
|
|
131
|
+
raise AgentPluginError(
|
|
132
|
+
f"Plugin file cannot be read: {mapping.source}. "
|
|
133
|
+
"Restore the file and retry."
|
|
134
|
+
) from error
|
|
135
|
+
if not source.is_file():
|
|
136
|
+
raise AgentPluginError(
|
|
137
|
+
f"Plugin source is not a file: {source}. "
|
|
138
|
+
"Select a regular file and retry."
|
|
139
|
+
)
|
|
140
|
+
files.append(target)
|
|
141
|
+
if PurePosixPath("plugin.json") not in seen:
|
|
142
|
+
raise AgentPluginError("Plugin plan must include plugin.json")
|
|
143
|
+
return tuple(files)
|
|
144
|
+
|
|
145
|
+
|
|
90
146
|
def _config(document: dict[str, object], pyproject: Path) -> dict[str, object]:
|
|
91
147
|
tool = document.get("tool")
|
|
92
148
|
if not isinstance(tool, dict):
|
|
@@ -133,6 +189,8 @@ def _add_tree(files: dict[PurePosixPath, Path], root: Path, directory: Path) ->
|
|
|
133
189
|
return
|
|
134
190
|
if not directory.is_dir():
|
|
135
191
|
raise AgentPluginError(f"Expected a directory: {directory}")
|
|
192
|
+
if directory.is_symlink():
|
|
193
|
+
raise AgentPluginError(f"Directory symlinks cannot be packaged: {directory}")
|
|
136
194
|
for candidate in directory.rglob("*"):
|
|
137
195
|
if candidate.is_symlink() and candidate.is_dir():
|
|
138
196
|
raise AgentPluginError(
|
|
@@ -35,7 +35,7 @@ def _rewrite(source_path: Path, target_path: Path, plan: BuildPlan) -> None:
|
|
|
35
35
|
with tarfile.open(source_path, "r:gz") as source:
|
|
36
36
|
members = source.getmembers()
|
|
37
37
|
root = _archive_root(members)
|
|
38
|
-
stage =
|
|
38
|
+
stage = PurePosixPath(root, STAGED_ROOT)
|
|
39
39
|
|
|
40
40
|
with (
|
|
41
41
|
target_path.open("wb") as raw_target,
|
|
@@ -47,7 +47,7 @@ def _rewrite(source_path: Path, target_path: Path, plan: BuildPlan) -> None:
|
|
|
47
47
|
) as target,
|
|
48
48
|
):
|
|
49
49
|
for member in members:
|
|
50
|
-
if member.name
|
|
50
|
+
if PurePosixPath(member.name).is_relative_to(stage):
|
|
51
51
|
continue
|
|
52
52
|
file_object = source.extractfile(member) if member.isfile() else None
|
|
53
53
|
try:
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Attach Agent Plugin payloads to wheel artifacts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import stat
|
|
8
|
+
import tempfile
|
|
9
|
+
import zipfile
|
|
10
|
+
from contextlib import suppress
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path, PurePosixPath
|
|
13
|
+
|
|
14
|
+
from .._errors import AgentPluginError
|
|
15
|
+
from .plan import BuildPlan, build_plan, validate_plan
|
|
16
|
+
from .wheel_archive import rewrite_wheel
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class WheelAttachment:
|
|
21
|
+
"""Describe an Agent Plugin attached to a wheel."""
|
|
22
|
+
|
|
23
|
+
source: Path
|
|
24
|
+
output: Path
|
|
25
|
+
dist_info: PurePosixPath
|
|
26
|
+
plugin_root: PurePosixPath
|
|
27
|
+
files: tuple[PurePosixPath, ...]
|
|
28
|
+
replaced_existing_plugin: bool
|
|
29
|
+
removed_signatures: tuple[PurePosixPath, ...]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def attach_wheel(
|
|
33
|
+
wheel: str | Path,
|
|
34
|
+
*,
|
|
35
|
+
project: str | Path | None = None,
|
|
36
|
+
plan: BuildPlan | None = None,
|
|
37
|
+
output_dir: str | Path | None = None,
|
|
38
|
+
) -> WheelAttachment:
|
|
39
|
+
"""Attach a planned Agent Plugin to one existing wheel."""
|
|
40
|
+
if project is not None and plan is not None:
|
|
41
|
+
raise AgentPluginError(
|
|
42
|
+
"Pass either project or plan to attach_wheel(), not both"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
source, source_mode = _resolve_wheel(wheel)
|
|
46
|
+
output = _resolve_output(source, output_dir)
|
|
47
|
+
selected_plan = plan if plan is not None else build_plan(project or Path.cwd())
|
|
48
|
+
files = validate_plan(selected_plan)
|
|
49
|
+
|
|
50
|
+
temporary = _temporary_wheel(output)
|
|
51
|
+
try:
|
|
52
|
+
try:
|
|
53
|
+
layout = rewrite_wheel(
|
|
54
|
+
source,
|
|
55
|
+
temporary,
|
|
56
|
+
plan=selected_plan,
|
|
57
|
+
editable_root=None,
|
|
58
|
+
)
|
|
59
|
+
temporary.chmod(source_mode)
|
|
60
|
+
_publish(temporary, output, in_place=output == source)
|
|
61
|
+
except AgentPluginError:
|
|
62
|
+
raise
|
|
63
|
+
except zipfile.BadZipFile as error:
|
|
64
|
+
raise AgentPluginError(
|
|
65
|
+
f"Wheel is corrupt: {source}. Rebuild the wheel and retry."
|
|
66
|
+
) from error
|
|
67
|
+
except zipfile.LargeZipFile as error:
|
|
68
|
+
raise AgentPluginError(
|
|
69
|
+
f"Wheel exceeds the supported ZIP limits: {source}. "
|
|
70
|
+
"Rebuild the wheel with ZIP64 support and retry."
|
|
71
|
+
) from error
|
|
72
|
+
except NotImplementedError as error:
|
|
73
|
+
raise AgentPluginError(
|
|
74
|
+
f"Wheel uses unsupported ZIP compression: {source}. "
|
|
75
|
+
"Rebuild the wheel with a supported compression method and retry."
|
|
76
|
+
) from error
|
|
77
|
+
except OSError as error:
|
|
78
|
+
raise AgentPluginError(
|
|
79
|
+
f"Cannot write attached wheel: {output}. "
|
|
80
|
+
"Check file access and available space, then retry."
|
|
81
|
+
) from error
|
|
82
|
+
finally:
|
|
83
|
+
with suppress(OSError):
|
|
84
|
+
temporary.unlink(missing_ok=True)
|
|
85
|
+
|
|
86
|
+
return WheelAttachment(
|
|
87
|
+
source=source,
|
|
88
|
+
output=output,
|
|
89
|
+
dist_info=layout.dist_info,
|
|
90
|
+
plugin_root=layout.plugin_root,
|
|
91
|
+
files=files,
|
|
92
|
+
replaced_existing_plugin=layout.replaced_existing_plugin,
|
|
93
|
+
removed_signatures=layout.removed_signatures,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _attach_editable_wheel(wheel: Path, plan: BuildPlan) -> None:
|
|
98
|
+
"""Attach an authored-root marker to an editable wheel."""
|
|
99
|
+
source, source_mode = _resolve_wheel(wheel)
|
|
100
|
+
validate_plan(plan)
|
|
101
|
+
temporary = _temporary_wheel(source)
|
|
102
|
+
try:
|
|
103
|
+
try:
|
|
104
|
+
rewrite_wheel(source, temporary, plan=plan, editable_root=plan.root)
|
|
105
|
+
temporary.chmod(source_mode)
|
|
106
|
+
temporary.replace(source)
|
|
107
|
+
except AgentPluginError:
|
|
108
|
+
raise
|
|
109
|
+
except zipfile.BadZipFile as error:
|
|
110
|
+
raise AgentPluginError(
|
|
111
|
+
f"Wheel is corrupt: {source}. Rebuild the wheel and retry."
|
|
112
|
+
) from error
|
|
113
|
+
except (zipfile.LargeZipFile, NotImplementedError) as error:
|
|
114
|
+
raise AgentPluginError(
|
|
115
|
+
f"Wheel uses an unsupported ZIP format: {source}. "
|
|
116
|
+
"Rebuild the wheel and retry."
|
|
117
|
+
) from error
|
|
118
|
+
except OSError as error:
|
|
119
|
+
raise AgentPluginError(
|
|
120
|
+
f"Cannot write editable wheel: {source}. "
|
|
121
|
+
"Check file access and available space, then retry."
|
|
122
|
+
) from error
|
|
123
|
+
finally:
|
|
124
|
+
with suppress(OSError):
|
|
125
|
+
temporary.unlink(missing_ok=True)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _resolve_wheel(wheel: str | Path) -> tuple[Path, int]:
|
|
129
|
+
candidate = Path(wheel).expanduser()
|
|
130
|
+
if candidate.suffix != ".whl":
|
|
131
|
+
raise AgentPluginError(
|
|
132
|
+
f"Wheel path must end in .whl: {candidate}. Select a built wheel and retry."
|
|
133
|
+
)
|
|
134
|
+
try:
|
|
135
|
+
path = candidate.resolve(strict=True)
|
|
136
|
+
metadata = path.stat()
|
|
137
|
+
except (OSError, RuntimeError, ValueError) as error:
|
|
138
|
+
raise AgentPluginError(
|
|
139
|
+
f"Wheel cannot be read: {candidate}. "
|
|
140
|
+
"Check the path and file access, then retry."
|
|
141
|
+
) from error
|
|
142
|
+
if not path.is_file():
|
|
143
|
+
raise AgentPluginError(
|
|
144
|
+
f"Wheel path is not a file: {path}. Select a built wheel and retry."
|
|
145
|
+
)
|
|
146
|
+
return path, stat.S_IMODE(metadata.st_mode)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _resolve_output(source: Path, output_dir: str | Path | None) -> Path:
|
|
150
|
+
if output_dir is None:
|
|
151
|
+
return source
|
|
152
|
+
candidate = Path(output_dir).expanduser()
|
|
153
|
+
try:
|
|
154
|
+
directory = candidate.resolve(strict=True)
|
|
155
|
+
except (OSError, RuntimeError, ValueError) as error:
|
|
156
|
+
raise AgentPluginError(
|
|
157
|
+
f"Output directory cannot be used: {candidate}. "
|
|
158
|
+
"Create the directory and check its access, then retry."
|
|
159
|
+
) from error
|
|
160
|
+
if not directory.is_dir():
|
|
161
|
+
raise AgentPluginError(
|
|
162
|
+
f"Output directory is not a directory: {directory}. "
|
|
163
|
+
"Select an existing directory and retry."
|
|
164
|
+
)
|
|
165
|
+
output = directory / source.name
|
|
166
|
+
if output.exists() or output.is_symlink():
|
|
167
|
+
raise AgentPluginError(
|
|
168
|
+
f"Output wheel already exists: {output}. "
|
|
169
|
+
"Remove it or select another output directory."
|
|
170
|
+
)
|
|
171
|
+
return output
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _temporary_wheel(output: Path) -> Path:
|
|
175
|
+
try:
|
|
176
|
+
with tempfile.NamedTemporaryFile(
|
|
177
|
+
dir=output.parent,
|
|
178
|
+
prefix=f".{output.name}.",
|
|
179
|
+
suffix=".tmp",
|
|
180
|
+
delete=False,
|
|
181
|
+
) as temporary:
|
|
182
|
+
return Path(temporary.name)
|
|
183
|
+
except OSError as error:
|
|
184
|
+
raise AgentPluginError(
|
|
185
|
+
f"Cannot create a temporary wheel in {output.parent}. "
|
|
186
|
+
"Check directory access and available space, then retry."
|
|
187
|
+
) from error
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _publish(temporary: Path, output: Path, *, in_place: bool) -> None:
|
|
191
|
+
if in_place:
|
|
192
|
+
temporary.replace(output)
|
|
193
|
+
return
|
|
194
|
+
try:
|
|
195
|
+
os.link(temporary, output)
|
|
196
|
+
except FileExistsError as error:
|
|
197
|
+
raise AgentPluginError(
|
|
198
|
+
f"Output wheel already exists: {output}. "
|
|
199
|
+
"Remove it or select another output directory."
|
|
200
|
+
) from error
|
|
201
|
+
except OSError:
|
|
202
|
+
_publish_reserved(temporary, output)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _publish_reserved(temporary: Path, output: Path) -> None:
|
|
206
|
+
descriptor: int | None = None
|
|
207
|
+
try:
|
|
208
|
+
mode = stat.S_IMODE(temporary.stat().st_mode)
|
|
209
|
+
descriptor = os.open(output, os.O_CREAT | os.O_EXCL | os.O_WRONLY, mode)
|
|
210
|
+
if os.name != "nt":
|
|
211
|
+
os.fchmod(descriptor, mode)
|
|
212
|
+
with (
|
|
213
|
+
temporary.open("rb") as source,
|
|
214
|
+
os.fdopen(descriptor, "wb", closefd=False) as target,
|
|
215
|
+
):
|
|
216
|
+
shutil.copyfileobj(source, target, length=1024 * 1024)
|
|
217
|
+
target.flush()
|
|
218
|
+
os.fsync(descriptor)
|
|
219
|
+
if not os.path.samestat(os.fstat(descriptor), output.lstat()):
|
|
220
|
+
raise AgentPluginError(
|
|
221
|
+
f"Output wheel changed during publication: {output}. "
|
|
222
|
+
"Inspect the destination and retry."
|
|
223
|
+
)
|
|
224
|
+
except FileExistsError as error:
|
|
225
|
+
raise AgentPluginError(
|
|
226
|
+
f"Output wheel already exists: {output}. "
|
|
227
|
+
"Remove it or select another output directory."
|
|
228
|
+
) from error
|
|
229
|
+
except AgentPluginError:
|
|
230
|
+
raise
|
|
231
|
+
except OSError as error:
|
|
232
|
+
raise AgentPluginError(
|
|
233
|
+
f"Cannot publish attached wheel: {output}. "
|
|
234
|
+
"Remove an incomplete destination, check file access, and retry."
|
|
235
|
+
) from error
|
|
236
|
+
finally:
|
|
237
|
+
if descriptor is not None:
|
|
238
|
+
with suppress(OSError):
|
|
239
|
+
os.close(descriptor)
|