@frockbot/configuration-core 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/src/index.test.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import {
3
- capabilityAssignmentFailureV1,
4
3
  ConfigurationDecodeError,
5
4
  decodeBotConfigurationExecuteRpcV1,
6
5
  decodeBotConfigurationReadRpcV1,
@@ -10,7 +9,6 @@ import {
10
9
  decodeCompositionGenerationListViewV1,
11
10
  decodeCompositionGenerationViewV1,
12
11
  decodeRevertCompositionCommandV1,
13
- decodeConnectionDependencyRequirementV1,
14
12
  decodeConfigurationCommandV1,
15
13
  decodeConfigurationQueryV1,
16
14
  decodeOperationReceiptV1,
@@ -20,10 +18,14 @@ import {
20
18
  decodeUserConfigurationReadRpcV1,
21
19
  decodeUserSettingsViewV1,
22
20
  initializeBotSettingsV1,
21
+ migrateStoredBotSettingsV1,
22
+ migrateStoredUserSettingsV1,
23
+ modelBindingFailureV1,
23
24
  resolveBotExecutionPlanV1,
24
25
  resolveBotModelBindingV1,
25
26
  resolveEffectiveBotModelV1,
26
- type ModelAssignment,
27
+ type ExecutionPackageDefinition,
28
+ type ModelBindingV1,
27
29
  } from "./index.js";
28
30
  import {
29
31
  isApplicationDeploymentHash,
@@ -32,6 +34,190 @@ import {
32
34
  isRpcIdentifier,
33
35
  } from "./identifiers.js";
34
36
 
37
+ describe("stored configuration migrations", () => {
38
+ test("migrates the pre-account-wide User record without honouring removed features", () => {
39
+ // Literal durable shape from eb0283edcce5daea976a21a9f6a6414bedc6e2bc,
40
+ // the first parent of PR #134's merge commit.
41
+ const stored = {
42
+ schemaVersion: 1,
43
+ revision: 7,
44
+ profile: { name: "Existing User" },
45
+ packages: [
46
+ {
47
+ packageId: "provider-ollama-cloud",
48
+ version: "0.0.1",
49
+ state: "installed" as const,
50
+ values: { "web-search-max-results": 5 },
51
+ },
52
+ ],
53
+ connections: [
54
+ {
55
+ connectionId: "ollama-1",
56
+ packageId: "provider-ollama-cloud",
57
+ connectionTypeId: "ollama-cloud-account",
58
+ displayName: "Work",
59
+ state: "ready" as const,
60
+ safeMetadata: {
61
+ region: "au",
62
+ dependentAssignments: [
63
+ {
64
+ botId: "bot-1",
65
+ generation: "assignment-1",
66
+ packageId: "provider-ollama-cloud",
67
+ capabilityId: "ollama-cloud-models",
68
+ claimOrder: 7,
69
+ status: "acknowledged",
70
+ },
71
+ ],
72
+ },
73
+ },
74
+ ],
75
+ newBotModelTemplate: {
76
+ connectionId: "ollama-1",
77
+ providerModelId: "glm-5.3-flash:cloud",
78
+ },
79
+ newBotModelTemplateSource: "user",
80
+ };
81
+
82
+ expect(
83
+ decodeUserSettingsViewV1(migrateStoredUserSettingsV1(stored)),
84
+ ).toEqual({
85
+ schemaVersion: 1,
86
+ revision: 7,
87
+ profile: { name: "Existing User" },
88
+ packages: stored.packages,
89
+ connections: [
90
+ {
91
+ ...stored.connections[0],
92
+ safeMetadata: { region: "au" },
93
+ },
94
+ ],
95
+ });
96
+ expect(stored.connections[0]?.safeMetadata).toHaveProperty(
97
+ "dependentAssignments",
98
+ );
99
+ });
100
+
101
+ test("keeps unknown User, Package, and Connection fields strict", () => {
102
+ const current = {
103
+ schemaVersion: 1,
104
+ revision: 0,
105
+ profile: { name: "Existing User" },
106
+ packages: [],
107
+ connections: [],
108
+ };
109
+ for (const stored of [
110
+ { ...current, unknown: true },
111
+ {
112
+ ...current,
113
+ packages: [
114
+ {
115
+ packageId: "settings",
116
+ version: "0.0.1",
117
+ state: "installed",
118
+ unknown: true,
119
+ },
120
+ ],
121
+ },
122
+ {
123
+ ...current,
124
+ connections: [
125
+ {
126
+ connectionId: "connection-1",
127
+ packageId: "provider-ollama-cloud",
128
+ connectionTypeId: "ollama-cloud-account",
129
+ displayName: "Work",
130
+ state: "ready",
131
+ safeMetadata: {},
132
+ unknown: true,
133
+ },
134
+ ],
135
+ },
136
+ ]) {
137
+ expect(() =>
138
+ decodeUserSettingsViewV1(migrateStoredUserSettingsV1(stored)),
139
+ ).toThrow(ConfigurationDecodeError);
140
+ }
141
+ expect(migrateStoredUserSettingsV1(current)).toBe(current);
142
+ });
143
+
144
+ test("migrates the pre-account-wide Bot record to an empty Package value bag", () => {
145
+ // Literal durable shape from eb0283edcce5daea976a21a9f6a6414bedc6e2bc,
146
+ // the first parent of PR #134's merge commit.
147
+ const stored = {
148
+ schemaVersion: 1,
149
+ botId: "primary",
150
+ revision: 4,
151
+ profile: { name: "Primary" },
152
+ notifications: { enabled: true },
153
+ assignments: [
154
+ {
155
+ assignmentId: "ollama-model",
156
+ packageId: "provider-ollama-cloud",
157
+ capabilityId: "ollama-cloud-models",
158
+ connectionId: "ollama-1",
159
+ state: "enabled",
160
+ },
161
+ ],
162
+ assignmentOperations: [
163
+ {
164
+ commandId: "replace-model",
165
+ kind: "replacing",
166
+ assignmentId: "ollama-model",
167
+ state: "retrying",
168
+ target: {
169
+ assignmentId: "ollama-model",
170
+ packageId: "provider-ollama-cloud",
171
+ capabilityId: "ollama-cloud-models",
172
+ connectionId: "ollama-1",
173
+ },
174
+ },
175
+ ],
176
+ model: {
177
+ connectionId: "ollama-1",
178
+ providerModelId: "glm-5.3-flash:cloud",
179
+ },
180
+ };
181
+
182
+ expect(decodeBotSettingsViewV1(migrateStoredBotSettingsV1(stored))).toEqual(
183
+ {
184
+ schemaVersion: 1,
185
+ botId: "primary",
186
+ revision: 4,
187
+ profile: { name: "Primary" },
188
+ notifications: { enabled: true },
189
+ packageValues: {},
190
+ },
191
+ );
192
+ });
193
+
194
+ test("keeps unknown Bot fields strict and current records unchanged", () => {
195
+ const current = initializeBotSettingsV1("primary");
196
+ expect(migrateStoredBotSettingsV1(current)).toBe(current);
197
+ const { packageValues: _packageValues, ...withoutPackageValues } = current;
198
+ expect(
199
+ decodeBotSettingsViewV1(
200
+ migrateStoredBotSettingsV1({
201
+ ...withoutPackageValues,
202
+ modelAssignment: {
203
+ connectionId: "ollama-1",
204
+ providerModelId: "glm-5.3-flash:cloud",
205
+ },
206
+ }),
207
+ ),
208
+ ).toEqual(current);
209
+ expect(() =>
210
+ decodeBotSettingsViewV1(
211
+ migrateStoredBotSettingsV1({
212
+ ...current,
213
+ assignments: [],
214
+ unknown: true,
215
+ }),
216
+ ),
217
+ ).toThrow(ConfigurationDecodeError);
218
+ });
219
+ });
220
+
35
221
  describe("shared public identifier policy", () => {
36
222
  test("accepts the bounded cross-runtime identifier grammar", () => {
37
223
  expect(isPublicIdentifier("Bot_1.release-candidate")).toBe(true);
@@ -100,41 +286,74 @@ describe("configuration DTO seam", () => {
100
286
  });
101
287
  });
102
288
 
103
- test("decodes exact Assign, Replace, and Unassign commands", () => {
289
+ test("decodes exact Bot-scoped Package setting commands", () => {
104
290
  const meta = {
105
291
  schemaVersion: 1,
106
292
  commandId: "operation-1",
107
293
  expectedRevision: 2,
108
294
  botId: "primary",
109
295
  } as const;
110
- const assignment = {
111
- assignmentId: "mail",
112
- packageId: "mail",
113
- capabilityId: "send",
114
- connectionId: "mail-1",
115
- };
116
296
  expect(
117
297
  decodeConfigurationCommandV1({
118
298
  ...meta,
119
- type: "bot/replace-capability",
120
- assignment,
299
+ type: "bot/set-package-settings",
300
+ packageId: "custom-models",
301
+ values: {
302
+ model: {
303
+ connectionId: "ollama-work",
304
+ providerModelId: "glm-5.3-flash:cloud",
305
+ },
306
+ },
121
307
  }),
122
- ).toMatchObject({ type: "bot/replace-capability", assignment });
123
- expect(
308
+ ).toMatchObject({
309
+ type: "bot/set-package-settings",
310
+ packageId: "custom-models",
311
+ values: { model: { connectionId: "ollama-work" } },
312
+ });
313
+ expect(() =>
124
314
  decodeConfigurationCommandV1({
125
315
  ...meta,
126
- type: "bot/unassign-capability",
127
- assignmentId: "mail",
316
+ type: "bot/set-package-settings",
317
+ packageId: "custom-models",
318
+ values: {},
128
319
  }),
129
- ).toMatchObject({ type: "bot/unassign-capability", assignmentId: "mail" });
130
- expect(() =>
320
+ ).toThrow("values names no setting");
321
+ expect(
131
322
  decodeConfigurationCommandV1({
132
323
  ...meta,
133
- type: "bot/unassign-capability",
134
- assignmentId: "mail",
135
- assignment,
324
+ type: "bot/set-package-settings",
325
+ packageId: "custom-models",
326
+ unset: ["model"],
136
327
  }),
137
- ).toThrow(ConfigurationDecodeError);
328
+ ).toMatchObject({ unset: ["model"] });
329
+ for (const invalid of [
330
+ { ...meta, type: "bot/set-package-settings", packageId: "custom-models" },
331
+ {
332
+ ...meta,
333
+ type: "bot/set-package-settings",
334
+ packageId: "custom-models",
335
+ unset: [],
336
+ },
337
+ {
338
+ ...meta,
339
+ type: "bot/set-package-settings",
340
+ packageId: "custom-models",
341
+ unset: ["model", "model"],
342
+ },
343
+ {
344
+ ...meta,
345
+ type: "bot/set-package-settings",
346
+ packageId: "custom-models",
347
+ values: {
348
+ model: { connectionId: "ollama-work", providerModelId: "m" },
349
+ },
350
+ unset: ["model"],
351
+ },
352
+ ]) {
353
+ expect(() => decodeConfigurationCommandV1(invalid)).toThrow(
354
+ ConfigurationDecodeError,
355
+ );
356
+ }
138
357
  });
139
358
 
140
359
  test("accepts provider model IDs through the catalog limit", () => {
@@ -143,49 +362,43 @@ describe("configuration DTO seam", () => {
143
362
  expect(
144
363
  decodeConfigurationCommandV1({
145
364
  schemaVersion: 1,
146
- type: "bot/select-model",
365
+ type: "user/set-platform-model",
147
366
  commandId: "command-model",
148
- botId: "primary",
149
367
  expectedRevision: 3,
150
368
  model: { connectionId: "connection-1", providerModelId },
151
369
  }),
152
370
  ).toMatchObject({ model: { providerModelId } });
153
371
  });
154
372
 
155
- test("decodes atomic model binding and explicit unbinding commands", () => {
373
+ test("decodes the provider-bootstrap platform model command", () => {
156
374
  expect(
157
375
  decodeConfigurationCommandV1({
158
376
  schemaVersion: 1,
159
- type: "bot/assign-capability",
160
- commandId: "bind-model",
161
- botId: "primary",
377
+ type: "user/set-platform-model",
378
+ commandId: "bootstrap-model",
162
379
  expectedRevision: 2,
163
- assignment: {
164
- assignmentId: "ollama-model",
165
- packageId: "provider-ollama-cloud",
166
- capabilityId: "ollama-cloud-models",
167
- connectionId: "ollama-work",
168
- },
169
380
  model: {
170
381
  connectionId: "ollama-work",
171
382
  providerModelId: "glm-5.3-flash:cloud",
172
383
  },
173
384
  }),
174
385
  ).toMatchObject({
175
- type: "bot/assign-capability",
176
- assignment: { connectionId: "ollama-work" },
386
+ type: "user/set-platform-model",
177
387
  model: { providerModelId: "glm-5.3-flash:cloud" },
178
388
  });
179
- expect(
389
+ expect(() =>
180
390
  decodeConfigurationCommandV1({
181
391
  schemaVersion: 1,
182
- type: "bot/unbind-model",
183
- commandId: "unbind-model",
184
- botId: "primary",
392
+ type: "user/set-platform-model",
393
+ commandId: "bootstrap-model",
185
394
  expectedRevision: 3,
186
- assignmentId: "ollama-model",
395
+ model: {
396
+ connectionId: "ollama-work",
397
+ providerModelId: "glm-5.3-flash:cloud",
398
+ extra: true,
399
+ },
187
400
  }),
188
- ).toMatchObject({ type: "bot/unbind-model", assignmentId: "ollama-model" });
401
+ ).toThrow(ConfigurationDecodeError);
189
402
  });
190
403
 
191
404
  test("rejects unversioned, malformed, and unknown commands", () => {
@@ -207,30 +420,6 @@ describe("configuration DTO seam", () => {
207
420
  }
208
421
  });
209
422
 
210
- test("requires a source for new-Bot defaults and forbids an empty automatic choice", () => {
211
- const command = {
212
- schemaVersion: 1,
213
- type: "user/set-new-bot-model",
214
- commandId: "choose-default",
215
- expectedRevision: 0,
216
- } as const;
217
- expect(() =>
218
- decodeConfigurationCommandV1({
219
- ...command,
220
- model: { connectionId: "connection-1", providerModelId: "model-1" },
221
- }),
222
- ).toThrow("invalid fields");
223
- expect(() =>
224
- decodeConfigurationCommandV1({ ...command, source: "provider" }),
225
- ).toThrow("source must be user or auto");
226
- expect(() =>
227
- decodeConfigurationCommandV1({ ...command, source: "auto" }),
228
- ).toThrow("must name a model");
229
- expect(
230
- decodeConfigurationCommandV1({ ...command, source: "user" }),
231
- ).toMatchObject({ source: "user", model: undefined });
232
- });
233
-
234
423
  test("rejects unknown fields throughout configuration commands", () => {
235
424
  const meta = {
236
425
  schemaVersion: 1,
@@ -245,8 +434,7 @@ describe("configuration DTO seam", () => {
245
434
  },
246
435
  {
247
436
  ...meta,
248
- type: "user/set-new-bot-model",
249
- source: "user",
437
+ type: "user/set-platform-model",
250
438
  model: {
251
439
  connectionId: "provider-1",
252
440
  providerModelId: "model-1",
@@ -261,14 +449,11 @@ describe("configuration DTO seam", () => {
261
449
  },
262
450
  {
263
451
  ...meta,
264
- type: "bot/assign-capability",
452
+ type: "bot/set-package-settings",
265
453
  botId: "primary",
266
- assignment: {
267
- assignmentId: "gmail",
268
- packageId: "composio",
269
- capabilityId: "gmail-tools",
270
- extra: true,
271
- },
454
+ packageId: "custom-models",
455
+ values: { model: "fast" },
456
+ extra: true,
272
457
  },
273
458
  {
274
459
  ...meta,
@@ -303,45 +488,6 @@ describe("configuration DTO seam", () => {
303
488
  }
304
489
  });
305
490
 
306
- test("decodes versioned Connection dependency requirements", () => {
307
- expect(
308
- decodeConnectionDependencyRequirementV1({
309
- schemaVersion: 1,
310
- packageId: "composio",
311
- packageVersion: "0.0.1",
312
- capabilityId: "gmail-tools",
313
- connectionTypeIds: ["gmail"],
314
- }),
315
- ).toEqual({
316
- schemaVersion: 1,
317
- packageId: "composio",
318
- packageVersion: "0.0.1",
319
- capabilityId: "gmail-tools",
320
- connectionTypeIds: ["gmail"],
321
- });
322
- for (const value of [
323
- {
324
- schemaVersion: 1,
325
- packageId: "composio",
326
- packageVersion: "0.0.1",
327
- capabilityId: "gmail-tools",
328
- connectionTypeIds: [],
329
- },
330
- {
331
- schemaVersion: 1,
332
- packageId: "composio",
333
- packageVersion: "0.0.1",
334
- capabilityId: "gmail-tools",
335
- connectionTypeIds: ["gmail"],
336
- extra: true,
337
- },
338
- ]) {
339
- expect(() => decodeConnectionDependencyRequirementV1(value)).toThrow(
340
- ConfigurationDecodeError,
341
- );
342
- }
343
- });
344
-
345
491
  test("strictly decodes versioned Connection commands", () => {
346
492
  expect(
347
493
  decodeStartConnectionCommandV1({
@@ -388,20 +534,20 @@ describe("configuration DTO seam", () => {
388
534
  }
389
535
  });
390
536
 
391
- test("rejects hidden and symbol fields across Assignment seams", () => {
392
- const assignment = {
393
- assignmentId: "mail",
394
- packageId: "mail",
395
- capabilityId: "send",
396
- connectionId: "mail-1",
397
- };
537
+ test("rejects hidden and symbol fields across Package-setting seams", () => {
398
538
  const command = {
399
539
  schemaVersion: 1,
400
- type: "bot/assign-capability",
540
+ type: "bot/set-package-settings",
401
541
  commandId: "command-1",
402
542
  expectedRevision: 0,
403
543
  botId: "primary",
404
- assignment,
544
+ packageId: "custom-models",
545
+ values: {
546
+ model: {
547
+ connectionId: "mail-1",
548
+ providerModelId: "model-1",
549
+ },
550
+ },
405
551
  };
406
552
  const receipt = {
407
553
  schemaVersion: 1,
@@ -415,16 +561,14 @@ describe("configuration DTO seam", () => {
415
561
  revision: 0,
416
562
  profile: { name: "Primary" },
417
563
  notifications: { enabled: false },
418
- assignments: [],
419
- assignmentOperations: [
420
- {
421
- commandId: "command-1",
422
- kind: "assigning",
423
- assignmentId: "mail",
424
- state: "retrying",
425
- target: assignment,
564
+ packageValues: {
565
+ "custom-models": {
566
+ model: {
567
+ connectionId: "mail-1",
568
+ providerModelId: "model-1",
569
+ },
426
570
  },
427
- ],
571
+ },
428
572
  };
429
573
  const rpc = {
430
574
  schemaVersion: 1,
@@ -434,7 +578,6 @@ describe("configuration DTO seam", () => {
434
578
  };
435
579
  for (const [decode, candidate] of [
436
580
  [decodeConfigurationCommandV1, command],
437
- [decodeConfigurationCommandV1, { ...command, assignment }],
438
581
  [decodeOperationReceiptV1, receipt],
439
582
  [decodeBotSettingsViewV1, view],
440
583
  [decodeBotConfigurationExecuteRpcV1, rpc],
@@ -445,20 +588,28 @@ describe("configuration DTO seam", () => {
445
588
  const symbol = { ...candidate, [Symbol("extra")]: true };
446
589
  expect(() => decode(symbol)).toThrow(ConfigurationDecodeError);
447
590
  }
448
- const hiddenTarget = { ...assignment };
449
- Object.defineProperty(hiddenTarget, "hidden", { value: true });
591
+ const hiddenValues = { ...command.values };
592
+ Object.defineProperty(hiddenValues, "hidden", { value: true });
450
593
  expect(() =>
451
- decodeConfigurationCommandV1({ ...command, assignment: hiddenTarget }),
594
+ decodeConfigurationCommandV1({ ...command, values: hiddenValues }),
452
595
  ).toThrow(ConfigurationDecodeError);
453
- const symbolTarget = { ...assignment, [Symbol("extra")]: true };
596
+ const symbolModel = {
597
+ ...command.values.model,
598
+ [Symbol("extra")]: true,
599
+ };
454
600
  expect(() =>
455
601
  decodeBotSettingsViewV1({
456
602
  ...view,
457
- assignmentOperations: [
458
- { ...view.assignmentOperations[0], target: symbolTarget },
459
- ],
603
+ packageValues: {
604
+ "custom-models": { model: symbolModel },
605
+ },
460
606
  }),
461
607
  ).toThrow(ConfigurationDecodeError);
608
+ const pollutedValues = Object.create({ inherited: true });
609
+ pollutedValues.model = command.values.model;
610
+ expect(() =>
611
+ decodeConfigurationCommandV1({ ...command, values: pollutedValues }),
612
+ ).toThrow(ConfigurationDecodeError);
462
613
  });
463
614
 
464
615
  test("rejects unknown configuration RPC envelope fields", () => {
@@ -684,17 +835,9 @@ describe("configuration DTO seam", () => {
684
835
  });
685
836
 
686
837
  describe("Bot execution-plan authority", () => {
687
- test("copies the User model template only when Bot settings initialize", () => {
688
- const template = {
689
- connectionId: "provider-1",
690
- providerModelId: "model-1",
691
- };
692
- const initialized = initializeBotSettingsV1("primary", template);
693
- template.providerModelId = "model-2";
694
- expect(initialized.model).toEqual({
695
- connectionId: "provider-1",
696
- providerModelId: "model-1",
697
- });
838
+ test("initializes Bot settings without seeding a model choice", () => {
839
+ const initialized = initializeBotSettingsV1("primary");
840
+ expect(initialized.packageValues).toEqual({});
698
841
  expect(initialized.notifications).toEqual({ enabled: true });
699
842
  });
700
843
 
@@ -704,28 +847,44 @@ describe("Bot execution-plan authority", () => {
704
847
  revision: 4,
705
848
  profile: { name: "Primary" },
706
849
  notifications: { enabled: false },
707
- assignments: [
708
- {
709
- assignmentId: "gmail-assignment",
710
- packageId: "composio",
711
- capabilityId: "gmail-tools",
712
- connectionId: "gmail-connection",
713
- state: "enabled" as const,
714
- },
715
- ],
716
- assignmentOperations: [],
850
+ packageValues: {},
717
851
  };
718
- const packages = [
852
+ const packages: ExecutionPackageDefinition[] = [
719
853
  {
720
854
  packageId: "composio",
721
855
  version: "0.0.1",
722
- capabilities: [{ id: "gmail-tools", connectionTypes: ["gmail"] }],
856
+ settings: [],
857
+ capabilities: [
858
+ { id: "clock", kind: "tool", connectionTypes: [] },
859
+ { id: "gmail-tools", kind: "tool", connectionTypes: ["gmail"] },
860
+ ],
723
861
  connectionTypes: [{ id: "gmail", capabilities: ["gmail-tools"] }],
724
862
  },
725
863
  ];
726
864
 
727
- test("resolves a Bot model through its explicit ready Connection", () => {
728
- const user = {
865
+ const modelPackages: ExecutionPackageDefinition[] = [
866
+ {
867
+ packageId: "provider-ollama-cloud",
868
+ version: "0.0.1",
869
+ settings: [],
870
+ capabilities: [
871
+ {
872
+ id: "ollama-cloud-models",
873
+ kind: "model",
874
+ connectionTypes: ["ollama-cloud-account"],
875
+ },
876
+ ],
877
+ connectionTypes: [
878
+ {
879
+ id: "ollama-cloud-account",
880
+ capabilities: ["ollama-cloud-models"],
881
+ },
882
+ ],
883
+ },
884
+ ];
885
+
886
+ function modelUser() {
887
+ return {
729
888
  schemaVersion: 1 as const,
730
889
  revision: 1,
731
890
  profile: { name: "User" },
@@ -765,45 +924,16 @@ describe("Bot execution-plan authority", () => {
765
924
  },
766
925
  ],
767
926
  };
768
- const modelPackages = [
769
- {
770
- packageId: "provider-ollama-cloud",
771
- version: "0.0.1",
772
- capabilities: [
773
- {
774
- id: "ollama-cloud-models",
775
- kind: "model" as const,
776
- connectionTypes: ["ollama-cloud-account"],
777
- },
778
- ],
779
- connectionTypes: [
780
- {
781
- id: "ollama-cloud-account",
782
- capabilities: ["ollama-cloud-models"],
783
- },
784
- ],
785
- },
786
- ];
787
- const assignments = [
788
- {
789
- assignmentId: "ollama-model",
790
- packageId: "provider-ollama-cloud",
791
- capabilityId: "ollama-cloud-models",
792
- connectionId: "ollama-work",
793
- state: "enabled" as const,
794
- },
795
- ];
927
+ }
796
928
 
929
+ test("resolves a model through User enablement and its ready Connection", () => {
930
+ const user = modelUser();
931
+ const model = {
932
+ connectionId: "ollama-work",
933
+ providerModelId: "glm-5.3-flash:cloud",
934
+ };
797
935
  expect(
798
- resolveBotModelBindingV1({
799
- model: {
800
- connectionId: "ollama-work",
801
- providerModelId: "glm-5.3-flash:cloud",
802
- },
803
- assignments,
804
- user,
805
- packages: modelPackages,
806
- }),
936
+ resolveBotModelBindingV1({ model, user, packages: modelPackages }),
807
937
  ).toMatchObject({
808
938
  state: "ready",
809
939
  providerType: "ollama-cloud",
@@ -811,43 +941,53 @@ describe("Bot execution-plan authority", () => {
811
941
  });
812
942
  expect(
813
943
  resolveBotModelBindingV1({
814
- model: {
815
- connectionId: "ollama-work",
816
- providerModelId: "glm-5.3-flash:cloud",
817
- },
818
- assignments,
819
- user,
820
- packages: modelPackages.map((pkg) => ({ ...pkg, version: "0.0.2" })),
821
- }).state,
822
- ).toBe("unavailable");
823
- expect(
824
- resolveBotModelBindingV1({
825
- model: {
826
- connectionId: "ollama-work",
827
- providerModelId: "new-model:cloud",
828
- },
829
- assignments,
944
+ model: { ...model, providerModelId: "new-model:cloud" },
830
945
  user,
831
946
  packages: modelPackages,
832
947
  }).state,
833
948
  ).toBe("requires-resolution");
949
+ });
950
+
951
+ test("disabling the Package or revoking the Connection fails every Bot closed", () => {
952
+ const model = {
953
+ connectionId: "ollama-work",
954
+ providerModelId: "glm-5.3-flash:cloud",
955
+ };
956
+ const user = {
957
+ ...modelUser(),
958
+ packages: [{ ...modelUser().packages[0]!, state: "disabled" as const }],
959
+ };
834
960
  expect(
835
- resolveBotModelBindingV1({
836
- model: {
837
- connectionId: "ollama-work",
838
- providerModelId: "glm-5.3-flash:cloud",
839
- },
840
- assignments: [],
841
- user,
842
- packages: modelPackages,
843
- }),
961
+ modelBindingFailureV1({ model, user, packages: modelPackages }),
962
+ ).toContain('Package "provider-ollama-cloud" is not installed and enabled');
963
+ expect(
964
+ resolveBotModelBindingV1({ model, user, packages: modelPackages }),
844
965
  ).toMatchObject({
845
966
  state: "unavailable",
846
- failure: "Bot is not assigned the Connection model capability",
967
+ failure: expect.stringContaining("enable it"),
847
968
  });
969
+
970
+ const revoked = {
971
+ ...modelUser(),
972
+ connections: [
973
+ { ...modelUser().connections[0]!, state: "revoked" as const },
974
+ ],
975
+ };
976
+ for (const _botId of ["bot-one", "bot-two"]) {
977
+ expect(
978
+ resolveBotModelBindingV1({
979
+ model,
980
+ user: revoked,
981
+ packages: modelPackages,
982
+ }),
983
+ ).toMatchObject({
984
+ state: "unavailable",
985
+ failure: expect.stringContaining("reconnect it"),
986
+ });
987
+ }
848
988
  });
849
989
 
850
- test("requires an enabled installation, declared capability, and ready typed Connection", () => {
990
+ test("projects the enabled User capability set in stable identity order", () => {
851
991
  const user = {
852
992
  schemaVersion: 1 as const,
853
993
  revision: 2,
@@ -861,42 +1001,44 @@ describe("Bot execution-plan authority", () => {
861
1001
  ],
862
1002
  connections: [
863
1003
  {
864
- connectionId: "gmail-connection",
1004
+ connectionId: "gmail-z",
1005
+ packageId: "composio",
1006
+ connectionTypeId: "gmail",
1007
+ displayName: "Gmail Z",
1008
+ state: "ready" as const,
1009
+ safeMetadata: {},
1010
+ },
1011
+ {
1012
+ connectionId: "gmail-a",
865
1013
  packageId: "composio",
866
1014
  connectionTypeId: "gmail",
867
- displayName: "Gmail",
1015
+ displayName: "Gmail A",
868
1016
  state: "ready" as const,
869
1017
  safeMetadata: {},
870
1018
  },
871
1019
  ],
872
1020
  };
873
1021
  expect(
874
- resolveBotExecutionPlanV1({ bot, user, packages }).assignments[0]?.state,
875
- ).toBe("enabled");
876
- expect(
877
- resolveBotExecutionPlanV1({
878
- bot: {
879
- ...bot,
880
- assignments: [{ ...bot.assignments[0]!, capabilityId: "anything" }],
881
- },
882
- user,
883
- packages,
884
- }).assignments[0]?.state,
885
- ).toBe("unavailable");
886
- expect(
887
- capabilityAssignmentFailureV1({
888
- assignment: { ...bot.assignments[0]!, connectionId: undefined },
889
- user,
890
- packages,
891
- }),
892
- ).toContain("requires a Connection");
893
- expect(
894
- capabilityAssignmentFailureV1({
895
- assignment: { ...bot.assignments[0]!, capabilityId: "anything" },
896
- user,
897
- packages,
898
- }),
899
- ).toContain("is not declared");
1022
+ resolveBotExecutionPlanV1({ bot, user, packages }).capabilities,
1023
+ ).toEqual([
1024
+ {
1025
+ packageId: "composio",
1026
+ capabilityId: "clock",
1027
+ kind: "tool",
1028
+ },
1029
+ {
1030
+ packageId: "composio",
1031
+ capabilityId: "gmail-tools",
1032
+ kind: "tool",
1033
+ connectionId: "gmail-a",
1034
+ },
1035
+ {
1036
+ packageId: "composio",
1037
+ capabilityId: "gmail-tools",
1038
+ kind: "tool",
1039
+ connectionId: "gmail-z",
1040
+ },
1041
+ ]);
900
1042
  expect(
901
1043
  resolveBotExecutionPlanV1({
902
1044
  bot,
@@ -905,20 +1047,27 @@ describe("Bot execution-plan authority", () => {
905
1047
  packages: [{ ...user.packages[0]!, state: "disabled" }],
906
1048
  },
907
1049
  packages,
908
- }).assignments[0]?.state,
909
- ).toBe("unavailable");
1050
+ }).capabilities,
1051
+ ).toEqual([]);
910
1052
  expect(
911
1053
  resolveBotExecutionPlanV1({
912
1054
  bot,
913
1055
  user: {
914
1056
  ...user,
915
- connections: [
916
- { ...user.connections[0]!, connectionTypeId: "calendar" },
917
- ],
1057
+ connections: user.connections.map((connection) => ({
1058
+ ...connection,
1059
+ state: "revoked" as const,
1060
+ })),
918
1061
  },
919
1062
  packages,
920
- }).assignments[0]?.state,
921
- ).toBe("unavailable");
1063
+ }).capabilities,
1064
+ ).toEqual([
1065
+ {
1066
+ packageId: "composio",
1067
+ capabilityId: "clock",
1068
+ kind: "tool",
1069
+ },
1070
+ ]);
922
1071
  });
923
1072
  });
924
1073
 
@@ -1087,10 +1236,28 @@ describe("Composition generation views", () => {
1087
1236
  });
1088
1237
 
1089
1238
  describe("effective Bot model resolution", () => {
1090
- const modelPackages = [
1239
+ const platformModel: ModelBindingV1 = {
1240
+ connectionId: "ollama-work",
1241
+ providerModelId: "glm-5.3-flash:cloud",
1242
+ };
1243
+ const accountModel: ModelBindingV1 = {
1244
+ connectionId: "ollama-work",
1245
+ providerModelId: "llama-3:cloud",
1246
+ };
1247
+ const modelSchema = {
1248
+ type: "object" as const,
1249
+ properties: {
1250
+ connectionId: { type: "string" as const },
1251
+ providerModelId: { type: "string" as const },
1252
+ },
1253
+ required: ["connectionId", "providerModelId"],
1254
+ additionalProperties: false,
1255
+ };
1256
+ const modelPackages: ExecutionPackageDefinition[] = [
1091
1257
  {
1092
1258
  packageId: "provider-ollama-cloud",
1093
1259
  version: "0.0.1",
1260
+ settings: [],
1094
1261
  capabilities: [
1095
1262
  {
1096
1263
  id: "ollama-cloud-models",
@@ -1102,10 +1269,23 @@ describe("effective Bot model resolution", () => {
1102
1269
  { id: "ollama-cloud-account", capabilities: ["ollama-cloud-models"] },
1103
1270
  ],
1104
1271
  },
1272
+ {
1273
+ packageId: "custom-models",
1274
+ version: "0.0.1",
1275
+ settings: [
1276
+ {
1277
+ id: "model",
1278
+ schemaVersion: 1,
1279
+ scopes: ["user", "bot"],
1280
+ role: "model",
1281
+ schema: modelSchema,
1282
+ },
1283
+ ],
1284
+ capabilities: [],
1285
+ connectionTypes: [],
1286
+ },
1105
1287
  ];
1106
- function user(
1107
- newBotModelTemplate?: ModelAssignment,
1108
- ): Parameters<typeof resolveEffectiveBotModelV1>[0]["user"] {
1288
+ function user(): Parameters<typeof resolveEffectiveBotModelV1>[0]["user"] {
1109
1289
  return {
1110
1290
  schemaVersion: 1,
1111
1291
  revision: 1,
@@ -1147,82 +1327,129 @@ describe("effective Bot model resolution", () => {
1147
1327
  safeMetadata: {},
1148
1328
  },
1149
1329
  ],
1150
- ...(newBotModelTemplate
1151
- ? { newBotModelTemplate, newBotModelTemplateSource: "auto" as const }
1152
- : {}),
1330
+ platformModel,
1153
1331
  };
1154
1332
  }
1155
- const assignments = [
1156
- {
1157
- assignmentId: "ollama-model",
1158
- packageId: "provider-ollama-cloud",
1159
- capabilityId: "ollama-cloud-models",
1160
- connectionId: "ollama-work",
1161
- state: "enabled" as const,
1162
- },
1163
- ];
1164
1333
 
1165
- test("follows the User default when the Bot has no model of its own", () => {
1334
+ test("uses the platform model when the Bot and User configured nothing", () => {
1166
1335
  const effective = resolveEffectiveBotModelV1({
1167
- bot: { assignments },
1168
- user: user({
1169
- connectionId: "ollama-work",
1170
- providerModelId: "llama-3:cloud",
1171
- }),
1336
+ bot: { packageValues: {} },
1337
+ user: user(),
1172
1338
  packages: modelPackages,
1173
1339
  });
1174
- expect(effective.source).toBe("default");
1175
- expect(effective.model).toEqual({
1176
- connectionId: "ollama-work",
1177
- providerModelId: "llama-3:cloud",
1178
- });
1340
+ expect(effective.source).toBe("platform");
1341
+ expect(effective.model).toEqual(platformModel);
1179
1342
  expect(effective.binding?.state).toBe("ready");
1180
1343
  });
1181
1344
 
1182
- test("prefers the Bot override over the User default", () => {
1183
- const effective = resolveEffectiveBotModelV1({
1184
- bot: {
1185
- model: {
1186
- connectionId: "ollama-work",
1187
- providerModelId: "glm-5.3-flash:cloud",
1345
+ test("prefers an enabled Package's User value, then its Bot value", () => {
1346
+ const configured = {
1347
+ ...user(),
1348
+ packages: [
1349
+ ...user().packages,
1350
+ {
1351
+ packageId: "custom-models",
1352
+ version: "0.0.1",
1353
+ state: "installed" as const,
1354
+ values: { model: accountModel },
1188
1355
  },
1189
- assignments,
1190
- },
1191
- user: user({
1192
- connectionId: "ollama-work",
1193
- providerModelId: "llama-3:cloud",
1194
- }),
1356
+ ],
1357
+ };
1358
+ const account = resolveEffectiveBotModelV1({
1359
+ bot: { packageValues: {} },
1360
+ user: configured,
1195
1361
  packages: modelPackages,
1196
1362
  });
1197
- expect(effective.source).toBe("bot");
1198
- expect(effective.model?.providerModelId).toBe("glm-5.3-flash:cloud");
1199
- expect(effective.binding?.state).toBe("ready");
1363
+ expect(account.source).toBe("account");
1364
+ expect(account.model).toEqual(accountModel);
1365
+
1366
+ const bot = resolveEffectiveBotModelV1({
1367
+ bot: { packageValues: { "custom-models": { model: platformModel } } },
1368
+ user: configured,
1369
+ packages: modelPackages,
1370
+ });
1371
+ expect(bot.source).toBe("bot");
1372
+ expect(bot.model).toEqual(platformModel);
1373
+ expect(bot.binding?.state).toBe("ready");
1200
1374
  });
1201
1375
 
1202
- test("reports no model when neither the Bot nor the User names one", () => {
1376
+ test("ignores disabled Package values without deleting them", () => {
1377
+ const disabled = decodeUserSettingsViewV1({
1378
+ ...user(),
1379
+ packages: [
1380
+ ...user().packages,
1381
+ {
1382
+ packageId: "custom-models",
1383
+ version: "0.0.1",
1384
+ state: "disabled" as const,
1385
+ values: { model: accountModel },
1386
+ },
1387
+ ],
1388
+ });
1389
+ const bot = decodeBotSettingsViewV1({
1390
+ schemaVersion: 1,
1391
+ botId: "primary",
1392
+ revision: 1,
1393
+ profile: { name: "Primary" },
1394
+ notifications: { enabled: true },
1395
+ packageValues: { "custom-models": { model: accountModel } },
1396
+ });
1203
1397
  expect(
1204
1398
  resolveEffectiveBotModelV1({
1205
- bot: { assignments },
1206
- user: user(),
1399
+ bot,
1400
+ user: disabled,
1207
1401
  packages: modelPackages,
1208
1402
  }),
1209
- ).toEqual({ source: "none" });
1403
+ ).toMatchObject({ source: "platform", model: platformModel });
1404
+ expect(disabled.packages.at(-1)?.values).toEqual({ model: accountModel });
1405
+ expect(bot.packageValues).toEqual({
1406
+ "custom-models": { model: accountModel },
1407
+ });
1210
1408
  });
1211
1409
 
1212
- test("keeps the default fail-closed until the Bot claims the Assignment", () => {
1410
+ test("fails closed when two enabled Packages declare one scope's role", () => {
1411
+ const competing: ExecutionPackageDefinition = {
1412
+ ...modelPackages[1]!,
1413
+ packageId: "other-models",
1414
+ };
1213
1415
  const effective = resolveEffectiveBotModelV1({
1214
- bot: { assignments: [] },
1215
- user: user({
1216
- connectionId: "ollama-work",
1217
- providerModelId: "llama-3:cloud",
1218
- }),
1219
- packages: modelPackages,
1416
+ bot: { packageValues: {} },
1417
+ user: {
1418
+ ...user(),
1419
+ packages: [
1420
+ ...user().packages,
1421
+ {
1422
+ packageId: "custom-models",
1423
+ version: "0.0.1",
1424
+ state: "installed",
1425
+ },
1426
+ {
1427
+ packageId: "other-models",
1428
+ version: "0.0.1",
1429
+ state: "installed",
1430
+ },
1431
+ ],
1432
+ },
1433
+ packages: [...modelPackages, competing],
1220
1434
  });
1221
- expect(effective.source).toBe("default");
1435
+ expect(effective.source).toBe("bot");
1222
1436
  expect(effective.binding).toMatchObject({
1223
1437
  state: "unavailable",
1224
- failure: "Bot is not assigned the Connection model capability",
1438
+ failure: expect.stringContaining('"custom-models" and "other-models"'),
1225
1439
  });
1440
+ expect(effective.model).toBeUndefined();
1441
+ });
1442
+
1443
+ test("reports none only when no Package or platform model supplies one", () => {
1444
+ const withoutPlatform = user();
1445
+ delete withoutPlatform.platformModel;
1446
+ expect(
1447
+ resolveEffectiveBotModelV1({
1448
+ bot: { packageValues: {} },
1449
+ user: withoutPlatform,
1450
+ packages: modelPackages,
1451
+ }),
1452
+ ).toEqual({ source: "none" });
1226
1453
  });
1227
1454
  });
1228
1455
 
@@ -1271,6 +1498,27 @@ describe("Catalog installs and uninstall", () => {
1271
1498
  });
1272
1499
  });
1273
1500
 
1501
+ test("decodes optional install enablement and rejects a non-boolean", () => {
1502
+ expect(
1503
+ decodeConfigurationCommandV1({
1504
+ ...meta,
1505
+ type: "user/install-package",
1506
+ packageId: "custom-models",
1507
+ version: "0.0.1",
1508
+ enabled: false,
1509
+ }),
1510
+ ).toMatchObject({ enabled: false });
1511
+ expect(() =>
1512
+ decodeConfigurationCommandV1({
1513
+ ...meta,
1514
+ type: "user/install-package",
1515
+ packageId: "custom-models",
1516
+ version: "0.0.1",
1517
+ enabled: "false",
1518
+ }),
1519
+ ).toThrow("enabled is invalid");
1520
+ });
1521
+
1274
1522
  test("refuses half a Catalog install", () => {
1275
1523
  expect(() =>
1276
1524
  decodeConfigurationCommandV1({