nutria-plugin 0.0.1a0__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.
Files changed (30) hide show
  1. nutria_plugin-0.0.1a0/.github/workflows/publish.yml +44 -0
  2. nutria_plugin-0.0.1a0/.gitignore +10 -0
  3. nutria_plugin-0.0.1a0/CHANGELOG.md +86 -0
  4. nutria_plugin-0.0.1a0/PKG-INFO +180 -0
  5. nutria_plugin-0.0.1a0/README.md +161 -0
  6. nutria_plugin-0.0.1a0/docs/cli.md +189 -0
  7. nutria_plugin-0.0.1a0/docs/connection-types.md +320 -0
  8. nutria_plugin-0.0.1a0/docs/index.md +53 -0
  9. nutria_plugin-0.0.1a0/docs/manifest.md +252 -0
  10. nutria_plugin-0.0.1a0/docs/python-api.md +412 -0
  11. nutria_plugin-0.0.1a0/docs/quickstart.md +215 -0
  12. nutria_plugin-0.0.1a0/docs/security.md +162 -0
  13. nutria_plugin-0.0.1a0/docs/skill-format.md +201 -0
  14. nutria_plugin-0.0.1a0/examples/my-first-plugin/README.md +7 -0
  15. nutria_plugin-0.0.1a0/examples/my-first-plugin/hooks/hooks.json +1 -0
  16. nutria_plugin-0.0.1a0/examples/my-first-plugin/plugin.json +14 -0
  17. nutria_plugin-0.0.1a0/examples/my-first-plugin/settings.schema.json +1 -0
  18. nutria_plugin-0.0.1a0/pyproject.toml +51 -0
  19. nutria_plugin-0.0.1a0/src/nutria_plugin/__init__.py +54 -0
  20. nutria_plugin-0.0.1a0/src/nutria_plugin/bundle.py +172 -0
  21. nutria_plugin-0.0.1a0/src/nutria_plugin/cli.py +157 -0
  22. nutria_plugin-0.0.1a0/src/nutria_plugin/manifest.py +182 -0
  23. nutria_plugin-0.0.1a0/src/nutria_plugin/packaging.py +202 -0
  24. nutria_plugin-0.0.1a0/src/nutria_plugin/signing.py +142 -0
  25. nutria_plugin-0.0.1a0/tests/test_bundle.py +157 -0
  26. nutria_plugin-0.0.1a0/tests/test_cli.py +69 -0
  27. nutria_plugin-0.0.1a0/tests/test_manifest.py +135 -0
  28. nutria_plugin-0.0.1a0/tests/test_packaging.py +131 -0
  29. nutria_plugin-0.0.1a0/tests/test_signing.py +133 -0
  30. nutria_plugin-0.0.1a0/uv.lock +524 -0
