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.
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/CHANGELOG.md +7 -0
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/PKG-INFO +18 -5
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/README.md +17 -4
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/docs/index.md +3 -2
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/docs/manifest.md +30 -2
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/pyproject.toml +1 -1
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/src/nutria_plugin/__init__.py +7 -1
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/src/nutria_plugin/capabilities.py +106 -3
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/src/nutria_plugin/manifest.py +23 -2
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/src/nutria_plugin/packaging.py +1 -1
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/tests/test_manifest.py +75 -1
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/uv.lock +1 -1
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/.github/workflows/publish.yml +0 -0
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/.gitignore +0 -0
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/docs/admin-extensions.md +0 -0
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/docs/admin-flows.md +0 -0
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/docs/cli.md +0 -0
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/docs/connection-types.md +0 -0
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/docs/python-api.md +0 -0
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/docs/quickstart.md +0 -0
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/docs/reviewable-actions.md +0 -0
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/docs/security.md +0 -0
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/docs/skill-format.md +0 -0
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/examples/my-first-plugin/README.md +0 -0
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/examples/my-first-plugin/hooks/hooks.json +0 -0
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/examples/my-first-plugin/plugin.json +0 -0
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/examples/my-first-plugin/settings.schema.json +0 -0
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/src/nutria_plugin/bundle.py +0 -0
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/src/nutria_plugin/cli.py +0 -0
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/src/nutria_plugin/signing.py +0 -0
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/tests/test_bundle.py +0 -0
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/tests/test_cli.py +0 -0
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/tests/test_packaging.py +0 -0
- {nutria_plugin-0.2.0 → nutria_plugin-0.2.1}/tests/test_reviewable_actions.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
@@ -15,7 +15,7 @@ Signing:
|
|
|
15
15
|
generate_keypair, sign_manifest, verify_manifest, SignatureStatus
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
__version__ = "0.2.
|
|
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)
|
|
@@ -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
|
|
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=[]))
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|