@frockbot/configuration-core 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/bot-identity.test.ts +3 -4
- package/src/index.test.ts +567 -319
- package/src/index.ts +659 -446
- 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
|
|
@@ -129,18 +136,20 @@ export interface PackageInstallationView {
|
|
|
129
136
|
catalogId?: string;
|
|
130
137
|
/** The immutable Catalog generation `catalogId` was read from. */
|
|
131
138
|
catalogGeneration?: string;
|
|
139
|
+
/** Exact non-first-party bundle admitted from the Catalog, when it carries code. */
|
|
140
|
+
contentHash?: string;
|
|
132
141
|
provenance?: PackageProvenanceV1;
|
|
133
142
|
/** The setup values the install carried, as GrokBot's `InstallPlugin{values}`. */
|
|
134
|
-
values?: Record<string, JsonValue>;
|
|
143
|
+
values?: Record<string, JsonValue | ModelBindingV1>;
|
|
135
144
|
}
|
|
136
145
|
|
|
137
146
|
/**
|
|
138
147
|
* A durable pending decision for the User: this Connection needs authorizing
|
|
139
148
|
* before it will do anything again.
|
|
140
149
|
*
|
|
141
|
-
* It carries **no URL**, and that is the whole design. A
|
|
142
|
-
*
|
|
143
|
-
* redirect is minted only by an authenticated User action. A single-use
|
|
150
|
+
* It carries **no URL**, and that is the whole design. A mount that meets a
|
|
151
|
+
* 401 may write one, but a Bot has no command that requests authorization and
|
|
152
|
+
* a redirect is minted only by an authenticated User action. A single-use
|
|
144
153
|
* ten-minute link stored in a projection every client reads, and replayed into
|
|
145
154
|
* every transcript, would outlive the decision it belonged to and would let a
|
|
146
155
|
* Bot hand its User a link it authored. The card is drawn from this record and
|
|
@@ -213,12 +222,11 @@ export interface UserSettingsViewV1 {
|
|
|
213
222
|
profile: { name: string; email?: string };
|
|
214
223
|
packages: PackageInstallationView[];
|
|
215
224
|
connections: ConnectionView[];
|
|
216
|
-
newBotModelTemplate?: ModelAssignment;
|
|
217
225
|
/**
|
|
218
|
-
*
|
|
219
|
-
*
|
|
226
|
+
* The model the platform chose for this User. Only a provider bootstrap
|
|
227
|
+
* writes it; no User command may do so (AGENTS.md Configuration shape).
|
|
220
228
|
*/
|
|
221
|
-
|
|
229
|
+
platformModel?: ModelBindingV1;
|
|
222
230
|
/**
|
|
223
231
|
* The remote Catalog generation this User is pinned to, and the content hash
|
|
224
232
|
* of that generation's index. Pinned on the first read that finds a Catalog
|
|
@@ -230,54 +238,27 @@ export interface UserSettingsViewV1 {
|
|
|
230
238
|
catalogIndexHash?: string;
|
|
231
239
|
}
|
|
232
240
|
|
|
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
241
|
export interface BotSettingsViewV1 {
|
|
258
242
|
schemaVersion: 1;
|
|
259
243
|
botId: string;
|
|
260
244
|
revision: number;
|
|
261
245
|
profile: BotProfile;
|
|
262
246
|
notifications: BotNotificationPolicy;
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
247
|
+
/**
|
|
248
|
+
* Package-owned Bot overrides. Disabling a Package leaves these durable but
|
|
249
|
+
* inert so re-enabling restores them (ADR 0019).
|
|
250
|
+
*/
|
|
251
|
+
packageValues: Record<string, Record<string, unknown>>;
|
|
266
252
|
}
|
|
267
253
|
|
|
268
|
-
export function initializeBotSettingsV1(
|
|
269
|
-
botId: string,
|
|
270
|
-
model?: ModelAssignment,
|
|
271
|
-
): BotSettingsViewV1 {
|
|
254
|
+
export function initializeBotSettingsV1(botId: string): BotSettingsViewV1 {
|
|
272
255
|
return {
|
|
273
256
|
schemaVersion: 1,
|
|
274
257
|
botId,
|
|
275
258
|
revision: 0,
|
|
276
259
|
profile: { name: botId === "default" ? "Barebones" : botId },
|
|
277
260
|
notifications: { enabled: true },
|
|
278
|
-
|
|
279
|
-
assignmentOperations: [],
|
|
280
|
-
model: model ? structuredClone(model) : undefined,
|
|
261
|
+
packageValues: {},
|
|
281
262
|
};
|
|
282
263
|
}
|
|
283
264
|
|
|
@@ -299,14 +280,20 @@ export type ConfigurationCommandV1 =
|
|
|
299
280
|
profile: UserSettingsViewV1["profile"];
|
|
300
281
|
})
|
|
301
282
|
| (CommandMetaV1 & {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
283
|
+
/**
|
|
284
|
+
* Only a backend Contribution may issue this command. The User gateway
|
|
285
|
+
* enforces that authority; a User can choose a model only by enabling a
|
|
286
|
+
* Package that owns model-role settings.
|
|
287
|
+
*/
|
|
288
|
+
type: "user/set-platform-model";
|
|
289
|
+
model: ModelBindingV1;
|
|
305
290
|
})
|
|
306
291
|
| (CommandMetaV1 & {
|
|
307
292
|
type: "user/install-package";
|
|
308
293
|
packageId: string;
|
|
309
294
|
version: string;
|
|
295
|
+
/** Installs enabled unless the caller explicitly asks otherwise. */
|
|
296
|
+
enabled?: boolean;
|
|
310
297
|
/**
|
|
311
298
|
* A Catalog install names the entry and the generation it was read
|
|
312
299
|
* from. The User Durable Object refuses a generation other than the one
|
|
@@ -315,14 +302,11 @@ export type ConfigurationCommandV1 =
|
|
|
315
302
|
*/
|
|
316
303
|
catalogId?: string;
|
|
317
304
|
catalogGeneration?: string;
|
|
305
|
+
contentHash?: string;
|
|
318
306
|
values?: Record<string, JsonValue>;
|
|
319
307
|
})
|
|
320
308
|
| (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
|
-
*/
|
|
309
|
+
/** Removes the installation; Connections remain User-owned (ADR 0019). */
|
|
326
310
|
type: "user/uninstall-package";
|
|
327
311
|
packageId: string;
|
|
328
312
|
})
|
|
@@ -342,7 +326,9 @@ export type ConfigurationCommandV1 =
|
|
|
342
326
|
*/
|
|
343
327
|
type: "user/set-package-settings";
|
|
344
328
|
packageId: string;
|
|
345
|
-
values
|
|
329
|
+
values?: Record<string, PackageSettingValueV1>;
|
|
330
|
+
/** Setting ids whose stored values are removed by this command. */
|
|
331
|
+
unset?: string[];
|
|
346
332
|
})
|
|
347
333
|
| (CommandMetaV1 & {
|
|
348
334
|
type: "bot/update-profile";
|
|
@@ -371,31 +357,17 @@ export type ConfigurationCommandV1 =
|
|
|
371
357
|
notifications: BotNotificationPolicy;
|
|
372
358
|
})
|
|
373
359
|
| (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";
|
|
360
|
+
/**
|
|
361
|
+
* A partial update of one installed Package's Bot-scoped setting bag.
|
|
362
|
+
* The Bot authority validates it against that installed version's
|
|
363
|
+
* manifest through the same codec as User-scoped Package settings.
|
|
364
|
+
*/
|
|
365
|
+
type: "bot/set-package-settings";
|
|
397
366
|
botId: string;
|
|
398
|
-
|
|
367
|
+
packageId: string;
|
|
368
|
+
values?: Record<string, PackageSettingValueV1>;
|
|
369
|
+
/** Setting ids whose stored values are removed by this command. */
|
|
370
|
+
unset?: string[];
|
|
399
371
|
});
|
|
400
372
|
|
|
401
373
|
export type UserConfigurationCommandV1 = Exclude<
|
|
@@ -502,16 +474,25 @@ export interface BotExecutionPlanV1 {
|
|
|
502
474
|
schemaVersion: 1;
|
|
503
475
|
botId: string;
|
|
504
476
|
revision: number;
|
|
505
|
-
model?:
|
|
506
|
-
|
|
477
|
+
model?: ModelBindingV1;
|
|
478
|
+
capabilities: EnabledCapabilityV1[];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** One Capability granted account-wide by an enabled Package (ADR 0019). */
|
|
482
|
+
export interface EnabledCapabilityV1 {
|
|
483
|
+
packageId: string;
|
|
484
|
+
capabilityId: string;
|
|
485
|
+
kind: "tool" | "model" | "memory" | "notification" | "computer";
|
|
486
|
+
connectionId?: string;
|
|
507
487
|
}
|
|
508
488
|
|
|
509
489
|
export interface ExecutionPackageDefinition {
|
|
510
490
|
packageId: string;
|
|
511
491
|
version: string;
|
|
492
|
+
settings: PackageSettingDefinition[];
|
|
512
493
|
capabilities: Array<{
|
|
513
494
|
id: string;
|
|
514
|
-
kind
|
|
495
|
+
kind: "tool" | "model" | "memory" | "notification" | "computer";
|
|
515
496
|
connectionTypes: string[];
|
|
516
497
|
}>;
|
|
517
498
|
connectionTypes: Array<{
|
|
@@ -520,62 +501,55 @@ export interface ExecutionPackageDefinition {
|
|
|
520
501
|
}>;
|
|
521
502
|
}
|
|
522
503
|
|
|
523
|
-
|
|
524
|
-
|
|
504
|
+
function compareIdentifiers(left: string, right: string): number {
|
|
505
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export function modelBindingFailureV1(input: {
|
|
509
|
+
model: ModelBindingV1;
|
|
525
510
|
user: UserSettingsViewV1;
|
|
526
511
|
packages: readonly ExecutionPackageDefinition[];
|
|
527
512
|
}): 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,
|
|
513
|
+
const connection = input.user.connections.find(
|
|
514
|
+
(candidate) => candidate.connectionId === input.model.connectionId,
|
|
542
515
|
);
|
|
543
|
-
if (!
|
|
544
|
-
return `
|
|
516
|
+
if (!connection) {
|
|
517
|
+
return `Connection "${input.model.connectionId}" is unavailable; reconnect it or choose another model`;
|
|
545
518
|
}
|
|
546
|
-
if (
|
|
547
|
-
return input.
|
|
548
|
-
? `Capability "${input.assignment.capabilityId}" does not accept a Connection`
|
|
549
|
-
: undefined;
|
|
519
|
+
if (connection.state !== "ready") {
|
|
520
|
+
return `Connection "${input.model.connectionId}" is ${connection.state}; enable or reconnect it`;
|
|
550
521
|
}
|
|
551
|
-
|
|
552
|
-
|
|
522
|
+
const installation = input.user.packages.find(
|
|
523
|
+
(candidate) => candidate.packageId === connection.packageId,
|
|
524
|
+
);
|
|
525
|
+
if (!installation || installation.state !== "installed") {
|
|
526
|
+
return `Package "${connection.packageId}" is not installed and enabled; enable it to use Connection "${connection.connectionId}"`;
|
|
553
527
|
}
|
|
554
|
-
const
|
|
555
|
-
(candidate) =>
|
|
528
|
+
const pkg = input.packages.find(
|
|
529
|
+
(candidate) =>
|
|
530
|
+
candidate.packageId === connection.packageId &&
|
|
531
|
+
candidate.version === installation.version,
|
|
556
532
|
);
|
|
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}"`;
|
|
533
|
+
if (!pkg) {
|
|
534
|
+
return `Installed Package "${connection.packageId}" version "${installation.version}" is unavailable`;
|
|
563
535
|
}
|
|
564
536
|
const connectionType = pkg.connectionTypes.find(
|
|
565
537
|
(candidate) => candidate.id === connection.connectionTypeId,
|
|
566
538
|
);
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
539
|
+
const modelCapability = pkg.capabilities.find(
|
|
540
|
+
(candidate) =>
|
|
541
|
+
candidate.kind === "model" &&
|
|
542
|
+
candidate.connectionTypes.includes(connection.connectionTypeId) &&
|
|
543
|
+
connectionType?.capabilities.includes(candidate.id),
|
|
544
|
+
);
|
|
545
|
+
if (!connectionType || !modelCapability) {
|
|
546
|
+
return `Connection "${connection.connectionId}" does not provide a model Capability accepted by Package "${pkg.packageId}"`;
|
|
573
547
|
}
|
|
574
548
|
return undefined;
|
|
575
549
|
}
|
|
576
550
|
|
|
577
551
|
export interface ResolvedModelBindingV1 {
|
|
578
|
-
|
|
552
|
+
model?: ModelBindingV1;
|
|
579
553
|
state: "ready" | "requires-resolution" | "unavailable";
|
|
580
554
|
connection?: ConnectionView;
|
|
581
555
|
packageId?: string;
|
|
@@ -584,65 +558,39 @@ export interface ResolvedModelBindingV1 {
|
|
|
584
558
|
}
|
|
585
559
|
|
|
586
560
|
export function resolveBotModelBindingV1(input: {
|
|
587
|
-
model:
|
|
588
|
-
assignments: readonly CapabilityAssignmentView[];
|
|
561
|
+
model: ModelBindingV1;
|
|
589
562
|
user: UserSettingsViewV1;
|
|
590
563
|
packages: readonly ExecutionPackageDefinition[];
|
|
591
564
|
}): ResolvedModelBindingV1 {
|
|
592
565
|
const unavailable = (failure: string): ResolvedModelBindingV1 => ({
|
|
593
|
-
|
|
566
|
+
model: structuredClone(input.model),
|
|
594
567
|
state: "unavailable",
|
|
595
568
|
failure,
|
|
596
569
|
});
|
|
570
|
+
const failure = modelBindingFailureV1(input);
|
|
571
|
+
if (failure) return unavailable(failure);
|
|
597
572
|
const connection = input.user.connections.find(
|
|
598
573
|
(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
|
-
}
|
|
574
|
+
)!;
|
|
604
575
|
const installation = input.user.packages.find(
|
|
605
576
|
(candidate) => candidate.packageId === connection.packageId,
|
|
606
|
-
)
|
|
607
|
-
if (!installation || installation.state !== "installed") {
|
|
608
|
-
return unavailable("Connection Package is not installed and enabled");
|
|
609
|
-
}
|
|
577
|
+
)!;
|
|
610
578
|
const pkg = input.packages.find(
|
|
611
579
|
(candidate) =>
|
|
612
580
|
candidate.packageId === connection.packageId &&
|
|
613
581
|
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
|
-
}
|
|
582
|
+
)!;
|
|
637
583
|
if (!connection.providerType) {
|
|
638
|
-
return unavailable(
|
|
584
|
+
return unavailable(
|
|
585
|
+
`Connection "${connection.connectionId}" provider type is unavailable`,
|
|
586
|
+
);
|
|
639
587
|
}
|
|
640
588
|
const knownModel = connection.modelCatalog?.models.some(
|
|
641
589
|
(candidate: { providerModelId: string }) =>
|
|
642
590
|
candidate.providerModelId === input.model.providerModelId,
|
|
643
591
|
);
|
|
644
592
|
return {
|
|
645
|
-
|
|
593
|
+
model: structuredClone(input.model),
|
|
646
594
|
state: knownModel ? "ready" : "requires-resolution",
|
|
647
595
|
connection: structuredClone(connection),
|
|
648
596
|
packageId: pkg.packageId,
|
|
@@ -652,35 +600,119 @@ export function resolveBotModelBindingV1(input: {
|
|
|
652
600
|
|
|
653
601
|
export interface EffectiveBotModelV1 {
|
|
654
602
|
/**
|
|
655
|
-
*
|
|
656
|
-
*
|
|
657
|
-
* set.
|
|
603
|
+
* The Package setting scope that supplied the model, or the platform
|
|
604
|
+
* bootstrap when no enabled model-choice Package supplied one.
|
|
658
605
|
*/
|
|
659
|
-
source: "bot" | "
|
|
660
|
-
model?:
|
|
606
|
+
source: "bot" | "account" | "platform" | "none";
|
|
607
|
+
model?: ModelBindingV1;
|
|
661
608
|
binding?: ResolvedModelBindingV1;
|
|
662
609
|
}
|
|
663
610
|
|
|
664
611
|
/**
|
|
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.
|
|
612
|
+
* The model a Bot actually runs on. The kernel knows only the manifest role:
|
|
613
|
+
* Bot value, User value, then platform bootstrap. Disabled Package values stay
|
|
614
|
+
* present but are inert, as required by AGENTS.md Configuration shape.
|
|
670
615
|
*/
|
|
671
616
|
export function resolveEffectiveBotModelV1(input: {
|
|
672
|
-
bot: Pick<BotSettingsViewV1, "
|
|
617
|
+
bot: Pick<BotSettingsViewV1, "packageValues">;
|
|
673
618
|
user: UserSettingsViewV1;
|
|
674
619
|
packages: readonly ExecutionPackageDefinition[];
|
|
675
620
|
}): EffectiveBotModelV1 {
|
|
676
|
-
const
|
|
621
|
+
const enabled = input.user.packages
|
|
622
|
+
.filter((installation) => installation.state === "installed")
|
|
623
|
+
.map((installation) => ({
|
|
624
|
+
installation,
|
|
625
|
+
pkg: input.packages.find(
|
|
626
|
+
(candidate) =>
|
|
627
|
+
candidate.packageId === installation.packageId &&
|
|
628
|
+
candidate.version === installation.version,
|
|
629
|
+
),
|
|
630
|
+
}))
|
|
631
|
+
.filter(
|
|
632
|
+
(
|
|
633
|
+
candidate,
|
|
634
|
+
): candidate is {
|
|
635
|
+
installation: PackageInstallationView;
|
|
636
|
+
pkg: ExecutionPackageDefinition;
|
|
637
|
+
} => candidate.pkg !== undefined,
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
const fromScope = (
|
|
641
|
+
scope: "user" | "bot",
|
|
642
|
+
): { model?: ModelBindingV1; conflict?: string } | undefined => {
|
|
643
|
+
const declarations = enabled
|
|
644
|
+
.map(({ installation, pkg }) => ({
|
|
645
|
+
installation,
|
|
646
|
+
pkg,
|
|
647
|
+
definition: pkg.settings
|
|
648
|
+
.filter(
|
|
649
|
+
(definition) =>
|
|
650
|
+
definition.role === "model" && definition.scopes.includes(scope),
|
|
651
|
+
)
|
|
652
|
+
.sort((left, right) => compareIdentifiers(left.id, right.id))[0],
|
|
653
|
+
}))
|
|
654
|
+
.filter(
|
|
655
|
+
(
|
|
656
|
+
candidate,
|
|
657
|
+
): candidate is typeof candidate & {
|
|
658
|
+
definition: PackageSettingDefinition;
|
|
659
|
+
} => candidate.definition !== undefined,
|
|
660
|
+
)
|
|
661
|
+
.sort((left, right) =>
|
|
662
|
+
compareIdentifiers(left.pkg.packageId, right.pkg.packageId),
|
|
663
|
+
);
|
|
664
|
+
if (declarations.length > 1) {
|
|
665
|
+
const names = declarations.map(({ pkg }) => `"${pkg.packageId}"`);
|
|
666
|
+
return {
|
|
667
|
+
conflict: `Enabled Packages ${names.join(" and ")} both declare the model role at ${scope === "user" ? "User" : "Bot"} scope; disable one Package`,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
const declaration = declarations[0];
|
|
671
|
+
if (!declaration) return undefined;
|
|
672
|
+
const stored =
|
|
673
|
+
scope === "bot"
|
|
674
|
+
? Object.hasOwn(input.bot.packageValues, declaration.pkg.packageId)
|
|
675
|
+
? input.bot.packageValues[declaration.pkg.packageId]
|
|
676
|
+
: undefined
|
|
677
|
+
: declaration.installation.values;
|
|
678
|
+
const value = resolvePackageSettingValuesV1(
|
|
679
|
+
declaration.pkg.settings,
|
|
680
|
+
stored,
|
|
681
|
+
scope,
|
|
682
|
+
)[declaration.definition.id];
|
|
683
|
+
return {
|
|
684
|
+
...(typeof value === "object" && value !== null ? { model: value } : {}),
|
|
685
|
+
};
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
for (const scope of ["bot", "user"] as const) {
|
|
689
|
+
const resolved = fromScope(scope);
|
|
690
|
+
if (resolved?.conflict) {
|
|
691
|
+
return {
|
|
692
|
+
source: scope === "bot" ? "bot" : "account",
|
|
693
|
+
binding: { state: "unavailable", failure: resolved.conflict },
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
if (resolved?.model) {
|
|
697
|
+
return {
|
|
698
|
+
source: scope === "bot" ? "bot" : "account",
|
|
699
|
+
model: structuredClone(resolved.model),
|
|
700
|
+
binding: resolveBotModelBindingV1({
|
|
701
|
+
model: resolved.model,
|
|
702
|
+
user: input.user,
|
|
703
|
+
packages: input.packages,
|
|
704
|
+
}),
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const model = input.user.platformModel;
|
|
677
710
|
if (!model) return { source: "none" };
|
|
678
711
|
return {
|
|
679
|
-
source:
|
|
712
|
+
source: "platform",
|
|
680
713
|
model: structuredClone(model),
|
|
681
714
|
binding: resolveBotModelBindingV1({
|
|
682
715
|
model,
|
|
683
|
-
assignments: input.bot.assignments,
|
|
684
716
|
user: input.user,
|
|
685
717
|
packages: input.packages,
|
|
686
718
|
}),
|
|
@@ -692,25 +724,53 @@ export function resolveBotExecutionPlanV1(input: {
|
|
|
692
724
|
user: UserSettingsViewV1;
|
|
693
725
|
packages: readonly ExecutionPackageDefinition[];
|
|
694
726
|
}): BotExecutionPlanV1 {
|
|
695
|
-
const
|
|
696
|
-
if (
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
) {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
727
|
+
const capabilities = input.user.packages.flatMap((installation) => {
|
|
728
|
+
if (installation.state !== "installed") return [];
|
|
729
|
+
const pkg = input.packages.find(
|
|
730
|
+
(candidate) =>
|
|
731
|
+
candidate.packageId === installation.packageId &&
|
|
732
|
+
candidate.version === installation.version,
|
|
733
|
+
);
|
|
734
|
+
if (!pkg) return [];
|
|
735
|
+
return pkg.capabilities.flatMap((capability): EnabledCapabilityV1[] => {
|
|
736
|
+
const base = {
|
|
737
|
+
packageId: pkg.packageId,
|
|
738
|
+
capabilityId: capability.id,
|
|
739
|
+
kind: capability.kind,
|
|
740
|
+
};
|
|
741
|
+
if (capability.connectionTypes.length === 0) return [base];
|
|
742
|
+
return input.user.connections
|
|
743
|
+
.filter((connection) => {
|
|
744
|
+
if (
|
|
745
|
+
connection.packageId !== pkg.packageId ||
|
|
746
|
+
connection.state !== "ready" ||
|
|
747
|
+
!capability.connectionTypes.includes(connection.connectionTypeId)
|
|
748
|
+
) {
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
return pkg.connectionTypes
|
|
752
|
+
.find((candidate) => candidate.id === connection.connectionTypeId)
|
|
753
|
+
?.capabilities.includes(capability.id);
|
|
754
|
+
})
|
|
755
|
+
.map((connection) => ({
|
|
756
|
+
...base,
|
|
757
|
+
connectionId: connection.connectionId,
|
|
758
|
+
}));
|
|
759
|
+
});
|
|
707
760
|
});
|
|
761
|
+
capabilities.sort(
|
|
762
|
+
(left, right) =>
|
|
763
|
+
compareIdentifiers(left.packageId, right.packageId) ||
|
|
764
|
+
compareIdentifiers(left.capabilityId, right.capabilityId) ||
|
|
765
|
+
compareIdentifiers(left.connectionId ?? "", right.connectionId ?? ""),
|
|
766
|
+
);
|
|
767
|
+
const effective = resolveEffectiveBotModelV1(input);
|
|
708
768
|
return {
|
|
709
769
|
schemaVersion: 1,
|
|
710
770
|
botId: input.bot.botId,
|
|
711
771
|
revision: input.bot.revision,
|
|
712
|
-
|
|
713
|
-
|
|
772
|
+
...(effective.model ? { model: structuredClone(effective.model) } : {}),
|
|
773
|
+
capabilities,
|
|
714
774
|
};
|
|
715
775
|
}
|
|
716
776
|
|
|
@@ -737,7 +797,17 @@ function record(value: unknown, label: string): Record<string, unknown> {
|
|
|
737
797
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
738
798
|
throw new ConfigurationDecodeError(`${label} must be an object`);
|
|
739
799
|
}
|
|
740
|
-
|
|
800
|
+
const candidate = value as Record<string, unknown>;
|
|
801
|
+
const prototype = Object.getPrototypeOf(candidate);
|
|
802
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
803
|
+
throw new ConfigurationDecodeError(`${label} must be a plain object`);
|
|
804
|
+
}
|
|
805
|
+
for (const key in candidate) {
|
|
806
|
+
if (!Object.hasOwn(candidate, key)) {
|
|
807
|
+
throw new ConfigurationDecodeError(`${label} has inherited fields`);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return candidate;
|
|
741
811
|
}
|
|
742
812
|
|
|
743
813
|
export function decodeBotIdV1(value: unknown, label = "botId"): string {
|
|
@@ -837,36 +907,6 @@ export function decodeRevokeConnectionCommandV1(
|
|
|
837
907
|
return { schemaVersion: 1, type: "connection/revoke" };
|
|
838
908
|
}
|
|
839
909
|
|
|
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
910
|
function text(value: unknown, label: string, maximum: number): string {
|
|
871
911
|
if (typeof value !== "string") {
|
|
872
912
|
throw new ConfigurationDecodeError(`${label} must be a string`);
|
|
@@ -1093,26 +1133,6 @@ function notifications(value: unknown): BotNotificationPolicy {
|
|
|
1093
1133
|
return { enabled: policy.enabled };
|
|
1094
1134
|
}
|
|
1095
1135
|
|
|
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
1136
|
export function decodeConfigurationQueryV1(
|
|
1117
1137
|
input: unknown,
|
|
1118
1138
|
): ConfigurationQueryV1 {
|
|
@@ -1161,30 +1181,19 @@ export function decodeConfigurationCommandV1(
|
|
|
1161
1181
|
},
|
|
1162
1182
|
};
|
|
1163
1183
|
}
|
|
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
|
-
}
|
|
1184
|
+
case "user/set-platform-model": {
|
|
1185
|
+
const command = exactCommand(input, ["model"]);
|
|
1176
1186
|
return {
|
|
1177
1187
|
...commandMeta(command),
|
|
1178
1188
|
type: value.type,
|
|
1179
|
-
model:
|
|
1180
|
-
source: command.source,
|
|
1189
|
+
model: decodeModelBindingV1(command.model),
|
|
1181
1190
|
};
|
|
1182
1191
|
}
|
|
1183
1192
|
case "user/install-package": {
|
|
1184
1193
|
const command = exactCommand(
|
|
1185
1194
|
input,
|
|
1186
1195
|
["packageId", "version"],
|
|
1187
|
-
["catalogId", "catalogGeneration", "values"],
|
|
1196
|
+
["catalogId", "catalogGeneration", "contentHash", "values", "enabled"],
|
|
1188
1197
|
);
|
|
1189
1198
|
// A Catalog install is all three of identity, generation and (optional)
|
|
1190
1199
|
// values or none of them: half a Catalog install would be an install
|
|
@@ -1202,11 +1211,26 @@ export function decodeConfigurationCommandV1(
|
|
|
1202
1211
|
"install values require a Catalog entry",
|
|
1203
1212
|
);
|
|
1204
1213
|
}
|
|
1214
|
+
if (
|
|
1215
|
+
command.catalogId === undefined &&
|
|
1216
|
+
command.contentHash !== undefined
|
|
1217
|
+
) {
|
|
1218
|
+
throw new ConfigurationDecodeError(
|
|
1219
|
+
"install contentHash requires a Catalog entry",
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
if (
|
|
1223
|
+
command.enabled !== undefined &&
|
|
1224
|
+
typeof command.enabled !== "boolean"
|
|
1225
|
+
) {
|
|
1226
|
+
throw new ConfigurationDecodeError("enabled is invalid");
|
|
1227
|
+
}
|
|
1205
1228
|
return {
|
|
1206
1229
|
...commandMeta(command),
|
|
1207
1230
|
type: value.type,
|
|
1208
1231
|
packageId: identifier(command.packageId, "packageId"),
|
|
1209
1232
|
version: text(command.version, "version", 100),
|
|
1233
|
+
...(command.enabled === undefined ? {} : { enabled: command.enabled }),
|
|
1210
1234
|
...(command.catalogId === undefined
|
|
1211
1235
|
? {}
|
|
1212
1236
|
: {
|
|
@@ -1215,6 +1239,14 @@ export function decodeConfigurationCommandV1(
|
|
|
1215
1239
|
command.catalogGeneration,
|
|
1216
1240
|
"catalogGeneration",
|
|
1217
1241
|
),
|
|
1242
|
+
...(command.contentHash === undefined
|
|
1243
|
+
? {}
|
|
1244
|
+
: {
|
|
1245
|
+
contentHash: compositionHash(
|
|
1246
|
+
command.contentHash,
|
|
1247
|
+
"contentHash",
|
|
1248
|
+
),
|
|
1249
|
+
}),
|
|
1218
1250
|
}),
|
|
1219
1251
|
...(command.values === undefined
|
|
1220
1252
|
? {}
|
|
@@ -1242,12 +1274,22 @@ export function decodeConfigurationCommandV1(
|
|
|
1242
1274
|
};
|
|
1243
1275
|
}
|
|
1244
1276
|
case "user/set-package-settings": {
|
|
1245
|
-
const command = exactCommand(input, ["packageId", "values"]);
|
|
1277
|
+
const command = exactCommand(input, ["packageId"], ["values", "unset"]);
|
|
1278
|
+
const values =
|
|
1279
|
+
command.values === undefined
|
|
1280
|
+
? undefined
|
|
1281
|
+
: packageSettingsPatch(command.values);
|
|
1282
|
+
const unset =
|
|
1283
|
+
command.unset === undefined
|
|
1284
|
+
? undefined
|
|
1285
|
+
: packageSettingIds(command.unset);
|
|
1286
|
+
requirePackageSettingsChange(values, unset);
|
|
1246
1287
|
return {
|
|
1247
1288
|
...commandMeta(command),
|
|
1248
1289
|
type: value.type,
|
|
1249
1290
|
packageId: identifier(command.packageId, "packageId"),
|
|
1250
|
-
values:
|
|
1291
|
+
...(values ? { values } : {}),
|
|
1292
|
+
...(unset ? { unset } : {}),
|
|
1251
1293
|
};
|
|
1252
1294
|
}
|
|
1253
1295
|
case "bot/update-profile": {
|
|
@@ -1296,34 +1338,28 @@ export function decodeConfigurationCommandV1(
|
|
|
1296
1338
|
notifications: notifications(command.notifications),
|
|
1297
1339
|
};
|
|
1298
1340
|
}
|
|
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"]);
|
|
1341
|
+
case "bot/set-package-settings": {
|
|
1342
|
+
const command = exactCommand(
|
|
1343
|
+
input,
|
|
1344
|
+
["botId", "packageId"],
|
|
1345
|
+
["values", "unset"],
|
|
1346
|
+
);
|
|
1347
|
+
const values =
|
|
1348
|
+
command.values === undefined
|
|
1349
|
+
? undefined
|
|
1350
|
+
: packageSettingsPatch(command.values);
|
|
1351
|
+
const unset =
|
|
1352
|
+
command.unset === undefined
|
|
1353
|
+
? undefined
|
|
1354
|
+
: packageSettingIds(command.unset);
|
|
1355
|
+
requirePackageSettingsChange(values, unset);
|
|
1322
1356
|
return {
|
|
1323
1357
|
...commandMeta(command),
|
|
1324
1358
|
type: value.type,
|
|
1325
1359
|
botId: identifier(command.botId, "botId"),
|
|
1326
|
-
|
|
1360
|
+
packageId: identifier(command.packageId, "packageId"),
|
|
1361
|
+
...(values ? { values } : {}),
|
|
1362
|
+
...(unset ? { unset } : {}),
|
|
1327
1363
|
};
|
|
1328
1364
|
}
|
|
1329
1365
|
default:
|
|
@@ -1423,11 +1459,41 @@ function safeJsonValue(value: unknown, label: string): JsonValue {
|
|
|
1423
1459
|
return value as JsonValue;
|
|
1424
1460
|
}
|
|
1425
1461
|
if (Array.isArray(value)) {
|
|
1462
|
+
if (
|
|
1463
|
+
Object.getPrototypeOf(value) !== Array.prototype ||
|
|
1464
|
+
Reflect.ownKeys(value).some(
|
|
1465
|
+
(key) =>
|
|
1466
|
+
typeof key !== "string" ||
|
|
1467
|
+
(key !== "length" &&
|
|
1468
|
+
(!/^(0|[1-9][0-9]*)$/.test(key) ||
|
|
1469
|
+
Number(key) >= value.length ||
|
|
1470
|
+
!Object.getOwnPropertyDescriptor(value, key)?.enumerable)),
|
|
1471
|
+
) ||
|
|
1472
|
+
Array.from({ length: value.length }, (_item, index) => index).some(
|
|
1473
|
+
(index) => !Object.hasOwn(value, index),
|
|
1474
|
+
)
|
|
1475
|
+
) {
|
|
1476
|
+
throw new ConfigurationDecodeError(`${label} is not plain JSON`);
|
|
1477
|
+
}
|
|
1426
1478
|
return value.map((item) => safeJsonValue(item, label));
|
|
1427
1479
|
}
|
|
1428
1480
|
if (typeof value === "object" && value !== null) {
|
|
1481
|
+
const object = record(value, label);
|
|
1482
|
+
if (
|
|
1483
|
+
Reflect.ownKeys(object).some((key) => {
|
|
1484
|
+
const descriptor = Object.getOwnPropertyDescriptor(object, key);
|
|
1485
|
+
return (
|
|
1486
|
+
typeof key !== "string" ||
|
|
1487
|
+
!descriptor ||
|
|
1488
|
+
!("value" in descriptor) ||
|
|
1489
|
+
!descriptor.enumerable
|
|
1490
|
+
);
|
|
1491
|
+
})
|
|
1492
|
+
) {
|
|
1493
|
+
throw new ConfigurationDecodeError(`${label} is not plain JSON`);
|
|
1494
|
+
}
|
|
1429
1495
|
return Object.fromEntries(
|
|
1430
|
-
Object.entries(
|
|
1496
|
+
Object.entries(object).map(([key, item]) => [
|
|
1431
1497
|
key,
|
|
1432
1498
|
safeJsonValue(item, label),
|
|
1433
1499
|
]),
|
|
@@ -1471,15 +1537,29 @@ function installValues(value: unknown): Record<string, JsonValue> {
|
|
|
1471
1537
|
* The shape of a `user/set-package-settings` payload, before anything knows
|
|
1472
1538
|
* which Package it is for.
|
|
1473
1539
|
*
|
|
1474
|
-
* Only the shape: ids are identifiers, values are scalars,
|
|
1475
|
-
*
|
|
1476
|
-
*
|
|
1477
|
-
*
|
|
1540
|
+
* Only the shape: ids are identifiers, ordinary values are scalars, the model
|
|
1541
|
+
* role may carry its exact binding object, and the bag is bounded. Whether a
|
|
1542
|
+
* named setting exists, and whether its value satisfies the schema the Package
|
|
1543
|
+
* declared, is the owning Durable Object's answer — it validates against the
|
|
1544
|
+
* installed version through `decodeInstalledPackageSettingsPatchV1`.
|
|
1478
1545
|
*/
|
|
1479
1546
|
function packageSettingsPatch(
|
|
1480
1547
|
value: unknown,
|
|
1481
1548
|
): Record<string, PackageSettingValueV1> {
|
|
1482
1549
|
const values = record(value, "values");
|
|
1550
|
+
if (
|
|
1551
|
+
Reflect.ownKeys(values).some((key) => {
|
|
1552
|
+
const descriptor = Object.getOwnPropertyDescriptor(values, key);
|
|
1553
|
+
return (
|
|
1554
|
+
typeof key !== "string" ||
|
|
1555
|
+
!descriptor ||
|
|
1556
|
+
!("value" in descriptor) ||
|
|
1557
|
+
!descriptor.enumerable
|
|
1558
|
+
);
|
|
1559
|
+
})
|
|
1560
|
+
) {
|
|
1561
|
+
throw new ConfigurationDecodeError("values has invalid fields");
|
|
1562
|
+
}
|
|
1483
1563
|
const entries = Object.entries(values);
|
|
1484
1564
|
if (entries.length === 0) {
|
|
1485
1565
|
throw new ConfigurationDecodeError("values names no setting");
|
|
@@ -1489,6 +1569,9 @@ function packageSettingsPatch(
|
|
|
1489
1569
|
}
|
|
1490
1570
|
return Object.fromEntries(
|
|
1491
1571
|
entries.map(([key, item]) => {
|
|
1572
|
+
if (typeof item === "object" && item !== null && !Array.isArray(item)) {
|
|
1573
|
+
return [identifier(key, "values key"), decodeModelBindingV1(item)];
|
|
1574
|
+
}
|
|
1492
1575
|
if (
|
|
1493
1576
|
typeof item !== "string" &&
|
|
1494
1577
|
typeof item !== "number" &&
|
|
@@ -1510,6 +1593,34 @@ function packageSettingsPatch(
|
|
|
1510
1593
|
);
|
|
1511
1594
|
}
|
|
1512
1595
|
|
|
1596
|
+
function packageSettingIds(value: unknown): string[] {
|
|
1597
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
1598
|
+
throw new ConfigurationDecodeError("unset names no setting");
|
|
1599
|
+
}
|
|
1600
|
+
if (value.length > MAX_PACKAGE_SETTINGS_V1) {
|
|
1601
|
+
throw new ConfigurationDecodeError("unset is too large");
|
|
1602
|
+
}
|
|
1603
|
+
const decoded = value.map((item) => identifier(item, "unset setting id"));
|
|
1604
|
+
if (new Set(decoded).size !== decoded.length) {
|
|
1605
|
+
throw new ConfigurationDecodeError("unset repeats a setting");
|
|
1606
|
+
}
|
|
1607
|
+
return decoded;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
function requirePackageSettingsChange(
|
|
1611
|
+
values: Record<string, PackageSettingValueV1> | undefined,
|
|
1612
|
+
unset: readonly string[] | undefined,
|
|
1613
|
+
): void {
|
|
1614
|
+
if (!values && !unset) {
|
|
1615
|
+
throw new ConfigurationDecodeError("Package settings command is empty");
|
|
1616
|
+
}
|
|
1617
|
+
if (values && unset?.some((settingId) => Object.hasOwn(values, settingId))) {
|
|
1618
|
+
throw new ConfigurationDecodeError(
|
|
1619
|
+
"Package settings command both sets and unsets a setting",
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1513
1624
|
function viewRevision(value: unknown): number {
|
|
1514
1625
|
if (!Number.isSafeInteger(value) || (value as number) < 0) {
|
|
1515
1626
|
throw new ConfigurationDecodeError("configuration revision is invalid");
|
|
@@ -1522,7 +1633,14 @@ function packageInstallation(value: unknown): PackageInstallationView {
|
|
|
1522
1633
|
value,
|
|
1523
1634
|
"Package installation",
|
|
1524
1635
|
["packageId", "version", "state"],
|
|
1525
|
-
[
|
|
1636
|
+
[
|
|
1637
|
+
"failure",
|
|
1638
|
+
"catalogId",
|
|
1639
|
+
"catalogGeneration",
|
|
1640
|
+
"contentHash",
|
|
1641
|
+
"provenance",
|
|
1642
|
+
"values",
|
|
1643
|
+
],
|
|
1526
1644
|
);
|
|
1527
1645
|
if (
|
|
1528
1646
|
installation.state !== "installed" &&
|
|
@@ -1556,6 +1674,11 @@ function packageInstallation(value: unknown): PackageInstallationView {
|
|
|
1556
1674
|
"catalogGeneration",
|
|
1557
1675
|
),
|
|
1558
1676
|
}),
|
|
1677
|
+
...(installation.contentHash === undefined
|
|
1678
|
+
? {}
|
|
1679
|
+
: {
|
|
1680
|
+
contentHash: compositionHash(installation.contentHash, "contentHash"),
|
|
1681
|
+
}),
|
|
1559
1682
|
...(installation.provenance === undefined
|
|
1560
1683
|
? {}
|
|
1561
1684
|
: { provenance: installation.provenance }),
|
|
@@ -1679,105 +1802,119 @@ export function decodePendingAuthorizationV1(
|
|
|
1679
1802
|
};
|
|
1680
1803
|
}
|
|
1681
1804
|
|
|
1682
|
-
|
|
1683
|
-
value
|
|
1684
|
-
|
|
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
|
-
);
|
|
1805
|
+
function schemaVersion(value: Record<string, unknown>): void {
|
|
1806
|
+
if (value.schemaVersion !== 1) {
|
|
1807
|
+
throw new ConfigurationDecodeError("unsupported configuration schema");
|
|
1699
1808
|
}
|
|
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
1809
|
}
|
|
1714
1810
|
|
|
1715
|
-
function
|
|
1811
|
+
function storedPlainRecordV1(
|
|
1716
1812
|
value: unknown,
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
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
|
-
};
|
|
1813
|
+
): Record<string, unknown> | undefined {
|
|
1814
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1815
|
+
return undefined;
|
|
1816
|
+
}
|
|
1817
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1818
|
+
return prototype === Object.prototype || prototype === null
|
|
1819
|
+
? (value as Record<string, unknown>)
|
|
1820
|
+
: undefined;
|
|
1734
1821
|
}
|
|
1735
1822
|
|
|
1736
|
-
function
|
|
1737
|
-
value: unknown
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
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
|
-
};
|
|
1823
|
+
function storedDataValueV1(
|
|
1824
|
+
value: Record<string, unknown>,
|
|
1825
|
+
key: string,
|
|
1826
|
+
): unknown {
|
|
1827
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1828
|
+
return descriptor && "value" in descriptor ? descriptor.value : undefined;
|
|
1775
1829
|
}
|
|
1776
1830
|
|
|
1777
|
-
function
|
|
1778
|
-
|
|
1779
|
-
|
|
1831
|
+
function cloneStoredRecordV1(
|
|
1832
|
+
value: Record<string, unknown>,
|
|
1833
|
+
changes: Readonly<Record<string, unknown>>,
|
|
1834
|
+
removed: readonly string[] = [],
|
|
1835
|
+
): Record<string, unknown> {
|
|
1836
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
1837
|
+
for (const key of removed) delete descriptors[key];
|
|
1838
|
+
for (const [key, next] of Object.entries(changes)) {
|
|
1839
|
+
const descriptor = descriptors[key];
|
|
1840
|
+
descriptors[key] =
|
|
1841
|
+
descriptor && "value" in descriptor
|
|
1842
|
+
? { ...descriptor, value: next }
|
|
1843
|
+
: {
|
|
1844
|
+
configurable: true,
|
|
1845
|
+
enumerable: true,
|
|
1846
|
+
value: next,
|
|
1847
|
+
writable: true,
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
return Object.create(Object.getPrototypeOf(value), descriptors) as Record<
|
|
1851
|
+
string,
|
|
1852
|
+
unknown
|
|
1853
|
+
>;
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
const PRE_ACCOUNT_WIDE_USER_FIELDS_V1 = [
|
|
1857
|
+
"newBotModelTemplate",
|
|
1858
|
+
"newBotModelTemplateSource",
|
|
1859
|
+
] as const;
|
|
1860
|
+
const PRE_ACCOUNT_WIDE_CONNECTION_METADATA_FIELDS_V1 = [
|
|
1861
|
+
"dependentAssignments",
|
|
1862
|
+
] as const;
|
|
1863
|
+
|
|
1864
|
+
/**
|
|
1865
|
+
* Migrates one raw User settings record across known durable shapes before the
|
|
1866
|
+
* current exact-field decoder sees it. Commit 1571b62 removed the model
|
|
1867
|
+
* template and commit d6730ad removed the Connection dependency ledger along
|
|
1868
|
+
* with the Assignment feature; migration drops those fields without
|
|
1869
|
+
* interpreting them.
|
|
1870
|
+
*/
|
|
1871
|
+
export function migrateStoredUserSettingsV1(stored: unknown): unknown {
|
|
1872
|
+
const settings = storedPlainRecordV1(stored);
|
|
1873
|
+
if (!settings || storedDataValueV1(settings, "schemaVersion") !== 1) {
|
|
1874
|
+
return stored;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
let changed = false;
|
|
1878
|
+
const removedSettingsFields = PRE_ACCOUNT_WIDE_USER_FIELDS_V1.filter((key) =>
|
|
1879
|
+
Object.hasOwn(settings, key),
|
|
1880
|
+
);
|
|
1881
|
+
changed ||= removedSettingsFields.length > 0;
|
|
1882
|
+
|
|
1883
|
+
const storedConnections = storedDataValueV1(settings, "connections");
|
|
1884
|
+
let connections = storedConnections;
|
|
1885
|
+
if (Array.isArray(storedConnections)) {
|
|
1886
|
+
let connectionsChanged = false;
|
|
1887
|
+
const nextConnections = storedConnections.map((storedConnection) => {
|
|
1888
|
+
const connection = storedPlainRecordV1(storedConnection);
|
|
1889
|
+
if (!connection) return storedConnection;
|
|
1890
|
+
const storedMetadata = storedDataValueV1(connection, "safeMetadata");
|
|
1891
|
+
const metadata = storedPlainRecordV1(storedMetadata);
|
|
1892
|
+
if (!metadata) return storedConnection;
|
|
1893
|
+
const removedMetadataFields =
|
|
1894
|
+
PRE_ACCOUNT_WIDE_CONNECTION_METADATA_FIELDS_V1.filter((key) =>
|
|
1895
|
+
Object.hasOwn(metadata, key),
|
|
1896
|
+
);
|
|
1897
|
+
if (removedMetadataFields.length === 0) return storedConnection;
|
|
1898
|
+
connectionsChanged = true;
|
|
1899
|
+
const safeMetadata = cloneStoredRecordV1(
|
|
1900
|
+
metadata,
|
|
1901
|
+
{},
|
|
1902
|
+
removedMetadataFields,
|
|
1903
|
+
);
|
|
1904
|
+
return cloneStoredRecordV1(connection, { safeMetadata });
|
|
1905
|
+
});
|
|
1906
|
+
if (connectionsChanged) {
|
|
1907
|
+
changed = true;
|
|
1908
|
+
connections = nextConnections;
|
|
1909
|
+
}
|
|
1780
1910
|
}
|
|
1911
|
+
|
|
1912
|
+
if (!changed) return stored;
|
|
1913
|
+
return cloneStoredRecordV1(
|
|
1914
|
+
settings,
|
|
1915
|
+
connections === storedConnections ? {} : { connections },
|
|
1916
|
+
removedSettingsFields,
|
|
1917
|
+
);
|
|
1781
1918
|
}
|
|
1782
1919
|
|
|
1783
1920
|
export function decodeUserSettingsViewV1(input: unknown): UserSettingsViewV1 {
|
|
@@ -1785,12 +1922,7 @@ export function decodeUserSettingsViewV1(input: unknown): UserSettingsViewV1 {
|
|
|
1785
1922
|
input,
|
|
1786
1923
|
"User settings",
|
|
1787
1924
|
["schemaVersion", "revision", "profile", "packages", "connections"],
|
|
1788
|
-
[
|
|
1789
|
-
"newBotModelTemplate",
|
|
1790
|
-
"newBotModelTemplateSource",
|
|
1791
|
-
"catalogGeneration",
|
|
1792
|
-
"catalogIndexHash",
|
|
1793
|
-
],
|
|
1925
|
+
["platformModel", "catalogGeneration", "catalogIndexHash"],
|
|
1794
1926
|
);
|
|
1795
1927
|
schemaVersion(value);
|
|
1796
1928
|
const profile = exactRecord(value.profile, "profile", ["name"], ["email"]);
|
|
@@ -1803,31 +1935,6 @@ export function decodeUserSettingsViewV1(input: unknown): UserSettingsViewV1 {
|
|
|
1803
1935
|
"User settings Packages and Connections must be bounded arrays",
|
|
1804
1936
|
);
|
|
1805
1937
|
}
|
|
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
1938
|
return {
|
|
1832
1939
|
schemaVersion: 1,
|
|
1833
1940
|
revision: viewRevision(value.revision),
|
|
@@ -1837,11 +1944,9 @@ export function decodeUserSettingsViewV1(input: unknown): UserSettingsViewV1 {
|
|
|
1837
1944
|
},
|
|
1838
1945
|
packages: value.packages.map(packageInstallation),
|
|
1839
1946
|
connections: value.connections.map(connectionView),
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
: model(value.newBotModelTemplate),
|
|
1844
|
-
newBotModelTemplateSource: value.newBotModelTemplateSource,
|
|
1947
|
+
...(value.platformModel === undefined
|
|
1948
|
+
? {}
|
|
1949
|
+
: { platformModel: decodeModelBindingV1(value.platformModel) }),
|
|
1845
1950
|
// The pin is optional — a deployment with no Catalog has none — but never
|
|
1846
1951
|
// half present: one field alone is a corrupt pin, not a pin.
|
|
1847
1952
|
...(value.catalogGeneration === undefined &&
|
|
@@ -1861,42 +1966,105 @@ export function decodeUserSettingsViewV1(input: unknown): UserSettingsViewV1 {
|
|
|
1861
1966
|
};
|
|
1862
1967
|
}
|
|
1863
1968
|
|
|
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);
|
|
1969
|
+
function packageValueBags(
|
|
1970
|
+
input: unknown,
|
|
1971
|
+
): Record<string, Record<string, unknown>> {
|
|
1972
|
+
const packages = record(input, "packageValues");
|
|
1880
1973
|
if (
|
|
1881
|
-
|
|
1882
|
-
|
|
1974
|
+
Reflect.ownKeys(packages).some((key) => {
|
|
1975
|
+
const descriptor = Object.getOwnPropertyDescriptor(packages, key);
|
|
1976
|
+
return (
|
|
1977
|
+
typeof key !== "string" ||
|
|
1978
|
+
!descriptor ||
|
|
1979
|
+
!("value" in descriptor) ||
|
|
1980
|
+
!descriptor.enumerable
|
|
1981
|
+
);
|
|
1982
|
+
})
|
|
1883
1983
|
) {
|
|
1884
|
-
throw new ConfigurationDecodeError(
|
|
1885
|
-
"Bot settings Assignments and operations must be arrays",
|
|
1886
|
-
);
|
|
1984
|
+
throw new ConfigurationDecodeError("packageValues has invalid fields");
|
|
1887
1985
|
}
|
|
1986
|
+
return Object.fromEntries(
|
|
1987
|
+
Object.entries(packages).map(([packageId, stored]) => {
|
|
1988
|
+
identifier(packageId, "packageValues packageId");
|
|
1989
|
+
const values = record(stored, `packageValues.${packageId}`);
|
|
1990
|
+
if (
|
|
1991
|
+
Reflect.ownKeys(values).length > MAX_PACKAGE_SETTINGS_V1 ||
|
|
1992
|
+
Reflect.ownKeys(values).some((key) => {
|
|
1993
|
+
const descriptor = Object.getOwnPropertyDescriptor(values, key);
|
|
1994
|
+
return (
|
|
1995
|
+
typeof key !== "string" ||
|
|
1996
|
+
!descriptor ||
|
|
1997
|
+
!("value" in descriptor) ||
|
|
1998
|
+
!descriptor.enumerable
|
|
1999
|
+
);
|
|
2000
|
+
})
|
|
2001
|
+
) {
|
|
2002
|
+
throw new ConfigurationDecodeError(
|
|
2003
|
+
`packageValues.${packageId} has invalid fields`,
|
|
2004
|
+
);
|
|
2005
|
+
}
|
|
2006
|
+
return [
|
|
2007
|
+
packageId,
|
|
2008
|
+
Object.fromEntries(
|
|
2009
|
+
Object.entries(values).map(([settingId, value]) => [
|
|
2010
|
+
identifier(settingId, "packageValues settingId"),
|
|
2011
|
+
safeJsonValue(value, `packageValues.${packageId}.${settingId}`),
|
|
2012
|
+
]),
|
|
2013
|
+
),
|
|
2014
|
+
];
|
|
2015
|
+
}),
|
|
2016
|
+
);
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
export function decodeBotSettingsViewV1(input: unknown): BotSettingsViewV1 {
|
|
2020
|
+
const value = exactRecord(input, "Bot settings", [
|
|
2021
|
+
"schemaVersion",
|
|
2022
|
+
"botId",
|
|
2023
|
+
"revision",
|
|
2024
|
+
"profile",
|
|
2025
|
+
"notifications",
|
|
2026
|
+
"packageValues",
|
|
2027
|
+
]);
|
|
2028
|
+
schemaVersion(value);
|
|
1888
2029
|
return {
|
|
1889
2030
|
schemaVersion: 1,
|
|
1890
2031
|
botId: identifier(value.botId, "botId"),
|
|
1891
2032
|
revision: viewRevision(value.revision),
|
|
1892
2033
|
profile: botProfile(value.profile),
|
|
1893
2034
|
notifications: notifications(value.notifications),
|
|
1894
|
-
|
|
1895
|
-
assignmentOperations: value.assignmentOperations.map(assignmentOperation),
|
|
1896
|
-
model: value.model === undefined ? undefined : model(value.model),
|
|
2035
|
+
packageValues: packageValueBags(value.packageValues),
|
|
1897
2036
|
};
|
|
1898
2037
|
}
|
|
1899
2038
|
|
|
2039
|
+
const PRE_ACCOUNT_WIDE_BOT_FIELDS_V1 = [
|
|
2040
|
+
"assignments",
|
|
2041
|
+
"assignmentOperations",
|
|
2042
|
+
"model",
|
|
2043
|
+
// Some pre-release records used this name for the same removed feature.
|
|
2044
|
+
"modelAssignment",
|
|
2045
|
+
] as const;
|
|
2046
|
+
|
|
2047
|
+
/**
|
|
2048
|
+
* Migrates one raw Bot settings record across known durable shapes before the
|
|
2049
|
+
* current exact-field decoder sees it. The removed Assignment/model fields are
|
|
2050
|
+
* discarded; their presence identifies the shape that predates packageValues.
|
|
2051
|
+
*/
|
|
2052
|
+
export function migrateStoredBotSettingsV1(stored: unknown): unknown {
|
|
2053
|
+
const settings = storedPlainRecordV1(stored);
|
|
2054
|
+
if (!settings || storedDataValueV1(settings, "schemaVersion") !== 1) {
|
|
2055
|
+
return stored;
|
|
2056
|
+
}
|
|
2057
|
+
const removed = PRE_ACCOUNT_WIDE_BOT_FIELDS_V1.filter((key) =>
|
|
2058
|
+
Object.hasOwn(settings, key),
|
|
2059
|
+
);
|
|
2060
|
+
if (removed.length === 0) return stored;
|
|
2061
|
+
return cloneStoredRecordV1(
|
|
2062
|
+
settings,
|
|
2063
|
+
Object.hasOwn(settings, "packageValues") ? {} : { packageValues: {} },
|
|
2064
|
+
removed,
|
|
2065
|
+
);
|
|
2066
|
+
}
|
|
2067
|
+
|
|
1900
2068
|
export function decodeConfigurationViewV1(input: unknown): ConfigurationViewV1 {
|
|
1901
2069
|
const value = record(input, "configuration");
|
|
1902
2070
|
return "botId" in value
|
|
@@ -1958,6 +2126,11 @@ const COMPOSITION_GENERATION_STATUSES_V1: readonly CompositionGenerationStatusVi
|
|
|
1958
2126
|
|
|
1959
2127
|
export type CompositionProvenanceViewV1 =
|
|
1960
2128
|
| { kind: "first-party" }
|
|
2129
|
+
| {
|
|
2130
|
+
kind: "catalog";
|
|
2131
|
+
catalogId: string;
|
|
2132
|
+
catalogGeneration: string;
|
|
2133
|
+
}
|
|
1961
2134
|
| { kind: "user"; userId: string; authoredAt: string }
|
|
1962
2135
|
| {
|
|
1963
2136
|
kind: "bot";
|
|
@@ -1972,7 +2145,14 @@ export type CompositionOriginViewV1 =
|
|
|
1972
2145
|
| { kind: "bootstrap" }
|
|
1973
2146
|
| { kind: "bot-authored"; runId: string; sessionId: string; turnId: string }
|
|
1974
2147
|
| { kind: "user-install"; userId: string }
|
|
1975
|
-
| { kind: "revert"; revertsTo: string; userId: string }
|
|
2148
|
+
| { kind: "revert"; revertsTo: string; userId: string }
|
|
2149
|
+
| {
|
|
2150
|
+
kind: "revert";
|
|
2151
|
+
revertsTo: string;
|
|
2152
|
+
botId: string;
|
|
2153
|
+
runId: string;
|
|
2154
|
+
turnId: string;
|
|
2155
|
+
};
|
|
1976
2156
|
|
|
1977
2157
|
export interface CompositionMemberViewV1 {
|
|
1978
2158
|
packageId: string;
|
|
@@ -2110,6 +2290,25 @@ function compositionProvenanceView(
|
|
|
2110
2290
|
exactRecord(input, "Composition provenance", ["kind"]);
|
|
2111
2291
|
return { kind: "first-party" };
|
|
2112
2292
|
}
|
|
2293
|
+
if (kind === "catalog") {
|
|
2294
|
+
const value = exactRecord(input, "Composition provenance", [
|
|
2295
|
+
"kind",
|
|
2296
|
+
"catalogId",
|
|
2297
|
+
"catalogGeneration",
|
|
2298
|
+
]);
|
|
2299
|
+
return {
|
|
2300
|
+
kind: "catalog",
|
|
2301
|
+
catalogId: identifier(
|
|
2302
|
+
value.catalogId,
|
|
2303
|
+
"Composition provenance catalogId",
|
|
2304
|
+
),
|
|
2305
|
+
catalogGeneration: text(
|
|
2306
|
+
value.catalogGeneration,
|
|
2307
|
+
"Composition provenance catalogGeneration",
|
|
2308
|
+
256,
|
|
2309
|
+
),
|
|
2310
|
+
};
|
|
2311
|
+
}
|
|
2113
2312
|
if (kind === "user") {
|
|
2114
2313
|
const value = exactRecord(input, "Composition provenance", [
|
|
2115
2314
|
"kind",
|
|
@@ -2177,18 +2376,32 @@ function compositionOriginView(input: unknown): CompositionOriginViewV1 {
|
|
|
2177
2376
|
};
|
|
2178
2377
|
}
|
|
2179
2378
|
if (kind === "revert") {
|
|
2180
|
-
const value =
|
|
2379
|
+
const value = record(input, "Composition origin");
|
|
2380
|
+
const revertsTo = decodeCompositionGenerationIdV1(
|
|
2381
|
+
value.revertsTo,
|
|
2382
|
+
"Composition origin revertsTo",
|
|
2383
|
+
);
|
|
2384
|
+
if (Object.hasOwn(value, "userId")) {
|
|
2385
|
+
exactRecord(input, "Composition origin", ["kind", "revertsTo", "userId"]);
|
|
2386
|
+
return {
|
|
2387
|
+
kind: "revert",
|
|
2388
|
+
revertsTo,
|
|
2389
|
+
userId: text(value.userId, "Composition origin userId", 256),
|
|
2390
|
+
};
|
|
2391
|
+
}
|
|
2392
|
+
const bot = exactRecord(input, "Composition origin", [
|
|
2181
2393
|
"kind",
|
|
2182
2394
|
"revertsTo",
|
|
2183
|
-
"
|
|
2395
|
+
"botId",
|
|
2396
|
+
"runId",
|
|
2397
|
+
"turnId",
|
|
2184
2398
|
]);
|
|
2185
2399
|
return {
|
|
2186
2400
|
kind: "revert",
|
|
2187
|
-
revertsTo
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
),
|
|
2191
|
-
userId: text(value.userId, "Composition origin userId", 256),
|
|
2401
|
+
revertsTo,
|
|
2402
|
+
botId: decodeBotIdV1(bot.botId),
|
|
2403
|
+
runId: text(bot.runId, "Composition origin runId", 128),
|
|
2404
|
+
turnId: text(bot.turnId, "Composition origin turnId", 128),
|
|
2192
2405
|
};
|
|
2193
2406
|
}
|
|
2194
2407
|
throw new ConfigurationDecodeError("Composition origin kind is invalid");
|