@@ -0,0 +1,44 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ build:
10
+ name: Build distribution
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - name: Install uv
16
+ uses: astral-sh/setup-uv@v5
17
+ with:
18
+ enable-cache: true
19
+
20
+ - name: Build packages
21
+ run: uv build
22
+
23
+ - name: Upload dist artifacts
24
+ uses: actions/upload-artifact@v4
25
+ with:
26
+ name: dist
27
+ path: dist/
28
+
29
+ publish:
30
+ name: Publish to PyPI
31
+ needs: build
32
+ runs-on: ubuntu-latest
33
+ environment: pypi
34
+ permissions:
35
+ id-token: write # required for OIDC trusted publishing
36
+ steps:
37
+ - name: Download dist artifacts
38
+ uses: actions/download-artifact@v4
39
+ with:
40
+ name: dist
41
+ path: dist/
42
+
43
+ - name: Publish to PyPI
44
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ *.egg-info/
6
+ .pytest_cache/
7
+ .ruff_cache/
8
+ *.zip
9
+ *.pem
10
+ *.pub.pem
@@ -0,0 +1,86 @@
1
+ # Changelog
2
+
3
+ All notable changes to `nutria-plugin` are documented here.
4
+
5
+ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
+ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ---
9
+
10
+ ## [0.0.1-alpha] — 2025-03-08
11
+
12
+ Initial alpha release. API and file format are not yet stable.
13
+
14
+ ### Added
15
+
16
+ **Manifest model (`manifest.py`)**
17
+ - `PluginManifest` — Pydantic v2 model for `plugin.json` with strict validation
18
+ - Fields: `id`, `name`, `version`, `description`, `author`, `runtime_types`,
19
+ `default_scope`, `compatibility`, `paths`, `required_secrets`,
20
+ `remote_endpoints`, `capabilities`, `tags`, `homepage`, `license`, `signature`
21
+ - `PluginRuntimeType` enum: `remote_mcp`, `declarative_api`, `openapi_bridge`, `soap_bridge`
22
+ - `PluginScope` enum: `platform`, `store`, `persona`
23
+ - `PluginCompatibility` model with semver-validated `min_nutria_version` / `max_nutria_version`
24
+ - `PluginPaths` model for overriding default component paths
25
+ - `PluginManifest.from_file()`, `from_json_bytes()`, `to_file()` helpers
26
+
27
+ **Bundle operations (`bundle.py`)**
28
+ - `load_plugin_bundle(data: bytes) -> PluginManifest` — parse and validate a plugin ZIP
29
+ - `extract_plugin_bundle(data: bytes, target_dir: Path) -> PluginManifest` — safe extraction
30
+ - `validate_zip(data: bytes) -> list[str]` — non-extracting ZIP validation
31
+ - `PluginBundleError` exception
32
+ - Decompression bomb guard: 100 MB uncompressed size limit
33
+ - Extension allowlist: `.json .md .yaml .yml .txt .png .jpg .jpeg .svg .ico .pdf .wsdl .xsd .xml .csv`
34
+ - Path traversal prevention via `PurePosixPath` normalization
35
+ - Maximum ZIP size: 20 MB
36
+
37
+ **Packaging (`packaging.py`)**
38
+ - `scaffold_plugin(plugin_id, name, target_dir)` — create standard plugin directory
39
+ - `pack_plugin(plugin_dir, output_path, sign, private_key_pem) -> Path` — validate and pack
40
+ - `validate_plugin_dir(plugin_dir) -> list[str]` — directory validation (hidden files skipped)
41
+ - `PackagingError` exception
42
+ - Symlink rejection at pack time
43
+ - Hidden file skipping (consistent between `validate_plugin_dir` and `pack_plugin`)
44
+
45
+ **Signing (`signing.py`)**
46
+ - `generate_keypair() -> tuple[str, str]` — ECDSA P-256 key pair generation
47
+ - `sign_manifest(manifest: dict, private_key_pem: str) -> str` — sign and return hex DER signature
48
+ - `verify_manifest(manifest: dict) -> SignatureStatus` — verify against `NUTRIA_PLUGIN_TRUSTED_KEYS`
49
+ - `SignatureStatus` enum: `VERIFIED`, `UNSIGNED`, `INVALID`, `UNTRUSTED`, `MISSING`
50
+ - Canonical payload serialization (sorted keys, no `signature` field, no whitespace)
51
+ - `NUTRIA_PLUGIN_TRUSTED_KEYS` env var for trusted public key list
52
+
53
+ **CLI (`cli.py`)**
54
+ - `nutria-plugin new <id>` — scaffold plugin directory
55
+ - `nutria-plugin validate [dir]` — validate plugin directory
56
+ - `nutria-plugin pack [dir]` — validate and pack to ZIP
57
+ - `nutria-plugin sign [manifest] --key <pem>` — sign manifest in-place
58
+ - `nutria-plugin keygen [--out <stem>]` — generate ECDSA P-256 key pair
59
+ - `--key` flag on `pack` for inline sign-and-pack
60
+ - `--output`/`-o` flag on `pack` for custom output path
61
+
62
+ **Documentation**
63
+ - `docs/index.md` — overview and navigation
64
+ - `docs/quickstart.md` — first plugin in 5 minutes
65
+ - `docs/manifest.md` — complete `plugin.json` field reference
66
+ - `docs/connection-types.md` — all 4 runtime types with annotated examples
67
+ - `docs/skill-format.md` — `SKILL.md` frontmatter schema and authoring guide
68
+ - `docs/security.md` — signing, trust policies, ZIP safety, secrets
69
+ - `docs/cli.md` — all CLI commands and flags
70
+ - `docs/python-api.md` — Python API reference
71
+
72
+ **Security hardening**
73
+ - SSRF protection: `remote_endpoints` blocks loopback, private, link-local, reserved IPs
74
+ - Decompression bomb: 100 MB limit on `ZipInfo.file_size` before extraction
75
+ - Symlink blocking: `PackagingError` raised on any symlink in plugin source
76
+ - Extension allowlist (not blocklist)
77
+ - CLI `keygen --out` path traversal prevention (output path must be within CWD)
78
+ - `_safe_zip_path`: checks `".." in path.parts` on normalized `PurePosixPath`
79
+
80
+ ### Notes
81
+
82
+ - This is an alpha release. Manifest schema, connection file format, and
83
+ Python API may change before `0.1.0`.
84
+ - `declarative_api` connection file format is not yet formally versioned.
85
+ - WSDL/OpenAPI bridge tool name conventions are established but the bridge
86
+ runtime is implemented in the ChatBotNutralia host, not this package.
@@ -0,0 +1,180 @@
1
+ Metadata-Version: 2.4
2
+ Name: nutria-plugin
3
+ Version: 0.0.1a0
4
+ Summary: SDK for building, validating, signing, and packaging Nutria plugins
5
+ Project-URL: Homepage, https://github.com/AlRos14/nutria-plugin-sdk
6
+ Project-URL: Repository, https://github.com/AlRos14/nutria-plugin-sdk
7
+ Project-URL: Changelog, https://github.com/AlRos14/nutria-plugin-sdk/blob/main/CHANGELOG.md
8
+ Author-email: Nutria <dev@nutria.ai>
9
+ License: MIT
10
+ Requires-Python: >=3.11
11
+ Requires-Dist: cryptography>=41.0
12
+ Requires-Dist: lxml>=4.9
13
+ Requires-Dist: pydantic>=2.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
16
+ Requires-Dist: pytest>=8.0; extra == 'dev'
17
+ Requires-Dist: ruff>=0.8; extra == 'dev'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # nutria-plugin SDK
21
+
22
+ SDK for building, validating, signing, and packaging Nutria plugins.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install nutria-plugin
28
+ # or with uv
29
+ uv add nutria-plugin
30
+ ```
31
+
32
+ ## Quickstart
33
+
34
+ ### 1. Scaffold a new plugin
35
+
36
+ ```bash
37
+ nutria-plugin new my-workspace-plugin --name "My Workspace Plugin"
38
+ ```
39
+
40
+ This creates:
41
+
42
+ ```
43
+ my-workspace-plugin/
44
+ plugin.json # manifest — edit this
45
+ README.md
46
+ connections/ # one JSON file per connection
47
+ skills/ # SKILL.md files
48
+ context_docs/ # Markdown docs injected into persona prompts
49
+ specs/ # OpenAPI/WSDL specs
50
+ hooks/hooks.json # declarative hooks
51
+ settings.schema.json # config schema shown in admin UI
52
+ ```
53
+
54
+ ### 2. Edit plugin.json
55
+
56
+ ```json
57
+ {
58
+ "schema_version": "1.0",
59
+ "id": "my-workspace-plugin",
60
+ "name": "My Workspace Plugin",
61
+ "version": "0.1.0",
62
+ "description": "Connects Nutria to My Workspace tool",
63
+ "author": "Your Name",
64
+ "runtime_types": ["declarative_api"],
65
+ "required_secrets": ["API_KEY"],
66
+ "remote_endpoints": ["https://api.myworkspace.com"]
67
+ }
68
+ ```
69
+
70
+ ### 3. Validate
71
+
72
+ ```bash
73
+ nutria-plugin validate .
74
+ # OK
75
+ ```
76
+
77
+ ### 4. Pack
78
+
79
+ ```bash
80
+ nutria-plugin pack . --output my-workspace-plugin-0.1.0.zip
81
+ # Packed: my-workspace-plugin-0.1.0.zip
82
+ ```
83
+
84
+ ### 5. Sign (optional)
85
+
86
+ Generate a key pair once:
87
+
88
+ ```bash
89
+ nutria-plugin keygen --out my-signing-key
90
+ # Private key: my-signing-key.pem
91
+ # Public key: my-signing-key.pub.pem
92
+ ```
93
+
94
+ Sign before packing:
95
+
96
+ ```bash
97
+ nutria-plugin sign plugin.json --key my-signing-key.pem
98
+ nutria-plugin pack . --output my-workspace-plugin-0.1.0.zip
99
+ ```
100
+
101
+ Or sign during pack:
102
+
103
+ ```bash
104
+ nutria-plugin pack . --key my-signing-key.pem --output my-workspace-plugin-0.1.0.zip
105
+ ```
106
+
107
+ Configure the Nutria instance to trust your public key:
108
+
109
+ ```bash
110
+ export NUTRIA_PLUGIN_TRUSTED_KEYS='["-----BEGIN PUBLIC KEY-----\n..."]'
111
+ ```
112
+
113
+ ## Python API
114
+
115
+ ```python
116
+ from nutria_plugin import (
117
+ PluginManifest,
118
+ load_plugin_bundle,
119
+ extract_plugin_bundle,
120
+ validate_zip,
121
+ scaffold_plugin,
122
+ pack_plugin,
123
+ validate_plugin_dir,
124
+ generate_keypair,
125
+ sign_manifest,
126
+ verify_manifest,
127
+ SignatureStatus,
128
+ )
129
+
130
+ # Parse a manifest
131
+ manifest = PluginManifest.from_file(Path("plugin.json"))
132
+
133
+ # Load from ZIP bytes
134
+ manifest = load_plugin_bundle(zip_bytes)
135
+
136
+ # Extract to disk
137
+ manifest = extract_plugin_bundle(zip_bytes, target_dir)
138
+
139
+ # Sign and verify
140
+ private_pem, public_pem = generate_keypair()
141
+ sig = sign_manifest(manifest.model_dump(), private_pem)
142
+ status = verify_manifest(manifest.model_dump())
143
+ assert status == SignatureStatus.VERIFIED
144
+ ```
145
+
146
+ ## Plugin ZIP format
147
+
148
+ | Path | Description |
149
+ |------|-------------|
150
+ | `plugin.json` | Manifest (required) |
151
+ | `README.md` | Human-readable description |
152
+ | `connections/*.json` | Connection definitions |
153
+ | `skills/<name>/SKILL.md` | Skill instructions |
154
+ | `context_docs/*.md` | Docs injected into persona prompts |
155
+ | `specs/openapi.json` | OpenAPI spec (for `openapi_bridge` runtime) |
156
+ | `specs/service.wsdl` | WSDL spec (for `soap_bridge` runtime) |
157
+ | `hooks/hooks.json` | Declarative hooks |
158
+ | `settings.schema.json` | JSON Schema for configuration |
159
+ | `assets/icon.png` | Plugin icon |
160
+
161
+ ### Security rules
162
+
163
+ - No executable files (`.py`, `.js`, `.sh`, etc.) allowed in the ZIP.
164
+ - No hidden files or directories.
165
+ - No absolute paths or path traversal in ZIP entries.
166
+ - Maximum bundle size: 20 MB.
167
+ - Secrets are **never** stored in the ZIP — they are configured after install.
168
+
169
+ ## Runtime types
170
+
171
+ | Value | Description |
172
+ |-------|-------------|
173
+ | `remote_mcp` | Plugin connects to a separately deployed MCP server |
174
+ | `declarative_api` | Plugin uses declarative connection JSON files (no server needed) |
175
+ | `openapi_bridge` | Nutria auto-generates tools from an OpenAPI/Swagger spec |
176
+ | `soap_bridge` | Nutria auto-generates tools from a WSDL/SOAP spec |
177
+
178
+ ## License
179
+
180
+ MIT
@@ -0,0 +1,161 @@
1
+ # nutria-plugin SDK
2
+
3
+ SDK for building, validating, signing, and packaging Nutria plugins.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install nutria-plugin
9
+ # or with uv
10
+ uv add nutria-plugin
11
+ ```
12
+
13
+ ## Quickstart
14
+
15
+ ### 1. Scaffold a new plugin
16
+
17
+ ```bash
18
+ nutria-plugin new my-workspace-plugin --name "My Workspace Plugin"
19
+ ```
20
+
21
+ This creates:
22
+
23
+ ```
24
+ my-workspace-plugin/
25
+ plugin.json # manifest — edit this
26
+ README.md
27
+ connections/ # one JSON file per connection
28
+ skills/ # SKILL.md files
29
+ context_docs/ # Markdown docs injected into persona prompts
30
+ specs/ # OpenAPI/WSDL specs
31
+ hooks/hooks.json # declarative hooks
32
+ settings.schema.json # config schema shown in admin UI
33
+ ```
34
+
35
+ ### 2. Edit plugin.json
36
+
37
+ ```json
38
+ {
39
+ "schema_version": "1.0",
40
+ "id": "my-workspace-plugin",
41
+ "name": "My Workspace Plugin",
42
+ "version": "0.1.0",
43
+ "description": "Connects Nutria to My Workspace tool",
44
+ "author": "Your Name",
45
+ "runtime_types": ["declarative_api"],
46
+ "required_secrets": ["API_KEY"],
47
+ "remote_endpoints": ["https://api.myworkspace.com"]
48
+ }
49
+ ```
50
+
51
+ ### 3. Validate
52
+
53
+ ```bash
54
+ nutria-plugin validate .
55
+ # OK
56
+ ```
57
+
58
+ ### 4. Pack
59
+
60
+ ```bash
61
+ nutria-plugin pack . --output my-workspace-plugin-0.1.0.zip
62
+ # Packed: my-workspace-plugin-0.1.0.zip
63
+ ```
64
+
65
+ ### 5. Sign (optional)
66
+
67
+ Generate a key pair once:
68
+
69
+ ```bash
70
+ nutria-plugin keygen --out my-signing-key
71
+ # Private key: my-signing-key.pem
72
+ # Public key: my-signing-key.pub.pem
73
+ ```
74
+
75
+ Sign before packing:
76
+
77
+ ```bash
78
+ nutria-plugin sign plugin.json --key my-signing-key.pem
79
+ nutria-plugin pack . --output my-workspace-plugin-0.1.0.zip
80
+ ```
81
+
82
+ Or sign during pack:
83
+
84
+ ```bash
85
+ nutria-plugin pack . --key my-signing-key.pem --output my-workspace-plugin-0.1.0.zip
86
+ ```
87
+
88
+ Configure the Nutria instance to trust your public key:
89
+
90
+ ```bash
91
+ export NUTRIA_PLUGIN_TRUSTED_KEYS='["-----BEGIN PUBLIC KEY-----\n..."]'
92
+ ```
93
+
94
+ ## Python API
95
+
96
+ ```python
97
+ from nutria_plugin import (
98
+ PluginManifest,
99
+ load_plugin_bundle,
100
+ extract_plugin_bundle,
101
+ validate_zip,
102
+ scaffold_plugin,
103
+ pack_plugin,
104
+ validate_plugin_dir,
105
+ generate_keypair,
106
+ sign_manifest,
107
+ verify_manifest,
108
+ SignatureStatus,
109
+ )
110
+
111
+ # Parse a manifest
112
+ manifest = PluginManifest.from_file(Path("plugin.json"))
113
+
114
+ # Load from ZIP bytes
115
+ manifest = load_plugin_bundle(zip_bytes)
116
+
117
+ # Extract to disk
118
+ manifest = extract_plugin_bundle(zip_bytes, target_dir)
119
+
120
+ # Sign and verify
121
+ private_pem, public_pem = generate_keypair()
122
+ sig = sign_manifest(manifest.model_dump(), private_pem)
123
+ status = verify_manifest(manifest.model_dump())
124
+ assert status == SignatureStatus.VERIFIED
125
+ ```
126
+
127
+ ## Plugin ZIP format
128
+
129
+ | Path | Description |
130
+ |------|-------------|
131
+ | `plugin.json` | Manifest (required) |
132
+ | `README.md` | Human-readable description |
133
+ | `connections/*.json` | Connection definitions |
134
+ | `skills/<name>/SKILL.md` | Skill instructions |
135
+ | `context_docs/*.md` | Docs injected into persona prompts |
136
+ | `specs/openapi.json` | OpenAPI spec (for `openapi_bridge` runtime) |
137
+ | `specs/service.wsdl` | WSDL spec (for `soap_bridge` runtime) |
138
+ | `hooks/hooks.json` | Declarative hooks |
139
+ | `settings.schema.json` | JSON Schema for configuration |
140
+ | `assets/icon.png` | Plugin icon |
141
+
142
+ ### Security rules
143
+
144
+ - No executable files (`.py`, `.js`, `.sh`, etc.) allowed in the ZIP.
145
+ - No hidden files or directories.
146
+ - No absolute paths or path traversal in ZIP entries.
147
+ - Maximum bundle size: 20 MB.
148
+ - Secrets are **never** stored in the ZIP — they are configured after install.
149
+
150
+ ## Runtime types
151
+
152
+ | Value | Description |
153
+ |-------|-------------|
154
+ | `remote_mcp` | Plugin connects to a separately deployed MCP server |
155
+ | `declarative_api` | Plugin uses declarative connection JSON files (no server needed) |
156
+ | `openapi_bridge` | Nutria auto-generates tools from an OpenAPI/Swagger spec |
157
+ | `soap_bridge` | Nutria auto-generates tools from a WSDL/SOAP spec |
158
+
159
+ ## License
160
+
161
+ MIT
@@ -0,0 +1,189 @@
1
+ # CLI Reference
2
+
3
+ The `nutria-plugin` CLI is the primary tool for plugin development.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ uv add nutria-plugin
9
+ # or
10
+ pip install nutria-plugin
11
+ ```
12
+
13
+ ---
14
+
15
+ ## `nutria-plugin new` — Scaffold a plugin
16
+
17
+ Create a new plugin directory with the standard structure.
18
+
19
+ ```bash
20
+ nutria-plugin new <id> [--name <display-name>] [--dir <target-dir>]
21
+ ```
22
+
23
+ | Argument | Description |
24
+ |----------|-------------|
25
+ | `id` | Plugin ID (lowercase, hyphens allowed, e.g. `my-plugin`) |
26
+ | `--name` | Display name. Defaults to title-cased `id` |
27
+ | `--dir` | Target directory. Defaults to `<id>/` in the current directory |
28
+
29
+ **Examples:**
30
+
31
+ ```bash
32
+ # Scaffold in ./my-crm-plugin/
33
+ nutria-plugin new my-crm-plugin
34
+
35
+ # Custom name and directory
36
+ nutria-plugin new my-crm-plugin --name "My CRM Plugin" --dir ~/plugins/my-crm
37
+ ```
38
+
39
+ **Created structure:**
40
+
41
+ ```
42
+ <dir>/
43
+ plugin.json # pre-filled manifest
44
+ README.md
45
+ connections/ # empty — add your connection JSON files
46
+ skills/ # empty — add your SKILL.md subdirectories
47
+ context_docs/ # empty — add your Markdown reference docs
48
+ specs/ # empty — add OpenAPI/WSDL files
49
+ hooks/hooks.json # empty hooks array
50
+ settings.schema.json # empty JSON Schema
51
+ assets/ # empty
52
+ ```
53
+
54
+ ---
55
+
56
+ ## `nutria-plugin validate` — Validate a plugin
57
+
58
+ Validate a plugin directory (does not pack).
59
+
60
+ ```bash
61
+ nutria-plugin validate [<dir>]
62
+ ```
63
+
64
+ | Argument | Description |
65
+ |----------|-------------|
66
+ | `dir` | Plugin directory to validate. Defaults to `.` |
67
+
68
+ **Exit codes:** `0` = valid, `1` = one or more errors found.
69
+
70
+ **Output:**
71
+
72
+ ```bash
73
+ $ nutria-plugin validate my-plugin/
74
+ OK
75
+
76
+ $ nutria-plugin validate broken-plugin/
77
+ Validation errors:
78
+ - invalid plugin.json: version must use semantic versioning
79
+ - invalid plugin.json: remote_endpoints[0] targets a private/internal address
80
+ ```
81
+
82
+ Validation checks:
83
+ - All `plugin.json` fields (see [manifest.md](manifest.md))
84
+ - Required files present (`plugin.json`, at least one connection or skill)
85
+ - No hidden files in the directory
86
+ - No symlinks
87
+
88
+ ---
89
+
90
+ ## `nutria-plugin pack` — Validate and pack a ZIP
91
+
92
+ Validate the plugin and produce a distributable ZIP archive.
93
+
94
+ ```bash
95
+ nutria-plugin pack [<dir>] [--output <path>] [--key <pem-file>]
96
+ ```
97
+
98
+ | Argument | Description |
99
+ |----------|-------------|
100
+ | `dir` | Plugin directory to pack. Defaults to `.` |
101
+ | `--output`, `-o` | Output ZIP path. Defaults to `<id>-<version>.zip` in the current directory |
102
+ | `--key` | Path to a PEM private key file. If provided, the manifest is signed before packing |
103
+
104
+ **Examples:**
105
+
106
+ ```bash
107
+ # Pack to default name (my-plugin-0.1.0.zip)
108
+ nutria-plugin pack my-plugin/
109
+
110
+ # Custom output path
111
+ nutria-plugin pack my-plugin/ --output dist/my-plugin-0.1.0.zip
112
+
113
+ # Sign and pack
114
+ nutria-plugin pack my-plugin/ --key my-signing-key.pem --output dist/my-plugin-0.1.0.zip
115
+ ```
116
+
117
+ **What pack does:**
118
+ 1. Runs full validation — aborts on any error
119
+ 2. Collects all files, skipping hidden files and checking extensions
120
+ 3. Rejects symlinks
121
+ 4. Writes a ZIP with stored (non-compressed) entries and safe paths
122
+ 5. Optionally signs the manifest before writing
123
+
124
+ ---
125
+
126
+ ## `nutria-plugin sign` — Sign a manifest in-place
127
+
128
+ Sign an existing `plugin.json` file. Modifies the file by adding or updating
129
+ the `signature` field.
130
+
131
+ ```bash
132
+ nutria-plugin sign [<manifest>] --key <pem-file>
133
+ ```
134
+
135
+ | Argument | Description |
136
+ |----------|-------------|
137
+ | `manifest` | Path to `plugin.json`. Defaults to `plugin.json` in the current directory |
138
+ | `--key` | Path to the PEM private key file (required) |
139
+
140
+ **Example:**
141
+
142
+ ```bash
143
+ nutria-plugin sign plugin.json --key my-signing-key.pem
144
+ ```
145
+
146
+ Signing does not pack — run `nutria-plugin pack` after signing to produce
147
+ the distributable ZIP.
148
+
149
+ ---
150
+
151
+ ## `nutria-plugin keygen` — Generate a signing key pair
152
+
153
+ Generate an ECDSA P-256 key pair for plugin signing.
154
+
155
+ ```bash
156
+ nutria-plugin keygen [--out <stem>]
157
+ ```
158
+
159
+ | Argument | Description |
160
+ |----------|-------------|
161
+ | `--out` | Output file stem. Defaults to `nutria-plugin`. Output path must be within the current directory |
162
+
163
+ **Output files:**
164
+
165
+ | File | Contents |
166
+ |------|----------|
167
+ | `<stem>.pem` | PEM-encoded private key — keep secret, never commit |
168
+ | `<stem>.pub.pem` | PEM-encoded public key — distribute to Nutria instances |
169
+
170
+ **Example:**
171
+
172
+ ```bash
173
+ nutria-plugin keygen --out acme-publisher
174
+ # Private key: acme-publisher.pem
175
+ # Public key: acme-publisher.pub.pem
176
+ ```
177
+
178
+ **Security note:** The output path is resolved and must be within the current
179
+ working directory. Path traversal in `--out` is rejected.
180
+
181
+ ---
182
+
183
+ ## Exit codes
184
+
185
+ | Code | Meaning |
186
+ |------|---------|
187
+ | `0` | Success |
188
+ | `1` | Validation error, signing error, or unexpected failure |
189
+ | `2` | CLI usage error (invalid arguments) |