@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.ts CHANGED
@@ -6,10 +6,18 @@ import {
6
6
  decodeConnectionAuthorizationViewV1,
7
7
  decodeConnectionModelCatalogV1,
8
8
  } from "@frockbot/connection-core";
9
+ import type { PackageSettingDefinition } from "@frockbot/kernel-composition";
9
10
  import { ConfigurationDecodeError } from "./errors.js";
10
11
  import {
12
+ decodeInstalledPackageSettingsPatchV1,
13
+ decodeInstalledPackageSettingIdsV1,
14
+ decodeModelBindingV1,
11
15
  MAX_PACKAGE_SETTINGS_V1,
12
16
  MAX_PACKAGE_SETTING_TEXT_V1,
17
+ resolvePackageSettingValuesV1,
18
+ type InstalledPackageSettingsV1,
19
+ type ModelBindingV1,
20
+ type PackageSettingsDefinitionV1,
13
21
  type PackageSettingValueV1,
14
22
  } from "./package-settings.js";
15
23
  export { ConfigurationDecodeError } from "./errors.js";
@@ -21,13 +29,20 @@ import {
21
29
  } from "./identifiers.js";
22
30
  export { isBotIdV1 } from "./bot-id.js";
23
31
  export {
32
+ decodeInstalledPackageSettingsPatchV1,
33
+ decodeInstalledPackageSettingIdsV1,
34
+ decodeModelBindingV1,
24
35
  decodePackageSettingsPatchV1,
36
+ decodePackageSettingIdsV1,
25
37
  decodePackageSettingValueV1,
26
38
  decodePackageSettingValuesV1,
27
39
  emptyPackageSettingValuesV1,
28
40
  MAX_PACKAGE_SETTINGS_V1,
29
41
  MAX_PACKAGE_SETTING_TEXT_V1,
30
42
  resolvePackageSettingValuesV1,
43
+ type InstalledPackageSettingsV1,
44
+ type ModelBindingV1,
45
+ type PackageSettingsDefinitionV1,
31
46
  type PackageSettingValueV1,
32
47
  type PackageSettingValuesV1,
33
48
  } from "./package-settings.js";
@@ -103,14 +118,6 @@ export interface BotNotificationPolicy {
103
118
  enabled: boolean;
104
119
  }
105
120
 
106
- export interface ModelAssignment {
107
- connectionId: string;
108
- providerModelId: string;
109
- }
110
-
111
- /** Whether the User chose the default model or a provider selected it safely. */
112
- export type NewBotModelTemplateSourceV1 = "user" | "auto";
113
-
114
121
  /**
115
122
  * Where an installed Package came from. `first-party` is a Package compiled
116
123
  * into the running application; `catalog` is one admitted from a pinned remote
@@ -131,7 +138,7 @@ export interface PackageInstallationView {
131
138
  catalogGeneration?: string;
132
139
  provenance?: PackageProvenanceV1;
133
140
  /** The setup values the install carried, as GrokBot's `InstallPlugin{values}`. */
134
- values?: Record<string, JsonValue>;
141
+ values?: Record<string, JsonValue | ModelBindingV1>;
135
142
  }
136
143
 
137
144
  /**
@@ -213,12 +220,11 @@ export interface UserSettingsViewV1 {
213
220
  profile: { name: string; email?: string };
214
221
  packages: PackageInstallationView[];
215
222
  connections: ConnectionView[];
216
- newBotModelTemplate?: ModelAssignment;
217
223
  /**
218
- * `user` is sticky, including when the User explicitly clears the default.
219
- * Providers may replace only an `auto` default (or the untouched absence).
224
+ * The model the platform chose for this User. Only a provider bootstrap
225
+ * writes it; no User command may do so (AGENTS.md Configuration shape).
220
226
  */
221
- newBotModelTemplateSource?: NewBotModelTemplateSourceV1;
227
+ platformModel?: ModelBindingV1;
222
228
  /**
223
229
  * The remote Catalog generation this User is pinned to, and the content hash
224
230
  * of that generation's index. Pinned on the first read that finds a Catalog
@@ -230,54 +236,27 @@ export interface UserSettingsViewV1 {
230
236
  catalogIndexHash?: string;
231
237
  }
232
238
 
233
- export interface CapabilityAssignmentView {
234
- assignmentId: string;
235
- packageId: string;
236
- capabilityId: string;
237
- connectionId?: string;
238
- state: "enabled" | "disabled" | "unavailable";
239
- }
240
-
241
- export interface ConnectionDependencyRequirementV1 {
242
- schemaVersion: 1;
243
- packageId: string;
244
- packageVersion: string;
245
- capabilityId: string;
246
- connectionTypeIds: string[];
247
- }
248
-
249
- export interface CapabilityAssignmentOperationViewV1 {
250
- commandId: string;
251
- kind: "assigning" | "replacing" | "unassigning";
252
- assignmentId: string;
253
- state: "pending" | "retrying";
254
- target?: Omit<CapabilityAssignmentView, "state">;
255
- }
256
-
257
239
  export interface BotSettingsViewV1 {
258
240
  schemaVersion: 1;
259
241
  botId: string;
260
242
  revision: number;
261
243
  profile: BotProfile;
262
244
  notifications: BotNotificationPolicy;
263
- assignments: CapabilityAssignmentView[];
264
- assignmentOperations: CapabilityAssignmentOperationViewV1[];
265
- model?: ModelAssignment;
245
+ /**
246
+ * Package-owned Bot overrides. Disabling a Package leaves these durable but
247
+ * inert so re-enabling restores them (ADR 0019).
248
+ */
249
+ packageValues: Record<string, Record<string, unknown>>;
266
250
  }
267
251
 
268
- export function initializeBotSettingsV1(
269
- botId: string,
270
- model?: ModelAssignment,
271
- ): BotSettingsViewV1 {
252
+ export function initializeBotSettingsV1(botId: string): BotSettingsViewV1 {
272
253
  return {
273
254
  schemaVersion: 1,
274
255
  botId,
275
256
  revision: 0,
276
257
  profile: { name: botId === "default" ? "Barebones" : botId },
277
258
  notifications: { enabled: true },
278
- assignments: [],
279
- assignmentOperations: [],
280
- model: model ? structuredClone(model) : undefined,
259
+ packageValues: {},
281
260
  };
282
261
  }
283
262
 
@@ -299,14 +278,20 @@ export type ConfigurationCommandV1 =
299
278
  profile: UserSettingsViewV1["profile"];
300
279
  })
