@standardagents/code 0.14.1-issue.658.a1599e → 0.14.1-issue.658.a1599e-linux-x64-gnu
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/bin/standard +4 -0
- package/bin/standardd +4 -0
- package/bin/tmux +0 -0
- package/lib/standard-plugin-runner/THIRD-PARTY.txt +252 -0
- package/lib/standard-plugin-runner/index.mjs +1125 -0
- package/lib/standard-plugin-runner/loader.mjs +210817 -0
- package/lib/standard-plugin-runner/runtime.json +1 -0
- package/lib/standard-plugin-runner/sdk.mjs +698 -0
- package/lib/standard-plugin-runner/worker.mjs +1663 -0
- package/licenses/THIRD-PARTY.txt +152 -0
- package/manifest.json +1 -0
- package/manifest.sig +1 -0
- package/package.json +18 -30
- package/bin/standard.mjs +0 -4
- package/bin/standardd.mjs +0 -4
- package/lib/artifact.mjs +0 -326
- package/lib/backend-profile.mjs +0 -114
- package/lib/launcher.mjs +0 -75
- package/lib/platform.mjs +0 -50
- package/postinstall.mjs +0 -12
|
@@ -0,0 +1,1125 @@
|
|
|
1
|
+
import { createRequire as __standardCreateRequire } from 'node:module'; import { fileURLToPath as __standardFileURLToPath } from 'node:url'; import { dirname as __standardDirname } from 'node:path'; const require = __standardCreateRequire(import.meta.url); const __filename = __standardFileURLToPath(import.meta.url); const __dirname = __standardDirname(__filename);
|
|
2
|
+
|
|
3
|
+
// src/supervisor.mjs
|
|
4
|
+
import { Worker } from "node:worker_threads";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
// ../plugin-sdk/src/manifest.mjs
|
|
8
|
+
var CAPABILITIES = Object.freeze([
|
|
9
|
+
"surfaces",
|
|
10
|
+
"events",
|
|
11
|
+
"hooks",
|
|
12
|
+
"panes",
|
|
13
|
+
"projects",
|
|
14
|
+
"notifications",
|
|
15
|
+
"url",
|
|
16
|
+
"fetch",
|
|
17
|
+
"secrets",
|
|
18
|
+
"webhook"
|
|
19
|
+
]);
|
|
20
|
+
var SURFACE_KINDS = Object.freeze([
|
|
21
|
+
"section",
|
|
22
|
+
"card",
|
|
23
|
+
"slot",
|
|
24
|
+
"badge",
|
|
25
|
+
"panel",
|
|
26
|
+
"overlay",
|
|
27
|
+
"menu",
|
|
28
|
+
"command",
|
|
29
|
+
"key",
|
|
30
|
+
"link"
|
|
31
|
+
]);
|
|
32
|
+
var PRESENTATIONS = Object.freeze(["popover", "column", "pane", "stage"]);
|
|
33
|
+
var MUX_EXPRESSIONS = Object.freeze([
|
|
34
|
+
"neutral",
|
|
35
|
+
"blink",
|
|
36
|
+
"happy",
|
|
37
|
+
"happy-smile",
|
|
38
|
+
"sad",
|
|
39
|
+
"grin",
|
|
40
|
+
"surprise",
|
|
41
|
+
"sleepy",
|
|
42
|
+
"angry",
|
|
43
|
+
"focus",
|
|
44
|
+
"question",
|
|
45
|
+
"exclaim",
|
|
46
|
+
"cross"
|
|
47
|
+
]);
|
|
48
|
+
var ANCHORS = Object.freeze([
|
|
49
|
+
"plugins",
|
|
50
|
+
"machine.before",
|
|
51
|
+
"machine.after",
|
|
52
|
+
"project.before",
|
|
53
|
+
"project.after",
|
|
54
|
+
"pane.header",
|
|
55
|
+
"pane.footer",
|
|
56
|
+
"account",
|
|
57
|
+
"machine",
|
|
58
|
+
"project",
|
|
59
|
+
"pane",
|
|
60
|
+
"section"
|
|
61
|
+
]);
|
|
62
|
+
var LIMITS = Object.freeze({
|
|
63
|
+
manifestBytes: 65536,
|
|
64
|
+
frameBytes: 262144,
|
|
65
|
+
responseFrameBytes: 64 * 1024 * 1024,
|
|
66
|
+
pendingRequests: 128,
|
|
67
|
+
subscriptions: 256,
|
|
68
|
+
schedules: 128,
|
|
69
|
+
contributions: 256,
|
|
70
|
+
queuedBytes: 4 * 1024 * 1024,
|
|
71
|
+
hookTimeoutMs: 6e4,
|
|
72
|
+
requestTimeoutMs: 3e4,
|
|
73
|
+
canvasColumns: 512,
|
|
74
|
+
canvasRows: 256,
|
|
75
|
+
canvasHoverBytes: 512,
|
|
76
|
+
canvasShapes: 256,
|
|
77
|
+
stateKeys: 256,
|
|
78
|
+
stateValueBytes: 64 * 1024
|
|
79
|
+
});
|
|
80
|
+
var PluginError = class extends Error {
|
|
81
|
+
constructor(code, message) {
|
|
82
|
+
super(message);
|
|
83
|
+
this.name = "PluginError";
|
|
84
|
+
this.code = code;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
function ensure(condition, code, message) {
|
|
88
|
+
if (!condition) throw new PluginError(code, message);
|
|
89
|
+
}
|
|
90
|
+
function object(value2) {
|
|
91
|
+
return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
|
|
92
|
+
}
|
|
93
|
+
function identifier(value2) {
|
|
94
|
+
return typeof value2 === "string" && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(value2);
|
|
95
|
+
}
|
|
96
|
+
function jsonBytes(value2, limit = LIMITS.frameBytes) {
|
|
97
|
+
const seen = /* @__PURE__ */ new Set();
|
|
98
|
+
function visit(item, depth) {
|
|
99
|
+
ensure(depth <= 32, "invalid_payload", "JSON nesting exceeds 32 levels");
|
|
100
|
+
if (item === null || typeof item === "string" || typeof item === "boolean") return;
|
|
101
|
+
if (typeof item === "number") {
|
|
102
|
+
ensure(Number.isFinite(item), "invalid_payload", "JSON numbers must be finite");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
ensure(object(item) || Array.isArray(item), "invalid_payload", "Payload must contain JSON values");
|
|
106
|
+
ensure(!seen.has(item), "invalid_payload", "Payload contains a cycle");
|
|
107
|
+
ensure(Array.isArray(item) || [Object.prototype, null].includes(Object.getPrototypeOf(item)), "invalid_payload", "Payload must contain plain objects");
|
|
108
|
+
seen.add(item);
|
|
109
|
+
for (const child of Object.values(item)) visit(child, depth + 1);
|
|
110
|
+
seen.delete(item);
|
|
111
|
+
}
|
|
112
|
+
visit(value2, 0);
|
|
113
|
+
const text = JSON.stringify(value2);
|
|
114
|
+
ensure(Buffer.byteLength(text) <= limit, "payload_too_large", `Payload exceeds ${limit} bytes`);
|
|
115
|
+
return text;
|
|
116
|
+
}
|
|
117
|
+
function freeze(value2) {
|
|
118
|
+
if (value2 && typeof value2 === "object") {
|
|
119
|
+
Object.values(value2).forEach(freeze);
|
|
120
|
+
Object.freeze(value2);
|
|
121
|
+
}
|
|
122
|
+
return value2;
|
|
123
|
+
}
|
|
124
|
+
function validateManifest(value2) {
|
|
125
|
+
ensure(object(value2), "invalid_manifest", "package.json must contain a standardPlugin object");
|
|
126
|
+
jsonBytes(value2, LIMITS.manifestBytes);
|
|
127
|
+
const allowed = [
|
|
128
|
+
"apiVersion",
|
|
129
|
+
"id",
|
|
130
|
+
"name",
|
|
131
|
+
"version",
|
|
132
|
+
"entry",
|
|
133
|
+
"singleton",
|
|
134
|
+
"order",
|
|
135
|
+
"capabilities",
|
|
136
|
+
"contributions",
|
|
137
|
+
"configSchema",
|
|
138
|
+
"hookTimeoutMs"
|
|
139
|
+
];
|
|
140
|
+
ensure(Object.keys(value2).every((key) => allowed.includes(key)), "invalid_manifest", "Unknown manifest field");
|
|
141
|
+
const manifest = structuredClone({ singleton: false, order: 0, capabilities: [], contributions: [], hookTimeoutMs: 5e3, ...value2 });
|
|
142
|
+
ensure(manifest.apiVersion === 1, "incompatible_version", "Plugin requires a different SDK API version");
|
|
143
|
+
ensure(identifier(manifest.id), "invalid_manifest", "Plugin id must be a stable identifier");
|
|
144
|
+
ensure(
|
|
145
|
+
typeof manifest.name === "string" && manifest.name.trim().length > 0 && manifest.name.length <= 128,
|
|
146
|
+
"invalid_manifest",
|
|
147
|
+
"Plugin name is required and bounded to 128 characters"
|
|
148
|
+
);
|
|
149
|
+
ensure(
|
|
150
|
+
typeof manifest.version === "string" && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?$/.test(manifest.version),
|
|
151
|
+
"invalid_manifest",
|
|
152
|
+
"Plugin version must be semantic"
|
|
153
|
+
);
|
|
154
|
+
ensure(
|
|
155
|
+
typeof manifest.entry === "string" && manifest.entry.length <= 512 && !/[\\\u0000?#]/.test(manifest.entry) && !manifest.entry.startsWith("/") && manifest.entry.split("/").every((part) => part && part !== "..") && /\.(?:mjs|cjs|js|mts|ts)$/.test(manifest.entry),
|
|
156
|
+
"invalid_manifest",
|
|
157
|
+
"Plugin entry must be a relative JavaScript or TypeScript file"
|
|
158
|
+
);
|
|
159
|
+
ensure(
|
|
160
|
+
typeof manifest.singleton === "boolean" && Number.isSafeInteger(manifest.order) && Math.abs(manifest.order) <= 2147483647,
|
|
161
|
+
"invalid_manifest",
|
|
162
|
+
"Invalid singleton or order"
|
|
163
|
+
);
|
|
164
|
+
ensure(
|
|
165
|
+
Number.isSafeInteger(manifest.hookTimeoutMs) && manifest.hookTimeoutMs > 0 && manifest.hookTimeoutMs <= LIMITS.hookTimeoutMs,
|
|
166
|
+
"invalid_manifest",
|
|
167
|
+
"Hook timeout must be between 1 and 60000 ms"
|
|
168
|
+
);
|
|
169
|
+
ensure(Array.isArray(manifest.capabilities) && manifest.capabilities.every((item) => CAPABILITIES.includes(item)) && new Set(manifest.capabilities).size === manifest.capabilities.length, "invalid_manifest", "Unknown or repeated capability");
|
|
170
|
+
ensure(
|
|
171
|
+
Array.isArray(manifest.contributions) && manifest.contributions.length <= LIMITS.contributions,
|
|
172
|
+
"invalid_manifest",
|
|
173
|
+
"Too many contributions"
|
|
174
|
+
);
|
|
175
|
+
const ids = /* @__PURE__ */ new Set();
|
|
176
|
+
for (const declaration of manifest.contributions) {
|
|
177
|
+
ensure(object(declaration) && identifier(declaration.id) && !ids.has(declaration.id) && SURFACE_KINDS.includes(declaration.kind) && ANCHORS.includes(declaration.anchor), "invalid_manifest", "Invalid contribution declaration");
|
|
178
|
+
ids.add(declaration.id);
|
|
179
|
+
ensure(
|
|
180
|
+
declaration.icon === void 0 || ["card", "panel"].includes(declaration.kind) && typeof declaration.icon === "string" && /^[\uE000-\uF8FF\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]$/u.test(declaration.icon),
|
|
181
|
+
"invalid_manifest",
|
|
182
|
+
"Card or panel icon must be one Nerd Font private-use glyph"
|
|
183
|
+
);
|
|
184
|
+
ensure(
|
|
185
|
+
declaration.border === void 0 || typeof declaration.border === "boolean" && declaration.kind === "card",
|
|
186
|
+
"invalid_manifest",
|
|
187
|
+
"Only sidebar cards declare a boolean border preference"
|
|
188
|
+
);
|
|
189
|
+
for (const [key, choices] of Object.entries({
|
|
190
|
+
merge: ["by-machine", "by-identity"],
|
|
191
|
+
width: ["full", "half"],
|
|
192
|
+
position: ["top", "after-open", "before-danger", "bottom"],
|
|
193
|
+
presentation: PRESENTATIONS
|
|
194
|
+
})) {
|
|
195
|
+
ensure(declaration[key] === void 0 || choices.includes(declaration[key]), "invalid_manifest", `Invalid contribution ${key}`);
|
|
196
|
+
}
|
|
197
|
+
for (const key of ["title", "group", "chord", "pattern", "actionId"]) {
|
|
198
|
+
ensure(
|
|
199
|
+
declaration[key] === void 0 || typeof declaration[key] === "string" && declaration[key].length <= 512,
|
|
200
|
+
"invalid_manifest",
|
|
201
|
+
`Invalid contribution ${key}`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
ensure(declaration.opens === void 0 || identifier(declaration.opens), "invalid_manifest", "Invalid contribution opens");
|
|
205
|
+
ensure(
|
|
206
|
+
declaration.presentation === void 0 || ["panel", "card", "command"].includes(declaration.kind),
|
|
207
|
+
"invalid_manifest",
|
|
208
|
+
"Only panels, cards, and commands declare a presentation"
|
|
209
|
+
);
|
|
210
|
+
ensure(declaration.kind !== "card" || declaration.anchor === "plugins", "invalid_manifest", "Cards use the plugins anchor");
|
|
211
|
+
ensure(
|
|
212
|
+
declaration.presentation !== "stage" || declaration.kind !== "panel" || declaration.anchor === "account",
|
|
213
|
+
"invalid_manifest",
|
|
214
|
+
"Stage panels use the account anchor"
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
for (const declaration of manifest.contributions) {
|
|
218
|
+
ensure(
|
|
219
|
+
declaration.opens === void 0 || manifest.contributions.some((item) => item.id === declaration.opens && item.kind === "panel"),
|
|
220
|
+
"invalid_manifest",
|
|
221
|
+
"Contribution opens must name a declared panel"
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
ensure(!manifest.contributions.length || manifest.capabilities.includes("surfaces"), "invalid_manifest", "Contributions require surfaces capability");
|
|
225
|
+
ensure(manifest.configSchema === void 0 || object(manifest.configSchema), "invalid_manifest", "Configuration schema must be an object");
|
|
226
|
+
return freeze(manifest);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ../plugin-sdk/src/protocol.mjs
|
|
230
|
+
var RUNNER_PROTOCOL_VERSION = 1;
|
|
231
|
+
var OPERATIONS = Object.freeze({
|
|
232
|
+
"pane.create": "panes",
|
|
233
|
+
"pane.close": "panes",
|
|
234
|
+
"pane.restart": "panes",
|
|
235
|
+
"pane.input": "panes",
|
|
236
|
+
"pane.focus": "panes",
|
|
237
|
+
"pane.wait": "panes",
|
|
238
|
+
"project.create": "projects",
|
|
239
|
+
"project.remove": "projects",
|
|
240
|
+
"notification.show": "notifications",
|
|
241
|
+
"url.open": "url",
|
|
242
|
+
fetch: "fetch",
|
|
243
|
+
"secret.get": "secrets",
|
|
244
|
+
"config.get": null,
|
|
245
|
+
"state.get": null,
|
|
246
|
+
"state.set": null,
|
|
247
|
+
"state.keys": null,
|
|
248
|
+
"panel.open": "surfaces",
|
|
249
|
+
"context.get": null,
|
|
250
|
+
"popover.open": "surfaces",
|
|
251
|
+
"canvas.write": "surfaces",
|
|
252
|
+
"remote.request": "surfaces",
|
|
253
|
+
"canvas.focus": "surfaces",
|
|
254
|
+
"subscription.add": null,
|
|
255
|
+
"subscription.remove": null,
|
|
256
|
+
"health.set": null,
|
|
257
|
+
"webhook.ack": "webhook"
|
|
258
|
+
});
|
|
259
|
+
var HOST_OPERATIONS = Object.freeze([
|
|
260
|
+
"runner.start",
|
|
261
|
+
"runner.stop",
|
|
262
|
+
"runner.reload",
|
|
263
|
+
"runner.diagnose",
|
|
264
|
+
"runner.ping",
|
|
265
|
+
"runtime.invoke",
|
|
266
|
+
"runtime.visibility"
|
|
267
|
+
]);
|
|
268
|
+
function authorize(operation, capabilities) {
|
|
269
|
+
ensure(
|
|
270
|
+
object(operation) && Object.hasOwn(OPERATIONS, operation.op) && object(operation.args),
|
|
271
|
+
"unsupported_operation",
|
|
272
|
+
"Unknown SDK operation or invalid arguments"
|
|
273
|
+
);
|
|
274
|
+
let capability = OPERATIONS[operation.op];
|
|
275
|
+
if (operation.op === "subscription.add") {
|
|
276
|
+
const { id, kind, name } = operation.args;
|
|
277
|
+
ensure(
|
|
278
|
+
identifier(id) && typeof name === "string" && name.length <= 256 && ["event", "hook", "action", "input", "select", "resize", "activate", "deactivate", "visibility"].includes(kind),
|
|
279
|
+
"invalid_payload",
|
|
280
|
+
"Invalid subscription"
|
|
281
|
+
);
|
|
282
|
+
capability = kind === "hook" ? "hooks" : kind === "event" ? name === "webhook" ? "webhook" : "events" : "surfaces";
|
|
283
|
+
}
|
|
284
|
+
ensure(!capability || capabilities.includes(capability), "capability_denied", `Operation requires ${capability} capability`);
|
|
285
|
+
jsonBytes(operation);
|
|
286
|
+
}
|
|
287
|
+
function validateProducer(producer) {
|
|
288
|
+
ensure(
|
|
289
|
+
object(producer) && identifier(producer.pluginId) && typeof producer.machineId === "string" && producer.machineId.length > 0 && producer.machineId.length <= 128 && typeof producer.epoch === "string" && /^(0|[1-9]\d{0,19})$/.test(producer.epoch) && BigInt(producer.epoch) <= 18446744073709551615n,
|
|
290
|
+
"invalid_payload",
|
|
291
|
+
"Invalid plugin producer identity"
|
|
292
|
+
);
|
|
293
|
+
return producer;
|
|
294
|
+
}
|
|
295
|
+
function sameProducer(a, b) {
|
|
296
|
+
return a.pluginId === b.pluginId && a.machineId === b.machineId && a.epoch === b.epoch;
|
|
297
|
+
}
|
|
298
|
+
function frameLimit(frame, inbound) {
|
|
299
|
+
return inbound && object(frame) && frame.kind === "response" ? LIMITS.responseFrameBytes : LIMITS.frameBytes;
|
|
300
|
+
}
|
|
301
|
+
function validateEnvelope(frame, { inbound = false } = {}) {
|
|
302
|
+
jsonBytes(frame, frameLimit(frame, inbound));
|
|
303
|
+
ensure(object(frame) && frame.version === RUNNER_PROTOCOL_VERSION, "incompatible_version", "Daemon and runner protocol versions differ");
|
|
304
|
+
validateProducer(frame.producer);
|
|
305
|
+
ensure(["request", "response", "cancel", "surface", "report"].includes(frame.kind), "invalid_payload", "Unknown runner frame kind");
|
|
306
|
+
if (frame.kind === "report") {
|
|
307
|
+
ensure(
|
|
308
|
+
identifier(frame.name) && object(frame.entity) && typeof frame.sequence === "string" && /^(0|[1-9]\d{0,19})$/.test(frame.sequence) && BigInt(frame.sequence) <= 18446744073709551615n && Number.isInteger(frame.schemaVersion) && frame.schemaVersion > 0 && frame.schemaVersion <= 65535 && Object.hasOwn(frame, "value") && frame.value !== void 0,
|
|
309
|
+
"invalid_payload",
|
|
310
|
+
"Invalid report replacement"
|
|
311
|
+
);
|
|
312
|
+
} else if (frame.kind === "surface") {
|
|
313
|
+
ensure(object(frame.key) && identifier(frame.key.contributionId) && typeof frame.sequence === "string" && /^(0|[1-9]\d{0,19})$/.test(frame.sequence) && BigInt(frame.sequence) <= 18446744073709551615n && Object.hasOwn(frame, "content"), "invalid_payload", "Invalid surface replacement");
|
|
314
|
+
} else {
|
|
315
|
+
ensure(typeof frame.id === "string" && frame.id.length > 0 && frame.id.length <= 128, "invalid_payload", "Missing correlation id");
|
|
316
|
+
if (frame.kind === "request") {
|
|
317
|
+
ensure(
|
|
318
|
+
object(frame.operation) && (Object.hasOwn(OPERATIONS, frame.operation.op) || HOST_OPERATIONS.includes(frame.operation.op)) && object(frame.operation.args) && Number.isSafeInteger(frame.timeoutMs) && frame.timeoutMs > 0 && frame.timeoutMs <= LIMITS.hookTimeoutMs,
|
|
319
|
+
"invalid_payload",
|
|
320
|
+
"Invalid runner request"
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
if (frame.kind === "response") {
|
|
324
|
+
ensure(
|
|
325
|
+
object(frame.result) && typeof frame.result.ok === "boolean" && (frame.result.ok ? Object.hasOwn(frame.result, "value") : object(frame.result.error) && typeof frame.result.error.code === "string" && typeof frame.result.error.message === "string"),
|
|
326
|
+
"invalid_payload",
|
|
327
|
+
"Invalid runner response"
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return frame;
|
|
332
|
+
}
|
|
333
|
+
function wireError(error) {
|
|
334
|
+
return {
|
|
335
|
+
code: error instanceof PluginError ? error.code : "plugin_error",
|
|
336
|
+
message: String(error?.message ?? "Plugin operation failed").slice(0, 1024)
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
var realClock = Object.freeze({ now: () => Date.now(), setTimeout: (fn, ms) => setTimeout(fn, ms), clearTimeout: (id) => clearTimeout(id) });
|
|
340
|
+
var RpcPeer = class {
|
|
341
|
+
constructor({ producer, send, clock = realClock, onRequest, onSurface, onReport, onError = () => {
|
|
342
|
+
}, idPrefix = "host" }) {
|
|
343
|
+
this.producer = Object.freeze({ ...validateProducer(producer) });
|
|
344
|
+
this.send = send;
|
|
345
|
+
this.clock = clock;
|
|
346
|
+
this.onRequest = onRequest;
|
|
347
|
+
this.onSurface = onSurface;
|
|
348
|
+
this.onReport = onReport;
|
|
349
|
+
this.onError = onError;
|
|
350
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
351
|
+
this.incoming = /* @__PURE__ */ new Map();
|
|
352
|
+
this.nextId = 0;
|
|
353
|
+
this.idPrefix = idPrefix;
|
|
354
|
+
this.closed = false;
|
|
355
|
+
}
|
|
356
|
+
frame(kind, values) {
|
|
357
|
+
return { version: RUNNER_PROTOCOL_VERSION, producer: this.producer, kind, ...values };
|
|
358
|
+
}
|
|
359
|
+
transmit(frame) {
|
|
360
|
+
validateEnvelope(frame);
|
|
361
|
+
this.send(frame);
|
|
362
|
+
}
|
|
363
|
+
request(operation, { signal, timeoutMs = LIMITS.requestTimeoutMs } = {}) {
|
|
364
|
+
if (this.closed) return Promise.reject(new PluginError("disposed", "Plugin runtime is disposed"));
|
|
365
|
+
if (signal?.aborted) return Promise.reject(new PluginError("cancelled", "Request was cancelled"));
|
|
366
|
+
if (this.pending.size >= LIMITS.pendingRequests) return Promise.reject(new PluginError("queue_full", "Too many pending requests"));
|
|
367
|
+
const id = `${this.idPrefix}-${++this.nextId}`;
|
|
368
|
+
return new Promise((resolve2, reject) => {
|
|
369
|
+
let timer;
|
|
370
|
+
const finish = (error, value2) => {
|
|
371
|
+
if (!this.pending.delete(id)) return;
|
|
372
|
+
this.clock.clearTimeout(timer);
|
|
373
|
+
signal?.removeEventListener("abort", cancel);
|
|
374
|
+
error ? reject(error) : resolve2(value2);
|
|
375
|
+
};
|
|
376
|
+
const cancelWith = (code) => {
|
|
377
|
+
finish(new PluginError(code, code === "timeout" ? "Request deadline elapsed" : "Request was cancelled"));
|
|
378
|
+
try {
|
|
379
|
+
this.transmit(this.frame("cancel", { id }));
|
|
380
|
+
} catch (error) {
|
|
381
|
+
this.onError(error);
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
const cancel = () => cancelWith("cancelled");
|
|
385
|
+
this.pending.set(id, { finish });
|
|
386
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
387
|
+
timer = this.clock.setTimeout(() => cancelWith("timeout"), timeoutMs);
|
|
388
|
+
try {
|
|
389
|
+
this.transmit(this.frame("request", { id, operation, timeoutMs }));
|
|
390
|
+
} catch (error) {
|
|
391
|
+
finish(error);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
receive(frame) {
|
|
396
|
+
if (this.closed) return;
|
|
397
|
+
validateEnvelope(frame, { inbound: true });
|
|
398
|
+
ensure(sameProducer(frame.producer, this.producer), "stale_producer", "Frame belongs to another plugin instance");
|
|
399
|
+
if (frame.kind === "surface") {
|
|
400
|
+
this.onSurface?.(frame);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (frame.kind === "report") {
|
|
404
|
+
this.onReport?.(frame);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (frame.kind === "response") {
|
|
408
|
+
const result = frame.result;
|
|
409
|
+
this.pending.get(frame.id)?.finish(result.ok ? null : new PluginError(result.error.code, result.error.message), result.value);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (frame.kind === "cancel") {
|
|
413
|
+
this.incoming.get(frame.id)?.cancel();
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
ensure(!this.incoming.has(frame.id), "duplicate_request", "Request id is still in flight");
|
|
417
|
+
const respond = (result) => {
|
|
418
|
+
if (!this.closed) {
|
|
419
|
+
try {
|
|
420
|
+
this.transmit(this.frame("response", { id: frame.id, result }));
|
|
421
|
+
} catch (error) {
|
|
422
|
+
this.onError(error);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
if (this.incoming.size >= LIMITS.pendingRequests) {
|
|
427
|
+
respond({ ok: false, error: { code: "queue_full", message: "Too many incoming requests" } });
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const controller = new AbortController();
|
|
431
|
+
const cancel = () => {
|
|
432
|
+
controller.abort();
|
|
433
|
+
this.clock.clearTimeout(timer);
|
|
434
|
+
};
|
|
435
|
+
const timer = this.clock.setTimeout(() => {
|
|
436
|
+
cancel();
|
|
437
|
+
respond({ ok: false, error: { code: "timeout", message: "Handler deadline elapsed" } });
|
|
438
|
+
}, frame.timeoutMs);
|
|
439
|
+
this.incoming.set(frame.id, { cancel });
|
|
440
|
+
Promise.resolve().then(() => {
|
|
441
|
+
ensure(!controller.signal.aborted, "cancelled", "Request was cancelled");
|
|
442
|
+
ensure(typeof this.onRequest === "function", "unsupported_operation", "No request handler");
|
|
443
|
+
return this.onRequest(frame.operation, { signal: controller.signal, timeoutMs: frame.timeoutMs });
|
|
444
|
+
}).then((value2) => {
|
|
445
|
+
if (!controller.signal.aborted) respond({ ok: true, value: value2 ?? null });
|
|
446
|
+
}, (error) => {
|
|
447
|
+
if (!controller.signal.aborted) respond({ ok: false, error: wireError(error) });
|
|
448
|
+
}).finally(() => {
|
|
449
|
+
this.clock.clearTimeout(timer);
|
|
450
|
+
this.incoming.delete(frame.id);
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
dispose() {
|
|
454
|
+
if (this.closed) return;
|
|
455
|
+
for (const [id, request] of this.pending) {
|
|
456
|
+
request.finish(new PluginError("disposed", "Plugin runtime was disposed"));
|
|
457
|
+
try {
|
|
458
|
+
this.transmit(this.frame("cancel", { id }));
|
|
459
|
+
} catch (error) {
|
|
460
|
+
this.onError(error);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
this.closed = true;
|
|
464
|
+
for (const request of this.incoming.values()) request.cancel();
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
// ../plugin-sdk/src/view.mjs
|
|
469
|
+
var VIEW_LIMITS = Object.freeze({
|
|
470
|
+
depth: 8,
|
|
471
|
+
nodes: 4096,
|
|
472
|
+
tableRows: 512,
|
|
473
|
+
tableColumns: 12,
|
|
474
|
+
items: 512,
|
|
475
|
+
logLines: 2048,
|
|
476
|
+
cardLines: 6,
|
|
477
|
+
actionValueBytes: 512
|
|
478
|
+
});
|
|
479
|
+
var TONES = Object.freeze(["ok", "info", "warn", "error", "muted", "accent", "pending", "bright"]);
|
|
480
|
+
function compact(value2) {
|
|
481
|
+
return Object.fromEntries(Object.entries(value2).filter(([, item]) => item !== void 0));
|
|
482
|
+
}
|
|
483
|
+
var node = (type, fields) => compact({ type, ...fields });
|
|
484
|
+
var clockTime = (at) => `${new Date(at).toISOString().slice(11, 16)} UTC`;
|
|
485
|
+
var timeSpan = (style, at, { text: fallback, tone, weight, mono } = {}) => compact({
|
|
486
|
+
text: fallback ?? (style === "duration" ? `since ${clockTime(at)}` : clockTime(at)),
|
|
487
|
+
tone,
|
|
488
|
+
weight,
|
|
489
|
+
mono,
|
|
490
|
+
time: { at: Math.max(0, Math.floor(at)), style }
|
|
491
|
+
});
|
|
492
|
+
var value = (content) => Array.isArray(content) ? { value: content.map((span) => span.text).join(""), valueSpans: content.map(compact) } : { value: content };
|
|
493
|
+
var ui = Object.freeze({
|
|
494
|
+
view: (root, { title } = {}) => compact({ kind: "view", root, title }),
|
|
495
|
+
action: (actionId, { value: value2, opens, agentPicker } = {}) => compact({ actionId, value: value2, opens, agentPicker }),
|
|
496
|
+
span: (content, { tone, weight, mono, time } = {}) => compact({ text: content, tone, weight, mono, time }),
|
|
497
|
+
elapsed: (since, options) => timeSpan("duration", since, options),
|
|
498
|
+
relativeTime: (at, options) => timeSpan("relative", at, options),
|
|
499
|
+
stack: (items, { gap } = {}) => node("stack", { gap, children: items }),
|
|
500
|
+
row: (items, { align } = {}) => node("row", { children: items, align }),
|
|
501
|
+
divider: (label) => node("divider", { label }),
|
|
502
|
+
/** A string sets `text`; an array of spans sets `spans`. */
|
|
503
|
+
text: (content, { tone, weight, mono } = {}) => node("text", { ...Array.isArray(content) ? { spans: content.map(compact) } : { text: content }, tone, weight, mono }),
|
|
504
|
+
badge: (label, tone) => node("badge", { label, tone }),
|
|
505
|
+
dot: (tone) => node("dot", { tone }),
|
|
506
|
+
progress: (value2, max, { tone, label } = {}) => node("progress", { value: value2, max, tone, label }),
|
|
507
|
+
segments: (items) => node("segments", { items: items.map(compact) }),
|
|
508
|
+
card: (items, { title, tone, tags, action, externalAction, footer } = {}) => node("card", { title, tone, tags, action, externalAction, footer, children: items }),
|
|
509
|
+
stat: (label, content, { tone, hint } = {}) => node("stat", { label, ...value(content), tone, hint }),
|
|
510
|
+
kv: (items) => node("kv", { items: items.map((item) => compact({ ...item, ...value(item.value) })) }),
|
|
511
|
+
tabs: (id, items, { filters, action } = {}) => node("tabs", { id, items: items.map(compact), filters, action }),
|
|
512
|
+
select: (id, label, options, { filters, action } = {}) => node("select", { id, label, options: options.map(compact), filters, action }),
|
|
513
|
+
table: (id, columns, rows = []) => node("table", { id, columns: columns.map(compact), rows: rows.map(compact) }),
|
|
514
|
+
log: (lines) => node("log", { lines }),
|
|
515
|
+
/** `action` is an action object or an action id. */
|
|
516
|
+
button: (label, action) => node("button", { label, action: typeof action === "string" ? { actionId: action } : action }),
|
|
517
|
+
input: (id, value2, action, { placeholder, maxLength } = {}) => node("input", { id, value: value2, placeholder, maxLength, action: typeof action === "string" ? { actionId: action } : action })
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// ../plugin-sdk/src/runtime.mjs
|
|
521
|
+
var always = Object.freeze({ kind: "always" });
|
|
522
|
+
|
|
523
|
+
// src/prepare.mjs
|
|
524
|
+
import { constants } from "node:fs";
|
|
525
|
+
import { open, realpath, stat } from "node:fs/promises";
|
|
526
|
+
import { createHash } from "node:crypto";
|
|
527
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
528
|
+
function contained(root, path) {
|
|
529
|
+
const part = relative(root, path);
|
|
530
|
+
return part !== ".." && !part.startsWith(`..${sep}`) && !isAbsolute(part);
|
|
531
|
+
}
|
|
532
|
+
async function inspectPlugin(root) {
|
|
533
|
+
ensure(typeof root === "string" && isAbsolute(root), "invalid_source", "Plugin root must be absolute");
|
|
534
|
+
root = await realpath(root);
|
|
535
|
+
const file = await open(join(root, "package.json"), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
536
|
+
let source;
|
|
537
|
+
try {
|
|
538
|
+
const metadata = await file.stat();
|
|
539
|
+
ensure(metadata.isFile() && metadata.size > 0 && metadata.size <= LIMITS.manifestBytes, "invalid_manifest", "Package metadata exceeds its size limit");
|
|
540
|
+
const buffer = Buffer.alloc(LIMITS.manifestBytes + 1);
|
|
541
|
+
let size = 0;
|
|
542
|
+
while (size < buffer.length) {
|
|
543
|
+
const read = await file.read(buffer, size, buffer.length - size, size);
|
|
544
|
+
if (!read.bytesRead) break;
|
|
545
|
+
size += read.bytesRead;
|
|
546
|
+
}
|
|
547
|
+
ensure(size <= LIMITS.manifestBytes, "invalid_manifest", "Package metadata exceeds its size limit");
|
|
548
|
+
source = buffer.subarray(0, size).toString("utf8");
|
|
549
|
+
} finally {
|
|
550
|
+
await file.close();
|
|
551
|
+
}
|
|
552
|
+
const packageMetadata = JSON.parse(source);
|
|
553
|
+
const manifest = validateManifest(packageMetadata.standardPlugin);
|
|
554
|
+
const entry = await realpath(resolve(root, manifest.entry));
|
|
555
|
+
ensure(contained(root, entry) && (await stat(entry)).isFile(), "invalid_source", "Plugin entry must remain inside its source directory");
|
|
556
|
+
const manifestDigest = createHash("sha256").update(source).digest("hex");
|
|
557
|
+
return Object.freeze({ root, entry, manifest, manifestDigest });
|
|
558
|
+
}
|
|
559
|
+
async function preparePlugin({ root, manifestDigest, capabilities }) {
|
|
560
|
+
const prepared = await inspectPlugin(root);
|
|
561
|
+
ensure(prepared.manifestDigest === manifestDigest, "manifest_changed", "Plugin metadata changed after preparation or consent");
|
|
562
|
+
ensure(
|
|
563
|
+
Array.isArray(capabilities) && prepared.manifest.capabilities.every((capability) => capabilities.includes(capability)),
|
|
564
|
+
"consent_required",
|
|
565
|
+
"Plugin capabilities require installation consent"
|
|
566
|
+
);
|
|
567
|
+
return prepared;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// src/channel.mjs
|
|
571
|
+
var framed = (message) => message?.type === "frame" ? message.frame : message;
|
|
572
|
+
var Outbox = class {
|
|
573
|
+
constructor(write) {
|
|
574
|
+
this.write = write;
|
|
575
|
+
this.queue = [];
|
|
576
|
+
this.bytes = 0;
|
|
577
|
+
this.closed = false;
|
|
578
|
+
}
|
|
579
|
+
push(message) {
|
|
580
|
+
ensure(!this.closed, "disposed", "Output channel is closed");
|
|
581
|
+
const frame = framed(message);
|
|
582
|
+
const serialized = jsonBytes(message, frameLimit(frame, true));
|
|
583
|
+
const size = Buffer.byteLength(serialized);
|
|
584
|
+
const key = frame.kind === "surface" ? JSON.stringify(["surface", frame.producer, frame.key]) : frame.kind === "report" ? JSON.stringify(["report", frame.producer, frame.name, frame.entity]) : null;
|
|
585
|
+
const previous = key === null ? -1 : this.queue.findIndex((item2) => item2.key === key);
|
|
586
|
+
const replaced = previous < 0 ? 0 : this.queue[previous].size;
|
|
587
|
+
const budget = LIMITS.queuedBytes + (frame.kind === "response" ? LIMITS.responseFrameBytes : 0);
|
|
588
|
+
ensure(
|
|
589
|
+
this.bytes - replaced + size <= budget && (previous >= 0 || this.queue.length < 256),
|
|
590
|
+
"queue_full",
|
|
591
|
+
"Runner output queue is full"
|
|
592
|
+
);
|
|
593
|
+
const item = { message: JSON.parse(serialized), size, key };
|
|
594
|
+
if (previous >= 0) this.queue.splice(previous, 1);
|
|
595
|
+
this.queue.push(item);
|
|
596
|
+
this.bytes += size - replaced;
|
|
597
|
+
this.flush();
|
|
598
|
+
}
|
|
599
|
+
flush() {
|
|
600
|
+
while (!this.closed && this.queue.length && this.write(this.queue[0].message)) {
|
|
601
|
+
this.bytes -= this.queue.shift().size;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
dispose() {
|
|
605
|
+
this.closed = true;
|
|
606
|
+
this.queue.length = 0;
|
|
607
|
+
this.bytes = 0;
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
function portChannel(port, receive, onError) {
|
|
611
|
+
let credits = 16;
|
|
612
|
+
let closed = false;
|
|
613
|
+
const outbox = new Outbox((message) => {
|
|
614
|
+
if (!credits) return false;
|
|
615
|
+
port.postMessage({ type: "delivery", message });
|
|
616
|
+
credits--;
|
|
617
|
+
return true;
|
|
618
|
+
});
|
|
619
|
+
function listener(envelope) {
|
|
620
|
+
if (closed) return;
|
|
621
|
+
try {
|
|
622
|
+
if (envelope?.type === "credit") {
|
|
623
|
+
ensure(credits < 16, "invalid_payload", "Unexpected port credit");
|
|
624
|
+
credits++;
|
|
625
|
+
outbox.flush();
|
|
626
|
+
} else {
|
|
627
|
+
ensure(envelope?.type === "delivery", "invalid_payload", "Invalid port message");
|
|
628
|
+
jsonBytes(envelope.message, frameLimit(framed(envelope.message), true));
|
|
629
|
+
receive(envelope.message);
|
|
630
|
+
port.postMessage({ type: "credit" });
|
|
631
|
+
}
|
|
632
|
+
} catch (error) {
|
|
633
|
+
onError(error);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
port.on("message", listener);
|
|
637
|
+
return { send: (message) => outbox.push(message), dispose() {
|
|
638
|
+
closed = true;
|
|
639
|
+
outbox.dispose();
|
|
640
|
+
port.off("message", listener);
|
|
641
|
+
} };
|
|
642
|
+
}
|
|
643
|
+
function stdioChannel(input, output, receive, onError) {
|
|
644
|
+
let blocked = false;
|
|
645
|
+
let buffer = Buffer.alloc(0);
|
|
646
|
+
const outbox = new Outbox((frame) => {
|
|
647
|
+
if (blocked) return false;
|
|
648
|
+
blocked = !output.write(`${JSON.stringify(frame)}
|
|
649
|
+
`);
|
|
650
|
+
return true;
|
|
651
|
+
});
|
|
652
|
+
const drain = () => {
|
|
653
|
+
blocked = false;
|
|
654
|
+
outbox.flush();
|
|
655
|
+
};
|
|
656
|
+
const data = (chunk) => {
|
|
657
|
+
try {
|
|
658
|
+
let start = 0;
|
|
659
|
+
for (let index = 0; index < chunk.length; index++) {
|
|
660
|
+
if (chunk[index] !== 10) continue;
|
|
661
|
+
const part = chunk.subarray(start, index);
|
|
662
|
+
ensure(buffer.length + part.length <= LIMITS.responseFrameBytes, "payload_too_large", "Runner input frame is too large");
|
|
663
|
+
const line = buffer.length ? Buffer.concat([buffer, part]) : part;
|
|
664
|
+
buffer = Buffer.alloc(0);
|
|
665
|
+
if (line.length) receive(JSON.parse(line.toString("utf8")));
|
|
666
|
+
start = index + 1;
|
|
667
|
+
}
|
|
668
|
+
const tail = chunk.subarray(start);
|
|
669
|
+
ensure(buffer.length + tail.length <= LIMITS.responseFrameBytes, "payload_too_large", "Runner input frame is too large");
|
|
670
|
+
buffer = Buffer.concat([buffer, tail]);
|
|
671
|
+
} catch (error) {
|
|
672
|
+
onError(error);
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
input.on("data", data);
|
|
676
|
+
output.on("drain", drain);
|
|
677
|
+
output.on("error", onError);
|
|
678
|
+
input.on("error", onError);
|
|
679
|
+
return { send: (frame) => outbox.push(frame), dispose() {
|
|
680
|
+
outbox.dispose();
|
|
681
|
+
input.off("data", data);
|
|
682
|
+
output.off("drain", drain);
|
|
683
|
+
input.off("error", onError);
|
|
684
|
+
output.off("error", onError);
|
|
685
|
+
} };
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// src/node.mjs
|
|
689
|
+
import { execFile } from "node:child_process";
|
|
690
|
+
import { promisify } from "node:util";
|
|
691
|
+
var run = promisify(execFile);
|
|
692
|
+
var NODE_MINIMUM_MAJOR = 20;
|
|
693
|
+
function nodeDiagnostic(executable = process.execPath, version = process.versions.node) {
|
|
694
|
+
const supported = /^\d+\.\d+\.\d+$/.test(version) && Number(version.split(".")[0]) >= NODE_MINIMUM_MAJOR;
|
|
695
|
+
return {
|
|
696
|
+
executable,
|
|
697
|
+
version,
|
|
698
|
+
supported,
|
|
699
|
+
minimumMajor: NODE_MINIMUM_MAJOR,
|
|
700
|
+
protocolVersion: RUNNER_PROTOCOL_VERSION,
|
|
701
|
+
isolation: "worker-thread",
|
|
702
|
+
message: supported ? "Node supports the plugin runner" : "Plugins require Node.js 20 or newer"
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// src/supervisor.mjs
|
|
707
|
+
var bundled = true;
|
|
708
|
+
var sdkUrl = new URL(bundled ? "./sdk.mjs" : "../../plugin-sdk/src/index.mjs", import.meta.url);
|
|
709
|
+
var loaderUrl = new URL("./loader.mjs", import.meta.url);
|
|
710
|
+
var PluginSupervisor = class {
|
|
711
|
+
constructor({ send, env = process.env, onDiagnostic = () => {
|
|
712
|
+
} }) {
|
|
713
|
+
this.send = send;
|
|
714
|
+
this.env = env;
|
|
715
|
+
this.onDiagnostic = onDiagnostic;
|
|
716
|
+
this.slots = /* @__PURE__ */ new Map();
|
|
717
|
+
this.closed = false;
|
|
718
|
+
this.nextWorker = 0;
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* The daemon's input. A frame this supervisor refuses fails by itself: the
|
|
722
|
+
* daemon gets an error answer when the frame was a request, and every other
|
|
723
|
+
* plugin keeps running. Only a broken channel ends the process.
|
|
724
|
+
*/
|
|
725
|
+
accept(frame) {
|
|
726
|
+
try {
|
|
727
|
+
this.receive(frame);
|
|
728
|
+
} catch (error) {
|
|
729
|
+
this.onDiagnostic(error);
|
|
730
|
+
if (frame?.kind !== "request" || typeof frame.id !== "string" || !frame.producer) return;
|
|
731
|
+
try {
|
|
732
|
+
this.send({
|
|
733
|
+
version: RUNNER_PROTOCOL_VERSION,
|
|
734
|
+
producer: frame.producer,
|
|
735
|
+
kind: "response",
|
|
736
|
+
id: frame.id,
|
|
737
|
+
result: { ok: false, error: wireError(error) }
|
|
738
|
+
});
|
|
739
|
+
} catch (failure) {
|
|
740
|
+
this.onDiagnostic(failure);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
receive(frame) {
|
|
745
|
+
validateEnvelope(frame, { inbound: true });
|
|
746
|
+
ensure(!this.closed, "disposed", "Runner is shutting down");
|
|
747
|
+
const key = `${frame.producer.machineId}/${frame.producer.pluginId}`;
|
|
748
|
+
let slot = this.slots.get(key);
|
|
749
|
+
if (slot && !sameProducer(frame.producer, slot.producer) && frame.kind === "request" && frame.operation.op === "runner.start") {
|
|
750
|
+
const retired = slot;
|
|
751
|
+
retired.desired = false;
|
|
752
|
+
clearTimeout(retired.restarting);
|
|
753
|
+
this.slots.delete(key);
|
|
754
|
+
void this.stopWorker(retired).catch(this.onDiagnostic).finally(() => retired.peer.dispose());
|
|
755
|
+
slot = void 0;
|
|
756
|
+
}
|
|
757
|
+
if (!slot) {
|
|
758
|
+
ensure(
|
|
759
|
+
frame.kind === "request" && ["runner.start", "runner.diagnose", "runner.ping"].includes(frame.operation.op),
|
|
760
|
+
"plugin_not_running",
|
|
761
|
+
"Plugin has no runner slot"
|
|
762
|
+
);
|
|
763
|
+
ensure(this.slots.size < 64, "queue_full", "Runner supports at most 64 plugin slots");
|
|
764
|
+
slot = this.createSlot(frame.producer);
|
|
765
|
+
this.slots.set(key, slot);
|
|
766
|
+
}
|
|
767
|
+
ensure(sameProducer(frame.producer, slot.producer), "stale_producer", "Stop the previous slot before replacing its epoch");
|
|
768
|
+
if (frame.kind === "response" || frame.kind === "cancel") {
|
|
769
|
+
if (slot.outgoing.has(frame.id)) {
|
|
770
|
+
const entry = slot.outgoing.get(frame.id);
|
|
771
|
+
if (entry.worker === slot.worker) slot.channel?.send({ type: "frame", frame: { ...frame, id: entry.id } });
|
|
772
|
+
if (frame.kind === "response") slot.outgoing.delete(frame.id);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
slot.peer.receive(frame);
|
|
777
|
+
}
|
|
778
|
+
createSlot(producer) {
|
|
779
|
+
const slot = {
|
|
780
|
+
producer: Object.freeze({ ...producer }),
|
|
781
|
+
source: null,
|
|
782
|
+
worker: null,
|
|
783
|
+
channel: null,
|
|
784
|
+
outgoing: /* @__PURE__ */ new Map(),
|
|
785
|
+
sequence: 0n,
|
|
786
|
+
failures: 0,
|
|
787
|
+
restarting: null,
|
|
788
|
+
health: "stopped",
|
|
789
|
+
busy: false,
|
|
790
|
+
surfaces: /* @__PURE__ */ new Map(),
|
|
791
|
+
reports: /* @__PURE__ */ new Map(),
|
|
792
|
+
subscriptions: /* @__PURE__ */ new Set(),
|
|
793
|
+
desired: false,
|
|
794
|
+
generation: 0,
|
|
795
|
+
bridge: null
|
|
796
|
+
};
|
|
797
|
+
slot.peer = new RpcPeer({
|
|
798
|
+
producer,
|
|
799
|
+
send: this.send,
|
|
800
|
+
onError: this.onDiagnostic,
|
|
801
|
+
onRequest: async (operation, options) => {
|
|
802
|
+
if (operation.op === "runner.ping") return { protocolVersion: RUNNER_PROTOCOL_VERSION, health: slot.health };
|
|
803
|
+
if (operation.op === "runner.diagnose") return { ...nodeDiagnostic(), health: slot.health, pluginId: producer.pluginId };
|
|
804
|
+
if (operation.op.startsWith("runtime.")) {
|
|
805
|
+
ensure(slot.bridge && slot.health === "running", "plugin_not_running", "Plugin worker is unavailable");
|
|
806
|
+
return slot.bridge.request(operation, options);
|
|
807
|
+
}
|
|
808
|
+
ensure(["runner.start", "runner.reload", "runner.stop"].includes(operation.op), "unsupported_operation", "Operation belongs to the daemon");
|
|
809
|
+
ensure(!slot.busy, "operation_in_progress", "Plugin lifecycle operation is in progress");
|
|
810
|
+
slot.busy = true;
|
|
811
|
+
try {
|
|
812
|
+
if (operation.op === "runner.stop") {
|
|
813
|
+
slot.desired = false;
|
|
814
|
+
clearTimeout(slot.restarting);
|
|
815
|
+
await this.stopWorker(slot);
|
|
816
|
+
setImmediate(() => {
|
|
817
|
+
if (!slot.busy && !slot.desired) {
|
|
818
|
+
slot.peer.dispose();
|
|
819
|
+
const key = `${producer.machineId}/${producer.pluginId}`;
|
|
820
|
+
if (this.slots.get(key) === slot) this.slots.delete(key);
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
if (operation.op === "runner.start") ensure(!slot.desired, "plugin_running", "Plugin is running");
|
|
826
|
+
if (operation.op === "runner.reload") ensure(
|
|
827
|
+
operation.args.linked === true || slot.source?.linked === true,
|
|
828
|
+
"invalid_source",
|
|
829
|
+
"Linked reload requires a linked source"
|
|
830
|
+
);
|
|
831
|
+
const prepared = await preparePlugin(operation.args);
|
|
832
|
+
ensure(prepared.manifest.id === producer.pluginId, "identity_mismatch", "Static plugin id differs from its runner slot");
|
|
833
|
+
ensure(!options.signal.aborted, "cancelled", "Plugin start was cancelled");
|
|
834
|
+
slot.desired = false;
|
|
835
|
+
clearTimeout(slot.restarting);
|
|
836
|
+
await this.stopWorker(slot);
|
|
837
|
+
ensure(!options.signal.aborted, "cancelled", "Plugin start was cancelled");
|
|
838
|
+
slot.source = { ...operation.args, root: prepared.root };
|
|
839
|
+
slot.manifest = prepared.manifest;
|
|
840
|
+
slot.failures = 0;
|
|
841
|
+
slot.desired = true;
|
|
842
|
+
await this.startWorker(slot, options.signal);
|
|
843
|
+
return { health: slot.health, manifestDigest: prepared.manifestDigest };
|
|
844
|
+
} finally {
|
|
845
|
+
slot.busy = false;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
return slot;
|
|
850
|
+
}
|
|
851
|
+
async startWorker(slot, signal) {
|
|
852
|
+
ensure(!this.closed && slot.desired, "disposed", "Plugin start is no longer desired");
|
|
853
|
+
const diagnostic = nodeDiagnostic();
|
|
854
|
+
ensure(diagnostic.supported, "node_unsupported", diagnostic.message);
|
|
855
|
+
const generation = ++this.nextWorker;
|
|
856
|
+
const worker = new Worker(new URL("./worker.mjs", import.meta.url), {
|
|
857
|
+
workerData: { version: RUNNER_PROTOCOL_VERSION, source: slot.source, producer: slot.producer, generation },
|
|
858
|
+
env: { ...this.env, STANDARD_PLUGIN_SDK_URL: sdkUrl.href },
|
|
859
|
+
execArgv: ["--experimental-loader", fileURLToPath(loaderUrl)],
|
|
860
|
+
stdout: true,
|
|
861
|
+
stderr: true,
|
|
862
|
+
resourceLimits: { maxOldGenerationSizeMb: 128, maxYoungGenerationSizeMb: 32, stackSizeMb: 4 }
|
|
863
|
+
});
|
|
864
|
+
slot.worker = worker;
|
|
865
|
+
slot.generation = generation;
|
|
866
|
+
slot.health = "starting";
|
|
867
|
+
worker.stdout.resume();
|
|
868
|
+
worker.stderr.resume();
|
|
869
|
+
let settle;
|
|
870
|
+
let settled = false;
|
|
871
|
+
let startupTimer;
|
|
872
|
+
let abort;
|
|
873
|
+
const started = new Promise((resolve2, reject) => {
|
|
874
|
+
settle = (error) => {
|
|
875
|
+
if (settled) return;
|
|
876
|
+
settled = true;
|
|
877
|
+
clearTimeout(startupTimer);
|
|
878
|
+
signal?.removeEventListener("abort", abort);
|
|
879
|
+
error ? reject(error) : resolve2();
|
|
880
|
+
};
|
|
881
|
+
abort = () => {
|
|
882
|
+
slot.desired = false;
|
|
883
|
+
settle(new PluginError("cancelled", "Plugin activation was cancelled"));
|
|
884
|
+
void this.stopWorker(slot).catch(this.onDiagnostic);
|
|
885
|
+
};
|
|
886
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
887
|
+
startupTimer = setTimeout(() => fail(new PluginError("timeout", "Plugin activation exceeded 30 seconds")), 3e4);
|
|
888
|
+
});
|
|
889
|
+
let failed = false;
|
|
890
|
+
const fail = (error) => {
|
|
891
|
+
if (failed || slot.worker !== worker || slot.stopping) return;
|
|
892
|
+
failed = true;
|
|
893
|
+
settle(error);
|
|
894
|
+
this.onDiagnostic(error);
|
|
895
|
+
void this.stopWorker(slot).then(() => {
|
|
896
|
+
if (slot.desired && !this.closed) {
|
|
897
|
+
slot.health = "backoff";
|
|
898
|
+
const delay = Math.min(6e4, 250 * 2 ** Math.min(slot.failures++, 8));
|
|
899
|
+
slot.restarting = setTimeout(() => {
|
|
900
|
+
slot.restarting = null;
|
|
901
|
+
if (slot.desired && !this.closed) void this.startWorker(slot).catch(this.onDiagnostic);
|
|
902
|
+
}, delay);
|
|
903
|
+
}
|
|
904
|
+
}).catch(this.onDiagnostic);
|
|
905
|
+
};
|
|
906
|
+
slot.channel = portChannel(worker, (message) => {
|
|
907
|
+
if (slot.worker !== worker) return;
|
|
908
|
+
switch (message.type) {
|
|
909
|
+
case "ready":
|
|
910
|
+
slot.health = "running";
|
|
911
|
+
settle();
|
|
912
|
+
return;
|
|
913
|
+
case "stopped":
|
|
914
|
+
slot.stopped?.();
|
|
915
|
+
return;
|
|
916
|
+
case "failure":
|
|
917
|
+
fail(new PluginError(message.error.code, message.error.message));
|
|
918
|
+
return;
|
|
919
|
+
case "diagnostic":
|
|
920
|
+
this.onDiagnostic(new PluginError(message.error.code, message.error.message));
|
|
921
|
+
return;
|
|
922
|
+
case "frame":
|
|
923
|
+
this.fromWorker(slot, worker, message.frame);
|
|
924
|
+
return;
|
|
925
|
+
default:
|
|
926
|
+
fail(new PluginError("invalid_payload", "Invalid worker message"));
|
|
927
|
+
}
|
|
928
|
+
}, fail);
|
|
929
|
+
slot.bridge = new RpcPeer({
|
|
930
|
+
producer: slot.producer,
|
|
931
|
+
send: (frame) => slot.channel.send({ type: "frame", frame }),
|
|
932
|
+
onError: fail
|
|
933
|
+
});
|
|
934
|
+
worker.on("error", fail);
|
|
935
|
+
worker.on("exit", (code) => fail(new PluginError("worker_exit", `Plugin worker exited with code ${code}`)));
|
|
936
|
+
if (signal?.aborted) abort();
|
|
937
|
+
return started;
|
|
938
|
+
}
|
|
939
|
+
fromWorker(slot, worker, frame) {
|
|
940
|
+
validateEnvelope(frame);
|
|
941
|
+
ensure(sameProducer(frame.producer, slot.producer), "stale_producer", "Worker changed its producer identity");
|
|
942
|
+
if (frame.kind === "response") {
|
|
943
|
+
slot.bridge.receive(frame);
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
if (frame.kind === "cancel") {
|
|
947
|
+
const id = `worker-${slot.generation}-${frame.id}`;
|
|
948
|
+
if (slot.outgoing.delete(id)) this.send({ ...frame, id });
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
if (frame.kind === "surface") {
|
|
952
|
+
ensure(
|
|
953
|
+
slot.manifest.capabilities.includes("surfaces") && slot.manifest.contributions.some((item) => item.id === frame.key.contributionId && item.anchor === frame.key.anchor),
|
|
954
|
+
"capability_denied",
|
|
955
|
+
"Worker published an undeclared surface"
|
|
956
|
+
);
|
|
957
|
+
const key = JSON.stringify(frame.key);
|
|
958
|
+
ensure(frame.content === null || slot.surfaces.has(key) || slot.surfaces.size < LIMITS.contributions, "queue_full", "Too many live surfaces");
|
|
959
|
+
if (frame.content === null) slot.surfaces.delete(key);
|
|
960
|
+
else slot.surfaces.set(key, frame.key);
|
|
961
|
+
this.send({ ...frame, sequence: String(++slot.sequence) });
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
if (frame.kind === "report") {
|
|
965
|
+
ensure(slot.manifest.capabilities.includes("surfaces"), "capability_denied", "Worker cannot publish reports");
|
|
966
|
+
const key = JSON.stringify([frame.name, frame.entity]);
|
|
967
|
+
ensure(
|
|
968
|
+
frame.value === null || slot.reports.has(key) || slot.reports.size < LIMITS.contributions,
|
|
969
|
+
"queue_full",
|
|
970
|
+
"Too many live reports"
|
|
971
|
+
);
|
|
972
|
+
if (frame.value === null) slot.reports.delete(key);
|
|
973
|
+
else slot.reports.set(key, { name: frame.name, entity: frame.entity });
|
|
974
|
+
this.send({ ...frame, sequence: String(++slot.sequence) });
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
try {
|
|
978
|
+
authorize(frame.operation, slot.manifest.capabilities);
|
|
979
|
+
if (frame.operation.op === "subscription.add") {
|
|
980
|
+
ensure(
|
|
981
|
+
slot.subscriptions.has(frame.operation.args.id) || slot.subscriptions.size < LIMITS.subscriptions,
|
|
982
|
+
"queue_full",
|
|
983
|
+
"Worker has too many subscriptions"
|
|
984
|
+
);
|
|
985
|
+
slot.subscriptions.add(frame.operation.args.id);
|
|
986
|
+
}
|
|
987
|
+
if (frame.operation.op === "subscription.remove") slot.subscriptions.delete(frame.operation.args.id);
|
|
988
|
+
ensure(slot.outgoing.size < LIMITS.pendingRequests, "queue_full", "Worker has too many pending requests");
|
|
989
|
+
const id = `worker-${slot.generation}-${frame.id}`;
|
|
990
|
+
ensure(!slot.outgoing.has(id), "duplicate_request", "Worker request id is in flight");
|
|
991
|
+
slot.outgoing.set(id, { worker, id: frame.id });
|
|
992
|
+
try {
|
|
993
|
+
this.send({ ...frame, id });
|
|
994
|
+
} catch (error) {
|
|
995
|
+
slot.outgoing.delete(id);
|
|
996
|
+
throw error;
|
|
997
|
+
}
|
|
998
|
+
} catch (error) {
|
|
999
|
+
slot.channel.send({ type: "frame", frame: {
|
|
1000
|
+
version: RUNNER_PROTOCOL_VERSION,
|
|
1001
|
+
producer: slot.producer,
|
|
1002
|
+
kind: "response",
|
|
1003
|
+
id: frame.id,
|
|
1004
|
+
result: { ok: false, error: wireError(error) }
|
|
1005
|
+
} });
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
async stopWorker(slot) {
|
|
1009
|
+
if (slot.stopping) return slot.stopping;
|
|
1010
|
+
const worker = slot.worker;
|
|
1011
|
+
if (!worker) return;
|
|
1012
|
+
slot.stopping = (async () => {
|
|
1013
|
+
let timer;
|
|
1014
|
+
await new Promise((resolve2) => {
|
|
1015
|
+
slot.stopped = resolve2;
|
|
1016
|
+
timer = setTimeout(resolve2, 1e3);
|
|
1017
|
+
try {
|
|
1018
|
+
slot.channel.send({ type: "stop" });
|
|
1019
|
+
} catch {
|
|
1020
|
+
resolve2();
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
clearTimeout(timer);
|
|
1024
|
+
slot.stopped = null;
|
|
1025
|
+
slot.worker = null;
|
|
1026
|
+
slot.bridge?.dispose();
|
|
1027
|
+
slot.bridge = null;
|
|
1028
|
+
slot.channel?.dispose();
|
|
1029
|
+
slot.channel = null;
|
|
1030
|
+
await worker.terminate();
|
|
1031
|
+
for (const id of slot.outgoing.keys()) {
|
|
1032
|
+
try {
|
|
1033
|
+
this.send({ version: RUNNER_PROTOCOL_VERSION, producer: slot.producer, kind: "cancel", id });
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
this.onDiagnostic(error);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
slot.outgoing.clear();
|
|
1039
|
+
await Promise.allSettled([...slot.subscriptions].map((id) => slot.peer.request(
|
|
1040
|
+
{ op: "subscription.remove", args: { id } },
|
|
1041
|
+
{ timeoutMs: 1e3 }
|
|
1042
|
+
)));
|
|
1043
|
+
slot.subscriptions.clear();
|
|
1044
|
+
for (const key of slot.surfaces.values()) {
|
|
1045
|
+
try {
|
|
1046
|
+
this.send({
|
|
1047
|
+
version: RUNNER_PROTOCOL_VERSION,
|
|
1048
|
+
producer: slot.producer,
|
|
1049
|
+
kind: "surface",
|
|
1050
|
+
key,
|
|
1051
|
+
sequence: String(++slot.sequence),
|
|
1052
|
+
content: null
|
|
1053
|
+
});
|
|
1054
|
+
} catch (error) {
|
|
1055
|
+
this.onDiagnostic(error);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
slot.surfaces.clear();
|
|
1059
|
+
for (const report of slot.reports.values()) {
|
|
1060
|
+
try {
|
|
1061
|
+
this.send({
|
|
1062
|
+
version: RUNNER_PROTOCOL_VERSION,
|
|
1063
|
+
producer: slot.producer,
|
|
1064
|
+
kind: "report",
|
|
1065
|
+
...report,
|
|
1066
|
+
sequence: String(++slot.sequence),
|
|
1067
|
+
schemaVersion: 1,
|
|
1068
|
+
value: null
|
|
1069
|
+
});
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
this.onDiagnostic(error);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
slot.reports.clear();
|
|
1075
|
+
slot.health = "stopped";
|
|
1076
|
+
})().finally(() => {
|
|
1077
|
+
slot.stopping = null;
|
|
1078
|
+
});
|
|
1079
|
+
return slot.stopping;
|
|
1080
|
+
}
|
|
1081
|
+
async dispose() {
|
|
1082
|
+
this.closed = true;
|
|
1083
|
+
await Promise.all([...this.slots.values()].map(async (slot) => {
|
|
1084
|
+
slot.desired = false;
|
|
1085
|
+
clearTimeout(slot.restarting);
|
|
1086
|
+
await this.stopWorker(slot);
|
|
1087
|
+
slot.peer.dispose();
|
|
1088
|
+
}));
|
|
1089
|
+
this.slots.clear();
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1092
|
+
|
|
1093
|
+
// src/index.mjs
|
|
1094
|
+
if (process.argv.includes("--doctor")) {
|
|
1095
|
+
const diagnostic = nodeDiagnostic();
|
|
1096
|
+
process.stdout.write(`${JSON.stringify(diagnostic)}
|
|
1097
|
+
`);
|
|
1098
|
+
if (!diagnostic.supported) process.exitCode = 1;
|
|
1099
|
+
} else {
|
|
1100
|
+
let stopping = false;
|
|
1101
|
+
let supervisor;
|
|
1102
|
+
const diagnostic = (error) => process.stderr.write(`${JSON.stringify(wireError(error))}
|
|
1103
|
+
`);
|
|
1104
|
+
const fail = (error) => {
|
|
1105
|
+
diagnostic(error);
|
|
1106
|
+
process.exitCode = 1;
|
|
1107
|
+
void shutdown();
|
|
1108
|
+
};
|
|
1109
|
+
const channel = stdioChannel(process.stdin, process.stdout, (frame) => supervisor.accept(frame), fail);
|
|
1110
|
+
supervisor = new PluginSupervisor({ send: channel.send, onDiagnostic: diagnostic });
|
|
1111
|
+
async function shutdown() {
|
|
1112
|
+
if (stopping) return;
|
|
1113
|
+
stopping = true;
|
|
1114
|
+
process.stdin.pause();
|
|
1115
|
+
try {
|
|
1116
|
+
await supervisor.dispose();
|
|
1117
|
+
} finally {
|
|
1118
|
+
channel.dispose();
|
|
1119
|
+
process.exit(process.exitCode ?? 0);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
process.stdin.on("end", () => void shutdown());
|
|
1123
|
+
process.on("SIGTERM", () => void shutdown());
|
|
1124
|
+
process.on("SIGINT", () => void shutdown());
|
|
1125
|
+
}
|