nutria-plugin 0.3.2__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.
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/CHANGELOG.md +10 -0
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/PKG-INFO +15 -14
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/README.md +14 -13
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/docs/index.md +3 -3
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/docs/manifest.md +13 -11
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/docs/python-api.md +2 -2
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/docs/quickstart.md +4 -4
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/docs/reviewable-actions.md +8 -7
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/pyproject.toml +1 -1
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/src/nutria_plugin/__init__.py +1 -1
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/src/nutria_plugin/capabilities.py +1 -15
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/src/nutria_plugin/manifest.py +17 -4
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/src/nutria_plugin/packaging.py +1 -1
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/tests/test_bundle.py +1 -1
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/tests/test_capability_contracts.py +56 -15
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/tests/test_manifest.py +4 -4
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/tests/test_reviewable_actions.py +35 -5
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/tests/test_signing.py +1 -1
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/uv.lock +1 -1
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/.github/workflows/publish.yml +0 -0
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/.gitignore +0 -0
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/docs/admin-extensions.md +0 -0
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/docs/admin-flows.md +0 -0
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/docs/cli.md +0 -0
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/docs/connection-types.md +0 -0
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/docs/security.md +0 -0
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/docs/skill-format.md +0 -0
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/examples/my-first-plugin/README.md +0 -0
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/examples/my-first-plugin/hooks/hooks.json +0 -0
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/examples/my-first-plugin/plugin.json +0 -0
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/examples/my-first-plugin/settings.schema.json +0 -0
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/src/nutria_plugin/bundle.py +0 -0
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/src/nutria_plugin/cli.py +0 -0
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/src/nutria_plugin/mcp_results.py +0 -0
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/src/nutria_plugin/signing.py +0 -0
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/tests/test_cli.py +0 -0
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/tests/test_mcp_results.py +0 -0
- {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/tests/test_packaging.py +0 -0
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
- Replace the `prepare` effect with structural `prepared_action` outputs and the
|
|
6
|
+
capability's actual `read` or `write` mutation scope.
|
|
7
|
+
- Resolve `requirements.task_context` universally with `optional` as the default
|
|
8
|
+
and support the `required | optional | forbidden` tri-state.
|
|
9
|
+
- Allow host-owned task resources as capability outputs without requiring an
|
|
10
|
+
external world provider to claim them.
|
|
11
|
+
- Accept only manifest schema 4.0; no legacy schema behavior is retained.
|
|
12
|
+
|
|
3
13
|
## 0.3.2
|
|
4
14
|
|
|
5
15
|
- Add transport-neutral MCP result normalization for FastMCP scalar JSON envelopes.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: nutria-plugin
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: SDK for building, validating, signing, and packaging Nutria plugins
|
|
5
5
|
Project-URL: Homepage, https://github.com/AlRos14/nutria-plugin-sdk
|
|
6
6
|
Project-URL: Repository, https://github.com/AlRos14/nutria-plugin-sdk
|
|
@@ -21,11 +21,11 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
|
|
22
22
|
SDK for building, validating, signing, and packaging Nutria plugins.
|
|
23
23
|
|
|
24
|
-
Release `0.
|
|
25
|
-
`
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
Release `0.4.0` accepts only manifest schema `4.0`. Capabilities use their real
|
|
25
|
+
business effect (`read`, `write`, or `external_write`); preparation is expressed
|
|
26
|
+
by producing `prepared_action`, not by a workflow-phase effect. Every parsed
|
|
27
|
+
requirement resolves `task_context` to `required`, `optional`, or `forbidden`,
|
|
28
|
+
with `optional` as the schema default. There is no legacy manifest compatibility.
|
|
29
29
|
|
|
30
30
|
ChatBotNutralia owns task context, reviewable drafts, revisions, approval,
|
|
31
31
|
idempotency, and completion receipts. Plugins own authoritative provider reads
|
|
@@ -34,7 +34,7 @@ and exact delivery of an approved snapshot.
|
|
|
34
34
|
## Install
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
uv add nutria-plugin==0.
|
|
37
|
+
uv add nutria-plugin==0.4.0
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
## Scaffold and validate
|
|
@@ -49,11 +49,11 @@ The generated directory contains `plugin.json`, component directories for
|
|
|
49
49
|
connections, skills, context documents, specs and hooks, plus an optional
|
|
50
50
|
settings schema and assets.
|
|
51
51
|
|
|
52
|
-
## Minimal schema
|
|
52
|
+
## Minimal schema 4.0 manifest
|
|
53
53
|
|
|
54
54
|
```json
|
|
55
55
|
{
|
|
56
|
-
"schema_version": "
|
|
56
|
+
"schema_version": "4.0",
|
|
57
57
|
"id": "my-workspace-plugin",
|
|
58
58
|
"name": "My Workspace Plugin",
|
|
59
59
|
"version": "0.1.0",
|
|
@@ -98,11 +98,12 @@ settings schema and assets.
|
|
|
98
98
|
}
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
Every capability must declare `requirements` and `exposure`.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
Every capability must declare `requirements` and `exposure`. `task_context`
|
|
102
|
+
defaults to `optional`; hosts resolve and may promote it to `required` when the
|
|
103
|
+
capability consumes or produces task-owned resources such as `task`, `artifact`,
|
|
104
|
+
or `prepared_action`. Host/admin-only capabilities also require a safe
|
|
105
|
+
`non_callable_reason`. Every connection referenced by a capability must be
|
|
106
|
+
represented by a matching world provider.
|
|
106
107
|
|
|
107
108
|
For customer messages, use a host-owned `reviewable_actions` contract. Do not
|
|
108
109
|
create plugin draft tables or native saved-draft tools. See
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
SDK for building, validating, signing, and packaging Nutria plugins.
|
|
4
4
|
|
|
5
|
-
Release `0.
|
|
6
|
-
`
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
Release `0.4.0` accepts only manifest schema `4.0`. Capabilities use their real
|
|
6
|
+
business effect (`read`, `write`, or `external_write`); preparation is expressed
|
|
7
|
+
by producing `prepared_action`, not by a workflow-phase effect. Every parsed
|
|
8
|
+
requirement resolves `task_context` to `required`, `optional`, or `forbidden`,
|
|
9
|
+
with `optional` as the schema default. There is no legacy manifest compatibility.
|
|
10
10
|
|
|
11
11
|
ChatBotNutralia owns task context, reviewable drafts, revisions, approval,
|
|
12
12
|
idempotency, and completion receipts. Plugins own authoritative provider reads
|
|
@@ -15,7 +15,7 @@ and exact delivery of an approved snapshot.
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
uv add nutria-plugin==0.
|
|
18
|
+
uv add nutria-plugin==0.4.0
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## Scaffold and validate
|
|
@@ -30,11 +30,11 @@ The generated directory contains `plugin.json`, component directories for
|
|
|
30
30
|
connections, skills, context documents, specs and hooks, plus an optional
|
|
31
31
|
settings schema and assets.
|
|
32
32
|
|
|
33
|
-
## Minimal schema
|
|
33
|
+
## Minimal schema 4.0 manifest
|
|
34
34
|
|
|
35
35
|
```json
|
|
36
36
|
{
|
|
37
|
-
"schema_version": "
|
|
37
|
+
"schema_version": "4.0",
|
|
38
38
|
"id": "my-workspace-plugin",
|
|
39
39
|
"name": "My Workspace Plugin",
|
|
40
40
|
"version": "0.1.0",
|
|
@@ -79,11 +79,12 @@ settings schema and assets.
|
|
|
79
79
|
}
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
Every capability must declare `requirements` and `exposure`.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
Every capability must declare `requirements` and `exposure`. `task_context`
|
|
83
|
+
defaults to `optional`; hosts resolve and may promote it to `required` when the
|
|
84
|
+
capability consumes or produces task-owned resources such as `task`, `artifact`,
|
|
85
|
+
or `prepared_action`. Host/admin-only capabilities also require a safe
|
|
86
|
+
`non_callable_reason`. Every connection referenced by a capability must be
|
|
87
|
+
represented by a matching world provider.
|
|
87
88
|
|
|
88
89
|
For customer messages, use a host-owned `reviewable_actions` contract. Do not
|
|
89
90
|
create plugin draft tables or native saved-draft tools. See
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# nutria-plugin SDK documentation
|
|
2
2
|
|
|
3
|
-
Developer reference for SDK `0.
|
|
3
|
+
Developer reference for SDK `0.4.0` and manifest schema `4.0`.
|
|
4
4
|
|
|
5
5
|
| Document | Purpose |
|
|
6
6
|
|---|---|
|
|
7
|
-
| [quickstart.md](quickstart.md) | Build a strict schema
|
|
7
|
+
| [quickstart.md](quickstart.md) | Build a strict schema 4.0 plugin |
|
|
8
8
|
| [manifest.md](manifest.md) | Manifest, capability, and provider contracts |
|
|
9
9
|
| [reviewable-actions.md](reviewable-actions.md) | Host-owned drafts and external delivery |
|
|
10
10
|
| [python-api.md](python-api.md) | Public Python API |
|
|
@@ -16,6 +16,6 @@ Developer reference for SDK `0.3.1` and manifest schema `3.0`.
|
|
|
16
16
|
| [cli.md](cli.md) | CLI commands |
|
|
17
17
|
|
|
18
18
|
A plugin bundle can contain connections, skills, context documents, hooks,
|
|
19
|
-
settings, specs, and declarative admin assets. Its schema
|
|
19
|
+
settings, specs, and declarative admin assets. Its schema 4.0 manifest must also
|
|
20
20
|
declare at least one typed capability and world provider. Older schemas are not
|
|
21
21
|
loaded or migrated.
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# plugin.json manifest reference
|
|
2
2
|
|
|
3
3
|
`plugin.json` is the single source of truth for plugin identity, runtime,
|
|
4
|
-
capability authority, and provider topology. SDK 0.
|
|
5
|
-
`
|
|
4
|
+
capability authority, and provider topology. SDK 0.4.0 accepts exactly schema
|
|
5
|
+
`4.0`; older schemas and unknown fields fail validation.
|
|
6
6
|
|
|
7
7
|
## Required top-level fields
|
|
8
8
|
|
|
9
9
|
| Field | Contract |
|
|
10
10
|
|---|---|
|
|
11
|
-
| `schema_version` | Literal `"
|
|
11
|
+
| `schema_version` | Literal `"4.0"` |
|
|
12
12
|
| `id` | Lowercase plugin slug |
|
|
13
13
|
| `name`, `description`, `author` | Non-empty display metadata |
|
|
14
14
|
| `version` | Semantic version |
|
|
@@ -19,23 +19,24 @@ capability authority, and provider topology. SDK 0.3.1 accepts exactly schema
|
|
|
19
19
|
Optional fields include `default_scope`, `paths`, `required_secrets`,
|
|
20
20
|
`optional_secrets`, `remote_endpoints`, `tags`, `reviewable_actions`,
|
|
21
21
|
`admin_extensions`, `admin_flows`, `mcp_server_entry`, `homepage`, `license`,
|
|
22
|
-
and `signature`. A `compatibility` field is not part of schema
|
|
22
|
+
and `signature`. A `compatibility` field is not part of schema 4.0.
|
|
23
23
|
|
|
24
24
|
## Capability descriptor
|
|
25
25
|
|
|
26
26
|
Each capability declares:
|
|
27
27
|
|
|
28
28
|
- stable `id`, `title`, and `description`;
|
|
29
|
-
- `effect`: `read`, `
|
|
29
|
+
- `effect`: `read`, `write`, or `external_write`;
|
|
30
30
|
- concrete `tool` and optional `connection_id`;
|
|
31
31
|
- typed `inputs`, `consumes`, and `produces` bindings;
|
|
32
|
-
- required `requirements.authority
|
|
33
|
-
`requirements.task_context
|
|
32
|
+
- required `requirements.authority` and `requirements.audience`, plus resolved
|
|
33
|
+
`requirements.task_context` (`required`, `optional`, or `forbidden`, default
|
|
34
|
+
`optional`);
|
|
34
35
|
- required `exposure`: `model`, `host`, or `admin`.
|
|
35
36
|
|
|
36
37
|
`host` and `admin` exposure require `non_callable_reason` with a stable code and
|
|
37
|
-
safe summary. `model` exposure must not declare it.
|
|
38
|
-
|
|
38
|
+
safe summary. `model` exposure must not declare it. Hosts structurally promote
|
|
39
|
+
task-owned resource bindings to effective `task_context: "required"`.
|
|
39
40
|
|
|
40
41
|
A model-exposed `external_write` must declare `prepared_action`, `idempotency`,
|
|
41
42
|
and `completion`. Reviewable delivery capabilities are instead host-only and
|
|
@@ -60,8 +61,9 @@ same manifest.
|
|
|
60
61
|
The host owns the encrypted draft and approval lifecycle. A reviewable action
|
|
61
62
|
maps stable semantic fields to one host-only external-write capability. Every
|
|
62
63
|
delivery maps `recipient`, `body`, and `idempotency_key`; required capability
|
|
63
|
-
inputs must be mapped. Preparation capabilities
|
|
64
|
-
|
|
64
|
+
inputs must be mapped. Preparation capabilities produce `prepared_action` and
|
|
65
|
+
declare their actual `read` or `write` mutation scope. See
|
|
66
|
+
[reviewable-actions.md](reviewable-actions.md).
|
|
65
67
|
|
|
66
68
|
## Paths and settings
|
|
67
69
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Python API reference
|
|
2
2
|
|
|
3
|
-
SDK 0.
|
|
3
|
+
SDK 0.4.0 exposes the strict schema 4.0 API.
|
|
4
4
|
|
|
5
5
|
```python
|
|
6
6
|
from nutria_plugin import (
|
|
@@ -39,7 +39,7 @@ manifest.to_file(Path("plugin.json"))
|
|
|
39
39
|
payload = manifest.model_dump(mode="json", exclude_none=True)
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
Validation is strict: schema version must be `
|
|
42
|
+
Validation is strict: schema version must be `4.0`, top-level extras are
|
|
43
43
|
rejected, and capability/provider/action references are checked together.
|
|
44
44
|
|
|
45
45
|
## Bundles and packaging
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
# Quickstart: a schema
|
|
1
|
+
# Quickstart: a schema 4.0 plugin
|
|
2
2
|
|
|
3
3
|
## Install and scaffold
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
uv add nutria-plugin==0.
|
|
6
|
+
uv add nutria-plugin==0.4.0
|
|
7
7
|
nutria-plugin new inventory-lookup --name "Inventory Lookup"
|
|
8
8
|
```
|
|
9
9
|
|
|
@@ -11,7 +11,7 @@ nutria-plugin new inventory-lookup --name "Inventory Lookup"
|
|
|
11
11
|
|
|
12
12
|
```json
|
|
13
13
|
{
|
|
14
|
-
"schema_version": "
|
|
14
|
+
"schema_version": "4.0",
|
|
15
15
|
"id": "inventory-lookup",
|
|
16
16
|
"name": "Inventory Lookup",
|
|
17
17
|
"version": "0.1.0",
|
|
@@ -67,5 +67,5 @@ uv run nutria-plugin pack inventory-lookup \
|
|
|
67
67
|
--output inventory-lookup-0.1.0.zip
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
Any schema
|
|
70
|
+
Any older-schema field, unknown property, missing provider, or incomplete
|
|
71
71
|
authority/exposure contract fails validation and must be corrected explicitly.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Reviewable actions and external writes
|
|
2
2
|
|
|
3
|
-
SDK 0.
|
|
3
|
+
SDK 0.4.0 schema 4.0 defines one reviewable-action architecture:
|
|
4
4
|
|
|
5
5
|
```text
|
|
6
6
|
ChatBotNutralia -> task context, encrypted draft, revision, approval, idempotency, receipts
|
|
7
|
-
Plugin -> provider reads,
|
|
7
|
+
Plugin -> provider reads, preparation capability, exact approved delivery
|
|
8
8
|
SDK -> strict capability/provider/reviewable-action contracts
|
|
9
9
|
```
|
|
10
10
|
|
|
@@ -68,12 +68,13 @@ Every delivery maps `idempotency_key`. Required execution inputs must appear in
|
|
|
68
68
|
the action map. Recipient, source/thread/order identities, channel, and mode are
|
|
69
69
|
immutable; only explicitly editable fields can change.
|
|
70
70
|
|
|
71
|
-
##
|
|
71
|
+
## Preparation capability
|
|
72
72
|
|
|
73
|
-
An optional reply-envelope preparation capability
|
|
74
|
-
It
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
An optional reply-envelope preparation capability is identified structurally by
|
|
74
|
+
producing `prepared_action`. It uses `read` when it only resolves authoritative
|
|
75
|
+
recipient/thread/source fingerprints, or `write` when it mutates local durable
|
|
76
|
+
state. The action's `prepare_capability` and `execute_capability` must be
|
|
77
|
+
distinct and use the same connection.
|
|
77
78
|
|
|
78
79
|
## Host lifecycle
|
|
79
80
|
|
|
@@ -13,7 +13,6 @@ _RESOURCE_TYPE_PATTERN = r"^[a-z][a-z0-9_.-]{0,127}$"
|
|
|
13
13
|
|
|
14
14
|
class CapabilityEffect(str, Enum):
|
|
15
15
|
READ = "read"
|
|
16
|
-
PREPARE = "prepare"
|
|
17
16
|
WRITE = "write"
|
|
18
17
|
EXTERNAL_WRITE = "external_write"
|
|
19
18
|
|
|
@@ -120,7 +119,7 @@ class CapabilityRequirement(BaseModel):
|
|
|
120
119
|
|
|
121
120
|
authority: Literal["read", "write_internal", "write_external"]
|
|
122
121
|
audience: list[str] = Field(..., min_length=1)
|
|
123
|
-
task_context: Literal["optional", "required"]
|
|
122
|
+
task_context: Literal["optional", "required", "forbidden"] = "optional"
|
|
124
123
|
|
|
125
124
|
model_config = {"extra": "forbid"}
|
|
126
125
|
|
|
@@ -312,19 +311,6 @@ class CapabilityDescriptor(BaseModel):
|
|
|
312
311
|
output_names = [item.output_name for item in self.produces]
|
|
313
312
|
if len(output_names) != len(set(output_names)):
|
|
314
313
|
raise ValueError("capability outputs must not repeat output names")
|
|
315
|
-
task_owned_types = {
|
|
316
|
-
ResourceType.TASK.value,
|
|
317
|
-
ResourceType.ARTIFACT.value,
|
|
318
|
-
ResourceType.PREPARED_ACTION.value,
|
|
319
|
-
}
|
|
320
|
-
bound_types = {
|
|
321
|
-
item.resource_type.value
|
|
322
|
-
if isinstance(item.resource_type, ResourceType)
|
|
323
|
-
else str(item.resource_type)
|
|
324
|
-
for item in (*self.consumes, *self.produces)
|
|
325
|
-
}
|
|
326
|
-
if bound_types & task_owned_types and self.requirements.task_context != "required":
|
|
327
|
-
raise ValueError("task-owned resources require task_context=required")
|
|
328
314
|
if self.effect == CapabilityEffect.EXTERNAL_WRITE and self.exposure == CapabilityExposure.MODEL:
|
|
329
315
|
missing = [
|
|
330
316
|
name
|
|
@@ -315,7 +315,7 @@ class PluginAdminFlow(BaseModel):
|
|
|
315
315
|
class PluginManifest(BaseModel):
|
|
316
316
|
"""Manifest stored in plugin.json — the single source of truth for plugin metadata."""
|
|
317
317
|
|
|
318
|
-
schema_version: Literal["
|
|
318
|
+
schema_version: Literal["4.0"]
|
|
319
319
|
id: str = Field(..., pattern=r"^[a-z][a-z0-9\-]*$", max_length=64)
|
|
320
320
|
name: str = Field(..., min_length=1, max_length=128)
|
|
321
321
|
version: str = Field(..., min_length=5, max_length=64)
|
|
@@ -391,6 +391,11 @@ class PluginManifest(BaseModel):
|
|
|
391
391
|
for provider in self.world_providers
|
|
392
392
|
if provider.connection_id
|
|
393
393
|
}
|
|
394
|
+
host_owned_resource_types = {
|
|
395
|
+
ResourceType.TASK.value,
|
|
396
|
+
ResourceType.ARTIFACT.value,
|
|
397
|
+
ResourceType.PREPARED_ACTION.value,
|
|
398
|
+
}
|
|
394
399
|
for capability in self.capabilities:
|
|
395
400
|
if not capability.connection_id:
|
|
396
401
|
continue
|
|
@@ -400,7 +405,7 @@ class PluginManifest(BaseModel):
|
|
|
400
405
|
if isinstance(output.resource_type, ResourceType)
|
|
401
406
|
else str(output.resource_type)
|
|
402
407
|
for output in capability.produces
|
|
403
|
-
} - available_types
|
|
408
|
+
} - available_types - host_owned_resource_types
|
|
404
409
|
if undeclared:
|
|
405
410
|
raise ValueError(
|
|
406
411
|
f"capability {capability.id!r} produces resource types not declared by "
|
|
@@ -475,8 +480,16 @@ class PluginManifest(BaseModel):
|
|
|
475
480
|
f"reviewable action {action.id!r} references unknown preparation capability "
|
|
476
481
|
f"{action.prepare_capability!r}"
|
|
477
482
|
)
|
|
478
|
-
|
|
479
|
-
|
|
483
|
+
prepared_outputs = {
|
|
484
|
+
output.resource_type.value
|
|
485
|
+
if isinstance(output.resource_type, ResourceType)
|
|
486
|
+
else str(output.resource_type)
|
|
487
|
+
for output in prepare.produces
|
|
488
|
+
}
|
|
489
|
+
if ResourceType.PREPARED_ACTION.value not in prepared_outputs:
|
|
490
|
+
raise ValueError(
|
|
491
|
+
"reviewable preparation capability must produce prepared_action"
|
|
492
|
+
)
|
|
480
493
|
if prepare.connection_id != action.connection_id:
|
|
481
494
|
raise ValueError(
|
|
482
495
|
"reviewable preparation capability connection does not match action"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Strict world-graph capability contracts
|
|
1
|
+
"""Strict world-graph capability contracts for SDK schema 4.0."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -130,21 +130,62 @@ def test_host_external_write_remains_valid_without_prepared_contract():
|
|
|
130
130
|
assert descriptor.exposure == CapabilityExposure.HOST
|
|
131
131
|
|
|
132
132
|
|
|
133
|
-
def
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
133
|
+
def test_task_context_defaults_to_optional_for_every_capability():
|
|
134
|
+
payload = _capability(
|
|
135
|
+
effect="read",
|
|
136
|
+
produces=[
|
|
137
|
+
{
|
|
138
|
+
"result_path": ".shipment",
|
|
139
|
+
"resource_type": "mrw.shipment",
|
|
140
|
+
"output_name": "shipment",
|
|
141
|
+
}
|
|
142
|
+
],
|
|
143
|
+
)
|
|
144
|
+
payload["requirements"].pop("task_context")
|
|
145
|
+
|
|
146
|
+
descriptor = CapabilityDescriptor.model_validate(payload)
|
|
147
|
+
|
|
148
|
+
assert descriptor.requirements.task_context == "optional"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_task_context_accepts_forbidden():
|
|
152
|
+
descriptor = CapabilityDescriptor.model_validate(
|
|
153
|
+
_capability(
|
|
154
|
+
effect="read",
|
|
155
|
+
requirements={
|
|
156
|
+
"authority": "read",
|
|
157
|
+
"audience": ["private_internal"],
|
|
158
|
+
"task_context": "forbidden",
|
|
159
|
+
},
|
|
160
|
+
produces=[
|
|
161
|
+
{
|
|
162
|
+
"result_path": ".shipment",
|
|
163
|
+
"resource_type": "mrw.shipment",
|
|
164
|
+
"output_name": "shipment",
|
|
165
|
+
}
|
|
166
|
+
],
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
assert descriptor.requirements.task_context == "forbidden"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_sdk_leaves_task_owned_resource_promotion_to_registry():
|
|
174
|
+
descriptor = CapabilityDescriptor.model_validate(
|
|
175
|
+
_capability(
|
|
176
|
+
effect="read",
|
|
177
|
+
consumes=[{"name": "action", "resource_type": "prepared_action"}],
|
|
178
|
+
produces=[
|
|
179
|
+
{
|
|
180
|
+
"result_path": ".shipment",
|
|
181
|
+
"resource_type": "mrw.shipment",
|
|
182
|
+
"output_name": "shipment",
|
|
183
|
+
}
|
|
184
|
+
],
|
|
147
185
|
)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
assert descriptor.requirements.task_context == "optional"
|
|
148
189
|
|
|
149
190
|
|
|
150
191
|
def test_model_read_requires_typed_output():
|
|
@@ -18,7 +18,7 @@ from nutria_plugin.manifest import (
|
|
|
18
18
|
|
|
19
19
|
def _minimal_manifest(**overrides) -> dict:
|
|
20
20
|
base = {
|
|
21
|
-
"schema_version": "
|
|
21
|
+
"schema_version": "4.0",
|
|
22
22
|
"id": "test-plugin",
|
|
23
23
|
"name": "Test Plugin",
|
|
24
24
|
"version": "1.0.0",
|
|
@@ -118,7 +118,7 @@ def test_other_schema_versions_are_rejected(version):
|
|
|
118
118
|
def test_schema_3_0_world_provider_accepts_custom_resource_type():
|
|
119
119
|
manifest = PluginManifest.model_validate(
|
|
120
120
|
_minimal_manifest(
|
|
121
|
-
schema_version="
|
|
121
|
+
schema_version="4.0",
|
|
122
122
|
capabilities=[
|
|
123
123
|
{
|
|
124
124
|
"id": "workspace.search",
|
|
@@ -166,7 +166,7 @@ def test_schema_3_0_world_provider_accepts_custom_resource_type():
|
|
|
166
166
|
)
|
|
167
167
|
)
|
|
168
168
|
|
|
169
|
-
assert manifest.schema_version == "
|
|
169
|
+
assert manifest.schema_version == "4.0"
|
|
170
170
|
assert manifest.world_providers[0].resource_types[0].id == "workspace.item"
|
|
171
171
|
|
|
172
172
|
|
|
@@ -174,7 +174,7 @@ def test_world_provider_rejects_unknown_capability_reference():
|
|
|
174
174
|
with pytest.raises(ValidationError, match="unknown capability"):
|
|
175
175
|
PluginManifest.model_validate(
|
|
176
176
|
_minimal_manifest(
|
|
177
|
-
schema_version="
|
|
177
|
+
schema_version="4.0",
|
|
178
178
|
world_providers=[
|
|
179
179
|
{
|
|
180
180
|
"id": "workspace",
|
|
@@ -39,9 +39,16 @@ def _capabilities():
|
|
|
39
39
|
"id": "email.reply.prepare",
|
|
40
40
|
"title": "Prepare email reply",
|
|
41
41
|
"description": "Resolve the source email envelope without writing.",
|
|
42
|
-
"effect": "
|
|
42
|
+
"effect": "read",
|
|
43
43
|
"tool": "prepare_email_reply",
|
|
44
44
|
"connection_id": "email",
|
|
45
|
+
"produces": [
|
|
46
|
+
{
|
|
47
|
+
"result_path": ".prepared_action",
|
|
48
|
+
"resource_type": "prepared_action",
|
|
49
|
+
"output_name": "prepared_action",
|
|
50
|
+
}
|
|
51
|
+
],
|
|
45
52
|
"requirements": {
|
|
46
53
|
"authority": "read",
|
|
47
54
|
"audience": ["private_internal", "team_internal"],
|
|
@@ -125,7 +132,7 @@ def _capabilities():
|
|
|
125
132
|
|
|
126
133
|
def _manifest(**overrides):
|
|
127
134
|
data = {
|
|
128
|
-
"schema_version": "
|
|
135
|
+
"schema_version": "4.0",
|
|
129
136
|
"id": "email-plugin",
|
|
130
137
|
"name": "Email",
|
|
131
138
|
"version": "1.0.0",
|
|
@@ -220,8 +227,31 @@ def test_reviewable_external_write_must_be_host_only():
|
|
|
220
227
|
PluginManifest.model_validate(_manifest(capabilities=capabilities))
|
|
221
228
|
|
|
222
229
|
|
|
223
|
-
def
|
|
230
|
+
def test_preparation_capability_is_identified_by_prepared_action_output():
|
|
224
231
|
capabilities = _capabilities()
|
|
225
|
-
capabilities[0]["
|
|
226
|
-
|
|
232
|
+
capabilities[0]["produces"] = [
|
|
233
|
+
{
|
|
234
|
+
"result_path": ".message",
|
|
235
|
+
"resource_type": "email_message",
|
|
236
|
+
"output_name": "message",
|
|
237
|
+
}
|
|
238
|
+
]
|
|
239
|
+
with pytest.raises(ValidationError, match="must produce prepared_action"):
|
|
227
240
|
PluginManifest.model_validate(_manifest(capabilities=capabilities))
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def test_preparation_capability_may_use_its_actual_write_effect():
|
|
244
|
+
capabilities = _capabilities()
|
|
245
|
+
capabilities[0]["effect"] = "write"
|
|
246
|
+
capabilities[0]["inputs"] = [
|
|
247
|
+
{
|
|
248
|
+
"kind": "value",
|
|
249
|
+
"semantic_field": "body",
|
|
250
|
+
"argument_name": "body",
|
|
251
|
+
"accepted_origins": ["current_user"],
|
|
252
|
+
}
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
manifest = PluginManifest.model_validate(_manifest(capabilities=capabilities))
|
|
256
|
+
|
|
257
|
+
assert manifest.capabilities[0].effect.value == "write"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|