ltcai 1.7.0 → 2.0.0
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.
- package/README.md +31 -21
- package/docs/CHANGELOG.md +65 -0
- package/docs/EDITION_STRATEGY.md +10 -4
- package/docs/ENTERPRISE.md +3 -1
- package/docs/MULTI_AGENT_RUNTIME.md +410 -0
- package/docs/PLUGIN_SDK.md +651 -0
- package/docs/REALTIME_COLLABORATION.md +410 -0
- package/docs/V2_ARCHITECTURE.md +528 -0
- package/docs/WORKFLOW_DESIGNER.md +475 -0
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agents.py +98 -0
- package/latticeai/api/plugins.py +115 -0
- package/latticeai/api/realtime.py +91 -0
- package/latticeai/api/workflow_designer.py +207 -0
- package/latticeai/core/multi_agent.py +270 -0
- package/latticeai/core/plugins.py +400 -0
- package/latticeai/core/realtime.py +190 -0
- package/latticeai/core/workflow_engine.py +329 -0
- package/latticeai/core/workspace_os.py +155 -2
- package/latticeai/server_app.py +76 -2
- package/latticeai/services/platform_runtime.py +200 -0
- package/package.json +8 -2
- package/plugins/README.md +35 -0
- package/plugins/git-insights/plugin.json +15 -0
- package/plugins/hello-world/plugin.json +16 -0
- package/plugins/hello-world/skills/hello_skill/SKILL.md +15 -0
- package/static/activity.html +70 -0
- package/static/agents.html +92 -0
- package/static/platform.css +75 -0
- package/static/plugins.html +82 -0
- package/static/scripts/platform.js +64 -0
- package/static/workflows.html +121 -0
- package/static/workspace.html +5 -1
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
# Lattice AI Plugin SDK
|
|
2
|
+
|
|
3
|
+
The Plugin SDK is the v2.0.0 extension layer for the Lattice AI Agentic Workspace
|
|
4
|
+
Platform. It lets you package skills, tools, workflow templates, and actions into
|
|
5
|
+
one versioned, permissioned unit — a *plugin*. A plugin is a directory under the
|
|
6
|
+
configured `plugins` root that ships a `plugin.json` manifest.
|
|
7
|
+
|
|
8
|
+
The SDK is intentionally **additive**. Plugins *extend* the existing Skill, Tool,
|
|
9
|
+
and Workflow surfaces; they never replace them. Standalone skills that are already
|
|
10
|
+
installed keep working untouched, and a plugin that bundles a skill registers it
|
|
11
|
+
through the existing skill registry rather than owning a parallel one.
|
|
12
|
+
|
|
13
|
+
> **Compatibility.** v1.x data and APIs are preserved. The Plugin SDK adds new
|
|
14
|
+
> code (`latticeai/core/plugins.py`, `latticeai/api/plugins.py`) and new
|
|
15
|
+
> persisted state (`plugin_registry` inside the Workspace OS store). Nothing in
|
|
16
|
+
> the v1.x contract changes. The new HTTP routes live under `/plugins/registry`
|
|
17
|
+
> and friends, which do **not** collide with the pre-existing
|
|
18
|
+
> `/plugins/directory` marketplace routes.
|
|
19
|
+
|
|
20
|
+
The host SDK version is exposed as:
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
PLUGIN_SDK_VERSION = "2.0.0"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## The `plugin.json` manifest
|
|
29
|
+
|
|
30
|
+
Every plugin ships a `plugin.json` at the root of its directory. The manifest is
|
|
31
|
+
parsed and validated into an immutable `PluginManifest`.
|
|
32
|
+
|
|
33
|
+
### Schema
|
|
34
|
+
|
|
35
|
+
| Field | Type | Required | Notes |
|
|
36
|
+
| --- | --- | --- | --- |
|
|
37
|
+
| `id` | string | yes | Lowercase alphanumeric with `-` or `_`, 2–64 chars (`^[a-z0-9][a-z0-9_-]{1,63}$`). Directory name should match. |
|
|
38
|
+
| `name` | string | yes | Human-readable name. Falls back to `id` if omitted. |
|
|
39
|
+
| `version` | string | yes | Semantic version (`^\d+\.\d+\.\d+([.-][0-9A-Za-z.]+)?$`). |
|
|
40
|
+
| `description` | string | no | Short summary. |
|
|
41
|
+
| `author` | string | no | Author or organization. |
|
|
42
|
+
| `lattice_version` | string | no | Minimum host version this plugin requires. May be bare (`"2.0.0"`) or prefixed (`">=2.0.0"`). Empty means "any host". |
|
|
43
|
+
| `permissions` | string[] | no | Must be a subset of the [permission allow-list](#permissions). Unknown values are rejected. |
|
|
44
|
+
| `provides` | object | no | What the plugin contributes. Keys must be in `("skills", "tools", "workflows", "actions")`; each value is a list of names. |
|
|
45
|
+
| `entrypoint` | string | no | Reserved for an optional code entrypoint. |
|
|
46
|
+
| `homepage` | string | no | Project / docs URL. |
|
|
47
|
+
|
|
48
|
+
The `provides` object declares the surfaces the plugin extends:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"provides": {
|
|
53
|
+
"skills": ["..."],
|
|
54
|
+
"tools": ["..."],
|
|
55
|
+
"workflows": ["..."],
|
|
56
|
+
"actions": ["..."]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The allowed keys are fixed:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
PLUGIN_PROVIDES = ("skills", "tools", "workflows", "actions")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Validation rules
|
|
68
|
+
|
|
69
|
+
`validate_manifest(data, *, path="")` returns `(manifest_or_None, errors)`. A
|
|
70
|
+
manifest with any error returns `None` for the manifest, so a caller can never
|
|
71
|
+
accidentally treat an invalid plugin as runnable. The validator enforces:
|
|
72
|
+
|
|
73
|
+
- `id`, `name`, and `version` are present.
|
|
74
|
+
- `id` matches the id pattern; `version` is a valid semantic version.
|
|
75
|
+
- `permissions` is a list and every entry is in `PLUGIN_PERMISSIONS`.
|
|
76
|
+
- `provides` is an object, every key is in `PLUGIN_PROVIDES`, and every value is
|
|
77
|
+
a list.
|
|
78
|
+
- If `lattice_version` is set, the host must be compatible (see
|
|
79
|
+
[version compatibility](#version-compatibility)).
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
def validate_manifest(
|
|
83
|
+
data: Dict[str, Any], *, path: str = ""
|
|
84
|
+
) -> Tuple[Optional[PluginManifest], List[str]]:
|
|
85
|
+
...
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Parsed manifest
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class PluginManifest:
|
|
93
|
+
id: str
|
|
94
|
+
name: str
|
|
95
|
+
version: str
|
|
96
|
+
description: str = ""
|
|
97
|
+
author: str = ""
|
|
98
|
+
lattice_version: str = ""
|
|
99
|
+
permissions: Tuple[str, ...] = ()
|
|
100
|
+
provides: Dict[str, List[str]] = field(default_factory=dict)
|
|
101
|
+
entrypoint: str = ""
|
|
102
|
+
homepage: str = ""
|
|
103
|
+
path: str = ""
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`PluginManifest.public()` returns the JSON-safe shape used by the API, including a
|
|
107
|
+
computed `compatible` flag and the on-disk `path`:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"id": "hello-world",
|
|
112
|
+
"name": "Hello World",
|
|
113
|
+
"version": "1.0.0",
|
|
114
|
+
"description": "...",
|
|
115
|
+
"author": "Lattice AI",
|
|
116
|
+
"lattice_version": "2.0.0",
|
|
117
|
+
"permissions": ["read_workspace", "run_skills"],
|
|
118
|
+
"provides": {
|
|
119
|
+
"skills": ["hello_skill"],
|
|
120
|
+
"workflows": ["hello-workflow"],
|
|
121
|
+
"actions": ["greet"]
|
|
122
|
+
},
|
|
123
|
+
"entrypoint": "",
|
|
124
|
+
"homepage": "https://github.com/TaeSooPark-PTS/LatticeAI/blob/main/docs/PLUGIN_SDK.md",
|
|
125
|
+
"path": "/abs/path/to/plugins/hello-world",
|
|
126
|
+
"compatible": true
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Permissions
|
|
133
|
+
|
|
134
|
+
Permissions are a small, fixed allow-list. A manifest may only request
|
|
135
|
+
permissions from this set; the execution boundary refuses any capability the
|
|
136
|
+
plugin did not declare *and* was not granted at install time.
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
PLUGIN_PERMISSIONS = (
|
|
140
|
+
"read_workspace",
|
|
141
|
+
"write_workspace",
|
|
142
|
+
"read_graph",
|
|
143
|
+
"write_graph",
|
|
144
|
+
"run_tools",
|
|
145
|
+
"run_skills",
|
|
146
|
+
"run_workflows",
|
|
147
|
+
"run_agents",
|
|
148
|
+
"network",
|
|
149
|
+
"manage_memory",
|
|
150
|
+
)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
| Permission | Grants |
|
|
154
|
+
| --- | --- |
|
|
155
|
+
| `read_workspace` | Read workspace state. |
|
|
156
|
+
| `write_workspace` | Mutate workspace state. |
|
|
157
|
+
| `read_graph` | Read the knowledge graph. |
|
|
158
|
+
| `write_graph` | Write to the knowledge graph. |
|
|
159
|
+
| `run_tools` | Invoke tools (required by the `run_tool` action). |
|
|
160
|
+
| `run_skills` | Invoke skills (required by the `run_skill` action). |
|
|
161
|
+
| `run_workflows` | Invoke workflows (required by the `run_workflow` action). |
|
|
162
|
+
| `run_agents` | Invoke agents (required by the `run_agent` action). |
|
|
163
|
+
| `network` | Make outbound network calls. |
|
|
164
|
+
| `manage_memory` | Read/write persistent memory. |
|
|
165
|
+
|
|
166
|
+
The list is kept deliberately small so the Enterprise seam can layer
|
|
167
|
+
finer-grained policy on top without changing the community contract. Each
|
|
168
|
+
permission maps to a concrete thing the [execution boundary](#permissioned-execution-boundary)
|
|
169
|
+
will or will not allow.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Version compatibility
|
|
174
|
+
|
|
175
|
+
A plugin's `lattice_version` is treated as a **minimum**: the host major version
|
|
176
|
+
must match, and the host version must be greater than or equal to the required
|
|
177
|
+
version. An empty/missing requirement means "any host".
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
def is_compatible(required: str, current: str = PLUGIN_SDK_VERSION) -> bool:
|
|
181
|
+
"""True if a plugin requiring ``required`` runs on ``current`` host version.
|
|
182
|
+
|
|
183
|
+
``required`` is a minimum (major must match, host must be >= required).
|
|
184
|
+
Empty / missing requirement is treated as "any host".
|
|
185
|
+
"""
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
A leading `>=` is stripped before comparison, so both forms below behave
|
|
189
|
+
identically:
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{ "lattice_version": "2.0.0" }
|
|
193
|
+
{ "lattice_version": ">=2.0.0" }
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Examples against a host of `2.0.0`:
|
|
197
|
+
|
|
198
|
+
| Required | Compatible | Why |
|
|
199
|
+
| --- | --- | --- |
|
|
200
|
+
| `""` (missing) | yes | Any host. |
|
|
201
|
+
| `2.0.0` / `>=2.0.0` | yes | Same major, host `>=` required. |
|
|
202
|
+
| `2.1.0` | no | Host is lower than the required minimum. |
|
|
203
|
+
| `1.0.0` | no | Major mismatch. |
|
|
204
|
+
| `3.0.0` | no | Major mismatch. |
|
|
205
|
+
|
|
206
|
+
Compatibility is checked at validation time and again at install time, so an
|
|
207
|
+
incompatible plugin can never be activated.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Lifecycle
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
discover -> validate -> install -> enable / disable -> uninstall
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Lifecycle *state* (installed / enabled / version / status) is delegated to the
|
|
218
|
+
Workspace OS store via a small `store` port, so plugins reuse the same
|
|
219
|
+
local-first JSON persistence, workspace scoping, and timeline events as skills.
|
|
220
|
+
The registry itself owns only manifest parsing and the execution boundary.
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
class PluginRegistry:
|
|
224
|
+
def __init__(self, plugins_dir: Path | str, *, store: Any = None): ...
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Discover
|
|
228
|
+
|
|
229
|
+
`discover()` scans `plugins_dir` for `<id>/plugin.json` manifests. It performs no
|
|
230
|
+
import-time I/O — the filesystem is only touched when you call it. Each manifest
|
|
231
|
+
is validated; results are split into `valid` and `invalid`.
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
def discover(self) -> Dict[str, Any]:
|
|
235
|
+
# {"valid": List[PluginManifest], "invalid": List[{path, id?, errors}]}
|
|
236
|
+
...
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`catalog()` merges discovered manifests with persisted lifecycle state for the
|
|
240
|
+
UI, returning `sdk_version`, the permission/provides vocabularies, the merged
|
|
241
|
+
`plugins` list, any `invalid` entries, `plugins_dir`, and `total`.
|
|
242
|
+
|
|
243
|
+
### Validate
|
|
244
|
+
|
|
245
|
+
A manifest can be validated standalone (without touching disk) via
|
|
246
|
+
`validate_manifest` or over HTTP via `POST /plugins/validate`.
|
|
247
|
+
|
|
248
|
+
### Install
|
|
249
|
+
|
|
250
|
+
`install(plugin_id, *, register_skill=None)` activates a discovered plugin. It
|
|
251
|
+
re-checks compatibility, registers the skills the plugin bundles through the
|
|
252
|
+
**existing** skill registry, and records lifecycle state in the store.
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
def install(
|
|
256
|
+
self,
|
|
257
|
+
plugin_id: str,
|
|
258
|
+
*,
|
|
259
|
+
register_skill: Optional[Callable[[str, str], Any]] = None,
|
|
260
|
+
) -> Dict[str, Any]:
|
|
261
|
+
...
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
`register_skill(skill_name, plugin_id)` is injected by the host so plugins
|
|
265
|
+
*extend* the existing skill registry instead of owning a parallel one. For each
|
|
266
|
+
name under `provides.skills`, the registry calls `register_skill`; registration
|
|
267
|
+
is best-effort (a failure for one skill does not abort the install). The returned
|
|
268
|
+
dict contains the public manifest, the persisted `registry` entry, and the list
|
|
269
|
+
of `registered_skills`.
|
|
270
|
+
|
|
271
|
+
Install persists this entry into the store's `plugin_registry`:
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
entry = self.store.mark_plugin_installed(
|
|
275
|
+
plugin_id,
|
|
276
|
+
version=manifest.version,
|
|
277
|
+
metadata={
|
|
278
|
+
"permissions": list(manifest.permissions),
|
|
279
|
+
"provides": {k: list(v) for k, v in manifest.provides.items()},
|
|
280
|
+
"registered_skills": registered_skills,
|
|
281
|
+
},
|
|
282
|
+
)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
The granted permissions are captured in `metadata.permissions` at this point —
|
|
286
|
+
this is the "granted at install time" set the execution boundary enforces.
|
|
287
|
+
|
|
288
|
+
If the plugin is missing/invalid, or its `lattice_version` is incompatible with
|
|
289
|
+
the host, `install` raises `PluginError`.
|
|
290
|
+
|
|
291
|
+
### Enable / Disable
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
def set_enabled(self, plugin_id: str, enabled: bool) -> Dict[str, Any]: ...
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
A disabled plugin remains installed but cannot execute actions (the boundary
|
|
298
|
+
returns `blocked`). Enable/disable is a toggle on the persisted registry entry.
|
|
299
|
+
|
|
300
|
+
### Uninstall
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
def uninstall(self, plugin_id: str) -> Dict[str, Any]: ...
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Uninstall marks the registry entry `installed: false` and `enabled: false`. It is
|
|
307
|
+
non-destructive to the on-disk plugin directory; re-installing re-activates it.
|
|
308
|
+
|
|
309
|
+
### Persisted state
|
|
310
|
+
|
|
311
|
+
State lives in `WorkspaceOSStore.plugin_registry`, a dict keyed by `plugin_id`.
|
|
312
|
+
The store methods used by the registry are:
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
def list_plugin_registry(self) -> Dict[str, Any]: ...
|
|
316
|
+
def mark_plugin_installed(self, plugin_id, *, version="0.0.0",
|
|
317
|
+
metadata=None) -> Dict[str, Any]: ...
|
|
318
|
+
def mark_plugin_uninstalled(self, plugin_id) -> Dict[str, Any]: ...
|
|
319
|
+
def set_plugin_enabled(self, plugin_id, enabled) -> Dict[str, Any]: ...
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
A persisted entry looks like:
|
|
323
|
+
|
|
324
|
+
```json
|
|
325
|
+
{
|
|
326
|
+
"id": "hello-world",
|
|
327
|
+
"installed": true,
|
|
328
|
+
"enabled": true,
|
|
329
|
+
"version": "1.0.0",
|
|
330
|
+
"install_status": "ready",
|
|
331
|
+
"validation_status": "valid",
|
|
332
|
+
"metadata": {
|
|
333
|
+
"permissions": ["read_workspace", "run_skills"],
|
|
334
|
+
"provides": {
|
|
335
|
+
"skills": ["hello_skill"],
|
|
336
|
+
"workflows": ["hello-workflow"],
|
|
337
|
+
"actions": ["greet"]
|
|
338
|
+
},
|
|
339
|
+
"registered_skills": ["hello_skill"]
|
|
340
|
+
},
|
|
341
|
+
"updated_at": "..."
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Each lifecycle mutation also records a Workspace OS timeline event
|
|
346
|
+
(`plugin_installed`, `plugin_uninstalled`, `plugin_enabled`, `plugin_disabled`)
|
|
347
|
+
under the `plugins` channel.
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## Permissioned execution boundary
|
|
352
|
+
|
|
353
|
+
Plugin actions never run arbitrary code directly. They pass through
|
|
354
|
+
`execute_action`, which enforces enablement and permissions, then dispatches to a
|
|
355
|
+
host-provided runner.
|
|
356
|
+
|
|
357
|
+
```python
|
|
358
|
+
def execute_action(
|
|
359
|
+
self,
|
|
360
|
+
plugin_id: str,
|
|
361
|
+
action: str,
|
|
362
|
+
args: Optional[Dict[str, Any]] = None,
|
|
363
|
+
*,
|
|
364
|
+
runners: Optional[Dict[str, Callable[..., Any]]] = None,
|
|
365
|
+
) -> PluginExecutionResult:
|
|
366
|
+
...
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
`runners` maps a capability (`"tools"`, `"skills"`, `"workflows"`, `"agents"`) to
|
|
370
|
+
a callable the host injects. Built-in actions map to a capability and the
|
|
371
|
+
permission they require:
|
|
372
|
+
|
|
373
|
+
| Action | Capability | Required permission |
|
|
374
|
+
| --- | --- | --- |
|
|
375
|
+
| `run_tool` | `tools` | `run_tools` |
|
|
376
|
+
| `run_skill` | `skills` | `run_skills` |
|
|
377
|
+
| `run_workflow` | `workflows` | `run_workflows` |
|
|
378
|
+
| `run_agent` | `agents` | `run_agents` |
|
|
379
|
+
| *(any other)* | `actions` | *(none)* |
|
|
380
|
+
|
|
381
|
+
The boundary applies these checks in order:
|
|
382
|
+
|
|
383
|
+
1. **Unknown / invalid plugin** → `status: "error"` (`plugin not found or invalid`).
|
|
384
|
+
2. **Disabled plugin** → `status: "blocked"` (`plugin is not enabled`). When a
|
|
385
|
+
store is wired, the plugin must be enabled (falling back to `installed`).
|
|
386
|
+
3. **Permission not declared in the manifest** → `status: "blocked"`
|
|
387
|
+
(`plugin did not declare required permission '...'`).
|
|
388
|
+
4. **Permission not granted at install time** → `status: "blocked"`
|
|
389
|
+
(`permission '...' not granted at install time`). The granted set comes from
|
|
390
|
+
the persisted `metadata.permissions`.
|
|
391
|
+
5. **No host runner wired for the capability** → `status: "skipped"`
|
|
392
|
+
(`no host runner for capability '...'`). This is safe-by-default: a missing
|
|
393
|
+
runner never crashes the caller.
|
|
394
|
+
6. Otherwise the runner is invoked. Success → `status: "ok"` with `output`; a
|
|
395
|
+
raised exception → `status: "error"` with `reason`.
|
|
396
|
+
|
|
397
|
+
The runner is called as:
|
|
398
|
+
|
|
399
|
+
```python
|
|
400
|
+
output = runner(plugin_id=plugin_id, action=action, args=args, manifest=manifest)
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Result shape
|
|
404
|
+
|
|
405
|
+
```python
|
|
406
|
+
@dataclass
|
|
407
|
+
class PluginExecutionResult:
|
|
408
|
+
plugin_id: str
|
|
409
|
+
action: str
|
|
410
|
+
status: str # "ok" | "blocked" | "error" | "skipped"
|
|
411
|
+
output: Any = None
|
|
412
|
+
reason: str = ""
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
`as_dict()` serializes it for the API:
|
|
416
|
+
|
|
417
|
+
```json
|
|
418
|
+
{
|
|
419
|
+
"plugin_id": "git-insights",
|
|
420
|
+
"action": "run_tool",
|
|
421
|
+
"status": "ok",
|
|
422
|
+
"output": "...",
|
|
423
|
+
"reason": ""
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
`PluginError` is raised for validation / lifecycle / execution failures at the
|
|
428
|
+
registry level (for example, installing a missing or incompatible plugin).
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## HTTP API
|
|
433
|
+
|
|
434
|
+
The API router is built with the same router-factory convention as the rest of
|
|
435
|
+
`latticeai.api`: `server_app` constructs the dependencies and passes them in; the
|
|
436
|
+
module never imports the app.
|
|
437
|
+
|
|
438
|
+
```python
|
|
439
|
+
def create_plugins_router(
|
|
440
|
+
*,
|
|
441
|
+
registry,
|
|
442
|
+
require_user: Callable[[Request], str],
|
|
443
|
+
require_admin: Callable[[Request], Any],
|
|
444
|
+
append_audit_event: Callable[..., None],
|
|
445
|
+
register_skill: Optional[Callable[[str, str], Any]] = None,
|
|
446
|
+
plugin_runners_factory: Optional[Callable[[], Dict[str, Callable[..., Any]]]] = None,
|
|
447
|
+
ui_file_response: Optional[Callable[[Path], Any]] = None,
|
|
448
|
+
static_dir: Optional[Path] = None,
|
|
449
|
+
) -> APIRouter:
|
|
450
|
+
...
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
> All paths are namespaced under `/plugins/registry` (and sibling action routes)
|
|
454
|
+
> so they do **not** collide with the pre-existing `/plugins/directory`
|
|
455
|
+
> marketplace routes.
|
|
456
|
+
|
|
457
|
+
### `GET /plugins/registry`
|
|
458
|
+
|
|
459
|
+
Requires an authenticated user. Returns the full `catalog()` (SDK version,
|
|
460
|
+
permission/provides vocabularies, merged plugin list, invalid manifests,
|
|
461
|
+
`plugins_dir`, total).
|
|
462
|
+
|
|
463
|
+
### `GET /plugins/registry/{plugin_id}`
|
|
464
|
+
|
|
465
|
+
Requires an authenticated user. Returns the public manifest plus the persisted
|
|
466
|
+
registry state. `404` if the plugin is not found or invalid.
|
|
467
|
+
|
|
468
|
+
```json
|
|
469
|
+
{
|
|
470
|
+
"plugin": { "...PluginManifest.public()..." },
|
|
471
|
+
"registry": { "...persisted entry..." }
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### `POST /plugins/validate`
|
|
476
|
+
|
|
477
|
+
Requires an authenticated user. Validates a manifest dict without touching disk.
|
|
478
|
+
|
|
479
|
+
```json
|
|
480
|
+
// request
|
|
481
|
+
{ "manifest": { "id": "...", "name": "...", "version": "1.0.0" } }
|
|
482
|
+
|
|
483
|
+
// response
|
|
484
|
+
{
|
|
485
|
+
"ok": true,
|
|
486
|
+
"errors": [],
|
|
487
|
+
"manifest": { "...public()..." }
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### `POST /plugins/install`
|
|
492
|
+
|
|
493
|
+
Requires **admin**. Installs the plugin (registering bundled skills via the
|
|
494
|
+
injected `register_skill`) and appends a `plugin_install` audit event. Returns
|
|
495
|
+
the install result. `400` on failure (`PluginError` message).
|
|
496
|
+
|
|
497
|
+
```json
|
|
498
|
+
{ "plugin_id": "hello-world" }
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### `POST /plugins/uninstall`
|
|
502
|
+
|
|
503
|
+
Requires **admin**. Uninstalls the plugin and appends a `plugin_uninstall` audit
|
|
504
|
+
event.
|
|
505
|
+
|
|
506
|
+
```json
|
|
507
|
+
{ "plugin_id": "hello-world" }
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### `POST /plugins/enable`
|
|
511
|
+
|
|
512
|
+
Requires an authenticated user. Enables the plugin.
|
|
513
|
+
|
|
514
|
+
```json
|
|
515
|
+
{ "plugin_id": "hello-world" }
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### `POST /plugins/disable`
|
|
519
|
+
|
|
520
|
+
Requires an authenticated user. Disables the plugin.
|
|
521
|
+
|
|
522
|
+
```json
|
|
523
|
+
{ "plugin_id": "hello-world" }
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### `POST /plugins/execute`
|
|
527
|
+
|
|
528
|
+
Requires an authenticated user. Runs an action through the
|
|
529
|
+
[execution boundary](#permissioned-execution-boundary), using runners from
|
|
530
|
+
`plugin_runners_factory()` (or an empty map if no factory is wired). Appends a
|
|
531
|
+
`plugin_execute` audit event including the resulting status.
|
|
532
|
+
|
|
533
|
+
```json
|
|
534
|
+
// request
|
|
535
|
+
{
|
|
536
|
+
"plugin_id": "git-insights",
|
|
537
|
+
"action": "run_tool",
|
|
538
|
+
"args": { "tool": "git_status" }
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// response — PluginExecutionResult.as_dict()
|
|
542
|
+
{
|
|
543
|
+
"plugin_id": "git-insights",
|
|
544
|
+
"action": "run_tool",
|
|
545
|
+
"status": "ok",
|
|
546
|
+
"output": "...",
|
|
547
|
+
"reason": ""
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### `GET /plugins/sdk`
|
|
552
|
+
|
|
553
|
+
Requires an authenticated user. Serves the Plugin SDK UI page (`plugins.html`).
|
|
554
|
+
Returns `404` if the UI response helper / static directory are not wired or the
|
|
555
|
+
page is missing.
|
|
556
|
+
|
|
557
|
+
### Request models
|
|
558
|
+
|
|
559
|
+
```python
|
|
560
|
+
class PluginActionRequest(BaseModel):
|
|
561
|
+
plugin_id: str
|
|
562
|
+
enabled: Optional[bool] = None
|
|
563
|
+
version: Optional[str] = None
|
|
564
|
+
|
|
565
|
+
class PluginValidateRequest(BaseModel):
|
|
566
|
+
manifest: Dict[str, Any] = {}
|
|
567
|
+
|
|
568
|
+
class PluginExecuteRequest(BaseModel):
|
|
569
|
+
plugin_id: str
|
|
570
|
+
action: str
|
|
571
|
+
args: Dict[str, Any] = {}
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
---
|
|
575
|
+
|
|
576
|
+
## Bundled example plugins
|
|
577
|
+
|
|
578
|
+
Two example plugins ship with the platform under the `plugins` root.
|
|
579
|
+
|
|
580
|
+
### `hello-world`
|
|
581
|
+
|
|
582
|
+
Demonstrates bundling a skill, a workflow template, and a custom action. It
|
|
583
|
+
requests only `read_workspace` and `run_skills`.
|
|
584
|
+
|
|
585
|
+
```json
|
|
586
|
+
{
|
|
587
|
+
"id": "hello-world",
|
|
588
|
+
"name": "Hello World",
|
|
589
|
+
"version": "1.0.0",
|
|
590
|
+
"description": "Example plugin demonstrating the Lattice AI Plugin SDK: bundles a skill, a workflow template, and a greet action.",
|
|
591
|
+
"author": "Lattice AI",
|
|
592
|
+
"lattice_version": "2.0.0",
|
|
593
|
+
"permissions": ["read_workspace", "run_skills"],
|
|
594
|
+
"provides": {
|
|
595
|
+
"skills": ["hello_skill"],
|
|
596
|
+
"workflows": ["hello-workflow"],
|
|
597
|
+
"actions": ["greet"]
|
|
598
|
+
},
|
|
599
|
+
"entrypoint": "",
|
|
600
|
+
"homepage": "https://github.com/TaeSooPark-PTS/LatticeAI/blob/main/docs/PLUGIN_SDK.md"
|
|
601
|
+
}
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
On install, `hello_skill` is registered through the existing skill registry. The
|
|
605
|
+
`greet` action maps to the generic `actions` capability (no permission gate); it
|
|
606
|
+
runs only when the host wires an `actions` runner, otherwise it is reported
|
|
607
|
+
`skipped`.
|
|
608
|
+
|
|
609
|
+
### `git-insights`
|
|
610
|
+
|
|
611
|
+
Surfaces read-only git status and log insights through the permissioned tool
|
|
612
|
+
execution boundary. It declares its requirement as `">=2.0.0"`, demonstrating the
|
|
613
|
+
prefixed form of `lattice_version`.
|
|
614
|
+
|
|
615
|
+
```json
|
|
616
|
+
{
|
|
617
|
+
"id": "git-insights",
|
|
618
|
+
"name": "Git Insights",
|
|
619
|
+
"version": "1.0.0",
|
|
620
|
+
"description": "Example plugin that surfaces read-only git status and log insights through the permissioned tool execution boundary.",
|
|
621
|
+
"author": "Lattice AI",
|
|
622
|
+
"lattice_version": ">=2.0.0",
|
|
623
|
+
"permissions": ["read_workspace", "run_tools"],
|
|
624
|
+
"provides": {
|
|
625
|
+
"tools": ["git_status", "git_log"],
|
|
626
|
+
"actions": ["summarize_repo"]
|
|
627
|
+
},
|
|
628
|
+
"entrypoint": "",
|
|
629
|
+
"homepage": "https://github.com/TaeSooPark-PTS/LatticeAI/blob/main/docs/PLUGIN_SDK.md"
|
|
630
|
+
}
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
Because it declares `run_tools`, a `run_tool` action passes the permission gate
|
|
634
|
+
(provided `run_tools` was also granted at install time) and dispatches to the
|
|
635
|
+
host's `tools` runner. Without a `tools` runner wired, the same call is reported
|
|
636
|
+
`skipped` rather than failing.
|
|
637
|
+
|
|
638
|
+
---
|
|
639
|
+
|
|
640
|
+
## Authoring checklist
|
|
641
|
+
|
|
642
|
+
1. Create a directory `plugins/<id>/` matching your `id`.
|
|
643
|
+
2. Add a `plugin.json` with `id`, `name`, and `version` at minimum.
|
|
644
|
+
3. Request only the permissions you need (subset of `PLUGIN_PERMISSIONS`).
|
|
645
|
+
4. Declare what you `provide` (`skills` / `tools` / `workflows` / `actions`).
|
|
646
|
+
5. Set `lattice_version` to your minimum supported host (e.g. `">=2.0.0"`).
|
|
647
|
+
6. Validate via `POST /plugins/validate` (or `validate_manifest`).
|
|
648
|
+
7. Install (admin), enable, and execute actions through `POST /plugins/execute`.
|
|
649
|
+
|
|
650
|
+
Remember: plugins **extend** existing skills, tools, and workflows — they never
|
|
651
|
+
replace them, and all v1.x data and APIs remain intact.
|