@xingjisoft/neutrino 0.6.0 → 0.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 +6 -1
- package/assets/fast-planner.txt +57 -0
- package/assets/fast-replanner.txt +28 -0
- package/dist/agents.d.ts +25 -0
- package/dist/app.d.ts +206 -0
- package/dist/app.js +15 -0
- package/dist/background.d.ts +96 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +40 -3
- package/dist/browser.d.ts +18 -0
- package/dist/clipboard.d.ts +16 -0
- package/dist/colors.d.ts +29 -0
- package/dist/compact.d.ts +52 -0
- package/dist/config.d.ts +221 -0
- package/dist/config.js +22 -7
- package/dist/cwd.d.ts +19 -0
- package/dist/fast/artifacts.d.ts +26 -0
- package/dist/fast/artifacts.js +109 -0
- package/dist/fast/engine.d.ts +78 -0
- package/dist/fast/engine.js +657 -0
- package/dist/fast/evidence.d.ts +21 -0
- package/dist/fast/evidence.js +68 -0
- package/dist/fast/planner.d.ts +10 -0
- package/dist/fast/planner.js +51 -0
- package/dist/fast/pool.d.ts +17 -0
- package/dist/fast/pool.js +78 -0
- package/dist/fast/scheduler.d.ts +54 -0
- package/dist/fast/scheduler.js +383 -0
- package/dist/fast/types.d.ts +100 -0
- package/dist/fast/types.js +21 -0
- package/dist/fast/worker.d.ts +23 -0
- package/dist/fast/worker.js +185 -0
- package/dist/header.d.ts +3 -0
- package/dist/index.d.ts +5 -0
- package/dist/llm/approval.d.ts +204 -0
- package/dist/llm/approval.js +14 -3
- package/dist/llm/client.d.ts +118 -0
- package/dist/llm/client.js +57 -8
- package/dist/llm/codeExecutor.d.ts +14 -0
- package/dist/llm/codemode.d.ts +15 -0
- package/dist/llm/context.d.ts +29 -0
- package/dist/llm/context.js +17 -4
- package/dist/llm/diagnostics.d.ts +91 -0
- package/dist/llm/diagnostics.js +242 -0
- package/dist/llm/plan.d.ts +148 -0
- package/dist/llm/retry.d.ts +80 -0
- package/dist/llm/risk.d.ts +21 -0
- package/dist/llm/risk.js +7 -0
- package/dist/llm/runtime.d.ts +189 -0
- package/dist/llm/runtime.js +178 -24
- package/dist/llm/search.d.ts +2 -0
- package/dist/llm/searchNetwork.d.ts +8 -0
- package/dist/llm/subagent.d.ts +112 -0
- package/dist/llm/subagent.js +25 -12
- package/dist/llm/tools.d.ts +20 -0
- package/dist/llm/tools.js +4 -1
- package/dist/llm/workspace.d.ts +6 -0
- package/dist/llm/workspace.js +271 -0
- package/dist/markdown.d.ts +58 -0
- package/dist/migration.d.ts +8 -0
- package/dist/persistence.d.ts +14 -0
- package/dist/plugins/catalog.d.ts +20 -0
- package/dist/plugins/catalog.js +45 -0
- package/dist/plugins/cli.d.ts +1 -0
- package/dist/plugins/cli.js +106 -0
- package/dist/plugins/host.d.ts +121 -0
- package/dist/plugins/host.js +450 -0
- package/dist/plugins/manager.d.ts +24 -0
- package/dist/plugins/manager.js +288 -0
- package/dist/plugins/package.d.ts +5 -0
- package/dist/plugins/package.js +53 -0
- package/dist/plugins/runner.d.ts +1 -0
- package/dist/plugins/runner.js +227 -0
- package/dist/plugins/session.d.ts +12 -0
- package/dist/plugins/session.js +97 -0
- package/dist/plugins/terminal.d.ts +22 -0
- package/dist/plugins/terminal.js +33 -0
- package/dist/plugins/types.d.ts +178 -0
- package/dist/plugins/types.js +6 -0
- package/dist/plugins/validation.d.ts +14 -0
- package/dist/plugins/validation.js +174 -0
- package/dist/plugins/web-sdk.d.ts +58 -0
- package/dist/plugins/web-sdk.js +1 -0
- package/dist/pro/agents.d.ts +34 -0
- package/dist/pro/agents.js +1 -1
- package/dist/pro/config.d.ts +75 -0
- package/dist/pro/config.js +3 -2
- package/dist/pro/preflight.d.ts +21 -0
- package/dist/sanitize.d.ts +34 -0
- package/dist/screens/agentConversation.d.ts +28 -0
- package/dist/screens/agentHistory.d.ts +25 -0
- package/dist/screens/agentSelect.d.ts +16 -0
- package/dist/screens/chat.d.ts +292 -0
- package/dist/screens/chat.js +179 -65
- package/dist/screens/history.d.ts +23 -0
- package/dist/screens/modelSelect.d.ts +29 -0
- package/dist/screens/plugins.d.ts +29 -0
- package/dist/screens/plugins.js +196 -0
- package/dist/screens/settings.d.ts +42 -0
- package/dist/screens/start.d.ts +43 -0
- package/dist/screens/start.js +25 -17
- package/dist/screens/updatePrompt.d.ts +41 -0
- package/dist/selection.d.ts +75 -0
- package/dist/state.d.ts +26 -0
- package/dist/terminal/buffer.d.ts +47 -0
- package/dist/terminal/input.d.ts +70 -0
- package/dist/terminal/renderer.d.ts +43 -0
- package/dist/terminal/restore.d.ts +41 -0
- package/dist/terminal/screen.d.ts +57 -0
- package/dist/tray.d.ts +14 -0
- package/dist/ui.d.ts +123 -0
- package/dist/update.d.ts +71 -0
- package/dist/updateWatch.d.ts +43 -0
- package/dist/web/approvalQueue.d.ts +8 -0
- package/dist/web/approvalQueue.js +59 -0
- package/dist/web/auth.d.ts +14 -0
- package/dist/web/clientState.d.ts +16 -0
- package/dist/web/folderPicker.d.ts +34 -0
- package/dist/web/folderPicker.js +544 -0
- package/dist/web/hub.d.ts +213 -0
- package/dist/web/hub.js +14 -2
- package/dist/web/localApi.d.ts +11 -0
- package/dist/web/localApi.js +260 -0
- package/dist/web/plugins.d.ts +8 -0
- package/dist/web/plugins.js +64 -0
- package/dist/web/server.d.ts +81 -0
- package/dist/web/server.js +127 -19
- package/dist/web/service.d.ts +7 -0
- package/dist/web/service.js +1 -0
- package/dist/web/session.d.ts +176 -0
- package/dist/web/session.js +170 -21
- package/dist/web/settings.d.ts +65 -0
- package/dist/web/terminalBridge.d.ts +15 -0
- package/dist/web/terminalBridge.js +8 -2
- package/dist/width.d.ts +6 -0
- package/docs/plugins/README.md +126 -0
- package/docs/plugins/agent-sdk.md +92 -0
- package/docs/plugins/architecture.md +64 -0
- package/docs/plugins/compatibility.md +34 -0
- package/docs/plugins/development.md +55 -0
- package/docs/plugins/manifest.md +59 -0
- package/docs/plugins/manifest.schema.json +286 -0
- package/docs/plugins/security.md +53 -0
- package/docs/plugins/skills-prompts.md +69 -0
- package/docs/plugins/ui-sdk.md +109 -0
- package/examples/local-api.py +105 -0
- package/examples/plugins/cards/card.html +6 -0
- package/examples/plugins/cards/neutrino.plugin.json +42 -0
- package/examples/plugins/composer/editor.html +10 -0
- package/examples/plugins/composer/neutrino.plugin.json +10 -0
- package/examples/plugins/function-tools/agent.js +32 -0
- package/examples/plugins/function-tools/neutrino.plugin.json +15 -0
- package/examples/plugins/menus-tabs/neutrino.plugin.json +15 -0
- package/examples/plugins/menus-tabs/panel.html +6 -0
- package/examples/plugins/skills-prompts/neutrino.plugin.json +13 -0
- package/examples/plugins/skills-prompts/prompts/evidence.md +1 -0
- package/examples/plugins/skills-prompts/prompts/reviewer.md +1 -0
- package/examples/plugins/skills-prompts/skills/review/SKILL.md +8 -0
- package/examples/plugins/theme/neutrino.plugin.json +12 -0
- package/package.json +66 -42
- package/web/LOCAL-API.md +65 -0
- package/web/README.md +30 -8
- package/web/app.js +256 -67
- package/web/i18n.js +18 -3
- package/web/index.html +3 -1
- package/web/plugins.css +10 -0
- package/web/plugins.js +230 -0
- package/web/style.css +12 -3
- package/web/turns.js +7 -8
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { fork } from "node:child_process";
|
|
3
|
+
import { AsyncResource } from "node:async_hooks";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { defaults, assert, NAME, object, safeFile, validatePrompt, validateSchema, validateUI, validateValue } from "./validation.js";
|
|
7
|
+
import { pluginRecords, pluginStorage, providerSelections } from "./manager.js";
|
|
8
|
+
const EFFECTS = ["filesystem.read", "filesystem.write", "network", "process"];
|
|
9
|
+
const EXCLUSIVE = new Set(["chat.composer", "chat.message.renderer", "chat.card.plan", "chat.card.tool"]);
|
|
10
|
+
class PluginWorker {
|
|
11
|
+
record;
|
|
12
|
+
timeout;
|
|
13
|
+
log;
|
|
14
|
+
child;
|
|
15
|
+
readyResolve;
|
|
16
|
+
readyReject;
|
|
17
|
+
ready;
|
|
18
|
+
current;
|
|
19
|
+
sequence = 0;
|
|
20
|
+
queue = Promise.resolve();
|
|
21
|
+
operationQueue = Promise.resolve();
|
|
22
|
+
closed = false;
|
|
23
|
+
initTimer;
|
|
24
|
+
constructor(record, scope, timeout, log) {
|
|
25
|
+
this.record = record;
|
|
26
|
+
this.timeout = timeout;
|
|
27
|
+
this.log = log;
|
|
28
|
+
this.ready = new Promise((resolve, reject) => { this.readyResolve = resolve; this.readyReject = reject; });
|
|
29
|
+
this.child = fork(fileURLToPath(new URL("./runner.js", import.meta.url)), [], { cwd: record.directory,
|
|
30
|
+
stdio: ["ignore", "ignore", "pipe", "ipc"], execArgv: ["--max-old-space-size=128"], ...{ windowsHide: true },
|
|
31
|
+
env: record.manifest.runtime === "native" ? process.env : { PATH: process.env.PATH, SystemRoot: process.env.SystemRoot },
|
|
32
|
+
});
|
|
33
|
+
this.initTimer = setTimeout(() => this.fail(new Error("Plugin activation timed out")), timeout);
|
|
34
|
+
this.child.stderr?.on("data", data => this.log(String(data).slice(0, 1000)));
|
|
35
|
+
this.child.on("error", error => this.fail(error));
|
|
36
|
+
this.child.on("exit", () => this.fail(new Error("Plugin process exited")));
|
|
37
|
+
this.child.on("message", message => { void this.receive(message).catch(error => this.fail(error instanceof Error ? error : new Error(String(error)))); });
|
|
38
|
+
this.child.send({ type: "init", initial: { id: record.manifest.id, entry: safeFile(record.directory, record.manifest.entries.agent), runtime: record.manifest.runtime ?? "sandbox", config: { ...defaults(record.manifest.configuration), ...record.activation.config }, scope } });
|
|
39
|
+
}
|
|
40
|
+
resetTimer() {
|
|
41
|
+
if (!this.current)
|
|
42
|
+
return;
|
|
43
|
+
clearTimeout(this.current.timer);
|
|
44
|
+
this.current.timer = setTimeout(() => this.fail(new Error("Plugin callback timed out")), this.timeout);
|
|
45
|
+
}
|
|
46
|
+
async receive(m) {
|
|
47
|
+
assert(object(m), "Invalid plugin protocol message");
|
|
48
|
+
if (m.type === "ready") {
|
|
49
|
+
clearTimeout(this.initTimer);
|
|
50
|
+
this.readyResolve(m.registrations);
|
|
51
|
+
}
|
|
52
|
+
else if (m.type === "fatal")
|
|
53
|
+
this.fail(new Error(String(m.error)));
|
|
54
|
+
else if (m.type === "result" && this.current?.id === m.id) {
|
|
55
|
+
const c = this.current;
|
|
56
|
+
this.current = undefined;
|
|
57
|
+
clearTimeout(c.timer);
|
|
58
|
+
c.signal?.removeEventListener("abort", c.abort);
|
|
59
|
+
if (c.operations) {
|
|
60
|
+
c.stop.abort();
|
|
61
|
+
c.reject(new Error("Plugin returned with unawaited host operations"));
|
|
62
|
+
}
|
|
63
|
+
else if (m.error)
|
|
64
|
+
c.reject(new Error(String(m.error)));
|
|
65
|
+
else
|
|
66
|
+
c.resolve(m.value);
|
|
67
|
+
}
|
|
68
|
+
else if (m.type === "operation") {
|
|
69
|
+
const c = this.current;
|
|
70
|
+
if (m.op === "log") {
|
|
71
|
+
this.log(String(m.value?.message ?? "").slice(0, 4000));
|
|
72
|
+
this.reply(m.id, null);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (!c) {
|
|
76
|
+
if (["storage.get", "storage.set"].includes(String(m.op)) && this.record.manifest.permissions?.includes("storage")) {
|
|
77
|
+
try {
|
|
78
|
+
const args = m.value;
|
|
79
|
+
this.reply(m.id, pluginStorage(this.record.manifest.id, String(args.key), args.value, m.op === "storage.set"));
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
this.reply(m.id, undefined, e.message);
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this.reply(m.id, undefined, "Host operations are available only inside an awaited callback");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (++c.calls > 256 || c.operations >= 64) {
|
|
90
|
+
this.fail(new Error("Plugin exceeded host operation limits"));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
c.operations++;
|
|
94
|
+
clearTimeout(c.timer);
|
|
95
|
+
const operation = this.operationQueue.then(async () => { assert(this.current === c && !c.stop.signal.aborted, "Plugin callback already ended"); return c.operation(String(m.op), (m.value ?? {})); });
|
|
96
|
+
this.operationQueue = operation.then(() => { }, () => { });
|
|
97
|
+
try {
|
|
98
|
+
this.reply(m.id, await operation);
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
this.reply(m.id, undefined, e.message);
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
c.operations--;
|
|
105
|
+
if (this.current === c && !c.operations)
|
|
106
|
+
this.resetTimer();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
reply(id, value, error) { if (this.child.connected)
|
|
111
|
+
this.child.send({ type: "operation-result", id, value, error }); }
|
|
112
|
+
fail(error) {
|
|
113
|
+
if (this.closed)
|
|
114
|
+
return;
|
|
115
|
+
this.closed = true;
|
|
116
|
+
clearTimeout(this.initTimer);
|
|
117
|
+
this.readyReject(error);
|
|
118
|
+
if (this.current) {
|
|
119
|
+
const c = this.current;
|
|
120
|
+
this.current = undefined;
|
|
121
|
+
clearTimeout(c.timer);
|
|
122
|
+
c.stop.abort(error);
|
|
123
|
+
c.signal?.removeEventListener("abort", c.abort);
|
|
124
|
+
c.reject(error);
|
|
125
|
+
}
|
|
126
|
+
this.child.kill("SIGKILL");
|
|
127
|
+
}
|
|
128
|
+
invoke(handler, value, context, operation) {
|
|
129
|
+
const stop = new AbortController();
|
|
130
|
+
const boundOperation = AsyncResource.bind((op, args) => operation(op, args, stop.signal));
|
|
131
|
+
const { signal, ...wire } = context;
|
|
132
|
+
const boundInvoke = AsyncResource.bind(() => new Promise((resolve, reject) => {
|
|
133
|
+
if (this.closed) {
|
|
134
|
+
reject(new Error("Plugin is unavailable"));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const abort = () => this.fail(new Error("Plugin callback cancelled"));
|
|
138
|
+
this.current = { id: ++this.sequence, resolve, reject, operation: boundOperation, abort, signal, operations: 0, calls: 0, stop };
|
|
139
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
140
|
+
if (signal?.aborted) {
|
|
141
|
+
abort();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
this.resetTimer();
|
|
145
|
+
this.child.send({ type: "invoke", id: this.sequence, handler, value, context: wire });
|
|
146
|
+
}));
|
|
147
|
+
const result = this.queue.then(boundInvoke);
|
|
148
|
+
this.queue = result.then(() => { }, () => { });
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
async dispose() {
|
|
152
|
+
if (this.closed)
|
|
153
|
+
return;
|
|
154
|
+
await this.queue;
|
|
155
|
+
await new Promise(resolve => {
|
|
156
|
+
const timer = setTimeout(() => { this.fail(new Error("Plugin cleanup timed out")); resolve(); }, 1000);
|
|
157
|
+
this.child.once("exit", () => { clearTimeout(timer); resolve(); });
|
|
158
|
+
if (this.child.connected)
|
|
159
|
+
this.child.send({ type: "dispose" });
|
|
160
|
+
else {
|
|
161
|
+
clearTimeout(timer);
|
|
162
|
+
resolve();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
this.closed = true;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** One frozen plugin composition per turn, shared by both frontends and all child scopes. */
|
|
169
|
+
export class PluginHost {
|
|
170
|
+
options;
|
|
171
|
+
providers;
|
|
172
|
+
tools = new Map();
|
|
173
|
+
commands = new Map();
|
|
174
|
+
ui = [];
|
|
175
|
+
skills = [];
|
|
176
|
+
pins = [];
|
|
177
|
+
diagnostics = [];
|
|
178
|
+
prompts = [];
|
|
179
|
+
theme = {};
|
|
180
|
+
workers = [];
|
|
181
|
+
hooks = [];
|
|
182
|
+
events = [];
|
|
183
|
+
disposed = false;
|
|
184
|
+
get profile() { return this.options.scope.profile; }
|
|
185
|
+
constructor(options, providers) {
|
|
186
|
+
this.options = options;
|
|
187
|
+
this.providers = providers;
|
|
188
|
+
}
|
|
189
|
+
static async create(options) {
|
|
190
|
+
let providers = {};
|
|
191
|
+
try {
|
|
192
|
+
providers = providerSelections(options.scope.profile);
|
|
193
|
+
}
|
|
194
|
+
catch (e) {
|
|
195
|
+
const host = new PluginHost(options, {});
|
|
196
|
+
host.report("host", e.message);
|
|
197
|
+
return host;
|
|
198
|
+
}
|
|
199
|
+
const host = new PluginHost(options, providers);
|
|
200
|
+
let records;
|
|
201
|
+
try {
|
|
202
|
+
records = pluginRecords(options.scope.profile, options.scope.cwd);
|
|
203
|
+
}
|
|
204
|
+
catch (e) {
|
|
205
|
+
host.report("host", e.message);
|
|
206
|
+
return host;
|
|
207
|
+
}
|
|
208
|
+
const failed = new Set();
|
|
209
|
+
for (const record of records) {
|
|
210
|
+
let worker;
|
|
211
|
+
const previousTheme = { ...host.theme };
|
|
212
|
+
try {
|
|
213
|
+
assert(!options.signal?.aborted, "Task cancelled during plugin activation");
|
|
214
|
+
assert(!record.error, record.error ?? "Invalid plugin");
|
|
215
|
+
for (const dep of Object.keys(record.manifest.dependencies ?? {}))
|
|
216
|
+
assert(!failed.has(dep), `Dependency ${dep} failed to activate`);
|
|
217
|
+
if (record.manifest.entries?.agent) {
|
|
218
|
+
worker = new PluginWorker(record, options.scope, options.timeoutMs ?? 15000, message => host.report(record.manifest.id, message));
|
|
219
|
+
host.workers.push(worker);
|
|
220
|
+
const registrations = await worker.ready;
|
|
221
|
+
assert(Array.isArray(registrations) && registrations.length <= 300, "Too many plugin registrations");
|
|
222
|
+
for (const item of registrations)
|
|
223
|
+
host.register(record, worker, item);
|
|
224
|
+
}
|
|
225
|
+
host.declarations(record);
|
|
226
|
+
host.pins.push({ id: record.manifest.id, version: record.manifest.version, integrity: record.integrity, config: { ...defaults(record.manifest.configuration), ...record.activation.config } });
|
|
227
|
+
}
|
|
228
|
+
catch (e) {
|
|
229
|
+
for (const key of Object.keys(host.theme))
|
|
230
|
+
delete host.theme[key];
|
|
231
|
+
Object.assign(host.theme, previousTheme);
|
|
232
|
+
failed.add(record.manifest.id);
|
|
233
|
+
host.remove(record.manifest.id);
|
|
234
|
+
await worker?.dispose();
|
|
235
|
+
host.report(record.manifest.id, e.message);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return host;
|
|
239
|
+
}
|
|
240
|
+
report(id, message) { if (this.diagnostics.length < 100)
|
|
241
|
+
this.diagnostics.push(`${id}: ${message.slice(0, 4000)}`); }
|
|
242
|
+
permission(record, permission) { assert(record.manifest.permissions?.includes(permission), `${record.manifest.id} requires ${permission}`); }
|
|
243
|
+
declarations(record) {
|
|
244
|
+
const c = record.manifest.contributes;
|
|
245
|
+
for (const prompt of c?.prompts ?? [])
|
|
246
|
+
this.addPrompt(record, prompt);
|
|
247
|
+
for (const ui of c?.ui ?? [])
|
|
248
|
+
this.addUI(record, ui);
|
|
249
|
+
if (c?.theme) {
|
|
250
|
+
this.permission(record, "ui.contribute");
|
|
251
|
+
Object.assign(this.theme, c.theme);
|
|
252
|
+
}
|
|
253
|
+
for (const file of c?.skills ?? []) {
|
|
254
|
+
this.addSkill(record, file);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
addSkill(record, file) {
|
|
258
|
+
this.permission(record, "prompts.contribute");
|
|
259
|
+
const text = this.resource(record, file);
|
|
260
|
+
const front = /^---\r?\n([\s\S]*?)\r?\n---\r?\n/.exec(text);
|
|
261
|
+
const name = /^name:\s*["']?([^\r\n"']+)/m.exec(front?.[1] ?? "")?.[1]?.trim() ?? file.split("/").slice(-2, -1)[0] ?? "skill";
|
|
262
|
+
const description = /^description:\s*["']?([^\r\n"']+)/m.exec(front?.[1] ?? "")?.[1]?.trim() ?? name;
|
|
263
|
+
const id = `${record.manifest.id}/${name}`;
|
|
264
|
+
assert(!this.skills.some(s => s.id === id), `Duplicate skill ${id}`);
|
|
265
|
+
this.skills.push({ id, name, description, text: text.slice(front?.[0].length ?? 0), pluginId: record.manifest.id });
|
|
266
|
+
}
|
|
267
|
+
resource(record, file) { const resolved = safeFile(record.directory, file); assert(fs.statSync(resolved).size <= 64_000, "Plugin text resource exceeds 64 KB"); return fs.readFileSync(resolved, "utf8"); }
|
|
268
|
+
addPrompt(record, prompt) {
|
|
269
|
+
this.permission(record, "prompts.contribute");
|
|
270
|
+
validatePrompt(prompt);
|
|
271
|
+
assert(!this.prompts.some(p => p.pluginId === record.manifest.id && p.id === prompt.id), "Duplicate prompt id");
|
|
272
|
+
this.prompts.push({ ...prompt, text: prompt.file ? this.resource(record, prompt.file) : prompt.text, file: undefined, pluginId: record.manifest.id });
|
|
273
|
+
}
|
|
274
|
+
addUI(record, ui) {
|
|
275
|
+
this.permission(record, "ui.contribute");
|
|
276
|
+
validateUI(ui);
|
|
277
|
+
const key = `${record.manifest.id}/${ui.id}`;
|
|
278
|
+
assert(!this.ui.some(u => u.key === key), `Duplicate UI contribution ${key}`);
|
|
279
|
+
if (ui.view) {
|
|
280
|
+
safeFile(record.directory, ui.view);
|
|
281
|
+
assert((record.manifest.contributes?.ui ?? []).some(u => u.view === ui.view) || record.manifest.webAssets?.includes(ui.view), "Dynamic views must be declared in webAssets");
|
|
282
|
+
}
|
|
283
|
+
this.ui.push({ ...ui, key, pluginId: record.manifest.id, entry: record.manifest.entries?.web, permissions: record.manifest.permissions ?? [] });
|
|
284
|
+
}
|
|
285
|
+
register(record, worker, item) {
|
|
286
|
+
assert(object(item) && typeof item.handler === "string", "Invalid registration");
|
|
287
|
+
if (item.kind === "prompt") {
|
|
288
|
+
this.addPrompt(record, item);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (item.kind === "skill") {
|
|
292
|
+
assert(typeof item.file === "string", "Invalid skill file");
|
|
293
|
+
this.addSkill(record, item.file);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (item.kind === "ui") {
|
|
297
|
+
this.addUI(record, item);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (item.kind === "tool" || item.kind === "command") {
|
|
301
|
+
this.permission(record, item.kind === "tool" ? "tools.register" : "commands.register");
|
|
302
|
+
assert(NAME.test(String(item.name)) && typeof item.description === "string" && item.description.length <= 8000, "Invalid tool/command name or description");
|
|
303
|
+
const parameters = (item.parameters ?? { type: "object", additionalProperties: false });
|
|
304
|
+
validateSchema(parameters);
|
|
305
|
+
assert(parameters.type === "object", "Tool arguments must be an object");
|
|
306
|
+
assert(Array.isArray(item.effects) && item.effects.every(e => EFFECTS.includes(String(e))), "Tools and commands must declare effects[] (empty for pure computation)");
|
|
307
|
+
for (const effect of item.effects)
|
|
308
|
+
this.permission(record, String(effect));
|
|
309
|
+
if (item.kind === "tool" && (record.manifest.runtime === "native" || item.effects.some(e => e !== "filesystem.read"))) {
|
|
310
|
+
const properties = (parameters.properties ?? {});
|
|
311
|
+
assert(!properties.reason || properties.reason.type === "string", "Risky tool reason must be a string");
|
|
312
|
+
parameters.properties = { ...properties, reason: properties.reason ?? { type: "string", minLength: 4, maxLength: 600, description: "Required: explain why this call is needed for the user's request." } };
|
|
313
|
+
parameters.required = [...new Set([...(parameters.required ?? []), "reason"])];
|
|
314
|
+
}
|
|
315
|
+
const common = { record, worker, handler: item.handler, effects: item.effects };
|
|
316
|
+
const publicName = `${record.manifest.id}/${item.name}`;
|
|
317
|
+
if (item.kind === "tool") {
|
|
318
|
+
const alias = `p_${createHash("sha256").update(record.manifest.id).digest("hex").slice(0, 12)}_${item.name}`;
|
|
319
|
+
assert(!this.tools.has(alias), `Duplicate tool ${publicName}`);
|
|
320
|
+
assert(item.roles === undefined || (Array.isArray(item.roles) && item.roles.every(r => typeof r === "string")), "Invalid tool roles");
|
|
321
|
+
this.tools.set(alias, { ...common, roles: item.roles,
|
|
322
|
+
definition: { type: "function", function: { name: alias, description: `[${publicName}] ${item.description}`, parameters } } });
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
assert(!this.commands.has(publicName), `Duplicate command ${publicName}`);
|
|
326
|
+
this.commands.set(publicName, { ...common, name: publicName, description: item.description, parameters });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
else if (item.kind === "hook" || item.kind === "event") {
|
|
330
|
+
this.permission(record, "hooks.register");
|
|
331
|
+
const names = item.kind === "hook" ? ["request.before", "tool.before"] : ["turn.started", "turn.finished", "tool.finished"];
|
|
332
|
+
assert(names.includes(String(item.name)), "Unsupported plugin hook/event");
|
|
333
|
+
(item.kind === "hook" ? this.hooks : this.events).push({ record, worker, handler: item.handler, name: String(item.name) });
|
|
334
|
+
}
|
|
335
|
+
else
|
|
336
|
+
throw new Error(`Unsupported registration ${item.kind}`);
|
|
337
|
+
}
|
|
338
|
+
remove(id) {
|
|
339
|
+
for (const [key, tool] of this.tools)
|
|
340
|
+
if (tool.record.manifest.id === id)
|
|
341
|
+
this.tools.delete(key);
|
|
342
|
+
for (const [key, cmd] of this.commands)
|
|
343
|
+
if (cmd.record.manifest.id === id)
|
|
344
|
+
this.commands.delete(key);
|
|
345
|
+
for (const list of [this.ui, this.prompts, this.skills])
|
|
346
|
+
for (let i = list.length - 1; i >= 0; i--)
|
|
347
|
+
if (list[i].pluginId === id)
|
|
348
|
+
list.splice(i, 1);
|
|
349
|
+
this.hooks = this.hooks.filter(h => h.record.manifest.id !== id);
|
|
350
|
+
this.events = this.events.filter(h => h.record.manifest.id !== id);
|
|
351
|
+
}
|
|
352
|
+
definitions(role = this.context().role) { return [...this.tools.values()].filter(t => !t.roles || t.roles.includes(role)).map(t => structuredClone(t.definition)); }
|
|
353
|
+
toolPolicy(name) {
|
|
354
|
+
const tool = this.tools.get(name);
|
|
355
|
+
return tool ? { effects: tool.effects, native: tool.record.manifest.runtime === "native", roles: tool.roles } : undefined;
|
|
356
|
+
}
|
|
357
|
+
context() { return this.options.context?.() ?? { ...this.options.scope, signal: this.options.signal }; }
|
|
358
|
+
async invoke(owned, value, effects = [], stack = []) {
|
|
359
|
+
assert(!this.disposed, "Plugin host was disposed");
|
|
360
|
+
const id = owned.record.manifest.id;
|
|
361
|
+
assert(owned.record.manifest.runtime !== "native" || this.options.allowNative?.() !== false, "Native plugins cannot run inside a restricted child scope");
|
|
362
|
+
assert(!stack.includes(id) && stack.length < 8, "Plugin service dependency cycle");
|
|
363
|
+
const context = this.context();
|
|
364
|
+
return owned.worker.invoke(owned.handler, structuredClone(value), context, async (op, args, signal) => {
|
|
365
|
+
if (op === "storage.get" || op === "storage.set") {
|
|
366
|
+
this.permission(owned.record, "storage");
|
|
367
|
+
return pluginStorage(id, String(args.key), args.value, op === "storage.set");
|
|
368
|
+
}
|
|
369
|
+
if (op === "service.call") {
|
|
370
|
+
const target = String(args.pluginId);
|
|
371
|
+
assert(Object.hasOwn(owned.record.manifest.dependencies ?? {}, target) || Object.hasOwn(owned.record.manifest.optionalDependencies ?? {}, target), "Declare a dependency before calling another plugin");
|
|
372
|
+
const command = this.commands.get(`${target}/${args.command}`);
|
|
373
|
+
assert(command, "Unknown plugin service command");
|
|
374
|
+
assert(command.effects.every(e => effects.includes(e)), "Service effects exceed the caller's declared effects");
|
|
375
|
+
return this.runCommand(command.name, (args.args ?? {}), [...stack, id]);
|
|
376
|
+
}
|
|
377
|
+
const effect = { read_file: "filesystem.read", list_files: "filesystem.read", write_file: "filesystem.write", run_command: "process", fetch: "network" }[op];
|
|
378
|
+
assert(effect && effects.includes(effect), `Operation ${op} exceeds callback effects`);
|
|
379
|
+
this.permission(owned.record, effect);
|
|
380
|
+
assert(this.options.broker, "Host operations require an active conversation");
|
|
381
|
+
assert(typeof args.reason === "string" && args.reason.trim().length >= 4, "Host operations need an honest reason");
|
|
382
|
+
return this.options.broker(op, args, signal);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
async execute(name, args) {
|
|
386
|
+
const tool = this.tools.get(name);
|
|
387
|
+
assert(tool, "Unknown plugin tool");
|
|
388
|
+
validateValue(args, tool.definition.function.parameters, "arguments");
|
|
389
|
+
const result = await this.invoke(tool, args, tool.effects);
|
|
390
|
+
return (typeof result === "string" ? result : JSON.stringify(result ?? null)).slice(0, 200_000);
|
|
391
|
+
}
|
|
392
|
+
async runCommand(name, args = {}, stack = []) {
|
|
393
|
+
const command = this.commands.get(name);
|
|
394
|
+
assert(command, "Unknown plugin command");
|
|
395
|
+
validateValue(args, command.parameters, "arguments");
|
|
396
|
+
return this.invoke(command, args, command.effects, stack);
|
|
397
|
+
}
|
|
398
|
+
async guardTool(name, args) {
|
|
399
|
+
const tool = this.tools.get(name);
|
|
400
|
+
if (tool)
|
|
401
|
+
validateValue(args, tool.definition.function.parameters, "arguments");
|
|
402
|
+
for (const hook of this.hooks.filter(h => h.name === "tool.before")) {
|
|
403
|
+
const result = await this.invoke(hook, { name, arguments: args });
|
|
404
|
+
assert(!object(result) || !result.deny, `${hook.record.manifest.id}: ${object(result) ? result.reason ?? "Tool denied by plugin" : "Tool denied"}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
async requestSuffix(value) {
|
|
408
|
+
let text = "";
|
|
409
|
+
for (const hook of this.hooks.filter(h => h.name === "request.before")) {
|
|
410
|
+
const result = await this.invoke(hook, value);
|
|
411
|
+
if (object(result)) {
|
|
412
|
+
assert(!result.deny, String(result.reason ?? "Request denied by plugin"));
|
|
413
|
+
if (typeof result.systemSuffix === "string")
|
|
414
|
+
text += `\n[Plugin ${hook.record.manifest.id}]\n${result.systemSuffix.slice(0, 32_000)}`;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return text;
|
|
418
|
+
}
|
|
419
|
+
async emit(name, value) {
|
|
420
|
+
for (const listener of this.events.filter(h => h.name === name))
|
|
421
|
+
try {
|
|
422
|
+
await this.invoke(listener, value);
|
|
423
|
+
}
|
|
424
|
+
catch (e) {
|
|
425
|
+
this.report(listener.record.manifest.id, e.message);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
composePrompt(base, role) {
|
|
429
|
+
if (["confirm_model", "reviewer"].includes(role))
|
|
430
|
+
return { text: base, sections: [] };
|
|
431
|
+
const active = this.prompts.filter(p => !p.roles || p.roles.includes(role)).sort((a, b) => ({ persona: 0, rules: 1, context: 2 }[a.slot ?? "rules"] - { persona: 0, rules: 1, context: 2 }[b.slot ?? "rules"]) || (a.order ?? 0) - (b.order ?? 0) || `${a.pluginId}/${a.id}`.localeCompare(`${b.pluginId}/${b.id}`));
|
|
432
|
+
const replacement = active.find(p => p.replace && this.providers.persona === `${p.pluginId}/${p.id}`);
|
|
433
|
+
if (base.includes("[Plugin section: "))
|
|
434
|
+
return { text: base, sections: [] };
|
|
435
|
+
const sections = active.filter(p => !p.replace).map(p => ({ id: `${p.pluginId}/${p.id}`, text: p.text ?? "" }));
|
|
436
|
+
if (this.skills.length) {
|
|
437
|
+
sections.push({ id: "skills/catalog", text: "Available skills (load by writing /skill <id> in the request):\n" + this.skills.map(s => `${s.id}: ${s.description}`).join("\n") });
|
|
438
|
+
for (const match of (this.options.scope.request ?? "").matchAll(/\/skill\s+([a-zA-Z0-9_.\/-]+)/g)) {
|
|
439
|
+
const skill = this.skills.find(s => s.id === match[1]);
|
|
440
|
+
if (skill)
|
|
441
|
+
sections.push({ id: skill.id, text: skill.text });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return { text: (replacement?.text ?? base) + sections.map(s => `\n\n[Plugin section: ${s.id}]\n${s.text}`).join(""), sections };
|
|
445
|
+
}
|
|
446
|
+
effectiveUI(platform) { return this.ui.filter(u => (!u.platforms || u.platforms.includes(platform)) && (!EXCLUSIVE.has(u.slot) || this.providers[u.slot] === u.key)).sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.key.localeCompare(b.key)); }
|
|
447
|
+
async dispose() { if (this.disposed)
|
|
448
|
+
return; this.disposed = true; for (const worker of this.workers.reverse())
|
|
449
|
+
await worker.dispose(); this.tools.clear(); this.commands.clear(); }
|
|
450
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { PluginRecord, Profile, PublicPlugin, JsonObject } from "./types.js";
|
|
2
|
+
export declare function pluginHome(): string;
|
|
3
|
+
export declare function safeMode(): boolean;
|
|
4
|
+
/** Re-hash the immutable package so a post-install edit invalidates native trust. */
|
|
5
|
+
export declare function packageIntegrity(directory: string): string;
|
|
6
|
+
export declare function installPlugin(source: string, expectedRevision?: number, sourceLabel?: string): PublicPlugin;
|
|
7
|
+
export declare function listPlugins(profile?: Profile, cwd?: string): {
|
|
8
|
+
revision: number;
|
|
9
|
+
safeMode: boolean;
|
|
10
|
+
plugins: PublicPlugin[];
|
|
11
|
+
providers: Record<string, string>;
|
|
12
|
+
};
|
|
13
|
+
export declare function configurePlugin(id: string, patch: {
|
|
14
|
+
enabled?: boolean;
|
|
15
|
+
config?: JsonObject;
|
|
16
|
+
trustNative?: boolean;
|
|
17
|
+
}, profile?: Profile, cwd?: string, revision?: number): void;
|
|
18
|
+
export declare function removePlugin(id: string, revision?: number): void;
|
|
19
|
+
export declare function rollbackPlugin(id: string, revision?: number): void;
|
|
20
|
+
export declare function selectProvider(slot: string, provider: string, profile: Profile, revision?: number): void;
|
|
21
|
+
export declare function pluginRecords(profile?: Profile, cwd?: string): PluginRecord[];
|
|
22
|
+
export declare function providerSelections(profile: Profile): Record<string, string>;
|
|
23
|
+
export declare function pluginAsset(id: string, file: string, profile: Profile, cwd?: string): string;
|
|
24
|
+
export declare function pluginStorage(id: string, key: string, value?: unknown, write?: boolean): unknown;
|