@frockbot/plugin-shell 0.1.3 → 0.1.4

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.
@@ -5,6 +5,7 @@ import type {
5
5
  BotSettingsViewV1,
6
6
  UserSettingsViewV1,
7
7
  } from "@frockbot/configuration-core";
8
+ import type { PackageSettingDefinition } from "@frockbot/kernel-composition";
8
9
  import { createShellBotBackendContribution } from "./backend.js";
9
10
 
10
11
  class MemoryStorage {
@@ -51,72 +52,62 @@ class MemoryStorage {
51
52
  }
52
53
  }
53
54
 
54
- function installedUser(): UserSettingsViewV1 {
55
- return {
56
- schemaVersion: 1,
57
- revision: 1,
58
- profile: { name: "User" },
59
- packages: [{ packageId: "composio", version: "0.0.1", state: "installed" }],
60
- connections: [
61
- {
62
- connectionId: "gmail-1",
63
- packageId: "composio",
64
- connectionTypeId: "gmail",
65
- displayName: "Gmail",
66
- state: "ready",
67
- providerType: "fixture-models",
68
- safeMetadata: {},
69
- },
70
- ],
71
- };
72
- }
55
+ const MODEL_SETTING = {
56
+ id: "model",
57
+ schemaVersion: 1,
58
+ scopes: ["user", "bot"],
59
+ role: "model",
60
+ schema: {
61
+ type: "object",
62
+ properties: {
63
+ connectionId: { type: "string" },
64
+ providerModelId: { type: "string" },
65
+ },
66
+ required: ["connectionId", "providerModelId"],
67
+ additionalProperties: false,
68
+ },
69
+ } as const satisfies PackageSettingDefinition;
70
+
71
+ const TONE_SETTING = {
72
+ id: "tone",
73
+ schemaVersion: 1,
74
+ scopes: ["bot"],
75
+ schema: { type: "string", maxLength: 40 },
76
+ } as const satisfies PackageSettingDefinition;
73
77
 
74
- async function compileAssignmentTestApplication(): ReturnType<
78
+ async function compileModelTestApplication(): ReturnType<
75
79
  typeof compileFoundationApplication
