@frockbot/plugin-flock 0.0.0 → 0.1.1

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.
Files changed (92) hide show
  1. package/README.md +8 -1
  2. package/assets/amber-glasses.webp +0 -0
  3. package/assets/background-bright-orange.webp +0 -0
  4. package/assets/background-canary-yellow.webp +0 -0
  5. package/assets/background-electric-blue.webp +0 -0
  6. package/assets/background-hot-pink.webp +0 -0
  7. package/assets/background-lime-green.webp +0 -0
  8. package/assets/background-vivid-purple.webp +0 -0
  9. package/assets/blue-sailor-cap.webp +0 -0
  10. package/assets/bronze-aviator-goggles.webp +0 -0
  11. package/assets/burgundy-beret.webp +0 -0
  12. package/assets/canary-party-hat.webp +0 -0
  13. package/assets/canary-rain-cap.webp +0 -0
  14. package/assets/canonical.webp +0 -0
  15. package/assets/chef-toque.webp +0 -0
  16. package/assets/cobalt-round-glasses.webp +0 -0
  17. package/assets/coral-bucket-hat.webp +0 -0
  18. package/assets/emerald-necktie.webp +0 -0
  19. package/assets/forest-ranger-fedora.webp +0 -0
  20. package/assets/gold-royal-crown.webp +0 -0
  21. package/assets/gold-sun-medallion.webp +0 -0
  22. package/assets/graduation-cap.webp +0 -0
  23. package/assets/green-square-glasses.webp +0 -0
  24. package/assets/layers.css +159 -0
  25. package/assets/leather-bell.webp +0 -0
  26. package/assets/lilac-flower-crown.webp +0 -0
  27. package/assets/manifest.json +769 -0
  28. package/assets/matte-black-top-hat.webp +0 -0
  29. package/assets/navy-wizard-hat.webp +0 -0
  30. package/assets/orange-hard-hat.webp +0 -0
  31. package/assets/plum-glasses.webp +0 -0
  32. package/assets/purple-scarf.webp +0 -0
  33. package/assets/purple-witch-hat.webp +0 -0
  34. package/assets/rainbow-propeller-cap.webp +0 -0
  35. package/assets/rainbow-visor.webp +0 -0
  36. package/assets/ranger-fedora-feather.webp +0 -0
  37. package/assets/red-polka-bowtie.webp +0 -0
  38. package/assets/retro-3d-glasses.webp +0 -0
  39. package/assets/rose-heart-sunglasses.webp +0 -0
  40. package/assets/royal-crown-rubies.webp +0 -0
  41. package/assets/sailor-cap-anchor.webp +0 -0
  42. package/assets/silver-space-helmet.webp +0 -0
  43. package/assets/straw-boater.webp +0 -0
  44. package/assets/teal-bowtie.webp +0 -0
  45. package/assets/teal-glasses.webp +0 -0
  46. package/assets/terracotta-beanie.webp +0 -0
  47. package/assets/tortoiseshell-readers.webp +0 -0
  48. package/assets/turquoise-bandana.webp +0 -0
  49. package/assets/violet-masquerade-mask.webp +0 -0
  50. package/assets/white-snow-goggles.webp +0 -0
  51. package/assets/white-tennis-visor.webp +0 -0
  52. package/assets/witch-hat-moon.webp +0 -0
  53. package/assets/yellow-star-glasses.webp +0 -0
  54. package/frockbot.json +38 -0
  55. package/package.json +47 -6
  56. package/src/agent.test.ts +401 -0
  57. package/src/agent.ts +571 -0
  58. package/src/assets.test.ts +31 -0
  59. package/src/backend.test.ts +205 -0
  60. package/src/backend.ts +283 -0
  61. package/src/bot.test.ts +190 -0
  62. package/src/bot.ts +255 -0
  63. package/src/client/BotAvatar.vue +20 -0
  64. package/src/client/FlockAvatar.vue +33 -0
  65. package/src/client/FlockAvatarEditor.vue +44 -0
  66. package/src/client/FlockCreateButton.vue +18 -0
  67. package/src/client/FlockIdentity.vue +31 -0
  68. package/src/client/FlockOverlay.vue +209 -0
  69. package/src/client/FlockSidebar.vue +254 -0
  70. package/src/client/SheepAvatar.vue +28 -0
  71. package/src/client/dialog-focus.test.ts +15 -0
  72. package/src/client/dialog-focus.ts +13 -0
  73. package/src/client/index.test.ts +502 -0
  74. package/src/client/index.ts +619 -0
  75. package/src/client/mobile-dialog-layout.test.ts +50 -0
  76. package/src/client/pending-create.test.ts +71 -0
  77. package/src/client/pending-create.ts +93 -0
  78. package/src/client/sidebar.test.ts +77 -0
  79. package/src/client/sidebar.ts +77 -0
  80. package/src/client/state.ts +53 -0
  81. package/src/client/styles.css +611 -0
  82. package/src/env.d.ts +9 -0
  83. package/src/index.ts +4 -0
  84. package/src/manifest.ts +3 -0
  85. package/src/package.test.ts +12 -0
  86. package/src/shared.test.ts +197 -0
  87. package/src/shared.ts +735 -0
  88. package/src/user.test.ts +455 -0
  89. package/src/user.ts +465 -0
  90. package/tsconfig.json +15 -0
  91. package/validate_readability.py +64 -0
  92. package/vite.config.ts +30 -0
