nutria-plugin 0.2.0__tar.gz → 0.2.1__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 (35) hide show
  1. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/CHANGELOG.md +7 -0
  2. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/PKG-INFO +18 -5
  3. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/README.md +17 -4
  4. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/docs/index.md +3 -2
  5. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/docs/manifest.md +30 -2
  6. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/pyproject.toml +1 -1
  7. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/src/nutria_plugin/__init__.py +7 -1
  8. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/src/nutria_plugin/capabilities.py +106 -3
  9. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/src/nutria_plugin/manifest.py +23 -2
  10. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/src/nutria_plugin/packaging.py +1 -1
  11. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/tests/test_manifest.py +75 -1
  12. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/uv.lock +1 -1
  13. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/.github/workflows/publish.yml +0 -0
  14. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/.gitignore +0 -0
  15. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/docs/admin-extensions.md +0 -0
  16. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/docs/admin-flows.md +0 -0
  17. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/docs/cli.md +0 -0
  18. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/docs/connection-types.md +0 -0
  19. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/docs/python-api.md +0 -0
  20. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/docs/quickstart.md +0 -0
  21. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/docs/reviewable-actions.md +0 -0
  22. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/docs/security.md +0 -0
  23. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/docs/skill-format.md +0 -0
  24. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/examples/my-first-plugin/README.md +0 -0
  25. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/examples/my-first-plugin/hooks/hooks.json +0 -0
  26. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/examples/my-first-plugin/plugin.json +0 -0
  27. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/examples/my-first-plugin/settings.schema.json +0 -0
  28. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/src/nutria_plugin/bundle.py +0 -0
  29. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/src/nutria_plugin/cli.py +0 -0
  30. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/src/nutria_plugin/signing.py +0 -0
  31. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/tests/test_bundle.py +0 -0
  32. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/tests/test_cli.py +0 -0
  33. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/tests/test_packaging.py +0 -0
  34. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/tests/test_reviewable_actions.py +0 -0
  35. {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/tests/test_signing.py +0 -0
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.1
4
+
5
+ - Add manifest schema `2.1` with typed world providers, custom resource types,
6
+ stable identity fields, safe projections, and provider capability references.
7
+ - Keep schema `2.0` compatible and export the new provider descriptor models.
8
+ - Scaffold new plugins with schema `2.1` while preserving the 0.3.0 roadmap.
9
+
3
10
  ## 0.2.0
4
11
 
5
12
  - Make manifest schema `2.0` the single production schema.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nutria-plugin
3
- Version: 0.2.0
3
+ Version: 0.2.1
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,8 +21,8 @@ Description-Content-Type: text/markdown
21
21
 
22
22
  SDK for building, validating, signing, and packaging Nutria plugins.
23
23
 
24
- Release `0.2.0` introduces the typed capability graph and manifest schema
25
- `2.0`. ChatBotNutralia owns reviewable drafts, revisions, and approval; a
24
+ Release `0.2.1` adds provider-backed world graph descriptors in manifest schema
25
+ `2.1` while continuing to accept schema `2.0`. ChatBotNutralia owns reviewable drafts, revisions, and approval; a
26
26
  plugin only reads channel state and delivers the exact approved snapshot.
27
27
 
28
28
  This release also supports **declarative admin extensions**, allowing plugins
@@ -62,7 +62,7 @@ my-workspace-plugin/
62
62
 
63
63
  ```json
64
64
  {
65
- "schema_version": "2.0",
65
+ "schema_version": "2.1",
66
66
  "id": "my-workspace-plugin",
67
67
  "name": "My Workspace Plugin",
68
68
  "version": "0.1.0",
@@ -81,7 +81,20 @@ my-workspace-plugin/
81
81
  "tool": "search_workspace",
82
82
  "connection_id": "workspace"
83
83
  }
84
- ]
84
+ ],
85
+ "world_providers": [{
86
+ "id": "workspace",
87
+ "title": "Workspace",
88
+ "description": "Authoritative workspace resources.",
89
+ "connection_id": "workspace",
90
+ "resource_types": [{
91
+ "id": "workspace.item",
92
+ "title": "Workspace item",
93
+ "description": "One stable workspace item.",
94
+ "identity_fields": ["id"],
95
+ "search_capability": "workspace.search"
96
+ }]
97
+ }]
85
98
  }
86
99
  ```
87
100
 
@@ -2,8 +2,8 @@
2
2
 
3
3
  SDK for building, validating, signing, and packaging Nutria plugins.
4
4
 
5
- Release `0.2.0` introduces the typed capability graph and manifest schema
6
- `2.0`. ChatBotNutralia owns reviewable drafts, revisions, and approval; a
5
+ Release `0.2.1` adds provider-backed world graph descriptors in manifest schema
6
+ `2.1` while continuing to accept schema `2.0`. ChatBotNutralia owns reviewable drafts, revisions, and approval; a
7
7
  plugin only reads channel state and delivers the exact approved snapshot.
8
8
 
9
9
  This release also supports **declarative admin extensions**, allowing plugins
@@ -43,7 +43,7 @@ my-workspace-plugin/
43
43
 
44
44
  ```json
45
45
  {
46
- "schema_version": "2.0",
46
+ "schema_version": "2.1",
47
47
  "id": "my-workspace-plugin",
48
48
  "name": "My Workspace Plugin",
49
49
  "version": "0.1.0",
@@ -62,7 +62,20 @@ my-workspace-plugin/
62
62
  "tool": "search_workspace",
63
63
  "connection_id": "workspace"
64
64
  }
65
- ]
65
+ ],
66
+ "world_providers": [{
67
+ "id": "workspace",
68
+ "title": "Workspace",
69
+ "description": "Authoritative workspace resources.",
70
+ "connection_id": "workspace",
71
+ "resource_types": [{
72
+ "id": "workspace.item",
73
+ "title": "Workspace item",
74
+ "description": "One stable workspace item.",
75
+ "identity_fields": ["id"],
76
+ "search_capability": "workspace.search"
77
+ }]
78
+ }]
66
79
  }
67
80
  ```
68
81
 
@@ -31,6 +31,7 @@ Plugins define:
31
31
  - **Settings schema** — admin-configurable options shown in the Nutria UI
32
32
  - **Admin extensions** — safe, declarative operator views rendered by the Nutria host
33
33
  - **Admin flows** — safe, declarative operator workflows such as external login/pairing
34
+ - **World providers** — resource types and capability paths injected into the host graph
34
35
 
35
36
  ## Minimal plugin structure
36
37
 
@@ -53,7 +54,7 @@ my-plugin/
53
54
 
54
55
  ## Version
55
56
 
56
- This documentation describes `nutria-plugin` **v0.2.0** and manifest schema
57
- **2.0**.
57
+ This documentation describes `nutria-plugin` **v0.2.1** and manifest schema
58
+ **2.1**. Schema 2.0 remains accepted for plugins that do not declare world providers.
58
59
 
59
60
  API and file format compatibility are not yet guaranteed between pre-release releases.
@@ -8,7 +8,7 @@ directory as `plugin.json`.
8
8
 
9
9
  ```json
10
10
  {
11
- "schema_version": "2.0",
11
+ "schema_version": "2.1",
12
12
  "id": "my-plugin",
13
13
  "name": "My Plugin",
14
14
  "version": "0.1.0",
@@ -42,6 +42,19 @@ directory as `plugin.json`.
42
42
  "connection_id": "workspace"
43
43
  }
44
44
  ],
45
+ "world_providers": [{
46
+ "id": "workspace",
47
+ "title": "Workspace",
48
+ "description": "Authoritative workspace resources.",
49
+ "connection_id": "workspace",
50
+ "resource_types": [{
51
+ "id": "workspace.item",
52
+ "title": "Workspace item",
53
+ "description": "One stable workspace item.",
54
+ "identity_fields": ["id"],
55
+ "search_capability": "workspace.search"
56
+ }]
57
+ }],
45
58
  "tags": ["crm", "sales"],
46
59
  "admin_extensions": [
47
60
  {
@@ -73,7 +86,8 @@ directory as `plugin.json`.
73
86
 
74
87
  ### `schema_version` *(string, required)*
75
88
 
76
- The manifest schema is `"2.0"`. Every plugin must declare this version.
89
+ The current manifest schema is `"2.1"`; schema `"2.0"` remains accepted for
90
+ backward compatibility. Schema 2.1 adds provider-backed world graph contracts.
77
91
  Capabilities and reviewable actions are typed by the SDK; schema `1.x` is not
78
92
  accepted by the production host.
79
93
 
@@ -285,6 +299,20 @@ See [reviewable-actions.md](reviewable-actions.md) for the complete schema.
285
299
 
286
300
  ---
287
301
 
302
+ ### `world_providers` *(array of objects, optional; schema 2.1)*
303
+
304
+ Each provider declares a stable `id`, title, description, optional
305
+ `connection_id` and `health_capability`, plus one or more resource types. A
306
+ resource type has a stable built-in or plugin-namespaced custom `id`, at least
307
+ one `identity_fields` entry, optional `search_capability` and
308
+ `inspect_capability`, bounded `ttl_seconds`, and named safe `projections`.
309
+
310
+ Referenced capabilities must exist in the same manifest. Provider declarations
311
+ describe navigation only: the host still enforces audience, persona assignment,
312
+ connection state, and effect authorization before executing a tool.
313
+
314
+ ---
315
+
288
316
  ### `tags` *(array of strings, optional)*
289
317
 
290
318
  Discovery tags shown in the plugin marketplace.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nutria-plugin"
7
- version = "0.2.0"
7
+ version = "0.2.1"
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.2.0"
18
+ __version__ = "0.2.1"
19
19
 
20
20
  from .capabilities import (
21
21
  CapabilityDescriptor,
@@ -25,6 +25,9 @@ from .capabilities import (
25
25
  CapabilityRequirement,
26
26
  ResourceBinding,
27
27
  ResourceType,
28
+ ResourceTypeDescriptor,
29
+ WorldProjectionDescriptor,
30
+ WorldProviderDescriptor,
28
31
  )
29
32
 
30
33
 
@@ -74,6 +77,9 @@ __all__ = [
74
77
  "CapabilityRequirement",
75
78
  "ResourceBinding",
76
79
  "ResourceType",
80
+ "ResourceTypeDescriptor",
81
+ "WorldProjectionDescriptor",
82
+ "WorldProviderDescriptor",
77
83
  # Bundle
78
84
  "PluginBundleError",
79
85
  "load_plugin_bundle",
@@ -8,6 +8,9 @@ from typing import Any
8
8
  from pydantic import BaseModel, Field, field_validator, model_validator
9
9
 
10
10
 
11
+ _RESOURCE_TYPE_PATTERN = r"^[a-z][a-z0-9_.-]{0,127}$"
12
+
13
+
11
14
  class CapabilityEffect(str, Enum):
12
15
  READ = "read"
13
16
  PREPARE = "prepare"
@@ -44,6 +47,14 @@ class ResourceType(str, Enum):
44
47
  WHATSAPP_CHAT = "whatsapp_chat"
45
48
  WHATSAPP_MESSAGE = "whatsapp_message"
46
49
  WHATSAPP_ATTACHMENT = "whatsapp_attachment"
50
+ PRODUCT = "product"
51
+ FILE = "file"
52
+ TASK = "task"
53
+ TRELLO_BOARD = "trello_board"
54
+ TRELLO_CARD = "trello_card"
55
+ TRELLO_LABEL = "trello_label"
56
+ MRW_SHIPMENT = "mrw_shipment"
57
+ MRW_LABEL = "mrw_label"
47
58
 
48
59
 
49
60
  class CapabilityRequirement(BaseModel):
@@ -65,34 +76,126 @@ class ResourceBinding(BaseModel):
65
76
  """A resource consumed or produced by a capability."""
66
77
 
67
78
  name: str = Field(..., pattern=r"^[a-z][a-z0-9_.-]{0,127}$")
68
- resource_type: ResourceType
79
+ resource_type: ResourceType | str
69
80
  required: bool = True
70
81
  many: bool = False
71
82
 
72
83
  model_config = {"extra": "forbid"}
73
84
 
85
+ @field_validator("resource_type")
86
+ @classmethod
87
+ def _validate_resource_type(cls, value: ResourceType | str) -> ResourceType | str:
88
+ _validate_resource_type_id(value)
89
+ return value
90
+
74
91
 
75
92
  class CapabilityInputBinding(BaseModel):
76
93
  """Maps a semantic input to one plugin tool argument."""
77
94
 
78
95
  semantic_field: str = Field(..., pattern=r"^[a-z][a-z0-9_.-]{0,127}$")
79
96
  argument_name: str = Field(..., pattern=r"^[a-zA-Z][a-zA-Z0-9_-]{0,127}$")
80
- resource_type: ResourceType | None = None
97
+ resource_type: ResourceType | str | None = None
81
98
  required: bool = True
82
99
 
83
100
  model_config = {"extra": "forbid"}
84
101
 
102
+ @field_validator("resource_type")
103
+ @classmethod
104
+ def _validate_resource_type(cls, value: ResourceType | str | None) -> ResourceType | str | None:
105
+ if value is not None:
106
+ _validate_resource_type_id(value)
107
+ return value
108
+
85
109
 
86
110
  class CapabilityOutputBinding(BaseModel):
87
111
  """Maps one plugin result field to a typed resource output."""
88
112
 
89
113
  field_name: str = Field(..., pattern=r"^[a-zA-Z][a-zA-Z0-9_.-]{0,127}$")
90
- resource_type: ResourceType
114
+ resource_type: ResourceType | str
91
115
  output_name: str = Field(..., pattern=r"^[a-z][a-z0-9_.-]{0,127}$")
92
116
  many: bool = False
93
117
 
94
118
  model_config = {"extra": "forbid"}
95
119
 
120
+ @field_validator("resource_type")
121
+ @classmethod
122
+ def _validate_resource_type(cls, value: ResourceType | str) -> ResourceType | str:
123
+ _validate_resource_type_id(value)
124
+ return value
125
+
126
+
127
+ def _validate_resource_type_id(value: ResourceType | str) -> None:
128
+ import re
129
+
130
+ raw = value.value if isinstance(value, ResourceType) else str(value)
131
+ if not re.fullmatch(_RESOURCE_TYPE_PATTERN, raw):
132
+ raise ValueError("resource type must be a stable lowercase identifier")
133
+
134
+
135
+ class WorldProjectionDescriptor(BaseModel):
136
+ """Named provider projection; payload fields remain provider-owned."""
137
+
138
+ id: str = Field(..., pattern=r"^[a-z][a-z0-9_.-]{0,63}$")
139
+ title: str = Field(..., min_length=1, max_length=160)
140
+ description: str = Field(default="", max_length=1_000)
141
+ fields: list[str] = Field(default_factory=list, max_length=64)
142
+ sensitivity: str = Field(default="safe", pattern=r"^(safe|authorized)$")
143
+
144
+ model_config = {"extra": "forbid"}
145
+
146
+
147
+ class ResourceTypeDescriptor(BaseModel):
148
+ """How one provider exposes a stable resource namespace to the world."""
149
+
150
+ id: ResourceType | str
151
+ title: str = Field(..., min_length=1, max_length=160)
152
+ description: str = Field(..., min_length=1, max_length=2_000)
153
+ identity_fields: list[str] = Field(default_factory=list, min_length=1, max_length=16)
154
+ version_field: str | None = Field(default=None, max_length=128)
155
+ search_capability: str | None = Field(default=None, pattern=r"^[a-z][a-z0-9_.-]{0,127}$")
156
+ inspect_capability: str | None = Field(default=None, pattern=r"^[a-z][a-z0-9_.-]{0,127}$")
157
+ ttl_seconds: int = Field(default=300, ge=0, le=86_400)
158
+ projections: list[WorldProjectionDescriptor] = Field(default_factory=list)
159
+
160
+ model_config = {"extra": "forbid"}
161
+
162
+ @field_validator("id")
163
+ @classmethod
164
+ def _validate_id(cls, value: ResourceType | str) -> ResourceType | str:
165
+ _validate_resource_type_id(value)
166
+ return value
167
+
168
+ @model_validator(mode="after")
169
+ def _validate_projections(self) -> "ResourceTypeDescriptor":
170
+ ids = [item.id for item in self.projections]
171
+ if len(ids) != len(set(ids)):
172
+ raise ValueError("resource projections must not repeat IDs")
173
+ return self
174
+
175
+
176
+ class WorldProviderDescriptor(BaseModel):
177
+ """Static contract projected into the host-owned agent world graph."""
178
+
179
+ id: str = Field(..., pattern=r"^[a-z][a-z0-9_.-]{0,127}$")
180
+ title: str = Field(..., min_length=1, max_length=160)
181
+ description: str = Field(..., min_length=1, max_length=2_000)
182
+ connection_id: str | None = Field(
183
+ default=None, pattern=r"^[a-zA-Z][a-zA-Z0-9_-]{0,127}$"
184
+ )
185
+ health_capability: str | None = Field(
186
+ default=None, pattern=r"^[a-z][a-z0-9_.-]{0,127}$"
187
+ )
188
+ resource_types: list[ResourceTypeDescriptor] = Field(default_factory=list, min_length=1)
189
+
190
+ model_config = {"extra": "forbid"}
191
+
192
+ @model_validator(mode="after")
193
+ def _validate_resources(self) -> "WorldProviderDescriptor":
194
+ ids = [item.id.value if isinstance(item.id, ResourceType) else str(item.id) for item in self.resource_types]
195
+ if len(ids) != len(set(ids)):
196
+ raise ValueError("world provider resource types must not repeat IDs")
197
+ return self
198
+
96
199
 
97
200
  class CapabilityDescriptor(BaseModel):
98
201
  """Declarative capability contract shared by SDK, host, and plugins."""
@@ -17,7 +17,7 @@ from typing import Any, Dict, List, Optional
17
17
 
18
18
  from pydantic import BaseModel, Field, field_validator, model_validator
19
19
 
20
- from .capabilities import CapabilityDescriptor, CapabilityEffect
20
+ from .capabilities import CapabilityDescriptor, CapabilityEffect, ResourceType, WorldProviderDescriptor
21
21
 
22
22
  _SEMVER_RE = re.compile(
23
23
  r"^(0|[1-9]\d*)\."
@@ -325,7 +325,7 @@ class PluginAdminFlow(BaseModel):
325
325
  class PluginManifest(BaseModel):
326
326
  """Manifest stored in plugin.json — the single source of truth for plugin metadata."""
327
327
 
328
- schema_version: str = Field(..., pattern=r"^2\.0$")
328
+ schema_version: str = Field(..., pattern=r"^2\.(0|1)$")
329
329
  id: str = Field(..., pattern=r"^[a-z][a-z0-9\-]*$", max_length=64)
330
330
  name: str = Field(..., min_length=1, max_length=128)
331
331
  version: str = Field(..., min_length=5, max_length=64)
@@ -339,6 +339,7 @@ class PluginManifest(BaseModel):
339
339
  optional_secrets: List[str] = Field(default_factory=list)
340
340
  remote_endpoints: List[str] = Field(default_factory=list)
341
341
  capabilities: List[CapabilityDescriptor] = Field(default_factory=list)
342
+ world_providers: List[WorldProviderDescriptor] = Field(default_factory=list)
342
343
  tags: List[str] = Field(default_factory=list)
343
344
  reviewable_actions: List[ReviewableActionContract] = Field(default_factory=list)
344
345
  admin_extensions: List[PluginAdminExtension] = Field(default_factory=list)
@@ -376,6 +377,26 @@ class PluginManifest(BaseModel):
376
377
  if len(action_ids_list) != len(set(action_ids_list)):
377
378
  raise ValueError("reviewable_actions must not contain duplicate contract IDs")
378
379
  capability_map = {capability.id: capability for capability in self.capabilities}
380
+ provider_ids = [provider.id for provider in self.world_providers]
381
+ if len(provider_ids) != len(set(provider_ids)):
382
+ raise ValueError("world_providers must not contain duplicate IDs")
383
+ for provider in self.world_providers:
384
+ for resource in provider.resource_types:
385
+ resource_id = resource.id.value if isinstance(resource.id, ResourceType) else str(resource.id)
386
+ for field_name, capability_id in (
387
+ ("search_capability", resource.search_capability),
388
+ ("inspect_capability", resource.inspect_capability),
389
+ ):
390
+ if capability_id and capability_id not in capability_map:
391
+ raise ValueError(
392
+ f"world provider resource {resource_id!r} {field_name} references "
393
+ f"unknown capability {capability_id!r}"
394
+ )
395
+ if provider.health_capability and provider.health_capability not in capability_map:
396
+ raise ValueError(
397
+ f"world provider {provider.id!r} references unknown health capability "
398
+ f"{provider.health_capability!r}"
399
+ )
379
400
  action_ids = set(action_ids_list)
380
401
  for action in self.reviewable_actions:
381
402
  execute = capability_map.get(action.execute_capability)
@@ -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": "2.0",
28
+ "schema_version": "2.1",
29
29
  "id": plugin_id,
30
30
  "name": name,
31
31
  "version": "0.1.0",
@@ -64,7 +64,7 @@ def test_version_must_be_semver():
64
64
  PluginManifest.model_validate(_minimal_manifest(version="1.0"))
65
65
 
66
66
 
67
- def test_schema_version_must_be_2_0():
67
+ def test_schema_version_must_be_2_x():
68
68
  with pytest.raises(ValidationError):
69
69
  PluginManifest.model_validate(_minimal_manifest(schema_version="1.1"))
70
70
 
@@ -74,6 +74,80 @@ def test_schema_version_1_x_is_rejected():
74
74
  PluginManifest.model_validate(_minimal_manifest(schema_version="1.0"))
75
75
 
76
76
 
77
+ def test_schema_2_1_world_provider_accepts_custom_resource_type():
78
+ manifest = PluginManifest.model_validate(
79
+ _minimal_manifest(
80
+ schema_version="2.1",
81
+ capabilities=[
82
+ {
83
+ "id": "workspace.search",
84
+ "title": "Search workspace",
85
+ "description": "Find authoritative workspace items.",
86
+ "effect": "read",
87
+ "tool": "search_workspace",
88
+ "connection_id": "workspace",
89
+ "produces": [
90
+ {
91
+ "field_name": "items",
92
+ "resource_type": "workspace.item",
93
+ "output_name": "items",
94
+ "many": True,
95
+ }
96
+ ],
97
+ }
98
+ ],
99
+ world_providers=[
100
+ {
101
+ "id": "workspace",
102
+ "title": "Workspace",
103
+ "description": "Authoritative workspace provider.",
104
+ "connection_id": "workspace",
105
+ "resource_types": [
106
+ {
107
+ "id": "workspace.item",
108
+ "title": "Workspace item",
109
+ "description": "A stable workspace item.",
110
+ "identity_fields": ["id"],
111
+ "search_capability": "workspace.search",
112
+ "projections": [
113
+ {"id": "admin", "title": "Admin", "fields": ["id"]}
114
+ ],
115
+ }
116
+ ],
117
+ }
118
+ ],
119
+ )
120
+ )
121
+
122
+ assert manifest.schema_version == "2.1"
123
+ assert manifest.world_providers[0].resource_types[0].id == "workspace.item"
124
+
125
+
126
+ def test_world_provider_rejects_unknown_capability_reference():
127
+ with pytest.raises(ValidationError, match="unknown capability"):
128
+ PluginManifest.model_validate(
129
+ _minimal_manifest(
130
+ schema_version="2.1",
131
+ world_providers=[
132
+ {
133
+ "id": "workspace",
134
+ "title": "Workspace",
135
+ "description": "Authoritative workspace provider.",
136
+ "resource_types": [
137
+ {
138
+ "id": "workspace.item",
139
+ "title": "Workspace item",
140
+ "description": "A stable workspace item.",
141
+ "identity_fields": ["id"],
142
+ "search_capability": "workspace.missing",
143
+ }
144
+ ],
145
+ }
146
+ ],
147
+ )
148
+ )
149
+
150
+
77
151
  def test_empty_runtime_types_rejected():
78
152
  with pytest.raises(ValidationError):
79
153
  PluginManifest.model_validate(_minimal_manifest(runtime_types=[]))
@@ -262,7 +262,7 @@ wheels = [
262
262
 
263
263
  [[package]]
264
264
  name = "nutria-plugin"
265
- version = "0.1.0"
265
+ version = "0.2.1"
266
266
  source = { editable = "." }
267
267
  dependencies = [
268
268
  { name = "cryptography" },
File without changes
File without changes