@frockbot/plugin-shell 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/frockbot.json +4 -0
- package/package.json +30 -28
- package/src/agent.ts +10 -13
- package/src/backend-authoring.test.ts +356 -2
- package/src/backend-authoring.ts +585 -20
- package/src/backend-composition-input.test.ts +65 -0
- package/src/backend-composition-input.ts +58 -0
- package/src/backend-composition.ts +23 -5
- package/src/backend-configuration.test.ts +549 -1468
- package/src/backend-contracts.test.ts +28 -0
- package/src/backend-contracts.ts +3 -1
- package/src/backend-execution.ts +0 -4
- package/src/backend-iframe-ui.test.ts +131 -0
- package/src/backend-image.test.ts +8 -8
- package/src/backend-image.ts +13 -13
- package/src/backend-isolate.test.ts +230 -89
- package/src/backend-isolate.ts +103 -200
- package/src/backend-package-catalog.test.ts +451 -0
- package/src/backend-package-catalog.ts +923 -0
- package/src/backend-recovery-integration.test.ts +17 -67
- package/src/backend-routines.ts +1 -1
- package/src/backend-runner-iframe.test.ts +74 -0
- package/src/backend-runner.ts +192 -0
- package/src/backend.ts +1198 -1781
- package/src/client/FrockBotApp.vue +44 -73
- package/src/client/PackageIframeHost.vue +218 -0
- package/src/client/PackageIframeSettings.vue +52 -0
- package/src/client/SendPayloadView.vue +0 -60
- package/src/client/index.test.ts +439 -380
- package/src/client/index.ts +163 -257
- package/src/client/model-presentation.test.ts +21 -8
- package/src/client/model-presentation.ts +14 -7
- package/src/client/package-iframe-host-message.test.ts +41 -0
- package/src/client/package-iframe-host-message.ts +27 -0
- package/src/client/styles.css +0 -27
- package/src/composition-views.ts +54 -0
- package/src/settings-links.test.ts +2 -10
- package/src/settings-links.ts +1 -13
- package/src/shared.ts +10 -13
- package/src/backend-assignment.test.ts +0 -161
- package/src/backend-assignment.ts +0 -274
package/src/client/index.test.ts
CHANGED
|
@@ -173,7 +173,7 @@ describe("application manifest protocol", () => {
|
|
|
173
173
|
],
|
|
174
174
|
}),
|
|
175
175
|
).toEqual([]);
|
|
176
|
-
// A tool Package a User
|
|
176
|
+
// A tool Package a User enables with no credential at all:
|
|
177
177
|
// one Capability, no Connection Type. It stays in the catalog, because
|
|
178
178
|
// needing no Connection is not the same as offering nothing.
|
|
179
179
|
expect(
|
|
@@ -202,6 +202,55 @@ describe("application manifest protocol", () => {
|
|
|
202
202
|
connectionTypes: [],
|
|
203
203
|
}),
|
|
204
204
|
]);
|
|
205
|
+
// A settings-only Package still has User-visible enablement: turning it on
|
|
206
|
+
// is what makes its otherwise-inert controls available.
|
|
207
|
+
expect(
|
|
208
|
+
decodePluginCatalog({
|
|
209
|
+
...emptyManifest,
|
|
210
|
+
packages: [
|
|
211
|
+
{
|
|
212
|
+
id: "custom-models",
|
|
213
|
+
displayName: "Custom models",
|
|
214
|
+
version: "0.0.1",
|
|
215
|
+
contributions: ["client"],
|
|
216
|
+
configuration: {
|
|
217
|
+
settings: [
|
|
218
|
+
{
|
|
219
|
+
id: "account-model",
|
|
220
|
+
schemaVersion: 1,
|
|
221
|
+
scopes: ["user"],
|
|
222
|
+
role: "model",
|
|
223
|
+
schema: {
|
|
224
|
+
type: "object",
|
|
225
|
+
properties: {
|
|
226
|
+
connectionId: { type: "string" },
|
|
227
|
+
providerModelId: { type: "string" },
|
|
228
|
+
},
|
|
229
|
+
required: ["connectionId", "providerModelId"],
|
|
230
|
+
additionalProperties: false,
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
connectionTypes: [],
|
|
235
|
+
capabilities: [],
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
}),
|
|
240
|
+
).toEqual([
|
|
241
|
+
expect.objectContaining({
|
|
242
|
+
packageId: "custom-models",
|
|
243
|
+
capabilities: [],
|
|
244
|
+
connectionTypes: [],
|
|
245
|
+
settings: [
|
|
246
|
+
expect.objectContaining({
|
|
247
|
+
id: "account-model",
|
|
248
|
+
scopes: ["user"],
|
|
249
|
+
role: "model",
|
|
250
|
+
}),
|
|
251
|
+
],
|
|
252
|
+
}),
|
|
253
|
+
]);
|
|
205
254
|
// The same Package with a turn-type admission ceiling: manifest v4 is what
|
|
206
255
|
// the catalog wraps the served configuration as, so the field decodes.
|
|
207
256
|
expect(
|
|
@@ -289,121 +338,6 @@ describe("composer hydration context", () => {
|
|
|
289
338
|
});
|
|
290
339
|
});
|
|
291
340
|
|
|
292
|
-
describe("hosted Assignment commands", () => {
|
|
293
|
-
test("uses distinct atomic Assign, Replace, and Unassign commands", async () => {
|
|
294
|
-
let provided: Ref<FrockBotWebData> | undefined;
|
|
295
|
-
const commands: unknown[] = [];
|
|
296
|
-
await shellClientPlugin({
|
|
297
|
-
transport: {
|
|
298
|
-
turn: () => Promise.resolve({ runId: "run", text: "", events: [] }),
|
|
299
|
-
readConfiguration: () =>
|
|
300
|
-
Promise.resolve(initializeBotSettingsV1("primary")),
|
|
301
|
-
executeConfiguration: (command) => {
|
|
302
|
-
commands.push(command);
|
|
303
|
-
return Promise.resolve({
|
|
304
|
-
schemaVersion: 1,
|
|
305
|
-
commandId: command.commandId,
|
|
306
|
-
revision: command.expectedRevision + 1,
|
|
307
|
-
status: "applied",
|
|
308
|
-
});
|
|
309
|
-
},
|
|
310
|
-
},
|
|
311
|
-
slot: () => () => {},
|
|
312
|
-
inject: () => {
|
|
313
|
-
throw new Error("unexpected client provider injection");
|
|
314
|
-
},
|
|
315
|
-
provide: (_key, value) => {
|
|
316
|
-
provided = value as Ref<FrockBotWebData>;
|
|
317
|
-
return () => {};
|
|
318
|
-
},
|
|
319
|
-
});
|
|
320
|
-
if (!provided) throw new Error("shell data was not provided");
|
|
321
|
-
provided.value.activeBotId = "primary";
|
|
322
|
-
provided.value.botSettings = initializeBotSettingsV1("primary");
|
|
323
|
-
const assignment = {
|
|
324
|
-
assignmentId: "mail",
|
|
325
|
-
packageId: "mail",
|
|
326
|
-
capabilityId: "send",
|
|
327
|
-
connectionId: "mail-1",
|
|
328
|
-
};
|
|
329
|
-
await provided.value.assignCapability(assignment);
|
|
330
|
-
provided.value.botSettings = {
|
|
331
|
-
...initializeBotSettingsV1("primary"),
|
|
332
|
-
revision: 1,
|
|
333
|
-
};
|
|
334
|
-
await provided.value.replaceCapability(assignment);
|
|
335
|
-
provided.value.botSettings = {
|
|
336
|
-
...initializeBotSettingsV1("primary"),
|
|
337
|
-
revision: 2,
|
|
338
|
-
};
|
|
339
|
-
await provided.value.unassignCapability("mail");
|
|
340
|
-
expect(commands).toMatchObject([
|
|
341
|
-
{ type: "bot/assign-capability", expectedRevision: 0, assignment },
|
|
342
|
-
{ type: "bot/replace-capability", expectedRevision: 1, assignment },
|
|
343
|
-
{
|
|
344
|
-
type: "bot/unassign-capability",
|
|
345
|
-
expectedRevision: 2,
|
|
346
|
-
assignmentId: "mail",
|
|
347
|
-
},
|
|
348
|
-
]);
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
test("surfaces rejected and retrying Assignment receipts", async () => {
|
|
352
|
-
let provided: Ref<FrockBotWebData> | undefined;
|
|
353
|
-
const receipts = [
|
|
354
|
-
{
|
|
355
|
-
schemaVersion: 1 as const,
|
|
356
|
-
commandId: "rejected",
|
|
357
|
-
revision: 0,
|
|
358
|
-
status: "rejected" as const,
|
|
359
|
-
failure: "Connection is unavailable",
|
|
360
|
-
},
|
|
361
|
-
{
|
|
362
|
-
schemaVersion: 1 as const,
|
|
363
|
-
commandId: "pending",
|
|
364
|
-
revision: 0,
|
|
365
|
-
status: "pending" as const,
|
|
366
|
-
},
|
|
367
|
-
];
|
|
368
|
-
await shellClientPlugin({
|
|
369
|
-
transport: {
|
|
370
|
-
turn: () => Promise.resolve({ runId: "run", text: "", events: [] }),
|
|
371
|
-
readConfiguration: () =>
|
|
372
|
-
Promise.resolve(initializeBotSettingsV1("primary")),
|
|
373
|
-
executeConfiguration: () => Promise.resolve(receipts.shift()!),
|
|
374
|
-
},
|
|
375
|
-
slot: () => () => {},
|
|
376
|
-
inject: () => {
|
|
377
|
-
throw new Error("unexpected client provider injection");
|
|
378
|
-
},
|
|
379
|
-
provide: (_key, value) => {
|
|
380
|
-
provided = value as Ref<FrockBotWebData>;
|
|
381
|
-
return () => {};
|
|
382
|
-
},
|
|
383
|
-
});
|
|
384
|
-
if (!provided) throw new Error("shell data was not provided");
|
|
385
|
-
provided.value.activeBotId = "primary";
|
|
386
|
-
provided.value.botSettings = initializeBotSettingsV1("primary");
|
|
387
|
-
const assignment = {
|
|
388
|
-
assignmentId: "mail",
|
|
389
|
-
packageId: "mail",
|
|
390
|
-
capabilityId: "send",
|
|
391
|
-
connectionId: "mail-1",
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
await expect(provided.value.assignCapability(assignment)).rejects.toThrow(
|
|
395
|
-
"Connection is unavailable",
|
|
396
|
-
);
|
|
397
|
-
expect(provided.value.settingsError).toBe("Connection is unavailable");
|
|
398
|
-
await expect(provided.value.assignCapability(assignment)).resolves.toBe(
|
|
399
|
-
undefined,
|
|
400
|
-
);
|
|
401
|
-
expect(provided.value.settingsError).toBe(
|
|
402
|
-
"Assignment operation is retrying.",
|
|
403
|
-
);
|
|
404
|
-
});
|
|
405
|
-
});
|
|
406
|
-
|
|
407
341
|
describe("Bot selection", () => {
|
|
408
342
|
test("passes explicit Bot IDs and ignores stale hydration", async () => {
|
|
409
343
|
Object.defineProperty(globalThis, "window", {
|
|
@@ -461,6 +395,125 @@ describe("Bot selection", () => {
|
|
|
461
395
|
expect(provided.value.activeBotId).toBe("new");
|
|
462
396
|
expect(provided.value.botSettings?.botId).toBe("new");
|
|
463
397
|
});
|
|
398
|
+
|
|
399
|
+
test("loads sandboxed Package UI and transports only declared iframe tools", async () => {
|
|
400
|
+
Object.defineProperty(globalThis, "window", {
|
|
401
|
+
configurable: true,
|
|
402
|
+
value: {
|
|
403
|
+
location: { href: "https://app.example/" },
|
|
404
|
+
history: { replaceState: () => undefined },
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
const contribution = {
|
|
408
|
+
packageId: "weather-page",
|
|
409
|
+
displayName: "Sydney Weather",
|
|
410
|
+
provenance: "Bot-authored" as const,
|
|
411
|
+
artifact: {
|
|
412
|
+
contentHash: "a".repeat(64),
|
|
413
|
+
size: 1,
|
|
414
|
+
mediaType: "text/html" as const,
|
|
415
|
+
bundlerVersion: "frockbot-inline-html@1",
|
|
416
|
+
},
|
|
417
|
+
mounts: [{ slot: "frockbot.bot-settings-sections", order: 20 }],
|
|
418
|
+
declaredTools: ["weather_lookup"],
|
|
419
|
+
};
|
|
420
|
+
const requests: Array<{
|
|
421
|
+
path: string;
|
|
422
|
+
method?: string;
|
|
423
|
+
body?: string;
|
|
424
|
+
}> = [];
|
|
425
|
+
const slots: string[] = [];
|
|
426
|
+
let provided: Ref<FrockBotWebData> | undefined;
|
|
427
|
+
await shellClientPlugin({
|
|
428
|
+
transport: {
|
|
429
|
+
turn: () => Promise.resolve({ runId: "run", text: "", events: [] }),
|
|
430
|
+
readConfiguration: (query) => {
|
|
431
|
+
if (query.type !== "bot/get")
|
|
432
|
+
throw new Error("unexpected User query");
|
|
433
|
+
return Promise.resolve(initializeBotSettingsV1(query.botId));
|
|
434
|
+
},
|
|
435
|
+
hostedRequest: (path, method, body) => {
|
|
436
|
+
requests.push({ path, method, body });
|
|
437
|
+
if (path.endsWith("/package-ui")) {
|
|
438
|
+
return Promise.resolve({
|
|
439
|
+
schemaVersion: 1,
|
|
440
|
+
botId: "primary",
|
|
441
|
+
generationId: "generation-ui",
|
|
442
|
+
artifactOrigin: "https://ui.app.example",
|
|
443
|
+
contributions: [contribution],
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
if (path.endsWith("/package-ui/tools")) {
|
|
447
|
+
return Promise.resolve({
|
|
448
|
+
schemaVersion: 1,
|
|
449
|
+
runId: "run-iframe-tool",
|
|
450
|
+
text: "Sydney Weather",
|
|
451
|
+
events: [],
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
return Promise.resolve({});
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
slot: (registration) => {
|
|
458
|
+
slots.push(registration.slot);
|
|
459
|
+
return () => {};
|
|
460
|
+
},
|
|
461
|
+
inject: () => {
|
|
462
|
+
throw new Error("unexpected client provider injection");
|
|
463
|
+
},
|
|
464
|
+
provide: (_key, value) => {
|
|
465
|
+
provided = value as Ref<FrockBotWebData>;
|
|
466
|
+
return () => {};
|
|
467
|
+
},
|
|
468
|
+
});
|
|
469
|
+
if (!provided) throw new Error("shell data was not provided");
|
|
470
|
+
provided.value.userSettings = {
|
|
471
|
+
schemaVersion: 1,
|
|
472
|
+
revision: 1,
|
|
473
|
+
profile: { name: "User" },
|
|
474
|
+
packages: [],
|
|
475
|
+
connections: [],
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
await provided.value.selectBot("primary");
|
|
479
|
+
|
|
480
|
+
expect(slots).toContain("frockbot.bot-settings-sections");
|
|
481
|
+
expect(requests.filter(({ path }) => path.endsWith("/package-ui"))).toEqual(
|
|
482
|
+
[
|
|
483
|
+
{
|
|
484
|
+
path: "/api/bots/primary/package-ui",
|
|
485
|
+
method: undefined,
|
|
486
|
+
body: undefined,
|
|
487
|
+
},
|
|
488
|
+
],
|
|
489
|
+
);
|
|
490
|
+
expect(provided.value.packageUi?.contributions).toEqual([contribution]);
|
|
491
|
+
|
|
492
|
+
expect(
|
|
493
|
+
await provided.value.callPackageUiTool(contribution, "weather_lookup", {
|
|
494
|
+
city: "Sydney",
|
|
495
|
+
}),
|
|
496
|
+
).toEqual({ content: "Sydney Weather", isError: false });
|
|
497
|
+
const toolRequest = requests.find(({ path }) =>
|
|
498
|
+
path.endsWith("/package-ui/tools"),
|
|
499
|
+
);
|
|
500
|
+
expect(toolRequest?.method).toBe("POST");
|
|
501
|
+
expect(JSON.parse(toolRequest?.body ?? "null")).toEqual({
|
|
502
|
+
schemaVersion: 1,
|
|
503
|
+
commandId: expect.any(String),
|
|
504
|
+
generationId: "generation-ui",
|
|
505
|
+
packageId: "weather-page",
|
|
506
|
+
name: "weather_lookup",
|
|
507
|
+
input: { city: "Sydney" },
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
const requestCount = requests.length;
|
|
511
|
+
await expect(
|
|
512
|
+
provided.value.callPackageUiTool(contribution, "package_author", {}),
|
|
513
|
+
).rejects.toThrow("did not declare");
|
|
514
|
+
expect(requests).toHaveLength(requestCount);
|
|
515
|
+
});
|
|
516
|
+
|
|
464
517
|
test("preserves load failures and ignores stale User settings", async () => {
|
|
465
518
|
let provided: Ref<FrockBotWebData> | undefined;
|
|
466
519
|
const older = Promise.withResolvers<UserSettingsViewV1>();
|
|
@@ -576,197 +629,218 @@ describe("Bot selection", () => {
|
|
|
576
629
|
expect(provided.value.userSettings?.profile.name).toBe("Newer");
|
|
577
630
|
});
|
|
578
631
|
|
|
579
|
-
test(
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
{
|
|
589
|
-
assignmentId: "ollama-model",
|
|
590
|
-
packageId: "provider-ollama-cloud",
|
|
591
|
-
capabilityId: "ollama-cloud-models",
|
|
592
|
-
connectionId: "ollama-work",
|
|
593
|
-
state: "enabled" as const,
|
|
594
|
-
},
|
|
595
|
-
],
|
|
596
|
-
};
|
|
597
|
-
const user: UserSettingsViewV1 = {
|
|
598
|
-
schemaVersion: 1,
|
|
599
|
-
revision: 1,
|
|
600
|
-
profile: { name: "User" },
|
|
601
|
-
packages: [
|
|
602
|
-
{
|
|
603
|
-
packageId: "provider-ollama-cloud",
|
|
604
|
-
version: "0.0.1",
|
|
605
|
-
state: "installed",
|
|
606
|
-
},
|
|
607
|
-
],
|
|
608
|
-
connections: [
|
|
609
|
-
{
|
|
610
|
-
connectionId: "ollama-work",
|
|
611
|
-
packageId: "provider-ollama-cloud",
|
|
612
|
-
connectionTypeId: "ollama-cloud-account",
|
|
613
|
-
displayName: "Work",
|
|
614
|
-
state: "ready",
|
|
615
|
-
providerType: "ollama-cloud",
|
|
616
|
-
safeMetadata: {},
|
|
632
|
+
test.each([
|
|
633
|
+
{
|
|
634
|
+
source: "bot" as const,
|
|
635
|
+
botValues: {
|
|
636
|
+
"custom-models": {
|
|
637
|
+
model: {
|
|
638
|
+
connectionId: "model-connection",
|
|
639
|
+
providerModelId: "model-id",
|
|
640
|
+
},
|
|
617
641
|
},
|
|
618
|
-
],
|
|
619
|
-
};
|
|
620
|
-
await shellClientPlugin({
|
|
621
|
-
transport: {
|
|
622
|
-
turn: () => Promise.resolve({ runId: "run", text: "", events: [] }),
|
|
623
|
-
readConfiguration: (query) =>
|
|
624
|
-
Promise.resolve(query.type === "user/get" ? user : bot),
|
|
625
|
-
},
|
|
626
|
-
slot: () => () => {},
|
|
627
|
-
inject: () => {
|
|
628
|
-
throw new Error("unexpected client provider injection");
|
|
629
642
|
},
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
643
|
+
accountValues: undefined,
|
|
644
|
+
platformModel: undefined,
|
|
645
|
+
expectedLabel: "Model name · Model provider · Bot override",
|
|
646
|
+
expectedProjection: "bot",
|
|
647
|
+
ready: true,
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
source: "account" as const,
|
|
651
|
+
botValues: {},
|
|
652
|
+
accountValues: {
|
|
653
|
+
model: {
|
|
654
|
+
connectionId: "model-connection",
|
|
655
|
+
providerModelId: "model-id",
|
|
656
|
+
},
|
|
633
657
|
},
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
658
|
+
platformModel: undefined,
|
|
659
|
+
expectedLabel: "Model name · Model provider · Account model",
|
|
660
|
+
expectedProjection: "default",
|
|
661
|
+
ready: true,
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
source: "platform" as const,
|
|
665
|
+
botValues: {},
|
|
666
|
+
accountValues: undefined,
|
|
667
|
+
platformModel: {
|
|
668
|
+
connectionId: "model-connection",
|
|
669
|
+
providerModelId: "model-id",
|
|
670
|
+
},
|
|
671
|
+
expectedLabel: "Model name · Model provider",
|
|
672
|
+
expectedProjection: "default",
|
|
673
|
+
ready: true,
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
source: "none" as const,
|
|
677
|
+
botValues: {},
|
|
678
|
+
accountValues: undefined,
|
|
679
|
+
platformModel: undefined,
|
|
680
|
+
expectedLabel: "Model unavailable",
|
|
681
|
+
expectedProjection: "none",
|
|
682
|
+
ready: false,
|
|
683
|
+
},
|
|
684
|
+
])(
|
|
685
|
+
"labels the $source effective model",
|
|
686
|
+
async ({
|
|
687
|
+
botValues,
|
|
688
|
+
accountValues,
|
|
689
|
+
platformModel,
|
|
690
|
+
expectedLabel,
|
|
691
|
+
expectedProjection,
|
|
692
|
+
ready,
|
|
693
|
+
}) => {
|
|
694
|
+
let provided: Ref<FrockBotWebData> | undefined;
|
|
695
|
+
const bot = {
|
|
696
|
+
...initializeBotSettingsV1("model-bot"),
|
|
697
|
+
packageValues: structuredClone(botValues) as Record<
|
|
698
|
+
string,
|
|
699
|
+
Record<string, unknown>
|
|
700
|
+
>,
|
|
701
|
+
};
|
|
702
|
+
const user: UserSettingsViewV1 = {
|
|
703
|
+
schemaVersion: 1,
|
|
704
|
+
revision: 1,
|
|
705
|
+
profile: { name: "User" },
|
|
706
|
+
packages: [
|
|
643
707
|
{
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
708
|
+
packageId: "custom-models",
|
|
709
|
+
version: "0.0.1",
|
|
710
|
+
state: "installed",
|
|
711
|
+
...(accountValues ? { values: accountValues } : {}),
|
|
647
712
|
},
|
|
648
713
|
{
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
714
|
+
packageId: "model-provider",
|
|
715
|
+
version: "0.0.1",
|
|
716
|
+
state: "installed",
|
|
652
717
|
},
|
|
653
718
|
],
|
|
654
|
-
|
|
719
|
+
connections: [
|
|
655
720
|
{
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
721
|
+
connectionId: "model-connection",
|
|
722
|
+
packageId: "model-provider",
|
|
723
|
+
connectionTypeId: "model-account",
|
|
724
|
+
displayName: "Work",
|
|
725
|
+
state: "ready",
|
|
726
|
+
providerType: "model-provider",
|
|
727
|
+
safeMetadata: {},
|
|
728
|
+
modelCatalog: {
|
|
729
|
+
schemaVersion: 1,
|
|
730
|
+
generation: "catalog-1",
|
|
731
|
+
state: "fresh",
|
|
732
|
+
models: [
|
|
733
|
+
{
|
|
734
|
+
providerModelId: "model-id",
|
|
735
|
+
displayName: "Model name",
|
|
736
|
+
capabilities: {
|
|
737
|
+
tools: true,
|
|
738
|
+
vision: false,
|
|
739
|
+
reasoning: false,
|
|
740
|
+
},
|
|
741
|
+
source: "discovered",
|
|
742
|
+
},
|
|
743
|
+
],
|
|
744
|
+
},
|
|
661
745
|
},
|
|
662
746
|
],
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
// label never says so: the Bot simply runs on that model.
|
|
684
|
-
bot.model = undefined as unknown as typeof bot.model;
|
|
685
|
-
user.newBotModelTemplate = {
|
|
686
|
-
connectionId: "ollama-work",
|
|
687
|
-
providerModelId: "llama-3:cloud",
|
|
688
|
-
};
|
|
689
|
-
user.newBotModelTemplateSource = "auto";
|
|
690
|
-
await provided.value.loadUserSettings();
|
|
691
|
-
await provided.value.loadBotSettings();
|
|
692
|
-
expect(provided.value.modelSource).toBe("default");
|
|
693
|
-
expect(provided.value.modelLabel).toBe("llama-3:cloud · Ollama Cloud");
|
|
694
|
-
expect(provided.value.modelReady).toBe(true);
|
|
695
|
-
|
|
696
|
-
user.newBotModelTemplate = undefined;
|
|
697
|
-
user.newBotModelTemplateSource = undefined;
|
|
698
|
-
await provided.value.loadUserSettings();
|
|
699
|
-
expect(provided.value.modelSource).toBe("none");
|
|
700
|
-
expect(provided.value.modelLabel).toBe("No default model");
|
|
701
|
-
expect(provided.value.modelReady).toBe(false);
|
|
702
|
-
});
|
|
703
|
-
|
|
704
|
-
test("assigns a newly connected model capability before model selection", async () => {
|
|
705
|
-
let provided: Ref<FrockBotWebData> | undefined;
|
|
706
|
-
let bot = initializeBotSettingsV1("ollama-bot");
|
|
707
|
-
const commands: Array<{ type: string; expectedRevision: number }> = [];
|
|
708
|
-
const user: UserSettingsViewV1 = {
|
|
709
|
-
schemaVersion: 1,
|
|
710
|
-
revision: 1,
|
|
711
|
-
profile: { name: "User" },
|
|
712
|
-
packages: [
|
|
747
|
+
...(platformModel ? { platformModel } : {}),
|
|
748
|
+
};
|
|
749
|
+
await shellClientPlugin({
|
|
750
|
+
transport: {
|
|
751
|
+
turn: () => Promise.resolve({ runId: "run", text: "", events: [] }),
|
|
752
|
+
readConfiguration: (query) =>
|
|
753
|
+
Promise.resolve(query.type === "user/get" ? user : bot),
|
|
754
|
+
},
|
|
755
|
+
slot: () => () => {},
|
|
756
|
+
inject: () => {
|
|
757
|
+
throw new Error("unexpected client provider injection");
|
|
758
|
+
},
|
|
759
|
+
provide: (_key, value) => {
|
|
760
|
+
provided = value as Ref<FrockBotWebData>;
|
|
761
|
+
return () => {};
|
|
762
|
+
},
|
|
763
|
+
});
|
|
764
|
+
if (!provided) throw new Error("shell data was not provided");
|
|
765
|
+
provided.value.activeBotId = bot.botId;
|
|
766
|
+
provided.value.pluginCatalog = [
|
|
713
767
|
{
|
|
714
|
-
packageId: "
|
|
768
|
+
packageId: "custom-models",
|
|
769
|
+
displayName: "Custom models",
|
|
715
770
|
version: "0.0.1",
|
|
716
|
-
|
|
771
|
+
capabilities: [],
|
|
772
|
+
connectionTypes: [],
|
|
773
|
+
settings: [
|
|
774
|
+
{
|
|
775
|
+
id: "model",
|
|
776
|
+
schemaVersion: 1,
|
|
777
|
+
scopes: ["user", "bot"],
|
|
778
|
+
role: "model",
|
|
779
|
+
schema: {
|
|
780
|
+
type: "object",
|
|
781
|
+
properties: {
|
|
782
|
+
connectionId: { type: "string" },
|
|
783
|
+
providerModelId: { type: "string" },
|
|
784
|
+
},
|
|
785
|
+
required: ["connectionId", "providerModelId"],
|
|
786
|
+
additionalProperties: false,
|
|
787
|
+
},
|
|
788
|
+
},
|
|
789
|
+
],
|
|
717
790
|
},
|
|
718
|
-
],
|
|
719
|
-
connections: [
|
|
720
791
|
{
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
792
|
+
packageId: "model-provider",
|
|
793
|
+
displayName: "Model provider",
|
|
794
|
+
version: "0.0.1",
|
|
795
|
+
capabilities: [
|
|
796
|
+
{
|
|
797
|
+
id: "models",
|
|
798
|
+
kind: "model",
|
|
799
|
+
connectionTypes: ["model-account"],
|
|
800
|
+
},
|
|
801
|
+
],
|
|
802
|
+
connectionTypes: [
|
|
803
|
+
{
|
|
804
|
+
id: "model-account",
|
|
805
|
+
displayName: "Model account",
|
|
806
|
+
allowMultiple: false,
|
|
807
|
+
authorizationKind: "api-key",
|
|
808
|
+
capabilities: ["models"],
|
|
809
|
+
},
|
|
810
|
+
],
|
|
811
|
+
settings: [],
|
|
728
812
|
},
|
|
729
|
-
]
|
|
813
|
+
];
|
|
814
|
+
|
|
815
|
+
await provided.value.loadBotSettings();
|
|
816
|
+
|
|
817
|
+
expect(provided.value.modelLabel).toBe(expectedLabel);
|
|
818
|
+
expect(provided.value.modelReady).toBe(ready);
|
|
819
|
+
expect(provided.value.modelSource).toBe(expectedProjection);
|
|
820
|
+
},
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
test("saves Bot-scoped Package settings through the generic command", async () => {
|
|
824
|
+
type PackageSettingsWebData = FrockBotWebData & {
|
|
825
|
+
saveBotPackageSettings(
|
|
826
|
+
packageId: string,
|
|
827
|
+
values: Record<string, unknown>,
|
|
828
|
+
): Promise<void>;
|
|
730
829
|
};
|
|
830
|
+
let provided: Ref<PackageSettingsWebData> | undefined;
|
|
831
|
+
const commands: unknown[] = [];
|
|
832
|
+
const bot = initializeBotSettingsV1("package-bot");
|
|
731
833
|
await shellClientPlugin({
|
|
732
834
|
transport: {
|
|
733
835
|
turn: () => Promise.resolve({ runId: "run", text: "", events: [] }),
|
|
734
|
-
readConfiguration: (
|
|
735
|
-
Promise.resolve(query.type === "user/get" ? user : bot),
|
|
836
|
+
readConfiguration: () => Promise.resolve(bot),
|
|
736
837
|
executeConfiguration: (command) => {
|
|
737
|
-
|
|
738
|
-
commands.push({
|
|
739
|
-
type: command.type,
|
|
740
|
-
expectedRevision: command.expectedRevision,
|
|
741
|
-
});
|
|
742
|
-
if (command.type === "bot/assign-capability") {
|
|
743
|
-
bot = {
|
|
744
|
-
...bot,
|
|
745
|
-
revision: 1,
|
|
746
|
-
model: command.model,
|
|
747
|
-
assignments: [{ ...command.assignment, state: "enabled" }],
|
|
748
|
-
};
|
|
749
|
-
} else if (command.type === "bot/select-model") {
|
|
750
|
-
bot = { ...bot, revision: bot.revision + 1, model: command.model };
|
|
751
|
-
} else if (command.type === "bot/unbind-model") {
|
|
752
|
-
bot = {
|
|
753
|
-
...bot,
|
|
754
|
-
revision: bot.revision + 1,
|
|
755
|
-
model: undefined,
|
|
756
|
-
assignments: bot.assignments.map((assignment) =>
|
|
757
|
-
assignment.assignmentId === command.assignmentId
|
|
758
|
-
? { ...assignment, state: "unavailable" }
|
|
759
|
-
: assignment,
|
|
760
|
-
),
|
|
761
|
-
};
|
|
762
|
-
} else {
|
|
763
|
-
throw new Error("unexpected Bot command");
|
|
764
|
-
}
|
|
838
|
+
commands.push(command);
|
|
765
839
|
return Promise.resolve({
|
|
766
840
|
schemaVersion: 1,
|
|
767
841
|
commandId: command.commandId,
|
|
768
|
-
revision:
|
|
769
|
-
status: "applied"
|
|
842
|
+
revision: command.expectedRevision + 1,
|
|
843
|
+
status: "applied",
|
|
770
844
|
});
|
|
771
845
|
},
|
|
772
846
|
},
|
|
@@ -775,102 +849,52 @@ describe("Bot selection", () => {
|
|
|
775
849
|
throw new Error("unexpected client provider injection");
|
|
776
850
|
},
|
|
777
851
|
provide: (_key, value) => {
|
|
778
|
-
provided = value as Ref<
|
|
852
|
+
provided = value as Ref<PackageSettingsWebData>;
|
|
779
853
|
return () => {};
|
|
780
854
|
},
|
|
781
855
|
});
|
|
782
856
|
if (!provided) throw new Error("shell data was not provided");
|
|
783
857
|
provided.value.activeBotId = bot.botId;
|
|
784
858
|
provided.value.botSettings = bot;
|
|
785
|
-
provided.value.userSettings = user;
|
|
786
|
-
provided.value.pluginCatalog = [
|
|
787
|
-
{
|
|
788
|
-
packageId: "provider-ollama-cloud",
|
|
789
|
-
displayName: "Ollama Cloud",
|
|
790
|
-
version: "0.0.1",
|
|
791
|
-
capabilities: [
|
|
792
|
-
{
|
|
793
|
-
id: "ollama-cloud-models",
|
|
794
|
-
kind: "model",
|
|
795
|
-
connectionTypes: ["ollama-cloud-account"],
|
|
796
|
-
},
|
|
797
|
-
],
|
|
798
|
-
connectionTypes: [
|
|
799
|
-
{
|
|
800
|
-
id: "ollama-cloud-account",
|
|
801
|
-
displayName: "Ollama Cloud account",
|
|
802
|
-
allowMultiple: true,
|
|
803
|
-
authorizationKind: "api-key",
|
|
804
|
-
capabilities: ["ollama-cloud-models"],
|
|
805
|
-
},
|
|
806
|
-
],
|
|
807
|
-
},
|
|
808
|
-
];
|
|
809
859
|
|
|
810
|
-
await provided.value.
|
|
811
|
-
connectionId: "ollama-work",
|
|
812
|
-
providerModelId: "glm-5.3-flash:cloud",
|
|
813
|
-
});
|
|
814
|
-
|
|
815
|
-
expect(commands).toEqual([
|
|
816
|
-
{ type: "bot/assign-capability", expectedRevision: 0 },
|
|
817
|
-
]);
|
|
818
|
-
expect(bot).toMatchObject({
|
|
819
|
-
revision: 1,
|
|
860
|
+
await provided.value.saveBotPackageSettings("custom-models", {
|
|
820
861
|
model: {
|
|
821
|
-
connectionId: "
|
|
822
|
-
providerModelId: "
|
|
862
|
+
connectionId: "model-connection",
|
|
863
|
+
providerModelId: "model-id",
|
|
823
864
|
},
|
|
824
|
-
assignments: [
|
|
825
|
-
{
|
|
826
|
-
packageId: "provider-ollama-cloud",
|
|
827
|
-
capabilityId: "ollama-cloud-models",
|
|
828
|
-
connectionId: "ollama-work",
|
|
829
|
-
state: "enabled",
|
|
830
|
-
},
|
|
831
|
-
],
|
|
832
865
|
});
|
|
833
866
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
});
|
|
849
|
-
expect(bot).toMatchObject({
|
|
850
|
-
revision: 2,
|
|
851
|
-
model: undefined,
|
|
852
|
-
assignments: [{ state: "unavailable" }],
|
|
853
|
-
});
|
|
867
|
+
expect(commands).toMatchObject([
|
|
868
|
+
{
|
|
869
|
+
type: "bot/set-package-settings",
|
|
870
|
+
botId: "package-bot",
|
|
871
|
+
packageId: "custom-models",
|
|
872
|
+
expectedRevision: 0,
|
|
873
|
+
values: {
|
|
874
|
+
model: {
|
|
875
|
+
connectionId: "model-connection",
|
|
876
|
+
providerModelId: "model-id",
|
|
877
|
+
},
|
|
878
|
+
},
|
|
879
|
+
},
|
|
880
|
+
]);
|
|
854
881
|
});
|
|
855
882
|
|
|
856
|
-
test("
|
|
883
|
+
test("shows the backend reason when Package enablement is refused", async () => {
|
|
857
884
|
let provided: Ref<FrockBotWebData> | undefined;
|
|
858
|
-
const
|
|
859
|
-
|
|
860
|
-
model: {
|
|
861
|
-
connectionId: "revoked-connection",
|
|
862
|
-
providerModelId: "glm-5.3-flash:cloud",
|
|
863
|
-
},
|
|
864
|
-
assignments: [],
|
|
865
|
-
};
|
|
866
|
-
let executed = false;
|
|
885
|
+
const failure =
|
|
886
|
+
'Enable dependency "custom-models" before enabling "provider-ollama-cloud"';
|
|
867
887
|
await shellClientPlugin({
|
|
868
888
|
transport: {
|
|
869
889
|
turn: () => Promise.resolve({ runId: "run", text: "", events: [] }),
|
|
870
|
-
executeConfiguration: () =>
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
890
|
+
executeConfiguration: (command) =>
|
|
891
|
+
Promise.resolve({
|
|
892
|
+
schemaVersion: 1,
|
|
893
|
+
commandId: command.commandId,
|
|
894
|
+
revision: command.expectedRevision,
|
|
895
|
+
status: "rejected",
|
|
896
|
+
failure,
|
|
897
|
+
}),
|
|
874
898
|
},
|
|
875
899
|
slot: () => () => {},
|
|
876
900
|
inject: () => {
|
|
@@ -882,19 +906,24 @@ describe("Bot selection", () => {
|
|
|
882
906
|
},
|
|
883
907
|
});
|
|
884
908
|
if (!provided) throw new Error("shell data was not provided");
|
|
885
|
-
provided.value.activeBotId = bot.botId;
|
|
886
|
-
provided.value.botSettings = bot;
|
|
887
909
|
provided.value.userSettings = {
|
|
888
910
|
schemaVersion: 1,
|
|
889
|
-
revision:
|
|
911
|
+
revision: 2,
|
|
890
912
|
profile: { name: "User" },
|
|
891
|
-
packages: [
|
|
913
|
+
packages: [
|
|
914
|
+
{
|
|
915
|
+
packageId: "provider-ollama-cloud",
|
|
916
|
+
version: "0.0.1",
|
|
917
|
+
state: "disabled",
|
|
918
|
+
},
|
|
919
|
+
],
|
|
892
920
|
connections: [],
|
|
893
921
|
};
|
|
894
922
|
|
|
895
|
-
await
|
|
896
|
-
|
|
897
|
-
|
|
923
|
+
await expect(
|
|
924
|
+
provided.value.setPackageEnabled("provider-ollama-cloud", true),
|
|
925
|
+
).rejects.toThrow(failure);
|
|
926
|
+
expect(provided.value.settingsError).toBe(failure);
|
|
898
927
|
});
|
|
899
928
|
});
|
|
900
929
|
|
|
@@ -984,7 +1013,7 @@ describe("detached Turn projection", () => {
|
|
|
984
1013
|
input: "Keep working",
|
|
985
1014
|
events: [],
|
|
986
1015
|
status: "completed",
|
|
987
|
-
responseText: "Finished
|
|
1016
|
+
responseText: "Finished successfully.",
|
|
988
1017
|
},
|
|
989
1018
|
],
|
|
990
1019
|
);
|
|
@@ -992,7 +1021,7 @@ describe("detached Turn projection", () => {
|
|
|
992
1021
|
expect(messages).toHaveLength(2);
|
|
993
1022
|
expect(messages[1]).toMatchObject({
|
|
994
1023
|
role: "assistant",
|
|
995
|
-
text: "Finished
|
|
1024
|
+
text: "Finished successfully.",
|
|
996
1025
|
status: "completed",
|
|
997
1026
|
});
|
|
998
1027
|
});
|
|
@@ -1423,8 +1452,18 @@ describe("active durable Turn projection", () => {
|
|
|
1423
1452
|
await shellClientPlugin({
|
|
1424
1453
|
transport: {
|
|
1425
1454
|
turn: () => Promise.resolve({ runId: "run", text: "", events: [] }),
|
|
1426
|
-
readConfiguration: () =>
|
|
1427
|
-
Promise.resolve(
|
|
1455
|
+
readConfiguration: (query) =>
|
|
1456
|
+
Promise.resolve(
|
|
1457
|
+
query.type === "bot/get"
|
|
1458
|
+
? initializeBotSettingsV1("default")
|
|
1459
|
+
: {
|
|
1460
|
+
schemaVersion: 1 as const,
|
|
1461
|
+
revision: 0,
|
|
1462
|
+
profile: { name: "User" },
|
|
1463
|
+
packages: [],
|
|
1464
|
+
connections: [],
|
|
1465
|
+
},
|
|
1466
|
+
),
|
|
1428
1467
|
listRuns: () =>
|
|
1429
1468
|
Promise.resolve([
|
|
1430
1469
|
{
|
|
@@ -1583,8 +1622,18 @@ describe("hosted Stop", () => {
|
|
|
1583
1622
|
await shellClientPlugin({
|
|
1584
1623
|
transport: {
|
|
1585
1624
|
turn: () => Promise.resolve({ runId: "run", text: "", events: [] }),
|
|
1586
|
-
readConfiguration: () =>
|
|
1587
|
-
Promise.resolve(
|
|
1625
|
+
readConfiguration: (query) =>
|
|
1626
|
+
Promise.resolve(
|
|
1627
|
+
query.type === "bot/get"
|
|
1628
|
+
? initializeBotSettingsV1("default")
|
|
1629
|
+
: {
|
|
1630
|
+
schemaVersion: 1 as const,
|
|
1631
|
+
revision: 0,
|
|
1632
|
+
profile: { name: "User" },
|
|
1633
|
+
packages: [],
|
|
1634
|
+
connections: [],
|
|
1635
|
+
},
|
|
1636
|
+
),
|
|
1588
1637
|
listRuns: () =>
|
|
1589
1638
|
Promise.resolve([
|
|
1590
1639
|
{
|
|
@@ -1619,7 +1668,7 @@ describe("hosted Stop", () => {
|
|
|
1619
1668
|
expect(provided.value.activeRun).toMatchObject({
|
|
1620
1669
|
runId: "run-1",
|
|
1621
1670
|
status: "running",
|
|
1622
|
-
message: "Stop
|
|
1671
|
+
message: "Stop requested; finishing up.",
|
|
1623
1672
|
canResume: false,
|
|
1624
1673
|
});
|
|
1625
1674
|
|
|
@@ -1715,8 +1764,18 @@ describe("hosted Stop", () => {
|
|
|
1715
1764
|
await shellClientPlugin({
|
|
1716
1765
|
transport: {
|
|
1717
1766
|
turn: () => Promise.resolve({ runId: "run", text: "", events: [] }),
|
|
1718
|
-
readConfiguration: () =>
|
|
1719
|
-
Promise.resolve(
|
|
1767
|
+
readConfiguration: (query) =>
|
|
1768
|
+
Promise.resolve(
|
|
1769
|
+
query.type === "bot/get"
|
|
1770
|
+
? initializeBotSettingsV1("other")
|
|
1771
|
+
: {
|
|
1772
|
+
schemaVersion: 1 as const,
|
|
1773
|
+
revision: 0,
|
|
1774
|
+
profile: { name: "User" },
|
|
1775
|
+
packages: [],
|
|
1776
|
+
connections: [],
|
|
1777
|
+
},
|
|
1778
|
+
),
|
|
1720
1779
|
listRuns: () => Promise.resolve([]),
|
|
1721
1780
|
listNotifications: () => Promise.resolve([]),
|
|
1722
1781
|
stopRun: () => {
|
|
@@ -1788,7 +1847,7 @@ describe("uncertain Turn admission", () => {
|
|
|
1788
1847
|
input: "continue",
|
|
1789
1848
|
status: lookups === 2 ? "running" : "completed",
|
|
1790
1849
|
events: [],
|
|
1791
|
-
...(lookups === 2 ? {} : { responseText: "Done
|
|
1850
|
+
...(lookups === 2 ? {} : { responseText: "Done successfully" }),
|
|
1792
1851
|
});
|
|
1793
1852
|
},
|
|
1794
1853
|
fenceRunAdmission: () =>
|
|
@@ -1822,7 +1881,7 @@ describe("uncertain Turn admission", () => {
|
|
|
1822
1881
|
expect(provided.value.settingsError).toBeUndefined();
|
|
1823
1882
|
expect(outstandingAbortListeners).toBe(0);
|
|
1824
1883
|
expect(provided.value.messages.at(-1)).toMatchObject({
|
|
1825
|
-
text: "Done
|
|
1884
|
+
text: "Done successfully",
|
|
1826
1885
|
status: "completed",
|
|
1827
1886
|
});
|
|
1828
1887
|
});
|