@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/package.json +3 -3
- package/src/bot-identity.test.ts +1 -2
- package/src/index.test.ts +381 -319
- package/src/index.ts +438 -439
- package/src/package-settings.test.ts +100 -0
- package/src/package-settings.ts +209 -28
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
|
-
*
|
|
219
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
type: "bot/
|
|
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
|
-
|
|
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?:
|
|
506
|
-
|
|
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
|
|
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
|
-
|
|
524
|
-
|
|
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
|
|
529
|
-
(
|
|
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 (!
|
|
544
|
-
return `
|
|
513
|
+
if (!connection) {
|
|
514
|
+
return `Connection "${input.model.connectionId}" is unavailable; reconnect it or choose another model`;
|
|
545
515
|
}
|
|
546
|
-
if (
|
|
547
|
-
return input.
|
|
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
|
-
|
|
552
|
-
|
|
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
|
|
555
|
-
(candidate) =>
|
|
525
|
+
const pkg = input.packages.find(
|
|
526
|
+
(candidate) =>
|
|
527
|
+
candidate.packageId === connection.packageId &&
|
|
528
|
+
candidate.version === installation.version,
|
|
556
529
|
);
|
|
557
|
-
if (
|
|
558
|
-
|
|
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
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
*
|
|
656
|
-
*
|
|
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" | "
|
|
660
|
-
model?:
|
|
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.
|
|
666
|
-
* User
|
|
667
|
-
*
|
|
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, "
|
|
614
|
+
bot: Pick<BotSettingsViewV1, "packageValues">;
|
|
673
615
|
user: UserSettingsViewV1;
|
|
674
616
|
packages: readonly ExecutionPackageDefinition[];
|
|
675
617
|
}): EffectiveBotModelV1 {
|
|
676
|
-
const
|
|
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:
|
|
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
|
|
696
|
-
if (
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
) {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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
|
-
|
|
713
|
-
|
|
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
|
-
|
|
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-
|
|
1165
|
-
const command = exactCommand(input, ["
|
|
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:
|
|
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:
|
|
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/
|
|
1300
|
-
const command = exactCommand(
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
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
|
-
|
|
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(
|
|
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,
|
|
1475
|
-
*
|
|
1476
|
-
*
|
|
1477
|
-
*
|
|
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
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
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
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
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
|
-
|
|
1882
|
-
|
|
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
|
-
|
|
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
|
|