301
280
  | (CommandMetaV1 & {
302
- type: "user/set-new-bot-model";
303
- model?: ModelAssignment;
304
- source: NewBotModelTemplateSourceV1;
281
+ /**
282
+ * Only a backend Contribution may issue this command. The User gateway
283
+ * enforces that authority; a User can choose a model only by enabling a
284
+ * Package that owns model-role settings.
285
+ */
286
+ type: "user/set-platform-model";
287
+ model: ModelBindingV1;
305
288
  })
306
289
  | (CommandMetaV1 & {
307
290
  type: "user/install-package";
308
291
  packageId: string;
309
292
  version: string;
293
+ /** Installs enabled unless the caller explicitly asks otherwise. */
294
+ enabled?: boolean;
310
295
  /**
311
296
  * A Catalog install names the entry and the generation it was read
312
297
  * from. The User Durable Object refuses a generation other than the one
@@ -318,11 +303,7 @@ export type ConfigurationCommandV1 =
318
303
  values?: Record<string, JsonValue>;
319
304
  })
320
305
  | (CommandMetaV1 & {
321
- /**
322
- * Removes the installation. Dependent Assignments are never deleted:
323
- * they resolve as unavailable tombstones the User can repair (ADR 0003).
324
- * Connections are untouched.
325
- */
306
+ /** Removes the installation; Connections remain User-owned (ADR 0019). */
326
307
  type: "user/uninstall-package";
327
308
  packageId: string;
328
309
  })
@@ -342,7 +323,9 @@ export type ConfigurationCommandV1 =
342
323
  */
343
324
  type: "user/set-package-settings";
344
325
  packageId: string;
345
- values: Record<string, PackageSettingValueV1>;
326
+ values?: Record<string, PackageSettingValueV1>;
327
+ /** Setting ids whose stored values are removed by this command. */
328
+ unset?: string[];
346
329
  })
347
330
  | (CommandMetaV1 & {
348
331
  type: "bot/update-profile";
@@ -371,31 +354,17 @@ export type ConfigurationCommandV1 =
371
354
  notifications: BotNotificationPolicy;
372
355
  })
373
356
  | (CommandMetaV1 & {
374
- type: "bot/select-model";
375
- botId: string;
376
- model: ModelAssignment;
377
- })
378
- | (CommandMetaV1 & {
379
- type: "bot/assign-capability";
380
- botId: string;
381
- assignment: Omit<CapabilityAssignmentView, "state">;
382
- model?: ModelAssignment;
383
- })
384
- | (CommandMetaV1 & {
385
- type: "bot/replace-capability";
386
- botId: string;
387
- assignment: Omit<CapabilityAssignmentView, "state">;
388
- model?: ModelAssignment;
389
- })
390
- | (CommandMetaV1 & {
391
- type: "bot/unassign-capability";
392
- botId: string;
393
- assignmentId: string;
394
- })
395
- | (CommandMetaV1 & {
396
- type: "bot/unbind-model";
357
+ /**
358
+ * A partial update of one installed Package's Bot-scoped setting bag.
359
+ * The Bot authority validates it against that installed version's
360
+ * manifest through the same codec as User-scoped Package settings.
361
+ */
362
+ type: "bot/set-package-settings";
397
363
  botId: string;
398
- assignmentId: string;
364
+ packageId: string;
365
+ values?: Record<string, PackageSettingValueV1>;
366
+ /** Setting ids whose stored values are removed by this command. */
367
+ unset?: string[];
399
368
  });
400
369
 
401
370
  export type UserConfigurationCommandV1 = Exclude<
@@ -502,16 +471,25 @@ export interface BotExecutionPlanV1 {
502
471
  schemaVersion: 1;
503
472
  botId: string;
504
473
  revision: number;
505
- model?: ModelAssignment;
506
- assignments: CapabilityAssignmentView[];
474
+ model?: ModelBindingV1;
475
+ capabilities: EnabledCapabilityV1[];
476
+ }
477
+
478
+ /** One Capability granted account-wide by an enabled Package (ADR 0019). */
479
+ export interface EnabledCapabilityV1 {
480
+ packageId: string;
481
+ capabilityId: string;
482
+ kind: "tool" | "model" | "memory" | "notification" | "computer";
483
+ connectionId?: string;
507
484
  }
508
485
 
