elsabro 2.2.0 → 3.7.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 +668 -20
- package/agents/elsabro-orchestrator.md +113 -0
- package/bin/install.js +0 -0
- package/commands/elsabro/execute.md +223 -46
- package/commands/elsabro/start.md +34 -0
- package/commands/elsabro/verify-work.md +29 -0
- package/flows/development-flow.json +452 -0
- package/flows/quick-flow.json +118 -0
- package/hooks/confirm-destructive.sh +145 -0
- package/hooks/hooks-config.json +81 -0
- package/hooks/lint-check.sh +238 -0
- package/hooks/post-edit-test.sh +189 -0
- package/package.json +5 -3
- package/references/SYSTEM_INDEX.md +379 -5
- package/references/agent-marketplace.md +2274 -0
- package/references/agent-protocol.md +1126 -0
- package/references/ai-code-suggestions.md +2413 -0
- package/references/checkpointing.md +595 -0
- package/references/collaboration-patterns.md +851 -0
- package/references/collaborative-sessions.md +1081 -0
- package/references/configuration-management.md +1810 -0
- package/references/cost-tracking.md +1095 -0
- package/references/enterprise-sso.md +2001 -0
- package/references/error-contracts-tests.md +1171 -0
- package/references/error-contracts-v2.md +968 -0
- package/references/error-contracts.md +3102 -0
- package/references/event-driven.md +1031 -0
- package/references/flow-orchestration.md +940 -0
- package/references/flow-visualization.md +1557 -0
- package/references/ide-integrations.md +3513 -0
- package/references/interrupt-system.md +681 -0
- package/references/kubernetes-deployment.md +3099 -0
- package/references/memory-system.md +683 -0
- package/references/mobile-companion.md +3236 -0
- package/references/multi-llm-providers.md +2494 -0
- package/references/multi-project-memory.md +1182 -0
- package/references/observability.md +793 -0
- package/references/output-schemas.md +858 -0
- package/references/parallel-worktrees.md +293 -0
- package/references/performance-profiler.md +955 -0
- package/references/plugin-system.md +1526 -0
- package/references/prompt-management.md +292 -0
- package/references/sandbox-execution.md +303 -0
- package/references/security-system.md +1253 -0
- package/references/streaming.md +696 -0
- package/references/testing-framework.md +1151 -0
- package/references/time-travel.md +802 -0
- package/references/tool-registry.md +886 -0
- package/references/voice-commands.md +3296 -0
- package/scripts/setup-parallel-worktrees.sh +319 -0
- package/skills/memory-update.md +207 -0
- package/skills/review.md +331 -0
- package/skills/techdebt.md +289 -0
- package/skills/tutor.md +219 -0
- package/templates/.planning/notes/.gitkeep +0 -0
- package/templates/CLAUDE.md.template +48 -0
- package/templates/agent-marketplace-config.json +220 -0
- package/templates/agent-protocol-config.json +136 -0
- package/templates/ai-suggestions-config.json +100 -0
- package/templates/checkpoint-state.json +61 -0
- package/templates/collaboration-config.json +157 -0
- package/templates/collaborative-sessions-config.json +153 -0
- package/templates/configuration-config.json +245 -0
- package/templates/cost-tracking-config.json +148 -0
- package/templates/enterprise-sso-config.json +438 -0
- package/templates/error-handling-config.json +79 -2
- package/templates/events-config.json +148 -0
- package/templates/flow-visualization-config.json +196 -0
- package/templates/ide-integrations-config.json +442 -0
- package/templates/kubernetes-config.json +764 -0
- package/templates/memory-state.json +84 -0
- package/templates/mistakes.md.template +52 -0
- package/templates/mobile-companion-config.json +600 -0
- package/templates/multi-llm-config.json +544 -0
- package/templates/multi-project-memory-config.json +145 -0
- package/templates/observability-config.json +109 -0
- package/templates/patterns.md.template +114 -0
- package/templates/performance-profiler-config.json +125 -0
- package/templates/plugin-config.json +170 -0
- package/templates/prompt-management-config.json +86 -0
- package/templates/sandbox-config.json +185 -0
- package/templates/schemas-config.json +65 -0
- package/templates/security-config.json +120 -0
- package/templates/streaming-config.json +72 -0
- package/templates/testing-config.json +81 -0
- package/templates/timetravel-config.json +62 -0
- package/templates/tool-registry-config.json +109 -0
- package/templates/voice-commands-config.json +658 -0
|
@@ -0,0 +1,1526 @@
|
|
|
1
|
+
# ELSABRO Plugin System
|
|
2
|
+
|
|
3
|
+
> Sistema de plugins extensible con carga dinámica, aislamiento y gestión de lifecycle.
|
|
4
|
+
|
|
5
|
+
## Arquitectura General
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
9
|
+
│ Plugin System │
|
|
10
|
+
├─────────────────────────────────────────────────────────────────────────┤
|
|
11
|
+
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
|
12
|
+
│ │ PluginLoader │ │ PluginRegistry │ │ PluginManager │ │
|
|
13
|
+
│ │ ───────────── │ │ ───────────── │ │ ───────────── │ │
|
|
14
|
+
│ │ • Discovery │ │ • Registration │ │ • Lifecycle │ │
|
|
15
|
+
│ │ • Validation │ │ • Dependencies │ │ • Hot-reload │ │
|
|
16
|
+
│ │ • Isolation │ │ • Versioning │ │ • Health │ │
|
|
17
|
+
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
|
18
|
+
│ │ │
|
|
19
|
+
│ ┌───────────────────────────┴───────────────────────────────┐ │
|
|
20
|
+
│ │ HookSystem │ │
|
|
21
|
+
│ │ • Before/After hooks • Middleware • Event propagation │ │
|
|
22
|
+
│ └────────────────────────────────────────────────────────────┘ │
|
|
23
|
+
│ │ │
|
|
24
|
+
│ ┌───────────────────────────┴───────────────────────────────┐ │
|
|
25
|
+
│ │ PluginContext │ │
|
|
26
|
+
│ │ • Sandboxed APIs • Resource limits • Permissions │ │
|
|
27
|
+
│ └────────────────────────────────────────────────────────────┘ │
|
|
28
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 1. Plugin Definition
|
|
34
|
+
|
|
35
|
+
### Estructura de Plugin
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
interface PluginManifest {
|
|
39
|
+
// Identity
|
|
40
|
+
name: string; // Unique identifier (e.g., "elsabro-git-plugin")
|
|
41
|
+
version: string; // Semver (e.g., "1.2.3")
|
|
42
|
+
displayName: string; // Human-readable name
|
|
43
|
+
description: string;
|
|
44
|
+
author: string | { name: string; email?: string; url?: string };
|
|
45
|
+
license: string;
|
|
46
|
+
|
|
47
|
+
// Entry points
|
|
48
|
+
main: string; // Main entry file
|
|
49
|
+
types?: string; // TypeScript definitions
|
|
50
|
+
|
|
51
|
+
// Dependencies
|
|
52
|
+
engines: {
|
|
53
|
+
elsabro: string; // Required ELSABRO version (semver range)
|
|
54
|
+
node?: string; // Node.js version
|
|
55
|
+
};
|
|
56
|
+
dependencies?: Record<string, string>; // npm packages
|
|
57
|
+
pluginDependencies?: Record<string, string>; // Other ELSABRO plugins
|
|
58
|
+
|
|
59
|
+
// Capabilities
|
|
60
|
+
contributes?: {
|
|
61
|
+
agents?: AgentContribution[];
|
|
62
|
+
tools?: ToolContribution[];
|
|
63
|
+
commands?: CommandContribution[];
|
|
64
|
+
hooks?: HookContribution[];
|
|
65
|
+
config?: ConfigContribution;
|
|
66
|
+
views?: ViewContribution[];
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Permissions
|
|
70
|
+
permissions?: PluginPermission[];
|
|
71
|
+
|
|
72
|
+
// Activation
|
|
73
|
+
activationEvents?: string[]; // When to activate (e.g., "onCommand:myCmd")
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type PluginPermission =
|
|
77
|
+
| 'filesystem:read'
|
|
78
|
+
| 'filesystem:write'
|
|
79
|
+
| 'network'
|
|
80
|
+
| 'shell'
|
|
81
|
+
| 'env'
|
|
82
|
+
| 'secrets'
|
|
83
|
+
| 'config:read'
|
|
84
|
+
| 'config:write';
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Plugin Interface
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
interface Plugin {
|
|
91
|
+
// Lifecycle
|
|
92
|
+
activate(context: PluginContext): Promise<void>;
|
|
93
|
+
deactivate?(): Promise<void>;
|
|
94
|
+
|
|
95
|
+
// Optional exports
|
|
96
|
+
api?: Record<string, unknown>; // Public API for other plugins
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface PluginContext {
|
|
100
|
+
// Plugin info
|
|
101
|
+
readonly pluginPath: string;
|
|
102
|
+
readonly manifest: PluginManifest;
|
|
103
|
+
|
|
104
|
+
// Storage
|
|
105
|
+
readonly globalState: Memento;
|
|
106
|
+
readonly workspaceState: Memento;
|
|
107
|
+
readonly secrets: SecretsStorage;
|
|
108
|
+
|
|
109
|
+
// Subscriptions (auto-disposed on deactivate)
|
|
110
|
+
subscriptions: Disposable[];
|
|
111
|
+
|
|
112
|
+
// Logging
|
|
113
|
+
readonly logger: Logger;
|
|
114
|
+
|
|
115
|
+
// Extension APIs
|
|
116
|
+
readonly elsabro: ElsabroAPI;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
interface Memento {
|
|
120
|
+
get<T>(key: string, defaultValue?: T): T;
|
|
121
|
+
update(key: string, value: unknown): Promise<void>;
|
|
122
|
+
keys(): readonly string[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface Disposable {
|
|
126
|
+
dispose(): void;
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 2. PluginLoader
|
|
133
|
+
|
|
134
|
+
### Propósito
|
|
135
|
+
Descubre, valida y carga plugins desde múltiples fuentes.
|
|
136
|
+
|
|
137
|
+
### Interfaz
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
interface PluginSource {
|
|
141
|
+
type: 'directory' | 'npm' | 'url';
|
|
142
|
+
path?: string;
|
|
143
|
+
package?: string;
|
|
144
|
+
url?: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
interface LoadedPlugin {
|
|
148
|
+
manifest: PluginManifest;
|
|
149
|
+
module: Plugin;
|
|
150
|
+
path: string;
|
|
151
|
+
state: PluginState;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
type PluginState =
|
|
155
|
+
| 'discovered'
|
|
156
|
+
| 'validated'
|
|
157
|
+
| 'loaded'
|
|
158
|
+
| 'activated'
|
|
159
|
+
| 'deactivated'
|
|
160
|
+
| 'error';
|
|
161
|
+
|
|
162
|
+
interface PluginLoader {
|
|
163
|
+
// Discovery
|
|
164
|
+
discover(sources: PluginSource[]): Promise<PluginManifest[]>;
|
|
165
|
+
discoverFromDirectory(path: string): Promise<PluginManifest[]>;
|
|
166
|
+
|
|
167
|
+
// Validation
|
|
168
|
+
validate(manifest: PluginManifest): ValidationResult;
|
|
169
|
+
checkDependencies(manifest: PluginManifest): DependencyResult;
|
|
170
|
+
|
|
171
|
+
// Loading
|
|
172
|
+
load(manifest: PluginManifest): Promise<LoadedPlugin>;
|
|
173
|
+
loadAll(manifests: PluginManifest[]): Promise<LoadedPlugin[]>;
|
|
174
|
+
|
|
175
|
+
// Isolation
|
|
176
|
+
createSandbox(plugin: LoadedPlugin): PluginSandbox;
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Implementación
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
class PluginLoaderImpl implements PluginLoader {
|
|
184
|
+
private loadedPlugins: Map<string, LoadedPlugin> = new Map();
|
|
185
|
+
|
|
186
|
+
constructor(
|
|
187
|
+
private config: ConfigManager,
|
|
188
|
+
private registry: PluginRegistry
|
|
189
|
+
) {}
|
|
190
|
+
|
|
191
|
+
async discover(sources: PluginSource[]): Promise<PluginManifest[]> {
|
|
192
|
+
const manifests: PluginManifest[] = [];
|
|
193
|
+
|
|
194
|
+
for (const source of sources) {
|
|
195
|
+
try {
|
|
196
|
+
switch (source.type) {
|
|
197
|
+
case 'directory':
|
|
198
|
+
manifests.push(...await this.discoverFromDirectory(source.path!));
|
|
199
|
+
break;
|
|
200
|
+
case 'npm':
|
|
201
|
+
manifests.push(await this.discoverFromNpm(source.package!));
|
|
202
|
+
break;
|
|
203
|
+
case 'url':
|
|
204
|
+
manifests.push(await this.discoverFromUrl(source.url!));
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
} catch (error) {
|
|
208
|
+
console.warn(`Failed to discover plugins from ${source.type}:`, error);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return manifests;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async discoverFromDirectory(dirPath: string): Promise<PluginManifest[]> {
|
|
216
|
+
const manifests: PluginManifest[] = [];
|
|
217
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
218
|
+
|
|
219
|
+
for (const entry of entries) {
|
|
220
|
+
if (!entry.isDirectory()) continue;
|
|
221
|
+
|
|
222
|
+
const pluginPath = path.join(dirPath, entry.name);
|
|
223
|
+
const manifestPath = path.join(pluginPath, 'plugin.json');
|
|
224
|
+
|
|
225
|
+
if (await this.fileExists(manifestPath)) {
|
|
226
|
+
try {
|
|
227
|
+
const content = await fs.readFile(manifestPath, 'utf-8');
|
|
228
|
+
const manifest = JSON.parse(content) as PluginManifest;
|
|
229
|
+
manifest._path = pluginPath;
|
|
230
|
+
manifests.push(manifest);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
console.warn(`Invalid plugin manifest at ${manifestPath}:`, error);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return manifests;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private async discoverFromNpm(packageName: string): Promise<PluginManifest> {
|
|
241
|
+
const packagePath = require.resolve(`${packageName}/plugin.json`);
|
|
242
|
+
const content = await fs.readFile(packagePath, 'utf-8');
|
|
243
|
+
const manifest = JSON.parse(content);
|
|
244
|
+
manifest._path = path.dirname(packagePath);
|
|
245
|
+
return manifest;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
validate(manifest: PluginManifest): ValidationResult {
|
|
249
|
+
const errors: string[] = [];
|
|
250
|
+
const warnings: string[] = [];
|
|
251
|
+
|
|
252
|
+
// Required fields
|
|
253
|
+
if (!manifest.name) errors.push('Missing required field: name');
|
|
254
|
+
if (!manifest.version) errors.push('Missing required field: version');
|
|
255
|
+
if (!manifest.main) errors.push('Missing required field: main');
|
|
256
|
+
|
|
257
|
+
// Version format
|
|
258
|
+
if (manifest.version && !semver.valid(manifest.version)) {
|
|
259
|
+
errors.push(`Invalid version format: ${manifest.version}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Engine compatibility
|
|
263
|
+
if (manifest.engines?.elsabro) {
|
|
264
|
+
const elsabroVersion = this.config.get<string>('version');
|
|
265
|
+
if (!semver.satisfies(elsabroVersion, manifest.engines.elsabro)) {
|
|
266
|
+
errors.push(
|
|
267
|
+
`Incompatible ELSABRO version. Required: ${manifest.engines.elsabro}, Current: ${elsabroVersion}`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Permissions validation
|
|
273
|
+
if (manifest.permissions) {
|
|
274
|
+
const validPermissions = [
|
|
275
|
+
'filesystem:read', 'filesystem:write', 'network',
|
|
276
|
+
'shell', 'env', 'secrets', 'config:read', 'config:write'
|
|
277
|
+
];
|
|
278
|
+
for (const perm of manifest.permissions) {
|
|
279
|
+
if (!validPermissions.includes(perm)) {
|
|
280
|
+
warnings.push(`Unknown permission: ${perm}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
valid: errors.length === 0,
|
|
287
|
+
errors,
|
|
288
|
+
warnings
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
checkDependencies(manifest: PluginManifest): DependencyResult {
|
|
293
|
+
const missing: string[] = [];
|
|
294
|
+
const incompatible: Array<{ name: string; required: string; installed: string }> = [];
|
|
295
|
+
|
|
296
|
+
// Check plugin dependencies
|
|
297
|
+
if (manifest.pluginDependencies) {
|
|
298
|
+
for (const [name, versionRange] of Object.entries(manifest.pluginDependencies)) {
|
|
299
|
+
const installed = this.registry.get(name);
|
|
300
|
+
if (!installed) {
|
|
301
|
+
missing.push(name);
|
|
302
|
+
} else if (!semver.satisfies(installed.manifest.version, versionRange)) {
|
|
303
|
+
incompatible.push({
|
|
304
|
+
name,
|
|
305
|
+
required: versionRange,
|
|
306
|
+
installed: installed.manifest.version
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
satisfied: missing.length === 0 && incompatible.length === 0,
|
|
314
|
+
missing,
|
|
315
|
+
incompatible
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async load(manifest: PluginManifest): Promise<LoadedPlugin> {
|
|
320
|
+
const validation = this.validate(manifest);
|
|
321
|
+
if (!validation.valid) {
|
|
322
|
+
throw new PluginLoadError(manifest.name, validation.errors);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const depCheck = this.checkDependencies(manifest);
|
|
326
|
+
if (!depCheck.satisfied) {
|
|
327
|
+
throw new PluginDependencyError(manifest.name, depCheck);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Load the plugin module
|
|
331
|
+
const mainPath = path.join(manifest._path!, manifest.main);
|
|
332
|
+
const module = await import(mainPath);
|
|
333
|
+
|
|
334
|
+
const plugin: LoadedPlugin = {
|
|
335
|
+
manifest,
|
|
336
|
+
module: module.default || module,
|
|
337
|
+
path: manifest._path!,
|
|
338
|
+
state: 'loaded'
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
this.loadedPlugins.set(manifest.name, plugin);
|
|
342
|
+
return plugin;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async loadAll(manifests: PluginManifest[]): Promise<LoadedPlugin[]> {
|
|
346
|
+
// Sort by dependencies
|
|
347
|
+
const sorted = this.topologicalSort(manifests);
|
|
348
|
+
|
|
349
|
+
const loaded: LoadedPlugin[] = [];
|
|
350
|
+
for (const manifest of sorted) {
|
|
351
|
+
try {
|
|
352
|
+
const plugin = await this.load(manifest);
|
|
353
|
+
loaded.push(plugin);
|
|
354
|
+
} catch (error) {
|
|
355
|
+
console.error(`Failed to load plugin ${manifest.name}:`, error);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return loaded;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private topologicalSort(manifests: PluginManifest[]): PluginManifest[] {
|
|
363
|
+
const graph = new Map<string, Set<string>>();
|
|
364
|
+
const manifestMap = new Map<string, PluginManifest>();
|
|
365
|
+
|
|
366
|
+
// Build dependency graph
|
|
367
|
+
for (const manifest of manifests) {
|
|
368
|
+
manifestMap.set(manifest.name, manifest);
|
|
369
|
+
graph.set(manifest.name, new Set(
|
|
370
|
+
Object.keys(manifest.pluginDependencies || {})
|
|
371
|
+
));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Kahn's algorithm
|
|
375
|
+
const result: PluginManifest[] = [];
|
|
376
|
+
const noDeps = manifests.filter(m => (graph.get(m.name)?.size || 0) === 0);
|
|
377
|
+
|
|
378
|
+
while (noDeps.length > 0) {
|
|
379
|
+
const current = noDeps.shift()!;
|
|
380
|
+
result.push(current);
|
|
381
|
+
|
|
382
|
+
for (const [name, deps] of graph) {
|
|
383
|
+
if (deps.has(current.name)) {
|
|
384
|
+
deps.delete(current.name);
|
|
385
|
+
if (deps.size === 0) {
|
|
386
|
+
noDeps.push(manifestMap.get(name)!);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Check for cycles
|
|
393
|
+
if (result.length !== manifests.length) {
|
|
394
|
+
throw new Error('Circular dependency detected in plugins');
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return result;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
createSandbox(plugin: LoadedPlugin): PluginSandbox {
|
|
401
|
+
return new PluginSandbox(plugin, {
|
|
402
|
+
permissions: plugin.manifest.permissions || [],
|
|
403
|
+
resourceLimits: this.config.get('plugins.resourceLimits', {
|
|
404
|
+
memory: 128 * 1024 * 1024, // 128MB
|
|
405
|
+
cpu: 0.5
|
|
406
|
+
})
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private async fileExists(path: string): Promise<boolean> {
|
|
411
|
+
try {
|
|
412
|
+
await fs.access(path);
|
|
413
|
+
return true;
|
|
414
|
+
} catch {
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## 3. PluginRegistry
|
|
424
|
+
|
|
425
|
+
### Propósito
|
|
426
|
+
Mantiene el registro de plugins instalados y sus contribuciones.
|
|
427
|
+
|
|
428
|
+
### Interfaz
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
interface PluginRegistry {
|
|
432
|
+
// Registration
|
|
433
|
+
register(plugin: LoadedPlugin): void;
|
|
434
|
+
unregister(name: string): void;
|
|
435
|
+
|
|
436
|
+
// Queries
|
|
437
|
+
get(name: string): LoadedPlugin | undefined;
|
|
438
|
+
getAll(): LoadedPlugin[];
|
|
439
|
+
getByState(state: PluginState): LoadedPlugin[];
|
|
440
|
+
find(predicate: (plugin: LoadedPlugin) => boolean): LoadedPlugin[];
|
|
441
|
+
|
|
442
|
+
// Contributions
|
|
443
|
+
getAgents(): AgentContribution[];
|
|
444
|
+
getTools(): ToolContribution[];
|
|
445
|
+
getCommands(): CommandContribution[];
|
|
446
|
+
getHooks(point: string): HookContribution[];
|
|
447
|
+
|
|
448
|
+
// Dependencies
|
|
449
|
+
getDependents(name: string): LoadedPlugin[];
|
|
450
|
+
getDependencies(name: string): LoadedPlugin[];
|
|
451
|
+
|
|
452
|
+
// Events
|
|
453
|
+
onRegister(callback: (plugin: LoadedPlugin) => void): Disposable;
|
|
454
|
+
onUnregister(callback: (name: string) => void): Disposable;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
interface AgentContribution {
|
|
458
|
+
pluginName: string;
|
|
459
|
+
name: string;
|
|
460
|
+
displayName: string;
|
|
461
|
+
description: string;
|
|
462
|
+
model?: string;
|
|
463
|
+
systemPrompt?: string;
|
|
464
|
+
tools?: string[];
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
interface ToolContribution {
|
|
468
|
+
pluginName: string;
|
|
469
|
+
name: string;
|
|
470
|
+
displayName: string;
|
|
471
|
+
description: string;
|
|
472
|
+
inputSchema: Record<string, unknown>;
|
|
473
|
+
handler: string; // Reference to handler function
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
interface CommandContribution {
|
|
477
|
+
pluginName: string;
|
|
478
|
+
command: string;
|
|
479
|
+
title: string;
|
|
480
|
+
description?: string;
|
|
481
|
+
category?: string;
|
|
482
|
+
handler: string;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
interface HookContribution {
|
|
486
|
+
pluginName: string;
|
|
487
|
+
point: string;
|
|
488
|
+
priority: number;
|
|
489
|
+
handler: string;
|
|
490
|
+
}
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### Implementación
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
class PluginRegistryImpl implements PluginRegistry {
|
|
497
|
+
private plugins: Map<string, LoadedPlugin> = new Map();
|
|
498
|
+
private contributions = {
|
|
499
|
+
agents: new Map<string, AgentContribution>(),
|
|
500
|
+
tools: new Map<string, ToolContribution>(),
|
|
501
|
+
commands: new Map<string, CommandContribution>(),
|
|
502
|
+
hooks: new Map<string, HookContribution[]>()
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
private registerCallbacks: Set<Function> = new Set();
|
|
506
|
+
private unregisterCallbacks: Set<Function> = new Set();
|
|
507
|
+
|
|
508
|
+
register(plugin: LoadedPlugin): void {
|
|
509
|
+
const { manifest } = plugin;
|
|
510
|
+
|
|
511
|
+
// Check for duplicates
|
|
512
|
+
if (this.plugins.has(manifest.name)) {
|
|
513
|
+
throw new Error(`Plugin already registered: ${manifest.name}`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
this.plugins.set(manifest.name, plugin);
|
|
517
|
+
|
|
518
|
+
// Register contributions
|
|
519
|
+
if (manifest.contributes) {
|
|
520
|
+
this.registerContributions(manifest.name, manifest.contributes);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Notify listeners
|
|
524
|
+
this.registerCallbacks.forEach(cb => cb(plugin));
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
unregister(name: string): void {
|
|
528
|
+
const plugin = this.plugins.get(name);
|
|
529
|
+
if (!plugin) return;
|
|
530
|
+
|
|
531
|
+
// Check for dependents
|
|
532
|
+
const dependents = this.getDependents(name);
|
|
533
|
+
if (dependents.length > 0) {
|
|
534
|
+
throw new Error(
|
|
535
|
+
`Cannot unregister ${name}: required by ${dependents.map(p => p.manifest.name).join(', ')}`
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Remove contributions
|
|
540
|
+
this.removeContributions(name);
|
|
541
|
+
|
|
542
|
+
this.plugins.delete(name);
|
|
543
|
+
this.unregisterCallbacks.forEach(cb => cb(name));
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
private registerContributions(pluginName: string, contributes: PluginManifest['contributes']): void {
|
|
547
|
+
if (contributes.agents) {
|
|
548
|
+
for (const agent of contributes.agents) {
|
|
549
|
+
this.contributions.agents.set(agent.name, { ...agent, pluginName });
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (contributes.tools) {
|
|
554
|
+
for (const tool of contributes.tools) {
|
|
555
|
+
this.contributions.tools.set(tool.name, { ...tool, pluginName });
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (contributes.commands) {
|
|
560
|
+
for (const cmd of contributes.commands) {
|
|
561
|
+
this.contributions.commands.set(cmd.command, { ...cmd, pluginName });
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (contributes.hooks) {
|
|
566
|
+
for (const hook of contributes.hooks) {
|
|
567
|
+
const hookWithPlugin = { ...hook, pluginName };
|
|
568
|
+
if (!this.contributions.hooks.has(hook.point)) {
|
|
569
|
+
this.contributions.hooks.set(hook.point, []);
|
|
570
|
+
}
|
|
571
|
+
this.contributions.hooks.get(hook.point)!.push(hookWithPlugin);
|
|
572
|
+
// Sort by priority
|
|
573
|
+
this.contributions.hooks.get(hook.point)!.sort((a, b) => a.priority - b.priority);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
private removeContributions(pluginName: string): void {
|
|
579
|
+
// Remove agents
|
|
580
|
+
for (const [name, agent] of this.contributions.agents) {
|
|
581
|
+
if (agent.pluginName === pluginName) {
|
|
582
|
+
this.contributions.agents.delete(name);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Remove tools
|
|
587
|
+
for (const [name, tool] of this.contributions.tools) {
|
|
588
|
+
if (tool.pluginName === pluginName) {
|
|
589
|
+
this.contributions.tools.delete(name);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Remove commands
|
|
594
|
+
for (const [name, cmd] of this.contributions.commands) {
|
|
595
|
+
if (cmd.pluginName === pluginName) {
|
|
596
|
+
this.contributions.commands.delete(name);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Remove hooks
|
|
601
|
+
for (const [point, hooks] of this.contributions.hooks) {
|
|
602
|
+
this.contributions.hooks.set(
|
|
603
|
+
point,
|
|
604
|
+
hooks.filter(h => h.pluginName !== pluginName)
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
get(name: string): LoadedPlugin | undefined {
|
|
610
|
+
return this.plugins.get(name);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
getAll(): LoadedPlugin[] {
|
|
614
|
+
return Array.from(this.plugins.values());
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
getByState(state: PluginState): LoadedPlugin[] {
|
|
618
|
+
return this.getAll().filter(p => p.state === state);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
find(predicate: (plugin: LoadedPlugin) => boolean): LoadedPlugin[] {
|
|
622
|
+
return this.getAll().filter(predicate);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
getAgents(): AgentContribution[] {
|
|
626
|
+
return Array.from(this.contributions.agents.values());
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
getTools(): ToolContribution[] {
|
|
630
|
+
return Array.from(this.contributions.tools.values());
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
getCommands(): CommandContribution[] {
|
|
634
|
+
return Array.from(this.contributions.commands.values());
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
getHooks(point: string): HookContribution[] {
|
|
638
|
+
return this.contributions.hooks.get(point) || [];
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
getDependents(name: string): LoadedPlugin[] {
|
|
642
|
+
return this.getAll().filter(plugin => {
|
|
643
|
+
const deps = plugin.manifest.pluginDependencies || {};
|
|
644
|
+
return name in deps;
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
getDependencies(name: string): LoadedPlugin[] {
|
|
649
|
+
const plugin = this.plugins.get(name);
|
|
650
|
+
if (!plugin) return [];
|
|
651
|
+
|
|
652
|
+
const deps = plugin.manifest.pluginDependencies || {};
|
|
653
|
+
return Object.keys(deps)
|
|
654
|
+
.map(depName => this.plugins.get(depName))
|
|
655
|
+
.filter((p): p is LoadedPlugin => p !== undefined);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
onRegister(callback: Function): Disposable {
|
|
659
|
+
this.registerCallbacks.add(callback);
|
|
660
|
+
return { dispose: () => this.registerCallbacks.delete(callback) };
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
onUnregister(callback: Function): Disposable {
|
|
664
|
+
this.unregisterCallbacks.add(callback);
|
|
665
|
+
return { dispose: () => this.unregisterCallbacks.delete(callback) };
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
---
|
|
671
|
+
|
|
672
|
+
## 4. PluginManager
|
|
673
|
+
|
|
674
|
+
### Propósito
|
|
675
|
+
Gestiona el lifecycle completo de plugins incluyendo activación, desactivación y hot-reload.
|
|
676
|
+
|
|
677
|
+
### Interfaz
|
|
678
|
+
|
|
679
|
+
```typescript
|
|
680
|
+
interface PluginManager {
|
|
681
|
+
// Lifecycle
|
|
682
|
+
activate(name: string): Promise<void>;
|
|
683
|
+
activateAll(): Promise<void>;
|
|
684
|
+
deactivate(name: string): Promise<void>;
|
|
685
|
+
deactivateAll(): Promise<void>;
|
|
686
|
+
reload(name: string): Promise<void>;
|
|
687
|
+
|
|
688
|
+
// Installation
|
|
689
|
+
install(source: PluginSource): Promise<LoadedPlugin>;
|
|
690
|
+
uninstall(name: string): Promise<void>;
|
|
691
|
+
update(name: string): Promise<LoadedPlugin>;
|
|
692
|
+
|
|
693
|
+
// Status
|
|
694
|
+
getStatus(name: string): PluginStatus;
|
|
695
|
+
getAllStatus(): Map<string, PluginStatus>;
|
|
696
|
+
isActive(name: string): boolean;
|
|
697
|
+
|
|
698
|
+
// Hot-reload
|
|
699
|
+
enableHotReload(name: string): void;
|
|
700
|
+
disableHotReload(name: string): void;
|
|
701
|
+
|
|
702
|
+
// Events
|
|
703
|
+
onActivate(callback: (plugin: LoadedPlugin) => void): Disposable;
|
|
704
|
+
onDeactivate(callback: (name: string) => void): Disposable;
|
|
705
|
+
onError(callback: (error: PluginError) => void): Disposable;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
interface PluginStatus {
|
|
709
|
+
state: PluginState;
|
|
710
|
+
activatedAt?: Date;
|
|
711
|
+
error?: Error;
|
|
712
|
+
resourceUsage?: {
|
|
713
|
+
memory: number;
|
|
714
|
+
cpu: number;
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
### Implementación
|
|
720
|
+
|
|
721
|
+
```typescript
|
|
722
|
+
class PluginManagerImpl implements PluginManager {
|
|
723
|
+
private contexts: Map<string, PluginContext> = new Map();
|
|
724
|
+
private watchers: Map<string, FSWatcher> = new Map();
|
|
725
|
+
private activateCallbacks: Set<Function> = new Set();
|
|
726
|
+
private deactivateCallbacks: Set<Function> = new Set();
|
|
727
|
+
private errorCallbacks: Set<Function> = new Set();
|
|
728
|
+
|
|
729
|
+
constructor(
|
|
730
|
+
private loader: PluginLoader,
|
|
731
|
+
private registry: PluginRegistry,
|
|
732
|
+
private config: ConfigManager
|
|
733
|
+
) {}
|
|
734
|
+
|
|
735
|
+
async activate(name: string): Promise<void> {
|
|
736
|
+
const plugin = this.registry.get(name);
|
|
737
|
+
if (!plugin) {
|
|
738
|
+
throw new Error(`Plugin not found: ${name}`);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (plugin.state === 'activated') {
|
|
742
|
+
return; // Already active
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Activate dependencies first
|
|
746
|
+
const deps = this.registry.getDependencies(name);
|
|
747
|
+
for (const dep of deps) {
|
|
748
|
+
if (dep.state !== 'activated') {
|
|
749
|
+
await this.activate(dep.manifest.name);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Create context
|
|
754
|
+
const context = this.createContext(plugin);
|
|
755
|
+
this.contexts.set(name, context);
|
|
756
|
+
|
|
757
|
+
try {
|
|
758
|
+
// Activate plugin
|
|
759
|
+
await plugin.module.activate(context);
|
|
760
|
+
plugin.state = 'activated';
|
|
761
|
+
|
|
762
|
+
this.activateCallbacks.forEach(cb => cb(plugin));
|
|
763
|
+
} catch (error) {
|
|
764
|
+
plugin.state = 'error';
|
|
765
|
+
this.errorCallbacks.forEach(cb => cb({ plugin: name, error }));
|
|
766
|
+
throw error;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
async activateAll(): Promise<void> {
|
|
771
|
+
const plugins = this.registry.getAll();
|
|
772
|
+
const sorted = this.sortByDependencies(plugins);
|
|
773
|
+
|
|
774
|
+
for (const plugin of sorted) {
|
|
775
|
+
try {
|
|
776
|
+
await this.activate(plugin.manifest.name);
|
|
777
|
+
} catch (error) {
|
|
778
|
+
console.error(`Failed to activate ${plugin.manifest.name}:`, error);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
async deactivate(name: string): Promise<void> {
|
|
784
|
+
const plugin = this.registry.get(name);
|
|
785
|
+
if (!plugin || plugin.state !== 'activated') {
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Check if any active plugins depend on this one
|
|
790
|
+
const dependents = this.registry.getDependents(name);
|
|
791
|
+
const activeDependents = dependents.filter(p => p.state === 'activated');
|
|
792
|
+
if (activeDependents.length > 0) {
|
|
793
|
+
throw new Error(
|
|
794
|
+
`Cannot deactivate ${name}: still required by active plugins: ` +
|
|
795
|
+
activeDependents.map(p => p.manifest.name).join(', ')
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const context = this.contexts.get(name);
|
|
800
|
+
|
|
801
|
+
try {
|
|
802
|
+
// Call deactivate if implemented
|
|
803
|
+
if (plugin.module.deactivate) {
|
|
804
|
+
await plugin.module.deactivate();
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Dispose all subscriptions
|
|
808
|
+
if (context) {
|
|
809
|
+
for (const sub of context.subscriptions) {
|
|
810
|
+
sub.dispose();
|
|
811
|
+
}
|
|
812
|
+
this.contexts.delete(name);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
plugin.state = 'deactivated';
|
|
816
|
+
this.deactivateCallbacks.forEach(cb => cb(name));
|
|
817
|
+
} catch (error) {
|
|
818
|
+
plugin.state = 'error';
|
|
819
|
+
this.errorCallbacks.forEach(cb => cb({ plugin: name, error }));
|
|
820
|
+
throw error;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
async deactivateAll(): Promise<void> {
|
|
825
|
+
const plugins = this.registry.getByState('activated');
|
|
826
|
+
const sorted = this.sortByDependencies(plugins).reverse();
|
|
827
|
+
|
|
828
|
+
for (const plugin of sorted) {
|
|
829
|
+
try {
|
|
830
|
+
await this.deactivate(plugin.manifest.name);
|
|
831
|
+
} catch (error) {
|
|
832
|
+
console.error(`Failed to deactivate ${plugin.manifest.name}:`, error);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
async reload(name: string): Promise<void> {
|
|
838
|
+
const plugin = this.registry.get(name);
|
|
839
|
+
if (!plugin) {
|
|
840
|
+
throw new Error(`Plugin not found: ${name}`);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const wasActive = plugin.state === 'activated';
|
|
844
|
+
|
|
845
|
+
// Deactivate if active
|
|
846
|
+
if (wasActive) {
|
|
847
|
+
await this.deactivate(name);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Unregister old version
|
|
851
|
+
this.registry.unregister(name);
|
|
852
|
+
|
|
853
|
+
// Clear module cache
|
|
854
|
+
this.clearModuleCache(plugin.path);
|
|
855
|
+
|
|
856
|
+
// Reload manifest and module
|
|
857
|
+
const manifests = await this.loader.discoverFromDirectory(
|
|
858
|
+
path.dirname(plugin.path)
|
|
859
|
+
);
|
|
860
|
+
const manifest = manifests.find(m => m.name === name);
|
|
861
|
+
if (!manifest) {
|
|
862
|
+
throw new Error(`Plugin manifest not found after reload: ${name}`);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Load and register
|
|
866
|
+
const reloaded = await this.loader.load(manifest);
|
|
867
|
+
this.registry.register(reloaded);
|
|
868
|
+
|
|
869
|
+
// Reactivate if was active
|
|
870
|
+
if (wasActive) {
|
|
871
|
+
await this.activate(name);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
async install(source: PluginSource): Promise<LoadedPlugin> {
|
|
876
|
+
// Discover plugin
|
|
877
|
+
const manifests = await this.loader.discover([source]);
|
|
878
|
+
if (manifests.length === 0) {
|
|
879
|
+
throw new Error('No plugin found at source');
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const manifest = manifests[0];
|
|
883
|
+
|
|
884
|
+
// Check if already installed
|
|
885
|
+
if (this.registry.get(manifest.name)) {
|
|
886
|
+
throw new Error(`Plugin already installed: ${manifest.name}`);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Load and register
|
|
890
|
+
const plugin = await this.loader.load(manifest);
|
|
891
|
+
this.registry.register(plugin);
|
|
892
|
+
|
|
893
|
+
return plugin;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
async uninstall(name: string): Promise<void> {
|
|
897
|
+
const plugin = this.registry.get(name);
|
|
898
|
+
if (!plugin) {
|
|
899
|
+
throw new Error(`Plugin not found: ${name}`);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Deactivate if active
|
|
903
|
+
if (plugin.state === 'activated') {
|
|
904
|
+
await this.deactivate(name);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Stop hot-reload watching
|
|
908
|
+
this.disableHotReload(name);
|
|
909
|
+
|
|
910
|
+
// Unregister
|
|
911
|
+
this.registry.unregister(name);
|
|
912
|
+
|
|
913
|
+
// Clear module cache
|
|
914
|
+
this.clearModuleCache(plugin.path);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
async update(name: string): Promise<LoadedPlugin> {
|
|
918
|
+
const plugin = this.registry.get(name);
|
|
919
|
+
if (!plugin) {
|
|
920
|
+
throw new Error(`Plugin not found: ${name}`);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// TODO: Fetch latest version from source
|
|
924
|
+
// For now, just reload
|
|
925
|
+
await this.reload(name);
|
|
926
|
+
return this.registry.get(name)!;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
enableHotReload(name: string): void {
|
|
930
|
+
const plugin = this.registry.get(name);
|
|
931
|
+
if (!plugin) return;
|
|
932
|
+
|
|
933
|
+
if (this.watchers.has(name)) return; // Already watching
|
|
934
|
+
|
|
935
|
+
const watcher = fs.watch(plugin.path, { recursive: true }, async (event, filename) => {
|
|
936
|
+
if (!filename || !this.shouldReload(filename)) return;
|
|
937
|
+
|
|
938
|
+
console.log(`Hot-reloading plugin ${name} due to change in ${filename}`);
|
|
939
|
+
try {
|
|
940
|
+
await this.reload(name);
|
|
941
|
+
} catch (error) {
|
|
942
|
+
console.error(`Hot-reload failed for ${name}:`, error);
|
|
943
|
+
this.errorCallbacks.forEach(cb => cb({ plugin: name, error }));
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
this.watchers.set(name, watcher);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
disableHotReload(name: string): void {
|
|
951
|
+
const watcher = this.watchers.get(name);
|
|
952
|
+
if (watcher) {
|
|
953
|
+
watcher.close();
|
|
954
|
+
this.watchers.delete(name);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
private shouldReload(filename: string): boolean {
|
|
959
|
+
const ext = path.extname(filename);
|
|
960
|
+
return ['.js', '.ts', '.json'].includes(ext) && !filename.includes('node_modules');
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
private createContext(plugin: LoadedPlugin): PluginContext {
|
|
964
|
+
const storagePath = path.join(
|
|
965
|
+
this.config.get('plugins.storagePath', '.elsabro/plugins'),
|
|
966
|
+
plugin.manifest.name
|
|
967
|
+
);
|
|
968
|
+
|
|
969
|
+
return {
|
|
970
|
+
pluginPath: plugin.path,
|
|
971
|
+
manifest: plugin.manifest,
|
|
972
|
+
globalState: new MementoImpl(path.join(storagePath, 'global-state.json')),
|
|
973
|
+
workspaceState: new MementoImpl(path.join(storagePath, 'workspace-state.json')),
|
|
974
|
+
secrets: new SecretsStorageImpl(plugin.manifest.name),
|
|
975
|
+
subscriptions: [],
|
|
976
|
+
logger: new PluginLogger(plugin.manifest.name),
|
|
977
|
+
elsabro: this.createElsabroAPI(plugin)
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
private createElsabroAPI(plugin: LoadedPlugin): ElsabroAPI {
|
|
982
|
+
const permissions = new Set(plugin.manifest.permissions || []);
|
|
983
|
+
|
|
984
|
+
return {
|
|
985
|
+
// Only expose APIs based on permissions
|
|
986
|
+
config: permissions.has('config:read') ? this.config : undefined,
|
|
987
|
+
secrets: permissions.has('secrets') ? secretsVault : undefined,
|
|
988
|
+
// ... other APIs
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
getStatus(name: string): PluginStatus {
|
|
993
|
+
const plugin = this.registry.get(name);
|
|
994
|
+
if (!plugin) {
|
|
995
|
+
return { state: 'error', error: new Error('Plugin not found') };
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
return {
|
|
999
|
+
state: plugin.state,
|
|
1000
|
+
activatedAt: plugin._activatedAt,
|
|
1001
|
+
error: plugin._lastError,
|
|
1002
|
+
resourceUsage: this.getResourceUsage(name)
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
getAllStatus(): Map<string, PluginStatus> {
|
|
1007
|
+
const status = new Map<string, PluginStatus>();
|
|
1008
|
+
for (const plugin of this.registry.getAll()) {
|
|
1009
|
+
status.set(plugin.manifest.name, this.getStatus(plugin.manifest.name));
|
|
1010
|
+
}
|
|
1011
|
+
return status;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
isActive(name: string): boolean {
|
|
1015
|
+
const plugin = this.registry.get(name);
|
|
1016
|
+
return plugin?.state === 'activated';
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
private sortByDependencies(plugins: LoadedPlugin[]): LoadedPlugin[] {
|
|
1020
|
+
// Topological sort by dependencies
|
|
1021
|
+
const visited = new Set<string>();
|
|
1022
|
+
const result: LoadedPlugin[] = [];
|
|
1023
|
+
|
|
1024
|
+
const visit = (plugin: LoadedPlugin) => {
|
|
1025
|
+
if (visited.has(plugin.manifest.name)) return;
|
|
1026
|
+
visited.add(plugin.manifest.name);
|
|
1027
|
+
|
|
1028
|
+
const deps = this.registry.getDependencies(plugin.manifest.name);
|
|
1029
|
+
for (const dep of deps) {
|
|
1030
|
+
visit(dep);
|
|
1031
|
+
}
|
|
1032
|
+
result.push(plugin);
|
|
1033
|
+
};
|
|
1034
|
+
|
|
1035
|
+
for (const plugin of plugins) {
|
|
1036
|
+
visit(plugin);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
return result;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
private clearModuleCache(pluginPath: string): void {
|
|
1043
|
+
// Clear all cached modules from this plugin
|
|
1044
|
+
for (const key of Object.keys(require.cache)) {
|
|
1045
|
+
if (key.startsWith(pluginPath)) {
|
|
1046
|
+
delete require.cache[key];
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
onActivate(callback: Function): Disposable {
|
|
1052
|
+
this.activateCallbacks.add(callback);
|
|
1053
|
+
return { dispose: () => this.activateCallbacks.delete(callback) };
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
onDeactivate(callback: Function): Disposable {
|
|
1057
|
+
this.deactivateCallbacks.add(callback);
|
|
1058
|
+
return { dispose: () => this.deactivateCallbacks.delete(callback) };
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
onError(callback: Function): Disposable {
|
|
1062
|
+
this.errorCallbacks.add(callback);
|
|
1063
|
+
return { dispose: () => this.errorCallbacks.delete(callback) };
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
```
|
|
1067
|
+
|
|
1068
|
+
---
|
|
1069
|
+
|
|
1070
|
+
## 5. Hook System
|
|
1071
|
+
|
|
1072
|
+
### Propósito
|
|
1073
|
+
Sistema de hooks para interceptar y extender funcionalidad del core.
|
|
1074
|
+
|
|
1075
|
+
### Interfaz
|
|
1076
|
+
|
|
1077
|
+
```typescript
|
|
1078
|
+
type HookCallback<T = unknown, R = T> = (
|
|
1079
|
+
context: HookContext<T>,
|
|
1080
|
+
next: () => Promise<R>
|
|
1081
|
+
) => Promise<R>;
|
|
1082
|
+
|
|
1083
|
+
interface HookContext<T> {
|
|
1084
|
+
data: T;
|
|
1085
|
+
plugin?: string;
|
|
1086
|
+
metadata: Record<string, unknown>;
|
|
1087
|
+
abort(reason?: string): void;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
interface HookSystem {
|
|
1091
|
+
// Registration
|
|
1092
|
+
register<T>(point: string, callback: HookCallback<T>, priority?: number): Disposable;
|
|
1093
|
+
registerBefore<T>(point: string, callback: (data: T) => T | Promise<T>): Disposable;
|
|
1094
|
+
registerAfter<T>(point: string, callback: (data: T, result: unknown) => void): Disposable;
|
|
1095
|
+
|
|
1096
|
+
// Execution
|
|
1097
|
+
execute<T, R>(point: string, data: T): Promise<R>;
|
|
1098
|
+
executeWaterfall<T>(point: string, data: T): Promise<T>;
|
|
1099
|
+
|
|
1100
|
+
// Queries
|
|
1101
|
+
getHooks(point: string): Array<{ callback: HookCallback; priority: number; plugin?: string }>;
|
|
1102
|
+
hasHooks(point: string): boolean;
|
|
1103
|
+
|
|
1104
|
+
// Built-in hook points
|
|
1105
|
+
readonly POINTS: {
|
|
1106
|
+
AGENT_BEFORE_RUN: 'agent:beforeRun';
|
|
1107
|
+
AGENT_AFTER_RUN: 'agent:afterRun';
|
|
1108
|
+
TOOL_BEFORE_EXECUTE: 'tool:beforeExecute';
|
|
1109
|
+
TOOL_AFTER_EXECUTE: 'tool:afterExecute';
|
|
1110
|
+
LLM_BEFORE_CALL: 'llm:beforeCall';
|
|
1111
|
+
LLM_AFTER_CALL: 'llm:afterCall';
|
|
1112
|
+
CONFIG_CHANGE: 'config:change';
|
|
1113
|
+
PLUGIN_ACTIVATE: 'plugin:activate';
|
|
1114
|
+
PLUGIN_DEACTIVATE: 'plugin:deactivate';
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
```
|
|
1118
|
+
|
|
1119
|
+
### Implementación
|
|
1120
|
+
|
|
1121
|
+
```typescript
|
|
1122
|
+
class HookSystemImpl implements HookSystem {
|
|
1123
|
+
private hooks: Map<string, Array<{
|
|
1124
|
+
callback: HookCallback;
|
|
1125
|
+
priority: number;
|
|
1126
|
+
plugin?: string;
|
|
1127
|
+
}>> = new Map();
|
|
1128
|
+
|
|
1129
|
+
readonly POINTS = {
|
|
1130
|
+
AGENT_BEFORE_RUN: 'agent:beforeRun',
|
|
1131
|
+
AGENT_AFTER_RUN: 'agent:afterRun',
|
|
1132
|
+
TOOL_BEFORE_EXECUTE: 'tool:beforeExecute',
|
|
1133
|
+
TOOL_AFTER_EXECUTE: 'tool:afterExecute',
|
|
1134
|
+
LLM_BEFORE_CALL: 'llm:beforeCall',
|
|
1135
|
+
LLM_AFTER_CALL: 'llm:afterCall',
|
|
1136
|
+
CONFIG_CHANGE: 'config:change',
|
|
1137
|
+
PLUGIN_ACTIVATE: 'plugin:activate',
|
|
1138
|
+
PLUGIN_DEACTIVATE: 'plugin:deactivate'
|
|
1139
|
+
} as const;
|
|
1140
|
+
|
|
1141
|
+
register<T>(
|
|
1142
|
+
point: string,
|
|
1143
|
+
callback: HookCallback<T>,
|
|
1144
|
+
priority: number = 100
|
|
1145
|
+
): Disposable {
|
|
1146
|
+
if (!this.hooks.has(point)) {
|
|
1147
|
+
this.hooks.set(point, []);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
const hook = { callback, priority };
|
|
1151
|
+
this.hooks.get(point)!.push(hook);
|
|
1152
|
+
this.hooks.get(point)!.sort((a, b) => a.priority - b.priority);
|
|
1153
|
+
|
|
1154
|
+
return {
|
|
1155
|
+
dispose: () => {
|
|
1156
|
+
const hooks = this.hooks.get(point);
|
|
1157
|
+
if (hooks) {
|
|
1158
|
+
const index = hooks.indexOf(hook);
|
|
1159
|
+
if (index > -1) hooks.splice(index, 1);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
registerBefore<T>(point: string, callback: (data: T) => T | Promise<T>): Disposable {
|
|
1166
|
+
return this.register<T>(point, async (ctx, next) => {
|
|
1167
|
+
ctx.data = await callback(ctx.data);
|
|
1168
|
+
return next();
|
|
1169
|
+
}, 50);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
registerAfter<T>(
|
|
1173
|
+
point: string,
|
|
1174
|
+
callback: (data: T, result: unknown) => void
|
|
1175
|
+
): Disposable {
|
|
1176
|
+
return this.register<T>(point, async (ctx, next) => {
|
|
1177
|
+
const result = await next();
|
|
1178
|
+
callback(ctx.data, result);
|
|
1179
|
+
return result;
|
|
1180
|
+
}, 150);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
async execute<T, R>(point: string, data: T): Promise<R> {
|
|
1184
|
+
const hooks = this.hooks.get(point) || [];
|
|
1185
|
+
let aborted = false;
|
|
1186
|
+
let abortReason: string | undefined;
|
|
1187
|
+
|
|
1188
|
+
const context: HookContext<T> = {
|
|
1189
|
+
data,
|
|
1190
|
+
metadata: {},
|
|
1191
|
+
abort: (reason) => {
|
|
1192
|
+
aborted = true;
|
|
1193
|
+
abortReason = reason;
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
|
|
1197
|
+
// Build middleware chain
|
|
1198
|
+
let index = -1;
|
|
1199
|
+
|
|
1200
|
+
const dispatch = async (i: number): Promise<R> => {
|
|
1201
|
+
if (aborted) {
|
|
1202
|
+
throw new HookAbortError(point, abortReason);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
if (i <= index) {
|
|
1206
|
+
throw new Error('next() called multiple times');
|
|
1207
|
+
}
|
|
1208
|
+
index = i;
|
|
1209
|
+
|
|
1210
|
+
if (i >= hooks.length) {
|
|
1211
|
+
// End of chain - return data as result
|
|
1212
|
+
return context.data as unknown as R;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const hook = hooks[i];
|
|
1216
|
+
context.plugin = hook.plugin;
|
|
1217
|
+
|
|
1218
|
+
return hook.callback(context, () => dispatch(i + 1)) as Promise<R>;
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
return dispatch(0);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
async executeWaterfall<T>(point: string, data: T): Promise<T> {
|
|
1225
|
+
const hooks = this.hooks.get(point) || [];
|
|
1226
|
+
let result = data;
|
|
1227
|
+
|
|
1228
|
+
for (const hook of hooks) {
|
|
1229
|
+
const context: HookContext<T> = {
|
|
1230
|
+
data: result,
|
|
1231
|
+
plugin: hook.plugin,
|
|
1232
|
+
metadata: {},
|
|
1233
|
+
abort: () => {
|
|
1234
|
+
throw new HookAbortError(point, 'Waterfall aborted');
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
|
|
1238
|
+
result = await hook.callback(context, async () => context.data) as T;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
return result;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
getHooks(point: string) {
|
|
1245
|
+
return this.hooks.get(point) || [];
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
hasHooks(point: string): boolean {
|
|
1249
|
+
return (this.hooks.get(point)?.length || 0) > 0;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
```
|
|
1253
|
+
|
|
1254
|
+
### Uso de Hooks
|
|
1255
|
+
|
|
1256
|
+
```typescript
|
|
1257
|
+
const hooks = new HookSystem();
|
|
1258
|
+
|
|
1259
|
+
// Plugin registers a hook to modify agent prompts
|
|
1260
|
+
hooks.registerBefore<AgentRunContext>('agent:beforeRun', async (data) => {
|
|
1261
|
+
// Add custom instructions to system prompt
|
|
1262
|
+
data.systemPrompt += '\n\nAdditional instructions from plugin...';
|
|
1263
|
+
return data;
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
// Plugin registers a hook to log all tool executions
|
|
1267
|
+
hooks.registerAfter<ToolExecuteContext>('tool:afterExecute', (data, result) => {
|
|
1268
|
+
analytics.track('tool_executed', {
|
|
1269
|
+
tool: data.toolName,
|
|
1270
|
+
duration: result.duration,
|
|
1271
|
+
success: !result.error
|
|
1272
|
+
});
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
// Middleware-style hook
|
|
1276
|
+
hooks.register<LLMCallContext>('llm:beforeCall', async (ctx, next) => {
|
|
1277
|
+
const start = Date.now();
|
|
1278
|
+
|
|
1279
|
+
try {
|
|
1280
|
+
const result = await next();
|
|
1281
|
+
metrics.recordLatency('llm_call', Date.now() - start);
|
|
1282
|
+
return result;
|
|
1283
|
+
} catch (error) {
|
|
1284
|
+
metrics.increment('llm_errors');
|
|
1285
|
+
throw error;
|
|
1286
|
+
}
|
|
1287
|
+
}, 10); // High priority (runs early)
|
|
1288
|
+
```
|
|
1289
|
+
|
|
1290
|
+
---
|
|
1291
|
+
|
|
1292
|
+
## 6. Plugin Examples
|
|
1293
|
+
|
|
1294
|
+
### Example: Git Integration Plugin
|
|
1295
|
+
|
|
1296
|
+
```typescript
|
|
1297
|
+
// plugins/elsabro-git/plugin.json
|
|
1298
|
+
{
|
|
1299
|
+
"name": "elsabro-git",
|
|
1300
|
+
"version": "1.0.0",
|
|
1301
|
+
"displayName": "Git Integration",
|
|
1302
|
+
"description": "Git operations for ELSABRO agents",
|
|
1303
|
+
"main": "dist/index.js",
|
|
1304
|
+
"engines": { "elsabro": ">=3.0.0" },
|
|
1305
|
+
"permissions": ["shell", "filesystem:read"],
|
|
1306
|
+
"contributes": {
|
|
1307
|
+
"tools": [
|
|
1308
|
+
{
|
|
1309
|
+
"name": "git-status",
|
|
1310
|
+
"displayName": "Git Status",
|
|
1311
|
+
"description": "Get the current git repository status",
|
|
1312
|
+
"inputSchema": {
|
|
1313
|
+
"type": "object",
|
|
1314
|
+
"properties": {
|
|
1315
|
+
"path": { "type": "string", "description": "Repository path" }
|
|
1316
|
+
}
|
|
1317
|
+
},
|
|
1318
|
+
"handler": "tools/gitStatus"
|
|
1319
|
+
},
|
|
1320
|
+
{
|
|
1321
|
+
"name": "git-commit",
|
|
1322
|
+
"displayName": "Git Commit",
|
|
1323
|
+
"description": "Create a git commit",
|
|
1324
|
+
"inputSchema": {
|
|
1325
|
+
"type": "object",
|
|
1326
|
+
"required": ["message"],
|
|
1327
|
+
"properties": {
|
|
1328
|
+
"message": { "type": "string" },
|
|
1329
|
+
"files": { "type": "array", "items": { "type": "string" } }
|
|
1330
|
+
}
|
|
1331
|
+
},
|
|
1332
|
+
"handler": "tools/gitCommit"
|
|
1333
|
+
}
|
|
1334
|
+
],
|
|
1335
|
+
"hooks": [
|
|
1336
|
+
{
|
|
1337
|
+
"point": "agent:afterRun",
|
|
1338
|
+
"priority": 100,
|
|
1339
|
+
"handler": "hooks/autoCommit"
|
|
1340
|
+
}
|
|
1341
|
+
]
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
```
|
|
1345
|
+
|
|
1346
|
+
```typescript
|
|
1347
|
+
// plugins/elsabro-git/src/index.ts
|
|
1348
|
+
import { Plugin, PluginContext } from '@elsabro/plugin-api';
|
|
1349
|
+
|
|
1350
|
+
export default class GitPlugin implements Plugin {
|
|
1351
|
+
private context!: PluginContext;
|
|
1352
|
+
|
|
1353
|
+
async activate(context: PluginContext): Promise<void> {
|
|
1354
|
+
this.context = context;
|
|
1355
|
+
|
|
1356
|
+
context.logger.info('Git plugin activated');
|
|
1357
|
+
|
|
1358
|
+
// Check if git is available
|
|
1359
|
+
try {
|
|
1360
|
+
await this.runGitCommand(['--version']);
|
|
1361
|
+
} catch (error) {
|
|
1362
|
+
context.logger.warn('Git not found in PATH');
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
async deactivate(): Promise<void> {
|
|
1367
|
+
this.context.logger.info('Git plugin deactivated');
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
private async runGitCommand(args: string[]): Promise<string> {
|
|
1371
|
+
// Implementation
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
```
|
|
1375
|
+
|
|
1376
|
+
### Example: Analytics Plugin
|
|
1377
|
+
|
|
1378
|
+
```typescript
|
|
1379
|
+
// plugins/elsabro-analytics/plugin.json
|
|
1380
|
+
{
|
|
1381
|
+
"name": "elsabro-analytics",
|
|
1382
|
+
"version": "1.0.0",
|
|
1383
|
+
"displayName": "Analytics",
|
|
1384
|
+
"description": "Usage analytics and insights",
|
|
1385
|
+
"main": "dist/index.js",
|
|
1386
|
+
"engines": { "elsabro": ">=3.0.0" },
|
|
1387
|
+
"permissions": ["network", "config:read"],
|
|
1388
|
+
"contributes": {
|
|
1389
|
+
"hooks": [
|
|
1390
|
+
{ "point": "agent:afterRun", "priority": 200, "handler": "hooks/trackAgentRun" },
|
|
1391
|
+
{ "point": "tool:afterExecute", "priority": 200, "handler": "hooks/trackToolUse" },
|
|
1392
|
+
{ "point": "llm:afterCall", "priority": 200, "handler": "hooks/trackLLMCall" }
|
|
1393
|
+
],
|
|
1394
|
+
"commands": [
|
|
1395
|
+
{
|
|
1396
|
+
"command": "analytics.dashboard",
|
|
1397
|
+
"title": "Show Analytics Dashboard",
|
|
1398
|
+
"category": "Analytics"
|
|
1399
|
+
}
|
|
1400
|
+
]
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
```
|
|
1404
|
+
|
|
1405
|
+
---
|
|
1406
|
+
|
|
1407
|
+
## 7. Plugin Security
|
|
1408
|
+
|
|
1409
|
+
### Sandbox Aislado
|
|
1410
|
+
|
|
1411
|
+
```typescript
|
|
1412
|
+
class PluginSandbox {
|
|
1413
|
+
private vm: NodeVM;
|
|
1414
|
+
|
|
1415
|
+
constructor(plugin: LoadedPlugin, options: SandboxOptions) {
|
|
1416
|
+
this.vm = new NodeVM({
|
|
1417
|
+
timeout: options.timeout || 30000,
|
|
1418
|
+
sandbox: {},
|
|
1419
|
+
require: {
|
|
1420
|
+
external: options.allowedModules || ['lodash', 'dayjs'],
|
|
1421
|
+
builtin: this.getAllowedBuiltins(plugin.manifest.permissions),
|
|
1422
|
+
root: plugin.path,
|
|
1423
|
+
mock: this.createMocks(plugin.manifest.permissions)
|
|
1424
|
+
}
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
private getAllowedBuiltins(permissions: PluginPermission[]): string[] {
|
|
1429
|
+
const allowed = ['path', 'url', 'util', 'events'];
|
|
1430
|
+
|
|
1431
|
+
if (permissions.includes('filesystem:read')) {
|
|
1432
|
+
allowed.push('fs/promises');
|
|
1433
|
+
}
|
|
1434
|
+
if (permissions.includes('network')) {
|
|
1435
|
+
allowed.push('http', 'https');
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
return allowed;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
private createMocks(permissions: PluginPermission[]): Record<string, unknown> {
|
|
1442
|
+
const mocks: Record<string, unknown> = {};
|
|
1443
|
+
|
|
1444
|
+
// Mock dangerous modules
|
|
1445
|
+
if (!permissions.includes('shell')) {
|
|
1446
|
+
mocks['child_process'] = {
|
|
1447
|
+
exec: () => { throw new Error('Permission denied: shell'); },
|
|
1448
|
+
spawn: () => { throw new Error('Permission denied: shell'); }
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
return mocks;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
```
|
|
1456
|
+
|
|
1457
|
+
### Permission Enforcement
|
|
1458
|
+
|
|
1459
|
+
```typescript
|
|
1460
|
+
class PermissionEnforcer {
|
|
1461
|
+
constructor(private permissions: Set<PluginPermission>) {}
|
|
1462
|
+
|
|
1463
|
+
checkFileRead(path: string): void {
|
|
1464
|
+
if (!this.permissions.has('filesystem:read')) {
|
|
1465
|
+
throw new PermissionDeniedError('filesystem:read', path);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
checkFileWrite(path: string): void {
|
|
1470
|
+
if (!this.permissions.has('filesystem:write')) {
|
|
1471
|
+
throw new PermissionDeniedError('filesystem:write', path);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
checkNetwork(url: string): void {
|
|
1476
|
+
if (!this.permissions.has('network')) {
|
|
1477
|
+
throw new PermissionDeniedError('network', url);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
checkSecrets(): void {
|
|
1482
|
+
if (!this.permissions.has('secrets')) {
|
|
1483
|
+
throw new PermissionDeniedError('secrets');
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
```
|
|
1488
|
+
|
|
1489
|
+
---
|
|
1490
|
+
|
|
1491
|
+
## 8. Plugin Dashboard
|
|
1492
|
+
|
|
1493
|
+
```
|
|
1494
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
1495
|
+
│ ELSABRO Plugin Manager │
|
|
1496
|
+
├─────────────────────────────────────────────────────────────────────────────┤
|
|
1497
|
+
│ Installed: 5 plugins (4 active, 1 disabled) │
|
|
1498
|
+
│ Hot-reload: Enabled for development │
|
|
1499
|
+
├─────────────────────────────────────────────────────────────────────────────┤
|
|
1500
|
+
│ Plugins │
|
|
1501
|
+
├──────────────────────┬─────────┬────────────┬─────────────┬─────────────────┤
|
|
1502
|
+
│ Name │ Version │ Status │ Permissions │ Memory │
|
|
1503
|
+
├──────────────────────┼─────────┼────────────┼─────────────┼─────────────────┤
|
|
1504
|
+
│ elsabro-git │ 1.2.0 │ ✓ Active │ shell, fs │ 12.3 MB │
|
|
1505
|
+
│ elsabro-analytics │ 2.0.1 │ ✓ Active │ network │ 8.1 MB │
|
|
1506
|
+
│ elsabro-jira │ 1.0.0 │ ✓ Active │ network │ 15.2 MB │
|
|
1507
|
+
│ elsabro-slack │ 1.5.2 │ ✓ Active │ network │ 10.8 MB │
|
|
1508
|
+
│ elsabro-debug │ 0.9.0 │ ✗ Disabled │ shell, fs │ - MB │
|
|
1509
|
+
└──────────────────────┴─────────┴────────────┴─────────────┴─────────────────┘
|
|
1510
|
+
│ Contributions │
|
|
1511
|
+
├─────────────────────────────────────────────────────────────────────────────┤
|
|
1512
|
+
│ Agents: 2 (git-reviewer, jira-assistant) │
|
|
1513
|
+
│ Tools: 8 (git-status, git-commit, jira-create-issue, ...) │
|
|
1514
|
+
│ Commands: 5 │
|
|
1515
|
+
│ Hooks: 12 registered │
|
|
1516
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
1517
|
+
```
|
|
1518
|
+
|
|
1519
|
+
---
|
|
1520
|
+
|
|
1521
|
+
## Referencias
|
|
1522
|
+
|
|
1523
|
+
- **REF-001**: Architecture Guide
|
|
1524
|
+
- **REF-022**: Security System
|
|
1525
|
+
- **REF-023**: Configuration Management
|
|
1526
|
+
- **REF-024**: Esta referencia (Plugin System)
|