@gobing-ai/spur-plugin-sdk 0.1.0 → 0.1.5
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 +296 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# @gobing-ai/spur-plugin-sdk
|
|
2
|
+
|
|
3
|
+
Spur Plugin SDK — types, schemas, and runtime for building Spur plugins with trust levels, capability registries, and event hooks.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```mermaid
|
|
8
|
+
classDiagram
|
|
9
|
+
direction TB
|
|
10
|
+
|
|
11
|
+
class SpurPlugin {
|
|
12
|
+
<<interface>>
|
|
13
|
+
+name: string
|
|
14
|
+
+version: string
|
|
15
|
+
+trust: TrustLevel
|
|
16
|
+
+onLoad(host: PluginHost)
|
|
17
|
+
+onUnload?(host: PluginHost)
|
|
18
|
+
+onServerStart?(host: PluginHost)
|
|
19
|
+
+onServerStop?(host: PluginHost)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class PluginHost {
|
|
23
|
+
+commands: CommandRegistry
|
|
24
|
+
+api: ApiRegistry
|
|
25
|
+
+ui: UiRegistry
|
|
26
|
+
+events: EventRegistry
|
|
27
|
+
+harnesses: HarnessRegistry
|
|
28
|
+
+providers: ProviderRegistry
|
|
29
|
+
+rules: RuleRegistry
|
|
30
|
+
+skills: SkillRegistry
|
|
31
|
+
+workers: WorkerRegistry
|
|
32
|
+
+eventRegistry: EventRegistry
|
|
33
|
+
+logger: Logger
|
|
34
|
+
+trust: TrustEngine
|
|
35
|
+
+loadPlugin(plugin, ctx, overrides?, env?) Promise~Config~
|
|
36
|
+
+unloadPlugin(name) Promise~void~
|
|
37
|
+
+isLoaded(name) boolean
|
|
38
|
+
+startServerHooks() Promise~void~
|
|
39
|
+
+stopServerHooks() Promise~void~
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class TrustEngine {
|
|
43
|
+
+check(manifest, capability, name, ctx) void
|
|
44
|
+
+enforce(capability, level, ctx) void
|
|
45
|
+
+declares(manifest, capability, name) boolean
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class TrustLevel {
|
|
49
|
+
<<enumeration>>
|
|
50
|
+
bundled
|
|
51
|
+
curated
|
|
52
|
+
local
|
|
53
|
+
untrusted
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class Capability {
|
|
57
|
+
<<type>>
|
|
58
|
+
commands | api | ui | events
|
|
59
|
+
harnesses | providers | rules
|
|
60
|
+
skills | workers
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
class EventRegistry {
|
|
64
|
+
+subscribe(pattern, handler) void
|
|
65
|
+
+unsubscribe(pattern) void
|
|
66
|
+
+unsubscribeAll() void
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
class PluginManifest {
|
|
70
|
+
+name: string
|
|
71
|
+
+version: string
|
|
72
|
+
+trust: TrustLevel
|
|
73
|
+
+capabilities: CapabilitiesManifest
|
|
74
|
+
+config?: Record~string,unknown~
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
class RegistrationContext {
|
|
78
|
+
+source: PluginSource
|
|
79
|
+
+pluginName: string
|
|
80
|
+
+trustLevel: TrustLevel
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
class PluginManifestError~Zod~ {
|
|
84
|
+
+issues: ZodIssue[]
|
|
85
|
+
}
|
|
86
|
+
class PluginCollisionError
|
|
87
|
+
class PluginTrustError
|
|
88
|
+
class PluginNotDeclaredError
|
|
89
|
+
|
|
90
|
+
SpurPlugin --> PluginHost : onLoad(host)
|
|
91
|
+
PluginHost *-- TrustEngine
|
|
92
|
+
PluginHost *-- EventRegistry
|
|
93
|
+
PluginHost *-- "9" Registry : typed registries
|
|
94
|
+
PluginHost --> RegistrationContext
|
|
95
|
+
TrustEngine --> TrustLevel
|
|
96
|
+
TrustEngine --> Capability
|
|
97
|
+
TrustEngine ..> PluginTrustError : throws
|
|
98
|
+
TrustEngine ..> PluginNotDeclaredError : throws
|
|
99
|
+
PluginHost ..> PluginManifest : validates via
|
|
100
|
+
PluginManifestError --|> Error
|
|
101
|
+
PluginCollisionError --|> Error
|
|
102
|
+
PluginTrustError --|> Error
|
|
103
|
+
PluginNotDeclaredError --|> Error
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Quick Start
|
|
107
|
+
|
|
108
|
+
### Installation
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
bun add @gobing-ai/spur-plugin-sdk
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 1. Define a plugin
|
|
115
|
+
|
|
116
|
+
Plugins implement the `SpurPlugin` interface. The entry point is `onLoad(host)`, which receives a `PluginHost` providing access to all capability registries.
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import type { SpurPlugin, PluginHost } from '@gobing-ai/spur-plugin-sdk';
|
|
120
|
+
|
|
121
|
+
export const myPlugin: SpurPlugin = {
|
|
122
|
+
name: 'my-plugin',
|
|
123
|
+
version: '1.0.0',
|
|
124
|
+
trust: 'local',
|
|
125
|
+
|
|
126
|
+
onLoad(host: PluginHost) {
|
|
127
|
+
// Register a slash command
|
|
128
|
+
host.commands.register({ name: 'my-cmd' }, {
|
|
129
|
+
name: 'my-cmd',
|
|
130
|
+
execute(args: string[]) {
|
|
131
|
+
host.logger.info(`my-cmd called with: ${args}`);
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Subscribe to agent lifecycle events
|
|
136
|
+
host.eventRegistry.subscribe('agent.*', (event, detail) => {
|
|
137
|
+
host.logger.info(`Agent event: ${event}`, detail);
|
|
138
|
+
});
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
onUnload(host: PluginHost) {
|
|
142
|
+
host.eventRegistry.unsubscribeAll();
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### 2. Write the manifest
|
|
148
|
+
|
|
149
|
+
Every plugin needs a `plugin.yaml` declaring its trust level and capabilities:
|
|
150
|
+
|
|
151
|
+
```yaml
|
|
152
|
+
# plugin.yaml
|
|
153
|
+
name: my-plugin
|
|
154
|
+
version: 1.0.0
|
|
155
|
+
description: Example Spur plugin
|
|
156
|
+
author: your-name
|
|
157
|
+
trust: local
|
|
158
|
+
capabilities:
|
|
159
|
+
commands:
|
|
160
|
+
- my-cmd
|
|
161
|
+
events:
|
|
162
|
+
- agent.run.start
|
|
163
|
+
- agent.run.complete
|
|
164
|
+
config:
|
|
165
|
+
greeting: hello
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Validate the manifest programmatically:
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { validateManifest, PluginManifestError } from '@gobing-ai/spur-plugin-sdk';
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const manifest = validateManifest(parsedYaml);
|
|
175
|
+
// Use manifest as a typed PluginManifest
|
|
176
|
+
} catch (err) {
|
|
177
|
+
if (err instanceof PluginManifestError) {
|
|
178
|
+
console.error('Invalid manifest:', err.issues);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### 3. Configuration merging
|
|
184
|
+
|
|
185
|
+
Plugin configuration merges three layers (lowest to highest precedence):
|
|
186
|
+
|
|
187
|
+
1. `plugin.yaml` `config:` defaults
|
|
188
|
+
2. `.spur/plugins/<name>.yaml` file overrides
|
|
189
|
+
3. `SPUR_PLUGIN_<NAME>_<KEY>` environment variables
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
import { mergePluginConfig } from '@gobing-ai/spur-plugin-sdk';
|
|
193
|
+
|
|
194
|
+
const config = mergePluginConfig(
|
|
195
|
+
{ greeting: 'hello', timeout: 30 }, // defaults from plugin.yaml
|
|
196
|
+
{ timeout: 60 }, // .spur/plugins/my-plugin.yaml overrides
|
|
197
|
+
process.env, // env vars (SPUR_PLUGIN_MY_PLUGIN_*)
|
|
198
|
+
'my-plugin',
|
|
199
|
+
);
|
|
200
|
+
// => { greeting: 'hello', timeout: <from env or 60> }
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Environment variables override everything. `SPUR_PLUGIN_MY_PLUGIN_TIMEOUT=90` sets `config.timeout` to `90`. Values are JSON-parsed when possible (`30` becomes a number, `true` becomes boolean), falling back to raw strings.
|
|
204
|
+
|
|
205
|
+
## Trust Levels & Policy
|
|
206
|
+
|
|
207
|
+
Every plugin declares a trust level in its manifest. The `TrustEngine` enforces capability access at registration time.
|
|
208
|
+
|
|
209
|
+
| Trust Level | Allowed Capabilities | Typical Use |
|
|
210
|
+
|---|---|---|
|
|
211
|
+
| `bundled` | Everything (unconditional) | Built-ins shipped with Spur |
|
|
212
|
+
| `curated` | Everything | Vetted, externally-reviewed plugins |
|
|
213
|
+
| `local` | `commands`, `api`, `ui`, `events`, `skills` | Project-local plugins |
|
|
214
|
+
| `untrusted` | `commands`, `api`, `ui`, `events`, `skills` | Downloaded plugins |
|
|
215
|
+
|
|
216
|
+
Capabilities denied to `local`/`untrusted`: `harnesses`, `providers`, `rules`, `workers`.
|
|
217
|
+
|
|
218
|
+
Attempting to register a capability beyond your trust level throws `PluginTrustError`. Attempting to register a capability not declared in the manifest throws `PluginNotDeclaredError`.
|
|
219
|
+
|
|
220
|
+
## Capability Registries
|
|
221
|
+
|
|
222
|
+
Each registry is typed — `register()` accepts a capability-specific `TImpl`:
|
|
223
|
+
|
|
224
|
+
| Registry | Purpose | TImpl |
|
|
225
|
+
|---|---|---|
|
|
226
|
+
| `CommandRegistry` | Slash commands | `{ name, execute(args) }` |
|
|
227
|
+
| `ApiRegistry` | Server API routes | `{ name, handler(req), openApi? }` |
|
|
228
|
+
| `UiRegistry` | UI components | `{ name, component }` |
|
|
229
|
+
| `EventRegistry` (plugin) | Domains events | `{ name, handlers }` |
|
|
230
|
+
| `HarnessRegistry` | Agent harnesses | `{ name, harness }` |
|
|
231
|
+
| `ProviderRegistry` | LLM/model providers | `{ name, provider }` |
|
|
232
|
+
| `RuleRegistry` | Validation rules | `{ name, rule }` |
|
|
233
|
+
| `SkillRegistry` | Agent skills | `{ name, skill }` |
|
|
234
|
+
| `WorkerRegistry` | Background workers | `{ name, worker }` |
|
|
235
|
+
|
|
236
|
+
All registries share a common `Registry<T>` base with collision detection (`PluginCollisionError` on duplicate `capability:name` pairs) and trust enforcement via `TrustEngine.check()`.
|
|
237
|
+
|
|
238
|
+
## Event System
|
|
239
|
+
|
|
240
|
+
The `EventRegistry` wraps `@gobing-ai/ts-infra`'s `EventBus` with glob-pattern subscriptions:
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
// Subscribe to all agent events
|
|
244
|
+
host.eventRegistry.subscribe('agent.*', (event, detail) => { ... });
|
|
245
|
+
|
|
246
|
+
// Subscribe to everything (use sparingly)
|
|
247
|
+
host.eventRegistry.subscribe('*', (event, detail) => { ... });
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Known events** (from `SpurEventMap`):
|
|
251
|
+
|
|
252
|
+
| Event | Payload |
|
|
253
|
+
|---|---|
|
|
254
|
+
| `agent.run.start` | `{ agent, prompt, cwd? }` |
|
|
255
|
+
| `agent.run.complete` | `{ agent, exitCode, durationMs }` |
|
|
256
|
+
| `agent.run.error` | `{ agent, error }` |
|
|
257
|
+
| `workflow.transition` | `{ workflowId, from, to }` |
|
|
258
|
+
| `rule.evaluate` | `{ ruleId, result }` |
|
|
259
|
+
| `usage.record` | `{ tokens, model, timestamp }` |
|
|
260
|
+
| `history.import.start` | `{ source }` |
|
|
261
|
+
| `history.import.complete` | `{ source, count }` |
|
|
262
|
+
| `plugin.load` | `{ name, version }` |
|
|
263
|
+
| `plugin.unload` | `{ name }` |
|
|
264
|
+
| `plugin.error` | `{ name, error }` |
|
|
265
|
+
|
|
266
|
+
High-churn events (`usage.record`) have a built-in token-bucket throttle.
|
|
267
|
+
|
|
268
|
+
## Within Spur (internal usage)
|
|
269
|
+
|
|
270
|
+
Spur's own rule engine and workflow engine register as plugins through this SDK:
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
// apps/server/src/plugins.ts
|
|
274
|
+
import { PluginHost } from '@gobing-ai/spur-plugin-sdk';
|
|
275
|
+
|
|
276
|
+
const host = new PluginHost(bus, { logger });
|
|
277
|
+
await host.loadPlugin(ruleEnginePlugin, { source: 'builtin', pluginName: 'spur-rule-engine', trustLevel: 'bundled' });
|
|
278
|
+
await host.loadPlugin(workflowEnginePlugin, { source: 'builtin', pluginName: 'spur-workflow-engine', trustLevel: 'bundled' });
|
|
279
|
+
|
|
280
|
+
// Built-in commands use seedBuiltin before loading any external plugins
|
|
281
|
+
host.commands.seedBuiltin('spur-rule-run', { name: 'spur-rule-run', execute: ruleCommandHandler });
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
External plugins are discovered via `PluginLoader` (in `@gobing-ai/spur-app`) from three roots:
|
|
285
|
+
1. **Bundled** — `packages/plugin-sdk/src/builtins/`
|
|
286
|
+
2. **User-global** — `~/.spur/plugins/`
|
|
287
|
+
3. **Project-local** — `.spur/plugins/`
|
|
288
|
+
|
|
289
|
+
## Error Handling
|
|
290
|
+
|
|
291
|
+
| Error | When |
|
|
292
|
+
|---|---|
|
|
293
|
+
| `PluginManifestError` | `plugin.yaml` fails Zod validation |
|
|
294
|
+
| `PluginCollisionError` | Two plugins register the same `capability:name` pair |
|
|
295
|
+
| `PluginTrustError` | Capability registration denied by trust policy |
|
|
296
|
+
| `PluginNotDeclaredError` | Capability not declared in `plugin.yaml` manifest |
|
package/package.json
CHANGED