package/src/shared.ts ADDED
@@ -0,0 +1,735 @@
1
+ // Shared configuration types remain provider-neutral at this package seam.
2
+ import {
3
+ decodeCapabilityAssignmentV1,
4
+ decodeBotIdV1,
5
+ decodeBotSelfWriterV1,
6
+ isPublicIdentifier,
7
+ type BotNameProvenanceV1,
8
+ type BotSelfWriterV1,
9
+ type CapabilityAssignmentView,
10
+ type ModelAssignment,
11
+ } from "@frockbot/configuration-core";
12
+ export type { BotSelfWriterV1 } from "@frockbot/configuration-core";
13
+ import assetManifest from "../assets/manifest.json" with { type: "json" };
14
+
15
+ export const FLOCK_DIRECTORY_LIMIT = 100;
16
+
17
+ export function isFlockIdentifier(value: unknown): value is string {
18
+ return isPublicIdentifier(value);
19
+ }
20
+
21
+ type Band = "upper" | "middle" | "lower";
22
+ export interface SheepRecipeV1 {
23
+ schemaVersion: 1;
24
+ background: string;
25
+ upper: string;
26
+ middle: string;
27
+ lower: string;
28
+ }
29
+ export interface InitialModelBindingV1 {
30
+ assignment: CapabilityAssignmentView;
31
+ generation: string;
32
+ }
33
+ export interface BotRegistrationV1 {
34
+ schemaVersion: 1;
35
+ botId: string;
36
+ registeredAt: string;
37
+ initialName: string;
38
+ /**
39
+ * The persona the Bot is materialized with. GrokBot's `CreateAgent` takes a
40
+ * `description` that becomes the new agent's profile, and the seed is the
41
+ * only place a creator can put it: the Bot's own profile lives in the Bot
42
+ * Durable Object, which no other Bot may write.
43
+ */
44
+ initialDescription?: string;
45
+ initialModel?: ModelAssignment;
46
+ initialModelBinding?: InitialModelBindingV1;
47
+ /**
48
+ * Connection-less Capability authority snapshotted from the User's installed
49
+ * Packages when this Bot was admitted.
50
+ */
51
+ initialAssignments?: CapabilityAssignmentView[];
52
+ /**
53
+ * The Bot and Turn that created this Bot, when a Bot created it. Absent for
54
+ * a User-created Bot and for every registration written before this existed.
55
+ */
56
+ createdBy?: BotSelfWriterV1;
57
+ sheep: SheepRecipeV1;
58
+ }
59
+ export interface BotMembershipViewV1 {
60
+ schemaVersion: 1;
61
+ botId: string;
62
+ registered: boolean;
63
+ }
64
+ export interface BotDirectoryViewV1 {
65
+ schemaVersion: 1;
66
+ revision: number;
67
+ bots: BotRegistrationV1[];
68
+ }
69
+ export type BotLifecycleStatusV1 = "active" | "archived";
70
+ export interface BotLifecycleViewV1 {
71
+ schemaVersion: 1;
72
+ botId: string;
73
+ status: BotLifecycleStatusV1;
74
+ revision: number;
75
+ }
76
+ export interface BotLifecycleDirectoryViewV1 {
77
+ schemaVersion: 1;
78
+ lifecycles: BotLifecycleViewV1[];
79
+ }
80
+ export interface BotLifecycleCommandV1 {
81
+ schemaVersion: 1;
82
+ type: "bot/archive" | "bot/restore";
83
+ commandId: string;
84
+ botId: string;
85
+ }
86
+ export interface BotLifecycleReceiptV1 {
87
+ schemaVersion: 1;
88
+ commandId: string;
89
+ botId: string;
90
+ status: "pending" | "applied" | "rejected";
91
+ lifecycle: BotLifecycleViewV1;
92
+ failure?: string;
93
+ }
94
+ export interface StoredBotLifecycleReceiptV1 {
95
+ fingerprint: string;
96
+ receipt: BotLifecycleReceiptV1;
97
+ }
98
+ export interface CreateBotCommandV1 {
99
+ schemaVersion: 1;
100
+ type: "bot/create";
101
+ commandId: string;
102
+ expectedRevision: number;
103
+ botId: string;
104
+ name: string;
105
+ /** The new Bot's persona, materialized into its profile. */
106
+ description?: string;
107
+ /** The Bot and Turn issuing this command, when a Bot issues it. */
108
+ createdBy?: BotSelfWriterV1;
109
+ sheep?: SheepRecipeV1;
110
+ }
111
+ export interface UpdateSheepCommandV1 {
112
+ schemaVersion: 1;
113
+ type: "bot/update-sheep";
114
+ commandId: string;
115
+ expectedRevision: number;
116
+ botId: string;
117
+ sheep: SheepRecipeV1;
118
+ }
119
+ export interface SheepIdentityViewV1 {
120
+ schemaVersion: 1;
121
+ botId: string;
122
+ revision: number;
123
+ sheep: SheepRecipeV1;
124
+ }
125
+ export interface FlockReceiptV1 {
126
+ schemaVersion: 1;
127
+ commandId: string;
128
+ status: "applied" | "rejected";
129
+ revision: number;
130
+ failure?: string;
131
+ }
132
+ export interface StoredFlockReceiptV1 {
133
+ fingerprint: string;
134
+ receipt: FlockReceiptV1;
135
+ }
136
+
137
+ /**
138
+ * One Bot's live identity, as the Flock directory surfaces it.
139
+ *
140
+ * The registration seed in the User Durable Object is immutable (ADR 0006), so
141
+ * the mutable half of a Bot's identity — the current name, its provenance, a
142
+ * title and whether the sidebar hides it — is read through
143
+ * from the Bot Durable Object that owns it rather than copied into the seed.
144
+ */
145
+ export interface BotIdentityViewV1 {
146
+ schemaVersion: 1;
147
+ botId: string;
148
+ name: string;
149
+ namedBy: BotNameProvenanceV1;
150
+ hiddenFromSidebar: boolean;
151
+ /** Purely organisational sidebar group; never part of Bot instructions. */
152
+ label?: string;
153
+ title?: string;
154
+ }
155
+
156
+ export interface BotIdentityDirectoryViewV1 {
157
+ schemaVersion: 1;
158
+ identities: BotIdentityViewV1[];
159
+ }
160
+
161
+ export class FlockDecodeError extends Error {
162
+ constructor(message: string) {
163
+ super(message);
164
+ this.name = "FlockDecodeError";
165
+ }
166
+ }
167
+ export class FlockConflictError extends Error {
168
+ constructor(readonly currentRevision: number) {
169
+ super(`flock revision is ${currentRevision}`);
170
+ this.name = "FlockConflictError";
171
+ }
172
+ }
173
+ export class BotNotFoundError extends Error {
174
+ constructor(readonly botId: string) {
175
+ super(`Bot "${botId}" is not registered`);
176
+ this.name = "BotNotFoundError";
177
+ }
178
+ }
179
+
180
+ const backgrounds = assetManifest.backgrounds.map((item) => item.id);
181
+ const tree = assetManifest.trees as Record<
182
+ Band,
183
+ Array<{ id: string; label: string; parent: string | null; kind: string }>
184
+ >;
185
+ const ids = Object.fromEntries(
186
+ (Object.keys(tree) as Band[]).map((band) => [
187
+ band,
188
+ tree[band].map((item) => item.id),
189
+ ]),
190
+ ) as Record<Band, string[]>;
191
+ const nodeIndex = new Map<
192
+ string,
193
+ { id: string; parent: string | null; band: Band }
194
+ >();
195
+ for (const band of Object.keys(tree) as Band[])
196
+ for (const node of tree[band])
197
+ nodeIndex.set(node.id, { id: node.id, parent: node.parent, band });
198
+
199
+ export const sheepCatalog = {
200
+ backgrounds: assetManifest.backgrounds,
201
+ trees: assetManifest.trees,
202
+ assets: assetManifest.assets,
203
+ } as const;
204
+
205
+ function record(input: unknown, label: string): Record<string, unknown> {
206
+ if (typeof input !== "object" || input === null || Array.isArray(input))
207
+ throw new FlockDecodeError(`${label} must be an object`);
208
+ return input as Record<string, unknown>;
209
+ }
210
+ function exact(
211
+ value: Record<string, unknown>,
212
+ required: string[],
213
+ optional: string[] = [],
214
+ ): void {
215
+ const allowed = new Set([...required, ...optional]);
216
+ // Values read over Durable Object RPC carry a disposal symbol as an own
217
+ // key; it is transport, not a field.
218
+ const keys = Reflect.ownKeys(value).filter(
219
+ (key) => key !== Symbol.dispose && key !== Symbol.asyncDispose,
220
+ );
221
+ if (
222
+ !required.every((key) => Object.hasOwn(value, key)) ||
223
+ keys.some((key) => typeof key !== "string" || !allowed.has(key))
224
+ )
225
+ throw new FlockDecodeError("unknown or missing field");
226
+ }
227
+ function identifier(value: unknown, label: string): string {
228
+ if (!isFlockIdentifier(value))
229
+ throw new FlockDecodeError(`${label} is invalid`);
230
+ return value;
231
+ }
232
+
233
+ function botIdentifier(value: unknown): string {
234
+ try {
235
+ return decodeBotIdV1(value);
236
+ } catch {
237
+ throw new FlockDecodeError("botId is invalid");
238
+ }
239
+ }
240
+ /** An optional Bot writer, decoded once at this seam and never re-shaped. */
241
+ function botWriter(value: unknown, label: string): BotSelfWriterV1 | undefined {
242
+ if (value === undefined) return undefined;
243
+ try {
244
+ return decodeBotSelfWriterV1(value, label);
245
+ } catch (error) {
246
+ throw new FlockDecodeError(
247
+ error instanceof Error ? error.message : `${label} is invalid`,
248
+ );
249
+ }
250
+ }
251
+
252
+ function boundedText(value: unknown, label: string, maximum: number): string {
253
+ if (
254
+ typeof value !== "string" ||
255
+ value.trim() !== value ||
256
+ value.length < 1 ||
257
+ value.length > maximum
258
+ )
259
+ throw new FlockDecodeError(`${label} is invalid`);
260
+ return value;
261
+ }
262
+ function revision(value: unknown): number {
263
+ if (!Number.isSafeInteger(value) || (value as number) < 0)
264
+ throw new FlockDecodeError("revision is invalid");
265
+ return value as number;
266
+ }
267
+ function modelAssignment(value: unknown): ModelAssignment {
268
+ const model = record(value, "initialModel");
269
+ exact(model, ["connectionId", "providerModelId"]);
270
+ return {
271
+ connectionId: identifier(model.connectionId, "initialModel.connectionId"),
272
+ providerModelId: boundedText(
273
+ model.providerModelId,
274
+ "initialModel.providerModelId",
275
+ 256,
276
+ ),
277
+ };
278
+ }
279
+
280
+ function initialModelBinding(value: unknown): InitialModelBindingV1 {
281
+ const binding = record(value, "initialModelBinding");
282
+ exact(binding, ["assignment", "generation"]);
283
+ const assignment = record(
284
+ binding.assignment,
285
+ "initialModelBinding.assignment",
286
+ );
287
+ exact(assignment, [
288
+ "assignmentId",
289
+ "packageId",
290
+ "capabilityId",
291
+ "connectionId",
292
+ "state",
293
+ ]);
294
+ if (assignment.state !== "enabled") {
295
+ throw new FlockDecodeError("initial model assignment is invalid");
296
+ }
297
+ return {
298
+ assignment: {
299
+ assignmentId: identifier(
300
+ assignment.assignmentId,
301
+ "initialModelBinding.assignment.assignmentId",
302
+ ),
303
+ packageId: identifier(
304
+ assignment.packageId,
305
+ "initialModelBinding.assignment.packageId",
306
+ ),
307
+ capabilityId: identifier(
308
+ assignment.capabilityId,
309
+ "initialModelBinding.assignment.capabilityId",
310
+ ),
311
+ connectionId: identifier(
312
+ assignment.connectionId,
313
+ "initialModelBinding.assignment.connectionId",
314
+ ),
315
+ state: "enabled",
316
+ },
317
+ generation: identifier(
318
+ binding.generation,
319
+ "initialModelBinding.generation",
320
+ ),
321
+ };
322
+ }
323
+
324
+ export function decodeSheepRecipeV1(input: unknown): SheepRecipeV1 {
325
+ const value = record(input, "sheep recipe");
326
+ exact(value, ["schemaVersion", "background", "upper", "middle", "lower"]);
327
+ if (value.schemaVersion !== 1)
328
+ throw new FlockDecodeError("unsupported sheep recipe");
329
+ const recipe = {
330
+ schemaVersion: 1,
331
+ background: identifier(value.background, "background"),
332
+ upper: identifier(value.upper, "upper"),
333
+ middle: identifier(value.middle, "middle"),
334
+ lower: identifier(value.lower, "lower"),
335
+ } satisfies SheepRecipeV1;
336
+ if (
337
+ !backgrounds.includes(recipe.background) ||
338
+ !ids.upper.includes(recipe.upper) ||
339
+ !ids.middle.includes(recipe.middle) ||
340
+ !ids.lower.includes(recipe.lower)
341
+ )
342
+ throw new FlockDecodeError(
343
+ "sheep recipe references an unknown catalog item",
344
+ );
345
+ return recipe;
346
+ }
347
+
348
+ export function decodeCreateBotCommandV1(input: unknown): CreateBotCommandV1 {
349
+ const value = record(input, "create Bot command");
350
+ exact(
351
+ value,
352
+ ["schemaVersion", "type", "commandId", "expectedRevision", "botId", "name"],
353
+ ["description", "createdBy", "sheep"],
354
+ );
355
+ if (value.schemaVersion !== 1 || value.type !== "bot/create")
356
+ throw new FlockDecodeError("unsupported create Bot command");
357
+ const botId = botIdentifier(value.botId);
358
+ const createdBy = botWriter(value.createdBy, "createdBy");
359
+ // The creator names itself; a Bot may not claim the new Bot wrote its own
360
+ // registration, and it may not attribute the create to some third Bot.
361
+ if (createdBy && createdBy.botId === botId)
362
+ throw new FlockDecodeError("createdBy is invalid");
363
+ return {
364
+ schemaVersion: 1,
365
+ type: "bot/create",
366
+ commandId: identifier(value.commandId, "commandId"),
367
+ expectedRevision: revision(value.expectedRevision),
368
+ botId,
369
+ name: boundedText(value.name, "name", 100),
370
+ ...(value.description === undefined
371
+ ? {}
372
+ : { description: boundedText(value.description, "description", 10_000) }),
373
+ ...(createdBy ? { createdBy } : {}),
374
+ sheep:
375
+ value.sheep === undefined ? undefined : decodeSheepRecipeV1(value.sheep),
376
+ };
377
+ }
378
+
379
+ export function decodeUpdateSheepCommandV1(
380
+ input: unknown,
381
+ ): UpdateSheepCommandV1 {
382
+ const value = record(input, "update sheep command");
383
+ exact(value, [
384
+ "schemaVersion",
385
+ "type",
386
+ "commandId",
387
+ "expectedRevision",
388
+ "botId",
389
+ "sheep",
390
+ ]);
391
+ if (value.schemaVersion !== 1 || value.type !== "bot/update-sheep")
392
+ throw new FlockDecodeError("unsupported update sheep command");
393
+ return {
394
+ schemaVersion: 1,
395
+ type: "bot/update-sheep",
396
+ commandId: identifier(value.commandId, "commandId"),
397
+ expectedRevision: revision(value.expectedRevision),
398
+ botId: botIdentifier(value.botId),
399
+ sheep: decodeSheepRecipeV1(value.sheep),
400
+ };
401
+ }
402
+
403
+ export function decodeBotRegistrationV1(input: unknown): BotRegistrationV1 {
404
+ const bot = record(input, "Bot registration");
405
+ exact(
406
+ bot,
407
+ ["schemaVersion", "botId", "registeredAt", "initialName", "sheep"],
408
+ [
409
+ "initialDescription",
410
+ "initialModel",
411
+ "initialModelBinding",
412
+ "initialAssignments",
413
+ "createdBy",
414
+ ],
415
+ );
416
+ if (bot.schemaVersion !== 1)
417
+ throw new FlockDecodeError("unsupported Bot registration");
418
+ const initialModel =
419
+ bot.initialModel === undefined
420
+ ? undefined
421
+ : modelAssignment(bot.initialModel);
422
+ const binding =
423
+ bot.initialModelBinding === undefined
424
+ ? undefined
425
+ : initialModelBinding(bot.initialModelBinding);
426
+ if (
427
+ bot.initialAssignments !== undefined &&
428
+ !Array.isArray(bot.initialAssignments)
429
+ )
430
+ throw new FlockDecodeError("initial Assignments are invalid");
431
+ const initialAssignments =
432
+ bot.initialAssignments === undefined
433
+ ? undefined
434
+ : bot.initialAssignments.map(decodeCapabilityAssignmentV1);
435
+ if (
436
+ new Set(
437
+ (initialAssignments ?? []).map((assignment) => assignment.assignmentId),
438
+ ).size !== (initialAssignments ?? []).length ||
439
+ (initialAssignments ?? []).some(
440
+ (assignment) =>
441
+ assignment.connectionId !== undefined || assignment.state !== "enabled",
442
+ )
443
+ ) {
444
+ throw new FlockDecodeError("initial Assignments are invalid");
445
+ }
446
+ if (
447
+ binding &&
448
+ (!initialModel ||
449
+ binding.assignment.connectionId !== initialModel.connectionId)
450
+ ) {
451
+ throw new FlockDecodeError("initial model binding is invalid");
452
+ }
453
+ const botId = botIdentifier(bot.botId);
454
+ const createdBy = botWriter(bot.createdBy, "createdBy");
455
+ if (createdBy && createdBy.botId === botId)
456
+ throw new FlockDecodeError("createdBy is invalid");
457
+ return {
458
+ schemaVersion: 1,
459
+ botId,
460
+ registeredAt: boundedText(bot.registeredAt, "registeredAt", 64),
461
+ initialName: boundedText(bot.initialName, "initialName", 100),
462
+ ...(bot.initialDescription === undefined
463
+ ? {}
464
+ : {
465
+ initialDescription: boundedText(
466
+ bot.initialDescription,
467
+ "initialDescription",
468
+ 10_000,
469
+ ),
470
+ }),
471
+ initialModel,
472
+ initialModelBinding: binding,
473
+ ...(initialAssignments ? { initialAssignments } : {}),
474
+ ...(createdBy ? { createdBy } : {}),
475
+ sheep: decodeSheepRecipeV1(bot.sheep),
476
+ };
477
+ }
478
+
479
+ export function decodeBotMembershipViewV1(input: unknown): BotMembershipViewV1 {
480
+ const value = record(input, "Bot membership");
481
+ exact(value, ["schemaVersion", "botId", "registered"]);
482
+ if (value.schemaVersion !== 1 || typeof value.registered !== "boolean")
483
+ throw new FlockDecodeError("Bot membership is invalid");
484
+ return {
485
+ schemaVersion: 1,
486
+ botId: botIdentifier(value.botId),
487
+ registered: value.registered,
488
+ };
489
+ }
490
+
491
+ export function decodeDirectoryViewV1(input: unknown): BotDirectoryViewV1 {
492
+ const value = record(input, "Bot directory");
493
+ exact(value, ["schemaVersion", "revision", "bots"]);
494
+ if (
495
+ value.schemaVersion !== 1 ||
496
+ !Array.isArray(value.bots) ||
497
+ value.bots.length > FLOCK_DIRECTORY_LIMIT
498
+ )
499
+ throw new FlockDecodeError("Bot directory is invalid");
500
+ const bots = value.bots.map(decodeBotRegistrationV1);
501
+ if (new Set(bots.map((bot) => bot.botId)).size !== bots.length)
502
+ throw new FlockDecodeError("Bot directory contains duplicate IDs");
503
+ return {
504
+ schemaVersion: 1,
505
+ revision: revision(value.revision),
506
+ bots,
507
+ };
508
+ }
509
+
510
+ export function decodeBotLifecycleCommandV1(
511
+ input: unknown,
512
+ ): BotLifecycleCommandV1 {
513
+ const value = record(input, "Bot lifecycle command");
514
+ exact(value, ["schemaVersion", "type", "commandId", "botId"]);
515
+ if (
516
+ value.schemaVersion !== 1 ||
517
+ (value.type !== "bot/archive" && value.type !== "bot/restore")
518
+ )
519
+ throw new FlockDecodeError("unsupported Bot lifecycle command");
520
+ return {
521
+ schemaVersion: 1,
522
+ type: value.type,
523
+ commandId: identifier(value.commandId, "commandId"),
524
+ botId: botIdentifier(value.botId),
525
+ };
526
+ }
527
+
528
+ export function decodeBotLifecycleViewV1(input: unknown): BotLifecycleViewV1 {
529
+ const value = record(input, "Bot lifecycle");
530
+ exact(value, ["schemaVersion", "botId", "status", "revision"]);
531
+ if (
532
+ value.schemaVersion !== 1 ||
533
+ (value.status !== "active" && value.status !== "archived")
534
+ )
535
+ throw new FlockDecodeError("Bot lifecycle is invalid");
536
+ return {
537
+ schemaVersion: 1,
538
+ botId: botIdentifier(value.botId),
539
+ status: value.status,
540
+ revision: revision(value.revision),
541
+ };
542
+ }
543
+
544
+ export function decodeBotLifecycleDirectoryViewV1(
545
+ input: unknown,
546
+ ): BotLifecycleDirectoryViewV1 {
547
+ const value = record(input, "Bot lifecycle directory");
548
+ exact(value, ["schemaVersion", "lifecycles"]);
549
+ if (
550
+ value.schemaVersion !== 1 ||
551
+ !Array.isArray(value.lifecycles) ||
552
+ value.lifecycles.length > FLOCK_DIRECTORY_LIMIT
553
+ )
554
+ throw new FlockDecodeError("Bot lifecycle directory is invalid");
555
+ const lifecycles = value.lifecycles.map(decodeBotLifecycleViewV1);
556
+ if (new Set(lifecycles.map((item) => item.botId)).size !== lifecycles.length)
557
+ throw new FlockDecodeError(
558
+ "Bot lifecycle directory contains duplicate IDs",
559
+ );
560
+ return { schemaVersion: 1, lifecycles };
561
+ }
562
+
563
+ export function decodeBotLifecycleReceiptV1(
564
+ input: unknown,
565
+ ): BotLifecycleReceiptV1 {
566
+ const value = record(input, "Bot lifecycle receipt");
567
+ exact(
568
+ value,
569
+ ["schemaVersion", "commandId", "botId", "status", "lifecycle"],
570
+ ["failure"],
571
+ );
572
+ if (
573
+ value.schemaVersion !== 1 ||
574
+ (value.status !== "pending" &&
575
+ value.status !== "applied" &&
576
+ value.status !== "rejected")
577
+ )
578
+ throw new FlockDecodeError("Bot lifecycle receipt is invalid");
579
+ const lifecycle = decodeBotLifecycleViewV1(value.lifecycle);
580
+ const botId = botIdentifier(value.botId);
581
+ if (lifecycle.botId !== botId)
582
+ throw new FlockDecodeError("Bot lifecycle receipt identity is invalid");
583
+ return {
584
+ schemaVersion: 1,
585
+ commandId: identifier(value.commandId, "commandId"),
586
+ botId,
587
+ status: value.status,
588
+ lifecycle,
589
+ failure:
590
+ value.failure === undefined
591
+ ? undefined
592
+ : boundedText(value.failure, "Bot lifecycle receipt failure", 1_000),
593
+ };
594
+ }
595
+
596
+ export function decodeStoredBotLifecycleReceiptV1(
597
+ input: unknown,
598
+ ): StoredBotLifecycleReceiptV1 {
599
+ const value = record(input, "stored Bot lifecycle receipt");
600
+ exact(value, ["fingerprint", "receipt"]);
601
+ return {
602
+ fingerprint: boundedText(value.fingerprint, "fingerprint", 10_000),
603
+ receipt: decodeBotLifecycleReceiptV1(value.receipt),
604
+ };
605
+ }
606
+
607
+ export function decodeFlockReceiptV1(input: unknown): FlockReceiptV1 {
608
+ const value = record(input, "Flock receipt");
609
+ exact(
610
+ value,
611
+ ["schemaVersion", "commandId", "status", "revision"],
612
+ ["failure"],
613
+ );
614
+ if (
615
+ value.schemaVersion !== 1 ||
616
+ (value.status !== "applied" && value.status !== "rejected")
617
+ )
618
+ throw new FlockDecodeError("Flock receipt is invalid");
619
+ const failure =
620
+ value.failure === undefined
621
+ ? undefined
622
+ : boundedText(value.failure, "Flock receipt failure", 1_000);
623
+ return {
624
+ schemaVersion: 1,
625
+ commandId: identifier(value.commandId, "commandId"),
626
+ status: value.status,
627
+ revision: revision(value.revision),
628
+ failure,
629
+ };
630
+ }
631
+
632
+ export function decodeStoredFlockReceiptV1(
633
+ input: unknown,
634
+ ): StoredFlockReceiptV1 {
635
+ const value = record(input, "stored Flock receipt");
636
+ exact(value, ["fingerprint", "receipt"]);
637
+ return {
638
+ fingerprint: boundedText(value.fingerprint, "fingerprint", 10_000),
639
+ receipt: decodeFlockReceiptV1(value.receipt),
640
+ };
641
+ }
642
+
643
+ export function decodeSheepIdentityViewV1(input: unknown): SheepIdentityViewV1 {
644
+ const value = record(input, "sheep identity");
645
+ exact(value, ["schemaVersion", "botId", "revision", "sheep"]);
646
+ if (value.schemaVersion !== 1)
647
+ throw new FlockDecodeError("unsupported sheep identity");
648
+ return {
649
+ schemaVersion: 1,
650
+ botId: botIdentifier(value.botId),
651
+ revision: revision(value.revision),
652
+ sheep: decodeSheepRecipeV1(value.sheep),
653
+ };
654
+ }
655
+
656
+ function nameProvenance(value: unknown): BotNameProvenanceV1 {
657
+ if (value !== "user" && value !== "bot")
658
+ throw new FlockDecodeError("namedBy is invalid");
659
+ return value;
660
+ }
661
+
662
+ export function decodeBotIdentityViewV1(input: unknown): BotIdentityViewV1 {
663
+ const value = record(input, "Bot identity");
664
+ exact(
665
+ value,
666
+ ["schemaVersion", "botId", "name", "namedBy", "hiddenFromSidebar"],
667
+ ["label", "title"],
668
+ );
669
+ if (value.schemaVersion !== 1 || typeof value.hiddenFromSidebar !== "boolean")
670
+ throw new FlockDecodeError("Bot identity is invalid");
671
+ return {
672
+ schemaVersion: 1,
673
+ botId: botIdentifier(value.botId),
674
+ name: boundedText(value.name, "name", 100),
675
+ namedBy: nameProvenance(value.namedBy),
676
+ hiddenFromSidebar: value.hiddenFromSidebar,
677
+ ...(value.label === undefined
678
+ ? {}
679
+ : { label: boundedText(value.label, "label", 120) }),
680
+ ...(value.title === undefined
681
+ ? {}
682
+ : { title: boundedText(value.title, "title", 120) }),
683
+ };
684
+ }
685
+
686
+ export function decodeBotIdentityDirectoryViewV1(
687
+ input: unknown,
688
+ ): BotIdentityDirectoryViewV1 {
689
+ const value = record(input, "Bot identity directory");
690
+ exact(value, ["schemaVersion", "identities"]);
691
+ if (
692
+ value.schemaVersion !== 1 ||
693
+ !Array.isArray(value.identities) ||
694
+ value.identities.length > FLOCK_DIRECTORY_LIMIT
695
+ )
696
+ throw new FlockDecodeError("Bot identity directory is invalid");
697
+ const identities = value.identities.map(decodeBotIdentityViewV1);
698
+ if (new Set(identities.map((item) => item.botId)).size !== identities.length)
699
+ throw new FlockDecodeError("Bot identity directory contains duplicate IDs");
700
+ return { schemaVersion: 1, identities };
701
+ }
702
+
703
+ export function randomSheepRecipeV1(
704
+ random: () => number = Math.random,
705
+ ): SheepRecipeV1 {
706
+ const pick = (items: string[]) =>
707
+ items[Math.min(items.length - 1, Math.floor(random() * items.length))]!;
708
+ return {
709
+ schemaVersion: 1,
710
+ background: pick(backgrounds),
711
+ upper: pick(ids.upper),
712
+ middle: pick(ids.middle),
713
+ lower: pick(ids.lower),
714
+ };
715
+ }
716
+
717
+ export function sheepLayerIds(recipe: SheepRecipeV1): string[] {
718
+ const result = [`background-${recipe.background}`, "canonical"];
719
+ for (const selected of [recipe.upper, recipe.middle, recipe.lower]) {
720
+ const path: string[] = [];
721
+ let node = nodeIndex.get(selected);
722
+ while (node) {
723
+ if (node.parent !== null) path.unshift(node.id);
724
+ node = node.parent ? nodeIndex.get(node.parent) : undefined;
725
+ }
726
+ result.push(...path);
727
+ }
728
+ return result;
729
+ }
730
+
731
+ export function flockCommandFingerprint(
732
+ value: CreateBotCommandV1 | UpdateSheepCommandV1 | BotLifecycleCommandV1,
733
+ ): string {
734
+ return JSON.stringify(value);
735
+ }