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.
Files changed (38) hide show
  1. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/CHANGELOG.md +10 -0
  2. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/PKG-INFO +15 -14
  3. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/README.md +14 -13
  4. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/docs/index.md +3 -3
  5. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/docs/manifest.md +13 -11
  6. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/docs/python-api.md +2 -2
  7. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/docs/quickstart.md +4 -4
  8. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/docs/reviewable-actions.md +8 -7
  9. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/pyproject.toml +1 -1
  10. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/src/nutria_plugin/__init__.py +1 -1
  11. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/src/nutria_plugin/capabilities.py +1 -15
  12. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/src/nutria_plugin/manifest.py +17 -4
  13. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/src/nutria_plugin/packaging.py +1 -1
  14. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/tests/test_bundle.py +1 -1
  15. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/tests/test_capability_contracts.py +56 -15
  16. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/tests/test_manifest.py +4 -4
  17. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/tests/test_reviewable_actions.py +35 -5
  18. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/tests/test_signing.py +1 -1
  19. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/uv.lock +1 -1
  20. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/.github/workflows/publish.yml +0 -0
  21. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/.gitignore +0 -0
  22. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/docs/admin-extensions.md +0 -0
  23. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/docs/admin-flows.md +0 -0
  24. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/docs/cli.md +0 -0
  25. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/docs/connection-types.md +0 -0
  26. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/docs/security.md +0 -0
  27. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/docs/skill-format.md +0 -0
  28. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/examples/my-first-plugin/README.md +0 -0
  29. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/examples/my-first-plugin/hooks/hooks.json +0 -0
  30. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/examples/my-first-plugin/plugin.json +0 -0
  31. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/examples/my-first-plugin/settings.schema.json +0 -0
  32. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/src/nutria_plugin/bundle.py +0 -0
  33. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/src/nutria_plugin/cli.py +0 -0
  34. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/src/nutria_plugin/mcp_results.py +0 -0
  35. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/src/nutria_plugin/signing.py +0 -0
  36. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/tests/test_cli.py +0 -0
  37. {nutria_plugin-0.3.2 → nutria_plugin-0.4.0}/tests/test_mcp_results.py +0 -0
  38. {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.2
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.3.2` uses the intentionally breaking schema introduced in 0.3.0. It accepts only manifest schema
25
- `3.0`, requires typed capabilities and world providers, and makes authority,
26
- audience, task-context requirements, and exposure explicit. There is no runtime
27
- migration for schema 2.0/2.1 manifests and no compatibility/model-callability
28
- field.
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.3.2
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 3.0 manifest
52
+ ## Minimal schema 4.0 manifest
53
53
 
54
54
  ```json
55
55
  {
56
- "schema_version": "3.0",
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`. Host/admin-only
102
- capabilities also require a safe `non_callable_reason`. Task-owned resources
103
- (`task`, `artifact`, `prepared_action`) require `task_context: "required"`.
104
- Every connection referenced by a capability must be represented by a matching
105
- world provider.
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.3.2` uses the intentionally breaking schema introduced in 0.3.0. It accepts only manifest schema
6
- `3.0`, requires typed capabilities and world providers, and makes authority,
7
- audience, task-context requirements, and exposure explicit. There is no runtime
8
- migration for schema 2.0/2.1 manifests and no compatibility/model-callability
9
- field.
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.3.2
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 3.0 manifest
33
+ ## Minimal schema 4.0 manifest
34
34
 
35
35
  ```json
36
36
  {
37
- "schema_version": "3.0",
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`. Host/admin-only
83
- capabilities also require a safe `non_callable_reason`. Task-owned resources
84
- (`task`, `artifact`, `prepared_action`) require `task_context: "required"`.
85
- Every connection referenced by a capability must be represented by a matching
86
- world provider.
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.1` and manifest schema `3.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 3.0 plugin |
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 3.0 manifest must also
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.3.1 accepts exactly schema
5
- `3.0`; older schemas and unknown fields fail validation.
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 `"3.0"` |
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 3.0.
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`, `prepare`, `write`, or `external_write`;
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`, `requirements.audience`, and
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. Task-owned resource bindings
38
- require `task_context: "required"`.
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 use `effect: "prepare"` and
64
- must be pure/read-only. See [reviewable-actions.md](reviewable-actions.md).
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.1 exposes the strict schema 3.0 API.
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 `3.0`, top-level extras are
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 3.0 plugin
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.3.1
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": "3.0",
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 2.0/2.1 field, unknown property, missing provider, or incomplete
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.1 schema 3.0 defines one reviewable-action architecture:
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, pure preparation, exact approved delivery
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
- ## Pure preparation
71
+ ## Preparation capability
72
72
 
73
- An optional reply-envelope preparation capability uses `effect: "prepare"`.
74
- It may resolve authoritative recipient/thread/source fingerprints but cannot
75
- persist a draft or write externally. The action's `prepare_capability` and
76
- `execute_capability` must be distinct and use the same connection.
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
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nutria-plugin"
7
- version = "0.3.2"
7
+ version = "0.4.0"
8
8
  description = "SDK for building, validating, signing, and packaging Nutria plugins"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -15,7 +15,7 @@ Signing:
15
15
  generate_keypair, sign_manifest, verify_manifest, SignatureStatus
16
16
  """
17
17
 
18
- __version__ = "0.3.2"
18
+ __version__ = "0.4.0"
19
19
 
20
20
  from .capabilities import (
21
21
  CapabilityDescriptor,
@@ -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["3.0"]
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
- if prepare.effect != CapabilityEffect.PREPARE:
479
- raise ValueError("reviewable preparation capability must use effect=prepare")
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"
@@ -25,7 +25,7 @@ class PackagingError(Exception):
25
25
  SCAFFOLD_TEMPLATE = {
26
26
  "plugin.json": lambda plugin_id, name: json.dumps(
27
27
  {
28
- "schema_version": "3.0",
28
+ "schema_version": "4.0",
29
29
  "id": plugin_id,
30
30
  "name": name,
31
31
  "version": "0.1.0",
@@ -28,7 +28,7 @@ def _make_zip(files: dict[str, str]) -> bytes:
28
28
 
29
29
  VALID_MANIFEST = json.dumps(
30
30
  {
31
- "schema_version": "3.0",
31
+ "schema_version": "4.0",
32
32
  "id": "test-plugin",
33
33
  "name": "Test",
34
34
  "version": "1.0.0",
@@ -1,4 +1,4 @@
1
- """Strict world-graph capability contracts introduced in SDK 0.3.0."""
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 test_task_owned_resources_require_task_context():
134
- with pytest.raises(ValidationError, match="task_context=required"):
135
- CapabilityDescriptor.model_validate(
136
- _capability(
137
- effect="read",
138
- consumes=[{"name": "action", "resource_type": "prepared_action"}],
139
- produces=[
140
- {
141
- "result_path": ".shipment",
142
- "resource_type": "mrw.shipment",
143
- "output_name": "shipment",
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": "3.0",
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="3.0",
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 == "3.0"
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="3.0",
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": "prepare",
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": "3.0",
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 test_preparation_capability_must_be_pure_prepare():
230
+ def test_preparation_capability_is_identified_by_prepared_action_output():
224
231
  capabilities = _capabilities()
225
- capabilities[0]["effect"] = "write"
226
- with pytest.raises(ValidationError):
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"
@@ -17,7 +17,7 @@ from nutria_plugin.signing import (
17
17
 
18
18
  def _minimal_dict(**overrides) -> dict:
19
19
  base = {
20
- "schema_version": "3.0",
20
+ "schema_version": "4.0",
21
21
  "id": "test-plugin",
22
22
  "name": "Test",
23
23
  "version": "1.0.0",
@@ -262,7 +262,7 @@ wheels = [
262
262
 
263
263
  [[package]]
264
264
  name = "nutria-plugin"
265
- version = "0.3.2"
265
+ version = "0.4.0"
266
266
  source = { editable = "." }
267
267
  dependencies = [
268
268
  { name = "cryptography" },
File without changes
File without changes