@frockbot/plugin-shell 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/backend.ts CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  type SessionEvent,
6
6
  validateToolOccurrenceJournal,
7
7
  type BotCapabilitiesStub,
8
- type IsolateModelInvocationV1,
8
+ type IsolateModelOutcomeV1,
9
9
  type IsolatePendingDecisionV1,
10
10
  type NormalizedModelRequest,
11
11
  type PackageBundlerBinding,
@@ -42,45 +42,40 @@ import {
42
42
  } from "@frockbot/application-foundation/runtime";
43
43
  import {
44
44
  applyBotProfilePatchV1,
45
- capabilityAssignmentFailureV1,
46
45
  configurationCommandFingerprintV1,
47
46
  ConfigurationConflictError,
48
47
  decodeBotConfigurationExecuteRpcV1,
49
48
  decodeBotConfigurationReadRpcV1,
50
49
  decodeCompositionCommandReceiptV1,
50
+ decodeInstalledPackageSettingIdsV1,
51
+ decodeInstalledPackageSettingsPatchV1,
51
52
  MAX_COMPOSITION_GENERATION_PAGE_V1,
52
53
  type CompositionCommandReceiptV1,
53
54
  type CompositionGenerationListViewV1,
54
55
  type CompositionGenerationViewV1,
55
56
  type RevertCompositionCommandV1,
56
- type ConnectionDependencyRequirementV1,
57
57
  type BotExecutionPlanV1,
58
58
  type BotSelfWriterV1,
59
59
  type BotSettingsViewV1,
60
- type CapabilityAssignmentView,
61
- type ModelAssignment,
60
+ type EnabledCapabilityV1,
62
61
  type ConnectionView,
63
62
  type ConfigurationCommandV1,
64
63
  type OperationReceiptV1,
64
+ type PackageSettingValueV1,
65
65
  type ResolvedModelBindingV1,
66
66
  initializeBotSettingsV1,
67
67
  resolvePackageSettingValuesV1,
68
68
  resolveBotExecutionPlanV1,
69
- resolveBotModelBindingV1,
70
69
  resolveEffectiveBotModelV1,
71
70
  type UserSettingsViewV1,
72
71
  } from "@frockbot/configuration-core";
73
72
  import {
74
- createFoundationAssignedRuntimePackages,
73
+ createFoundationEnabledRuntimePackages,
75
74
  mergeFoundationRuntimePackages,
76
75
  createFoundationHostedRuntimePackages,
77
76
  mergeFoundationRuntimePackagesV1,
78
77
  type PackagePublisherAgentHost,
79
78
  } from "@frockbot/application-foundation/runtime";
