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