@frockbot/frock-compose 0.0.0 → 0.7.292
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 +22 -2
- package/index.ts +18 -0
- package/package.json +33 -5
- package/plugin-worker-host.ts +2152 -0
- package/plugin-worker-wrapper.ts +1545 -0
|
@@ -0,0 +1,1545 @@
|
|
|
1
|
+
// The kernel-generated index module (`index.js`) for a User's Plugin worker.
|
|
2
|
+
//
|
|
3
|
+
// This is composition, not contract: the kernel *generates* this text and
|
|
4
|
+
// content-addresses it with every Plugin artifact it imports, so changing a
|
|
5
|
+
// byte of it is a new module set and therefore a new loader identity. Plugin
|
|
6
|
+
// code never implements the wrapper; each Plugin exports `tools` and
|
|
7
|
+
// `execute`, optionally `hooks`, `services` and `triggers`, and the index
|
|
8
|
+
// adapts: it decodes each invocation, enforces the deadline, fans a hook out
|
|
9
|
+
// to the enabled Plugins in mount order, and hands each Plugin a narrow `ctx`
|
|
10
|
+
// that names only what that Plugin may do.
|
|
11
|
+
//
|
|
12
|
+
// The wrapper is emitted as plain JavaScript because it is a module in the
|
|
13
|
+
// loaded Worker's module map, not a source file this repository compiles.
|
|
14
|
+
import {
|
|
15
|
+
BOT_ISOLATE_HOOK_EVENTS_V1,
|
|
16
|
+
BOT_ISOLATE_SERVING_CONTEXT_KEYS_V1,
|
|
17
|
+
ISOLATE_CONTRACT_VERSION,
|
|
18
|
+
MAX_FAILURE_REASON_V1,
|
|
19
|
+
PLUGIN_CARD_ACTION_NAME_PATTERN_V1,
|
|
20
|
+
type BotPackageContextV1,
|
|
21
|
+
} from "@frockbot/core/contracts";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The deadline guard, shared verbatim between the generated wrapper and the
|
|
25
|
+
* Bun test that proves it. Kept as source text so the tested function and the
|
|
26
|
+
* shipped function cannot drift.
|
|
27
|
+
*/
|
|
28
|
+
export const BOT_ISOLATE_DEADLINE_SOURCE = `function withIsolateDeadline(work, deadlineMs) {
|
|
29
|
+
if (!Number.isSafeInteger(deadlineMs) || deadlineMs <= 0 || deadlineMs > 60000) {
|
|
30
|
+
return Promise.reject(new Error("isolate invocation deadline is out of range"));
|
|
31
|
+
}
|
|
32
|
+
let timer;
|
|
33
|
+
const expiry = new Promise(function (_resolve, reject) {
|
|
34
|
+
timer = setTimeout(function () {
|
|
35
|
+
reject(new Error("isolate invocation exceeded its deadline of " + deadlineMs + "ms"));
|
|
36
|
+
}, deadlineMs);
|
|
37
|
+
});
|
|
38
|
+
return Promise.race([Promise.resolve().then(work), expiry]).finally(function () {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
});
|
|
41
|
+
}`;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The invocation guards. The worker re-decodes what the Durable Object sent:
|
|
45
|
+
* the boundary is crossed in both directions and both sides decode.
|
|
46
|
+
*/
|
|
47
|
+
export const BOT_ISOLATE_INVOCATION_SOURCE = `var TOOL_NAME = /^[a-z][a-z0-9_]{0,63}$/;
|
|
48
|
+
var PLUGIN_ID = /^[a-z][a-z0-9-]{0,63}$/;
|
|
49
|
+
var PROVIDER_ID = /^[a-z][a-z0-9-]{0,63}$/;
|
|
50
|
+
var TRIGGER_NAME = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
51
|
+
var SURFACE_ID = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
|
|
52
|
+
var CARD_ID = /^[a-z][a-z0-9_]{0,31}$/;
|
|
53
|
+
var CARD_ACTION_NAME = new RegExp(${JSON.stringify(PLUGIN_CARD_ACTION_NAME_PATTERN_V1)});
|
|
54
|
+
var HOOK_EVENTS = ${JSON.stringify(BOT_ISOLATE_HOOK_EVENTS_V1)};
|
|
55
|
+
var IDENTITY_KEYS = ["botId", "sessionId", "runId", "turnId", "generationId"];
|
|
56
|
+
function isRecord(value) {
|
|
57
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
58
|
+
}
|
|
59
|
+
function exactKeys(value, keys, label) {
|
|
60
|
+
if (!isRecord(value)) {
|
|
61
|
+
throw new Error(label + " must be an object");
|
|
62
|
+
}
|
|
63
|
+
if (
|
|
64
|
+
Object.keys(value).length !== keys.length ||
|
|
65
|
+
!keys.every(function (key) {
|
|
66
|
+
return Object.hasOwn(value, key);
|
|
67
|
+
})
|
|
68
|
+
) {
|
|
69
|
+
throw new Error(label + " has invalid fields");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function identityFields(value, label) {
|
|
73
|
+
for (const key of IDENTITY_KEYS) {
|
|
74
|
+
if (typeof value[key] !== "string" || value[key].length === 0) {
|
|
75
|
+
throw new Error(label + " " + key + " is invalid");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
var INVOCATION_KEYS = [
|
|
80
|
+
"schemaVersion",
|
|
81
|
+
"pluginId",
|
|
82
|
+
"tool",
|
|
83
|
+
"input",
|
|
84
|
+
"botId",
|
|
85
|
+
"sessionId",
|
|
86
|
+
"runId",
|
|
87
|
+
"turnId",
|
|
88
|
+
"generationId",
|
|
89
|
+
"deadlineMs",
|
|
90
|
+
];
|
|
91
|
+
function decodeInvocation(value) {
|
|
92
|
+
// A tool call inside a Turn names the effect it runs as; one outside any
|
|
93
|
+
// Turn names none.
|
|
94
|
+
const withEffect = isRecord(value) && Object.hasOwn(value, "effectId");
|
|
95
|
+
exactKeys(
|
|
96
|
+
value,
|
|
97
|
+
withEffect ? INVOCATION_KEYS.concat(["effectId"]) : INVOCATION_KEYS,
|
|
98
|
+
"plugin worker tool invocation",
|
|
99
|
+
);
|
|
100
|
+
if (withEffect && (typeof value.effectId !== "string" || value.effectId.length === 0)) {
|
|
101
|
+
throw new Error("plugin worker tool invocation effectId is invalid");
|
|
102
|
+
}
|
|
103
|
+
if (value.schemaVersion !== 1) {
|
|
104
|
+
throw new Error("plugin worker tool invocation schemaVersion is unsupported");
|
|
105
|
+
}
|
|
106
|
+
if (typeof value.pluginId !== "string" || !PLUGIN_ID.test(value.pluginId)) {
|
|
107
|
+
throw new Error("plugin worker tool invocation pluginId is invalid");
|
|
108
|
+
}
|
|
109
|
+
if (typeof value.tool !== "string" || !TOOL_NAME.test(value.tool)) {
|
|
110
|
+
throw new Error("plugin worker tool invocation tool is invalid");
|
|
111
|
+
}
|
|
112
|
+
identityFields(value, "plugin worker tool invocation");
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
var HOOK_INVOCATION_KEYS = [
|
|
116
|
+
"schemaVersion",
|
|
117
|
+
"event",
|
|
118
|
+
"payload",
|
|
119
|
+
"botId",
|
|
120
|
+
"sessionId",
|
|
121
|
+
"runId",
|
|
122
|
+
"turnId",
|
|
123
|
+
"generationId",
|
|
124
|
+
"deadlineMs",
|
|
125
|
+
"enabled",
|
|
126
|
+
];
|
|
127
|
+
function decodeHookInvocation(value) {
|
|
128
|
+
exactKeys(value, HOOK_INVOCATION_KEYS, "plugin worker hook invocation");
|
|
129
|
+
if (value.schemaVersion !== 1 || !HOOK_EVENTS.includes(value.event)) {
|
|
130
|
+
throw new Error("plugin worker hook invocation is unsupported");
|
|
131
|
+
}
|
|
132
|
+
if (!isRecord(value.payload)) {
|
|
133
|
+
throw new Error("plugin worker hook invocation payload is invalid");
|
|
134
|
+
}
|
|
135
|
+
if (
|
|
136
|
+
!Array.isArray(value.enabled) ||
|
|
137
|
+
!value.enabled.every(function (id) {
|
|
138
|
+
return typeof id === "string" && PLUGIN_ID.test(id);
|
|
139
|
+
})
|
|
140
|
+
) {
|
|
141
|
+
throw new Error("plugin worker hook invocation enabled is invalid");
|
|
142
|
+
}
|
|
143
|
+
identityFields(value, "plugin worker hook invocation");
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
var MODEL_INVOCATION_KEYS = [
|
|
147
|
+
"schemaVersion",
|
|
148
|
+
"pluginId",
|
|
149
|
+
"provider",
|
|
150
|
+
"protocolVersion",
|
|
151
|
+
"request",
|
|
152
|
+
"transportId",
|
|
153
|
+
"botId",
|
|
154
|
+
"sessionId",
|
|
155
|
+
"runId",
|
|
156
|
+
"turnId",
|
|
157
|
+
"generationId",
|
|
158
|
+
"deadlineMs",
|
|
159
|
+
"firstEventDeadlineMs",
|
|
160
|
+
];
|
|
161
|
+
function decodeModelInvocation(value) {
|
|
162
|
+
exactKeys(value, MODEL_INVOCATION_KEYS, "plugin model invocation");
|
|
163
|
+
if (value.schemaVersion !== 1) {
|
|
164
|
+
throw new Error("plugin model invocation schemaVersion is unsupported");
|
|
165
|
+
}
|
|
166
|
+
if (typeof value.pluginId !== "string" || !PLUGIN_ID.test(value.pluginId)) {
|
|
167
|
+
throw new Error("plugin model invocation pluginId is invalid");
|
|
168
|
+
}
|
|
169
|
+
if (typeof value.provider !== "string" || !PROVIDER_ID.test(value.provider)) {
|
|
170
|
+
throw new Error("plugin model invocation provider is invalid");
|
|
171
|
+
}
|
|
172
|
+
if (!isRecord(value.request) || value.request.provider !== value.provider) {
|
|
173
|
+
throw new Error("plugin model invocation request is invalid");
|
|
174
|
+
}
|
|
175
|
+
if (value.request.modelBinding !== undefined) {
|
|
176
|
+
throw new Error("plugin model invocation request must not carry a Connection binding");
|
|
177
|
+
}
|
|
178
|
+
if (typeof value.transportId !== "string" || value.transportId.length === 0) {
|
|
179
|
+
throw new Error("plugin model invocation transportId is invalid");
|
|
180
|
+
}
|
|
181
|
+
identityFields(value, "plugin model invocation");
|
|
182
|
+
if (
|
|
183
|
+
!Number.isSafeInteger(value.deadlineMs) ||
|
|
184
|
+
value.deadlineMs <= 0 ||
|
|
185
|
+
value.deadlineMs > 60000
|
|
186
|
+
) {
|
|
187
|
+
throw new Error("plugin model invocation deadlineMs is out of range");
|
|
188
|
+
}
|
|
189
|
+
if (
|
|
190
|
+
!Number.isSafeInteger(value.firstEventDeadlineMs) ||
|
|
191
|
+
value.firstEventDeadlineMs < value.deadlineMs ||
|
|
192
|
+
value.firstEventDeadlineMs > 120000
|
|
193
|
+
) {
|
|
194
|
+
throw new Error("plugin model invocation firstEventDeadlineMs is out of range");
|
|
195
|
+
}
|
|
196
|
+
return value;
|
|
197
|
+
}
|
|
198
|
+
var TRIGGER_INVOCATION_KEYS = [
|
|
199
|
+
"schemaVersion",
|
|
200
|
+
"pluginId",
|
|
201
|
+
"trigger",
|
|
202
|
+
"headers",
|
|
203
|
+
"body",
|
|
204
|
+
"botId",
|
|
205
|
+
"sessionId",
|
|
206
|
+
"runId",
|
|
207
|
+
"turnId",
|
|
208
|
+
"generationId",
|
|
209
|
+
"routineId",
|
|
210
|
+
"deadlineMs",
|
|
211
|
+
];
|
|
212
|
+
var VIEW_INVOCATION_KEYS = [
|
|
213
|
+
"schemaVersion",
|
|
214
|
+
"pluginId",
|
|
215
|
+
"surfaceId",
|
|
216
|
+
"botId",
|
|
217
|
+
"sessionId",
|
|
218
|
+
"runId",
|
|
219
|
+
"turnId",
|
|
220
|
+
"generationId",
|
|
221
|
+
"deadlineMs",
|
|
222
|
+
];
|
|
223
|
+
function decodeViewInvocation(value) {
|
|
224
|
+
exactKeys(value, VIEW_INVOCATION_KEYS, "plugin worker view invocation");
|
|
225
|
+
if (value.schemaVersion !== 1) {
|
|
226
|
+
throw new Error("plugin worker view invocation schemaVersion is unsupported");
|
|
227
|
+
}
|
|
228
|
+
if (typeof value.pluginId !== "string" || !PLUGIN_ID.test(value.pluginId)) {
|
|
229
|
+
throw new Error("plugin worker view invocation pluginId is invalid");
|
|
230
|
+
}
|
|
231
|
+
if (typeof value.surfaceId !== "string" || !SURFACE_ID.test(value.surfaceId)) {
|
|
232
|
+
throw new Error("plugin worker view invocation surfaceId is invalid");
|
|
233
|
+
}
|
|
234
|
+
identityFields(value, "plugin worker view invocation");
|
|
235
|
+
return value;
|
|
236
|
+
}
|
|
237
|
+
var CARD_ACTION_INVOCATION_KEYS = [
|
|
238
|
+
"schemaVersion",
|
|
239
|
+
"pluginId",
|
|
240
|
+
"cardId",
|
|
241
|
+
"surfaceId",
|
|
242
|
+
"action",
|
|
243
|
+
"botId",
|
|
244
|
+
"sessionId",
|
|
245
|
+
"runId",
|
|
246
|
+
"turnId",
|
|
247
|
+
"generationId",
|
|
248
|
+
"deadlineMs",
|
|
249
|
+
];
|
|
250
|
+
function decodeCardActionInvocation(value) {
|
|
251
|
+
if (!isRecord(value)) {
|
|
252
|
+
throw new Error("plugin worker card action invocation must be an object");
|
|
253
|
+
}
|
|
254
|
+
for (const key of Object.keys(value)) {
|
|
255
|
+
if (!CARD_ACTION_INVOCATION_KEYS.includes(key) && key !== "context" && key !== "dataModel" && key !== "record") {
|
|
256
|
+
throw new Error("plugin worker card action invocation has invalid fields");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
for (const key of CARD_ACTION_INVOCATION_KEYS) {
|
|
260
|
+
if (!Object.hasOwn(value, key)) {
|
|
261
|
+
throw new Error("plugin worker card action invocation has invalid fields");
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (value.schemaVersion !== 1) {
|
|
265
|
+
throw new Error("plugin worker card action invocation schemaVersion is unsupported");
|
|
266
|
+
}
|
|
267
|
+
if (typeof value.pluginId !== "string" || !PLUGIN_ID.test(value.pluginId)) {
|
|
268
|
+
throw new Error("plugin worker card action invocation pluginId is invalid");
|
|
269
|
+
}
|
|
270
|
+
if (typeof value.cardId !== "string" || !CARD_ID.test(value.cardId)) {
|
|
271
|
+
throw new Error("plugin worker card action invocation cardId is invalid");
|
|
272
|
+
}
|
|
273
|
+
if (typeof value.surfaceId !== "string" || !SURFACE_ID.test(value.surfaceId)) {
|
|
274
|
+
throw new Error("plugin worker card action invocation surfaceId is invalid");
|
|
275
|
+
}
|
|
276
|
+
if (typeof value.action !== "string" || !CARD_ACTION_NAME.test(value.action)) {
|
|
277
|
+
throw new Error("plugin worker card action invocation action is invalid");
|
|
278
|
+
}
|
|
279
|
+
if (value.context !== undefined && !isRecord(value.context)) {
|
|
280
|
+
throw new Error("plugin worker card action invocation context is invalid");
|
|
281
|
+
}
|
|
282
|
+
if (value.dataModel !== undefined && !isRecord(value.dataModel)) {
|
|
283
|
+
throw new Error("plugin worker card action invocation dataModel is invalid");
|
|
284
|
+
}
|
|
285
|
+
if (value.record !== undefined && !isRecord(value.record)) {
|
|
286
|
+
throw new Error("plugin worker card action invocation record is invalid");
|
|
287
|
+
}
|
|
288
|
+
identityFields(value, "plugin worker card action invocation");
|
|
289
|
+
return value;
|
|
290
|
+
}
|
|
291
|
+
var RENDER_CARD_INVOCATION_KEYS = [
|
|
292
|
+
"schemaVersion",
|
|
293
|
+
"pluginId",
|
|
294
|
+
"cardId",
|
|
295
|
+
"surfaceId",
|
|
296
|
+
"data",
|
|
297
|
+
"botId",
|
|
298
|
+
"sessionId",
|
|
299
|
+
"runId",
|
|
300
|
+
"turnId",
|
|
301
|
+
"generationId",
|
|
302
|
+
"deadlineMs",
|
|
303
|
+
];
|
|
304
|
+
function decodeRenderCardInvocation(value) {
|
|
305
|
+
exactKeys(value, RENDER_CARD_INVOCATION_KEYS, "plugin worker render card invocation");
|
|
306
|
+
if (value.schemaVersion !== 1) {
|
|
307
|
+
throw new Error("plugin worker render card invocation schemaVersion is unsupported");
|
|
308
|
+
}
|
|
309
|
+
if (typeof value.pluginId !== "string" || !PLUGIN_ID.test(value.pluginId)) {
|
|
310
|
+
throw new Error("plugin worker render card invocation pluginId is invalid");
|
|
311
|
+
}
|
|
312
|
+
if (typeof value.cardId !== "string" || !CARD_ID.test(value.cardId)) {
|
|
313
|
+
throw new Error("plugin worker render card invocation cardId is invalid");
|
|
314
|
+
}
|
|
315
|
+
if (typeof value.surfaceId !== "string" || !SURFACE_ID.test(value.surfaceId)) {
|
|
316
|
+
throw new Error("plugin worker render card invocation surfaceId is invalid");
|
|
317
|
+
}
|
|
318
|
+
if (!isRecord(value.data)) {
|
|
319
|
+
throw new Error("plugin worker render card invocation data is invalid");
|
|
320
|
+
}
|
|
321
|
+
identityFields(value, "plugin worker render card invocation");
|
|
322
|
+
return value;
|
|
323
|
+
}
|
|
324
|
+
var REVISE_CARD_INVOCATION_KEYS = [
|
|
325
|
+
"schemaVersion",
|
|
326
|
+
"pluginId",
|
|
327
|
+
"cardId",
|
|
328
|
+
"surfaceId",
|
|
329
|
+
"dataModel",
|
|
330
|
+
"record",
|
|
331
|
+
"botId",
|
|
332
|
+
"sessionId",
|
|
333
|
+
"runId",
|
|
334
|
+
"turnId",
|
|
335
|
+
"generationId",
|
|
336
|
+
"deadlineMs",
|
|
337
|
+
];
|
|
338
|
+
function decodeReviseCardInvocation(value) {
|
|
339
|
+
exactKeys(value, REVISE_CARD_INVOCATION_KEYS, "plugin worker revise card invocation");
|
|
340
|
+
if (value.schemaVersion !== 1) {
|
|
341
|
+
throw new Error("plugin worker revise card invocation schemaVersion is unsupported");
|
|
342
|
+
}
|
|
343
|
+
if (typeof value.pluginId !== "string" || !PLUGIN_ID.test(value.pluginId)) {
|
|
344
|
+
throw new Error("plugin worker revise card invocation pluginId is invalid");
|
|
345
|
+
}
|
|
346
|
+
if (typeof value.cardId !== "string" || !CARD_ID.test(value.cardId)) {
|
|
347
|
+
throw new Error("plugin worker revise card invocation cardId is invalid");
|
|
348
|
+
}
|
|
349
|
+
if (typeof value.surfaceId !== "string" || !SURFACE_ID.test(value.surfaceId)) {
|
|
350
|
+
throw new Error("plugin worker revise card invocation surfaceId is invalid");
|
|
351
|
+
}
|
|
352
|
+
if (!isRecord(value.dataModel) || !isRecord(value.record)) {
|
|
353
|
+
throw new Error("plugin worker revise card invocation data model is invalid");
|
|
354
|
+
}
|
|
355
|
+
identityFields(value, "plugin worker revise card invocation");
|
|
356
|
+
return value;
|
|
357
|
+
}
|
|
358
|
+
function decodeTriggerInvocation(value) {
|
|
359
|
+
exactKeys(
|
|
360
|
+
value,
|
|
361
|
+
isRecord(value) && Object.hasOwn(value, "source")
|
|
362
|
+
? TRIGGER_INVOCATION_KEYS.concat("source")
|
|
363
|
+
: TRIGGER_INVOCATION_KEYS,
|
|
364
|
+
"plugin worker trigger invocation",
|
|
365
|
+
);
|
|
366
|
+
if (
|
|
367
|
+
Object.hasOwn(value, "source") &&
|
|
368
|
+
(!isRecord(value.source) ||
|
|
369
|
+
value.source.kind !== "device-module" ||
|
|
370
|
+
typeof value.source.moduleId !== "string" ||
|
|
371
|
+
typeof value.source.machineId !== "string" ||
|
|
372
|
+
typeof value.source.key !== "string")
|
|
373
|
+
) {
|
|
374
|
+
throw new Error("plugin worker trigger invocation source is invalid");
|
|
375
|
+
}
|
|
376
|
+
if (value.schemaVersion !== 1) {
|
|
377
|
+
throw new Error("plugin worker trigger invocation schemaVersion is unsupported");
|
|
378
|
+
}
|
|
379
|
+
if (typeof value.pluginId !== "string" || !PLUGIN_ID.test(value.pluginId)) {
|
|
380
|
+
throw new Error("plugin worker trigger invocation pluginId is invalid");
|
|
381
|
+
}
|
|
382
|
+
if (typeof value.trigger !== "string" || !TRIGGER_NAME.test(value.trigger)) {
|
|
383
|
+
throw new Error("plugin worker trigger invocation trigger is invalid");
|
|
384
|
+
}
|
|
385
|
+
if (!isRecord(value.headers) || typeof value.body !== "string") {
|
|
386
|
+
throw new Error("plugin worker trigger invocation event is invalid");
|
|
387
|
+
}
|
|
388
|
+
identityFields(value, "plugin worker trigger invocation");
|
|
389
|
+
if (typeof value.routineId !== "string" || value.routineId.length === 0) {
|
|
390
|
+
throw new Error("plugin worker trigger invocation routineId is invalid");
|
|
391
|
+
}
|
|
392
|
+
return value;
|
|
393
|
+
}`;
|
|
394
|
+
|
|
395
|
+
/** Decodes one NDJSON line of the `invokeModel` byte stream inside the isolate. */
|
|
396
|
+
export const BOT_ISOLATE_MODEL_SOURCE = `async function* modelEvents(stream) {
|
|
397
|
+
const reader = stream.getReader();
|
|
398
|
+
const decoder = new TextDecoder();
|
|
399
|
+
let buffer = "";
|
|
400
|
+
try {
|
|
401
|
+
for (;;) {
|
|
402
|
+
const chunk = await reader.read();
|
|
403
|
+
if (chunk.done) break;
|
|
404
|
+
buffer += decoder.decode(chunk.value, { stream: true });
|
|
405
|
+
let newline = buffer.indexOf("\\n");
|
|
406
|
+
while (newline >= 0) {
|
|
407
|
+
const line = buffer.slice(0, newline).trim();
|
|
408
|
+
buffer = buffer.slice(newline + 1);
|
|
409
|
+
if (line.length > 0) yield JSON.parse(line);
|
|
410
|
+
newline = buffer.indexOf("\\n");
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const tail = buffer.trim();
|
|
414
|
+
if (tail.length > 0) yield JSON.parse(tail);
|
|
415
|
+
} finally {
|
|
416
|
+
reader.releaseLock();
|
|
417
|
+
}
|
|
418
|
+
}`;
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* The `ctx` members that are always present: the identity of the call, the
|
|
422
|
+
* three context keys, and the services the Plugin consumes. Nothing here is
|
|
423
|
+
* authority.
|
|
424
|
+
*/
|
|
425
|
+
const BOT_ISOLATE_CONTEXT_PROPERTY_SOURCE_V1 = {
|
|
426
|
+
tool: "invocation.tool",
|
|
427
|
+
event: "invocation.event",
|
|
428
|
+
user: "{ userId: env.IDENTITY.userId }",
|
|
429
|
+
bot: "{ botId: invocation.botId }",
|
|
430
|
+
session: `{
|
|
431
|
+
sessionId: invocation.sessionId,
|
|
432
|
+
runId: invocation.runId,
|
|
433
|
+
turnId: invocation.turnId,
|
|
434
|
+
generationId: invocation.generationId,
|
|
435
|
+
}`,
|
|
436
|
+
packageId: "plugin.pluginId",
|
|
437
|
+
deadlineMs: "deadlineMs",
|
|
438
|
+
bindings: "Object.keys(env).sort()",
|
|
439
|
+
capabilities: `{
|
|
440
|
+
list: function () {
|
|
441
|
+
return capabilities.list(scope);
|
|
442
|
+
},
|
|
443
|
+
}`,
|
|
444
|
+
services: "plugin.services",
|
|
445
|
+
settings: `{
|
|
446
|
+
read: function () {
|
|
447
|
+
return capabilities.settings(scope);
|
|
448
|
+
},
|
|
449
|
+
}`,
|
|
450
|
+
} satisfies Partial<Record<keyof BotPackageContextV1, string>>;
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* One `ctx` member per grant. A grant the Plugin did not declare is never
|
|
454
|
+
* built, so `ctx.workspace` on a Plugin without the Workspace grant is
|
|
455
|
+
* `undefined` rather than a call that reaches the authority and is refused.
|
|
456
|
+
*/
|
|
457
|
+
const BOT_ISOLATE_GRANT_PROPERTY_SOURCE_V1 = {
|
|
458
|
+
ai: [
|
|
459
|
+
[
|
|
460
|
+
"model",
|
|
461
|
+
`{
|
|
462
|
+
invoke: async function (request) {
|
|
463
|
+
const outcome = await capabilities.invokeModel(scope, request);
|
|
464
|
+
if (!outcome || outcome.status !== "streaming") return outcome;
|
|
465
|
+
return {
|
|
466
|
+
status: "streaming",
|
|
467
|
+
requestId: outcome.requestId,
|
|
468
|
+
events: modelEvents(outcome.events),
|
|
469
|
+
};
|
|
470
|
+
},
|
|
471
|
+
}`,
|
|
472
|
+
],
|
|
473
|
+
],
|
|
474
|
+
jev: [
|
|
475
|
+
[
|
|
476
|
+
"jev",
|
|
477
|
+
`{
|
|
478
|
+
decide: function (request) {
|
|
479
|
+
return capabilities.jevDecide(scope, request);
|
|
480
|
+
},
|
|
481
|
+
}`,
|
|
482
|
+
],
|
|
483
|
+
],
|
|
484
|
+
memory: [
|
|
485
|
+
[
|
|
486
|
+
"memory",
|
|
487
|
+
`{
|
|
488
|
+
read: function (request) {
|
|
489
|
+
return capabilities.memoryRead(scope, request);
|
|
490
|
+
},
|
|
491
|
+
write: function (request) {
|
|
492
|
+
return capabilities.memoryWrite(scope, request);
|
|
493
|
+
},
|
|
494
|
+
forget: function (request) {
|
|
495
|
+
return capabilities.memoryForget(scope, request);
|
|
496
|
+
},
|
|
497
|
+
}`,
|
|
498
|
+
],
|
|
499
|
+
],
|
|
500
|
+
workspace: [
|
|
501
|
+
[
|
|
502
|
+
"workspace",
|
|
503
|
+
`{
|
|
504
|
+
read: function (path) {
|
|
505
|
+
return capabilities.workspaceRead(scope, path);
|
|
506
|
+
},
|
|
507
|
+
list: function (request) {
|
|
508
|
+
return capabilities.workspaceList(scope, request);
|
|
509
|
+
},
|
|
510
|
+
stat: function (path) {
|
|
511
|
+
return capabilities.workspaceStat(scope, path);
|
|
512
|
+
},
|
|
513
|
+
write: function (request) {
|
|
514
|
+
return capabilities.workspaceWrite(scope, request);
|
|
515
|
+
},
|
|
516
|
+
delete: function (request) {
|
|
517
|
+
return capabilities.workspaceDelete(scope, request);
|
|
518
|
+
},
|
|
519
|
+
}`,
|
|
520
|
+
],
|
|
521
|
+
],
|
|
522
|
+
// The one grant that opens two members: reaching a declared host, and
|
|
523
|
+
// asking the deployment's own sender to send mail for the Bot.
|
|
524
|
+
http: [
|
|
525
|
+
[
|
|
526
|
+
"connection",
|
|
527
|
+
"function (connectionId) { return capabilities.connection(scope, connectionId); }",
|
|
528
|
+
],
|
|
529
|
+
[
|
|
530
|
+
"email",
|
|
531
|
+
"function (request) { return capabilities.sendEmail(scope, request); }",
|
|
532
|
+
],
|
|
533
|
+
],
|
|
534
|
+
schedule: [
|
|
535
|
+
[
|
|
536
|
+
"schedule",
|
|
537
|
+
"function (request) { return capabilities.schedule(scope, request); }",
|
|
538
|
+
],
|
|
539
|
+
],
|
|
540
|
+
// A call to one of the Plugin's own device modules (ADR 0037). It is keyed
|
|
541
|
+
// by the tool call it runs inside and its place among that call's device
|
|
542
|
+
// calls, so a replayed Turn is answered from the record, never sent again.
|
|
543
|
+
device: [
|
|
544
|
+
[
|
|
545
|
+
"device",
|
|
546
|
+
`{
|
|
547
|
+
call: function (moduleId, call, input, options) {
|
|
548
|
+
const request = {
|
|
549
|
+
moduleId: moduleId,
|
|
550
|
+
call: call,
|
|
551
|
+
input: input === undefined ? null : input,
|
|
552
|
+
sequence: deviceCalls++,
|
|
553
|
+
};
|
|
554
|
+
if (options && options.deviceId !== undefined) {
|
|
555
|
+
request.deviceId = options.deviceId;
|
|
556
|
+
}
|
|
557
|
+
if (typeof invocation.effectId === "string") {
|
|
558
|
+
request.effectId = invocation.effectId;
|
|
559
|
+
}
|
|
560
|
+
return capabilities.deviceCall(scope, request);
|
|
561
|
+
},
|
|
562
|
+
}`,
|
|
563
|
+
],
|
|
564
|
+
],
|
|
565
|
+
storage: [
|
|
566
|
+
[
|
|
567
|
+
"storage",
|
|
568
|
+
`{
|
|
569
|
+
get: function (request) {
|
|
570
|
+
return capabilities.storageGet(scope, request);
|
|
571
|
+
},
|
|
572
|
+
put: function (request) {
|
|
573
|
+
return capabilities.storagePut(scope, request);
|
|
574
|
+
},
|
|
575
|
+
delete: function (request) {
|
|
576
|
+
return capabilities.storageDelete(scope, request);
|
|
577
|
+
},
|
|
578
|
+
list: function (request) {
|
|
579
|
+
return capabilities.storageList(scope, request);
|
|
580
|
+
},
|
|
581
|
+
}`,
|
|
582
|
+
],
|
|
583
|
+
],
|
|
584
|
+
} satisfies Record<string, [keyof BotPackageContextV1, string][]>;
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* The keys the generated wrapper can place on `ctx`. Every member of the
|
|
588
|
+
* context type is here: the common ones always, a grant member when its grant
|
|
589
|
+
* is held, and the serving-only ones — `modelTransport` above all — only while
|
|
590
|
+
* the host is serving a model call, which the contract names separately in
|
|
591
|
+
* `BOT_ISOLATE_SERVING_CONTEXT_KEYS_V1`.
|
|
592
|
+
*/
|
|
593
|
+
export const BOT_ISOLATE_NARROW_CONTEXT_KEYS_V1 = [
|
|
594
|
+
...Object.keys(BOT_ISOLATE_CONTEXT_PROPERTY_SOURCE_V1),
|
|
595
|
+
...Object.values(BOT_ISOLATE_GRANT_PROPERTY_SOURCE_V1).flatMap((members) =>
|
|
596
|
+
members.map(([key]) => key),
|
|
597
|
+
),
|
|
598
|
+
...BOT_ISOLATE_SERVING_CONTEXT_KEYS_V1,
|
|
599
|
+
] as Array<keyof BotPackageContextV1>;
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* One model provider's answer (ADR 0032), shared verbatim between the
|
|
603
|
+
* generated wrapper and the Bun test that proves it.
|
|
604
|
+
*
|
|
605
|
+
* The Plugin's `stream` is an async iterable of normalized events; the
|
|
606
|
+
* boundary carries bytes, so the wrapper encodes each event as one NDJSON
|
|
607
|
+
* line. The deadline is the model protocol's silence allowance rather than a
|
|
608
|
+
* bound on the whole answer: a reply may stream for minutes, and the timer is
|
|
609
|
+
* reset by every event, so a provider that stops producing them is stopped
|
|
610
|
+
* and one that keeps producing them is not. Cancelling the stream — a stop,
|
|
611
|
+
* the host's idle deadline, the Turn ending — returns the generator, which is
|
|
612
|
+
* what lets a Plugin release the upstream body it is reading.
|
|
613
|
+
*/
|
|
614
|
+
export const BOT_ISOLATE_MODEL_PROVIDER_SOURCE = `function modelEventStream(iterable, deadlineMs, firstEventDeadlineMs) {
|
|
615
|
+
const encoder = new TextEncoder();
|
|
616
|
+
const iterator = iterable[Symbol.asyncIterator]();
|
|
617
|
+
let timer;
|
|
618
|
+
let controller;
|
|
619
|
+
let done = false;
|
|
620
|
+
function stop() {
|
|
621
|
+
if (timer !== undefined) {
|
|
622
|
+
clearTimeout(timer);
|
|
623
|
+
timer = undefined;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
function abort(error) {
|
|
627
|
+
if (done) return;
|
|
628
|
+
done = true;
|
|
629
|
+
stop();
|
|
630
|
+
// The generator is released and the stream fails. A generator parked on a
|
|
631
|
+
// promise of its own is settled by the host aborting the call it is
|
|
632
|
+
// waiting on, which is why the host owns the upstream call.
|
|
633
|
+
void Promise.resolve(iterator.return?.(undefined)).catch(function () {});
|
|
634
|
+
if (controller) controller.error(error);
|
|
635
|
+
}
|
|
636
|
+
function arm(allowedMs) {
|
|
637
|
+
stop();
|
|
638
|
+
timer = setTimeout(function () {
|
|
639
|
+
abort(new Error("the model provider produced no event for " + allowedMs + "ms"));
|
|
640
|
+
}, allowedMs);
|
|
641
|
+
}
|
|
642
|
+
const stream = new ReadableStream({
|
|
643
|
+
start(value) {
|
|
644
|
+
controller = value;
|
|
645
|
+
// Waiting for the first event is the model protocol's first-byte
|
|
646
|
+
// allowance — a provider that has accepted a request and not answered
|
|
647
|
+
// yet is slow, not dead; after that, a minute of silence is a dead
|
|
648
|
+
// socket.
|
|
649
|
+
arm(firstEventDeadlineMs);
|
|
650
|
+
},
|
|
651
|
+
async pull(value) {
|
|
652
|
+
if (done) return;
|
|
653
|
+
try {
|
|
654
|
+
const next = await iterator.next();
|
|
655
|
+
if (done) return;
|
|
656
|
+
if (next.done) {
|
|
657
|
+
done = true;
|
|
658
|
+
stop();
|
|
659
|
+
value.close();
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
arm(deadlineMs);
|
|
663
|
+
value.enqueue(encoder.encode(JSON.stringify(next.value) + "\\n"));
|
|
664
|
+
} catch (error) {
|
|
665
|
+
abort(error);
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
cancel() {
|
|
669
|
+
done = true;
|
|
670
|
+
stop();
|
|
671
|
+
// Not awaited: a generator parked inside its transport call returns
|
|
672
|
+
// only when the host aborts that call, and the host is waiting on this
|
|
673
|
+
// cancel to do it. The return is queued instead, so cancellation is
|
|
674
|
+
// never what a hung provider is waiting behind.
|
|
675
|
+
void Promise.resolve(iterator.return?.(undefined)).catch(function () {});
|
|
676
|
+
return;
|
|
677
|
+
},
|
|
678
|
+
});
|
|
679
|
+
return stream;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
async function runModelStream(invocation, resolve, contextFor) {
|
|
683
|
+
try {
|
|
684
|
+
const plugin = resolve(invocation.pluginId);
|
|
685
|
+
if (!plugin.modelProviders.includes(invocation.provider)) {
|
|
686
|
+
throw new Error('plugin "' + invocation.pluginId + '" does not serve provider "' + invocation.provider + '"');
|
|
687
|
+
}
|
|
688
|
+
const context = contextFor(invocation, plugin, invocation.deadlineMs, invocation.transportId);
|
|
689
|
+
const iterable = plugin.module.modelProviders[invocation.provider].stream(
|
|
690
|
+
invocation.request,
|
|
691
|
+
context,
|
|
692
|
+
);
|
|
693
|
+
if (!iterable || typeof iterable[Symbol.asyncIterator] !== "function") {
|
|
694
|
+
throw new Error('plugin "' + invocation.pluginId + '" provider "' + invocation.provider + '" returned no event stream');
|
|
695
|
+
}
|
|
696
|
+
return {
|
|
697
|
+
schemaVersion: 1,
|
|
698
|
+
status: "streaming",
|
|
699
|
+
events: modelEventStream(
|
|
700
|
+
iterable,
|
|
701
|
+
invocation.deadlineMs,
|
|
702
|
+
invocation.firstEventDeadlineMs,
|
|
703
|
+
),
|
|
704
|
+
};
|
|
705
|
+
} catch (error) {
|
|
706
|
+
return { schemaVersion: 1, status: "refused", reason: errorText(error) };
|
|
707
|
+
}
|
|
708
|
+
}`;
|
|
709
|
+
|
|
710
|
+
export const BOT_ISOLATE_NARROW_CONTEXT_SOURCE_V1 = `function narrowContext(env, invocation, plugin, deadlineMs, transportId) {
|
|
711
|
+
const capabilities = env.CAPABILITIES;
|
|
712
|
+
const grants = plugin.grants || [];
|
|
713
|
+
// Every loopback call names the Turn, the Bot and the Plugin it is for: the
|
|
714
|
+
// stub itself is per User and carries none of that.
|
|
715
|
+
const scope = {
|
|
716
|
+
botId: invocation.botId,
|
|
717
|
+
sessionId: invocation.sessionId,
|
|
718
|
+
runId: invocation.runId,
|
|
719
|
+
turnId: invocation.turnId,
|
|
720
|
+
generationId: invocation.generationId,
|
|
721
|
+
pluginId: plugin.pluginId,
|
|
722
|
+
};
|
|
723
|
+
let deviceCalls = 0;
|
|
724
|
+
const context = {
|
|
725
|
+
${Object.entries(BOT_ISOLATE_CONTEXT_PROPERTY_SOURCE_V1)
|
|
726
|
+
.map(([key, source]) => ` ${JSON.stringify(key)}: ${source},`)
|
|
727
|
+
.join("\n")}
|
|
728
|
+
};
|
|
729
|
+
${Object.entries(BOT_ISOLATE_GRANT_PROPERTY_SOURCE_V1)
|
|
730
|
+
.flatMap(([grant, members]) =>
|
|
731
|
+
members.map(
|
|
732
|
+
([key, source]) =>
|
|
733
|
+
` if (grants.includes(${JSON.stringify(grant)})) {\n context[${JSON.stringify(key)}] = ${source};\n }`,
|
|
734
|
+
),
|
|
735
|
+
)
|
|
736
|
+
.join("\n")}
|
|
737
|
+
// One model call's one credentialed transport, and only while the host is
|
|
738
|
+
// serving that call: the ticket is the host's own and is spent by the call
|
|
739
|
+
// it is spent on, and the destination is the host's to choose. A tool
|
|
740
|
+
// call's context has no member to call.
|
|
741
|
+
if (transportId) {
|
|
742
|
+
context.modelTransport = function (request) {
|
|
743
|
+
const call = request && typeof request === "object" ? request : {};
|
|
744
|
+
return capabilities.modelTransport(scope, {
|
|
745
|
+
schemaVersion: 1,
|
|
746
|
+
transportId: transportId,
|
|
747
|
+
body: call.body,
|
|
748
|
+
});
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
return context;
|
|
752
|
+
}`;
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Which key of a hook's payload carries the value the hook may replace, so a
|
|
756
|
+
* later Plugin sees an earlier Plugin's change. A notification carries none.
|
|
757
|
+
*/
|
|
758
|
+
export const BOT_ISOLATE_HOOK_VALUE_KEYS_V1 = {
|
|
759
|
+
"system-prompt/assemble": "assembly",
|
|
760
|
+
"agent/tool-exposure": "tools",
|
|
761
|
+
"agent/request": "request",
|
|
762
|
+
"tools/pre-execute": "preparation",
|
|
763
|
+
"tools/post-execute": "result",
|
|
764
|
+
"agent/turn-stopping": null,
|
|
765
|
+
"theme/assemble": "document",
|
|
766
|
+
} as const satisfies Record<
|
|
767
|
+
(typeof BOT_ISOLATE_HOOK_EVENTS_V1)[number],
|
|
768
|
+
string | null
|
|
769
|
+
>;
|
|
770
|
+
|
|
771
|
+
/** How an error anywhere in the index is reduced to text for the kernel. */
|
|
772
|
+
export const BOT_ISOLATE_ERROR_TEXT_SOURCE = `function errorText(error) {
|
|
773
|
+
var text = String((error && error.message) || error).slice(0, ${MAX_FAILURE_REASON_V1});
|
|
774
|
+
return text.length > 0 ? text : "unknown error";
|
|
775
|
+
}`;
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* The hook chain, shared verbatim between the generated wrapper and the Bun
|
|
779
|
+
* test that proves it. The invocation's deadline is the budget for the whole
|
|
780
|
+
* chain, because the Durable Object races the single `hook()` call against
|
|
781
|
+
* that number plus a fixed margin: each Plugin is given only what is left of
|
|
782
|
+
* it, and a Plugin the chain reaches with nothing left is skipped and named
|
|
783
|
+
* rather than started on borrowed time the kernel would charge to everyone.
|
|
784
|
+
*/
|
|
785
|
+
export const BOT_ISOLATE_HOOK_CHAIN_SOURCE = `var HOOK_MIN_SLICE_MS = 25;
|
|
786
|
+
async function runHookChain(plugins, invocation, contextFor) {
|
|
787
|
+
const startedAt = Date.now();
|
|
788
|
+
const valueKey = HOOK_VALUE_KEYS[invocation.event];
|
|
789
|
+
const failures = [];
|
|
790
|
+
let replacement;
|
|
791
|
+
let replaced = false;
|
|
792
|
+
for (const plugin of plugins) {
|
|
793
|
+
if (!plugin.ok || !invocation.enabled.includes(plugin.pluginId)) continue;
|
|
794
|
+
if (!plugin.hooks.includes(invocation.event)) continue;
|
|
795
|
+
const remaining = invocation.deadlineMs - (Date.now() - startedAt);
|
|
796
|
+
if (remaining < HOOK_MIN_SLICE_MS) {
|
|
797
|
+
failures.push({
|
|
798
|
+
pluginId: plugin.pluginId,
|
|
799
|
+
reason: "the hook chain exhausted its deadline of " + invocation.deadlineMs + "ms before this plugin ran",
|
|
800
|
+
});
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
const payload =
|
|
804
|
+
replaced && valueKey
|
|
805
|
+
? Object.assign({}, invocation.payload, { [valueKey]: replacement })
|
|
806
|
+
: invocation.payload;
|
|
807
|
+
const context = contextFor(plugin, remaining);
|
|
808
|
+
try {
|
|
809
|
+
const value = await withIsolateDeadline(function () {
|
|
810
|
+
return plugin.module.hooks[invocation.event](payload, context);
|
|
811
|
+
}, remaining);
|
|
812
|
+
if (value !== undefined) {
|
|
813
|
+
if (!valueKey) {
|
|
814
|
+
throw new Error("a notification hook cannot replace a value");
|
|
815
|
+
}
|
|
816
|
+
replacement = value;
|
|
817
|
+
replaced = true;
|
|
818
|
+
}
|
|
819
|
+
} catch (error) {
|
|
820
|
+
failures.push({ pluginId: plugin.pluginId, reason: errorText(error) });
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return replaced
|
|
824
|
+
? { schemaVersion: 1, status: "replaced", replacement: replacement, failures: failures }
|
|
825
|
+
: { schemaVersion: 1, status: "unchanged", failures: failures };
|
|
826
|
+
}`;
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* One trigger delivered to one Plugin, shared verbatim between the generated
|
|
830
|
+
* wrapper and the Bun test that proves it. A trigger runs outside any Turn, so
|
|
831
|
+
* it narrows its context with the standalone call's identity the Bot sent.
|
|
832
|
+
* The fired text is returned whole: the Durable Object holds the contract's
|
|
833
|
+
* bound and names the Plugin when a body exceeds it.
|
|
834
|
+
*/
|
|
835
|
+
export const BOT_ISOLATE_TRIGGER_SOURCE = `async function runTrigger(invocation, resolve, contextFor) {
|
|
836
|
+
try {
|
|
837
|
+
const plugin = resolve(invocation.pluginId);
|
|
838
|
+
if (!plugin.triggers.includes(invocation.trigger)) {
|
|
839
|
+
throw new Error('plugin "' + invocation.pluginId + '" did not declare trigger "' + invocation.trigger + '"');
|
|
840
|
+
}
|
|
841
|
+
const context = contextFor(invocation, plugin, invocation.deadlineMs);
|
|
842
|
+
const value = await withIsolateDeadline(function () {
|
|
843
|
+
return plugin.module.triggers[invocation.trigger](
|
|
844
|
+
invocation.source
|
|
845
|
+
? {
|
|
846
|
+
headers: invocation.headers,
|
|
847
|
+
body: invocation.body,
|
|
848
|
+
source: {
|
|
849
|
+
kind: invocation.source.kind,
|
|
850
|
+
moduleId: invocation.source.moduleId,
|
|
851
|
+
machineId: invocation.source.machineId,
|
|
852
|
+
key: invocation.source.key,
|
|
853
|
+
},
|
|
854
|
+
}
|
|
855
|
+
: { headers: invocation.headers, body: invocation.body },
|
|
856
|
+
context,
|
|
857
|
+
);
|
|
858
|
+
}, invocation.deadlineMs);
|
|
859
|
+
if (typeof value === "string" && value.length > 0) {
|
|
860
|
+
return { schemaVersion: 1, status: "fire", text: value };
|
|
861
|
+
}
|
|
862
|
+
if (value && typeof value === "object" && value.drop === true) {
|
|
863
|
+
return Object.assign(
|
|
864
|
+
{ schemaVersion: 1, status: "drop" },
|
|
865
|
+
typeof value.reason === "string" ? { reason: errorText(value.reason) } : {},
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
return { schemaVersion: 1, status: "drop", reason: "the trigger returned no text" };
|
|
869
|
+
} catch (error) {
|
|
870
|
+
return { schemaVersion: 1, status: "drop", reason: errorText(error) };
|
|
871
|
+
}
|
|
872
|
+
}`;
|
|
873
|
+
|
|
874
|
+
/** What one Plugin's module must export, checked once at mount. */
|
|
875
|
+
export const BOT_ISOLATE_DECLARATION_SOURCE = `function declaredTools(module, pluginId) {
|
|
876
|
+
// A Plugin that only serves hooks, triggers or views exports an empty
|
|
877
|
+
// array: the build admits one, so the worker does too.
|
|
878
|
+
if (!Array.isArray(module.tools)) {
|
|
879
|
+
throw new Error('plugin "' + pluginId + '" must export a "tools" array');
|
|
880
|
+
}
|
|
881
|
+
const declared = module.tools;
|
|
882
|
+
if (typeof module.execute !== "function") {
|
|
883
|
+
throw new Error('plugin "' + pluginId + '" must export an "execute" function');
|
|
884
|
+
}
|
|
885
|
+
return declared.map(function (tool) {
|
|
886
|
+
if (!tool || typeof tool.name !== "string" || !TOOL_NAME.test(tool.name)) {
|
|
887
|
+
throw new Error('plugin "' + pluginId + '" declared a tool with an invalid name');
|
|
888
|
+
}
|
|
889
|
+
const schema =
|
|
890
|
+
tool.inputSchema && typeof tool.inputSchema === "object" && !Array.isArray(tool.inputSchema)
|
|
891
|
+
? tool.inputSchema
|
|
892
|
+
: {};
|
|
893
|
+
const admission =
|
|
894
|
+
tool.admission && typeof tool.admission === "object"
|
|
895
|
+
? {
|
|
896
|
+
turnTypes: tool.admission.turnTypes,
|
|
897
|
+
...(tool.admission.subagentRoles
|
|
898
|
+
? { subagentRoles: tool.admission.subagentRoles }
|
|
899
|
+
: {}),
|
|
900
|
+
}
|
|
901
|
+
: undefined;
|
|
902
|
+
return Object.assign(
|
|
903
|
+
{
|
|
904
|
+
name: tool.name,
|
|
905
|
+
description: typeof tool.description === "string" ? tool.description : "",
|
|
906
|
+
inputSchema: schema,
|
|
907
|
+
idempotent: tool.idempotent === true,
|
|
908
|
+
},
|
|
909
|
+
admission ? { admission: admission } : {},
|
|
910
|
+
);
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function declaredHooks(module, pluginId) {
|
|
915
|
+
if (module.hooks === undefined) return [];
|
|
916
|
+
if (!isRecord(module.hooks)) {
|
|
917
|
+
throw new Error('plugin "' + pluginId + '" "hooks" must be an object');
|
|
918
|
+
}
|
|
919
|
+
return Object.keys(module.hooks).map(function (event) {
|
|
920
|
+
if (!HOOK_EVENTS.includes(event)) {
|
|
921
|
+
throw new Error('plugin "' + pluginId + '" declared an unsupported hook "' + event + '"');
|
|
922
|
+
}
|
|
923
|
+
if (typeof module.hooks[event] !== "function") {
|
|
924
|
+
throw new Error('plugin "' + pluginId + '" hook "' + event + '" must be a function');
|
|
925
|
+
}
|
|
926
|
+
return event;
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function declaredServices(module, pluginId) {
|
|
931
|
+
if (module.services === undefined) return {};
|
|
932
|
+
if (!isRecord(module.services)) {
|
|
933
|
+
throw new Error('plugin "' + pluginId + '" "services" must be an object');
|
|
934
|
+
}
|
|
935
|
+
return module.services;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function declaredTriggers(module, pluginId) {
|
|
939
|
+
if (module.triggers === undefined) return [];
|
|
940
|
+
if (!isRecord(module.triggers)) {
|
|
941
|
+
throw new Error('plugin "' + pluginId + '" "triggers" must be an object');
|
|
942
|
+
}
|
|
943
|
+
return Object.keys(module.triggers).map(function (name) {
|
|
944
|
+
if (!TRIGGER_NAME.test(name)) {
|
|
945
|
+
throw new Error('plugin "' + pluginId + '" declared a trigger with an invalid name');
|
|
946
|
+
}
|
|
947
|
+
if (typeof module.triggers[name] !== "function") {
|
|
948
|
+
throw new Error('plugin "' + pluginId + '" trigger "' + name + '" must be a function');
|
|
949
|
+
}
|
|
950
|
+
return name;
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
function declaredCards(module, pluginId) {
|
|
954
|
+
if (module.cards === undefined) return [];
|
|
955
|
+
if (!isRecord(module.cards)) {
|
|
956
|
+
throw new Error('plugin "' + pluginId + '" "cards" must be an object');
|
|
957
|
+
}
|
|
958
|
+
const owner = {};
|
|
959
|
+
return Object.keys(module.cards).map(function (cardId) {
|
|
960
|
+
if (!CARD_ID.test(cardId)) {
|
|
961
|
+
throw new Error('plugin "' + pluginId + '" declared a card with an invalid id');
|
|
962
|
+
}
|
|
963
|
+
const card = module.cards[cardId];
|
|
964
|
+
if (!isRecord(card) || typeof card.render !== "function") {
|
|
965
|
+
throw new Error('plugin "' + pluginId + '" card "' + cardId + '" must export a render function');
|
|
966
|
+
}
|
|
967
|
+
if (card.revise !== undefined && typeof card.revise !== "function") {
|
|
968
|
+
throw new Error('plugin "' + pluginId + '" card "' + cardId + '" revise must be a function');
|
|
969
|
+
}
|
|
970
|
+
const actions = [];
|
|
971
|
+
if (card.actions !== undefined) {
|
|
972
|
+
if (!isRecord(card.actions)) {
|
|
973
|
+
throw new Error('plugin "' + pluginId + '" card "' + cardId + '" actions must be an object');
|
|
974
|
+
}
|
|
975
|
+
for (const name of Object.keys(card.actions)) {
|
|
976
|
+
if (typeof card.actions[name] !== "function") {
|
|
977
|
+
throw new Error('plugin "' + pluginId + '" card action "' + name + '" must be a function');
|
|
978
|
+
}
|
|
979
|
+
// A press names no card, so one action name on two cards would be two
|
|
980
|
+
// handlers behind one press and the one reached would be whichever
|
|
981
|
+
// card was scanned first. The descriptor refuses it; so does the
|
|
982
|
+
// mount, and a module that does it runs nothing at all.
|
|
983
|
+
if (Object.hasOwn(owner, name)) {
|
|
984
|
+
throw new Error('plugin "' + pluginId + '" declares card action "' + name + '" on both "' + owner[name] + '" and "' + cardId + '"');
|
|
985
|
+
}
|
|
986
|
+
owner[name] = cardId;
|
|
987
|
+
actions.push(name);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
return { id: cardId, actions: actions };
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
function declaredModelProviders(module, pluginId) {
|
|
994
|
+
if (module.modelProviders === undefined) return [];
|
|
995
|
+
if (!isRecord(module.modelProviders)) {
|
|
996
|
+
throw new Error('plugin "' + pluginId + '" "modelProviders" must be an object');
|
|
997
|
+
}
|
|
998
|
+
return Object.keys(module.modelProviders).map(function (providerId) {
|
|
999
|
+
if (!PROVIDER_ID.test(providerId)) {
|
|
1000
|
+
throw new Error('plugin "' + pluginId + '" declared a model provider with an invalid id');
|
|
1001
|
+
}
|
|
1002
|
+
const provider = module.modelProviders[providerId];
|
|
1003
|
+
if (!isRecord(provider) || typeof provider.stream !== "function") {
|
|
1004
|
+
throw new Error('plugin "' + pluginId + '" model provider "' + providerId + '" must export a stream function');
|
|
1005
|
+
}
|
|
1006
|
+
return providerId;
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function declaredViews(module, pluginId) {
|
|
1011
|
+
if (module.views === undefined) return [];
|
|
1012
|
+
if (!isRecord(module.views)) {
|
|
1013
|
+
throw new Error('plugin "' + pluginId + '" "views" must be an object');
|
|
1014
|
+
}
|
|
1015
|
+
return Object.keys(module.views).map(function (surfaceId) {
|
|
1016
|
+
if (!SURFACE_ID.test(surfaceId)) {
|
|
1017
|
+
throw new Error('plugin "' + pluginId + '" declared a view with an invalid surface id');
|
|
1018
|
+
}
|
|
1019
|
+
if (typeof module.views[surfaceId] !== "function") {
|
|
1020
|
+
throw new Error('plugin "' + pluginId + '" view "' + surfaceId + '" must be a function');
|
|
1021
|
+
}
|
|
1022
|
+
return surfaceId;
|
|
1023
|
+
});
|
|
1024
|
+
}`;
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* One slot render: the Plugin's view function is handed a `ctx` shaped like a
|
|
1028
|
+
* tool call's and answers with a document, or drops with a reason.
|
|
1029
|
+
*/
|
|
1030
|
+
export const BOT_ISOLATE_VIEW_SOURCE = `async function runView(invocation, resolve, contextFor) {
|
|
1031
|
+
try {
|
|
1032
|
+
const plugin = resolve(invocation.pluginId);
|
|
1033
|
+
if (!plugin.views.includes(invocation.surfaceId)) {
|
|
1034
|
+
throw new Error('plugin "' + invocation.pluginId + '" did not declare view "' + invocation.surfaceId + '"');
|
|
1035
|
+
}
|
|
1036
|
+
const context = contextFor(invocation, plugin, invocation.deadlineMs);
|
|
1037
|
+
const value = await withIsolateDeadline(function () {
|
|
1038
|
+
return plugin.module.views[invocation.surfaceId](context);
|
|
1039
|
+
}, invocation.deadlineMs);
|
|
1040
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
1041
|
+
return { schemaVersion: 1, status: "rendered", document: value };
|
|
1042
|
+
}
|
|
1043
|
+
return { schemaVersion: 1, status: "drop", reason: "the view returned no document" };
|
|
1044
|
+
} catch (error) {
|
|
1045
|
+
return { schemaVersion: 1, status: "drop", reason: errorText(error) };
|
|
1046
|
+
}
|
|
1047
|
+
}`;
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* Drawing one Card and answering one press on it, shared verbatim between the
|
|
1051
|
+
* generated wrapper and the Bun test that proves it.
|
|
1052
|
+
*
|
|
1053
|
+
* A handler answers with the A2UI messages the kernel folds — an array, or
|
|
1054
|
+
* `{ messages, input }` when it also has a line for the Bot's next Turn — and
|
|
1055
|
+
* anything else is a drop with its reason, so a Card is never half-redrawn.
|
|
1056
|
+
* A press's name carries no card: `plugin/<pluginId>/<action>` is the
|
|
1057
|
+
* plugin's namespace, so the press is resolved against the card the module
|
|
1058
|
+
* declared that action on. One name on two cards fails the mount, and the
|
|
1059
|
+
* host checks the declaration against the descriptor at health, so the card a
|
|
1060
|
+
* press reaches is the one the descriptor says owns it — never whichever card
|
|
1061
|
+
* the module happened to be scanned in first. The invocation's `cardId` is
|
|
1062
|
+
* the card the pressed surface was minted for: it is what the handler is told
|
|
1063
|
+
* it is on, and a press whose surface names a different card of the same
|
|
1064
|
+
* Plugin is refused rather than answered on the wrong surface.
|
|
1065
|
+
*/
|
|
1066
|
+
export const BOT_ISOLATE_CARD_SOURCE = `function cardAnswer(value) {
|
|
1067
|
+
if (Array.isArray(value)) {
|
|
1068
|
+
return { schemaVersion: 1, status: "rendered", messages: value };
|
|
1069
|
+
}
|
|
1070
|
+
if (isRecord(value) && Array.isArray(value.messages)) {
|
|
1071
|
+
return Object.assign(
|
|
1072
|
+
{ schemaVersion: 1, status: "rendered", messages: value.messages },
|
|
1073
|
+
typeof value.input === "string" && value.input.length > 0 ? { input: value.input } : {},
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
if (isRecord(value) && value.drop === true) {
|
|
1077
|
+
// The handler refused on purpose. Marked so the kernel can tell this
|
|
1078
|
+
// apart from a throw, an overrun or an answer it could not read, which
|
|
1079
|
+
// are the only drops a Plugin's health is charged for.
|
|
1080
|
+
return Object.assign(
|
|
1081
|
+
{ schemaVersion: 1, status: "drop", deliberate: true },
|
|
1082
|
+
typeof value.reason === "string" ? { reason: errorText(value.reason) } : {},
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
return { schemaVersion: 1, status: "drop", reason: "the card handler drew nothing" };
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
async function runRenderCard(invocation, resolve, contextFor) {
|
|
1089
|
+
try {
|
|
1090
|
+
const plugin = resolve(invocation.pluginId);
|
|
1091
|
+
const declared = plugin.cards.find(function (candidate) {
|
|
1092
|
+
return candidate.id === invocation.cardId;
|
|
1093
|
+
});
|
|
1094
|
+
if (!declared) {
|
|
1095
|
+
throw new Error('plugin "' + invocation.pluginId + '" did not declare card "' + invocation.cardId + '"');
|
|
1096
|
+
}
|
|
1097
|
+
const context = contextFor(invocation, plugin, invocation.deadlineMs);
|
|
1098
|
+
const value = await withIsolateDeadline(function () {
|
|
1099
|
+
return plugin.module.cards[invocation.cardId].render(
|
|
1100
|
+
{ surfaceId: invocation.surfaceId, data: invocation.data },
|
|
1101
|
+
context,
|
|
1102
|
+
);
|
|
1103
|
+
}, invocation.deadlineMs);
|
|
1104
|
+
const answer = cardAnswer(value);
|
|
1105
|
+
// A draw has no line for the Bot; only a press does. The deliberate flag
|
|
1106
|
+
// stays: a draw that refused in as many words is charged nothing, and
|
|
1107
|
+
// everything else is charged to the Plugin's health.
|
|
1108
|
+
delete answer.input;
|
|
1109
|
+
// What this draw is about, in the Plugin's own words. A card that asks
|
|
1110
|
+
// for a decision and names none of these is refused at the seam, so the
|
|
1111
|
+
// decision a person gives can never cover values they were not shown.
|
|
1112
|
+
if (answer.status === "rendered" && isRecord(value) && isRecord(value.covers)) {
|
|
1113
|
+
answer.covers = value.covers;
|
|
1114
|
+
}
|
|
1115
|
+
// And what that decision asks, in the Plugin's own words. The catalog
|
|
1116
|
+
// allows the ApprovalActions component nothing but its id and its labels,
|
|
1117
|
+
// so the wording rides here and the kernel records the Approval with it.
|
|
1118
|
+
if (answer.status === "rendered" && isRecord(value) && isRecord(value.decision)) {
|
|
1119
|
+
answer.decision = value.decision;
|
|
1120
|
+
}
|
|
1121
|
+
return answer;
|
|
1122
|
+
} catch (error) {
|
|
1123
|
+
return { schemaVersion: 1, status: "drop", reason: errorText(error) };
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
async function runCardAction(invocation, resolve, contextFor) {
|
|
1128
|
+
try {
|
|
1129
|
+
const plugin = resolve(invocation.pluginId);
|
|
1130
|
+
const owner = plugin.cards.find(function (candidate) {
|
|
1131
|
+
return candidate.actions.includes(invocation.action);
|
|
1132
|
+
});
|
|
1133
|
+
if (owner === undefined) {
|
|
1134
|
+
throw new Error('plugin "' + invocation.pluginId + '" declares no card action "' + invocation.action + '"');
|
|
1135
|
+
}
|
|
1136
|
+
// The card the pressed surface was minted for, as the kernel read it off
|
|
1137
|
+
// the surface id. The handler is still the one that declared the action;
|
|
1138
|
+
// this only refuses a press whose surface belongs to a different card of
|
|
1139
|
+
// the same Plugin, which would otherwise hand one card's handler another
|
|
1140
|
+
// card's surface, data model and record.
|
|
1141
|
+
if (owner.id !== invocation.cardId) {
|
|
1142
|
+
throw new Error('plugin "' + invocation.pluginId + '" card "' + invocation.cardId + '" does not declare action "' + invocation.action + '"');
|
|
1143
|
+
}
|
|
1144
|
+
const cardId = owner.id;
|
|
1145
|
+
const context = contextFor(invocation, plugin, invocation.deadlineMs);
|
|
1146
|
+
const value = await withIsolateDeadline(function () {
|
|
1147
|
+
return plugin.module.cards[cardId].actions[invocation.action](
|
|
1148
|
+
{
|
|
1149
|
+
cardId: cardId,
|
|
1150
|
+
surfaceId: invocation.surfaceId,
|
|
1151
|
+
action: invocation.action,
|
|
1152
|
+
context: invocation.context,
|
|
1153
|
+
dataModel: invocation.dataModel,
|
|
1154
|
+
record: invocation.record,
|
|
1155
|
+
},
|
|
1156
|
+
context,
|
|
1157
|
+
);
|
|
1158
|
+
}, invocation.deadlineMs);
|
|
1159
|
+
return cardAnswer(value);
|
|
1160
|
+
} catch (error) {
|
|
1161
|
+
return { schemaVersion: 1, status: "drop", reason: errorText(error) };
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
async function runReviseCard(invocation, resolve, contextFor) {
|
|
1166
|
+
try {
|
|
1167
|
+
const plugin = resolve(invocation.pluginId);
|
|
1168
|
+
const declared = plugin.cards.find(function (candidate) {
|
|
1169
|
+
return candidate.id === invocation.cardId;
|
|
1170
|
+
});
|
|
1171
|
+
if (!declared) {
|
|
1172
|
+
throw new Error('plugin "' + invocation.pluginId + '" did not declare card "' + invocation.cardId + '"');
|
|
1173
|
+
}
|
|
1174
|
+
const card = plugin.module.cards[invocation.cardId];
|
|
1175
|
+
// A card that takes no edits is decided as it was drawn. Whatever its
|
|
1176
|
+
// fields hold is the person's answer to the Bot, not a change to what
|
|
1177
|
+
// the decision covers, and saying so costs the Plugin nothing.
|
|
1178
|
+
if (typeof card.revise !== "function") {
|
|
1179
|
+
return { schemaVersion: 1, status: "unchanged" };
|
|
1180
|
+
}
|
|
1181
|
+
const context = contextFor(invocation, plugin, invocation.deadlineMs);
|
|
1182
|
+
const value = await withIsolateDeadline(function () {
|
|
1183
|
+
return card.revise(
|
|
1184
|
+
{
|
|
1185
|
+
cardId: invocation.cardId,
|
|
1186
|
+
surfaceId: invocation.surfaceId,
|
|
1187
|
+
dataModel: invocation.dataModel,
|
|
1188
|
+
record: invocation.record,
|
|
1189
|
+
},
|
|
1190
|
+
context,
|
|
1191
|
+
);
|
|
1192
|
+
}, invocation.deadlineMs);
|
|
1193
|
+
if (isRecord(value) && value.drop === true) {
|
|
1194
|
+
return Object.assign(
|
|
1195
|
+
{ schemaVersion: 1, status: "drop", deliberate: true },
|
|
1196
|
+
typeof value.reason === "string" ? { reason: errorText(value.reason) } : {},
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
// A revision has to say what the decision now covers and in what words,
|
|
1200
|
+
// or there is nothing the kernel could bind the person's Send to.
|
|
1201
|
+
if (isRecord(value) && isRecord(value.covers) && isRecord(value.decision)) {
|
|
1202
|
+
return Object.assign(
|
|
1203
|
+
{ schemaVersion: 1, status: "revised", covers: value.covers, decision: value.decision },
|
|
1204
|
+
Array.isArray(value.messages) && value.messages.length > 0 ? { messages: value.messages } : {},
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
return { schemaVersion: 1, status: "drop", reason: "the card's revise named no covers or no decision" };
|
|
1208
|
+
} catch (error) {
|
|
1209
|
+
return { schemaVersion: 1, status: "drop", reason: errorText(error) };
|
|
1210
|
+
}
|
|
1211
|
+
}`;
|
|
1212
|
+
|
|
1213
|
+
/** The path a Plugin's artifact is mounted at inside the worker's module map. */
|
|
1214
|
+
export function pluginWorkerModulePathV1(pluginId: string): string {
|
|
1215
|
+
return `plugins/${pluginId}.js`;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
export const PLUGIN_WORKER_MAIN_MODULE = "index.js";
|
|
1219
|
+
|
|
1220
|
+
/**
|
|
1221
|
+
* The index module text for one set of Plugins, in mount order. The order is
|
|
1222
|
+
* the host's: providers before the Plugins that consume them.
|
|
1223
|
+
*/
|
|
1224
|
+
export function pluginWorkerIndexSourceV1(
|
|
1225
|
+
pluginIds: readonly string[],
|
|
1226
|
+
): string {
|
|
1227
|
+
const imports = pluginIds
|
|
1228
|
+
.map(
|
|
1229
|
+
(pluginId, index) =>
|
|
1230
|
+
`import * as plugin_${index} from ${JSON.stringify(`./${pluginWorkerModulePathV1(pluginId)}`)};`,
|
|
1231
|
+
)
|
|
1232
|
+
.join("\n");
|
|
1233
|
+
const modules = pluginIds
|
|
1234
|
+
.map((pluginId, index) => ` ${JSON.stringify(pluginId)}: plugin_${index},`)
|
|
1235
|
+
.join("\n");
|
|
1236
|
+
return `// Generated by @frockbot/frock-compose. Do not edit inside the isolate.
|
|
1237
|
+
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
1238
|
+
${imports}
|
|
1239
|
+
|
|
1240
|
+
const CONTRACT_VERSION = ${ISOLATE_CONTRACT_VERSION};
|
|
1241
|
+
const PLUGIN_MODULES = {
|
|
1242
|
+
${modules}
|
|
1243
|
+
};
|
|
1244
|
+
const HOOK_VALUE_KEYS = ${JSON.stringify(BOT_ISOLATE_HOOK_VALUE_KEYS_V1)};
|
|
1245
|
+
|
|
1246
|
+
${BOT_ISOLATE_DEADLINE_SOURCE}
|
|
1247
|
+
|
|
1248
|
+
${BOT_ISOLATE_INVOCATION_SOURCE}
|
|
1249
|
+
|
|
1250
|
+
${BOT_ISOLATE_MODEL_SOURCE}
|
|
1251
|
+
|
|
1252
|
+
${BOT_ISOLATE_DECLARATION_SOURCE}
|
|
1253
|
+
|
|
1254
|
+
${BOT_ISOLATE_NARROW_CONTEXT_SOURCE_V1}
|
|
1255
|
+
|
|
1256
|
+
${BOT_ISOLATE_ERROR_TEXT_SOURCE}
|
|
1257
|
+
|
|
1258
|
+
${BOT_ISOLATE_HOOK_CHAIN_SOURCE}
|
|
1259
|
+
|
|
1260
|
+
${BOT_ISOLATE_TRIGGER_SOURCE}
|
|
1261
|
+
|
|
1262
|
+
${BOT_ISOLATE_VIEW_SOURCE}
|
|
1263
|
+
|
|
1264
|
+
${BOT_ISOLATE_CARD_SOURCE}
|
|
1265
|
+
|
|
1266
|
+
${BOT_ISOLATE_MODEL_PROVIDER_SOURCE}
|
|
1267
|
+
|
|
1268
|
+
/**
|
|
1269
|
+
* Every Plugin the identity names, mounted once in identity order. A Plugin
|
|
1270
|
+
* whose module does not declare itself correctly is carried as not ok and
|
|
1271
|
+
* never invoked; the others still mount. A service a Plugin provides is read
|
|
1272
|
+
* from its module once here and handed to the Plugins mounted after it that
|
|
1273
|
+
* consume it, so a provider is always mounted before its consumers.
|
|
1274
|
+
*/
|
|
1275
|
+
let mountedPlugins;
|
|
1276
|
+
function mountAll(env) {
|
|
1277
|
+
if (mountedPlugins) return mountedPlugins;
|
|
1278
|
+
const provided = {};
|
|
1279
|
+
mountedPlugins = (env.IDENTITY.plugins || []).map(function (identity) {
|
|
1280
|
+
const pluginId = identity.pluginId;
|
|
1281
|
+
const module = PLUGIN_MODULES[pluginId];
|
|
1282
|
+
const plugin = {
|
|
1283
|
+
pluginId: pluginId,
|
|
1284
|
+
grants: identity.grants || [],
|
|
1285
|
+
consumes: identity.consumes || [],
|
|
1286
|
+
module: module,
|
|
1287
|
+
ok: false,
|
|
1288
|
+
reason: undefined,
|
|
1289
|
+
tools: [],
|
|
1290
|
+
hooks: [],
|
|
1291
|
+
provides: [],
|
|
1292
|
+
triggers: [],
|
|
1293
|
+
modelProviders: [],
|
|
1294
|
+
views: [],
|
|
1295
|
+
cards: [],
|
|
1296
|
+
services: {},
|
|
1297
|
+
};
|
|
1298
|
+
try {
|
|
1299
|
+
if (!module) throw new Error('plugin "' + pluginId + '" has no module');
|
|
1300
|
+
plugin.tools = declaredTools(module, pluginId);
|
|
1301
|
+
plugin.hooks = declaredHooks(module, pluginId);
|
|
1302
|
+
plugin.triggers = declaredTriggers(module, pluginId);
|
|
1303
|
+
plugin.views = declaredViews(module, pluginId);
|
|
1304
|
+
plugin.cards = declaredCards(module, pluginId);
|
|
1305
|
+
plugin.modelProviders = declaredModelProviders(module, pluginId);
|
|
1306
|
+
const services = declaredServices(module, pluginId);
|
|
1307
|
+
plugin.provides = Object.keys(services);
|
|
1308
|
+
for (const name of plugin.consumes) {
|
|
1309
|
+
if (!Object.hasOwn(provided, name)) {
|
|
1310
|
+
throw new Error('plugin "' + pluginId + '" consumes "' + name + '", which no earlier plugin provides');
|
|
1311
|
+
}
|
|
1312
|
+
plugin.services[name] = provided[name];
|
|
1313
|
+
}
|
|
1314
|
+
for (const name of plugin.provides) provided[name] = services[name];
|
|
1315
|
+
plugin.ok = true;
|
|
1316
|
+
} catch (error) {
|
|
1317
|
+
plugin.reason = errorText(error);
|
|
1318
|
+
}
|
|
1319
|
+
return plugin;
|
|
1320
|
+
});
|
|
1321
|
+
return mountedPlugins;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function findPlugin(env, pluginId) {
|
|
1325
|
+
const plugin = mountAll(env).find(function (candidate) {
|
|
1326
|
+
return candidate.pluginId === pluginId;
|
|
1327
|
+
});
|
|
1328
|
+
if (!plugin) throw new Error('plugin "' + pluginId + '" is not mounted');
|
|
1329
|
+
if (!plugin.ok) throw new Error('plugin "' + pluginId + '" failed to mount: ' + plugin.reason);
|
|
1330
|
+
return plugin;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
export default class extends WorkerEntrypoint {
|
|
1334
|
+
async health() {
|
|
1335
|
+
return {
|
|
1336
|
+
schemaVersion: 1,
|
|
1337
|
+
contractVersion: CONTRACT_VERSION,
|
|
1338
|
+
plugins: mountAll(this.env).map(function (plugin) {
|
|
1339
|
+
return Object.assign(
|
|
1340
|
+
{
|
|
1341
|
+
pluginId: plugin.pluginId,
|
|
1342
|
+
ok: plugin.ok,
|
|
1343
|
+
tools: plugin.tools,
|
|
1344
|
+
hooks: plugin.hooks,
|
|
1345
|
+
provides: plugin.provides.map(function (name) {
|
|
1346
|
+
return { name: name, version: 1 };
|
|
1347
|
+
}),
|
|
1348
|
+
consumes: plugin.consumes.map(function (name) {
|
|
1349
|
+
return { name: name, version: 1 };
|
|
1350
|
+
}),
|
|
1351
|
+
triggers: plugin.triggers,
|
|
1352
|
+
modelProviders: plugin.modelProviders,
|
|
1353
|
+
views: plugin.views,
|
|
1354
|
+
cards: plugin.cards,
|
|
1355
|
+
},
|
|
1356
|
+
plugin.ok ? {} : { reason: plugin.reason },
|
|
1357
|
+
);
|
|
1358
|
+
}),
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
async execute(rawInvocation) {
|
|
1363
|
+
let invocation;
|
|
1364
|
+
try {
|
|
1365
|
+
invocation = decodeInvocation(rawInvocation);
|
|
1366
|
+
} catch (error) {
|
|
1367
|
+
return { schemaVersion: 1, content: errorText(error), isError: true };
|
|
1368
|
+
}
|
|
1369
|
+
try {
|
|
1370
|
+
const plugin = findPlugin(this.env, invocation.pluginId);
|
|
1371
|
+
const context = narrowContext(this.env, invocation, plugin, invocation.deadlineMs);
|
|
1372
|
+
const value = await withIsolateDeadline(function () {
|
|
1373
|
+
return plugin.module.execute(invocation.tool, invocation.input, context);
|
|
1374
|
+
}, invocation.deadlineMs);
|
|
1375
|
+
return {
|
|
1376
|
+
schemaVersion: 1,
|
|
1377
|
+
content: typeof value === "string" ? value : JSON.stringify(value ?? null),
|
|
1378
|
+
isError: false,
|
|
1379
|
+
};
|
|
1380
|
+
} catch (error) {
|
|
1381
|
+
return { schemaVersion: 1, content: errorText(error), isError: true };
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
/**
|
|
1386
|
+
* One model call, served by the Plugin that declared the provider
|
|
1387
|
+
* (ADR 0032). The answer's events are an NDJSON byte stream; a provider
|
|
1388
|
+
* that refuses before reaching upstream answers with its reason.
|
|
1389
|
+
*/
|
|
1390
|
+
async streamModel(rawInvocation) {
|
|
1391
|
+
let invocation;
|
|
1392
|
+
try {
|
|
1393
|
+
invocation = decodeModelInvocation(rawInvocation);
|
|
1394
|
+
} catch (error) {
|
|
1395
|
+
return { schemaVersion: 1, status: "refused", reason: errorText(error) };
|
|
1396
|
+
}
|
|
1397
|
+
const env = this.env;
|
|
1398
|
+
return runModelStream(
|
|
1399
|
+
invocation,
|
|
1400
|
+
function (pluginId) {
|
|
1401
|
+
return findPlugin(env, pluginId);
|
|
1402
|
+
},
|
|
1403
|
+
function (identity, plugin, deadlineMs, transportId) {
|
|
1404
|
+
return narrowContext(env, identity, plugin, deadlineMs, transportId);
|
|
1405
|
+
},
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
/**
|
|
1410
|
+
* One call per open hook per Turn. The enabled Plugins that declared the
|
|
1411
|
+
* event run in mount order; each sees the value the one before it left,
|
|
1412
|
+
* and a Plugin that throws, times out or answers with something that is
|
|
1413
|
+
* not a value is skipped and named, never allowed to stop the chain.
|
|
1414
|
+
*/
|
|
1415
|
+
async hook(rawInvocation) {
|
|
1416
|
+
const invocation = decodeHookInvocation(rawInvocation);
|
|
1417
|
+
const env = this.env;
|
|
1418
|
+
return runHookChain(mountAll(env), invocation, function (plugin, deadlineMs) {
|
|
1419
|
+
return narrowContext(env, invocation, plugin, deadlineMs);
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
async receiveTrigger(rawInvocation) {
|
|
1424
|
+
const invocation = decodeTriggerInvocation(rawInvocation);
|
|
1425
|
+
const env = this.env;
|
|
1426
|
+
return runTrigger(
|
|
1427
|
+
invocation,
|
|
1428
|
+
function (pluginId) {
|
|
1429
|
+
return findPlugin(env, pluginId);
|
|
1430
|
+
},
|
|
1431
|
+
function (identity, plugin, deadlineMs) {
|
|
1432
|
+
return narrowContext(env, identity, plugin, deadlineMs);
|
|
1433
|
+
},
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
/**
|
|
1438
|
+
* One press on a Card this Plugin drew (ADR 0030). The handler answers with
|
|
1439
|
+
* the messages the kernel folds; a handler that throws, overruns or answers
|
|
1440
|
+
* with anything else is a drop and the Card is left exactly as it was.
|
|
1441
|
+
*/
|
|
1442
|
+
async cardAction(rawInvocation) {
|
|
1443
|
+
let invocation;
|
|
1444
|
+
try {
|
|
1445
|
+
invocation = decodeCardActionInvocation(rawInvocation);
|
|
1446
|
+
} catch (error) {
|
|
1447
|
+
return { schemaVersion: 1, status: "drop", reason: errorText(error) };
|
|
1448
|
+
}
|
|
1449
|
+
const env = this.env;
|
|
1450
|
+
return runCardAction(
|
|
1451
|
+
invocation,
|
|
1452
|
+
function (pluginId) {
|
|
1453
|
+
return findPlugin(env, pluginId);
|
|
1454
|
+
},
|
|
1455
|
+
function (identity, plugin, deadlineMs) {
|
|
1456
|
+
return narrowContext(env, identity, plugin, deadlineMs);
|
|
1457
|
+
},
|
|
1458
|
+
);
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
/** One Card drawn from the values the Bot sent to its card tool. */
|
|
1462
|
+
async renderCard(rawInvocation) {
|
|
1463
|
+
let invocation;
|
|
1464
|
+
try {
|
|
1465
|
+
invocation = decodeRenderCardInvocation(rawInvocation);
|
|
1466
|
+
} catch (error) {
|
|
1467
|
+
return { schemaVersion: 1, status: "drop", reason: errorText(error) };
|
|
1468
|
+
}
|
|
1469
|
+
const env = this.env;
|
|
1470
|
+
return runRenderCard(
|
|
1471
|
+
invocation,
|
|
1472
|
+
function (pluginId) {
|
|
1473
|
+
return findPlugin(env, pluginId);
|
|
1474
|
+
},
|
|
1475
|
+
function (identity, plugin, deadlineMs) {
|
|
1476
|
+
return narrowContext(env, identity, plugin, deadlineMs);
|
|
1477
|
+
},
|
|
1478
|
+
);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
/**
|
|
1482
|
+
* One card a person edited and then approved: its Plugin restates what the
|
|
1483
|
+
* decision now covers before the kernel records it.
|
|
1484
|
+
*/
|
|
1485
|
+
async reviseCard(rawInvocation) {
|
|
1486
|
+
let invocation;
|
|
1487
|
+
try {
|
|
1488
|
+
invocation = decodeReviseCardInvocation(rawInvocation);
|
|
1489
|
+
} catch (error) {
|
|
1490
|
+
return { schemaVersion: 1, status: "drop", reason: errorText(error) };
|
|
1491
|
+
}
|
|
1492
|
+
const env = this.env;
|
|
1493
|
+
return runReviseCard(
|
|
1494
|
+
invocation,
|
|
1495
|
+
function (pluginId) {
|
|
1496
|
+
return findPlugin(env, pluginId);
|
|
1497
|
+
},
|
|
1498
|
+
function (identity, plugin, deadlineMs) {
|
|
1499
|
+
return narrowContext(env, identity, plugin, deadlineMs);
|
|
1500
|
+
},
|
|
1501
|
+
);
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
async view(rawInvocation) {
|
|
1505
|
+
let invocation;
|
|
1506
|
+
try {
|
|
1507
|
+
invocation = decodeViewInvocation(rawInvocation);
|
|
1508
|
+
} catch (error) {
|
|
1509
|
+
return { schemaVersion: 1, status: "drop", reason: errorText(error) };
|
|
1510
|
+
}
|
|
1511
|
+
const env = this.env;
|
|
1512
|
+
return runView(
|
|
1513
|
+
invocation,
|
|
1514
|
+
function (pluginId) {
|
|
1515
|
+
return findPlugin(env, pluginId);
|
|
1516
|
+
},
|
|
1517
|
+
function (identity, plugin, deadlineMs) {
|
|
1518
|
+
return narrowContext(env, identity, plugin, deadlineMs);
|
|
1519
|
+
},
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
`;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
/**
|
|
1527
|
+
* Bumped with any change to the generated text; folded into the module-set
|
|
1528
|
+
* hash beside the contract version, so a wrapper change is a new worker.
|
|
1529
|
+
*/
|
|
1530
|
+
export const PLUGIN_WORKER_INDEX_VERSION = "index-v12";
|
|
1531
|
+
|
|
1532
|
+
/** The module map a Plugin worker mounts: the index and one module per Plugin. */
|
|
1533
|
+
export function pluginWorkerModuleMap(
|
|
1534
|
+
plugins: readonly { pluginId: string; source: string }[],
|
|
1535
|
+
): Record<string, { js: string }> {
|
|
1536
|
+
const modules: Record<string, { js: string }> = {
|
|
1537
|
+
[PLUGIN_WORKER_MAIN_MODULE]: {
|
|
1538
|
+
js: pluginWorkerIndexSourceV1(plugins.map((plugin) => plugin.pluginId)),
|
|
1539
|
+
},
|
|
1540
|
+
};
|
|
1541
|
+
for (const plugin of plugins) {
|
|
1542
|
+
modules[pluginWorkerModulePathV1(plugin.pluginId)] = { js: plugin.source };
|
|
1543
|
+
}
|
|
1544
|
+
return modules;
|
|
1545
|
+
}
|