80
- import {
81
- requireStoredAssignmentSaga,
82
- type StoredAssignmentSaga,
83
- } from "./backend-assignment.js";
84
79
  import {
85
80
  cancelStoredRun,
86
81
  completeStoredRun,
@@ -125,10 +120,7 @@ import {
125
120
  type TemplateShareReceiptV1,
126
121
  } from "@frockbot/plugin-bot-template/shared";
127
122
  import { createBotMemoryHost } from "./backend-memory.js";
128
- import {
129
- createBotImageHost,
130
- type WorkersAiBindingV1,
131
- } from "./backend-image.js";
123
+ import { createBotImageHost, type NativeAiBindingV1 } from "./backend-image.js";
132
124
  import {
133
125
  createBotPluginSkillsSource,
134
126
  createBotSkillCatalogReader,
@@ -298,12 +290,13 @@ import {
298
290
  createR2PackageArtifactStore,
299
291
  isolateBindingDigestV1,
300
292
  type BotCapabilitiesPropsV1,
301
- type IsolateAssignmentV1,
293
+ type IsolateCapabilityV1,
302
294
  type IsolateCapabilityHost,
303
295
  type IsolateModelBindingV1,
304
296
  type IsolateModelPath,
305
297
  type IsolateModelRequestRecordV1,
306
298
  type IsolatePendingAuthorityDecisionV1,
299
+ type IsolateUnavailableModelBindingV1,
307
300
  } from "./backend-isolate.js";
308
301
  import type { BotIsolateLoader } from "@frockbot/kernel-composition/isolate";
309
302
  import type {
@@ -379,10 +372,6 @@ import {
379
372
 
380
373
  export const BOT_CONFIGURATION_KEY = "bot-configuration";
381
374
  const CONFIGURATION_RECEIPT_PREFIX = "configuration-receipt:";
382
- const ASSIGNMENT_GENERATION_PREFIX = "assignment-generation:";
383
- const ASSIGNMENT_COMPENSATION_PREFIX = "assignment-compensation:";
384
- const ASSIGNMENT_TOMBSTONE_PREFIX = "assignment-tombstone:";
385
- const ASSIGNMENT_SAGA_PREFIX = "assignment-saga:";
386
375
  const STOP_RECEIPT_PREFIX = "stop-receipt:";
387
376
  /**
388
377
  * The Bot's durable announcement log: Session events that happen outside any
@@ -396,18 +385,9 @@ export const BOT_ANNOUNCEMENT_RETENTION = 32;
396
385
  function botAnnouncementKey(seq: number): string {
397
386
  return `${BOT_ANNOUNCEMENT_PREFIX}${String(seq).padStart(12, "0")}`;
398
387
  }
399
- const ASSIGNMENT_SAGA_DEADLINE_MS = 60_000;
400
388
  /** Idempotency records for Composition commands this Package admits. */
401
389
  const COMPOSITION_COMMAND_PREFIX = "composition-command:";
402
390
 
403
- function assignmentGenerationKey(assignmentId: string): string {
404
- return `${ASSIGNMENT_GENERATION_PREFIX}${assignmentId}`;
405
- }
406
-
407
- function capabilityKey(packageId: string, capabilityId: string): string {
408
- return `${packageId}:${capabilityId}`;
409
- }
410
-
411
391
  interface StoredConfigurationReceipt {
412
392
  commandFingerprint: string;
413
393
  receipt: OperationReceiptV1;
@@ -428,7 +408,7 @@ function isTerminalStoredRunStatus(status: StoredRunStatus): boolean {
428
408
  );
429
409
  }
430
410
 
431
- interface AssignmentActivity {
411
+ interface ConfigurationActivity {
432
412
  commandFingerprint: string;
433
413
  promise: Promise<OperationReceiptV1>;
434
414
  }
@@ -471,8 +451,16 @@ export interface BotStateEnv {
471
451
  */
472
452
  PACKAGE_BUNDLER?: PackageBundlerBinding;
473
453
  MEMORY_INDEX: VectorizeIndex;
474
- /** The native Workers AI binding consumed through narrow Package adapters. */
475
- AI?: WorkersAiBindingV1;
454
+ /** The native AI binding consumed through the image Package adapter. */
455
+ AI?: NativeAiBindingV1;
456
+ /** The Flock AI Gateway adapter constructed by the Cloudflare host. */
457
+ FLOCK_AI?: {
458
+ autoRoute: string;
459
+ runChatCompletion(
460
+ gatewayModel: string,
461
+ body: Record<string, unknown>,
462
+ ): Promise<ReadableStream<Uint8Array>>;
463
+ };
476
464
  USER_CONFIGURATIONS: DurableObjectNamespace;
477
465
  /**
478
466
  * The Bot Durable Object namespace, as the Subagent Durable Object namespace
@@ -545,7 +533,10 @@ export class ShellBotBackendContribution {
545
533
  Promise<ClientTurnV1>
546
534
  >();
547
535
  private readonly outboundFetch?: typeof fetch;
548
- private readonly assignmentActivities = new Map<string, AssignmentActivity>();
536
+ private readonly configurationActivities = new Map<
537
+ string,
538
+ ConfigurationActivity
539
+ >();
549
540
  /** The Turn currently executing on this object, for durable Stop. */
550
541
  private activeTurn:
551
542
  { runId: string; sessionId: string; cancel(): void } | undefined;
@@ -620,7 +611,7 @@ export class ShellBotBackendContribution {
620
611
  terminalRecords: (input) => this.terminalPackageRecords(input),
621
612
  scheduledDeadlines: (transaction) =>
622
613
  this.scheduledDeadlines(transaction),
623
- scheduledWorkInFlight: () => this.assignmentActivities.size > 0,
614
+ scheduledWorkInFlight: () => false,
624
615
  deferScheduledWork: (transaction) =>
625
616
  this.deferScheduledWork(transaction),
626
617
  settleScheduledWork: () => this.settleScheduledWork(),
@@ -634,12 +625,6 @@ export class ShellBotBackendContribution {
634
625
  name: string;
635
626
  /** The persona the Bot's profile is seeded with, when its creator gave one. */
636
627
  description?: string;
637
- model?: BotSettingsViewV1["model"];
638
- modelBinding?: {
639
- assignment: BotSettingsViewV1["assignments"][number];
640
- generation: string;
641
- };
642
- assignments?: BotSettingsViewV1["assignments"];
643
628
  },
644
629
  ): Promise<BotSettingsViewV1> {
645
630
  return this.ctx.storage.transaction(async (transaction) => {
@@ -656,58 +641,24 @@ export class ShellBotBackendContribution {
656
641
  );
657
642
  if (existing) return existing;
658
643
  const settings = {
659
- ...this.initialBotSettings(identity.botId, initial.model),
644
+ ...this.initialBotSettings(identity.botId),
660
645
  profile: {
661
646
  name: initial.name,
662
647
  ...(initial.description === undefined
663
648
  ? {}
664
649
  : { description: initial.description }),
665
650
  },
666
- assignments: [
667
- ...(initial.assignments ?? []).map((assignment) =>
668
- structuredClone(assignment),
669
- ),
670
- ...(initial.modelBinding
671
- ? [structuredClone(initial.modelBinding.assignment)]
672
- : []),
673
- ],
674
651
  } satisfies BotSettingsViewV1;
675
652
  await transaction.put({
676
653
  [IDENTITY_KEY]: durableIdentity ?? identity,
677
654
  [BOT_CONFIGURATION_KEY]: settings,
678
- ...(initial.modelBinding
679
- ? {
680
- [assignmentGenerationKey(
681
- initial.modelBinding.assignment.assignmentId,
682
- )]: initial.modelBinding.generation,
683
- }
684
- : {}),
685
655
  });
686
656
  return settings;
687
657
  });
688
658
  }
689
659
 
690
660
  async getSettings(identity: BotIdentity): Promise<BotSettingsViewV1> {
691
- const settings = await this.ensureBotSettings(identity);
692
- if (settings.assignments.length === 0) return settings;
693
- const [user, application] = await Promise.all([
694
- this.userConfiguration(identity).readConfiguration({
695
- schemaVersion: 1,
696
- userId: identity.userId,
697
- }),
698
- this.compileApplication(),
699
- ]);
700
- const plan = resolveBotExecutionPlanV1({
701
- bot: settings,
702
- user,
703
- packages: application.packages.map((pkg) => ({
704
- packageId: pkg.id,
705
- version: pkg.version,
706
- capabilities: pkg.manifest.configuration?.capabilities ?? [],
707
- connectionTypes: pkg.manifest.configuration?.connectionTypes ?? [],
708
- })),
709
- });
710
- return { ...settings, assignments: plan.assignments };
661
+ return this.ensureBotSettings(identity);
711
662
  }
712
663
 
713
664
  async readConfiguration(input: unknown): Promise<BotSettingsViewV1> {
@@ -729,7 +680,7 @@ export class ShellBotBackendContribution {
729
680
  command: Extract<ConfigurationCommandV1, { botId: string }>,
730
681
  ): Promise<OperationReceiptV1> {
731
682
  const commandFingerprint = configurationCommandFingerprintV1(command);
732
- const active = this.assignmentActivities.get(command.commandId);
683
+ const active = this.configurationActivities.get(command.commandId);
733
684
  if (active) {
734
685
  if (active.commandFingerprint !== commandFingerprint) {
735
686
  throw new Error(
@@ -745,12 +696,13 @@ export class ShellBotBackendContribution {
745
696
  commandFingerprint,
746
697
  ).finally(() => {
747
698
  if (
748
- this.assignmentActivities.get(command.commandId)?.promise === activity
699
+ this.configurationActivities.get(command.commandId)?.promise ===
700
+ activity
749
701
  ) {
750
- this.assignmentActivities.delete(command.commandId);
702
+ this.configurationActivities.delete(command.commandId);
751
703
  }
752
704
  });
753
- this.assignmentActivities.set(command.commandId, {
705
+ this.configurationActivities.set(command.commandId, {
754
706
  commandFingerprint,
755
707
  promise: activity,
756
708
  });
@@ -767,46 +719,18 @@ export class ShellBotBackendContribution {
767
719
  const existing =
768
720
  await this.ctx.storage.get<StoredConfigurationReceipt>(receiptKey);
769
721
  if (existing) {
770
- const receipt = requireMatchingConfigurationReceipt(
722
+ return requireMatchingConfigurationReceipt(
771
723
  existing,
772
724
  commandFingerprint,
773
725
  command.commandId,
774
726
  );
775
- if (
776
- command.type === "bot/assign-capability" ||
777
- command.type === "bot/replace-capability" ||
778
- command.type === "bot/unassign-capability"
779
- ) {
780
- try {
781
- await this.reconcileStoredAssignmentSaga(
782
- identity,
783
- command.commandId,
784
- commandFingerprint,
785
- );
786
- } catch {
787
- // The durable accepted receipt remains replayable while recovery is
788
- // retrying; alarm reconciliation owns eventual settlement.
789
- }
790
- const pending = await this.ctx.storage.get<unknown>(
791
- `${ASSIGNMENT_SAGA_PREFIX}${command.commandId}`,
792
- );
793
- if (pending !== undefined) {
794
- const saga = requireStoredAssignmentSaga(pending);
795
- if (saga.commandFingerprint !== commandFingerprint) {
796
- throw new Error(
797
- `Configuration command idempotency key "${command.commandId}" was reused for a different command`,
798
- );
799
- }
800
- return saga.acceptedReceipt;
801
- }
802
- }
803
- return receipt;
804
727
  }
805
728
  if (command.expectedRevision !== settings.revision) {
806
729
  throw new ConfigurationConflictError(settings.revision);
807
730
  }
808
- let modelCapabilities = new Set<string>();
809
- if (command.type === "bot/select-model") {
731
+ let packageValues: Record<string, unknown> | undefined;
732
+ let packageUnset: string[] | undefined;
733
+ if (command.type === "bot/set-package-settings") {
810
734
  const [user, application] = await Promise.all([
811
735
  this.userConfiguration(identity).readConfiguration({
812
736
  schemaVersion: 1,
@@ -814,243 +738,39 @@ export class ShellBotBackendContribution {
814
738
  }),
815
739
  this.compileApplication(),
816
740
  ]);
817
- modelCapabilities = new Set(
818
- application.packages.flatMap((pkg) =>
819
- (pkg.manifest.configuration?.capabilities ?? []).flatMap(
820
- (capability) =>
821
- capability.kind === "model"
822
- ? [capabilityKey(pkg.id, capability.id)]
823
- : [],
824
- ),
825
- ),
826
- );
827
- const binding = resolveBotModelBindingV1({
828
- model: command.model,
829
- assignments: settings.assignments,
830
- user,
831
- packages: application.packages.map((pkg) => ({
832
- packageId: pkg.id,
833
- version: pkg.version,
834
- capabilities: pkg.manifest.configuration?.capabilities ?? [],
835
- connectionTypes: pkg.manifest.configuration?.connectionTypes ?? [],
836
- })),
837
- });
838
- if (binding.state === "unavailable") {
839
- return this.rejectConfigurationCommand(
840
- identity,
841
- command,
842
- commandFingerprint,
843
- binding.failure ?? "Bot model binding is unavailable",
844
- );
845
- }
846
- }
847
- if (
848
- command.type === "bot/assign-capability" ||
849
- command.type === "bot/replace-capability"
850
- ) {
851
- const existingAssignment = settings.assignments.find(
852
- (assignment) =>
853
- assignment.assignmentId === command.assignment.assignmentId,
854
- );
855
- if (
856
- existingAssignment &&
857
- (existingAssignment.packageId !== command.assignment.packageId ||
858
- existingAssignment.capabilityId !== command.assignment.capabilityId)
859
- ) {
860
- return this.rejectConfigurationCommand(
861
- identity,
862
- command,
863
- commandFingerprint,
864
- "Assignment ID cannot change Package Capability authority",
865
- );
866
- }
867
- const [user, application] = await Promise.all([
868
- this.userConfiguration(identity).readConfiguration({
869
- schemaVersion: 1,
870
- userId: identity.userId,
871
- }),
872
- this.compileApplication(),
873
- ]);
874
- modelCapabilities = new Set(
875
- application.packages.flatMap((pkg) =>
876
- (pkg.manifest.configuration?.capabilities ?? []).flatMap(
877
- (capability) =>
878
- capability.kind === "model"
879
- ? [capabilityKey(pkg.id, capability.id)]
880
- : [],
881
- ),
882
- ),
883
- );
884
- const failure = capabilityAssignmentFailureV1({
885
- assignment: command.assignment,
886
- user,
887
- packages: application.packages.map((pkg) => ({
888
- packageId: pkg.id,
889
- version: pkg.version,
890
- capabilities: pkg.manifest.configuration?.capabilities ?? [],
891
- connectionTypes: pkg.manifest.configuration?.connectionTypes ?? [],
892
- })),
893
- });
894
- if (failure) {
895
- return this.rejectConfigurationCommand(
896
- identity,
897
- command,
898
- commandFingerprint,
899
- failure,
900
- );
901
- }
902
- if (command.model) {
903
- if (command.model.connectionId !== command.assignment.connectionId) {
904
- return this.rejectConfigurationCommand(
905
- identity,
906
- command,
907
- commandFingerprint,
908
- "Model binding must use the assigned Connection",
909
- );
910
- }
911
- const binding = resolveBotModelBindingV1({
912
- model: command.model,
913
- assignments: [
914
- ...settings.assignments,
915
- { ...command.assignment, state: "enabled" },
916
- ],
917
- user,
918
- packages: application.packages.map((pkg) => ({
919
- packageId: pkg.id,
920
- version: pkg.version,
921
- capabilities: pkg.manifest.configuration?.capabilities ?? [],
922
- connectionTypes: pkg.manifest.configuration?.connectionTypes ?? [],
923
- })),
741
+ const packages = application.packages.map((pkg) => ({
742
+ packageId: pkg.id,
743
+ version: pkg.version,
744
+ settings: pkg.manifest.configuration?.settings ?? [],
745
+ }));
746
+ if (command.values) {
747
+ packageValues = decodeInstalledPackageSettingsPatchV1({
748
+ packageId: command.packageId,
749
+ values: command.values,
750
+ scope: "bot",
751
+ installations: user.packages,
752
+ packages,
924
753
  });
925
- if (binding.state === "unavailable") {
926
- return this.rejectConfigurationCommand(
927
- identity,
928
- command,
929
- commandFingerprint,
930
- binding.failure ?? "Bot model binding is unavailable",
931
- );
932
- }
933
754
  }
934
- }
935
- if (
936
- command.type === "bot/select-model" &&
937
- settings.model?.connectionId &&
938
- settings.model.connectionId !== command.model.connectionId
939
- ) {
940
- // Moving the model to another Connection changes Assignment authority,
941
- // so it is a Replace, not a select. One durable shape, one saga.
942
- return this.rejectConfigurationCommand(
943
- identity,
944
- command,
945
- commandFingerprint,
946
- "Selecting a model on another Connection requires Replace",
947
- );
948
- }
949
- if (command.type === "bot/unbind-model") {
950
- const assignment = settings.assignments.find(
951
- (candidate) =>
952
- candidate.assignmentId === command.assignmentId &&
953
- (candidate.state === "enabled" ||
954
- candidate.state === "unavailable") &&
955
- candidate.connectionId === settings.model?.connectionId,
956
- );
957
- const application = await this.compileApplication();
958
- const capability = application.packages
959
- .find((pkg) => pkg.id === assignment?.packageId)
960
- ?.manifest.configuration?.capabilities.find(
961
- (candidate) => candidate.id === assignment?.capabilityId,
962
- );
963
- if (!assignment?.connectionId || capability?.kind !== "model") {
964
- return this.rejectConfigurationCommand(
965
- identity,
966
- command,
967
- commandFingerprint,
968
- "Bot model assignment is unavailable",
969
- );
970
- }
971
- const generation = await this.ctx.storage.get<string>(
972
- assignmentGenerationKey(assignment.assignmentId),
973
- );
974
- if (!generation) {
975
- return this.rejectConfigurationCommand(
976
- identity,
977
- command,
978
- commandFingerprint,
979
- "Bot model assignment generation is unavailable",
980
- );
755
+ if (command.unset) {
756
+ packageUnset = decodeInstalledPackageSettingIdsV1({
757
+ packageId: command.packageId,
758
+ unset: command.unset,
759
+ scope: "bot",
760
+ installations: user.packages,
761
+ packages,
762
+ });
981
763
  }
982
- // Unbinding the model is the Assignment's Unassign: one saga releases
983
- // the Connection dependency and clears the Bot's model together.
984
- return this.executeAssignmentCommand(
985
- identity,
986
- {
987
- ...command,
988
- type: "bot/unassign-capability",
989
- assignmentId: assignment.assignmentId,
990
- },
991
- commandFingerprint,
992
- { clearModel: true },
993
- );
994
- }
995
-
996
- if (
997
- command.type !== "bot/assign-capability" &&
998
- command.type !== "bot/replace-capability" &&
999
- command.type !== "bot/unassign-capability"
1000
- ) {
1001
- return this.applySimpleConfigurationCommand(
1002
- identity,
1003
- command,
1004
- commandFingerprint,
1005
- );
1006
764
  }
1007
- return this.executeAssignmentCommand(
765
+ return this.applySimpleConfigurationCommand(
1008
766
  identity,
1009
767
  command,
1010
768
  commandFingerprint,
1011
- {
1012
- ...(command.type === "bot/unassign-capability"
1013
- ? {}
1014
- : { model: command.model }),
1015
- },
769
+ packageValues,
770
+ packageUnset,
1016
771
  );
1017
772
  }
1018
773
 
1019
- private async rejectConfigurationCommand(
1020
- identity: BotIdentity,
1021
- command: Extract<ConfigurationCommandV1, { botId: string }>,
1022
- commandFingerprint: string,
1023
- failure: string,
1024
- ): Promise<OperationReceiptV1> {
1025
- return this.ctx.storage.transaction(async (transaction) => {
1026
- const receiptKey = `${CONFIGURATION_RECEIPT_PREFIX}${command.commandId}`;
1027
- const existing =
1028
- await transaction.get<StoredConfigurationReceipt>(receiptKey);
1029
- if (existing) {
1030
- return requireMatchingConfigurationReceipt(
1031
- existing,
1032
- commandFingerprint,
1033
- command.commandId,
1034
- );
1035
- }
1036
- const current =
1037
- (await transaction.get<BotSettingsViewV1>(BOT_CONFIGURATION_KEY)) ??
1038
- this.initialBotSettings(identity.botId);
1039
- if (command.expectedRevision !== current.revision) {
1040
- throw new ConfigurationConflictError(current.revision);
1041
- }
1042
- const receipt: OperationReceiptV1 = {
1043
- schemaVersion: 1,
1044
- commandId: command.commandId,
1045
- revision: current.revision,
1046
- status: "rejected",
1047
- failure,
1048
- };
1049
- await transaction.put(receiptKey, { commandFingerprint, receipt });
1050
- return receipt;
1051
- });
1052
- }
1053
-
1054
774
  private async applySimpleConfigurationCommand(
1055
775
  identity: BotIdentity,
1056
776
  command: Extract<
@@ -1060,10 +780,12 @@ export class ShellBotBackendContribution {
1060
780
  | "bot/update-profile"
1061
781
  | "bot/set-profile"
1062
782
  | "bot/update-notifications"
1063
- | "bot/select-model";
783
+ | "bot/set-package-settings";
1064
784
  }
1065
785
  >,
1066
786
  commandFingerprint: string,
787
+ packageValues?: Record<string, unknown>,
788
+ packageUnset: readonly string[] = [],
1067
789
  ): Promise<OperationReceiptV1> {
1068
790
  return this.ctx.storage.transaction(async (transaction) => {
1069
791
  await this.lifecycleAdmission?.(transaction, identity.botId);
@@ -1083,9 +805,6 @@ export class ShellBotBackendContribution {
1083
805
  if (command.expectedRevision !== current.revision) {
1084
806
  throw new ConfigurationConflictError(current.revision);
1085
807
  }
1086
- if (current.assignmentOperations.length > 0) {
1087
- throw new Error("An Assignment operation is still retrying");
1088
- }
1089
808
  const revision = current.revision + 1;
1090
809
  const next: BotSettingsViewV1 =
1091
810
  command.type === "bot/update-profile"
@@ -1102,7 +821,25 @@ export class ShellBotBackendContribution {
1102
821
  }
1103
822
  : command.type === "bot/update-notifications"
1104
823
  ? { ...current, revision, notifications: command.notifications }
1105
- : { ...current, revision, model: command.model };
824
+ : (() => {
825
+ const values = {
826
+ ...(current.packageValues[command.packageId] ?? {}),
827
+ ...structuredClone(packageValues ?? {}),
828
+ };
829
+ for (const settingId of packageUnset)
830
+ delete values[settingId];
831
+ const nextPackageValues = { ...current.packageValues };
832
+ if (Object.keys(values).length > 0) {
833
+ nextPackageValues[command.packageId] = values;
834
+ } else {
835
+ delete nextPackageValues[command.packageId];
836
+ }
837
+ return {
838
+ ...current,
839
+ revision,
840
+ packageValues: nextPackageValues,
841
+ };
842
+ })();
1106
843
  const receipt: OperationReceiptV1 = {
1107
844
  schemaVersion: 1,
1108
845
  commandId: command.commandId,
@@ -1187,752 +924,23 @@ export class ShellBotBackendContribution {
1187
924
  const expired = [...stored.keys()]
1188
925
  .sort()
1189
926
  .slice(0, Math.max(0, stored.size - BOT_ANNOUNCEMENT_RETENTION));
1190
- if (expired.length > 0) await transaction.delete(expired);
1191
- }
1192
-
1193
- /** The announcements the Session shows, oldest first. */
1194
- async listAnnouncements(): Promise<SessionEvent[]> {
1195
- const stored = await this.ctx.storage.list<unknown>({
1196
- prefix: BOT_ANNOUNCEMENT_PREFIX,
1197
- });
1198
- return [...stored.entries()]
1199
- .sort(([left], [right]) => left.localeCompare(right))
1200
- .map(([, value]) => decodeSessionEvent(value));
1201
- }
1202
-
1203
- private async assignmentRequirement(
1204
- identity: BotIdentity,
1205
- assignment: Omit<BotSettingsViewV1["assignments"][number], "state">,
1206
- ): Promise<
1207
- | {
1208
- user: UserSettingsViewV1;
1209
- requirement?: ConnectionDependencyRequirementV1;
1210
- }
1211
- | { failure: string }
1212
- > {
1213
- const [user, application] = await Promise.all([
1214
- this.userConfiguration(identity).readConfiguration({
1215
- schemaVersion: 1,
1216
- userId: identity.userId,
1217
- }),
1218
- this.compileApplication(),
1219
- ]);
1220
- const packages = application.packages.map((pkg) => ({
1221
- packageId: pkg.id,
1222
- version: pkg.version,
1223
- capabilities: pkg.manifest.configuration?.capabilities ?? [],
1224
- connectionTypes: pkg.manifest.configuration?.connectionTypes ?? [],
1225
- }));
1226
- const failure = capabilityAssignmentFailureV1({
1227
- assignment,
1228
- user,
1229
- packages,
1230
- });
1231
- if (failure) return { failure };
1232
- if (!assignment.connectionId) return { user };
1233
- const installation = user.packages.find(
1234
- (pkg) =>
1235
- pkg.packageId === assignment.packageId && pkg.state === "installed",
1236
- );
1237
- const pkg = application.packages.find(
1238
- (candidate) =>
1239
- candidate.id === assignment.packageId &&
1240
- candidate.version === installation?.version,
1241
- );
1242
- const capability = pkg?.manifest.configuration?.capabilities.find(
1243
- (candidate) => candidate.id === assignment.capabilityId,
1244
- );
1245
- if (!installation || !pkg || !capability) {
1246
- return {
1247
- failure: "Capability assignment policy changed during validation",
1248
- };
1249
- }
1250
- return {
1251
- user,
1252
- requirement: {
1253
- schemaVersion: 1,
1254
- packageId: pkg.id,
1255
- packageVersion: pkg.version,
1256
- capabilityId: capability.id,
1257
- connectionTypeIds: [...capability.connectionTypes],
1258
- },
1259
- };
1260
- }
1261
-
1262
- private async executeAssignmentCommand(
1263
- identity: BotIdentity,
1264
- command: Extract<
1265
- ConfigurationCommandV1,
1266
- {
1267
- type:
1268
- | "bot/assign-capability"
1269
- | "bot/replace-capability"
1270
- | "bot/unassign-capability";
1271
- }
1272
- >,
1273
- commandFingerprint: string,
1274
- binding: { model?: ModelAssignment; clearModel?: boolean } = {},
1275
- ): Promise<OperationReceiptV1> {
1276
- try {
1277
- await this.reconcileStoredAssignmentSaga(
1278
- identity,
1279
- command.commandId,
1280
- commandFingerprint,
1281
- );
1282
- } catch (error) {
1283
- const pending = await this.ctx.storage.get<unknown>(
1284
- `${ASSIGNMENT_SAGA_PREFIX}${command.commandId}`,
1285
- );
1286
- if (pending === undefined) throw error;
1287
- const saga = requireStoredAssignmentSaga(pending);
1288
- if (saga.commandFingerprint !== commandFingerprint) throw error;
1289
- return saga.acceptedReceipt;
1290
- }
1291
- const receiptKey = `${CONFIGURATION_RECEIPT_PREFIX}${command.commandId}`;
1292
- const receipt =
1293
- await this.ctx.storage.get<StoredConfigurationReceipt>(receiptKey);
1294
- if (receipt) {
1295
- return requireMatchingConfigurationReceipt(
1296
- receipt,
1297
- commandFingerprint,
1298
- command.commandId,
1299
- );
1300
- }
1301
- const pending = await this.ctx.storage.get<unknown>(
1302
- `${ASSIGNMENT_SAGA_PREFIX}${command.commandId}`,
1303
- );
1304
- if (pending !== undefined) {
1305
- const saga = requireStoredAssignmentSaga(pending);
1306
- if (saga.commandFingerprint !== commandFingerprint) {
1307
- throw new Error(
1308
- `Configuration command idempotency key "${command.commandId}" was reused for a different command`,
1309
- );
1310
- }
1311
- return saga.acceptedReceipt;
1312
- }
1313
- const current = await this.ensureBotSettings(identity);
1314
- const assignmentId =
1315
- command.type === "bot/unassign-capability"
1316
- ? command.assignmentId
1317
- : command.assignment.assignmentId;
1318
- const previous = current.assignments.find(
1319
- (assignment) => assignment.assignmentId === assignmentId,
1320
- );
1321
- if (command.type === "bot/assign-capability" && previous) {
1322
- return this.rejectAssignmentCommand(
1323
- identity,
1324
- command.commandId,
1325
- commandFingerprint,
1326
- `Assignment "${assignmentId}" already exists; use Replace`,
1327
- );
1328
- }
1329
- if (command.type !== "bot/assign-capability" && !previous) {
1330
- return this.rejectAssignmentCommand(
1331
- identity,
1332
- command.commandId,
1333
- commandFingerprint,
1334
- `Assignment "${assignmentId}" does not exist`,
1335
- );
1336
- }
1337
- let targetRequirement: ConnectionDependencyRequirementV1 | undefined;
1338
- if (command.type !== "bot/unassign-capability") {
1339
- const validation = await this.assignmentRequirement(
1340
- identity,
1341
- command.assignment,
1342
- );
1343
- if ("failure" in validation) {
1344
- return this.rejectAssignmentCommand(
1345
- identity,
1346
- command.commandId,
1347
- commandFingerprint,
1348
- validation.failure,
1349
- );
1350
- }
1351
- targetRequirement = validation.requirement;
1352
- }
1353
- const previousGeneration = previous?.connectionId
1354
- ? await this.ctx.storage.get<string>(
1355
- `${ASSIGNMENT_GENERATION_PREFIX}${previous.assignmentId}`,
1356
- )
1357
- : undefined;
1358
- const operation =
1359
- command.type === "bot/assign-capability"
1360
- ? "assigning"
1361
- : command.type === "bot/replace-capability"
1362
- ? "replacing"
1363
- : "unassigning";
1364
- await this.ctx.storage.transaction(async (transaction) => {
1365
- await this.lifecycleAdmission?.(transaction, identity.botId);
1366
- const existing = await transaction.get<StoredAssignmentSaga>(
1367
- `${ASSIGNMENT_SAGA_PREFIX}${command.commandId}`,
1368
- );
1369
- if (existing) return;
1370
- const durable =
1371
- (await transaction.get<BotSettingsViewV1>(BOT_CONFIGURATION_KEY)) ??
1372
- this.initialBotSettings(identity.botId);
1373
- if (durable.revision !== command.expectedRevision) {
1374
- throw new ConfigurationConflictError(durable.revision);
1375
- }
1376
- if (durable.assignmentOperations.length > 0) {
1377
- throw new Error("Another Assignment operation is already pending");
1378
- }
1379
- const target =
1380
- command.type === "bot/unassign-capability"
1381
- ? undefined
1382
- : structuredClone(command.assignment);
1383
- const phase =
1384
- operation === "unassigning"
1385
- ? "releasing"
1386
- : target?.connectionId
1387
- ? "claiming"
1388
- : "committing";
1389
- const acceptedReceipt: OperationReceiptV1 = {
1390
- schemaVersion: 1,
1391
- commandId: command.commandId,
1392
- revision: durable.revision,
1393
- status: "pending",
1394
- };
1395
- const saga: StoredAssignmentSaga = {
1396
- schemaVersion: 1,
1397
- commandId: command.commandId,
1398
- commandFingerprint,
1399
- userId: identity.userId,
1400
- botId: identity.botId,
1401
- operation,
1402
- assignmentId,
1403
- generation: command.commandId,
1404
- phase,
1405
- target,
1406
- targetRequirement,
1407
- previous: previous ? structuredClone(previous) : undefined,
1408
- previousGeneration,
1409
- // The Bot's model commits with the Assignment, in the saga's commit
1410
- // phase, so the dependency claim and the binding are one durable unit.
1411
- model: binding.model ? structuredClone(binding.model) : undefined,
1412
- clearModel: binding.clearModel,
1413
- deadlineAt: Date.now() + ASSIGNMENT_SAGA_DEADLINE_MS,
1414
- acceptedReceipt,
1415
- };
1416
- await transaction.put({
1417
- [`${ASSIGNMENT_SAGA_PREFIX}${command.commandId}`]: saga,
1418
- [BOT_CONFIGURATION_KEY]: {
1419
- ...durable,
1420
- assignmentOperations: [
1421
- {
1422
- commandId: command.commandId,
1423
- kind: operation,
1424
- assignmentId,
1425
- state: "pending",
1426
- target,
1427
- },
1428
- ],
1429
- } satisfies BotSettingsViewV1,
1430
- });
1431
- await this.refreshRecoveryAlarm(transaction);
1432
- });
1433
- try {
1434
- await this.reconcileStoredAssignmentSaga(
1435
- identity,
1436
- command.commandId,
1437
- commandFingerprint,
1438
- );
1439
- } catch {
1440
- const pending = requireStoredAssignmentSaga(
1441
- await this.ctx.storage.get<unknown>(
1442
- `${ASSIGNMENT_SAGA_PREFIX}${command.commandId}`,
1443
- ),
1444
- );
1445
- return pending.acceptedReceipt;
1446
- }
1447
- const completed =
1448
- await this.ctx.storage.get<StoredConfigurationReceipt>(receiptKey);
1449
- if (!completed) {
1450
- const pending = requireStoredAssignmentSaga(
1451
- await this.ctx.storage.get<unknown>(
1452
- `${ASSIGNMENT_SAGA_PREFIX}${command.commandId}`,
1453
- ),
1454
- );
1455
- return pending.acceptedReceipt;
1456
- }
1457
- return requireMatchingConfigurationReceipt(
1458
- completed,
1459
- commandFingerprint,
1460
- command.commandId,
1461
- );
1462
- }
1463
-
1464
- private async rejectAssignmentCommand(
1465
- identity: BotIdentity,
1466
- commandId: string,
1467
- commandFingerprint: string,
1468
- failure: string,
1469
- ): Promise<OperationReceiptV1> {
1470
- return this.ctx.storage.transaction(async (transaction) => {
1471
- await this.lifecycleAdmission?.(transaction, identity.botId);
1472
- const current =
1473
- (await transaction.get<BotSettingsViewV1>(BOT_CONFIGURATION_KEY)) ??
1474
- this.initialBotSettings(identity.botId);
1475
- const receipt: OperationReceiptV1 = {
1476
- schemaVersion: 1,
1477
- commandId,
1478
- revision: current.revision,
1479
- status: "rejected",
1480
- failure,
1481
- };
1482
- await transaction.put(`${CONFIGURATION_RECEIPT_PREFIX}${commandId}`, {
1483
- commandFingerprint,
1484
- receipt,
1485
- } satisfies StoredConfigurationReceipt);
1486
- return receipt;
1487
- });
1488
- }
1489
-
1490
- private async dependencyResult(
1491
- identity: BotIdentity,
1492
- saga: StoredAssignmentSaga,
1493
- action: "claim" | "read" | "acknowledge" | "release" | "reconcile",
1494
- target: "new" | "old",
1495
- ): Promise<import("@frockbot/connection-core").ConnectionDependencyResultV1> {
1496
- const connectionId =
1497
- target === "new"
1498
- ? saga.target?.connectionId
1499
- : saga.previous?.connectionId;
1500
- const generation =
1501
- target === "new" ? saga.generation : saga.previousGeneration;
1502
- if (!connectionId) {
1503
- return { schemaVersion: 1, status: "released" };
1504
- }
1505
- if (!generation) {
1506
- return {
1507
- schemaVersion: 1,
1508
- status: "unavailable",
1509
- failure: `Assignment "${saga.assignmentId}" dependency generation is unavailable`,
1510
- };
1511
- }
1512
- const packageId =
1513
- target === "new" ? saga.target?.packageId : saga.previous?.packageId;
1514
- if (!packageId) {
1515
- return {
1516
- schemaVersion: 1,
1517
- status: "unavailable",
1518
- failure: `Assignment "${saga.assignmentId}" Package identity is unavailable`,
1519
- };
1520
- }
1521
- const base = {
1522
- schemaVersion: 1 as const,
1523
- operationId: `${saga.commandId}:${target}`,
1524
- userId: identity.userId,
1525
- packageId,
1526
- connectionId,
1527
- botId: identity.botId,
1528
- generation,
1529
- };
1530
- return this.userConfiguration(identity).executeConnectionDependency(
1531
- action === "claim"
1532
- ? { ...base, action, requirement: saga.targetRequirement! }
1533
- : { ...base, action },
1534
- );
1535
- }
1536
-
1537
- private async rejectPendingAssignmentSaga(
1538
- saga: StoredAssignmentSaga,
1539
- failure: string,
1540
- ): Promise<void> {
1541
- await this.ctx.storage.transaction(async (transaction) => {
1542
- const settings = await transaction.get<BotSettingsViewV1>(
1543
- BOT_CONFIGURATION_KEY,
1544
- );
1545
- if (!settings) throw new Error("Bot settings are unavailable");
1546
- const receipt: OperationReceiptV1 = {
1547
- schemaVersion: 1,
1548
- commandId: saga.commandId,
1549
- revision: settings.revision,
1550
- status: "rejected",
1551
- failure,
1552
- };
1553
- await transaction.put({
1554
- [BOT_CONFIGURATION_KEY]: {
1555
- ...settings,
1556
- assignmentOperations: settings.assignmentOperations.filter(
1557
- (operation) => operation.commandId !== saga.commandId,
1558
- ),
1559
- } satisfies BotSettingsViewV1,
1560
- [`${CONFIGURATION_RECEIPT_PREFIX}${saga.commandId}`]: {
1561
- commandFingerprint: saga.commandFingerprint,
1562
- receipt,
1563
- } satisfies StoredConfigurationReceipt,
1564
- });
1565
- await transaction.delete(`${ASSIGNMENT_SAGA_PREFIX}${saga.commandId}`);
1566
- await this.refreshRecoveryAlarm(transaction);
1567
- });
1568
- }
1569
-
1570
- private async persistSaga(
1571
- saga: StoredAssignmentSaga,
1572
- patch: Partial<StoredAssignmentSaga>,
1573
- retrying = false,
1574
- ): Promise<void> {
1575
- await this.ctx.storage.transaction(async (transaction) => {
1576
- const key = `${ASSIGNMENT_SAGA_PREFIX}${saga.commandId}`;
1577
- const current = await transaction.get<StoredAssignmentSaga>(key);
1578
- if (!current || current.generation !== saga.generation) return;
1579
- await transaction.put(key, {
1580
- ...current,
1581
- ...patch,
1582
- deadlineAt: Date.now() + ASSIGNMENT_SAGA_DEADLINE_MS,
1583
- } satisfies StoredAssignmentSaga);
1584
- if (retrying) {
1585
- const settings = await transaction.get<BotSettingsViewV1>(
1586
- BOT_CONFIGURATION_KEY,
1587
- );
1588
- if (settings) {
1589
- await transaction.put(BOT_CONFIGURATION_KEY, {
1590
- ...settings,
1591
- assignmentOperations: settings.assignmentOperations.map(
1592
- (operation) =>
1593
- operation.commandId === saga.commandId
1594
- ? { ...operation, state: "retrying" as const }
1595
- : operation,
1596
- ),
1597
- } satisfies BotSettingsViewV1);
1598
- }
1599
- }
1600
- await this.refreshRecoveryAlarm(transaction);
1601
- });
1602
- }
1603
-
1604
- private async commitAssignmentSaga(
1605
- saga: StoredAssignmentSaga,
1606
- ): Promise<OperationReceiptV1> {
1607
- return this.ctx.storage.transaction(async (transaction) => {
1608
- const current = (await transaction.get<BotSettingsViewV1>(
1609
- BOT_CONFIGURATION_KEY,
1610
- ))!;
1611
- const existing = await transaction.get<StoredConfigurationReceipt>(
1612
- `${CONFIGURATION_RECEIPT_PREFIX}${saga.commandId}`,
1613
- );
1614
- if (existing) return existing.receipt;
1615
- const revision = current.revision + 1;
1616
- const assignments =
1617
- saga.operation === "unassigning"
1618
- ? current.assignments.filter(
1619
- (assignment) => assignment.assignmentId !== saga.assignmentId,
1620
- )
1621
- : [
1622
- ...current.assignments.filter(
1623
- (assignment) => assignment.assignmentId !== saga.assignmentId,
1624
- ),
1625
- { ...saga.target!, state: "enabled" as const },
1626
- ];
1627
- const receipt: OperationReceiptV1 = {
1628
- schemaVersion: 1,
1629
- commandId: saga.commandId,
1630
- revision,
1631
- status: "applied",
1632
- };
1633
- await transaction.put({
1634
- [BOT_CONFIGURATION_KEY]: {
1635
- ...current,
1636
- revision,
1637
- assignments,
1638
- ...(saga.clearModel
1639
- ? { model: undefined }
1640
- : saga.model
1641
- ? { model: structuredClone(saga.model) }
1642
- : {}),
1643
- } satisfies BotSettingsViewV1,
1644
- [`${CONFIGURATION_RECEIPT_PREFIX}${saga.commandId}`]: {
1645
- commandFingerprint: saga.commandFingerprint,
1646
- receipt,
1647
- } satisfies StoredConfigurationReceipt,
1648
- });
1649
- if (saga.target?.connectionId) {
1650
- await transaction.put(
1651
- `${ASSIGNMENT_GENERATION_PREFIX}${saga.assignmentId}`,
1652
- saga.generation,
1653
- );
1654
- } else if (saga.operation === "unassigning") {
1655
- await transaction.delete(
1656
- `${ASSIGNMENT_GENERATION_PREFIX}${saga.assignmentId}`,
1657
- );
1658
- }
1659
- return receipt;
1660
- });
1661
- }
1662
-
1663
- private async markSagaAssignmentUnavailable(
1664
- saga: StoredAssignmentSaga,
1665
- ): Promise<void> {
1666
- await this.ctx.storage.transaction(async (transaction) => {
1667
- const settings = await transaction.get<BotSettingsViewV1>(
1668
- BOT_CONFIGURATION_KEY,
1669
- );
1670
- if (!settings) return;
1671
- await transaction.put(BOT_CONFIGURATION_KEY, {
1672
- ...settings,
1673
- assignments: settings.assignments.map((assignment) =>
1674
- assignment.assignmentId === saga.assignmentId
1675
- ? { ...assignment, state: "unavailable" as const }
1676
- : assignment,
1677
- ),
1678
- } satisfies BotSettingsViewV1);
1679
- });
1680
- }
1681
-
1682
- private async finishAssignmentSaga(
1683
- saga: StoredAssignmentSaga,
1684
- ): Promise<void> {
1685
- await this.ctx.storage.transaction(async (transaction) => {
1686
- const key = `${ASSIGNMENT_SAGA_PREFIX}${saga.commandId}`;
1687
- const current = await transaction.get<StoredAssignmentSaga>(key);
1688
- if (current?.generation !== saga.generation) return;
1689
- const settings = await transaction.get<BotSettingsViewV1>(
1690
- BOT_CONFIGURATION_KEY,
1691
- );
1692
- if (settings) {
1693
- await transaction.put(BOT_CONFIGURATION_KEY, {
1694
- ...settings,
1695
- assignmentOperations: settings.assignmentOperations.filter(
1696
- (operation) => operation.commandId !== saga.commandId,
1697
- ),
1698
- } satisfies BotSettingsViewV1);
1699
- }
1700
- await transaction.delete(key);
1701
- await this.refreshRecoveryAlarm(transaction);
1702
- });
1703
- }
1704
-
1705
- private async reconcileStoredAssignmentSaga(
1706
- identity: BotIdentity,
1707
- commandId: string,
1708
- commandFingerprint?: string,
1709
- ): Promise<void> {
1710
- const key = `${ASSIGNMENT_SAGA_PREFIX}${commandId}`;
1711
- const storedSaga = await this.ctx.storage.get<unknown>(key);
1712
- let saga =
1713
- storedSaga === undefined
1714
- ? undefined
1715
- : requireStoredAssignmentSaga(storedSaga);
1716
- if (!saga) return;
1717
- if (saga.userId !== identity.userId || saga.botId !== identity.botId) {
1718
- throw new Error("Assignment saga does not match its durable identity");
1719
- }
1720
- if (
1721
- commandFingerprint !== undefined &&
1722
- saga.commandFingerprint !== commandFingerprint
1723
- ) {
1724
- throw new Error(
1725
- `Configuration command idempotency key "${commandId}" was reused for a different command`,
1726
- );
1727
- }
1728
- try {
1729
- for (let step = 0; step < 8 && saga; step += 1) {
1730
- if (saga.phase === "claiming") {
1731
- if (!saga.claimDispatched) {
1732
- await this.persistSaga(saga, { claimDispatched: true });
1733
- saga = { ...saga, claimDispatched: true };
1734
- }
1735
- let result = await this.dependencyResult(
1736
- identity,
1737
- saga,
1738
- saga.claimDispatched ? "read" : "claim",
1739
- "new",
1740
- );
1741
- if (result.status === "absent") {
1742
- result = await this.dependencyResult(
1743
- identity,
1744
- saga,
1745
- "claim",
1746
- "new",
1747
- );
1748
- }
1749
- if (result.status === "pending" || result.status === "unavailable") {
1750
- if (result.status === "pending") {
1751
- await this.dependencyResult(identity, saga, "reconcile", "new");
1752
- }
1753
- await this.persistSaga(saga, {}, true);
1754
- return;
1755
- }
1756
- if (result.status !== "claimed" && result.status !== "acknowledged") {
1757
- await this.rejectPendingAssignmentSaga(
1758
- saga,
1759
- ("failure" in result ? result.failure : undefined) ??
1760
- "Connection dependency claim rejected",
1761
- );
1762
- return;
1763
- }
1764
- await this.persistSaga(saga, { phase: "committing" });
1765
- saga = { ...saga, phase: "committing" };
1766
- continue;
1767
- }
1768
- if (saga.phase === "committing") {
1769
- const receipt = await this.commitAssignmentSaga(saga);
1770
- const phase: StoredAssignmentSaga["phase"] | undefined = saga.target
1771
- ?.connectionId
1772
- ? "acknowledging"
1773
- : saga.previous?.connectionId
1774
- ? "releasing"
1775
- : undefined;
1776
- if (!phase) {
1777
- await this.finishAssignmentSaga({ ...saga, receipt });
1778
- return;
1779
- }
1780
- await this.persistSaga(saga, { phase, receipt });
1781
- saga = { ...saga, phase, receipt };
1782
- continue;
1783
- }
1784
- if (saga.phase === "acknowledging") {
1785
- if (!saga.acknowledgeDispatched) {
1786
- await this.persistSaga(saga, { acknowledgeDispatched: true });
1787
- saga = { ...saga, acknowledgeDispatched: true };
1788
- }
1789
- let result = await this.dependencyResult(
1790
- identity,
1791
- saga,
1792
- "read",
1793
- "new",
1794
- );
1795
- if (result.status === "claimed") {
1796
- result = await this.dependencyResult(
1797
- identity,
1798
- saga,
1799
- "acknowledge",
1800
- "new",
1801
- );
1802
- }
1803
- if (result.status === "pending" || result.status === "unavailable") {
1804
- if (result.status === "pending") {
1805
- await this.dependencyResult(identity, saga, "reconcile", "new");
1806
- }
1807
- await this.persistSaga(saga, {}, true);
1808
- return;
1809
- }
1810
- if (result.status !== "acknowledged") {
1811
- if (result.status === "rejected" || result.status === "absent") {
1812
- await this.markSagaAssignmentUnavailable(saga);
1813
- if (!saga.previous?.connectionId) {
1814
- await this.finishAssignmentSaga(saga);
1815
- return;
1816
- }
1817
- await this.persistSaga(saga, { phase: "releasing" });
1818
- saga = { ...saga, phase: "releasing" };
1819
- continue;
1820
- }
1821
- throw new Error(
1822
- ("failure" in result ? result.failure : undefined) ??
1823
- "Connection dependency acknowledgement rejected",
1824
- );
1825
- }
1826
- if (!saga.previous?.connectionId) {
1827
- await this.finishAssignmentSaga(saga);
1828
- return;
1829
- }
1830
- await this.persistSaga(saga, { phase: "releasing" });
1831
- saga = { ...saga, phase: "releasing" };
1832
- continue;
1833
- }
1834
- if (!saga.previous?.connectionId) {
1835
- if (saga.operation === "unassigning")
1836
- await this.commitAssignmentSaga(saga);
1837
- await this.finishAssignmentSaga(saga);
1838
- return;
1839
- }
1840
- if (!saga.releaseDispatched) {
1841
- await this.persistSaga(saga, { releaseDispatched: true });
1842
- saga = { ...saga, releaseDispatched: true };
1843
- }
1844
- let result = await this.dependencyResult(identity, saga, "read", "old");
1845
- if (result.status === "claimed" || result.status === "acknowledged") {
1846
- result = await this.dependencyResult(
1847
- identity,
1848
- saga,
1849
- "release",
1850
- "old",
1851
- );
1852
- }
1853
- if (result.status === "pending" || result.status === "unavailable") {
1854
- if (result.status === "pending") {
1855
- await this.dependencyResult(identity, saga, "reconcile", "old");
1856
- }
1857
- await this.persistSaga(saga, {}, true);
1858
- return;
1859
- }
1860
- if (result.status !== "released" && result.status !== "absent") {
1861
- throw new Error(
1862
- ("failure" in result ? result.failure : undefined) ??
1863
- "Connection dependency release rejected",
1864
- );
1865
- }
1866
- if (saga.operation === "unassigning")
1867
- await this.commitAssignmentSaga(saga);
1868
- await this.finishAssignmentSaga(saga);
1869
- return;
1870
- }
1871
- } catch (error) {
1872
- await this.persistSaga(saga, {}, true);
1873
- throw error;
1874
- }
1875
- }
1876
-
1877
- private async refreshRecoveryAlarm(
1878
- transaction: DurableObjectTransaction,
1879
- ): Promise<void> {
1880
- await this.authority.refreshRecoveryAlarm(transaction);
1881
- }
1882
- async markConnectionUnavailable(
1883
- identity: BotIdentity,
1884
- connectionId: string,
1885
- compensation: { id: string; expectedGeneration: string },
1886
- ): Promise<"applied" | "stale"> {
1887
- await this.ensureBotSettings(identity);
1888
- return this.ctx.storage.transaction(async (transaction) => {
1889
- const receiptKey = `${ASSIGNMENT_COMPENSATION_PREFIX}${compensation.id}`;
1890
- const existing = await transaction.get<"applied" | "stale">(receiptKey);
1891
- if (existing) return existing;
1892
- const current =
1893
- (await transaction.get<BotSettingsViewV1>(BOT_CONFIGURATION_KEY)) ??
1894
- this.initialBotSettings(identity.botId);
1895
- // Every enabled Assignment on this Connection whose durable generation
1896
- // is the compensated one becomes unavailable. A compensation that names
1897
- // a generation no live Assignment holds is stale, not a silent no-op.
1898
- const matching: string[] = [];
1899
- for (const assignment of current.assignments) {
1900
- if (
1901
- assignment.connectionId !== connectionId ||
1902
- assignment.state !== "enabled"
1903
- ) {
1904
- continue;
1905
- }
1906
- if (
1907
- (await transaction.get<string>(
1908
- assignmentGenerationKey(assignment.assignmentId),
1909
- )) === compensation.expectedGeneration
1910
- ) {
1911
- matching.push(assignment.assignmentId);
1912
- }
1913
- }
1914
- await transaction.put(
1915
- `${ASSIGNMENT_TOMBSTONE_PREFIX}${connectionId}:${compensation.expectedGeneration}`,
1916
- compensation.id,
1917
- );
1918
- if (matching.length === 0) {
1919
- await transaction.put(receiptKey, "stale");
1920
- return "stale";
1921
- }
1922
- const unavailable = new Set(matching);
1923
- await transaction.put(BOT_CONFIGURATION_KEY, {
1924
- ...current,
1925
- revision: current.revision + 1,
1926
- assignments: current.assignments.map((assignment) =>
1927
- unavailable.has(assignment.assignmentId)
1928
- ? { ...assignment, state: "unavailable" as const }
1929
- : assignment,
1930
- ),
1931
- } satisfies BotSettingsViewV1);
1932
- await this.refreshRecoveryAlarm(transaction);
1933
- await transaction.put(receiptKey, "applied");
1934
- return "applied";
927
+ if (expired.length > 0) await transaction.delete(expired);
928
+ }
929
+
930
+ /** The announcements the Session shows, oldest first. */
931
+ async listAnnouncements(): Promise<SessionEvent[]> {
932
+ const stored = await this.ctx.storage.list<unknown>({
933
+ prefix: BOT_ANNOUNCEMENT_PREFIX,
1935
934
  });
935
+ return [...stored.entries()]
936
+ .sort(([left], [right]) => left.localeCompare(right))
937
+ .map(([, value]) => decodeSessionEvent(value));
938
+ }
939
+
940
+ private async refreshRecoveryAlarm(
941
+ transaction: DurableObjectTransaction,
942
+ ): Promise<void> {
943
+ await this.authority.refreshRecoveryAlarm(transaction);
1936
944
  }
1937
945
 
1938
946
  async resolveConfiguration(
@@ -2225,7 +1233,7 @@ export class ShellBotBackendContribution {
2225
1233
  runId: input.command.runId,
2226
1234
  sessionId: input.command.sessionId,
2227
1235
  generationId: mounting.generationId,
2228
- assignments: settings.assignments,
1236
+ capabilities: runtime.capabilities,
2229
1237
  });
2230
1238
  return createShellCompositionHost({
2231
1239
  botId: input.identity.botId,
@@ -2386,7 +1394,7 @@ export class ShellBotBackendContribution {
2386
1394
  runId: string;
2387
1395
  sessionId: string;
2388
1396
  generationId: string;
2389
- assignments: readonly CapabilityAssignmentView[];
1397
+ capabilities: readonly EnabledCapabilityV1[];
2390
1398
  },
2391
1399
  ): Promise<ShellIsolateMountOptions | undefined> {
2392
1400
  const loader = this.env.BOT_PACKAGES;
@@ -2402,7 +1410,9 @@ export class ShellBotBackendContribution {
2402
1410
  ).exports;
2403
1411
  if (!loader || !artifacts || !exports?.BotCapabilities) return undefined;
2404
1412
  const mintCapabilities = exports.BotCapabilities;
2405
- const assignments = await this.isolateAssignments(turn.assignments);
1413
+ const capabilities = turn.capabilities.map((capability) =>
1414
+ structuredClone(capability),
1415
+ );
2406
1416
  return {
2407
1417
  userId: identity.userId,
2408
1418
  runId: turn.runId,
@@ -2417,13 +1427,15 @@ export class ShellBotBackendContribution {
2417
1427
  botId: identity.botId,
2418
1428
  generationId: turn.generationId,
2419
1429
  packageId: member.packageId,
2420
- assignments: structuredClone(assignments),
1430
+ capabilities: structuredClone(capabilities),
2421
1431
  },
2422
1432
  }),
2423
- bindingDigest: await isolateBindingDigestV1(
2424
- assignments,
2425
- turn.generationId,
2426
- ),
1433
+ bindingDigest: await isolateBindingDigestV1({
1434
+ userId: identity.userId,
1435
+ botId: identity.botId,
1436
+ generationId: turn.generationId,
1437
+ capabilities,
1438
+ }),
2427
1439
  compatibilityDate: BOT_ISOLATE_COMPATIBILITY_DATE,
2428
1440
  };
2429
1441
  }
@@ -2438,14 +1450,14 @@ export class ShellBotBackendContribution {
2438
1450
  generationId: string;
2439
1451
  request: unknown;
2440
1452
  }): Promise<IsolatePendingDecisionV1> {
2441
- return await this.isolateCapabilities(input, []).requestAuthority(
1453
+ return await this.isolateCapabilityHost(input, []).requestAuthority(
2442
1454
  input.request,
2443
1455
  );
2444
1456
  }
2445
1457
 
2446
1458
  /**
2447
1459
  * The Bot Durable Object side of `CAPABILITIES.invokeModel`. Refuses with a
2448
- * pending decision unless an enabled model Assignment matches; otherwise
1460
+ * pending decision unless an enabled model Capability matches; otherwise
2449
1461
  * records the normalized request and streams through the provider path,
2450
1462
  * which takes the credential lease on the way.
2451
1463
  */
@@ -2456,48 +1468,38 @@ export class ShellBotBackendContribution {
2456
1468
  generationId: string;
2457
1469
  request: NormalizedModelRequest;
2458
1470
  },
2459
- ): Promise<IsolateModelInvocationV1> {
2460
- // The Bot Durable Object's own durable configuration is what decides
2461
- // whether the Assignment exists at all, and its durable model binding is
2462
- // what the Assignment authorizes. Nothing the Bot supplied is read.
2463
- const stored = await this.ctx.storage.get<BotSettingsViewV1>(
1471
+ ): Promise<IsolateModelOutcomeV1> {
1472
+ // The effective binding and enabled set both come from authority-owned
1473
+ // User and Bot state. Nothing the Bot supplied is read.
1474
+ const durableSettings = await this.ctx.storage.get<BotSettingsViewV1>(
2464
1475
  BOT_CONFIGURATION_KEY,
2465
1476
  );
2466
- // An isolate model request is authorized exactly as an admitted Turn is,
2467
- // so it resolves the same execution context: a Bot that follows the
2468
- // User's default model claims its own durable Assignment here rather than
2469
- // failing closed on an authority it is entitled to hold.
2470
- let settings = stored;
2471
- if (stored) {
2472
- try {
2473
- settings = (await this.resolveExecutionContext(identity)).settings;
2474
- } catch {
2475
- settings = stored;
2476
- }
1477
+ if (!durableSettings) {
1478
+ return await this.isolateCapabilityHost(
1479
+ {
1480
+ botId: identity.botId,
1481
+ packageId: input.packageId,
1482
+ generationId: input.generationId,
1483
+ },
1484
+ [],
1485
+ ).invokeModel(input.request);
2477
1486
  }
2478
- const projected = await this.isolateAssignments(
2479
- settings?.assignments ?? [],
1487
+ const context = await this.resolveExecutionContext(identity);
1488
+ const capabilities = structuredClone(context.plan.capabilities);
1489
+ const bound = await this.isolateModelBinding(
1490
+ identity,
1491
+ context.settings,
1492
+ context.user,
1493
+ capabilities,
2480
1494
  );
2481
- const bound = await this.isolateModelBinding(identity, settings, projected);
2482
- const assignments = bound
2483
- ? projected.map((assignment) =>
2484
- assignment.assignmentId === bound.binding.assignmentId
2485
- ? {
2486
- ...assignment,
2487
- connectionId: bound.binding.connectionId,
2488
- providerModelId: bound.binding.providerModelId,
2489
- }
2490
- : assignment,
2491
- )
2492
- : projected;
2493
- const host = this.isolateCapabilities(
1495
+ const host = this.isolateCapabilityHost(
2494
1496
  {
2495
1497
  botId: identity.botId,
2496
1498
  packageId: input.packageId,
2497
1499
  generationId: input.generationId,
2498
1500
  },
2499
- assignments,
2500
- bound
1501
+ capabilities,
1502
+ bound?.state === "ready"
2501
1503
  ? {
2502
1504
  binding: bound.binding,
2503
1505
  path: this.isolateModelPath(
@@ -2507,33 +1509,85 @@ export class ShellBotBackendContribution {
2507
1509
  ),
2508
1510
  }
2509
1511
  : undefined,
1512
+ bound?.state === "unavailable" ? bound.binding : undefined,
2510
1513
  );
2511
1514
  return await host.invokeModel(input.request);
2512
1515
  }
2513
1516
 
2514
1517
  /**
2515
- * The Bot's one durable model binding, projected onto the isolate view. It
2516
- * resolves the Bot's durable `model` through the User's Connection exactly
2517
- * as an admitted Turn does, so an isolate model request is authorized
2518
- * against the same Package, Connection, and provider model a Turn would use.
2519
- * An unresolvable binding is no binding: the request becomes a pending
2520
- * decision rather than an error thrown into Bot code.
1518
+ * The effective model binding, projected onto the isolate view. It resolves
1519
+ * generic Package settings and the platform fallback through the User's
1520
+ * Connection exactly as an admitted Turn does. No configured binding means
1521
+ * an authority-widening request and a pending decision; a configured binding
1522
+ * whose Connection cannot be resolved stays distinct as unavailable.
2521
1523
  */
2522
1524
  private async isolateModelBinding(
2523
1525
  identity: BotIdentity,
2524
1526
  settings: BotSettingsViewV1 | undefined,
2525
- assignments: readonly IsolateAssignmentV1[],
1527
+ user: UserSettingsViewV1,
1528
+ capabilities: readonly IsolateCapabilityV1[],
2526
1529
  ): Promise<
2527
1530
  | {
1531
+ state: "ready";
2528
1532
  binding: IsolateModelBindingV1;
2529
1533
  runtime: {
2530
1534
  agentPackages: FoundationAgentPackage[];
2531
1535
  modelSelection: RuntimeModelSelection;
2532
1536
  };
2533
1537
  }
1538
+ | {
1539
+ state: "unavailable";
1540
+ binding: IsolateUnavailableModelBindingV1;
1541
+ }
2534
1542
  | undefined
2535
1543
  > {
2536
1544
  if (!settings) return undefined;
1545
+ const application = await this.compileApplication();
1546
+ const packageDefinitions = application.packages.map((pkg) => ({
1547
+ packageId: pkg.id,
1548
+ version: pkg.version,
1549
+ settings: pkg.manifest.configuration?.settings ?? [],
1550
+ capabilities: pkg.manifest.configuration?.capabilities ?? [],
1551
+ connectionTypes: pkg.manifest.configuration?.connectionTypes ?? [],
1552
+ }));
1553
+ const effective = resolveEffectiveBotModelV1({
1554
+ bot: settings,
1555
+ user,
1556
+ packages: packageDefinitions,
1557
+ });
1558
+ const effectiveModel = effective.model;
1559
+ if (!effectiveModel) return undefined;
1560
+ const configuredConnection = user.connections.find(
1561
+ (connection) => connection.connectionId === effectiveModel.connectionId,
1562
+ );
1563
+ const unavailable = (): {
1564
+ state: "unavailable";
1565
+ binding: IsolateUnavailableModelBindingV1;
1566
+ } => ({
1567
+ state: "unavailable",
1568
+ binding: {
1569
+ ...(effective.binding?.providerType
1570
+ ? { provider: effective.binding.providerType }
1571
+ : configuredConnection?.providerType
1572
+ ? { provider: configuredConnection.providerType }
1573
+ : {}),
1574
+ providerModelId: effectiveModel.providerModelId,
1575
+ },
1576
+ });
1577
+ if (!effective.binding || effective.binding.state === "unavailable") {
1578
+ return unavailable();
1579
+ }
1580
+ const connection = effective.binding.connection;
1581
+ const provider = effective.binding.providerType;
1582
+ const packageId = effective.binding.packageId;
1583
+ if (!connection || !provider || !packageId) return unavailable();
1584
+ const capability = capabilities.find(
1585
+ (candidate) =>
1586
+ candidate.kind === "model" &&
1587
+ candidate.packageId === packageId &&
1588
+ candidate.connectionId === connection.connectionId,
1589
+ );
1590
+ if (!capability) return undefined;
2537
1591
  let runtime: {
2538
1592
  agentPackages: FoundationAgentPackage[];
2539
1593
  modelSelection: RuntimeModelSelection;
@@ -2541,22 +1595,17 @@ export class ShellBotBackendContribution {
2541
1595
  try {
2542
1596
  runtime = await this.agentRuntime(identity, settings);
2543
1597
  } catch {
2544
- return undefined;
1598
+ return unavailable();
2545
1599
  }
2546
1600
  const selection = runtime.modelSelection;
2547
1601
  const connectionId = selection.connectionId;
2548
- if (!connectionId) return undefined;
2549
- const assignment = assignments.find(
2550
- (candidate) =>
2551
- candidate.kind === "model" && candidate.connectionId === connectionId,
2552
- );
2553
- if (!assignment) return undefined;
1602
+ if (!connectionId) return unavailable();
2554
1603
  return {
1604
+ state: "ready",
2555
1605
  runtime,
2556
1606
  binding: {
2557
- assignmentId: assignment.assignmentId,
2558
- packageId: assignment.packageId,
2559
- capabilityId: assignment.capabilityId,
1607
+ packageId: capability.packageId,
1608
+ capabilityId: capability.capabilityId,
2560
1609
  connectionId,
2561
1610
  provider: selection.provider,
2562
1611
  providerModelId: selection.model,
@@ -2570,15 +1619,16 @@ export class ShellBotBackendContribution {
2570
1619
  };
2571
1620
  }
2572
1621
 
2573
- private isolateCapabilities(
1622
+ private isolateCapabilityHost(
2574
1623
  scope: {
2575
1624
  botId: string;
2576
1625
  packageId: string;
2577
1626
  generationId: string;
2578
1627
  request?: unknown;
2579
1628
  },
2580
- assignments: readonly IsolateAssignmentV1[],
1629
+ capabilities: readonly IsolateCapabilityV1[],
2581
1630
  model?: { binding: IsolateModelBindingV1; path: IsolateModelPath },
1631
+ unavailableModelBinding?: IsolateUnavailableModelBindingV1,
2582
1632
  ): IsolateCapabilityHost {
2583
1633
  return createIsolateCapabilityHost({
2584
1634
  storage: {
@@ -2589,44 +1639,18 @@ export class ShellBotBackendContribution {
2589
1639
  botId: scope.botId,
2590
1640
  packageId: scope.packageId,
2591
1641
  generationId: scope.generationId,
2592
- assignments,
1642
+ capabilities,
2593
1643
  ...(model ? { modelBinding: model.binding, modelPath: model.path } : {}),
1644
+ ...(unavailableModelBinding ? { unavailableModelBinding } : {}),
2594
1645
  });
2595
1646
  }
2596
1647
 
2597
- /** The Bot's enabled Assignments, projected onto the isolate capability DTO. */
2598
- private async isolateAssignments(
2599
- assignments: readonly CapabilityAssignmentView[],
2600
- ): Promise<IsolateAssignmentV1[]> {
2601
- const application = await this.compileApplication();
2602
- const projected: IsolateAssignmentV1[] = [];
2603
- for (const assignment of assignments) {
2604
- if (assignment.state !== "enabled") continue;
2605
- const capability = application.packages
2606
- .find((candidate) => candidate.id === assignment.packageId)
2607
- ?.manifest.configuration?.capabilities.find(
2608
- (candidate) => candidate.id === assignment.capabilityId,
2609
- );
2610
- if (!capability) continue;
2611
- projected.push({
2612
- assignmentId: assignment.assignmentId,
2613
- packageId: assignment.packageId,
2614
- capabilityId: assignment.capabilityId,
2615
- kind: capability.kind,
2616
- ...(assignment.connectionId
2617
- ? { connectionId: assignment.connectionId }
2618
- : {}),
2619
- });
2620
- }
2621
- return projected;
2622
- }
2623
-
2624
1648
  /**
2625
1649
  * Streams through the pinned Composition's mounted `ctx.llm` — the same
2626
1650
  * provider path a Turn uses, so whichever provider Plugin serves the request
2627
1651
  * is the one that takes the credential lease. The runtime is the one the
2628
- * durable model binding resolved to, so the Package that streams is the
2629
- * Package the Assignment names.
1652
+ * effective model binding resolved to, so the Package that streams is the
1653
+ * Package the enabled Capability names.
2630
1654
  */
2631
1655
  private isolateModelPath(
2632
1656
  identity: BotIdentity,
@@ -2653,7 +1677,7 @@ export class ShellBotBackendContribution {
2653
1677
  agentPackages: runtime.agentPackages,
2654
1678
  modelSelection: runtime.modelSelection,
2655
1679
  // An isolate model invocation is not an admitted Turn, so there is
2656
- // no run to fence it against; it is admitted by its Assignment.
1680
+ // no run to fence it against; User enablement admitted it.
2657
1681
  admitEffect: () => Promise.resolve(true),
2658
1682
  }).mount(generation, signal);
2659
1683
  try {
@@ -2676,11 +1700,7 @@ export class ShellBotBackendContribution {
2676
1700
  private async resolveAdmissionSnapshot(
2677
1701
  command: OwnedBotTurnCommand,
2678
1702
  ): Promise<BotSettingsViewV1> {
2679
- const context = await this.resolveExecutionContext(command);
2680
- return {
2681
- ...context.settings,
2682
- assignments: context.plan.assignments,
2683
- } satisfies BotSettingsViewV1;
1703
+ return (await this.resolveExecutionContext(command)).settings;
2684
1704
  }
2685
1705
 
2686
1706
  private async admittedSnapshot(
@@ -2696,9 +1716,6 @@ export class ShellBotBackendContribution {
2696
1716
  private async scheduledDeadlines(
2697
1717
  transaction: DurableObjectTransaction,
2698
1718
  ): Promise<number[]> {
2699
- const sagas = await transaction.list<unknown>({
2700
- prefix: ASSIGNMENT_SAGA_PREFIX,
2701
- });
2702
1719
  // A pending approval is a deadline like any other: the object already owns
2703
1720
  // one alarm, and expiry rides it rather than inventing a second clock.
2704
1721
  const approvals = await transaction.list<unknown>({
@@ -2711,9 +1728,6 @@ export class ShellBotBackendContribution {
2711
1728
  expiries.push(Date.parse(approval.expiresAt));
2712
1729
  }
2713
1730
  return [
2714
- ...[...sagas.values()].map(
2715
- (stored) => requireStoredAssignmentSaga(stored).deadlineAt,
2716
- ),
2717
1731
  ...(await this.routineScheduler.deadlines(transaction)),
2718
1732
  ...expiries.filter((at) => Number.isFinite(at)),
2719
1733
  // A dispatched task's 30-minute lifetime, and a child's own owed Turn,
@@ -2761,38 +1775,12 @@ export class ShellBotBackendContribution {
2761
1775
  private async deferScheduledWork(
2762
1776
  transaction: DurableObjectTransaction,
2763
1777
  ): Promise<void> {
2764
- const sagas = await transaction.list<StoredAssignmentSaga>({
2765
- prefix: ASSIGNMENT_SAGA_PREFIX,
2766
- });
2767
- for (const [key, saga] of sagas) {
2768
- await transaction.put(key, {
2769
- ...saga,
2770
- deadlineAt: Date.now() + ASSIGNMENT_SAGA_DEADLINE_MS,
2771
- } satisfies StoredAssignmentSaga);
2772
- }
2773
- // A saga's deadline is a retry, so pushing it forward loses nothing. A
2774
- // Routine's is a debt, so the scheduler holds it instead of moving it.
1778
+ // A Routine's deadline is a debt, so the scheduler holds it rather than
1779
+ // moving it while other durable work remains in flight.
2775
1780
  await this.routineScheduler.defer(transaction);
2776
1781
  }
2777
1782
 
2778
1783
  private async settleScheduledWork(): Promise<void> {
2779
- const stored = await this.ctx.storage.list<unknown>({
2780
- prefix: ASSIGNMENT_SAGA_PREFIX,
2781
- });
2782
- for (const value of stored.values()) {
2783
- const saga = requireStoredAssignmentSaga(value);
2784
- try {
2785
- await this.reconcileStoredAssignmentSaga(
2786
- { userId: saga.userId, botId: saga.botId },
2787
- saga.commandId,
2788
- );
2789
- } catch (error) {
2790
- console.error(
2791
- "Assignment saga remains durably scheduled after reconciliation failure",
2792
- error instanceof Error ? error.message : "unknown failure",
2793
- );
2794
- }
2795
- }
2796
1784
  await this.settleRoutineFirings();
2797
1785
  await this.runOwedSubagentTurns();
2798
1786
  await this.reconcileOverdueTasks();
@@ -2867,7 +1855,7 @@ export class ShellBotBackendContribution {
2867
1855
  *
2868
1856
  * The Bot Durable Object cannot serialize across a User's Bots — they are
2869
1857
  * separate objects — and the User Durable Object owns the Computer
2870
- * assignment but not the desktop. The host's `control` op is already the
1858
+ * allocation but not the desktop. The host's `control` op is already the
2871
1859
  * single writer that serializes human takeover, so it is where a second
2872
1860
  * opinion cannot exist (plan decision 3): the Bot records the intent, the
2873
1861
  * host grants or refuses, and the refusal names the holder.
@@ -3885,6 +2873,7 @@ export class ShellBotBackendContribution {
3885
2873
  },
3886
2874
  ): Promise<{
3887
2875
  agentPackages: FoundationAgentPackage[];
2876
+ capabilities: EnabledCapabilityV1[];
3888
2877
  modelSelection: RuntimeModelSelection;
3889
2878
  }> {
3890
2879
  const userConfiguration = this.userConfiguration(identity);
@@ -3896,32 +2885,42 @@ export class ShellBotBackendContribution {
3896
2885
  const packageDefinitions = application.packages.map((pkg) => ({
3897
2886
  packageId: pkg.id,
3898
2887
  version: pkg.version,
2888
+ settings: pkg.manifest.configuration?.settings ?? [],
3899
2889
  capabilities: pkg.manifest.configuration?.capabilities ?? [],
3900
2890
  connectionTypes: pkg.manifest.configuration?.connectionTypes ?? [],
3901
2891
  }));
3902
- const plan = admittedRequest
3903
- ? {
3904
- schemaVersion: 1 as const,
3905
- botId: settings.botId,
3906
- revision: settings.revision,
3907
- model: settings.model ? structuredClone(settings.model) : undefined,
3908
- assignments: structuredClone(settings.assignments),
3909
- }
3910
- : resolveBotExecutionPlanV1({
3911
- bot: settings,
3912
- user,
3913
- packages: packageDefinitions,
3914
- });
2892
+ const plan = resolveBotExecutionPlanV1({
2893
+ bot: settings,
2894
+ user,
2895
+ packages: packageDefinitions,
2896
+ });
3915
2897
  const readSecret = (name: string) => {
3916
2898
  // SAFETY: Worker secrets are dynamic string bindings not enumerable in Env.
3917
2899
  const value = (this.env as unknown as Record<string, unknown>)[name];
3918
2900
  return typeof value === "string" ? value : undefined;
3919
2901
  };
3920
- const authorizeAssignedConnection = admittedRequest
3921
- ? (assignment: BotSettingsViewV1["assignments"][number]) =>
3922
- this.authorizeAdmittedAssignedEffect(identity, assignment)
3923
- : (assignment: BotSettingsViewV1["assignments"][number]) =>
3924
- this.authorizeAssignedEffect(identity, assignment);
2902
+ const authorizeEnabledConnection = (
2903
+ capability: EnabledCapabilityV1,
2904
+ ): Promise<ConnectionView> => {
2905
+ const enabled = plan.capabilities.some(
2906
+ (candidate) =>
2907
+ candidate.packageId === capability.packageId &&
2908
+ candidate.capabilityId === capability.capabilityId &&
2909
+ candidate.connectionId === capability.connectionId,
2910
+ );
2911
+ const connection = user.connections.find(
2912
+ (candidate) =>
2913
+ candidate.connectionId === capability.connectionId &&
2914
+ candidate.packageId === capability.packageId &&
2915
+ candidate.state === "ready",
2916
+ );
2917
+ if (!enabled || !connection) {
2918
+ return Promise.reject(
2919
+ new Error("Enabled effect is no longer authorized"),
2920
+ );
2921
+ }
2922
+ return Promise.resolve(structuredClone(connection));
2923
+ };
3925
2924
  // The Package-level settings this User holds, resolved against the manifest
3926
2925
  // of the Composition this Turn is pinned to. They come from the same `user`
3927
2926
  // read the rest of this Composition uses, so a value the User changed is
@@ -3929,7 +2928,7 @@ export class ShellBotBackendContribution {
3929
2928
  // one already running.
3930
2929
  const packageSettings = (
3931
2930
  packageId: string,
3932
- ): Record<string, string | number | boolean> => {
2931
+ ): Record<string, PackageSettingValueV1> => {
3933
2932
  const installation = user.packages.find(
3934
2933
  (candidate) => candidate.packageId === packageId,
3935
2934
  );
@@ -3943,6 +2942,15 @@ export class ShellBotBackendContribution {
3943
2942
  installation?.values,
3944
2943
  );
3945
2944
  };
2945
+ const primitivePackageSettings = (
2946
+ packageId: string,
2947
+ ): Record<string, string | number | boolean> =>
2948
+ Object.fromEntries(
2949
+ Object.entries(packageSettings(packageId)).filter(
2950
+ (entry): entry is [string, string | number | boolean] =>
2951
+ typeof entry[1] !== "object",
2952
+ ),
2953
+ );
3946
2954
  // The `image.model` Package setting, already checked against the enum the
3947
2955
  // Image Package's manifest declares.
3948
2956
  const configuredImageModel = packageSettings("image").model;
@@ -3953,7 +2961,7 @@ export class ShellBotBackendContribution {
3953
2961
  const machineSeam = turn ? this.machineSeam(identity) : undefined;
3954
2962
  const messagesGate = machineSeam
3955
2963
  ? await resolveBotMachineMessagesGateV1(
3956
- packageSettings("machine-messages"),
2964
+ primitivePackageSettings("machine-messages"),
3957
2965
  () => machineSeam.list(),
3958
2966
  )
3959
2967
  : ({ status: "off" } as const);
@@ -4168,55 +3176,53 @@ export class ShellBotBackendContribution {
4168
3176
  ),
4169
3177
  },
4170
3178
  }),
4171
- ...(await createFoundationAssignedRuntimePackages(
4172
- application,
4173
- settings,
4174
- plan,
4175
- {
4176
- userId: identity.userId,
4177
- readSecret,
4178
- authorizeConnection: authorizeAssignedConnection,
4179
- packageSettings,
4180
- // Assigned Contributions reach the network through the same
4181
- // outbound seam the model provider uses, so a deployment that stubs
4182
- // it stubs every one of them.
4183
- ...(this.outboundFetch ? { fetch: this.outboundFetch } : {}),
4184
- leaseCredential: async (
4185
- assignment,
3179
+ ...(await createFoundationEnabledRuntimePackages(application, plan, {
3180
+ userId: identity.userId,
3181
+ readSecret,
3182
+ authorizeConnection: authorizeEnabledConnection,
3183
+ packageSettings,
3184
+ // Enabled Contributions reach the network through the same
3185
+ // outbound seam the model provider uses, so a deployment that stubs
3186
+ // it stubs every one of them.
3187
+ ...(this.outboundFetch ? { fetch: this.outboundFetch } : {}),
3188
+ leaseCredential: async (
3189
+ capability: EnabledCapabilityV1,
3190
+ effectId: string,
3191
+ expectedGeneration?: string,
3192
+ ): Promise<CredentialLeaseV1> => {
3193
+ if (!capability.connectionId || !expectedGeneration) {
3194
+ throw new Error("Enabled Connection generation is unavailable");
3195
+ }
3196
+ return userConfiguration.leaseToolCredential(
3197
+ identity.userId,
3198
+ capability.connectionId,
4186
3199
  effectId,
4187
3200
  expectedGeneration,
4188
- ): Promise<CredentialLeaseV1> => {
4189
- if (!assignment.connectionId || !expectedGeneration) {
4190
- throw new Error("Assigned Connection generation is unavailable");
4191
- }
4192
- return userConfiguration.leaseToolCredential(
4193
- identity.userId,
4194
- assignment.connectionId,
4195
- effectId,
4196
- expectedGeneration,
4197
- );
4198
- },
4199
- settleCredential: async (assignment, effectId): Promise<void> => {
4200
- if (!assignment.connectionId) return;
4201
- await userConfiguration.settleToolCredential(
4202
- identity.userId,
4203
- assignment.connectionId,
4204
- effectId,
4205
- );
4206
- },
4207
- // A mount that could not reach its server writes that down where
4208
- // the User can read it. The Bot holds no MCP record; the User
4209
- // Durable Object that owns the Connection does.
4210
- recordOutcome: (outcome) =>
4211
- userConfiguration.recordMcpMountOutcome(identity.userId, outcome),
3201
+ );
4212
3202
  },
4213
- )),
3203
+ settleCredential: async (
3204
+ capability: EnabledCapabilityV1,
3205
+ effectId: string,
3206
+ ): Promise<void> => {
3207
+ if (!capability.connectionId) return;
3208
+ await userConfiguration.settleToolCredential(
3209
+ identity.userId,
3210
+ capability.connectionId,
3211
+ effectId,
3212
+ );
3213
+ },
3214
+ // A mount that could not reach its server writes that down where
3215
+ // the User can read it. The Bot holds no MCP record; the User
3216
+ // Durable Object that owns the Connection does.
3217
+ recordOutcome: (outcome: McpMountOutcomeReportV1) =>
3218
+ userConfiguration.recordMcpMountOutcome(identity.userId, outcome),
3219
+ })),
4214
3220
  ];
4215
3221
  const agentPackages: FoundationAgentPackage[] =
4216
3222
  mergeFoundationRuntimePackages(resolvedAgentPackages);
4217
- // A Bot without its own `model` follows the User's default model. The
4218
- // Bot's own Assignment still carries the authority (ADR 0003); the default
4219
- // only names which model that Assignment's Connection should run.
3223
+ // One generic resolver owns precedence: enabled Bot-scoped Package value,
3224
+ // enabled User-scoped Package value, then the platform model. The kernel
3225
+ // names no Package (AGENTS.md Configuration shape; ADR 0019).
4220
3226
  const effective = resolveEffectiveBotModelV1({
4221
3227
  bot: settings,
4222
3228
  user,
@@ -4224,64 +3230,37 @@ export class ShellBotBackendContribution {
4224
3230
  });
4225
3231
  const effectiveModel = effective.model;
4226
3232
  if (!effectiveModel) {
4227
- throw new Error("Bot model Connection is not configured");
4228
- }
4229
-
4230
- let binding: ResolvedModelBindingV1;
4231
- if (admittedRequest) {
4232
- const admittedBinding = admittedRequest.modelBinding;
4233
- const assignment = settings.assignments.find(
4234
- (candidate) =>
4235
- candidate.connectionId === admittedBinding?.connectionId &&
4236
- candidate.state === "enabled",
4237
- );
4238
- const pkg = application.packages.find(
4239
- (candidate) => candidate.id === assignment?.packageId,
4240
- );
4241
- const capability = pkg?.manifest.configuration?.capabilities.find(
4242
- (candidate) => candidate.id === assignment?.capabilityId,
3233
+ throw new Error(
3234
+ effective.binding?.failure ??
3235
+ "No model is configured; enable a model Package or restore the platform model",
4243
3236
  );
4244
- const connectionTypeId = capability?.connectionTypes[0];
4245
- if (
4246
- !admittedBinding?.connectionGeneration ||
4247
- !assignment?.connectionId ||
4248
- !pkg ||
4249
- !connectionTypeId ||
4250
- effectiveModel.connectionId !== admittedBinding.connectionId ||
4251
- effectiveModel.providerModelId !== admittedRequest.model
4252
- ) {
4253
- throw new Error("Admitted model binding is unavailable");
4254
- }
4255
- binding = {
4256
- state: "ready",
4257
- assignment: structuredClone(effectiveModel),
4258
- packageId: pkg.id,
4259
- providerType: admittedRequest.provider,
4260
- connection: {
4261
- connectionId: admittedBinding.connectionId,
4262
- packageId: pkg.id,
4263
- connectionTypeId,
4264
- displayName: "Admitted model Connection",
4265
- state: "ready",
4266
- providerType: admittedRequest.provider,
4267
- generation: admittedBinding.connectionGeneration,
4268
- safeMetadata: {},
4269
- },
4270
- };
4271
- } else {
4272
- binding = effective.binding ?? {
4273
- assignment: structuredClone(effectiveModel),
4274
- state: "unavailable",
4275
- failure: "Bot model Connection is unavailable",
4276
- };
4277
3237
  }
3238
+ const binding: ResolvedModelBindingV1 = effective.binding ?? {
3239
+ model: structuredClone(effectiveModel),
3240
+ state: "unavailable",
3241
+ failure: "The resolved model Connection is unavailable",
3242
+ };
4278
3243
  if (
4279
3244
  binding.state === "unavailable" ||
4280
3245
  !binding.connection ||
4281
3246
  !binding.providerType ||
4282
3247
  !binding.packageId
4283
3248
  ) {
4284
- throw new Error(binding.failure ?? "Bot model Connection is unavailable");
3249
+ throw new Error(
3250
+ binding.failure ?? "The resolved model Connection is unavailable",
3251
+ );
3252
+ }
3253
+ if (
3254
+ admittedRequest &&
3255
+ (admittedRequest.provider !== binding.providerType ||
3256
+ admittedRequest.model !== effectiveModel.providerModelId ||
3257
+ admittedRequest.modelBinding?.connectionId !==
3258
+ binding.connection.connectionId ||
3259
+ !admittedRequest.modelBinding.connectionGeneration ||
3260
+ admittedRequest.modelBinding.connectionGeneration !==
3261
+ binding.connection.generation)
3262
+ ) {
3263
+ throw new Error("Admitted model binding is unavailable");
4285
3264
  }
4286
3265
  const bindingPackageId = binding.packageId;
4287
3266
  agentPackages.push(
@@ -4312,27 +3291,28 @@ export class ShellBotBackendContribution {
4312
3291
  bindingPackageId,
4313
3292
  effectId,
4314
3293
  ),
4315
- ...(this.env.AI
3294
+ ...(this.env.FLOCK_AI
4316
3295
  ? {
4317
- runWorkersAi: (model, input) => this.env.AI!.run(model, input),
3296
+ flockAiAutoRoute: this.env.FLOCK_AI.autoRoute,
3297
+ runFlockAiChatCompletion: (gatewayModel, body) =>
3298
+ this.env.FLOCK_AI!.runChatCompletion(gatewayModel, body),
4318
3299
  }
4319
3300
  : {}),
4320
3301
  fetch: this.outboundFetch,
4321
3302
  }),
4322
3303
  );
4323
3304
  // The slugs `<available_subagent_models>` renders, and the only ones a
4324
- // `Task` call may name. They are the Bot's own enabled model Assignment as
4325
- // resolved for this Turn — never anything the Bot claimed about a model.
4326
- const modelAssignment = settings.assignments.find(
3305
+ // `Task` call may name. They come from User enablement as resolved for this
3306
+ // Turn — never anything the Bot claimed about a model.
3307
+ const modelCapability = plan.capabilities.find(
4327
3308
  (candidate) =>
4328
- candidate.state === "enabled" &&
3309
+ candidate.kind === "model" &&
4329
3310
  candidate.connectionId === binding.connection!.connectionId,
4330
3311
  );
4331
- if (modelAssignment) {
3312
+ if (modelCapability) {
4332
3313
  const subagentBinding = {
4333
- assignmentId: modelAssignment.assignmentId,
4334
- packageId: modelAssignment.packageId,
4335
- capabilityId: modelAssignment.capabilityId,
3314
+ packageId: modelCapability.packageId,
3315
+ capabilityId: modelCapability.capabilityId,
4336
3316
  connectionId: binding.connection.connectionId,
4337
3317
  provider: binding.providerType,
4338
3318
  providerModelId: effectiveModel.providerModelId,
@@ -4342,7 +3322,7 @@ export class ShellBotBackendContribution {
4342
3322
  };
4343
3323
  subagentModels.push(
4344
3324
  ...subagentModelCatalogV1({
4345
- assignments: [subagentBinding],
3325
+ bindings: [subagentBinding],
4346
3326
  defaultBinding: subagentBinding,
4347
3327
  turnType: turn?.turnType ?? "chat",
4348
3328
  }),
@@ -4353,6 +3333,7 @@ export class ShellBotBackendContribution {
4353
3333
  // Cloud is both the model provider and the `web_search` Capability — and
4354
3334
  // the runtime resolves one Plugin per Contribution specifier.
4355
3335
  agentPackages: mergeFoundationRuntimePackagesV1(agentPackages),
3336
+ capabilities: structuredClone(plan.capabilities),
4356
3337
  modelSelection: {
4357
3338
  provider: binding.providerType,
4358
3339
  model: effectiveModel.providerModelId,
@@ -4372,77 +3353,6 @@ export class ShellBotBackendContribution {
4372
3353
  };
4373
3354
  }
4374
3355
 
4375
- private async authorizeAdmittedAssignedEffect(
4376
- identity: BotIdentity,
4377
- assignment: BotSettingsViewV1["assignments"][number],
4378
- ): Promise<ConnectionView> {
4379
- const user = await this.userConfiguration(identity).readConfiguration({
4380
- schemaVersion: 1,
4381
- userId: identity.userId,
4382
- });
4383
- const application = await this.compileApplication();
4384
- const connection = user.connections.find(
4385
- (candidate) =>
4386
- candidate.connectionId === assignment.connectionId &&
4387
- candidate.packageId === assignment.packageId &&
4388
- candidate.state === "ready",
4389
- );
4390
- const pkg = application.packages.find(
4391
- (candidate) => candidate.id === assignment.packageId,
4392
- );
4393
- const capability = pkg?.manifest.configuration?.capabilities.find(
4394
- (candidate) =>
4395
- candidate.id === assignment.capabilityId &&
4396
- connection !== undefined &&
4397
- candidate.connectionTypes.includes(connection.connectionTypeId),
4398
- );
4399
- if (assignment.state !== "enabled" || !connection || !capability) {
4400
- throw new Error("Admitted assigned effect is unavailable");
4401
- }
4402
- return structuredClone(connection);
4403
- }
4404
-
4405
- private async authorizeAssignedEffect(
4406
- identity: BotIdentity,
4407
- admittedAssignment: BotSettingsViewV1["assignments"][number],
4408
- ): Promise<ConnectionView> {
4409
- const user = await this.userConfiguration(identity).readConfiguration({
4410
- schemaVersion: 1,
4411
- userId: identity.userId,
4412
- });
4413
- const application = await this.compileApplication();
4414
- const admittedBot = {
4415
- ...this.initialBotSettings(identity.botId),
4416
- assignments: [admittedAssignment],
4417
- } satisfies BotSettingsViewV1;
4418
- const plan = resolveBotExecutionPlanV1({
4419
- bot: admittedBot,
4420
- user,
4421
- packages: application.packages.map((pkg) => ({
4422
- packageId: pkg.id,
4423
- version: pkg.version,
4424
- capabilities: pkg.manifest.configuration?.capabilities ?? [],
4425
- connectionTypes: pkg.manifest.configuration?.connectionTypes ?? [],
4426
- })),
4427
- });
4428
- const assignment = plan.assignments.find(
4429
- (candidate) =>
4430
- candidate.assignmentId === admittedAssignment.assignmentId &&
4431
- candidate.packageId === admittedAssignment.packageId &&
4432
- candidate.capabilityId === admittedAssignment.capabilityId &&
4433
- candidate.connectionId === admittedAssignment.connectionId &&
4434
- candidate.state === "enabled",
4435
- );
4436
- const connection = user.connections.find(
4437
- (candidate) =>
4438
- candidate.connectionId === assignment?.connectionId &&
4439
- candidate.packageId === admittedAssignment.packageId &&
4440
- candidate.state === "ready",
4441
- );
4442
- if (!connection) throw new Error("Assigned effect is no longer authorized");
4443
- return connection;
4444
- }
4445
-
4446
3356
  async readDurableIdentity(): Promise<BotIdentity | undefined> {
4447
3357
  return this.authority.readDurableIdentity();
4448
3358
  }
@@ -5127,8 +4037,8 @@ export class ShellBotBackendContribution {
5127
4037
  }
5128
4038
 
5129
4039
  async alarm(): Promise<void> {
5130
- // One alarm: the kernel defers while work is in flight, settles the
5131
- // Package's Assignment sagas, and recovers the active run. A run left
4040
+ // One alarm: the kernel defers while work is in flight, settles Package
4041
+ // scheduled work, and recovers the active run. A run left
5132
4042
  // durably `reconciliation-required` stays scheduled and visible; only an
5133
4043
  // explicit resume retrieves the original effect, so the alarm never
5134
4044
  // terminalizes an uncertain outcome on its own.
@@ -5426,11 +4336,8 @@ export class ShellBotBackendContribution {
5426
4336
  await this.authority.fenceRunAdmission(identity, query.runId),
5427
4337
  );
5428
4338
  }
5429
- private initialBotSettings(
5430
- botId: string,
5431
- model?: BotSettingsViewV1["model"],
5432
- ): BotSettingsViewV1 {
5433
- return initializeBotSettingsV1(botId, model);
4339
+ private initialBotSettings(botId: string): BotSettingsViewV1 {
4340
+ return initializeBotSettingsV1(botId);
5434
4341
  }
5435
4342
 
5436
4343
  private userConfiguration(identity: BotIdentity): {
@@ -5449,40 +4356,6 @@ export class ShellBotBackendContribution {
5449
4356
  userId: string,
5450
4357
  command: Parameters<PackagePublisherAgentHost["rollback"]>[0],
5451
4358
  ): ReturnType<PackagePublisherAgentHost["rollback"]>;
5452
- getConnection(
5453
- userId: string,
5454
- connectionId: string,
5455
- ): Promise<ConnectionView | undefined>;
5456
- executeConnectionDependency(
5457
- input: import("@frockbot/connection-core").ConnectionDependencyCommandV1,
5458
- ): Promise<
5459
- import("@frockbot/connection-core").ConnectionDependencyResultV1
5460
- >;
5461
- claimConnectionDependency(
5462
- userId: string,
5463
- connectionId: string,
5464
- botId: string,
5465
- generation: string,
5466
- requirement: ConnectionDependencyRequirementV1,
5467
- ): Promise<boolean>;
5468
- acknowledgeConnectionDependency(
5469
- userId: string,
5470
- connectionId: string,
5471
- botId: string,
5472
- generation: string,
5473
- ): Promise<boolean>;
5474
- releaseConnectionDependency(
5475
- userId: string,
5476
- connectionId: string,
5477
- botId: string,
5478
- generation: string,
5479
- ): Promise<boolean>;
5480
- compensateConnectionDependency(
5481
- userId: string,
5482
- connectionId: string,
5483
- botId: string,
5484
- generation: string,
5485
- ): Promise<boolean>;
5486
4359
  leaseModelCredential(
5487
4360
  userId: string,
5488
4361
  connectionId: string,
@@ -5552,16 +4425,6 @@ export class ShellBotBackendContribution {
5552
4425
  rollbackPackage(
5553
4426
  input: unknown,
5554
4427
  ): ReturnType<PackagePublisherAgentHost["rollback"]>;
5555
- getConnection(input: unknown): Promise<ConnectionView | undefined>;
5556
- executeConnectionDependency(
5557
- input: unknown,
5558
- ): Promise<
5559
- import("@frockbot/connection-core").ConnectionDependencyResultV1
5560
- >;
5561
- claimConnectionDependency(input: unknown): Promise<boolean>;
5562
- acknowledgeConnectionDependency(input: unknown): Promise<boolean>;
5563
- releaseConnectionDependency(input: unknown): Promise<boolean>;
5564
- compensateConnectionDependency(input: unknown): Promise<boolean>;
5565
4428
  leaseModelCredential(input: unknown): Promise<unknown>;
5566
4429
  settleModelCredential(input: unknown): Promise<void>;
5567
4430
  leaseToolCredential(input: unknown): Promise<unknown>;
@@ -5617,8 +4480,6 @@ export class ShellBotBackendContribution {
5617
4480
  rpc.publishPackage({ schemaVersion: 1, userId, command }),
5618
4481
  rollbackPackage: (userId, command) =>
5619
4482
  rpc.rollbackPackage({ schemaVersion: 1, userId, command }),
5620
- getConnection: (userId, connectionId) =>
5621
- rpc.getConnection({ schemaVersion: 1, userId, connectionId }),
5622
4483
  // Flock state crosses a Durable Object seam, so it decodes on arrival
5623
4484
  // rather than being trusted in the shape RPC happened to return.
5624
4485
  listBots: async (userId) =>
@@ -5635,57 +4496,6 @@ export class ShellBotBackendContribution {
5635
4496
  command,
5636
4497
  }),
5637
4498
  ),
5638
- executeConnectionDependency: (input) =>
5639
- rpc.executeConnectionDependency(input),
5640
- claimConnectionDependency: (
5641
- userId,
5642
- connectionId,
5643
- botId,
5644
- generation,
5645
- requirement,
5646
- ) =>
5647
- rpc.claimConnectionDependency({
5648
- schemaVersion: 1,
5649
- userId,
5650
- connectionId,
5651
- botId,
5652
- generation,
5653
- requirement,
5654
- }),
5655
- acknowledgeConnectionDependency: (
5656
- userId,
5657
- connectionId,
5658
- botId,
5659
- generation,
5660
- ) =>
5661
- rpc.acknowledgeConnectionDependency({
5662
- schemaVersion: 1,
5663
- userId,
5664
- connectionId,
5665
- botId,
5666
- generation,
5667
- }),
5668
- releaseConnectionDependency: (userId, connectionId, botId, generation) =>
5669
- rpc.releaseConnectionDependency({
5670
- schemaVersion: 1,
5671
- userId,
5672
- connectionId,
5673
- botId,
5674
- generation,
5675
- }),
5676
- compensateConnectionDependency: (
5677
- userId,
5678
- connectionId,
5679
- botId,
5680
- generation,
5681
- ) =>
5682
- rpc.compensateConnectionDependency({
5683
- schemaVersion: 1,
5684
- userId,
5685
- connectionId,
5686
- botId,
5687
- generation,
5688
- }),
5689
4499
  leaseModelCredential: async (
5690
4500
  userId,
5691
4501
  connectionId,
@@ -5760,169 +4570,35 @@ export class ShellBotBackendContribution {
5760
4570
  return existing;
5761
4571
  }
5762
4572
 
5763
- /**
5764
- * A Bot that follows the User's default model still needs its own durable
5765
- * Assignment: authority reaches a Bot only through an explicit Assignment
5766
- * and, when required, a Connection (ADR 0003). The Assignment is claimed
5767
- * lazily the first time the Bot resolves its execution context under a
5768
- * default it has not yet claimed, exactly as Flock claims one when a Bot is
5769
- * created, so the User Connection's dependency ledger stays accurate and
5770
- * revocation still fails closed.
5771
- */
5772
- private async claimDefaultModelAssignment(
5773
- identity: BotIdentity,
5774
- settings: BotSettingsViewV1,
5775
- user: UserSettingsViewV1,
5776
- application: Awaited<ReturnType<typeof compileFoundationApplication>>,
5777
- ): Promise<BotSettingsViewV1> {
5778
- const model = user.newBotModelTemplate;
5779
- if (settings.model || !model) return settings;
5780
- // One Assignment operation at a time: a claim still reconciling owns the
5781
- // Bot's Assignment authority until it settles.
5782
- if (settings.assignmentOperations.length > 0) return settings;
5783
- const connection = user.connections.find(
5784
- (candidate) => candidate.connectionId === model.connectionId,
5785
- );
5786
- const installation = user.packages.find(
5787
- (candidate) =>
5788
- candidate.packageId === connection?.packageId &&
5789
- candidate.state === "installed",
5790
- );
5791
- const pkg = application.packages.find(
5792
- (candidate) =>
5793
- candidate.id === connection?.packageId &&
5794
- candidate.version === installation?.version,
5795
- );
5796
- const connectionType = pkg?.manifest.configuration?.connectionTypes.find(
5797
- (candidate) => candidate.id === connection?.connectionTypeId,
5798
- );
5799
- const capability = pkg?.manifest.configuration?.capabilities.find(
5800
- (candidate) =>
5801
- candidate.kind === "model" &&
5802
- connectionType?.capabilities.includes(candidate.id) &&
5803
- candidate.connectionTypes.includes(connectionType.id),
5804
- );
5805
- if (connection?.state !== "ready" || !pkg || !capability) return settings;
5806
- if (
5807
- settings.assignments.some(
5808
- (assignment) =>
5809
- assignment.packageId === pkg.id &&
5810
- assignment.capabilityId === capability.id &&
5811
- assignment.connectionId === connection.connectionId,
5812
- )
5813
- ) {
5814
- return settings;
5815
- }
5816
- const commandId = crypto.randomUUID();
5817
- try {
5818
- const receipt = await this.executeConfigurationCommand(identity, {
5819
- schemaVersion: 1,
5820
- type: "bot/assign-capability",
5821
- commandId,
5822
- expectedRevision: settings.revision,
5823
- botId: identity.botId,
5824
- assignment: {
5825
- assignmentId: commandId,
5826
- packageId: pkg.id,
5827
- capabilityId: capability.id,
5828
- connectionId: connection.connectionId,
5829
- },
5830
- });
5831
- if (receipt.status !== "applied") return settings;
5832
- } catch (error) {
5833
- // The default model is not the Bot's own binding: a claim that cannot be
5834
- // made leaves the Bot without a model, visibly, rather than failing the
5835
- // caller that only wanted to read the plan.
5836
- console.error(
5837
- "Default model Assignment claim failed",
5838
- error instanceof Error ? error.message : "unknown failure",
5839
- );
5840
- return settings;
5841
- }
5842
- return this.ensureBotSettings(identity);
5843
- }
5844
-
5845
4573
  private async resolveExecutionContext(identity: BotIdentity): Promise<{
5846
4574
  settings: BotSettingsViewV1;
5847
4575
  user: UserSettingsViewV1;
5848
4576
  plan: BotExecutionPlanV1;
5849
4577
  }> {
5850
- let settings = await this.ensureBotSettings(identity);
4578
+ const settings = await this.ensureBotSettings(identity);
5851
4579
  const user = await this.userConfiguration(identity).readConfiguration({
5852
4580
  schemaVersion: 1,
5853
4581
  userId: identity.userId,
5854
4582
  });
5855
4583
  const application = await this.compileApplication();
5856
- let plan = resolveBotExecutionPlanV1({
4584
+ const plan = resolveBotExecutionPlanV1({
5857
4585
  bot: settings,
5858
4586
  user,
5859
4587
  packages: application.packages.map((pkg) => ({
5860
4588
  packageId: pkg.id,
5861
4589
  version: pkg.version,
4590
+ settings: pkg.manifest.configuration?.settings ?? [],
5862
4591
  capabilities: pkg.manifest.configuration?.capabilities ?? [],
5863
4592
  connectionTypes: pkg.manifest.configuration?.connectionTypes ?? [],
5864
4593
  })),
5865
4594
  });
5866
- settings = await this.claimDefaultModelAssignment(
5867
- identity,
5868
- settings,
5869
- user,
5870
- application,
5871
- );
5872
- if (settings.revision !== plan.revision) {
5873
- plan = resolveBotExecutionPlanV1({
5874
- bot: settings,
5875
- user,
5876
- packages: application.packages.map((pkg) => ({
5877
- packageId: pkg.id,
5878
- version: pkg.version,
5879
- capabilities: pkg.manifest.configuration?.capabilities ?? [],
5880
- connectionTypes: pkg.manifest.configuration?.connectionTypes ?? [],
5881
- })),
5882
- });
5883
- }
5884
- const changed = settings.assignments.some(
5885
- (assignment, index) =>
5886
- assignment.state !== plan.assignments[index]?.state,
5887
- );
5888
- if (changed) {
5889
- settings = await this.ctx.storage.transaction(async (transaction) => {
5890
- const current =
5891
- (await transaction.get<BotSettingsViewV1>(BOT_CONFIGURATION_KEY)) ??
5892
- settings;
5893
- if (current.revision !== settings.revision) return current;
5894
- const next = {
5895
- ...current,
5896
- revision: current.revision + 1,
5897
- assignments: plan.assignments,
5898
- } satisfies BotSettingsViewV1;
5899
- await transaction.put(BOT_CONFIGURATION_KEY, next);
5900
- await this.refreshRecoveryAlarm(transaction);
5901
- return next;
5902
- });
5903
- plan = {
5904
- ...plan,
5905
- revision: settings.revision,
5906
- assignments: settings.assignments,
5907
- };
5908
- }
5909
4595
  return { settings, user, plan };
5910
4596
  }
5911
4597
 
5912
4598
  async archiveEligible(storage: {
5913
4599
  get<T>(key: string): Promise<T | undefined>;
5914
- list<T>(options: { prefix: string }): Promise<Map<string, T>>;
5915
4600
  }): Promise<boolean> {
5916
- const [activeRunId, settings, sagas] = await Promise.all([
5917
- storage.get<string>(ACTIVE_RUN_KEY),
5918
- storage.get<BotSettingsViewV1>(BOT_CONFIGURATION_KEY),
5919
- storage.list<unknown>({ prefix: ASSIGNMENT_SAGA_PREFIX }),
5920
- ]);
5921
- return (
5922
- activeRunId === undefined &&
5923
- (settings?.assignmentOperations.length ?? 0) === 0 &&
5924
- sagas.size === 0
5925
- );
4601
+ return (await storage.get<string>(ACTIVE_RUN_KEY)) === undefined;
5926
4602
  }
5927
4603
 
5928
4604
  async assertLifecycleActive(botId: string): Promise<void> {