flavor-code 1.2.17 → 1.2.20
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 +2 -2
- package/README.zh-CN.md +2 -2
- package/dist/{app-F4P7RV2X.js → app-5D5RSGQX.js} +47 -20
- package/dist/{chunk-YRIPE5K5.js → chunk-A7PMQ22O.js} +1 -1
- package/dist/{chunk-QQCXJC4U.js → chunk-AXLUVJKC.js} +784 -405
- package/dist/cli.js +3 -3
- package/dist/desktop/main.js +1063 -685
- package/dist/desktop/plugin-worker-entry.js +230 -0
- package/dist/evolve/loader.d.ts +1 -1
- package/dist/plugin-worker-entry.js +233 -0
- package/dist/plugins/host.d.ts +2 -3
- package/dist/plugins/sandbox-protocol.d.ts +72 -0
- package/dist/plugins/types.d.ts +3 -0
- package/dist/plugins/worker-host.d.ts +19 -0
- package/dist/production.d.ts +8 -0
- package/dist/sdk/index.js +2 -2
- package/dist/skills/arguments.d.ts +8 -0
- package/dist/skills/tool.d.ts +5 -0
- package/dist/ui/session.d.ts +1 -0
- package/dist/utils/async.d.ts +14 -0
- package/package.json +5 -4
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// src/plugins/plugin-worker-entry.ts
|
|
2
|
+
import { readFile, realpath } from "fs/promises";
|
|
3
|
+
import { isAbsolute, relative, resolve, sep } from "path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
5
|
+
import { createContext, runInContext, SourceTextModule } from "vm";
|
|
6
|
+
import { parentPort, workerData } from "worker_threads";
|
|
7
|
+
var MAX_MODULE_BYTES = 2 * 1024 * 1024;
|
|
8
|
+
var CONTRIBUTION_NAME_SOURCE = "^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$";
|
|
9
|
+
var data = workerData;
|
|
10
|
+
var port = parentPort;
|
|
11
|
+
if (port === null) throw new Error("Plugin sandbox requires a parent port");
|
|
12
|
+
var activationKeepAlive = setInterval(() => void 0, 1e3);
|
|
13
|
+
function send(message) {
|
|
14
|
+
port.postMessage(message);
|
|
15
|
+
}
|
|
16
|
+
function errorMessage(error) {
|
|
17
|
+
return error instanceof Error ? error.message : String(error);
|
|
18
|
+
}
|
|
19
|
+
function assertContained(root, candidate) {
|
|
20
|
+
const delta = relative(root, candidate);
|
|
21
|
+
if (delta === ".." || delta.startsWith(`..${sep}`) || isAbsolute(delta)) throw new Error("Plugin module import escapes the plugin root");
|
|
22
|
+
}
|
|
23
|
+
var SANDBOX_BOOTSTRAP = `(() => {
|
|
24
|
+
"use strict";
|
|
25
|
+
const stores = { command: new Map(), tool: new Map(), hook: new Map(), skillRoot: new Map(), modelAdapter: new Map() };
|
|
26
|
+
let deactivate;
|
|
27
|
+
let accepting = true;
|
|
28
|
+
const signal = Object.freeze({ aborted: false, reason: undefined, addEventListener() {}, removeEventListener() {}, throwIfAborted() {} });
|
|
29
|
+
const cloneJson = (value) => value === undefined ? undefined : JSON.parse(JSON.stringify(value));
|
|
30
|
+
const deepFreeze = (value) => {
|
|
31
|
+
if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
|
|
32
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
33
|
+
return Object.freeze(value);
|
|
34
|
+
};
|
|
35
|
+
const config = deepFreeze(JSON.parse(globalThis.__flavorConfigJson));
|
|
36
|
+
delete globalThis.__flavorConfigJson;
|
|
37
|
+
const namePattern = new RegExp(${JSON.stringify(CONTRIBUTION_NAME_SOURCE)});
|
|
38
|
+
const name = (value) => {
|
|
39
|
+
const result = String(value);
|
|
40
|
+
if (!namePattern.test(result)) throw new Error("Invalid contribution name: " + result);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
const register = (kind, rawName, value, metadata) => {
|
|
44
|
+
if (!accepting) throw new Error("Plugin registrations are closed after activation");
|
|
45
|
+
const contributionName = name(rawName);
|
|
46
|
+
if (stores[kind].has(contributionName)) throw new Error("Duplicate " + kind + " contribution: " + contributionName);
|
|
47
|
+
stores[kind].set(contributionName, { value, metadata });
|
|
48
|
+
let active = true;
|
|
49
|
+
return Object.freeze(function disposeRegistration() {
|
|
50
|
+
if (!active) return;
|
|
51
|
+
active = false;
|
|
52
|
+
stores[kind].delete(contributionName);
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
const context = Object.freeze({
|
|
56
|
+
signal,
|
|
57
|
+
config,
|
|
58
|
+
logger: Object.freeze({ debug() {}, info() {}, warn() {}, error() {} }),
|
|
59
|
+
services: Object.freeze({ filesystem: Object.freeze({
|
|
60
|
+
async readFile() { throw new Error("Filesystem read is not available in sandbox"); },
|
|
61
|
+
async writeFile() { throw new Error("Filesystem write is not available in sandbox"); },
|
|
62
|
+
}) }),
|
|
63
|
+
registerCommand(rawName, handler, description) {
|
|
64
|
+
if (typeof handler !== "function") throw new Error("Sandbox command handler must be a function");
|
|
65
|
+
return register("command", rawName, handler, { description: typeof description === "string" ? description : undefined });
|
|
66
|
+
},
|
|
67
|
+
registerTool(rawName, tool) {
|
|
68
|
+
if (!tool || typeof tool !== "object" || typeof tool.execute !== "function") throw new Error("Sandbox tool must provide execute(input, signal, context)");
|
|
69
|
+
return register("tool", rawName, tool, {
|
|
70
|
+
toolName: typeof tool.name === "string" ? tool.name : String(rawName),
|
|
71
|
+
description: typeof tool.description === "string" ? tool.description : "Plugin tool " + String(rawName),
|
|
72
|
+
modelInputSchema: cloneJson(tool.modelInputSchema), readOnly: tool.readOnly === true,
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
registerHook(rawName, handler, options) {
|
|
76
|
+
if (typeof handler !== "function") throw new Error("Sandbox hook handler must be a function");
|
|
77
|
+
return register("hook", rawName, handler, { options: cloneJson(options) });
|
|
78
|
+
},
|
|
79
|
+
registerSkillRoot(rawName, root) {
|
|
80
|
+
if (typeof root !== "string") throw new Error("Sandbox skill root must be a string");
|
|
81
|
+
return register("skillRoot", rawName, root, { root });
|
|
82
|
+
},
|
|
83
|
+
registerModelAdapter(rawName, adapter) {
|
|
84
|
+
if (!adapter || typeof adapter.stream !== "function") throw new Error("Sandbox model adapter must provide stream(request)");
|
|
85
|
+
return register("modelAdapter", rawName, adapter, {});
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
const describe = () => ({
|
|
89
|
+
commands: [...stores.command].map(([entryName, entry]) => ({ name: entryName, ...entry.metadata })),
|
|
90
|
+
tools: [...stores.tool].map(([entryName, entry]) => ({ name: entryName, ...entry.metadata })),
|
|
91
|
+
hooks: [...stores.hook].map(([entryName, entry]) => ({ name: entryName, ...entry.metadata })),
|
|
92
|
+
skillRoots: [...stores.skillRoot].map(([entryName, entry]) => ({ name: entryName, ...entry.metadata })),
|
|
93
|
+
modelAdapters: [...stores.modelAdapter].map(([entryName]) => ({ name: entryName })),
|
|
94
|
+
});
|
|
95
|
+
const invoke = async (kind, entryName, args) => {
|
|
96
|
+
const entry = stores[kind]?.get(entryName);
|
|
97
|
+
if (!entry) throw new Error("Unknown sandbox contribution: " + kind + ":" + entryName);
|
|
98
|
+
if (kind === "command") return entry.value(args[0], Object.freeze({ ...(args[1] || {}), signal }));
|
|
99
|
+
if (kind === "tool") return entry.value.execute(args[0], signal, args[1]);
|
|
100
|
+
if (kind === "hook") return entry.value(args[0], signal);
|
|
101
|
+
if (kind === "modelAdapter") {
|
|
102
|
+
const events = [];
|
|
103
|
+
for await (const event of entry.value.stream(args[0])) events.push(event);
|
|
104
|
+
return events;
|
|
105
|
+
}
|
|
106
|
+
throw new Error("Unsupported sandbox invocation kind: " + kind);
|
|
107
|
+
};
|
|
108
|
+
globalThis.__flavorSandbox = Object.freeze({
|
|
109
|
+
async activate(fn) {
|
|
110
|
+
const result = await fn(context);
|
|
111
|
+
if (result !== undefined && typeof result !== "function") throw new Error("Plugin activate result must be a disposer or undefined");
|
|
112
|
+
deactivate = result;
|
|
113
|
+
accepting = false;
|
|
114
|
+
return JSON.stringify(describe());
|
|
115
|
+
},
|
|
116
|
+
async invokeJson(kind, entryName, argsJson) {
|
|
117
|
+
try { return JSON.stringify({ ok: true, value: await invoke(kind, entryName, JSON.parse(argsJson)) }); }
|
|
118
|
+
catch (error) { return JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) }); }
|
|
119
|
+
},
|
|
120
|
+
async shutdown() { if (deactivate) await deactivate(); deactivate = undefined; },
|
|
121
|
+
});
|
|
122
|
+
})();`;
|
|
123
|
+
async function loadPlugin(pluginRoot, entryPath) {
|
|
124
|
+
const context = createContext(/* @__PURE__ */ Object.create(null), {
|
|
125
|
+
name: `flavor-plugin:${data.pluginName}`,
|
|
126
|
+
codeGeneration: { strings: false, wasm: false }
|
|
127
|
+
});
|
|
128
|
+
context.__flavorConfigJson = data.configJson;
|
|
129
|
+
runInContext(SANDBOX_BOOTSTRAP, context, { timeout: 1e3 });
|
|
130
|
+
const modules = /* @__PURE__ */ new Map();
|
|
131
|
+
const getModule = async (input) => {
|
|
132
|
+
const physical = await realpath(input);
|
|
133
|
+
assertContained(pluginRoot, physical);
|
|
134
|
+
const existing = modules.get(physical);
|
|
135
|
+
if (existing !== void 0) return existing;
|
|
136
|
+
const source = await readFile(physical, "utf8");
|
|
137
|
+
if (Buffer.byteLength(source) > MAX_MODULE_BYTES) throw new Error(`Plugin module exceeds ${MAX_MODULE_BYTES} bytes`);
|
|
138
|
+
const module = new SourceTextModule(source, {
|
|
139
|
+
context,
|
|
140
|
+
identifier: pathToFileURL(physical).href,
|
|
141
|
+
initializeImportMeta(meta) {
|
|
142
|
+
meta.url = pathToFileURL(physical).href;
|
|
143
|
+
},
|
|
144
|
+
importModuleDynamically: async (specifier, referencingModule) => {
|
|
145
|
+
const imported = await resolveModule(specifier, referencingModule);
|
|
146
|
+
if (imported.status === "unlinked") await imported.link(linker);
|
|
147
|
+
if (imported.status === "linked") await imported.evaluate();
|
|
148
|
+
return imported;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
modules.set(physical, module);
|
|
152
|
+
return module;
|
|
153
|
+
};
|
|
154
|
+
const resolveModule = async (specifier, referencingModule) => {
|
|
155
|
+
if (!specifier.startsWith("./") && !specifier.startsWith("../")) throw new Error(`Sandbox blocks external module import: ${specifier}`);
|
|
156
|
+
return getModule(resolve(fileURLToPath(referencingModule.identifier), "..", specifier));
|
|
157
|
+
};
|
|
158
|
+
const linker = (specifier, referencingModule) => resolveModule(specifier, referencingModule);
|
|
159
|
+
const entry = await getModule(entryPath);
|
|
160
|
+
await entry.link(linker);
|
|
161
|
+
await entry.evaluate();
|
|
162
|
+
const activate = entry.namespace.activate;
|
|
163
|
+
if (typeof activate !== "function") throw new Error("Plugin entry must export activate(context)");
|
|
164
|
+
context.__flavorActivate = activate;
|
|
165
|
+
const contributionsJson = await runInContext(
|
|
166
|
+
"globalThis.__flavorSandbox.activate(globalThis.__flavorActivate)",
|
|
167
|
+
context
|
|
168
|
+
);
|
|
169
|
+
return { context, contributions: JSON.parse(contributionsJson) };
|
|
170
|
+
}
|
|
171
|
+
async function main() {
|
|
172
|
+
const pluginRoot = await realpath(data.pluginRoot);
|
|
173
|
+
assertContained(pluginRoot, await realpath(data.entryPath));
|
|
174
|
+
const loaded = await loadPlugin(pluginRoot, data.entryPath);
|
|
175
|
+
const { contributions } = loaded;
|
|
176
|
+
const result = {
|
|
177
|
+
ok: true,
|
|
178
|
+
name: data.pluginName,
|
|
179
|
+
version: data.pluginVersion,
|
|
180
|
+
registeredTools: contributions.tools.map(({ name }) => name),
|
|
181
|
+
registeredCommands: contributions.commands.map(({ name }) => name),
|
|
182
|
+
registeredHooks: contributions.hooks.map(({ name }) => name),
|
|
183
|
+
registeredSkillRoots: contributions.skillRoots.map(({ name }) => name),
|
|
184
|
+
registeredModelAdapters: contributions.modelAdapters.map(({ name }) => name),
|
|
185
|
+
contributions
|
|
186
|
+
};
|
|
187
|
+
send({ type: "ready", result });
|
|
188
|
+
port.on("message", (raw) => {
|
|
189
|
+
void handleMessage(raw, loaded.context);
|
|
190
|
+
});
|
|
191
|
+
clearInterval(activationKeepAlive);
|
|
192
|
+
}
|
|
193
|
+
async function handleMessage(raw, context) {
|
|
194
|
+
if (typeof raw !== "object" || raw === null || !("type" in raw)) {
|
|
195
|
+
send({ type: "fatal", error: "Invalid sandbox parent message" });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const message = raw;
|
|
199
|
+
if (message.type === "shutdown") {
|
|
200
|
+
try {
|
|
201
|
+
await runInContext("globalThis.__flavorSandbox.shutdown()", context);
|
|
202
|
+
send({ type: "disposed" });
|
|
203
|
+
} catch (error) {
|
|
204
|
+
send({ type: "fatal", error: `Plugin sandbox shutdown failed: ${errorMessage(error)}` });
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (message.type !== "invoke" || !Number.isSafeInteger(message.id) || typeof message.name !== "string" || typeof message.argsJson !== "string" || !["command", "tool", "hook", "modelAdapter"].includes(message.kind)) {
|
|
209
|
+
send({ type: "fatal", error: "Invalid sandbox invocation message" });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
context.__flavorKind = message.kind;
|
|
213
|
+
context.__flavorName = message.name;
|
|
214
|
+
context.__flavorArgs = message.argsJson;
|
|
215
|
+
try {
|
|
216
|
+
const responseJson = await runInContext(
|
|
217
|
+
"globalThis.__flavorSandbox.invokeJson(globalThis.__flavorKind, globalThis.__flavorName, globalThis.__flavorArgs)",
|
|
218
|
+
context
|
|
219
|
+
);
|
|
220
|
+
const response = JSON.parse(responseJson);
|
|
221
|
+
if (response.ok) send({ type: "response", id: message.id, ok: true, valueJson: response.value === void 0 ? "null" : JSON.stringify(response.value) });
|
|
222
|
+
else send({ type: "response", id: message.id, ok: false, error: response.error ?? "Sandbox invocation failed" });
|
|
223
|
+
} catch (error) {
|
|
224
|
+
send({ type: "response", id: message.id, ok: false, error: errorMessage(error) });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
main().catch((error) => {
|
|
228
|
+
clearInterval(activationKeepAlive);
|
|
229
|
+
send({ type: "fatal", error: errorMessage(error) });
|
|
230
|
+
});
|
package/dist/evolve/loader.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export interface FixPluginVerifyReport {
|
|
|
13
13
|
commands: string[];
|
|
14
14
|
error?: string;
|
|
15
15
|
}
|
|
16
|
-
/**
|
|
16
|
+
/** Verify a plugin in an isolated Worker sandbox. The real host is untouched. */
|
|
17
17
|
export declare function verifyFixPlugin(workspace: string, name: string): Promise<FixPluginVerifyReport>;
|
|
18
18
|
/** Copy the current plugin dir into .versions/<name>/<timestamp>/. */
|
|
19
19
|
export declare function snapshotFixPlugin(workspace: string, name: string): Promise<string>;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module"; const require = createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// src/plugins/plugin-worker-entry.ts
|
|
5
|
+
import { readFile, realpath } from "fs/promises";
|
|
6
|
+
import { isAbsolute, relative, resolve, sep } from "path";
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
8
|
+
import { createContext, runInContext, SourceTextModule } from "vm";
|
|
9
|
+
import { parentPort, workerData } from "worker_threads";
|
|
10
|
+
var MAX_MODULE_BYTES = 2 * 1024 * 1024;
|
|
11
|
+
var CONTRIBUTION_NAME_SOURCE = "^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$";
|
|
12
|
+
var data = workerData;
|
|
13
|
+
var port = parentPort;
|
|
14
|
+
if (port === null) throw new Error("Plugin sandbox requires a parent port");
|
|
15
|
+
var activationKeepAlive = setInterval(() => void 0, 1e3);
|
|
16
|
+
function send(message) {
|
|
17
|
+
port.postMessage(message);
|
|
18
|
+
}
|
|
19
|
+
function errorMessage(error) {
|
|
20
|
+
return error instanceof Error ? error.message : String(error);
|
|
21
|
+
}
|
|
22
|
+
function assertContained(root, candidate) {
|
|
23
|
+
const delta = relative(root, candidate);
|
|
24
|
+
if (delta === ".." || delta.startsWith(`..${sep}`) || isAbsolute(delta)) throw new Error("Plugin module import escapes the plugin root");
|
|
25
|
+
}
|
|
26
|
+
var SANDBOX_BOOTSTRAP = `(() => {
|
|
27
|
+
"use strict";
|
|
28
|
+
const stores = { command: new Map(), tool: new Map(), hook: new Map(), skillRoot: new Map(), modelAdapter: new Map() };
|
|
29
|
+
let deactivate;
|
|
30
|
+
let accepting = true;
|
|
31
|
+
const signal = Object.freeze({ aborted: false, reason: undefined, addEventListener() {}, removeEventListener() {}, throwIfAborted() {} });
|
|
32
|
+
const cloneJson = (value) => value === undefined ? undefined : JSON.parse(JSON.stringify(value));
|
|
33
|
+
const deepFreeze = (value) => {
|
|
34
|
+
if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
|
|
35
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
36
|
+
return Object.freeze(value);
|
|
37
|
+
};
|
|
38
|
+
const config = deepFreeze(JSON.parse(globalThis.__flavorConfigJson));
|
|
39
|
+
delete globalThis.__flavorConfigJson;
|
|
40
|
+
const namePattern = new RegExp(${JSON.stringify(CONTRIBUTION_NAME_SOURCE)});
|
|
41
|
+
const name = (value) => {
|
|
42
|
+
const result = String(value);
|
|
43
|
+
if (!namePattern.test(result)) throw new Error("Invalid contribution name: " + result);
|
|
44
|
+
return result;
|
|
45
|
+
};
|
|
46
|
+
const register = (kind, rawName, value, metadata) => {
|
|
47
|
+
if (!accepting) throw new Error("Plugin registrations are closed after activation");
|
|
48
|
+
const contributionName = name(rawName);
|
|
49
|
+
if (stores[kind].has(contributionName)) throw new Error("Duplicate " + kind + " contribution: " + contributionName);
|
|
50
|
+
stores[kind].set(contributionName, { value, metadata });
|
|
51
|
+
let active = true;
|
|
52
|
+
return Object.freeze(function disposeRegistration() {
|
|
53
|
+
if (!active) return;
|
|
54
|
+
active = false;
|
|
55
|
+
stores[kind].delete(contributionName);
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
const context = Object.freeze({
|
|
59
|
+
signal,
|
|
60
|
+
config,
|
|
61
|
+
logger: Object.freeze({ debug() {}, info() {}, warn() {}, error() {} }),
|
|
62
|
+
services: Object.freeze({ filesystem: Object.freeze({
|
|
63
|
+
async readFile() { throw new Error("Filesystem read is not available in sandbox"); },
|
|
64
|
+
async writeFile() { throw new Error("Filesystem write is not available in sandbox"); },
|
|
65
|
+
}) }),
|
|
66
|
+
registerCommand(rawName, handler, description) {
|
|
67
|
+
if (typeof handler !== "function") throw new Error("Sandbox command handler must be a function");
|
|
68
|
+
return register("command", rawName, handler, { description: typeof description === "string" ? description : undefined });
|
|
69
|
+
},
|
|
70
|
+
registerTool(rawName, tool) {
|
|
71
|
+
if (!tool || typeof tool !== "object" || typeof tool.execute !== "function") throw new Error("Sandbox tool must provide execute(input, signal, context)");
|
|
72
|
+
return register("tool", rawName, tool, {
|
|
73
|
+
toolName: typeof tool.name === "string" ? tool.name : String(rawName),
|
|
74
|
+
description: typeof tool.description === "string" ? tool.description : "Plugin tool " + String(rawName),
|
|
75
|
+
modelInputSchema: cloneJson(tool.modelInputSchema), readOnly: tool.readOnly === true,
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
registerHook(rawName, handler, options) {
|
|
79
|
+
if (typeof handler !== "function") throw new Error("Sandbox hook handler must be a function");
|
|
80
|
+
return register("hook", rawName, handler, { options: cloneJson(options) });
|
|
81
|
+
},
|
|
82
|
+
registerSkillRoot(rawName, root) {
|
|
83
|
+
if (typeof root !== "string") throw new Error("Sandbox skill root must be a string");
|
|
84
|
+
return register("skillRoot", rawName, root, { root });
|
|
85
|
+
},
|
|
86
|
+
registerModelAdapter(rawName, adapter) {
|
|
87
|
+
if (!adapter || typeof adapter.stream !== "function") throw new Error("Sandbox model adapter must provide stream(request)");
|
|
88
|
+
return register("modelAdapter", rawName, adapter, {});
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
const describe = () => ({
|
|
92
|
+
commands: [...stores.command].map(([entryName, entry]) => ({ name: entryName, ...entry.metadata })),
|
|
93
|
+
tools: [...stores.tool].map(([entryName, entry]) => ({ name: entryName, ...entry.metadata })),
|
|
94
|
+
hooks: [...stores.hook].map(([entryName, entry]) => ({ name: entryName, ...entry.metadata })),
|
|
95
|
+
skillRoots: [...stores.skillRoot].map(([entryName, entry]) => ({ name: entryName, ...entry.metadata })),
|
|
96
|
+
modelAdapters: [...stores.modelAdapter].map(([entryName]) => ({ name: entryName })),
|
|
97
|
+
});
|
|
98
|
+
const invoke = async (kind, entryName, args) => {
|
|
99
|
+
const entry = stores[kind]?.get(entryName);
|
|
100
|
+
if (!entry) throw new Error("Unknown sandbox contribution: " + kind + ":" + entryName);
|
|
101
|
+
if (kind === "command") return entry.value(args[0], Object.freeze({ ...(args[1] || {}), signal }));
|
|
102
|
+
if (kind === "tool") return entry.value.execute(args[0], signal, args[1]);
|
|
103
|
+
if (kind === "hook") return entry.value(args[0], signal);
|
|
104
|
+
if (kind === "modelAdapter") {
|
|
105
|
+
const events = [];
|
|
106
|
+
for await (const event of entry.value.stream(args[0])) events.push(event);
|
|
107
|
+
return events;
|
|
108
|
+
}
|
|
109
|
+
throw new Error("Unsupported sandbox invocation kind: " + kind);
|
|
110
|
+
};
|
|
111
|
+
globalThis.__flavorSandbox = Object.freeze({
|
|
112
|
+
async activate(fn) {
|
|
113
|
+
const result = await fn(context);
|
|
114
|
+
if (result !== undefined && typeof result !== "function") throw new Error("Plugin activate result must be a disposer or undefined");
|
|
115
|
+
deactivate = result;
|
|
116
|
+
accepting = false;
|
|
117
|
+
return JSON.stringify(describe());
|
|
118
|
+
},
|
|
119
|
+
async invokeJson(kind, entryName, argsJson) {
|
|
120
|
+
try { return JSON.stringify({ ok: true, value: await invoke(kind, entryName, JSON.parse(argsJson)) }); }
|
|
121
|
+
catch (error) { return JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) }); }
|
|
122
|
+
},
|
|
123
|
+
async shutdown() { if (deactivate) await deactivate(); deactivate = undefined; },
|
|
124
|
+
});
|
|
125
|
+
})();`;
|
|
126
|
+
async function loadPlugin(pluginRoot, entryPath) {
|
|
127
|
+
const context = createContext(/* @__PURE__ */ Object.create(null), {
|
|
128
|
+
name: `flavor-plugin:${data.pluginName}`,
|
|
129
|
+
codeGeneration: { strings: false, wasm: false }
|
|
130
|
+
});
|
|
131
|
+
context.__flavorConfigJson = data.configJson;
|
|
132
|
+
runInContext(SANDBOX_BOOTSTRAP, context, { timeout: 1e3 });
|
|
133
|
+
const modules = /* @__PURE__ */ new Map();
|
|
134
|
+
const getModule = async (input) => {
|
|
135
|
+
const physical = await realpath(input);
|
|
136
|
+
assertContained(pluginRoot, physical);
|
|
137
|
+
const existing = modules.get(physical);
|
|
138
|
+
if (existing !== void 0) return existing;
|
|
139
|
+
const source = await readFile(physical, "utf8");
|
|
140
|
+
if (Buffer.byteLength(source) > MAX_MODULE_BYTES) throw new Error(`Plugin module exceeds ${MAX_MODULE_BYTES} bytes`);
|
|
141
|
+
const module = new SourceTextModule(source, {
|
|
142
|
+
context,
|
|
143
|
+
identifier: pathToFileURL(physical).href,
|
|
144
|
+
initializeImportMeta(meta) {
|
|
145
|
+
meta.url = pathToFileURL(physical).href;
|
|
146
|
+
},
|
|
147
|
+
importModuleDynamically: async (specifier, referencingModule) => {
|
|
148
|
+
const imported = await resolveModule(specifier, referencingModule);
|
|
149
|
+
if (imported.status === "unlinked") await imported.link(linker);
|
|
150
|
+
if (imported.status === "linked") await imported.evaluate();
|
|
151
|
+
return imported;
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
modules.set(physical, module);
|
|
155
|
+
return module;
|
|
156
|
+
};
|
|
157
|
+
const resolveModule = async (specifier, referencingModule) => {
|
|
158
|
+
if (!specifier.startsWith("./") && !specifier.startsWith("../")) throw new Error(`Sandbox blocks external module import: ${specifier}`);
|
|
159
|
+
return getModule(resolve(fileURLToPath(referencingModule.identifier), "..", specifier));
|
|
160
|
+
};
|
|
161
|
+
const linker = (specifier, referencingModule) => resolveModule(specifier, referencingModule);
|
|
162
|
+
const entry = await getModule(entryPath);
|
|
163
|
+
await entry.link(linker);
|
|
164
|
+
await entry.evaluate();
|
|
165
|
+
const activate = entry.namespace.activate;
|
|
166
|
+
if (typeof activate !== "function") throw new Error("Plugin entry must export activate(context)");
|
|
167
|
+
context.__flavorActivate = activate;
|
|
168
|
+
const contributionsJson = await runInContext(
|
|
169
|
+
"globalThis.__flavorSandbox.activate(globalThis.__flavorActivate)",
|
|
170
|
+
context
|
|
171
|
+
);
|
|
172
|
+
return { context, contributions: JSON.parse(contributionsJson) };
|
|
173
|
+
}
|
|
174
|
+
async function main() {
|
|
175
|
+
const pluginRoot = await realpath(data.pluginRoot);
|
|
176
|
+
assertContained(pluginRoot, await realpath(data.entryPath));
|
|
177
|
+
const loaded = await loadPlugin(pluginRoot, data.entryPath);
|
|
178
|
+
const { contributions } = loaded;
|
|
179
|
+
const result = {
|
|
180
|
+
ok: true,
|
|
181
|
+
name: data.pluginName,
|
|
182
|
+
version: data.pluginVersion,
|
|
183
|
+
registeredTools: contributions.tools.map(({ name }) => name),
|
|
184
|
+
registeredCommands: contributions.commands.map(({ name }) => name),
|
|
185
|
+
registeredHooks: contributions.hooks.map(({ name }) => name),
|
|
186
|
+
registeredSkillRoots: contributions.skillRoots.map(({ name }) => name),
|
|
187
|
+
registeredModelAdapters: contributions.modelAdapters.map(({ name }) => name),
|
|
188
|
+
contributions
|
|
189
|
+
};
|
|
190
|
+
send({ type: "ready", result });
|
|
191
|
+
port.on("message", (raw) => {
|
|
192
|
+
void handleMessage(raw, loaded.context);
|
|
193
|
+
});
|
|
194
|
+
clearInterval(activationKeepAlive);
|
|
195
|
+
}
|
|
196
|
+
async function handleMessage(raw, context) {
|
|
197
|
+
if (typeof raw !== "object" || raw === null || !("type" in raw)) {
|
|
198
|
+
send({ type: "fatal", error: "Invalid sandbox parent message" });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const message = raw;
|
|
202
|
+
if (message.type === "shutdown") {
|
|
203
|
+
try {
|
|
204
|
+
await runInContext("globalThis.__flavorSandbox.shutdown()", context);
|
|
205
|
+
send({ type: "disposed" });
|
|
206
|
+
} catch (error) {
|
|
207
|
+
send({ type: "fatal", error: `Plugin sandbox shutdown failed: ${errorMessage(error)}` });
|
|
208
|
+
}
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (message.type !== "invoke" || !Number.isSafeInteger(message.id) || typeof message.name !== "string" || typeof message.argsJson !== "string" || !["command", "tool", "hook", "modelAdapter"].includes(message.kind)) {
|
|
212
|
+
send({ type: "fatal", error: "Invalid sandbox invocation message" });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
context.__flavorKind = message.kind;
|
|
216
|
+
context.__flavorName = message.name;
|
|
217
|
+
context.__flavorArgs = message.argsJson;
|
|
218
|
+
try {
|
|
219
|
+
const responseJson = await runInContext(
|
|
220
|
+
"globalThis.__flavorSandbox.invokeJson(globalThis.__flavorKind, globalThis.__flavorName, globalThis.__flavorArgs)",
|
|
221
|
+
context
|
|
222
|
+
);
|
|
223
|
+
const response = JSON.parse(responseJson);
|
|
224
|
+
if (response.ok) send({ type: "response", id: message.id, ok: true, valueJson: response.value === void 0 ? "null" : JSON.stringify(response.value) });
|
|
225
|
+
else send({ type: "response", id: message.id, ok: false, error: response.error ?? "Sandbox invocation failed" });
|
|
226
|
+
} catch (error) {
|
|
227
|
+
send({ type: "response", id: message.id, ok: false, error: errorMessage(error) });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
main().catch((error) => {
|
|
231
|
+
clearInterval(activationKeepAlive);
|
|
232
|
+
send({ type: "fatal", error: errorMessage(error) });
|
|
233
|
+
});
|
package/dist/plugins/host.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { LoadedPlugin, PluginDiagnostic, PluginHostOptions } from "./types.js";
|
|
2
2
|
/**
|
|
3
|
-
* Hosts
|
|
4
|
-
*
|
|
5
|
-
* code already has Node.js authority; this is explicitly not a process sandbox.
|
|
3
|
+
* Hosts plugins behind a narrow host API. The default backend loads trusted plugins in-process;
|
|
4
|
+
* sandbox mode uses an isolated Worker/vm realm and RPC-backed contributions.
|
|
6
5
|
*/
|
|
7
6
|
export declare class PluginHost {
|
|
8
7
|
#private;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export type SandboxContributionKind = "command" | "tool" | "hook" | "modelAdapter";
|
|
2
|
+
export interface SandboxCommandRegistration {
|
|
3
|
+
name: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface SandboxToolRegistration {
|
|
7
|
+
name: string;
|
|
8
|
+
toolName: string;
|
|
9
|
+
description: string;
|
|
10
|
+
modelInputSchema?: Record<string, unknown>;
|
|
11
|
+
readOnly?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface SandboxHookRegistration {
|
|
14
|
+
name: string;
|
|
15
|
+
options?: {
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
failurePolicy?: "error" | "allow" | "deny" | "ask";
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export interface SandboxSkillRootRegistration {
|
|
21
|
+
name: string;
|
|
22
|
+
root: string;
|
|
23
|
+
}
|
|
24
|
+
export interface SandboxPluginContributions {
|
|
25
|
+
commands: SandboxCommandRegistration[];
|
|
26
|
+
tools: SandboxToolRegistration[];
|
|
27
|
+
hooks: SandboxHookRegistration[];
|
|
28
|
+
skillRoots: SandboxSkillRootRegistration[];
|
|
29
|
+
modelAdapters: {
|
|
30
|
+
name: string;
|
|
31
|
+
}[];
|
|
32
|
+
}
|
|
33
|
+
export interface SandboxedPluginResult {
|
|
34
|
+
ok: boolean;
|
|
35
|
+
name: string;
|
|
36
|
+
version: string;
|
|
37
|
+
registeredTools: string[];
|
|
38
|
+
registeredCommands: string[];
|
|
39
|
+
registeredHooks: string[];
|
|
40
|
+
registeredSkillRoots: string[];
|
|
41
|
+
registeredModelAdapters: string[];
|
|
42
|
+
contributions: SandboxPluginContributions;
|
|
43
|
+
error?: string;
|
|
44
|
+
}
|
|
45
|
+
export type SandboxParentMessage = {
|
|
46
|
+
type: "invoke";
|
|
47
|
+
id: number;
|
|
48
|
+
kind: SandboxContributionKind;
|
|
49
|
+
name: string;
|
|
50
|
+
argsJson: string;
|
|
51
|
+
} | {
|
|
52
|
+
type: "shutdown";
|
|
53
|
+
};
|
|
54
|
+
export type SandboxWorkerMessage = {
|
|
55
|
+
type: "ready";
|
|
56
|
+
result: SandboxedPluginResult;
|
|
57
|
+
} | {
|
|
58
|
+
type: "response";
|
|
59
|
+
id: number;
|
|
60
|
+
ok: true;
|
|
61
|
+
valueJson: string;
|
|
62
|
+
} | {
|
|
63
|
+
type: "response";
|
|
64
|
+
id: number;
|
|
65
|
+
ok: false;
|
|
66
|
+
error: string;
|
|
67
|
+
} | {
|
|
68
|
+
type: "disposed";
|
|
69
|
+
} | {
|
|
70
|
+
type: "fatal";
|
|
71
|
+
error: string;
|
|
72
|
+
};
|
package/dist/plugins/types.d.ts
CHANGED
|
@@ -134,4 +134,7 @@ export interface PluginHostOptions {
|
|
|
134
134
|
emitLifecycle?: (type: "PluginLoad" | "PluginUnload", plugin: LoadedPlugin) => void | Promise<void>;
|
|
135
135
|
activationTimeoutMs?: number;
|
|
136
136
|
unloadTimeoutMs?: number;
|
|
137
|
+
/** When true, plugin modules run in an isolated Worker/vm realm. Only relative
|
|
138
|
+
* modules inside the plugin root and RPC-backed contribution calls are available. */
|
|
139
|
+
sandbox?: boolean;
|
|
137
140
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { SandboxedPluginResult, SandboxContributionKind } from "./sandbox-protocol.js";
|
|
2
|
+
export type { SandboxedPluginResult } from "./sandbox-protocol.js";
|
|
3
|
+
export interface PluginWorkerOptions {
|
|
4
|
+
entryPath: string;
|
|
5
|
+
pluginRoot?: string;
|
|
6
|
+
pluginName: string;
|
|
7
|
+
pluginVersion: string;
|
|
8
|
+
config?: unknown;
|
|
9
|
+
activationTimeoutMs?: number;
|
|
10
|
+
invocationTimeoutMs?: number;
|
|
11
|
+
maxOldSpaceMb?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface SandboxedPluginSession {
|
|
14
|
+
readonly result: SandboxedPluginResult;
|
|
15
|
+
invoke<T>(kind: SandboxContributionKind, name: string, args: readonly unknown[]): Promise<T>;
|
|
16
|
+
dispose(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export declare function startPluginSandbox(options: PluginWorkerOptions): Promise<SandboxedPluginSession>;
|
|
19
|
+
export declare function runPluginInSandbox(options: PluginWorkerOptions): Promise<SandboxedPluginResult>;
|
package/dist/production.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ import { type TranscriptState } from "./ui/transcript.js";
|
|
|
11
11
|
import { MemoryReviewBridge } from "./memory/review.js";
|
|
12
12
|
import { type JobSnapshot } from "./jobs/registry.js";
|
|
13
13
|
import { type PalClientLike } from "./pals/tools.js";
|
|
14
|
+
/**
|
|
15
|
+
* Per-step shutdown budget. On Windows a hung child-process pipe, named pipe,
|
|
16
|
+
* or MCP stdio server can block one cleanup step forever; abandoning that step
|
|
17
|
+
* after the budget lets the remaining steps — and process exit — proceed.
|
|
18
|
+
*/
|
|
19
|
+
export declare const DEFAULT_SHUTDOWN_STEP_TIMEOUT_MS = 3000;
|
|
14
20
|
export interface ProductionRuntimeOptions {
|
|
15
21
|
workspace?: string;
|
|
16
22
|
home?: string;
|
|
@@ -27,6 +33,8 @@ export interface ProductionRuntimeOptions {
|
|
|
27
33
|
resumeSession?: string | true;
|
|
28
34
|
/** Test and embedding seam for creating configured MCP clients. */
|
|
29
35
|
mcpClientFactory?: McpClientFactory;
|
|
36
|
+
/** Per-step timeout (ms) for shutdown/disposal steps so a hung cleanup cannot block process exit. */
|
|
37
|
+
shutdownStepTimeoutMs?: number;
|
|
30
38
|
/** Additional tools provided by embedders (e.g. desktop-only D2C tools). */
|
|
31
39
|
extraTools?: readonly ToolDefinition<unknown>[];
|
|
32
40
|
/** Explicitly opt into CLI-local collaboration. Omit for print/RPC/eval callers. */
|
package/dist/sdk/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
FlavorRpcServer,
|
|
5
5
|
TraceRecordSchema,
|
|
6
6
|
TraceRecorder
|
|
7
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-A7PMQ22O.js";
|
|
8
8
|
import {
|
|
9
9
|
AgentMessageQueue,
|
|
10
10
|
DockerExecutionEnvironment,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
WorkspaceCheckpointStore,
|
|
14
14
|
buildDockerInvocation,
|
|
15
15
|
createProductionRuntime
|
|
16
|
-
} from "../chunk-
|
|
16
|
+
} from "../chunk-AXLUVJKC.js";
|
|
17
17
|
import "../chunk-IG4Z23CL.js";
|
|
18
18
|
import "../chunk-RQTYHUMK.js";
|
|
19
19
|
import "../chunk-S32JRDZK.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expand the portable argument placeholders used by Claude-style Agent Skills.
|
|
3
|
+
* Flavor still passes the original argument text as the user prompt; expansion
|
|
4
|
+
* makes the skill body itself equivalent across hosts and is especially useful
|
|
5
|
+
* when one skill loads another through the Skill tool.
|
|
6
|
+
*/
|
|
7
|
+
export declare function expandSkillArguments(body: string, argumentsText: string): string;
|
|
8
|
+
export declare function splitSkillArguments(input: string): string[];
|
package/dist/skills/tool.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { ToolDefinition } from "../tools/types.js";
|
|
3
3
|
import type { SkillRegistry } from "./registry.js";
|
|
4
|
+
declare const SkillInput: z.ZodObject<{
|
|
5
|
+
skill: z.ZodString;
|
|
6
|
+
arguments: z.ZodOptional<z.ZodString>;
|
|
7
|
+
}, z.core.$strict>;
|
|
4
8
|
declare const SkillResourceInput: z.ZodObject<{
|
|
5
9
|
skill: z.ZodString;
|
|
6
10
|
reference: z.ZodString;
|
|
7
11
|
}, z.core.$strict>;
|
|
12
|
+
export declare function createSkillTool(registry: SkillRegistry): ToolDefinition<z.infer<typeof SkillInput>>;
|
|
8
13
|
export declare function createSkillResourceTool(registry: SkillRegistry): ToolDefinition<z.infer<typeof SkillResourceInput>>;
|
|
9
14
|
export {};
|
package/dist/ui/session.d.ts
CHANGED
|
@@ -101,6 +101,7 @@ export interface SessionServices {
|
|
|
101
101
|
subagentModel(): string;
|
|
102
102
|
llmServiceName?(): string | undefined;
|
|
103
103
|
permissionMode(): PermissionMode;
|
|
104
|
+
addContext?(content: string): void | Promise<void>;
|
|
104
105
|
run(prompt: string, signal: AbortSignal, options?: {
|
|
105
106
|
getSteeringMessages(): readonly string[];
|
|
106
107
|
initialUserMessage?: Extract<ModelMessage, {
|