76
80
  > {
77
81
  const application = await compileFoundationApplication();
82
+ const provider = application.packages.find(
83
+ (pkg) => pkg.id === "provider-flock-ai",
84
+ );
78
85
  const template = application.packages.find((pkg) => pkg.id === "settings");
79
- if (!template) throw new Error("Settings fixture Package is unavailable");
86
+ if (!provider || !template) throw new Error("Fixture Packages unavailable");
80
87
  return {
81
88
  ...application,
82
89
  packages: [
83
- ...application.packages,
90
+ ...application.packages.filter(
91
+ (pkg) => pkg.id !== provider.id && pkg.id !== "custom-models",
92
+ ),
93
+ provider,
84
94
  {
85
95
  ...template,
86
- id: "composio",
87
- specifier: "@test/composio",
96
+ id: "custom-models",
97
+ specifier: "@test/custom-models",
88
98
  version: "0.0.1",
89
99
  manifest: {
90
100
  ...template.manifest,
91
- id: "composio",
92
- displayName: "Connection fixture",
101
+ id: "custom-models",
102
+ displayName: "Custom models",
93
103
  version: "0.0.1",
94
104
  dependencies: {},
95
105
  contributions: {},
96
106
  permissions: [],
97
107
  configuration: {
98
- settings: [],
99
- connectionTypes: [
100
- {
101
- id: "gmail",
102
- displayName: "Gmail",
103
- allowMultiple: true,
104
- authorization: { kind: "grant", driverId: "fixture" },
105
- capabilities: ["gmail-tools", "gmail-send"],
106
- },
107
- ],
108
- capabilities: [
109
- {
110
- id: "gmail-tools",
111
- kind: "model",
112
- connectionTypes: ["gmail"],
113
- },
114
- {
115
- id: "gmail-send",
116
- kind: "tool",
117
- connectionTypes: ["gmail"],
118
- },
119
- ],
108
+ settings: [MODEL_SETTING, TONE_SETTING],
109
+ connectionTypes: [],
110
+ capabilities: [],
120
111
  },
121
112
  },
122
113
  },
@@ -124,34 +115,113 @@ async function compileAssignmentTestApplication(): ReturnType<
124
115
  };
125
116
  }
126
117
 
127
- function assignmentCommand(
128
- commandId: string,
129
- assignment: {
130
- packageId: string;
131
- capabilityId: string;
132
- connectionId?: string;
133
- },
134
- ): BotConfigurationCommandV1 {
118
+ function model(connectionId: string, providerModelId: string) {
119
+ return { connectionId, providerModelId };
120
+ }
121
+
122
+ function configuredUser(): UserSettingsViewV1 {
135
123
  return {
136
124
  schemaVersion: 1,
137
- type: "bot/assign-capability",
138
- commandId,
139
- botId: "primary",
140
- expectedRevision: 0,
141
- assignment: {
142
- assignmentId: commandId,
143
- ...assignment,
144
- },
125
+ revision: 1,
126
+ profile: { name: "User" },
127
+ packages: [
128
+ {
129
+ packageId: "provider-flock-ai",
130
+ version: "0.0.1",
131
+ state: "installed",
132
+ },
133
+ {
134
+ packageId: "custom-models",
135
+ version: "0.0.1",
136
+ state: "disabled",
137
+ },
138
+ ],
139
+ connections: [
140
+ {
141
+ connectionId: "flock-ai-ambient",
142
+ packageId: "provider-flock-ai",
143
+ connectionTypeId: "flock-ai-account",
144
+ displayName: "Flock AI",
145
+ state: "ready",
146
+ providerType: "flock-ai",
147
+ generation: "foundation-generation-1",
148
+ modelCatalog: {
149
+ schemaVersion: 1,
150
+ generation: "catalog-1",
151
+ state: "fresh",
152
+ models: [
153
+ {
154
+ providerModelId: "@flock/auto",
155
+ displayName: "Platform",
156
+ capabilities: {
157
+ tools: true,
158
+ vision: false,
159
+ reasoning: false,
160
+ },
161
+ source: "discovered",
162
+ },
163
+ ],
164
+ },
165
+ safeMetadata: {},
166
+ },
167
+ ],
168
+ platformModel: model("flock-ai-ambient", "@flock/auto"),
169
+ };
170
+ }
171
+
172
+ function host(storage: MemoryStorage, readUser: () => UserSettingsViewV1) {
173
+ return createShellBotBackendContribution({
174
+ state: { storage } as unknown as DurableObjectState,
175
+ env: {
176
+ CREDENTIAL_KEYRING:
177
+ '{"schemaVersion":1,"currentKeyId":"primary","keys":{"primary":"MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY"}}',
178
+ USER_CONFIGURATIONS: {
179
+ idFromName: () => "user-1",
180
+ get: () => ({
181
+ readConfiguration: () => Promise.resolve(structuredClone(readUser())),
182
+ listBots: () =>
183
+ Promise.resolve({ schemaVersion: 1, revision: 0, bots: [] }),
184
+ }),
185
+ },
186
+ MEMORY_FILES: {},
187
+ MEMORY_INDEX: {},
188
+ FLOCK_AI: {
189
+ autoRoute: "flock-auto",
190
+ runChatCompletion: () =>
191
+ Promise.resolve(
192
+ new ReadableStream({
193
+ start(controller) {
194
+ controller.enqueue(
195
+ new TextEncoder().encode(
196
+ 'data: {"choices":[{"delta":{"content":"Cordis runtime: hello"}}]}\n\n' +
197
+ 'data: {"choices":[{"delta":{},"finish_reason":"stop"}]}\n\n' +
198
+ "data: [DONE]\n\n",
199
+ ),
200
+ );
201
+ controller.close();
202
+ },
203
+ }),
204
+ ),
205
+ },
206
+ } as never,
207
+ compileApplication: compileModelTestApplication,
208
+ });
209
+ }
210
+
211
+ function request(command: BotConfigurationCommandV1) {
212
+ return {
213
+ schemaVersion: 1 as const,
214
+ userId: "user-1",
215
+ botId: command.botId,
216
+ command,
145
217
  };
146
218
  }
147
219
 
148
- describe("Bot capability assignment admission", () => {
220
+ describe("Bot configuration admission", () => {
149
221
  test("rejects an unmaterialized Bot without writing durable state", async () => {
150
222
  const storage = new MemoryStorage();
151
- const contribution = createShellBotBackendContribution({
152
- state: { storage } as unknown as DurableObjectState,
153
- env: {} as never,
154
- });
223
+ const contribution = host(storage, configuredUser);
224
+
155
225
  await expect(
156
226
  contribution.readConfiguration({
157
227
  schemaVersion: 1,
@@ -177,20 +247,18 @@ describe("Bot capability assignment admission", () => {
177
247
  const identity = { userId: "user-1", botId: "primary" };
178
248
  await contribution.materializeSettings(identity, { name: "Primary" });
179
249
  archived = true;
250
+
180
251
  await expect(
181
- contribution.executeConfiguration({
182
- schemaVersion: 1,
183
- userId: identity.userId,
184
- botId: identity.botId,
185
- command: {
252
+ contribution.executeConfiguration(
253
+ request({
186
254
  schemaVersion: 1,
187
255
  type: "bot/update-profile",
188
256
  commandId: "archived-profile",
189
257
  botId: identity.botId,
190
258
  expectedRevision: 0,
191
259
  profile: { name: "Changed" },
192
- },
193
- }),
260
+ }),
261
+ ),
194
262
  ).rejects.toThrow("archived");
195
263
  expect(await contribution.getSettings(identity)).toMatchObject({
196
264
  revision: 0,
@@ -198,7 +266,7 @@ describe("Bot capability assignment admission", () => {
198
266
  });
199
267
  });
200
268
 
201
- test("rechecks lifecycle admission in the configuration mutation transaction", async () => {
269
+ test("rechecks lifecycle admission in the mutation transaction", async () => {
202
270
  const storage = new MemoryStorage();
203
271
  let admissions = 0;
204
272
  const contribution = createShellBotBackendContribution({
@@ -211,24 +279,26 @@ describe("Bot capability assignment admission", () => {
211
279
  : Promise.reject(new Error(`Bot "${botId}" is archived`));
212
280
  },
213
281
  });
214
- const identity = { userId: "user-1", botId: "primary" };
215
- await contribution.materializeSettings(identity, { name: "Primary" });
282
+ await contribution.materializeSettings(
283
+ { userId: "user-1", botId: "primary" },
284
+ { name: "Primary" },
285
+ );
286
+
216
287
  await expect(
217
- contribution.executeConfiguration({
218
- schemaVersion: 1,
219
- userId: identity.userId,
220
- botId: identity.botId,
221
- command: {
288
+ contribution.executeConfiguration(
289
+ request({
222
290
  schemaVersion: 1,
223
291
  type: "bot/update-profile",
224
292
  commandId: "racing-profile",
225
- botId: identity.botId,
293
+ botId: "primary",
226
294
  expectedRevision: 0,
227
295
  profile: { name: "Changed" },
228
- },
229
- }),
296
+ }),
297
+ ),
230
298
  ).rejects.toThrow("archived");
231
- expect(await contribution.getSettings(identity)).toMatchObject({
299
+ expect(
300
+ await contribution.getSettings({ userId: "user-1", botId: "primary" }),
301
+ ).toMatchObject({
232
302
  revision: 0,
233
303
  profile: { name: "Primary" },
234
304
  });
@@ -242,8 +312,7 @@ describe("Bot capability assignment admission", () => {
242
312
  revision: 7,
243
313
  profile: { name: "Primary" },
244
314
  notifications: { enabled: true },
245
- assignments: [],
246
- assignmentOperations: [],
315
+ packageValues: {},
247
316
  } satisfies BotSettingsViewV1;
248
317
  await storage.put({
249
318
  identity: { userId: "user-1", botId: "primary" },
@@ -278,58 +347,23 @@ describe("Bot capability assignment admission", () => {
278
347
  });
279
348
  const contribution = createShellBotBackendContribution({
280
349
  state: { storage } as unknown as DurableObjectState,
281
- env: {
282
- USER_CONFIGURATIONS: {
283
- idFromName: () => "user-1",
284
- get: () => ({
285
- readConfiguration: () =>
286
- Promise.resolve({
287
- ...installedUser(),
288
- newBotModelTemplate: {
289
- connectionId: "provider-1",
290
- providerModelId: "model-1",
291
- },
292
- newBotModelTemplateSource: "auto",
293
- }),
294
- }),
295
- },
296
- } as never,
350
+ env: {} as never,
297
351
  });
352
+ const identity = { userId: "user-1", botId: "primary" };
298
353
 
299
- await contribution.materializeSettings(
300
- { userId: "user-1", botId: "primary" },
301
- {
302
- name: "Primary",
303
- model: { connectionId: "provider-1", providerModelId: "model-1" },
304
- },
305
- );
306
- await expect(
307
- contribution.readConfiguration({
308
- schemaVersion: 1,
309
- userId: "user-1",
310
- botId: "primary",
311
- }),
312
- ).resolves.toMatchObject({
313
- model: {
314
- connectionId: "provider-1",
315
- providerModelId: "model-1",
316
- },
354
+ await contribution.materializeSettings(identity, { name: "Primary" });
355
+ await expect(contribution.getSettings(identity)).resolves.toMatchObject({
356
+ revision: 0,
357
+ profile: { name: "Primary" },
358
+ packageValues: {},
317
359
  });
318
360
  });
319
361
 
320
362
  test("binds in-flight and durable receipts to the complete Bot command", async () => {
321
363
  const storage = new MemoryStorage();
322
- const userConfiguration = {
323
- readConfiguration: () => Promise.resolve(installedUser()),
324
- };
325
- const host = {
364
+ const backendHost = {
326
365
  state: { storage } as unknown as DurableObjectState,
327
- env: {
328
- USER_CONFIGURATIONS: {
329
- idFromName: () => "user-1",
330
- get: () => userConfiguration,
331
- },
332
- } as never,
366
+ env: {} as never,
333
367
  };
334
368
  const original: BotConfigurationCommandV1 = {
335
369
  schemaVersion: 1,
@@ -343,13 +377,7 @@ describe("Bot capability assignment admission", () => {
343
377
  ...original,
344
378
  profile: { name: "Collision" },
345
379
  };
346
- const request = (command: BotConfigurationCommandV1) => ({
347
- schemaVersion: 1 as const,
348
- userId: "user-1",
349
- botId: "primary",
350
- command,
351
- });
352
- const contribution = createShellBotBackendContribution(host);
380
+ const contribution = createShellBotBackendContribution(backendHost);
353
381
  await contribution.materializeSettings(
354
382
  { userId: "user-1", botId: "primary" },
355
383
  { name: "Primary" },
@@ -363,7 +391,7 @@ describe("Bot capability assignment admission", () => {
363
391
  );
364
392
  const receipt = await first;
365
393
 
366
- const redeployed = createShellBotBackendContribution(host);
394
+ const redeployed = createShellBotBackendContribution(backendHost);
367
395
  await expect(
368
396
  redeployed.executeConfiguration(request(original)),
369
397
  ).resolves.toEqual(receipt);
@@ -383,1375 +411,262 @@ describe("Bot capability assignment admission", () => {
383
411
  profile: { name: "Original" },
384
412
  });
385
413
  });
414
+ });
386
415
 
387
- test("durably rejects invalid assignments before dependency claims", async () => {
416
+ describe("generic per-Turn model resolution", () => {
417
+ test("runs on the platform model without creating a per-Bot model record", async () => {
388
418
  const storage = new MemoryStorage();
389
- let user = installedUser();
390
- let dependencyClaims = 0;
391
- let dependencyAcknowledgements = 0;
392
- let reads = 0;
393
- let claimAuthorized = true;
394
- const dependencyStates = new Map<string, "claimed" | "acknowledged">();
395
- const userConfiguration = {
396
- readConfiguration: () => {
397
- reads += 1;
398
- return Promise.resolve(structuredClone(user));
399
- },
400
- executeConnectionDependency: (request: {
401
- action: "claim" | "read" | "acknowledge" | "release" | "reconcile";
402
- operationId: string;
403
- requirement?: unknown;
404
- }) => {
405
- if (request.action === "read") {
406
- return Promise.resolve({
407
- schemaVersion: 1 as const,
408
- status:
409
- dependencyStates.get(request.operationId) ?? ("absent" as const),
410
- });
411
- }
412
- if (request.action === "claim") {
413
- expect(request.requirement).toEqual({
414
- schemaVersion: 1,
415
- packageId: "composio",
416
- packageVersion: "0.0.1",
417
- capabilityId: "gmail-tools",
418
- connectionTypeIds: ["gmail"],
419
- });
420
- dependencyClaims += 1;
421
- if (claimAuthorized) {
422
- dependencyStates.set(request.operationId, "claimed");
423
- }
424
- return Promise.resolve(
425
- claimAuthorized
426
- ? { schemaVersion: 1 as const, status: "claimed" as const }
427
- : {
428
- schemaVersion: 1 as const,
429
- status: "rejected" as const,
430
- failure: "claim rejected",
431
- },
432
- );
433
- }
434
- if (request.action === "acknowledge") {
435
- dependencyAcknowledgements += 1;
436
- dependencyStates.set(request.operationId, "acknowledged");
437
- return Promise.resolve({
438
- schemaVersion: 1 as const,
439
- status: "acknowledged" as const,
440
- });
441
- }
442
- return Promise.resolve({
443
- schemaVersion: 1 as const,
444
- status: "released" as const,
445
- });
446
- },
447
- };
448
- const contribution = createShellBotBackendContribution({
449
- state: { storage } as unknown as DurableObjectState,
450
- env: {
451
- USER_CONFIGURATIONS: {
452
- idFromName: () => "user-1",
453
- get: () => userConfiguration,
454
- },
455
- } as never,
456
- compileApplication: compileAssignmentTestApplication,
457
- });
419
+ const contribution = host(storage, configuredUser);
458
420
  const identity = { userId: "user-1", botId: "primary" };
459
421
  await contribution.materializeSettings(identity, { name: "Primary" });
460
- const execute = (command: BotConfigurationCommandV1) =>
461
- contribution.executeConfiguration({
462
- schemaVersion: 1,
463
- ...identity,
464
- command,
465
- });
466
- const read = () =>
467
- contribution.readConfiguration({ schemaVersion: 1, ...identity });
468
422
 
469
- await expect(
470
- contribution.readConfiguration({
471
- schemaVersion: 1,
472
- userId: "user-1",
473
- botId: "../primary",
474
- }),
475
- ).rejects.toThrow("botId is invalid");
476
- await expect(
477
- contribution.executeConfiguration({
478
- schemaVersion: 1,
479
- ...identity,
480
- command: {
481
- schemaVersion: 1,
482
- type: "bot/update-profile",
483
- commandId: "malformed-profile",
484
- botId: "primary",
485
- expectedRevision: 0,
486
- profile: { name: 42 },
487
- },
488
- }),
489
- ).rejects.toThrow("profile.name must be a string");
490
- expect(await read()).toMatchObject({ revision: 0, assignments: [] });
491
-
492
- const missingConnection = assignmentCommand("missing-connection", {
493
- packageId: "composio",
494
- capabilityId: "gmail-tools",
495
- });
496
- const first = await execute(missingConnection);
497
- expect(first).toMatchObject({
498
- status: "rejected",
499
- revision: 0,
500
- failure: expect.stringContaining("requires a Connection"),
501
- });
502
- const readsAfterFirst = reads;
503
- expect(await execute(missingConnection)).toEqual(first);
504
- expect(reads).toBe(readsAfterFirst);
505
-
506
- expect(
507
- await execute(
508
- assignmentCommand("unknown-capability", {
509
- packageId: "composio",
510
- capabilityId: "unknown",
511
- connectionId: "gmail-1",
512
- }),
513
- ),
514
- ).toMatchObject({ status: "rejected", revision: 0 });
515
-
516
- user = {
517
- ...installedUser(),
518
- connections: [
519
- { ...installedUser().connections[0]!, connectionTypeId: "calendar" },
520
- ],
521
- };
522
- expect(
523
- await execute(
524
- assignmentCommand("wrong-connection-type", {
525
- packageId: "composio",
526
- capabilityId: "gmail-tools",
527
- connectionId: "gmail-1",
528
- }),
529
- ),
530
- ).toMatchObject({ status: "rejected", revision: 0 });
531
-
532
- user = {
533
- ...installedUser(),
534
- packages: [{ ...installedUser().packages[0]!, state: "disabled" }],
535
- };
536
- expect(
537
- await execute(
538
- assignmentCommand("disabled-package", {
539
- packageId: "composio",
540
- capabilityId: "gmail-tools",
541
- connectionId: "gmail-1",
542
- }),
543
- ),
544
- ).toMatchObject({ status: "rejected", revision: 0 });
545
-
546
- expect(dependencyClaims).toBe(0);
547
- expect(await read()).toMatchObject({
548
- revision: 0,
549
- assignments: [],
550
- } satisfies Partial<BotSettingsViewV1>);
551
-
552
- user = installedUser();
553
- claimAuthorized = false;
554
- const changedDuringClaim = assignmentCommand("changed-during-claim", {
555
- packageId: "composio",
556
- capabilityId: "gmail-tools",
557
- connectionId: "gmail-1",
558
- });
559
- const changedReceipt = await execute(changedDuringClaim);
560
- expect(changedReceipt).toMatchObject({ status: "rejected", revision: 0 });
561
- expect(await execute(changedDuringClaim)).toEqual(changedReceipt);
562
- expect(dependencyClaims).toBe(1);
563
- expect(dependencyAcknowledgements).toBe(0);
564
- expect(await read()).toMatchObject({
565
- revision: 0,
566
- assignments: [],
567
- });
568
-
569
- claimAuthorized = true;
570
- expect(
571
- await execute(
572
- assignmentCommand("valid-assignment", {
573
- packageId: "composio",
574
- capabilityId: "gmail-tools",
575
- connectionId: "gmail-1",
576
- }),
577
- ),
578
- ).toMatchObject({ status: "applied", revision: 1 });
579
- expect(dependencyClaims).toBe(2);
580
- expect(dependencyAcknowledgements).toBe(1);
581
- expect(await read()).toMatchObject({
582
- revision: 1,
583
- assignments: [
584
- {
585
- assignmentId: "valid-assignment",
586
- state: "enabled",
587
- connectionId: "gmail-1",
588
- },
589
- ],
423
+ const result = await contribution.run({
424
+ ...identity,
425
+ runId: "platform-model-run",
426
+ sessionId: "user-1:primary",
427
+ acceptedAt: "2026-09-02T00:00:00.000Z",
428
+ text: "hello",
590
429
  });
591
430
 
592
- await expect(
593
- execute({
594
- schemaVersion: 1,
595
- type: "bot/assign-capability",
596
- commandId: "reuse-assignment-authority",
597
- botId: "primary",
598
- expectedRevision: 1,
599
- assignment: {
600
- assignmentId: "valid-assignment",
601
- packageId: "composio",
602
- capabilityId: "gmail-send",
603
- connectionId: "gmail-1",
604
- },
605
- }),
606
- ).resolves.toMatchObject({
607
- status: "rejected",
608
- revision: 1,
609
- failure: "Assignment ID cannot change Package Capability authority",
610
- });
611
- expect(dependencyClaims).toBe(2);
431
+ expect(result.text).toBe("Cordis runtime: hello");
432
+ const settings = await contribution.getSettings(identity);
433
+ expect(settings).toMatchObject({ revision: 0, packageValues: {} });
434
+ expect(Object.hasOwn(settings, "model")).toBe(false);
612
435
  });
613
436
 
614
- test("atomically binds and durably unbinds a Connection model", async () => {
437
+ test("uses an enabled Bot-scoped model value and preserves it while disabled", async () => {
615
438
  const storage = new MemoryStorage();
616
- let dependencyGeneration: string | undefined;
617
- let releaseAttempts = 0;
618
- const userConfiguration = {
619
- readConfiguration: () => Promise.resolve(installedUser()),
620
- executeConnectionDependency: (request: {
621
- action: "claim" | "read" | "acknowledge" | "release" | "reconcile";
622
- generation: string;
623
- }) => {
624
- if (request.action === "claim") {
625
- dependencyGeneration = request.generation;
626
- return Promise.resolve({
627
- schemaVersion: 1 as const,
628
- status: "claimed" as const,
629
- });
630
- }
631
- if (request.action === "acknowledge") {
632
- return Promise.resolve({
633
- schemaVersion: 1 as const,
634
- status: "acknowledged" as const,
635
- });
636
- }
637
- if (request.action === "read") {
638
- return Promise.resolve({
639
- schemaVersion: 1 as const,
640
- status:
641
- request.generation === dependencyGeneration
642
- ? ("acknowledged" as const)
643
- : ("absent" as const),
644
- });
645
- }
646
- if (request.action === "release") {
647
- releaseAttempts += 1;
648
- if (releaseAttempts === 1) {
649
- return Promise.resolve({
650
- schemaVersion: 1 as const,
651
- status: "pending" as const,
652
- });
653
- }
654
- if (request.generation === dependencyGeneration) {
655
- dependencyGeneration = undefined;
656
- }
657
- return Promise.resolve({
658
- schemaVersion: 1 as const,
659
- status: "released" as const,
660
- });
661
- }
662
- return Promise.resolve({
663
- schemaVersion: 1 as const,
664
- status: "released" as const,
665
- });
666
- },
439
+ let user = configuredUser();
440
+ user.packages[1] = {
441
+ ...user.packages[1]!,
442
+ state: "installed",
443
+ values: { model: model("flock-ai-ambient", "account-model") },
667
444
  };
668
- const contribution = createShellBotBackendContribution({
669
- state: { storage } as unknown as DurableObjectState,
670
- env: {
671
- USER_CONFIGURATIONS: {
672
- idFromName: () => "user-1",
673
- get: () => userConfiguration,
674
- },
675
- } as never,
676
- compileApplication: compileAssignmentTestApplication,
677
- });
445
+ const contribution = host(storage, () => user);
678
446
  const identity = { userId: "user-1", botId: "primary" };
679
447
  await contribution.materializeSettings(identity, { name: "Primary" });
680
- const execute = (command: BotConfigurationCommandV1) =>
681
- contribution.executeConfiguration({
448
+ await contribution.executeConfiguration(
449
+ request({
682
450
  schemaVersion: 1,
683
- ...identity,
684
- command,
685
- });
686
-
687
- const bound = await execute({
688
- schemaVersion: 1,
689
- type: "bot/assign-capability",
690
- commandId: "bind-model",
691
- botId: "primary",
692
- expectedRevision: 0,
693
- assignment: {
694
- assignmentId: "fixture-model",
695
- packageId: "composio",
696
- capabilityId: "gmail-tools",
697
- connectionId: "gmail-1",
698
- },
699
- model: {
700
- connectionId: "gmail-1",
701
- providerModelId: "fixture-model:latest",
702
- },
703
- });
704
-
705
- expect(bound).toMatchObject({ status: "applied", revision: 1 });
706
- expect(dependencyGeneration).toBe("bind-model");
707
- expect(await contribution.getSettings(identity)).toMatchObject({
708
- revision: 1,
709
- model: {
710
- connectionId: "gmail-1",
711
- providerModelId: "fixture-model:latest",
712
- },
713
- assignments: [{ assignmentId: "fixture-model", state: "enabled" }],
714
- });
451
+ type: "bot/set-package-settings",
452
+ commandId: "set-bot-model",
453
+ botId: "primary",
454
+ expectedRevision: 0,
455
+ packageId: "custom-models",
456
+ values: { model: model("flock-ai-ambient", "bot-model") },
457
+ }),
458
+ );
715
459
 
716
- const unavailable = await contribution.getSettings(identity);
717
- await storage.put("bot-configuration", {
718
- ...unavailable,
719
- assignments: unavailable.assignments.map((assignment) => ({
720
- ...assignment,
721
- state: "unavailable" as const,
722
- })),
460
+ expect(await contribution.resolveConfiguration(identity)).toMatchObject({
461
+ model: model("flock-ai-ambient", "bot-model"),
723
462
  });
724
463
 
725
- const unbind: BotConfigurationCommandV1 = {
726
- schemaVersion: 1,
727
- type: "bot/unbind-model",
728
- commandId: "unbind-model",
729
- botId: "primary",
730
- expectedRevision: 1,
731
- assignmentId: "fixture-model",
464
+ user = {
465
+ ...user,
466
+ packages: user.packages.map((pkg) =>
467
+ pkg.packageId === "custom-models"
468
+ ? { ...pkg, state: "disabled" as const }
469
+ : pkg,
470
+ ),
732
471
  };
733
- // The release is delayed once: the Unassign stays visibly retrying and its
734
- // durable receipt replays until the dependency is actually released.
735
- await expect(execute(unbind)).resolves.toMatchObject({
736
- status: "pending",
472
+ expect(await contribution.resolveConfiguration(identity)).toMatchObject({
473
+ model: model("flock-ai-ambient", "@flock/auto"),
737
474
  });
738
- expect(dependencyGeneration).toBe("bind-model");
739
- await contribution.alarm();
740
-
741
- const unbound = await execute(unbind);
742
-
743
- expect(unbound).toMatchObject({ status: "applied", revision: 2 });
744
- expect(releaseAttempts).toBe(2);
745
- expect(dependencyGeneration).toBeUndefined();
746
475
  expect(await contribution.getSettings(identity)).toMatchObject({
747
- revision: 2,
748
- model: undefined,
749
- assignments: [],
750
- });
751
- });
752
-
753
- test("releases the superseded model dependency after switching Connections", async () => {
754
- const storage = new MemoryStorage();
755
- const user = installedUser();
756
- user.connections.push({
757
- ...user.connections[0]!,
758
- connectionId: "gmail-2",
759
- displayName: "Gmail 2",
760
- });
761
- const generations = new Set<string>();
762
- const released: Array<{ connectionId: string; generation: string }> = [];
763
- let releaseAttempts = 0;
764
- const userConfiguration = {
765
- readConfiguration: () => Promise.resolve(user),
766
- executeConnectionDependency: (request: {
767
- action: "claim" | "read" | "acknowledge" | "release" | "reconcile";
768
- connectionId: string;
769
- generation: string;
770
- }) => {
771
- if (request.action === "claim") {
772
- generations.add(request.generation);
773
- return Promise.resolve({
774
- schemaVersion: 1 as const,
775
- status: "claimed" as const,
776
- });
777
- }
778
- if (request.action === "acknowledge") {
779
- return Promise.resolve({
780
- schemaVersion: 1 as const,
781
- status: "acknowledged" as const,
782
- });
783
- }
784
- if (request.action === "read") {
785
- return Promise.resolve({
786
- schemaVersion: 1 as const,
787
- status: generations.has(request.generation)
788
- ? ("acknowledged" as const)
789
- : ("absent" as const),
790
- });
791
- }
792
- if (request.action === "release") {
793
- releaseAttempts += 1;
794
- if (releaseAttempts === 1) {
795
- return Promise.resolve({
796
- schemaVersion: 1 as const,
797
- status: "pending" as const,
798
- });
799
- }
800
- released.push({
801
- connectionId: request.connectionId,
802
- generation: request.generation,
803
- });
804
- generations.delete(request.generation);
805
- return Promise.resolve({
806
- schemaVersion: 1 as const,
807
- status: "released" as const,
808
- });
809
- }
810
- return Promise.resolve({
811
- schemaVersion: 1 as const,
812
- status: "released" as const,
813
- });
814
- },
815
- };
816
- const contribution = createShellBotBackendContribution({
817
- state: { storage } as unknown as DurableObjectState,
818
- env: {
819
- USER_CONFIGURATIONS: {
820
- idFromName: () => "user-1",
821
- get: () => userConfiguration,
476
+ packageValues: {
477
+ "custom-models": {
478
+ model: model("flock-ai-ambient", "bot-model"),
822
479
  },
823
- } as never,
824
- compileApplication: compileAssignmentTestApplication,
825
- });
826
- const identity = { userId: "user-1", botId: "primary" };
827
- await contribution.materializeSettings(identity, { name: "Primary" });
828
- const execute = (command: BotConfigurationCommandV1) =>
829
- contribution.executeConfiguration({
830
- schemaVersion: 1,
831
- ...identity,
832
- command,
833
- });
834
-
835
- await execute({
836
- schemaVersion: 1,
837
- type: "bot/assign-capability",
838
- commandId: "bind-gmail-1",
839
- botId: "primary",
840
- expectedRevision: 0,
841
- assignment: {
842
- assignmentId: "gmail-model-1",
843
- packageId: "composio",
844
- capabilityId: "gmail-tools",
845
- connectionId: "gmail-1",
846
- },
847
- model: {
848
- connectionId: "gmail-1",
849
- providerModelId: "fixture-model:latest",
850
- },
851
- });
852
- const beforeSwitch = await contribution.getSettings(identity);
853
- await storage.put({
854
- "bot-configuration": {
855
- ...beforeSwitch,
856
- assignments: [
857
- ...beforeSwitch.assignments,
858
- {
859
- assignmentId: "gmail-tool",
860
- packageId: "composio",
861
- capabilityId: "gmail-send",
862
- connectionId: "gmail-1",
863
- state: "enabled" as const,
864
- },
865
- ],
866
480
  },
867
- "assignment-generation:gmail-tool": "tool-generation",
868
481
  });
869
- generations.add("tool-generation");
870
482
 
871
- // Moving the model to another Connection is an atomic Replace on the same
872
- // Assignment: it claims the new dependency, commits the swap, then
873
- // releases the old one. A delayed release stays visibly retrying rather
874
- // than failing the command.
875
- const switchModel: BotConfigurationCommandV1 = {
876
- schemaVersion: 1,
877
- type: "bot/replace-capability",
878
- commandId: "bind-gmail-2",
879
- botId: "primary",
880
- expectedRevision: 1,
881
- assignment: {
882
- assignmentId: "gmail-model-1",
883
- packageId: "composio",
884
- capabilityId: "gmail-tools",
885
- connectionId: "gmail-2",
886
- },
887
- model: {
888
- connectionId: "gmail-2",
889
- providerModelId: "fixture-model:latest",
890
- },
483
+ user = {
484
+ ...user,
485
+ packages: user.packages.map((pkg) =>
486
+ pkg.packageId === "custom-models"
487
+ ? { ...pkg, state: "installed" as const }
488
+ : pkg,
489
+ ),
891
490
  };
892
- await expect(execute(switchModel)).resolves.toMatchObject({
893
- status: "applied",
894
- revision: 2,
895
- });
896
- // The commit is durable; only the old release is still retrying.
897
- await contribution.alarm();
898
- await expect(execute(switchModel)).resolves.toMatchObject({
899
- status: "applied",
900
- revision: 2,
491
+ expect(await contribution.resolveConfiguration(identity)).toMatchObject({
492
+ model: model("flock-ai-ambient", "bot-model"),
901
493
  });
494
+ });
902
495
 
903
- expect(releaseAttempts).toBe(2);
904
- expect(released).toEqual([
905
- { connectionId: "gmail-1", generation: "bind-gmail-1" },
906
- ]);
907
- expect(generations.has("bind-gmail-1")).toBe(false);
908
- expect(generations.has("tool-generation")).toBe(true);
909
- expect(generations.has("bind-gmail-2")).toBe(true);
910
- expect(await contribution.getSettings(identity)).toMatchObject({
911
- revision: 2,
912
- model: { connectionId: "gmail-2" },
913
- assignments: [
914
- { assignmentId: "gmail-tool", state: "enabled" },
915
- { assignmentId: "gmail-model-1", state: "enabled" },
916
- ],
496
+ test("Package disablement and Connection revocation fail every Bot's next Turn closed", async () => {
497
+ let user = configuredUser();
498
+ const bots = ["alpha", "beta"].map((botId) => {
499
+ const storage = new MemoryStorage();
500
+ return {
501
+ botId,
502
+ storage,
503
+ contribution: host(storage, () => user),
504
+ };
917
505
  });
506
+ for (const bot of bots) {
507
+ await bot.contribution.materializeSettings(
508
+ { userId: "user-1", botId: bot.botId },
509
+ { name: bot.botId },
510
+ );
511
+ }
918
512
 
919
- // Each further move is another Replace on the same Assignment, so each
920
- // one releases exactly the Connection it superseded.
921
- for (const [commandId, connectionId, expectedRevision] of [
922
- ["bind-gmail-1-again", "gmail-1", 2],
923
- ["bind-gmail-2-again", "gmail-2", 3],
924
- ] as const) {
513
+ user = {
514
+ ...user,
515
+ packages: user.packages.map((pkg) =>
516
+ pkg.packageId === "provider-flock-ai"
517
+ ? { ...pkg, state: "disabled" as const }
518
+ : pkg,
519
+ ),
520
+ };
521
+ for (const bot of bots) {
522
+ const runId = `${bot.botId}-disabled-package`;
925
523
  await expect(
926
- execute({
927
- schemaVersion: 1,
928
- type: "bot/replace-capability",
929
- commandId,
930
- botId: "primary",
931
- expectedRevision,
932
- assignment: {
933
- assignmentId: "gmail-model-1",
934
- packageId: "composio",
935
- capabilityId: "gmail-tools",
936
- connectionId,
937
- },
938
- model: {
939
- connectionId,
940
- providerModelId: "fixture-model:latest",
941
- },
524
+ bot.contribution.run({
525
+ userId: "user-1",
526
+ botId: bot.botId,
527
+ runId,
528
+ sessionId: `user-1:${bot.botId}`,
529
+ acceptedAt: "2026-09-02T00:01:00.000Z",
530
+ text: "must fail",
942
531
  }),
943
- ).resolves.toMatchObject({
944
- status: "applied",
945
- revision: expectedRevision + 1,
532
+ ).rejects.toThrow("not installed and enabled");
533
+ expect(await bot.storage.get(`run:${runId}`)).toMatchObject({
534
+ status: "failed",
535
+ failure: expect.stringContaining("not installed and enabled"),
946
536
  });
947
537
  }
948
538
 
949
- expect(released.slice(-2)).toEqual([
950
- { connectionId: "gmail-2", generation: "bind-gmail-2" },
951
- { connectionId: "gmail-1", generation: "bind-gmail-1-again" },
952
- ]);
953
- expect(generations.has("bind-gmail-1")).toBe(false);
954
- expect(generations.has("bind-gmail-2")).toBe(false);
955
- expect(generations.has("bind-gmail-1-again")).toBe(false);
956
- expect(generations.has("tool-generation")).toBe(true);
957
- expect(await contribution.getSettings(identity)).toMatchObject({
958
- revision: 4,
959
- model: { connectionId: "gmail-2" },
960
- assignments: [
961
- { assignmentId: "gmail-tool", state: "enabled" },
962
- { assignmentId: "gmail-model-1", state: "enabled" },
963
- ],
964
- });
539
+ user = configuredUser();
540
+ user.connections[0] = { ...user.connections[0]!, state: "revoked" };
541
+ for (const bot of bots) {
542
+ const runId = `${bot.botId}-revoked-connection`;
543
+ await expect(
544
+ bot.contribution.run({
545
+ userId: "user-1",
546
+ botId: bot.botId,
547
+ runId,
548
+ sessionId: `user-1:${bot.botId}`,
549
+ acceptedAt: "2026-09-02T00:02:00.000Z",
550
+ text: "must fail",
551
+ }),
552
+ ).rejects.toThrow("is revoked");
553
+ expect(await bot.storage.get(`run:${runId}`)).toMatchObject({
554
+ status: "failed",
555
+ failure: expect.stringContaining("is revoked"),
556
+ });
557
+ }
965
558
  });
559
+ });
966
560
 
967
- test("orders atomic Replace and keeps Unassign stable until release", async () => {
561
+ describe("Bot Package setting commands", () => {
562
+ test("validates, revision-fences, merges, and replays durably", async () => {
968
563
  const storage = new MemoryStorage();
969
- const user = installedUser();
970
- user.connections.push({
971
- ...user.connections[0]!,
972
- connectionId: "gmail-2",
973
- displayName: "Gmail replacement",
974
- });
975
- const dependencies = new Map<
976
- string,
977
- "claimed" | "acknowledged" | "released"
978
- >();
979
- const log: string[] = [];
980
- let holdRelease = false;
981
- const userConfiguration = {
982
- readConfiguration: () => Promise.resolve(structuredClone(user)),
983
- executeConnectionDependency: (request: {
984
- action: "claim" | "read" | "acknowledge" | "release" | "reconcile";
985
- generation: string;
986
- }) => {
987
- log.push(`${request.action}:${request.generation}`);
988
- if (request.action === "read") {
989
- return Promise.resolve({
990
- schemaVersion: 1 as const,
991
- status: dependencies.get(request.generation) ?? ("absent" as const),
992
- });
993
- }
994
- if (request.action === "claim") {
995
- dependencies.set(request.generation, "claimed");
996
- return Promise.resolve({
997
- schemaVersion: 1 as const,
998
- status: "claimed" as const,
999
- });
1000
- }
1001
- if (request.action === "acknowledge") {
1002
- dependencies.set(request.generation, "acknowledged");
1003
- return Promise.resolve({
1004
- schemaVersion: 1 as const,
1005
- status: "acknowledged" as const,
1006
- });
1007
- }
1008
- if (request.action === "release" && holdRelease) {
1009
- return Promise.resolve({
1010
- schemaVersion: 1 as const,
1011
- status: "pending" as const,
1012
- });
1013
- }
1014
- if (request.action === "release")
1015
- dependencies.set(request.generation, "released");
1016
- return Promise.resolve({
1017
- schemaVersion: 1 as const,
1018
- status:
1019
- request.action === "reconcile"
1020
- ? ("pending" as const)
1021
- : ("released" as const),
1022
- });
1023
- },
1024
- };
1025
- const contribution = createShellBotBackendContribution({
1026
- state: { storage } as unknown as DurableObjectState,
1027
- env: {
1028
- USER_CONFIGURATIONS: {
1029
- idFromName: () => "user-1",
1030
- get: () => userConfiguration,
1031
- },
1032
- } as never,
1033
- compileApplication: compileAssignmentTestApplication,
1034
- });
564
+ const user = configuredUser();
565
+ const contribution = host(storage, () => user);
1035
566
  const identity = { userId: "user-1", botId: "primary" };
1036
567
  await contribution.materializeSettings(identity, { name: "Primary" });
1037
- const execute = (command: BotConfigurationCommandV1) =>
1038
- contribution.executeConfiguration({
1039
- schemaVersion: 1,
1040
- ...identity,
1041
- command,
1042
- });
1043
-
1044
- await expect(
1045
- execute({
1046
- schemaVersion: 1,
1047
- type: "bot/assign-capability",
1048
- commandId: "assign-1",
1049
- botId: "primary",
1050
- expectedRevision: 0,
1051
- assignment: {
1052
- assignmentId: "mail",
1053
- packageId: "composio",
1054
- capabilityId: "gmail-tools",
1055
- connectionId: "gmail-1",
1056
- },
1057
- }),
1058
- ).resolves.toMatchObject({ status: "applied", revision: 1 });
1059
- log.length = 0;
1060
-
1061
- await expect(
1062
- execute({
1063
- schemaVersion: 1,
1064
- type: "bot/replace-capability",
1065
- commandId: "replace-1",
1066
- botId: "primary",
1067
- expectedRevision: 1,
1068
- assignment: {
1069
- assignmentId: "mail",
1070
- packageId: "composio",
1071
- capabilityId: "gmail-tools",
1072
- connectionId: "gmail-2",
1073
- },
1074
- }),
1075
- ).resolves.toMatchObject({ status: "applied", revision: 2 });
1076
- expect(log).toEqual([
1077
- "read:replace-1",
1078
- "claim:replace-1",
1079
- "read:replace-1",
1080
- "acknowledge:replace-1",
1081
- "read:assign-1",
1082
- "release:assign-1",
1083
- ]);
1084
- expect(await contribution.getSettings(identity)).toMatchObject({
1085
- revision: 2,
1086
- assignments: [{ assignmentId: "mail", connectionId: "gmail-2" }],
1087
- assignmentOperations: [],
1088
- });
1089
-
1090
- holdRelease = true;
1091
- const pendingUnassign = await execute({
1092
- schemaVersion: 1,
1093
- type: "bot/unassign-capability",
1094
- commandId: "unassign-1",
1095
- botId: "primary",
1096
- expectedRevision: 2,
1097
- assignmentId: "mail",
1098
- });
1099
- expect(pendingUnassign).toEqual({
1100
- schemaVersion: 1,
1101
- commandId: "unassign-1",
1102
- revision: 2,
1103
- status: "pending",
1104
- });
1105
- await expect(
1106
- execute({
1107
- schemaVersion: 1,
1108
- type: "bot/unassign-capability",
1109
- commandId: "unassign-1",
1110
- botId: "primary",
1111
- expectedRevision: 2,
1112
- assignmentId: "mail",
1113
- }),
1114
- ).resolves.toEqual(pendingUnassign);
1115
- expect(await contribution.getSettings(identity)).toMatchObject({
1116
- revision: 2,
1117
- assignments: [{ assignmentId: "mail", connectionId: "gmail-2" }],
1118
- assignmentOperations: [
1119
- { commandId: "unassign-1", kind: "unassigning", state: "retrying" },
1120
- ],
1121
- });
1122
568
 
1123
- holdRelease = false;
1124
- const reconstructed = createShellBotBackendContribution({
1125
- state: { storage } as unknown as DurableObjectState,
1126
- env: {
1127
- USER_CONFIGURATIONS: {
1128
- idFromName: () => "user-1",
1129
- get: () => userConfiguration,
1130
- },
1131
- } as never,
1132
- compileApplication: compileAssignmentTestApplication,
1133
- });
1134
- await reconstructed.alarm();
1135
- expect(await reconstructed.getSettings(identity)).toMatchObject({
1136
- revision: 3,
1137
- assignments: [],
1138
- assignmentOperations: [],
1139
- });
1140
569
  await expect(
1141
- reconstructed.executeConfiguration({
1142
- schemaVersion: 1,
1143
- ...identity,
1144
- command: {
570
+ contribution.executeConfiguration(
571
+ request({
1145
572
  schemaVersion: 1,
1146
- type: "bot/unassign-capability",
1147
- commandId: "unassign-1",
573
+ type: "bot/set-package-settings",
574
+ commandId: "invalid-model",
1148
575
  botId: "primary",
1149
- expectedRevision: 2,
1150
- assignmentId: "mail",
1151
- },
1152
- }),
1153
- ).resolves.toMatchObject({ status: "applied", revision: 3 });
1154
- await expect(
1155
- reconstructed.executeConfiguration({
1156
- schemaVersion: 1,
1157
- ...identity,
1158
- command: {
1159
- schemaVersion: 1,
1160
- type: "bot/unassign-capability",
1161
- commandId: "unassign-1",
1162
- botId: "primary",
1163
- expectedRevision: 2,
1164
- assignmentId: "other",
1165
- },
1166
- }),
1167
- ).rejects.toThrow("reused for a different command");
1168
- });
1169
-
1170
- test("releases the old Replace dependency when new acknowledgement is absent or rejected", async () => {
1171
- for (const acknowledgement of ["absent", "rejected"] as const) {
1172
- const storage = new MemoryStorage();
1173
- const user = installedUser();
1174
- user.connections.push({
1175
- ...user.connections[0]!,
1176
- connectionId: "gmail-2",
1177
- displayName: "Gmail replacement",
1178
- });
1179
- const dependencies = new Map<
1180
- string,
1181
- "claimed" | "acknowledged" | "released"
1182
- >();
1183
- const log: string[] = [];
1184
- let replacementReads = 0;
1185
- let holdOldRelease = true;
1186
- const userConfiguration = {
1187
- readConfiguration: () => Promise.resolve(structuredClone(user)),
1188
- executeConnectionDependency: (request: {
1189
- action: "claim" | "read" | "acknowledge" | "release" | "reconcile";
1190
- generation: string;
1191
- }) => {
1192
- log.push(`${request.action}:${request.generation}`);
1193
- if (request.action === "read") {
1194
- if (request.generation.startsWith("replace-")) {
1195
- replacementReads += 1;
1196
- if (replacementReads > 1 && acknowledgement === "absent") {
1197
- return Promise.resolve({
1198
- schemaVersion: 1 as const,
1199
- status: "absent" as const,
1200
- });
1201
- }
1202
- }
1203
- return Promise.resolve({
1204
- schemaVersion: 1 as const,
1205
- status:
1206
- dependencies.get(request.generation) ?? ("absent" as const),
1207
- });
1208
- }
1209
- if (request.action === "claim") {
1210
- dependencies.set(request.generation, "claimed");
1211
- return Promise.resolve({
1212
- schemaVersion: 1 as const,
1213
- status: "claimed" as const,
1214
- });
1215
- }
1216
- if (request.action === "acknowledge") {
1217
- if (
1218
- request.generation.startsWith("replace-") &&
1219
- acknowledgement === "rejected"
1220
- ) {
1221
- return Promise.resolve({
1222
- schemaVersion: 1 as const,
1223
- status: "rejected" as const,
1224
- failure: "acknowledgement rejected",
1225
- });
1226
- }
1227
- dependencies.set(request.generation, "acknowledged");
1228
- return Promise.resolve({
1229
- schemaVersion: 1 as const,
1230
- status: "acknowledged" as const,
1231
- });
1232
- }
1233
- if (request.action === "release") {
1234
- if (request.generation === "assign-old" && holdOldRelease) {
1235
- return Promise.resolve({
1236
- schemaVersion: 1 as const,
1237
- status: "pending" as const,
1238
- });
1239
- }
1240
- dependencies.set(request.generation, "released");
1241
- return Promise.resolve({
1242
- schemaVersion: 1 as const,
1243
- status: "released" as const,
1244
- });
1245
- }
1246
- return Promise.resolve({
1247
- schemaVersion: 1 as const,
1248
- status: "pending" as const,
1249
- });
1250
- },
1251
- };
1252
- const makeContribution = () =>
1253
- createShellBotBackendContribution({
1254
- state: { storage } as unknown as DurableObjectState,
1255
- env: {
1256
- USER_CONFIGURATIONS: {
1257
- idFromName: () => "user-1",
1258
- get: () => userConfiguration,
1259
- },
1260
- } as never,
1261
- compileApplication: compileAssignmentTestApplication,
1262
- });
1263
- const contribution = makeContribution();
1264
- const identity = { userId: "user-1", botId: "primary" };
1265
- await contribution.materializeSettings(identity, { name: "Primary" });
1266
- const execute = (
1267
- backend: ReturnType<typeof createShellBotBackendContribution>,
1268
- command: BotConfigurationCommandV1,
1269
- ) =>
1270
- backend.executeConfiguration({
1271
- schemaVersion: 1,
1272
- ...identity,
1273
- command,
1274
- });
1275
- await execute(contribution, {
1276
- schemaVersion: 1,
1277
- type: "bot/assign-capability",
1278
- commandId: "assign-old",
1279
- botId: "primary",
1280
- expectedRevision: 0,
1281
- assignment: {
1282
- assignmentId: "mail",
1283
- packageId: "composio",
1284
- capabilityId: "gmail-tools",
1285
- connectionId: "gmail-1",
1286
- },
1287
- });
1288
- const replaceCommand = {
1289
- schemaVersion: 1 as const,
1290
- type: "bot/replace-capability" as const,
1291
- commandId: `replace-${acknowledgement}`,
1292
- botId: "primary",
1293
- expectedRevision: 1,
1294
- assignment: {
1295
- assignmentId: "mail",
1296
- packageId: "composio",
1297
- capabilityId: "gmail-tools",
1298
- connectionId: "gmail-2",
1299
- },
1300
- };
1301
-
1302
- await expect(
1303
- execute(contribution, replaceCommand),
1304
- ).resolves.toMatchObject({ status: "applied", revision: 2 });
1305
- expect(dependencies.get("assign-old")).toBe("acknowledged");
1306
- expect(await contribution.getSettings(identity)).toMatchObject({
1307
- revision: 2,
1308
- assignments: [
1309
- {
1310
- assignmentId: "mail",
1311
- connectionId: "gmail-2",
1312
- state: "unavailable",
1313
- },
1314
- ],
1315
- assignmentOperations: [
1316
- {
1317
- commandId: `replace-${acknowledgement}`,
1318
- kind: "replacing",
1319
- state: "retrying",
1320
- },
1321
- ],
1322
- });
1323
- expect(log).toContain("release:assign-old");
1324
-
1325
- holdOldRelease = false;
1326
- const reconstructed = makeContribution();
1327
- await reconstructed.alarm();
1328
- expect(dependencies.get("assign-old")).toBe("released");
1329
- expect(await reconstructed.getSettings(identity)).toMatchObject({
1330
- revision: 2,
1331
- assignments: [
1332
- {
1333
- assignmentId: "mail",
1334
- connectionId: "gmail-2",
1335
- state: "unavailable",
1336
- },
1337
- ],
1338
- assignmentOperations: [],
1339
- });
1340
- await expect(
1341
- execute(reconstructed, replaceCommand),
1342
- ).resolves.toMatchObject({ status: "applied", revision: 2 });
1343
- }
1344
- });
576
+ expectedRevision: 0,
577
+ packageId: "custom-models",
578
+ values: { model: { connectionId: "flock-ai-ambient" } as never },
579
+ }),
580
+ ),
581
+ ).rejects.toThrow("model has invalid fields");
582
+ expect((await contribution.getSettings(identity)).revision).toBe(0);
1345
583
 
1346
- test("keeps provider absence retrying until the owner becomes available", async () => {
1347
- const storage = new MemoryStorage();
1348
- let available = false;
1349
- let dependency: "absent" | "claimed" | "acknowledged" = "absent";
1350
- const userConfiguration = {
1351
- readConfiguration: () => Promise.resolve(installedUser()),
1352
- executeConnectionDependency: (request: { action: string }) => {
1353
- if (!available) {
1354
- return Promise.resolve({
1355
- schemaVersion: 1 as const,
1356
- status: "unavailable" as const,
1357
- failure: "Connection owner is unavailable",
1358
- });
1359
- }
1360
- if (request.action === "read") {
1361
- return Promise.resolve({
1362
- schemaVersion: 1 as const,
1363
- status: dependency,
1364
- });
1365
- }
1366
- if (request.action === "claim") dependency = "claimed";
1367
- if (request.action === "acknowledge") dependency = "acknowledged";
1368
- return Promise.resolve({
1369
- schemaVersion: 1 as const,
1370
- status:
1371
- request.action === "claim"
1372
- ? ("claimed" as const)
1373
- : request.action === "acknowledge"
1374
- ? ("acknowledged" as const)
1375
- : ("pending" as const),
1376
- });
584
+ const first: BotConfigurationCommandV1 = {
585
+ schemaVersion: 1,
586
+ type: "bot/set-package-settings",
587
+ commandId: "set-package-values",
588
+ botId: "primary",
589
+ expectedRevision: 0,
590
+ packageId: "custom-models",
591
+ values: {
592
+ model: model("flock-ai-ambient", "bot-model"),
593
+ tone: "concise",
1377
594
  },
1378
595
  };
1379
- const host = {
1380
- state: { storage } as unknown as DurableObjectState,
1381
- env: {
1382
- USER_CONFIGURATIONS: {
1383
- idFromName: () => "user-1",
1384
- get: () => userConfiguration,
1385
- },
1386
- } as never,
1387
- compileApplication: compileAssignmentTestApplication,
1388
- };
1389
- const identity = { userId: "user-1", botId: "primary" };
1390
- const contribution = createShellBotBackendContribution(host);
1391
- await contribution.materializeSettings(identity, { name: "Primary" });
1392
- const command = assignmentCommand("owner-retry", {
1393
- packageId: "composio",
1394
- capabilityId: "gmail-tools",
1395
- connectionId: "gmail-1",
1396
- });
596
+ const receipt = await contribution.executeConfiguration(request(first));
1397
597
  await expect(
1398
- contribution.executeConfiguration({
1399
- schemaVersion: 1,
1400
- ...identity,
1401
- command,
1402
- }),
1403
- ).resolves.toEqual({
1404
- schemaVersion: 1,
1405
- commandId: "owner-retry",
1406
- revision: 0,
1407
- status: "pending",
1408
- });
598
+ contribution.executeConfiguration(request(first)),
599
+ ).resolves.toEqual(receipt);
1409
600
  expect(await contribution.getSettings(identity)).toMatchObject({
1410
- revision: 0,
1411
- assignments: [],
1412
- assignmentOperations: [{ commandId: "owner-retry", state: "retrying" }],
1413
- });
1414
- available = true;
1415
- const reconstructed = createShellBotBackendContribution(host);
1416
- await reconstructed.alarm();
1417
- expect(await reconstructed.getSettings(identity)).toMatchObject({
1418
601
  revision: 1,
1419
- assignments: [{ assignmentId: "owner-retry", state: "enabled" }],
1420
- assignmentOperations: [],
1421
- });
1422
- });
1423
-
1424
- test("scopes generations to Assignments that share one Connection", async () => {
1425
- const storage = new MemoryStorage();
1426
- const user = installedUser();
1427
- user.connections.push({
1428
- ...user.connections[0]!,
1429
- connectionId: "gmail-2",
1430
- displayName: "Gmail replacement",
1431
- });
1432
- const dependencies = new Map<
1433
- string,
1434
- "claimed" | "acknowledged" | "released"
1435
- >();
1436
- const userConfiguration = {
1437
- readConfiguration: () => Promise.resolve(structuredClone(user)),
1438
- executeConnectionDependency: (request: {
1439
- action: "claim" | "read" | "acknowledge" | "release" | "reconcile";
1440
- generation: string;
1441
- }) => {
1442
- if (request.action === "read") {
1443
- return Promise.resolve({
1444
- schemaVersion: 1 as const,
1445
- status: dependencies.get(request.generation) ?? ("absent" as const),
1446
- });
1447
- }
1448
- if (request.action === "claim") {
1449
- dependencies.set(request.generation, "claimed");
1450
- return Promise.resolve({
1451
- schemaVersion: 1 as const,
1452
- status: "claimed" as const,
1453
- });
1454
- }
1455
- if (request.action === "acknowledge") {
1456
- dependencies.set(request.generation, "acknowledged");
1457
- return Promise.resolve({
1458
- schemaVersion: 1 as const,
1459
- status: "acknowledged" as const,
1460
- });
1461
- }
1462
- if (request.action === "release") {
1463
- dependencies.set(request.generation, "released");
1464
- }
1465
- return Promise.resolve({
1466
- schemaVersion: 1 as const,
1467
- status: "released" as const,
1468
- });
1469
- },
1470
- };
1471
- const contribution = createShellBotBackendContribution({
1472
- state: { storage } as unknown as DurableObjectState,
1473
- env: {
1474
- USER_CONFIGURATIONS: {
1475
- idFromName: () => "user-1",
1476
- get: () => userConfiguration,
602
+ packageValues: {
603
+ "custom-models": {
604
+ model: model("flock-ai-ambient", "bot-model"),
605
+ tone: "concise",
1477
606
  },
1478
- } as never,
1479
- compileApplication: compileAssignmentTestApplication,
607
+ },
1480
608
  });
1481
- const identity = { userId: "user-1", botId: "primary" };
1482
- await contribution.materializeSettings(identity, { name: "Primary" });
1483
- const execute = (command: BotConfigurationCommandV1) =>
1484
- contribution.executeConfiguration({
1485
- schemaVersion: 1,
1486
- ...identity,
1487
- command,
1488
- });
1489
- const assign = (
1490
- commandId: string,
1491
- expectedRevision: number,
1492
- assignmentId: string,
1493
- connectionId: string,
1494
- type:
1495
- | "bot/assign-capability"
1496
- | "bot/replace-capability" = "bot/assign-capability",
1497
- ) =>
1498
- execute({
1499
- schemaVersion: 1,
1500
- type,
1501
- commandId,
1502
- botId: "primary",
1503
- expectedRevision,
1504
- assignment: {
1505
- assignmentId,
1506
- packageId: "composio",
1507
- capabilityId: "gmail-tools",
1508
- connectionId,
1509
- },
1510
- });
1511
609
 
1512
- await assign("assign-a", 0, "mail-a", "gmail-1");
1513
- await assign("assign-b", 1, "mail-b", "gmail-1");
1514
- await assign("replace-a", 2, "mail-a", "gmail-2", "bot/replace-capability");
1515
- expect(dependencies.get("assign-a")).toBe("released");
1516
- expect(dependencies.get("assign-b")).toBe("acknowledged");
1517
- await execute({
1518
- schemaVersion: 1,
1519
- type: "bot/unassign-capability",
1520
- commandId: "unassign-a",
1521
- botId: "primary",
1522
- expectedRevision: 3,
1523
- assignmentId: "mail-a",
1524
- });
610
+ await contribution.executeConfiguration(
611
+ request({
612
+ ...first,
613
+ commandId: "update-model-only",
614
+ expectedRevision: 1,
615
+ values: { model: model("flock-ai-ambient", "new-bot-model") },
616
+ }),
617
+ );
1525
618
  expect(await contribution.getSettings(identity)).toMatchObject({
1526
- revision: 4,
1527
- assignments: [
1528
- {
1529
- assignmentId: "mail-b",
1530
- connectionId: "gmail-1",
1531
- state: "enabled",
1532
- },
1533
- ],
1534
- });
1535
- expect(dependencies.get("assign-b")).toBe("acknowledged");
1536
- });
1537
-
1538
- test("settles a committed assignment saga before replaying its receipt", async () => {
1539
- const storage = new MemoryStorage();
1540
- let acknowledgementAttempts = 0;
1541
- let acknowledged = false;
1542
- let dependencyStatus: "absent" | "claimed" | "acknowledged" = "absent";
1543
- const userConfiguration = {
1544
- readConfiguration: () => Promise.resolve(installedUser()),
1545
- executeConnectionDependency: (request: { action: string }) => {
1546
- if (request.action === "read") {
1547
- return Promise.resolve({
1548
- schemaVersion: 1 as const,
1549
- status: dependencyStatus,
1550
- });
1551
- }
1552
- if (request.action === "claim") {
1553
- dependencyStatus = "claimed";
1554
- return Promise.resolve({
1555
- schemaVersion: 1 as const,
1556
- status: "claimed" as const,
1557
- });
1558
- }
1559
- if (request.action === "acknowledge") {
1560
- acknowledgementAttempts += 1;
1561
- if (acknowledgementAttempts <= 1) {
1562
- return Promise.reject(new Error("acknowledgement response lost"));
1563
- }
1564
- dependencyStatus = "acknowledged";
1565
- acknowledged = true;
1566
- return Promise.resolve({
1567
- schemaVersion: 1 as const,
1568
- status: "acknowledged" as const,
1569
- });
1570
- }
1571
- return Promise.resolve({
1572
- schemaVersion: 1 as const,
1573
- status: "released" as const,
1574
- });
1575
- },
1576
- };
1577
- const contribution = createShellBotBackendContribution({
1578
- state: { storage } as unknown as DurableObjectState,
1579
- env: {
1580
- USER_CONFIGURATIONS: {
1581
- idFromName: () => "user-1",
1582
- get: () => userConfiguration,
619
+ revision: 2,
620
+ packageValues: {
621
+ "custom-models": {
622
+ model: model("flock-ai-ambient", "new-bot-model"),
623
+ tone: "concise",
1583
624
  },
1584
- } as never,
1585
- compileApplication: compileAssignmentTestApplication,
1586
- });
1587
- await contribution.materializeSettings(
1588
- { userId: "user-1", botId: "primary" },
1589
- { name: "Primary" },
1590
- );
1591
- const command = assignmentCommand("lost-assignment-response", {
1592
- packageId: "composio",
1593
- capabilityId: "gmail-tools",
1594
- connectionId: "gmail-1",
1595
- });
1596
- const execute = () =>
1597
- contribution.executeConfiguration({
1598
- schemaVersion: 1,
1599
- userId: "user-1",
1600
- botId: "primary",
1601
- command,
1602
- });
1603
-
1604
- await expect(execute()).resolves.toEqual({
1605
- schemaVersion: 1,
1606
- commandId: "lost-assignment-response",
1607
- status: "pending",
1608
- revision: 0,
1609
- });
1610
- expect(acknowledgementAttempts).toBe(1);
1611
- expect(acknowledged).toBe(false);
1612
-
1613
- await expect(execute()).resolves.toMatchObject({
1614
- commandId: "lost-assignment-response",
1615
- status: "applied",
1616
- revision: 1,
1617
- });
1618
- expect(acknowledgementAttempts).toBe(2);
1619
- expect(acknowledged).toBe(true);
1620
- });
1621
- });
1622
-
1623
- describe("User default model", () => {
1624
- test("claims one durable Assignment for a Bot that follows the default", async () => {
1625
- const storage = new MemoryStorage();
1626
- let dependencyClaims = 0;
1627
- let dependencyAcknowledgements = 0;
1628
- let dependencyStatus: "absent" | "claimed" | "acknowledged" = "absent";
1629
- const user: UserSettingsViewV1 = {
1630
- ...installedUser(),
1631
- newBotModelTemplate: {
1632
- connectionId: "gmail-1",
1633
- providerModelId: "fixture-model",
1634
625
  },
1635
- newBotModelTemplateSource: "auto",
1636
- };
1637
- const userConfiguration = {
1638
- readConfiguration: () => Promise.resolve(structuredClone(user)),
1639
- executeConnectionDependency: (request: { action: string }) => {
1640
- if (request.action === "read") {
1641
- return Promise.resolve({
1642
- schemaVersion: 1 as const,
1643
- status: dependencyStatus,
1644
- });
1645
- }
1646
- if (request.action === "claim") {
1647
- dependencyClaims += 1;
1648
- dependencyStatus = "claimed";
1649
- return Promise.resolve({
1650
- schemaVersion: 1 as const,
1651
- status: "claimed" as const,
1652
- });
1653
- }
1654
- if (request.action === "acknowledge") {
1655
- dependencyAcknowledgements += 1;
1656
- dependencyStatus = "acknowledged";
1657
- return Promise.resolve({
1658
- schemaVersion: 1 as const,
1659
- status: "acknowledged" as const,
1660
- });
1661
- }
1662
- dependencyStatus = "absent";
1663
- return Promise.resolve({
1664
- schemaVersion: 1 as const,
1665
- status: "released" as const,
1666
- });
1667
- },
1668
- };
1669
- const contribution = createShellBotBackendContribution({
1670
- state: { storage } as unknown as DurableObjectState,
1671
- env: {
1672
- USER_CONFIGURATIONS: {
1673
- idFromName: () => "user-1",
1674
- get: () => userConfiguration,
1675
- },
1676
- } as never,
1677
- compileApplication: compileAssignmentTestApplication,
1678
626
  });
1679
- const identity = { userId: "user-1", botId: "primary" };
1680
- await contribution.materializeSettings(identity, { name: "Primary" });
1681
-
1682
- const plan = await contribution.resolveConfiguration(identity);
1683
- expect(plan.model).toBeUndefined();
1684
- expect(plan.assignments).toMatchObject([
1685
- {
1686
- packageId: "composio",
1687
- capabilityId: "gmail-tools",
1688
- connectionId: "gmail-1",
1689
- state: "enabled",
1690
- },
1691
- ]);
1692
- expect(dependencyClaims).toBe(1);
1693
- expect(dependencyAcknowledgements).toBe(1);
1694
627
 
1695
- // The claim is made once: a Bot that already holds the Assignment keeps it
1696
- // and follows the default without further durable writes.
1697
- const settings = await contribution.readConfiguration({
1698
- schemaVersion: 1,
1699
- ...identity,
628
+ await contribution.executeConfiguration(
629
+ request({
630
+ ...first,
631
+ commandId: "unset-bot-model",
632
+ expectedRevision: 2,
633
+ values: undefined,
634
+ unset: ["model"],
635
+ }),
636
+ );
637
+ expect(await contribution.getSettings(identity)).toMatchObject({
638
+ revision: 3,
639
+ packageValues: { "custom-models": { tone: "concise" } },
1700
640
  });
1701
- await contribution.resolveConfiguration(identity);
1702
- expect(dependencyClaims).toBe(1);
1703
- expect(
1704
- await contribution.readConfiguration({ schemaVersion: 1, ...identity }),
1705
- ).toEqual(settings);
1706
- expect(settings.model).toBeUndefined();
1707
- });
1708
641
 
1709
- test("leaves a Bot unclaimed when the default Connection is unavailable", async () => {
1710
- const storage = new MemoryStorage();
1711
- let dependencyClaims = 0;
1712
- const userConfiguration = {
1713
- readConfiguration: () =>
1714
- Promise.resolve({
1715
- ...installedUser(),
1716
- connections: [
1717
- { ...installedUser().connections[0]!, state: "revoked" as const },
1718
- ],
1719
- newBotModelTemplate: {
1720
- connectionId: "gmail-1",
1721
- providerModelId: "fixture-model",
1722
- },
1723
- newBotModelTemplateSource: "auto",
642
+ await expect(
643
+ contribution.executeConfiguration(
644
+ request({
645
+ ...first,
646
+ commandId: "unset-unknown",
647
+ expectedRevision: 3,
648
+ values: undefined,
649
+ unset: ["unknown-setting"],
1724
650
  }),
1725
- executeConnectionDependency: (request: { action: string }) => {
1726
- if (request.action === "read") {
1727
- return Promise.resolve({
1728
- schemaVersion: 1 as const,
1729
- status: "absent" as const,
1730
- });
1731
- }
1732
- if (request.action === "claim") dependencyClaims += 1;
1733
- return Promise.resolve({
1734
- schemaVersion: 1 as const,
1735
- status: "unauthorized" as const,
1736
- });
1737
- },
1738
- };
1739
- const contribution = createShellBotBackendContribution({
1740
- state: { storage } as unknown as DurableObjectState,
1741
- env: {
1742
- USER_CONFIGURATIONS: {
1743
- idFromName: () => "user-1",
1744
- get: () => userConfiguration,
1745
- },
1746
- } as never,
1747
- compileApplication: compileAssignmentTestApplication,
1748
- });
1749
- const identity = { userId: "user-1", botId: "primary" };
1750
- await contribution.materializeSettings(identity, { name: "Primary" });
651
+ ),
652
+ ).rejects.toThrow(/not declared by this Package/);
1751
653
 
1752
- expect(
1753
- (await contribution.resolveConfiguration(identity)).assignments,
1754
- ).toEqual([]);
1755
- expect(dependencyClaims).toBe(0);
654
+ await expect(
655
+ contribution.executeConfiguration(
656
+ request({
657
+ ...first,
658
+ commandId: "stale-revision",
659
+ expectedRevision: 2,
660
+ }),
661
+ ),
662
+ ).rejects.toThrow("configuration revision is 3");
663
+ await expect(
664
+ contribution.executeConfiguration(
665
+ request({
666
+ ...first,
667
+ values: { tone: "different" },
668
+ }),
669
+ ),
670
+ ).rejects.toThrow("reused for a different command");
1756
671
  });
1757
672
  });