509
486
  export interface ExecutionPackageDefinition {
510
487
  packageId: string;
511
488
  version: string;
489
+ settings: PackageSettingDefinition[];
512
490
  capabilities: Array<{
513
491
  id: string;
514
- kind?: "tool" | "model" | "memory" | "notification" | "computer";
492
+ kind: "tool" | "model" | "memory" | "notification" | "computer";
515
493
  connectionTypes: string[];
516
494
  }>;
517
495
  connectionTypes: Array<{
@@ -520,62 +498,55 @@ export interface ExecutionPackageDefinition {
520
498
  }>;
521
499
  }
522
500
 
523
- export function capabilityAssignmentFailureV1(input: {
524
- assignment: Omit<CapabilityAssignmentView, "state">;
501
+ function compareIdentifiers(left: string, right: string): number {
502
+ return left < right ? -1 : left > right ? 1 : 0;
503
+ }
504
+
505
+ export function modelBindingFailureV1(input: {
506
+ model: ModelBindingV1;
525
507
  user: UserSettingsViewV1;
526
508
  packages: readonly ExecutionPackageDefinition[];
527
509
  }): string | undefined {
528
- const installation = input.user.packages.find(
529
- (pkg) =>
530
- pkg.packageId === input.assignment.packageId && pkg.state === "installed",
531
- );
532
- const pkg = input.packages.find(
533
- (candidate) =>
534
- candidate.packageId === input.assignment.packageId &&
535
- candidate.version === installation?.version,
536
- );
537
- if (!installation || !pkg) {
538
- return `Package "${input.assignment.packageId}" is not installed and enabled`;
539
- }
540
- const capability = pkg.capabilities.find(
541
- (candidate) => candidate.id === input.assignment.capabilityId,
510
+ const connection = input.user.connections.find(
511
+ (candidate) => candidate.connectionId === input.model.connectionId,
542
512
  );
543
- if (!capability) {
544
- return `Capability "${input.assignment.capabilityId}" is not declared by Package "${input.assignment.packageId}"`;
513
+ if (!connection) {
514
+ return `Connection "${input.model.connectionId}" is unavailable; reconnect it or choose another model`;
545
515
  }
546
- if (capability.connectionTypes.length === 0) {
547
- return input.assignment.connectionId
548
- ? `Capability "${input.assignment.capabilityId}" does not accept a Connection`
549
- : undefined;
516
+ if (connection.state !== "ready") {
517
+ return `Connection "${input.model.connectionId}" is ${connection.state}; enable or reconnect it`;
550
518
  }
551
- if (!input.assignment.connectionId) {
552
- return `Capability "${input.assignment.capabilityId}" requires a Connection`;
519
+ const installation = input.user.packages.find(
520
+ (candidate) => candidate.packageId === connection.packageId,
521
+ );
522
+ if (!installation || installation.state !== "installed") {
523
+ return `Package "${connection.packageId}" is not installed and enabled; enable it to use Connection "${connection.connectionId}"`;
553
524
  }
554
- const connection = input.user.connections.find(
555
- (candidate) => candidate.connectionId === input.assignment.connectionId,
525
+ const pkg = input.packages.find(
526
+ (candidate) =>
527
+ candidate.packageId === connection.packageId &&
528
+ candidate.version === installation.version,
556
529
  );
557
- if (
558
- !connection ||
559
- connection.packageId !== input.assignment.packageId ||
560
- connection.state !== "ready"
561
- ) {
562
- return `Connection "${input.assignment.connectionId}" is not a ready Connection for Package "${input.assignment.packageId}"`;
530
+ if (!pkg) {
531
+ return `Installed Package "${connection.packageId}" version "${installation.version}" is unavailable`;
563
532
  }
564
533
  const connectionType = pkg.connectionTypes.find(
565
534
  (candidate) => candidate.id === connection.connectionTypeId,
566
535
  );
567
- if (
568
- !connectionType ||
569
- !capability.connectionTypes.includes(connectionType.id) ||
570
- !connectionType.capabilities.includes(capability.id)
571
- ) {
572
- return `Connection "${input.assignment.connectionId}" has an incompatible Connection Type`;
536
+ const modelCapability = pkg.capabilities.find(
537
+ (candidate) =>
538
+ candidate.kind === "model" &&
539
+ candidate.connectionTypes.includes(connection.connectionTypeId) &&
540
+ connectionType?.capabilities.includes(candidate.id),
541
+ );
542
+ if (!connectionType || !modelCapability) {
543
+ return `Connection "${connection.connectionId}" does not provide a model Capability accepted by Package "${pkg.packageId}"`;
573
544
  }
574
545
  return undefined;
575
546
  }
576
547
 
577
548
  export interface ResolvedModelBindingV1 {
578
- assignment: ModelAssignment;
549
+ model?: ModelBindingV1;
579
550
  state: "ready" | "requires-resolution" | "unavailable";
580
551
  connection?: ConnectionView;
581
552
  packageId?: string;
@@ -584,65 +555,39 @@ export interface ResolvedModelBindingV1 {
584
555
  }
585
556
 
586
557
  export function resolveBotModelBindingV1(input: {
587
- model: ModelAssignment;
588
- assignments: readonly CapabilityAssignmentView[];
558
+ model: ModelBindingV1;
589
559
  user: UserSettingsViewV1;
590
560
  packages: readonly ExecutionPackageDefinition[];
591
561
  }): ResolvedModelBindingV1 {
592
562
  const unavailable = (failure: string): ResolvedModelBindingV1 => ({
593
- assignment: structuredClone(input.model),
563
+ model: structuredClone(input.model),
594
564
  state: "unavailable",
595
565
  failure,
596
566
  });
567
+ const failure = modelBindingFailureV1(input);
568
+ if (failure) return unavailable(failure);
597
569
  const connection = input.user.connections.find(
598
570
  (candidate) => candidate.connectionId === input.model.connectionId,
599
- );
600
- if (!connection) return unavailable("Connection is unavailable");
601
- if (connection.state !== "ready") {
602
- return unavailable(`Connection is ${connection.state}`);
603
- }
571
+ )!;
604
572
  const installation = input.user.packages.find(
605
573
  (candidate) => candidate.packageId === connection.packageId,
606
- );
607
- if (!installation || installation.state !== "installed") {
608
- return unavailable("Connection Package is not installed and enabled");
609
- }
574
+ )!;
610
575
  const pkg = input.packages.find(
611
576
  (candidate) =>
612
577
  candidate.packageId === connection.packageId &&
613
578
  candidate.version === installation.version,
614
- );
615
- const connectionType = pkg?.connectionTypes.find(
616
- (candidate) => candidate.id === connection.connectionTypeId,
617
- );
618
- const modelCapability = pkg?.capabilities.find(
619
- (candidate) =>
620
- candidate.kind === "model" &&
621
- connectionType?.capabilities.includes(candidate.id) &&
622
- candidate.connectionTypes.includes(connection.connectionTypeId),
623
- );
624
- if (!pkg || !connectionType || !modelCapability) {
625
- return unavailable("Connection does not provide models");
626
- }
627
- const assignment = input.assignments.find(
628
- (candidate) =>
629
- candidate.packageId === pkg.packageId &&
630
- candidate.capabilityId === modelCapability.id &&
631
- candidate.connectionId === connection.connectionId &&
632
- candidate.state === "enabled",
633
- );
634
- if (!assignment) {
635
- return unavailable("Bot is not assigned the Connection model capability");
636
- }
579
+ )!;
637
580
  if (!connection.providerType) {
638
- return unavailable("Connection provider type is unavailable");
581
+ return unavailable(
582
+ `Connection "${connection.connectionId}" provider type is unavailable`,
583
+ );
639
584
  }
640
585
  const knownModel = connection.modelCatalog?.models.some(
641
586
  (candidate: { providerModelId: string }) =>
642
587
  candidate.providerModelId === input.model.providerModelId,
643
588
  );
644
589
  return {
645
- assignment: structuredClone(input.model),
590
+ model: structuredClone(input.model),
646
591
  state: knownModel ? "ready" : "requires-resolution",
647
592
  connection: structuredClone(connection),
648
593
  packageId: pkg.packageId,
@@ -652,35 +597,119 @@ export function resolveBotModelBindingV1(input: {
652
597
 
653
598
  export interface EffectiveBotModelV1 {
654
599
  /**
655
- * "bot" when the Bot overrides the User default, "default" when the Bot
656
- * follows `UserSettingsViewV1.newBotModelTemplate`, "none" when neither is
657
- * set.
600
+ * The Package setting scope that supplied the model, or the platform
601
+ * bootstrap when no enabled model-choice Package supplied one.
658
602
  */
659
- source: "bot" | "default" | "none";
660
- model?: ModelAssignment;
603
+ source: "bot" | "account" | "platform" | "none";
604
+ model?: ModelBindingV1;
661
605
  binding?: ResolvedModelBindingV1;
662
606
  }
663
607
 
664
608
  /**
665
- * The model a Bot actually runs on. A Bot without its own `model` follows the
666
- * User's default dynamically, so changing the default changes every Bot that
667
- * has not overridden it. Authority is unchanged: the returned binding is still
668
- * resolved against the Bot's own Assignments, so a Bot that has never claimed
669
- * the Connection's model Capability resolves "unavailable" until it does.
609
+ * The model a Bot actually runs on. The kernel knows only the manifest role:
610
+ * Bot value, User value, then platform bootstrap. Disabled Package values stay
611
+ * present but are inert, as required by AGENTS.md Configuration shape.
670
612
  */
671
613
  export function resolveEffectiveBotModelV1(input: {
672
- bot: Pick<BotSettingsViewV1, "model" | "assignments">;
614
+ bot: Pick<BotSettingsViewV1, "packageValues">;
673
615
  user: UserSettingsViewV1;
674
616
  packages: readonly ExecutionPackageDefinition[];
675
617
  }): EffectiveBotModelV1 {
676
- const model = input.bot.model ?? input.user.newBotModelTemplate;
618
+ const enabled = input.user.packages
619
+ .filter((installation) => installation.state === "installed")
620
+ .map((installation) => ({
621
+ installation,
622
+ pkg: input.packages.find(
623
+ (candidate) =>
624
+ candidate.packageId === installation.packageId &&
625
+ candidate.version === installation.version,
626
+ ),
627
+ }))
628
+ .filter(
629
+ (
630
+ candidate,
631
+ ): candidate is {
632
+ installation: PackageInstallationView;
633
+ pkg: ExecutionPackageDefinition;
634
+ } => candidate.pkg !== undefined,
635
+ );
636
+
637
+ const fromScope = (
638
+ scope: "user" | "bot",
639
+ ): { model?: ModelBindingV1; conflict?: string } | undefined => {
640
+ const declarations = enabled
641
+ .map(({ installation, pkg }) => ({
642
+ installation,
643
+ pkg,
644
+ definition: pkg.settings
645
+ .filter(
646
+ (definition) =>
647
+ definition.role === "model" && definition.scopes.includes(scope),
648
+ )
649
+ .sort((left, right) => compareIdentifiers(left.id, right.id))[0],
650
+ }))
651
+ .filter(
652
+ (
653
+ candidate,
654
+ ): candidate is typeof candidate & {
655
+ definition: PackageSettingDefinition;
656
+ } => candidate.definition !== undefined,
657
+ )
658
+ .sort((left, right) =>
659
+ compareIdentifiers(left.pkg.packageId, right.pkg.packageId),
660
+ );
661
+ if (declarations.length > 1) {
662
+ const names = declarations.map(({ pkg }) => `"${pkg.packageId}"`);
663
+ return {
664
+ conflict: `Enabled Packages ${names.join(" and ")} both declare the model role at ${scope === "user" ? "User" : "Bot"} scope; disable one Package`,
665
+ };
666
+ }
667
+ const declaration = declarations[0];
668
+ if (!declaration) return undefined;
669
+ const stored =
670
+ scope === "bot"
671
+ ? Object.hasOwn(input.bot.packageValues, declaration.pkg.packageId)
672
+ ? input.bot.packageValues[declaration.pkg.packageId]
673
+ : undefined
674
+ : declaration.installation.values;
675
+ const value = resolvePackageSettingValuesV1(
676
+ declaration.pkg.settings,
677
+ stored,
678
+ scope,
679
+ )[declaration.definition.id];
680
+ return {
681
+ ...(typeof value === "object" && value !== null ? { model: value } : {}),
682
+ };
683
+ };
684
+
685
+ for (const scope of ["bot", "user"] as const) {
686
+ const resolved = fromScope(scope);
687
+ if (resolved?.conflict) {
688
+ return {
689
+ source: scope === "bot" ? "bot" : "account",
690
+ binding: { state: "unavailable", failure: resolved.conflict },
691
+ };
692
+ }
693
+ if (resolved?.model) {
694
+ return {
695
+ source: scope === "bot" ? "bot" : "account",
696
+ model: structuredClone(resolved.model),
697
+ binding: resolveBotModelBindingV1({
698
+ model: resolved.model,
699
+ user: input.user,
700
+ packages: input.packages,
701
+ }),
702
+ };
703
+ }
704
+ }
705
+
706
+ const model = input.user.platformModel;
677
707
  if (!model) return { source: "none" };
678
708
  return {
679
- source: input.bot.model ? "bot" : "default",
709
+ source: "platform",
680
710
  model: structuredClone(model),
681
711
  binding: resolveBotModelBindingV1({
682
712
  model,
683
- assignments: input.bot.assignments,
684
713
  user: input.user,
685
714
  packages: input.packages,
686
715
  }),
@@ -692,25 +721,53 @@ export function resolveBotExecutionPlanV1(input: {
692
721
  user: UserSettingsViewV1;
693
722
  packages: readonly ExecutionPackageDefinition[];
694
723
  }): BotExecutionPlanV1 {
695
- const assignments = input.bot.assignments.map((assignment) => {
696
- if (assignment.state !== "enabled") return structuredClone(assignment);
697
- if (
698
- capabilityAssignmentFailureV1({
699
- assignment,
700
- user: input.user,
701
- packages: input.packages,
702
- })
703
- ) {
704
- return { ...assignment, state: "unavailable" as const };
705
- }
706
- return structuredClone(assignment);
724
+ const capabilities = input.user.packages.flatMap((installation) => {
725
+ if (installation.state !== "installed") return [];
726
+ const pkg = input.packages.find(
727
+ (candidate) =>
728
+ candidate.packageId === installation.packageId &&
729
+ candidate.version === installation.version,
730
+ );
731
+ if (!pkg) return [];
732
+ return pkg.capabilities.flatMap((capability): EnabledCapabilityV1[] => {
733
+ const base = {
734
+ packageId: pkg.packageId,
735
+ capabilityId: capability.id,
736
+ kind: capability.kind,
737
+ };
738
+ if (capability.connectionTypes.length === 0) return [base];
739
+ return input.user.connections
740
+ .filter((connection) => {
741
+ if (
742
+ connection.packageId !== pkg.packageId ||
743
+ connection.state !== "ready" ||
744
+ !capability.connectionTypes.includes(connection.connectionTypeId)
745
+ ) {
746
+ return false;
747
+ }
748
+ return pkg.connectionTypes
749
+ .find((candidate) => candidate.id === connection.connectionTypeId)
750
+ ?.capabilities.includes(capability.id);
751
+ })
752
+ .map((connection) => ({
753
+ ...base,
754
+ connectionId: connection.connectionId,
755
+ }));
756
+ });
707
757
  });
758
+ capabilities.sort(
759
+ (left, right) =>
760
+ compareIdentifiers(left.packageId, right.packageId) ||
761
+ compareIdentifiers(left.capabilityId, right.capabilityId) ||
762
+ compareIdentifiers(left.connectionId ?? "", right.connectionId ?? ""),
763
+ );
764
+ const effective = resolveEffectiveBotModelV1(input);
708
765
  return {
709
766
  schemaVersion: 1,
710
767
  botId: input.bot.botId,
711
768
  revision: input.bot.revision,
712
- model: input.bot.model ? structuredClone(input.bot.model) : undefined,
713
- assignments,
769
+ ...(effective.model ? { model: structuredClone(effective.model) } : {}),
770
+ capabilities,
714
771
  };
715
772
  }
716
773
 
@@ -737,7 +794,17 @@ function record(value: unknown, label: string): Record<string, unknown> {
737
794
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
738
795
  throw new ConfigurationDecodeError(`${label} must be an object`);
739
796
  }
740
- return value as Record<string, unknown>;
797
+ const candidate = value as Record<string, unknown>;
798
+ const prototype = Object.getPrototypeOf(candidate);
799
+ if (prototype !== Object.prototype && prototype !== null) {
800
+ throw new ConfigurationDecodeError(`${label} must be a plain object`);
801
+ }
802
+ for (const key in candidate) {
803
+ if (!Object.hasOwn(candidate, key)) {
804
+ throw new ConfigurationDecodeError(`${label} has inherited fields`);
805
+ }
806
+ }
807
+ return candidate;
741
808
  }
742
809
 
743
810
  export function decodeBotIdV1(value: unknown, label = "botId"): string {
@@ -837,36 +904,6 @@ export function decodeRevokeConnectionCommandV1(
837
904
  return { schemaVersion: 1, type: "connection/revoke" };
838
905
  }
839
906
 
840
- export function decodeConnectionDependencyRequirementV1(
841
- input: unknown,
842
- ): ConnectionDependencyRequirementV1 {
843
- const value = exactRecord(input, "Connection dependency requirement", [
844
- "schemaVersion",
845
- "packageId",
846
- "packageVersion",
847
- "capabilityId",
848
- "connectionTypeIds",
849
- ]);
850
- schemaVersion(value);
851
- if (
852
- !Array.isArray(value.connectionTypeIds) ||
853
- value.connectionTypeIds.length === 0
854
- ) {
855
- throw new ConfigurationDecodeError(
856
- "Connection dependency requirement connectionTypeIds are invalid",
857
- );
858
- }
859
- return {
860
- schemaVersion: 1,
861
- packageId: identifier(value.packageId, "packageId"),
862
- packageVersion: text(value.packageVersion, "packageVersion", 128),
863
- capabilityId: identifier(value.capabilityId, "capabilityId"),
864
- connectionTypeIds: value.connectionTypeIds.map((item) =>
865
- identifier(item, "connectionTypeId"),
866
- ),
867
- };
868
- }
869
-
870
907
  function text(value: unknown, label: string, maximum: number): string {
871
908
  if (typeof value !== "string") {
872
909
  throw new ConfigurationDecodeError(`${label} must be a string`);
@@ -1093,26 +1130,6 @@ function notifications(value: unknown): BotNotificationPolicy {
1093
1130
  return { enabled: policy.enabled };
1094
1131
  }
1095
1132
 
1096
- /** The exact Bot model DTO, decoded wherever it crosses a durable seam. */
1097
- export function decodeModelAssignmentV1(value: unknown): ModelAssignment {
1098
- return model(value);
1099
- }
1100
-
1101
- function model(value: unknown): ModelAssignment {
1102
- const assignment = exactRecord(value, "model", [
1103
- "connectionId",
1104
- "providerModelId",
1105
- ]);
1106
- return {
1107
- connectionId: identifier(assignment.connectionId, "model.connectionId"),
1108
- providerModelId: text(
1109
- assignment.providerModelId,
1110
- "model.providerModelId",
1111
- 256,
1112
- ),
1113
- };
1114
- }
1115
-
1116
1133
  export function decodeConfigurationQueryV1(
1117
1134
  input: unknown,
1118
1135
  ): ConfigurationQueryV1 {
@@ -1161,30 +1178,19 @@ export function decodeConfigurationCommandV1(
1161
1178
  },
1162
1179
  };
1163
1180
  }
1164
- case "user/set-new-bot-model": {
1165
- const command = exactCommand(input, ["source"], ["model"]);
1166
- if (command.source !== "user" && command.source !== "auto") {
1167
- throw new ConfigurationDecodeError(
1168
- "new Bot model source must be user or auto",
1169
- );
1170
- }
1171
- if (command.source === "auto" && command.model === undefined) {
1172
- throw new ConfigurationDecodeError(
1173
- "an automatic new Bot model must name a model",
1174
- );
1175
- }
1181
+ case "user/set-platform-model": {
1182
+ const command = exactCommand(input, ["model"]);
1176
1183
  return {
1177
1184
  ...commandMeta(command),
1178
1185
  type: value.type,
1179
- model: command.model === undefined ? undefined : model(command.model),
1180
- source: command.source,
1186
+ model: decodeModelBindingV1(command.model),
1181
1187
  };
1182
1188
  }
1183
1189
  case "user/install-package": {
1184
1190
  const command = exactCommand(
1185
1191
  input,
1186
1192
  ["packageId", "version"],
1187
- ["catalogId", "catalogGeneration", "values"],
1193
+ ["catalogId", "catalogGeneration", "values", "enabled"],
1188
1194
  );
1189
1195
  // A Catalog install is all three of identity, generation and (optional)
1190
1196
  // values or none of them: half a Catalog install would be an install
@@ -1202,11 +1208,18 @@ export function decodeConfigurationCommandV1(
1202
1208
  "install values require a Catalog entry",
1203
1209
  );
1204
1210
  }
1211
+ if (
1212
+ command.enabled !== undefined &&
1213
+ typeof command.enabled !== "boolean"
1214
+ ) {
1215
+ throw new ConfigurationDecodeError("enabled is invalid");
1216
+ }
1205
1217
  return {
1206
1218
  ...commandMeta(command),
1207
1219
  type: value.type,
1208
1220
  packageId: identifier(command.packageId, "packageId"),
1209
1221
  version: text(command.version, "version", 100),
1222
+ ...(command.enabled === undefined ? {} : { enabled: command.enabled }),
1210
1223
  ...(command.catalogId === undefined
1211
1224
  ? {}
1212
1225
  : {
@@ -1242,12 +1255,22 @@ export function decodeConfigurationCommandV1(
1242
1255
  };
1243
1256
  }
1244
1257
  case "user/set-package-settings": {
1245
- const command = exactCommand(input, ["packageId", "values"]);
1258
+ const command = exactCommand(input, ["packageId"], ["values", "unset"]);
1259
+ const values =
1260
+ command.values === undefined
1261
+ ? undefined
1262
+ : packageSettingsPatch(command.values);
1263
+ const unset =
1264
+ command.unset === undefined
1265
+ ? undefined
1266
+ : packageSettingIds(command.unset);
1267
+ requirePackageSettingsChange(values, unset);
1246
1268
  return {
1247
1269
  ...commandMeta(command),
1248
1270
  type: value.type,
1249
1271
  packageId: identifier(command.packageId, "packageId"),
1250
- values: packageSettingsPatch(command.values),
1272
+ ...(values ? { values } : {}),
1273
+ ...(unset ? { unset } : {}),
1251
1274
  };
1252
1275
  }
1253
1276
  case "bot/update-profile": {
@@ -1296,34 +1319,28 @@ export function decodeConfigurationCommandV1(
1296
1319
  notifications: notifications(command.notifications),
1297
1320
  };
1298
1321
  }
1299
- case "bot/select-model": {
1300
- const command = exactCommand(input, ["botId", "model"]);
1301
- return {
1302
- ...commandMeta(command),
1303
- type: value.type,
1304
- botId: identifier(command.botId, "botId"),
1305
- model: model(command.model),
1306
- };
1307
- }
1308
- case "bot/assign-capability":
1309
- case "bot/replace-capability": {
1310
- const command = exactCommand(input, ["botId", "assignment"], ["model"]);
1311
- return {
1312
- ...commandMeta(command),
1313
- type: value.type,
1314
- botId: identifier(command.botId, "botId"),
1315
- assignment: assignmentTarget(command.assignment, "assignment"),
1316
- model: command.model === undefined ? undefined : model(command.model),
1317
- };
1318
- }
1319
- case "bot/unassign-capability":
1320
- case "bot/unbind-model": {
1321
- const command = exactCommand(input, ["botId", "assignmentId"]);
1322
+ case "bot/set-package-settings": {
1323
+ const command = exactCommand(
1324
+ input,
1325
+ ["botId", "packageId"],
1326
+ ["values", "unset"],
1327
+ );
1328
+ const values =
1329
+ command.values === undefined
1330
+ ? undefined
1331
+ : packageSettingsPatch(command.values);
1332
+ const unset =
1333
+ command.unset === undefined
1334
+ ? undefined
1335
+ : packageSettingIds(command.unset);
1336
+ requirePackageSettingsChange(values, unset);
1322
1337
  return {
1323
1338
  ...commandMeta(command),
1324
1339
  type: value.type,
1325
1340
  botId: identifier(command.botId, "botId"),
1326
- assignmentId: identifier(command.assignmentId, "assignmentId"),
1341
+ packageId: identifier(command.packageId, "packageId"),
1342
+ ...(values ? { values } : {}),
1343
+ ...(unset ? { unset } : {}),
1327
1344
  };
1328
1345
  }
1329
1346
  default:
@@ -1423,11 +1440,41 @@ function safeJsonValue(value: unknown, label: string): JsonValue {
1423
1440
  return value as JsonValue;
1424
1441
  }
1425
1442
  if (Array.isArray(value)) {
1443
+ if (
1444
+ Object.getPrototypeOf(value) !== Array.prototype ||
1445
+ Reflect.ownKeys(value).some(
1446
+ (key) =>
1447
+ typeof key !== "string" ||
1448
+ (key !== "length" &&
1449
+ (!/^(0|[1-9][0-9]*)$/.test(key) ||
1450
+ Number(key) >= value.length ||
1451
+ !Object.getOwnPropertyDescriptor(value, key)?.enumerable)),
1452
+ ) ||
1453
+ Array.from({ length: value.length }, (_item, index) => index).some(
1454
+ (index) => !Object.hasOwn(value, index),
1455
+ )
1456
+ ) {
1457
+ throw new ConfigurationDecodeError(`${label} is not plain JSON`);
1458
+ }
1426
1459
  return value.map((item) => safeJsonValue(item, label));
1427
1460
  }
1428
1461
  if (typeof value === "object" && value !== null) {
1462
+ const object = record(value, label);
1463
+ if (
1464
+ Reflect.ownKeys(object).some((key) => {
1465
+ const descriptor = Object.getOwnPropertyDescriptor(object, key);
1466
+ return (
1467
+ typeof key !== "string" ||
1468
+ !descriptor ||
1469
+ !("value" in descriptor) ||
1470
+ !descriptor.enumerable
1471
+ );
1472
+ })
1473
+ ) {
1474
+ throw new ConfigurationDecodeError(`${label} is not plain JSON`);
1475
+ }
1429
1476
  return Object.fromEntries(
1430
- Object.entries(value).map(([key, item]) => [
1477
+ Object.entries(object).map(([key, item]) => [
1431
1478
  key,
1432
1479
  safeJsonValue(item, label),
1433
1480
  ]),
@@ -1471,15 +1518,29 @@ function installValues(value: unknown): Record<string, JsonValue> {
1471
1518
  * The shape of a `user/set-package-settings` payload, before anything knows
1472
1519
  * which Package it is for.
1473
1520
  *
1474
- * Only the shape: ids are identifiers, values are scalars, and the bag is
1475
- * bounded. Whether a named setting exists, and whether its value satisfies the
1476
- * schema the Package declared, is the User Durable Object's answer it is the
1477
- * authority that holds the installed version.
1521
+ * Only the shape: ids are identifiers, ordinary values are scalars, the model
1522
+ * role may carry its exact binding object, and the bag is bounded. Whether a
1523
+ * named setting exists, and whether its value satisfies the schema the Package
1524
+ * declared, is the owning Durable Object's answer — it validates against the
1525
+ * installed version through `decodeInstalledPackageSettingsPatchV1`.
1478
1526
  */
1479
1527
  function packageSettingsPatch(
1480
1528
  value: unknown,
1481
1529
  ): Record<string, PackageSettingValueV1> {
1482
1530
  const values = record(value, "values");
1531
+ if (
1532
+ Reflect.ownKeys(values).some((key) => {
1533
+ const descriptor = Object.getOwnPropertyDescriptor(values, key);
1534
+ return (
1535
+ typeof key !== "string" ||
1536
+ !descriptor ||
1537
+ !("value" in descriptor) ||
1538
+ !descriptor.enumerable
1539
+ );
1540
+ })
1541
+ ) {
1542
+ throw new ConfigurationDecodeError("values has invalid fields");
1543
+ }
1483
1544
  const entries = Object.entries(values);
1484
1545
  if (entries.length === 0) {
1485
1546
  throw new ConfigurationDecodeError("values names no setting");
@@ -1489,6 +1550,9 @@ function packageSettingsPatch(
1489
1550
  }
1490
1551
  return Object.fromEntries(
1491
1552
  entries.map(([key, item]) => {
1553
+ if (typeof item === "object" && item !== null && !Array.isArray(item)) {
1554
+ return [identifier(key, "values key"), decodeModelBindingV1(item)];
1555
+ }
1492
1556
  if (
1493
1557
  typeof item !== "string" &&
1494
1558
  typeof item !== "number" &&
@@ -1510,6 +1574,34 @@ function packageSettingsPatch(
1510
1574
  );
1511
1575
  }
1512
1576
 
1577
+ function packageSettingIds(value: unknown): string[] {
1578
+ if (!Array.isArray(value) || value.length === 0) {
1579
+ throw new ConfigurationDecodeError("unset names no setting");
1580
+ }
1581
+ if (value.length > MAX_PACKAGE_SETTINGS_V1) {
1582
+ throw new ConfigurationDecodeError("unset is too large");
1583
+ }
1584
+ const decoded = value.map((item) => identifier(item, "unset setting id"));
1585
+ if (new Set(decoded).size !== decoded.length) {
1586
+ throw new ConfigurationDecodeError("unset repeats a setting");
1587
+ }
1588
+ return decoded;
1589
+ }
1590
+
1591
+ function requirePackageSettingsChange(
1592
+ values: Record<string, PackageSettingValueV1> | undefined,
1593
+ unset: readonly string[] | undefined,
1594
+ ): void {
1595
+ if (!values && !unset) {
1596
+ throw new ConfigurationDecodeError("Package settings command is empty");
1597
+ }
1598
+ if (values && unset?.some((settingId) => Object.hasOwn(values, settingId))) {
1599
+ throw new ConfigurationDecodeError(
1600
+ "Package settings command both sets and unsets a setting",
1601
+ );
1602
+ }
1603
+ }
1604
+
1513
1605
  function viewRevision(value: unknown): number {
1514
1606
  if (!Number.isSafeInteger(value) || (value as number) < 0) {
1515
1607
  throw new ConfigurationDecodeError("configuration revision is invalid");
@@ -1679,101 +1771,6 @@ export function decodePendingAuthorizationV1(
1679
1771
  };
1680
1772
  }
1681
1773
 
1682
- export function decodeCapabilityAssignmentV1(
1683
- value: unknown,
1684
- ): CapabilityAssignmentView {
1685
- const assignment = exactRecord(
1686
- value,
1687
- "Capability Assignment",
1688
- ["assignmentId", "packageId", "capabilityId", "state"],
1689
- ["connectionId"],
1690
- );
1691
- if (
1692
- assignment.state !== "enabled" &&
1693
- assignment.state !== "disabled" &&
1694
- assignment.state !== "unavailable"
1695
- ) {
1696
- throw new ConfigurationDecodeError(
1697
- "Capability Assignment state is invalid",
1698
- );
1699
- }
1700
- return {
1701
- assignmentId: identifier(assignment.assignmentId, "assignmentId"),
1702
- packageId: identifier(assignment.packageId, "assignment.packageId"),
1703
- capabilityId: identifier(
1704
- assignment.capabilityId,
1705
- "assignment.capabilityId",
1706
- ),
1707
- connectionId:
1708
- assignment.connectionId === undefined
1709
- ? undefined
1710
- : identifier(assignment.connectionId, "assignment.connectionId"),
1711
- state: assignment.state,
1712
- };
1713
- }
1714
-
1715
- function assignmentTarget(
1716
- value: unknown,
1717
- label = "assignment target",
1718
- ): Omit<CapabilityAssignmentView, "state"> {
1719
- const assignment = exactRecord(
1720
- value,
1721
- label,
1722
- ["assignmentId", "packageId", "capabilityId"],
1723
- ["connectionId"],
1724
- );
1725
- return {
1726
- assignmentId: identifier(assignment.assignmentId, `${label}.assignmentId`),
1727
- packageId: identifier(assignment.packageId, `${label}.packageId`),
1728
- capabilityId: identifier(assignment.capabilityId, `${label}.capabilityId`),
1729
- connectionId:
1730
- assignment.connectionId === undefined
1731
- ? undefined
1732
- : identifier(assignment.connectionId, `${label}.connectionId`),
1733
- };
1734
- }
1735
-
1736
- function assignmentOperation(
1737
- value: unknown,
1738
- ): CapabilityAssignmentOperationViewV1 {
1739
- const operation = exactRecord(
1740
- value,
1741
- "Assignment operation",
1742
- ["commandId", "kind", "assignmentId", "state"],
1743
- ["target"],
1744
- );
1745
- if (
1746
- operation.kind !== "assigning" &&
1747
- operation.kind !== "replacing" &&
1748
- operation.kind !== "unassigning"
1749
- ) {
1750
- throw new ConfigurationDecodeError("Assignment operation kind is invalid");
1751
- }
1752
- if (operation.state !== "pending" && operation.state !== "retrying") {
1753
- throw new ConfigurationDecodeError("Assignment operation state is invalid");
1754
- }
1755
- if (operation.kind !== "unassigning" && operation.target === undefined) {
1756
- throw new ConfigurationDecodeError(
1757
- "Assignment operation target is required",
1758
- );
1759
- }
1760
- if (operation.kind === "unassigning" && operation.target !== undefined) {
1761
- throw new ConfigurationDecodeError(
1762
- "Unassign operation cannot have a target",
1763
- );
1764
- }
1765
- return {
1766
- commandId: identifier(operation.commandId, "operation.commandId"),
1767
- kind: operation.kind,
1768
- assignmentId: identifier(operation.assignmentId, "operation.assignmentId"),
1769
- state: operation.state,
1770
- target:
1771
- operation.target === undefined
1772
- ? undefined
1773
- : assignmentTarget(operation.target),
1774
- };
1775
- }
1776
-
1777
1774
  function schemaVersion(value: Record<string, unknown>): void {
1778
1775
  if (value.schemaVersion !== 1) {
1779
1776
  throw new ConfigurationDecodeError("unsupported configuration schema");
@@ -1785,12 +1782,7 @@ export function decodeUserSettingsViewV1(input: unknown): UserSettingsViewV1 {
1785
1782
  input,
1786
1783
  "User settings",
1787
1784
  ["schemaVersion", "revision", "profile", "packages", "connections"],
1788
- [
1789
- "newBotModelTemplate",
1790
- "newBotModelTemplateSource",
1791
- "catalogGeneration",
1792
- "catalogIndexHash",
1793
- ],
1785
+ ["platformModel", "catalogGeneration", "catalogIndexHash"],
1794
1786
  );
1795
1787
  schemaVersion(value);
1796
1788
  const profile = exactRecord(value.profile, "profile", ["name"], ["email"]);
@@ -1803,31 +1795,6 @@ export function decodeUserSettingsViewV1(input: unknown): UserSettingsViewV1 {
1803
1795
  "User settings Packages and Connections must be bounded arrays",
1804
1796
  );
1805
1797
  }
1806
- if (
1807
- value.newBotModelTemplateSource !== undefined &&
1808
- value.newBotModelTemplateSource !== "user" &&
1809
- value.newBotModelTemplateSource !== "auto"
1810
- ) {
1811
- throw new ConfigurationDecodeError(
1812
- "new Bot model source must be user or auto",
1813
- );
1814
- }
1815
- if (
1816
- value.newBotModelTemplate !== undefined &&
1817
- value.newBotModelTemplateSource === undefined
1818
- ) {
1819
- throw new ConfigurationDecodeError(
1820
- "a new Bot model must record its source",
1821
- );
1822
- }
1823
- if (
1824
- value.newBotModelTemplate === undefined &&
1825
- value.newBotModelTemplateSource === "auto"
1826
- ) {
1827
- throw new ConfigurationDecodeError(
1828
- "an automatic new Bot model must name a model",
1829
- );
1830
- }
1831
1798
  return {
1832
1799
  schemaVersion: 1,
1833
1800
  revision: viewRevision(value.revision),
@@ -1837,11 +1804,9 @@ export function decodeUserSettingsViewV1(input: unknown): UserSettingsViewV1 {
1837
1804
  },
1838
1805
  packages: value.packages.map(packageInstallation),
1839
1806
  connections: value.connections.map(connectionView),
1840
- newBotModelTemplate:
1841
- value.newBotModelTemplate === undefined
1842
- ? undefined
1843
- : model(value.newBotModelTemplate),
1844
- newBotModelTemplateSource: value.newBotModelTemplateSource,
1807
+ ...(value.platformModel === undefined
1808
+ ? {}
1809
+ : { platformModel: decodeModelBindingV1(value.platformModel) }),
1845
1810
  // The pin is optional — a deployment with no Catalog has none — but never
1846
1811
  // half present: one field alone is a corrupt pin, not a pin.
1847
1812
  ...(value.catalogGeneration === undefined &&
@@ -1861,39 +1826,73 @@ export function decodeUserSettingsViewV1(input: unknown): UserSettingsViewV1 {
1861
1826
  };
1862
1827
  }
1863
1828
 
1864
- export function decodeBotSettingsViewV1(input: unknown): BotSettingsViewV1 {
1865
- const value = exactRecord(
1866
- input,
1867
- "Bot settings",
1868
- [
1869
- "schemaVersion",
1870
- "botId",
1871
- "revision",
1872
- "profile",
1873
- "notifications",
1874
- "assignments",
1875
- "assignmentOperations",
1876
- ],
1877
- ["model"],
1878
- );
1879
- schemaVersion(value);
1829
+ function packageValueBags(
1830
+ input: unknown,
1831
+ ): Record<string, Record<string, unknown>> {
1832
+ const packages = record(input, "packageValues");
1880
1833
  if (
1881
- !Array.isArray(value.assignments) ||
1882
- !Array.isArray(value.assignmentOperations)
1834
+ Reflect.ownKeys(packages).some((key) => {
1835
+ const descriptor = Object.getOwnPropertyDescriptor(packages, key);
1836
+ return (
1837
+ typeof key !== "string" ||
1838
+ !descriptor ||
1839
+ !("value" in descriptor) ||
1840
+ !descriptor.enumerable
1841
+ );
1842
+ })
1883
1843
  ) {
1884
- throw new ConfigurationDecodeError(
1885
- "Bot settings Assignments and operations must be arrays",
1886
- );
1844
+ throw new ConfigurationDecodeError("packageValues has invalid fields");
1887
1845
  }
1846
+ return Object.fromEntries(
1847
+ Object.entries(packages).map(([packageId, stored]) => {
1848
+ identifier(packageId, "packageValues packageId");
1849
+ const values = record(stored, `packageValues.${packageId}`);
1850
+ if (
1851
+ Reflect.ownKeys(values).length > MAX_PACKAGE_SETTINGS_V1 ||
1852
+ Reflect.ownKeys(values).some((key) => {
1853
+ const descriptor = Object.getOwnPropertyDescriptor(values, key);
1854
+ return (
1855
+ typeof key !== "string" ||
1856
+ !descriptor ||
1857
+ !("value" in descriptor) ||
1858
+ !descriptor.enumerable
1859
+ );
1860
+ })
1861
+ ) {
1862
+ throw new ConfigurationDecodeError(
1863
+ `packageValues.${packageId} has invalid fields`,
1864
+ );
1865
+ }
1866
+ return [
1867
+ packageId,
1868
+ Object.fromEntries(
1869
+ Object.entries(values).map(([settingId, value]) => [
1870
+ identifier(settingId, "packageValues settingId"),
1871
+ safeJsonValue(value, `packageValues.${packageId}.${settingId}`),
1872
+ ]),
1873
+ ),
1874
+ ];
1875
+ }),
1876
+ );
1877
+ }
1878
+
1879
+ export function decodeBotSettingsViewV1(input: unknown): BotSettingsViewV1 {
1880
+ const value = exactRecord(input, "Bot settings", [
1881
+ "schemaVersion",
1882
+ "botId",
1883
+ "revision",
1884
+ "profile",
1885
+ "notifications",
1886
+ "packageValues",
1887
+ ]);
1888
+ schemaVersion(value);
1888
1889
  return {
1889
1890
  schemaVersion: 1,
1890
1891
  botId: identifier(value.botId, "botId"),
1891
1892
  revision: viewRevision(value.revision),
1892
1893
  profile: botProfile(value.profile),
1893
1894
  notifications: notifications(value.notifications),
1894
- assignments: value.assignments.map(decodeCapabilityAssignmentV1),
1895
- assignmentOperations: value.assignmentOperations.map(assignmentOperation),
1896
- model: value.model === undefined ? undefined : model(value.model),
1895
+ packageValues: packageValueBags(value.packageValues),
1897
1896
  };
1898
1897
  }
1899
1898