@frockbot/configuration-core 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.
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,12 @@ import {
20
18
  decodeUserConfigurationReadRpcV1,
21
19
  decodeUserSettingsViewV1,
22
20
  initializeBotSettingsV1,
21
+ modelBindingFailureV1,
23
22
  resolveBotExecutionPlanV1,
24
23
  resolveBotModelBindingV1,
25
24
  resolveEffectiveBotModelV1,
26
- type ModelAssignment,
25
+ type ExecutionPackageDefinition,
26
+ type ModelBindingV1,
27
27
  } from "./index.js";
28
28
  import {
29
29
  isApplicationDeploymentHash,
@@ -100,41 +100,74 @@ describe("configuration DTO seam", () => {
100
100
  });
101
101
  });
102
102
 
103
- test("decodes exact Assign, Replace, and Unassign commands", () => {
103
+ test("decodes exact Bot-scoped Package setting commands", () => {
104
104
  const meta = {
105
105
  schemaVersion: 1,
106
106
  commandId: "operation-1",
107
107
  expectedRevision: 2,
108
108
  botId: "primary",
109
109
  } as const;
110
- const assignment = {
111
- assignmentId: "mail",
112
- packageId: "mail",
113
- capabilityId: "send",
114
- connectionId: "mail-1",
115
- };
116
110
  expect(
117
111
  decodeConfigurationCommandV1({
118
112
  ...meta,
119
- type: "bot/replace-capability",
120
- assignment,
113
+ type: "bot/set-package-settings",
114
+ packageId: "custom-models",
115
+ values: {
116
+ model: {
117
+ connectionId: "ollama-work",
118
+ providerModelId: "glm-5.3-flash:cloud",
119
+ },
120
+ },
121
121
  }),
122
- ).toMatchObject({ type: "bot/replace-capability", assignment });
123
- expect(
122
+ ).toMatchObject({
123
+ type: "bot/set-package-settings",
124
+ packageId: "custom-models",
125
+ values: { model: { connectionId: "ollama-work" } },
126
+ });
127
+ expect(() =>
124
128
  decodeConfigurationCommandV1({
125
129
  ...meta,
126
- type: "bot/unassign-capability",
127
- assignmentId: "mail",
130
+ type: "bot/set-package-settings",
131
+ packageId: "custom-models",
132
+ values: {},
128
133
  }),
129
- ).toMatchObject({ type: "bot/unassign-capability", assignmentId: "mail" });
130
- expect(() =>
134
+ ).toThrow("values names no setting");
135
+ expect(
131
136
  decodeConfigurationCommandV1({
132
137
  ...meta,
133
- type: "bot/unassign-capability",
134
- assignmentId: "mail",
135
- assignment,
138
+ type: "bot/set-package-settings",
139
+ packageId: "custom-models",
140
+ unset: ["model"],
136
141
  }),
137
- ).toThrow(ConfigurationDecodeError);
142
+ ).toMatchObject({ unset: ["model"] });
143
+ for (const invalid of [
144
+ { ...meta, type: "bot/set-package-settings", packageId: "custom-models" },
145
+ {
146
+ ...meta,
147
+ type: "bot/set-package-settings",
148
+ packageId: "custom-models",
149
+ unset: [],
150
+ },
151
+ {
152
+ ...meta,
153
+ type: "bot/set-package-settings",
154
+ packageId: "custom-models",
155
+ unset: ["model", "model"],
156
+ },
157
+ {
158
+ ...meta,
159
+ type: "bot/set-package-settings",
160
+ packageId: "custom-models",
161
+ values: {
162
+ model: { connectionId: "ollama-work", providerModelId: "m" },
163
+ },
164
+ unset: ["model"],
165
+ },
166
+ ]) {
167
+ expect(() => decodeConfigurationCommandV1(invalid)).toThrow(
168
+ ConfigurationDecodeError,
169
+ );
170
+ }
138
171
  });
139
172
 
140
173
  test("accepts provider model IDs through the catalog limit", () => {
@@ -143,49 +176,43 @@ describe("configuration DTO seam", () => {
143
176
  expect(
144
177
  decodeConfigurationCommandV1({
145
178
  schemaVersion: 1,
146
- type: "bot/select-model",
179
+ type: "user/set-platform-model",
147
180
  commandId: "command-model",
148
- botId: "primary",
149
181
  expectedRevision: 3,
150
182
  model: { connectionId: "connection-1", providerModelId },
151
183
  }),
152
184
  ).toMatchObject({ model: { providerModelId } });
153
185
  });
154
186
 
155
- test("decodes atomic model binding and explicit unbinding commands", () => {
187
+ test("decodes the provider-bootstrap platform model command", () => {
156
188
  expect(
157
189
  decodeConfigurationCommandV1({
158
190
  schemaVersion: 1,
159
- type: "bot/assign-capability",
160
- commandId: "bind-model",
161
- botId: "primary",
191
+ type: "user/set-platform-model",
192
+ commandId: "bootstrap-model",
162
193
  expectedRevision: 2,
163
- assignment: {
164
- assignmentId: "ollama-model",
165
- packageId: "provider-ollama-cloud",
166
- capabilityId: "ollama-cloud-models",
167
- connectionId: "ollama-work",
168
- },
169
194
  model: {
170
195
  connectionId: "ollama-work",
171
196
  providerModelId: "glm-5.3-flash:cloud",
172
197
  },
173
198
  }),
174
199
  ).toMatchObject({
175
- type: "bot/assign-capability",
176
- assignment: { connectionId: "ollama-work" },
200
+ type: "user/set-platform-model",
177
201
  model: { providerModelId: "glm-5.3-flash:cloud" },
178
202
  });
179
- expect(
203
+ expect(() =>
180
204
  decodeConfigurationCommandV1({
181
205
  schemaVersion: 1,
182
- type: "bot/unbind-model",
183
- commandId: "unbind-model",
184
- botId: "primary",
206
+ type: "user/set-platform-model",
207
+ commandId: "bootstrap-model",
185
208
  expectedRevision: 3,
186
- assignmentId: "ollama-model",
209
+ model: {
210
+ connectionId: "ollama-work",
211
+ providerModelId: "glm-5.3-flash:cloud",
212
+ extra: true,
213
+ },
187
214
  }),
188
- ).toMatchObject({ type: "bot/unbind-model", assignmentId: "ollama-model" });
215
+ ).toThrow(ConfigurationDecodeError);
189
216
  });
190
217
 
191
218
  test("rejects unversioned, malformed, and unknown commands", () => {
@@ -207,30 +234,6 @@ describe("configuration DTO seam", () => {
207
234
  }
208
235
  });
209
236
 
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
237
  test("rejects unknown fields throughout configuration commands", () => {
235
238
  const meta = {
236
239
  schemaVersion: 1,
@@ -245,8 +248,7 @@ describe("configuration DTO seam", () => {
245
248
  },
246
249
  {
247
250
  ...meta,
248
- type: "user/set-new-bot-model",
249
- source: "user",
251
+ type: "user/set-platform-model",
250
252
  model: {
251
253
  connectionId: "provider-1",
252
254
  providerModelId: "model-1",
@@ -261,14 +263,11 @@ describe("configuration DTO seam", () => {
261
263
  },
262
264
  {
263
265
  ...meta,
264
- type: "bot/assign-capability",
266
+ type: "bot/set-package-settings",
265
267
  botId: "primary",
266
- assignment: {
267
- assignmentId: "gmail",
268
- packageId: "composio",
269
- capabilityId: "gmail-tools",
270
- extra: true,
271
- },
268
+ packageId: "custom-models",
269
+ values: { model: "fast" },
270
+ extra: true,
272
271
  },
273
272
  {
274
273
  ...meta,
@@ -303,45 +302,6 @@ describe("configuration DTO seam", () => {
303
302
  }
304
303
  });
305
304
 
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
305
  test("strictly decodes versioned Connection commands", () => {
346
306
  expect(
347
307
  decodeStartConnectionCommandV1({
@@ -388,20 +348,20 @@ describe("configuration DTO seam", () => {
388
348
  }
389
349
  });
390
350
 
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
- };
351
+ test("rejects hidden and symbol fields across Package-setting seams", () => {
398
352
  const command = {
399
353
  schemaVersion: 1,
400
- type: "bot/assign-capability",
354
+ type: "bot/set-package-settings",
401
355
  commandId: "command-1",
402
356
  expectedRevision: 0,
403
357
  botId: "primary",
404
- assignment,
358
+ packageId: "custom-models",
359
+ values: {
360
+ model: {
361
+ connectionId: "mail-1",
362
+ providerModelId: "model-1",
363
+ },
364
+ },
405
365
  };
406
366
  const receipt = {
407
367
  schemaVersion: 1,
@@ -415,16 +375,14 @@ describe("configuration DTO seam", () => {
415
375
  revision: 0,
416
376
  profile: { name: "Primary" },
417
377
  notifications: { enabled: false },
418
- assignments: [],
419
- assignmentOperations: [
420
- {
421
- commandId: "command-1",
422
- kind: "assigning",
423
- assignmentId: "mail",
424
- state: "retrying",
425
- target: assignment,
378
+ packageValues: {
379
+ "custom-models": {
380
+ model: {
381
+ connectionId: "mail-1",
382
+ providerModelId: "model-1",
383
+ },
426
384
  },
427
- ],
385
+ },
428
386
  };
429
387
  const rpc = {
430
388
  schemaVersion: 1,
@@ -434,7 +392,6 @@ describe("configuration DTO seam", () => {
434
392
  };
435
393
  for (const [decode, candidate] of [
436
394
  [decodeConfigurationCommandV1, command],
437
- [decodeConfigurationCommandV1, { ...command, assignment }],
438
395
  [decodeOperationReceiptV1, receipt],
439
396
  [decodeBotSettingsViewV1, view],
440
397
  [decodeBotConfigurationExecuteRpcV1, rpc],
@@ -445,20 +402,28 @@ describe("configuration DTO seam", () => {
445
402
  const symbol = { ...candidate, [Symbol("extra")]: true };
446
403
  expect(() => decode(symbol)).toThrow(ConfigurationDecodeError);
447
404
  }
448
- const hiddenTarget = { ...assignment };
449
- Object.defineProperty(hiddenTarget, "hidden", { value: true });
405
+ const hiddenValues = { ...command.values };
406
+ Object.defineProperty(hiddenValues, "hidden", { value: true });
450
407
  expect(() =>
451
- decodeConfigurationCommandV1({ ...command, assignment: hiddenTarget }),
408
+ decodeConfigurationCommandV1({ ...command, values: hiddenValues }),
452
409
  ).toThrow(ConfigurationDecodeError);
453
- const symbolTarget = { ...assignment, [Symbol("extra")]: true };
410
+ const symbolModel = {
411
+ ...command.values.model,
412
+ [Symbol("extra")]: true,
413
+ };
454
414
  expect(() =>
455
415
  decodeBotSettingsViewV1({
456
416
  ...view,
457
- assignmentOperations: [
458
- { ...view.assignmentOperations[0], target: symbolTarget },
459
- ],
417
+ packageValues: {
418
+ "custom-models": { model: symbolModel },
419
+ },
460
420
  }),
461
421
  ).toThrow(ConfigurationDecodeError);
422
+ const pollutedValues = Object.create({ inherited: true });
423
+ pollutedValues.model = command.values.model;
424
+ expect(() =>
425
+ decodeConfigurationCommandV1({ ...command, values: pollutedValues }),
426
+ ).toThrow(ConfigurationDecodeError);
462
427
  });
463
428
 
464
429
  test("rejects unknown configuration RPC envelope fields", () => {
@@ -684,17 +649,9 @@ describe("configuration DTO seam", () => {
684
649
  });
685
650
 
686
651
  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
- });
652
+ test("initializes Bot settings without seeding a model choice", () => {
653
+ const initialized = initializeBotSettingsV1("primary");
654
+ expect(initialized.packageValues).toEqual({});
698
655
  expect(initialized.notifications).toEqual({ enabled: true });
699
656
  });
700
657
 
@@ -704,28 +661,44 @@ describe("Bot execution-plan authority", () => {
704
661
  revision: 4,
705
662
  profile: { name: "Primary" },
706
663
  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: [],
664
+ packageValues: {},
717
665
  };
718
- const packages = [
666
+ const packages: ExecutionPackageDefinition[] = [
719
667
  {
720
668
  packageId: "composio",
721
669
  version: "0.0.1",
722
- capabilities: [{ id: "gmail-tools", connectionTypes: ["gmail"] }],
670
+ settings: [],
671
+ capabilities: [
672
+ { id: "clock", kind: "tool", connectionTypes: [] },
673
+ { id: "gmail-tools", kind: "tool", connectionTypes: ["gmail"] },
674
+ ],
723
675
  connectionTypes: [{ id: "gmail", capabilities: ["gmail-tools"] }],
724
676
  },
725
677
  ];
726
678
 
727
- test("resolves a Bot model through its explicit ready Connection", () => {
728
- const user = {
679
+ const modelPackages: ExecutionPackageDefinition[] = [
680
+ {
681
+ packageId: "provider-ollama-cloud",
682
+ version: "0.0.1",
683
+ settings: [],
684
+ capabilities: [
685
+ {
686
+ id: "ollama-cloud-models",
687
+ kind: "model",
688
+ connectionTypes: ["ollama-cloud-account"],
689
+ },
690
+ ],
691
+ connectionTypes: [
692
+ {
693
+ id: "ollama-cloud-account",
694
+ capabilities: ["ollama-cloud-models"],
695
+ },
696
+ ],
697
+ },
698
+ ];
699
+
700
+ function modelUser() {
701
+ return {
729
702
  schemaVersion: 1 as const,
730
703
  revision: 1,
731
704
  profile: { name: "User" },
@@ -765,45 +738,16 @@ describe("Bot execution-plan authority", () => {
765
738
  },
766
739
  ],
767
740
  };
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
- ];
741
+ }
796
742
 
743
+ test("resolves a model through User enablement and its ready Connection", () => {
744
+ const user = modelUser();
745
+ const model = {
746
+ connectionId: "ollama-work",
747
+ providerModelId: "glm-5.3-flash:cloud",
748
+ };
797
749
  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
- }),
750
+ resolveBotModelBindingV1({ model, user, packages: modelPackages }),
807
751
  ).toMatchObject({
808
752
  state: "ready",
809
753
  providerType: "ollama-cloud",
@@ -811,43 +755,53 @@ describe("Bot execution-plan authority", () => {
811
755
  });
812
756
  expect(
813
757
  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,
758
+ model: { ...model, providerModelId: "new-model:cloud" },
830
759
  user,
831
760
  packages: modelPackages,
832
761
  }).state,
833
762
  ).toBe("requires-resolution");
763
+ });
764
+
765
+ test("disabling the Package or revoking the Connection fails every Bot closed", () => {
766
+ const model = {
767
+ connectionId: "ollama-work",
768
+ providerModelId: "glm-5.3-flash:cloud",
769
+ };
770
+ const user = {
771
+ ...modelUser(),
772
+ packages: [{ ...modelUser().packages[0]!, state: "disabled" as const }],
773
+ };
834
774
  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
- }),
775
+ modelBindingFailureV1({ model, user, packages: modelPackages }),
776
+ ).toContain('Package "provider-ollama-cloud" is not installed and enabled');
777
+ expect(
778
+ resolveBotModelBindingV1({ model, user, packages: modelPackages }),
844
779
  ).toMatchObject({
845
780
  state: "unavailable",
846
- failure: "Bot is not assigned the Connection model capability",
781
+ failure: expect.stringContaining("enable it"),
847
782
  });
783
+
784
+ const revoked = {
785
+ ...modelUser(),
786
+ connections: [
787
+ { ...modelUser().connections[0]!, state: "revoked" as const },
788
+ ],
789
+ };
790
+ for (const _botId of ["bot-one", "bot-two"]) {
791
+ expect(
792
+ resolveBotModelBindingV1({
793
+ model,
794
+ user: revoked,
795
+ packages: modelPackages,
796
+ }),
797
+ ).toMatchObject({
798
+ state: "unavailable",
799
+ failure: expect.stringContaining("reconnect it"),
800
+ });
801
+ }
848
802
  });
849
803
 
850
- test("requires an enabled installation, declared capability, and ready typed Connection", () => {
804
+ test("projects the enabled User capability set in stable identity order", () => {
851
805
  const user = {
852
806
  schemaVersion: 1 as const,
853
807
  revision: 2,
@@ -861,42 +815,44 @@ describe("Bot execution-plan authority", () => {
861
815
  ],
862
816
  connections: [
863
817
  {
864
- connectionId: "gmail-connection",
818
+ connectionId: "gmail-z",
865
819
  packageId: "composio",
866
820
  connectionTypeId: "gmail",
867
- displayName: "Gmail",
821
+ displayName: "Gmail Z",
822
+ state: "ready" as const,
823
+ safeMetadata: {},
824
+ },
825
+ {
826
+ connectionId: "gmail-a",
827
+ packageId: "composio",
828
+ connectionTypeId: "gmail",
829
+ displayName: "Gmail A",
868
830
  state: "ready" as const,
869
831
  safeMetadata: {},
870
832
  },
871
833
  ],
872
834
  };
873
835
  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");
836
+ resolveBotExecutionPlanV1({ bot, user, packages }).capabilities,
837
+ ).toEqual([
838
+ {
839
+ packageId: "composio",
840
+ capabilityId: "clock",
841
+ kind: "tool",
842
+ },
843
+ {
844
+ packageId: "composio",
845
+ capabilityId: "gmail-tools",
846
+ kind: "tool",
847
+ connectionId: "gmail-a",
848
+ },
849
+ {
850
+ packageId: "composio",
851
+ capabilityId: "gmail-tools",
852
+ kind: "tool",
853
+ connectionId: "gmail-z",
854
+ },
855
+ ]);
900
856
  expect(
901
857
  resolveBotExecutionPlanV1({
902
858
  bot,
@@ -905,20 +861,27 @@ describe("Bot execution-plan authority", () => {
905
861
  packages: [{ ...user.packages[0]!, state: "disabled" }],
906
862
  },
907
863
  packages,
908
- }).assignments[0]?.state,
909
- ).toBe("unavailable");
864
+ }).capabilities,
865
+ ).toEqual([]);
910
866
  expect(
911
867
  resolveBotExecutionPlanV1({
912
868
  bot,
913
869
  user: {
914
870
  ...user,
915
- connections: [
916
- { ...user.connections[0]!, connectionTypeId: "calendar" },
917
- ],
871
+ connections: user.connections.map((connection) => ({
872
+ ...connection,
873
+ state: "revoked" as const,
874
+ })),
918
875
  },
919
876
  packages,
920
- }).assignments[0]?.state,
921
- ).toBe("unavailable");
877
+ }).capabilities,
878
+ ).toEqual([
879
+ {
880
+ packageId: "composio",
881
+ capabilityId: "clock",
882
+ kind: "tool",
883
+ },
884
+ ]);
922
885
  });
923
886
  });
924
887
 
@@ -1087,10 +1050,28 @@ describe("Composition generation views", () => {
1087
1050
  });
1088
1051
 
1089
1052
  describe("effective Bot model resolution", () => {
1090
- const modelPackages = [
1053
+ const platformModel: ModelBindingV1 = {
1054
+ connectionId: "ollama-work",
1055
+ providerModelId: "glm-5.3-flash:cloud",
1056
+ };
1057
+ const accountModel: ModelBindingV1 = {
1058
+ connectionId: "ollama-work",
1059
+ providerModelId: "llama-3:cloud",
1060
+ };
1061
+ const modelSchema = {
1062
+ type: "object" as const,
1063
+ properties: {
1064
+ connectionId: { type: "string" as const },
1065
+ providerModelId: { type: "string" as const },
1066
+ },
1067
+ required: ["connectionId", "providerModelId"],
1068
+ additionalProperties: false,
1069
+ };
1070
+ const modelPackages: ExecutionPackageDefinition[] = [
1091
1071
  {
1092
1072
  packageId: "provider-ollama-cloud",
1093
1073
  version: "0.0.1",
1074
+ settings: [],
1094
1075
  capabilities: [
1095
1076
  {
1096
1077
  id: "ollama-cloud-models",
@@ -1102,10 +1083,23 @@ describe("effective Bot model resolution", () => {
1102
1083
  { id: "ollama-cloud-account", capabilities: ["ollama-cloud-models"] },
1103
1084
  ],
1104
1085
  },
1086
+ {
1087
+ packageId: "custom-models",
1088
+ version: "0.0.1",
1089
+ settings: [
1090
+ {
1091
+ id: "model",
1092
+ schemaVersion: 1,
1093
+ scopes: ["user", "bot"],
1094
+ role: "model",
1095
+ schema: modelSchema,
1096
+ },
1097
+ ],
1098
+ capabilities: [],
1099
+ connectionTypes: [],
1100
+ },
1105
1101
  ];
1106
- function user(
1107
- newBotModelTemplate?: ModelAssignment,
1108
- ): Parameters<typeof resolveEffectiveBotModelV1>[0]["user"] {
1102
+ function user(): Parameters<typeof resolveEffectiveBotModelV1>[0]["user"] {
1109
1103
  return {
1110
1104
  schemaVersion: 1,
1111
1105
  revision: 1,
@@ -1147,82 +1141,129 @@ describe("effective Bot model resolution", () => {
1147
1141
  safeMetadata: {},
1148
1142
  },
1149
1143
  ],
1150
- ...(newBotModelTemplate
1151
- ? { newBotModelTemplate, newBotModelTemplateSource: "auto" as const }
1152
- : {}),
1144
+ platformModel,
1153
1145
  };
1154
1146
  }
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
1147
 
1165
- test("follows the User default when the Bot has no model of its own", () => {
1148
+ test("uses the platform model when the Bot and User configured nothing", () => {
1166
1149
  const effective = resolveEffectiveBotModelV1({
1167
- bot: { assignments },
1168
- user: user({
1169
- connectionId: "ollama-work",
1170
- providerModelId: "llama-3:cloud",
1171
- }),
1150
+ bot: { packageValues: {} },
1151
+ user: user(),
1172
1152
  packages: modelPackages,
1173
1153
  });
1174
- expect(effective.source).toBe("default");
1175
- expect(effective.model).toEqual({
1176
- connectionId: "ollama-work",
1177
- providerModelId: "llama-3:cloud",
1178
- });
1154
+ expect(effective.source).toBe("platform");
1155
+ expect(effective.model).toEqual(platformModel);
1179
1156
  expect(effective.binding?.state).toBe("ready");
1180
1157
  });
1181
1158
 
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",
1159
+ test("prefers an enabled Package's User value, then its Bot value", () => {
1160
+ const configured = {
1161
+ ...user(),
1162
+ packages: [
1163
+ ...user().packages,
1164
+ {
1165
+ packageId: "custom-models",
1166
+ version: "0.0.1",
1167
+ state: "installed" as const,
1168
+ values: { model: accountModel },
1188
1169
  },
1189
- assignments,
1190
- },
1191
- user: user({
1192
- connectionId: "ollama-work",
1193
- providerModelId: "llama-3:cloud",
1194
- }),
1170
+ ],
1171
+ };
1172
+ const account = resolveEffectiveBotModelV1({
1173
+ bot: { packageValues: {} },
1174
+ user: configured,
1195
1175
  packages: modelPackages,
1196
1176
  });
1197
- expect(effective.source).toBe("bot");
1198
- expect(effective.model?.providerModelId).toBe("glm-5.3-flash:cloud");
1199
- expect(effective.binding?.state).toBe("ready");
1177
+ expect(account.source).toBe("account");
1178
+ expect(account.model).toEqual(accountModel);
1179
+
1180
+ const bot = resolveEffectiveBotModelV1({
1181
+ bot: { packageValues: { "custom-models": { model: platformModel } } },
1182
+ user: configured,
1183
+ packages: modelPackages,
1184
+ });
1185
+ expect(bot.source).toBe("bot");
1186
+ expect(bot.model).toEqual(platformModel);
1187
+ expect(bot.binding?.state).toBe("ready");
1200
1188
  });
1201
1189
 
1202
- test("reports no model when neither the Bot nor the User names one", () => {
1190
+ test("ignores disabled Package values without deleting them", () => {
1191
+ const disabled = decodeUserSettingsViewV1({
1192
+ ...user(),
1193
+ packages: [
1194
+ ...user().packages,
1195
+ {
1196
+ packageId: "custom-models",
1197
+ version: "0.0.1",
1198
+ state: "disabled" as const,
1199
+ values: { model: accountModel },
1200
+ },
1201
+ ],
1202
+ });
1203
+ const bot = decodeBotSettingsViewV1({
1204
+ schemaVersion: 1,
1205
+ botId: "primary",
1206
+ revision: 1,
1207
+ profile: { name: "Primary" },
1208
+ notifications: { enabled: true },
1209
+ packageValues: { "custom-models": { model: accountModel } },
1210
+ });
1203
1211
  expect(
1204
1212
  resolveEffectiveBotModelV1({
1205
- bot: { assignments },
1206
- user: user(),
1213
+ bot,
1214
+ user: disabled,
1207
1215
  packages: modelPackages,
1208
1216
  }),
1209
- ).toEqual({ source: "none" });
1217
+ ).toMatchObject({ source: "platform", model: platformModel });
1218
+ expect(disabled.packages.at(-1)?.values).toEqual({ model: accountModel });
1219
+ expect(bot.packageValues).toEqual({
1220
+ "custom-models": { model: accountModel },
1221
+ });
1210
1222
  });
1211
1223
 
1212
- test("keeps the default fail-closed until the Bot claims the Assignment", () => {
1224
+ test("fails closed when two enabled Packages declare one scope's role", () => {
1225
+ const competing: ExecutionPackageDefinition = {
1226
+ ...modelPackages[1]!,
1227
+ packageId: "other-models",
1228
+ };
1213
1229
  const effective = resolveEffectiveBotModelV1({
1214
- bot: { assignments: [] },
1215
- user: user({
1216
- connectionId: "ollama-work",
1217
- providerModelId: "llama-3:cloud",
1218
- }),
1219
- packages: modelPackages,
1230
+ bot: { packageValues: {} },
1231
+ user: {
1232
+ ...user(),
1233
+ packages: [
1234
+ ...user().packages,
1235
+ {
1236
+ packageId: "custom-models",
1237
+ version: "0.0.1",
1238
+ state: "installed",
1239
+ },
1240
+ {
1241
+ packageId: "other-models",
1242
+ version: "0.0.1",
1243
+ state: "installed",
1244
+ },
1245
+ ],
1246
+ },
1247
+ packages: [...modelPackages, competing],
1220
1248
  });
1221
- expect(effective.source).toBe("default");
1249
+ expect(effective.source).toBe("bot");
1222
1250
  expect(effective.binding).toMatchObject({
1223
1251
  state: "unavailable",
1224
- failure: "Bot is not assigned the Connection model capability",
1252
+ failure: expect.stringContaining('"custom-models" and "other-models"'),
1225
1253
  });
1254
+ expect(effective.model).toBeUndefined();
1255
+ });
1256
+
1257
+ test("reports none only when no Package or platform model supplies one", () => {
1258
+ const withoutPlatform = user();
1259
+ delete withoutPlatform.platformModel;
1260
+ expect(
1261
+ resolveEffectiveBotModelV1({
1262
+ bot: { packageValues: {} },
1263
+ user: withoutPlatform,
1264
+ packages: modelPackages,
1265
+ }),
1266
+ ).toEqual({ source: "none" });
1226
1267
  });
1227
1268
  });
1228
1269
 
@@ -1271,6 +1312,27 @@ describe("Catalog installs and uninstall", () => {
1271
1312
  });
1272
1313
  });
1273
1314
 
1315
+ test("decodes optional install enablement and rejects a non-boolean", () => {
1316
+ expect(
1317
+ decodeConfigurationCommandV1({
1318
+ ...meta,
1319
+ type: "user/install-package",
1320
+ packageId: "custom-models",
1321
+ version: "0.0.1",
1322
+ enabled: false,
1323
+ }),
1324
+ ).toMatchObject({ enabled: false });
1325
+ expect(() =>
1326
+ decodeConfigurationCommandV1({
1327
+ ...meta,
1328
+ type: "user/install-package",
1329
+ packageId: "custom-models",
1330
+ version: "0.0.1",
1331
+ enabled: "false",
1332
+ }),
1333
+ ).toThrow("enabled is invalid");
1334
+ });
1335
+
1274
1336
  test("refuses half a Catalog install", () => {
1275
1337
  expect(() =>
1276
1338
  decodeConfigurationCommandV1({