@frockbot/plugin-shell 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/frockbot.json +4 -0
  2. package/package.json +30 -28
  3. package/src/agent.ts +10 -13
  4. package/src/backend-authoring.test.ts +356 -2
  5. package/src/backend-authoring.ts +585 -20
  6. package/src/backend-composition-input.test.ts +65 -0
  7. package/src/backend-composition-input.ts +58 -0
  8. package/src/backend-composition.ts +23 -5
  9. package/src/backend-configuration.test.ts +549 -1468
  10. package/src/backend-contracts.test.ts +28 -0
  11. package/src/backend-contracts.ts +3 -1
  12. package/src/backend-execution.ts +0 -4
  13. package/src/backend-iframe-ui.test.ts +131 -0
  14. package/src/backend-image.test.ts +8 -8
  15. package/src/backend-image.ts +13 -13
  16. package/src/backend-isolate.test.ts +230 -89
  17. package/src/backend-isolate.ts +103 -200
  18. package/src/backend-package-catalog.test.ts +451 -0
  19. package/src/backend-package-catalog.ts +923 -0
  20. package/src/backend-recovery-integration.test.ts +17 -67
  21. package/src/backend-routines.ts +1 -1
  22. package/src/backend-runner-iframe.test.ts +74 -0
  23. package/src/backend-runner.ts +192 -0
  24. package/src/backend.ts +1198 -1781
  25. package/src/client/FrockBotApp.vue +44 -73
  26. package/src/client/PackageIframeHost.vue +218 -0
  27. package/src/client/PackageIframeSettings.vue +52 -0
  28. package/src/client/SendPayloadView.vue +0 -60
  29. package/src/client/index.test.ts +439 -380
  30. package/src/client/index.ts +163 -257
  31. package/src/client/model-presentation.test.ts +21 -8
  32. package/src/client/model-presentation.ts +14 -7
  33. package/src/client/package-iframe-host-message.test.ts +41 -0
  34. package/src/client/package-iframe-host-message.ts +27 -0
  35. package/src/client/styles.css +0 -27
  36. package/src/composition-views.ts +54 -0
  37. package/src/settings-links.test.ts +2 -10
  38. package/src/settings-links.ts +1 -13
  39. package/src/shared.ts +10 -13
  40. package/src/backend-assignment.test.ts +0 -161
  41. package/src/backend-assignment.ts +0 -274
package/src/backend.ts CHANGED
@@ -1,17 +1,41 @@
1
1
  import type { AgentEffectAdmission } from "@frockbot/kernel-agent-loop/agent";
2
2
  import {
3
+ decodeIsolateMemoryReadRequestV1,
4
+ decodeIsolateMemoryWriteRequestV1,
5
+ decodeIsolateNotificationRequestV1,
6
+ decodeIsolateScheduleRequestV1,
7
+ decodeIsolateToolRequestV1,
8
+ decodeIsolateWorkspaceDeleteRequestV1,
9
+ decodeIsolateWorkspaceListRequestV1,
10
+ decodeIsolateWorkspacePathV1,
11
+ decodeIsolateWorkspaceWriteRequestV1,
12
+ decodeWorkspacePathV1,
13
+ decodeWorkspaceRootV1,
3
14
  decodeSessionEvent,
15
+ type IsolateConnectionOutcomeV1,
16
+ type IsolateConnectionV1,
17
+ type IsolateMemoryOutcomeV1,
18
+ type IsolateNotificationOutcomeV1,
19
+ type IsolateToolOutcomeV1,
20
+ type IsolateWorkspaceOutcomeV1,
4
21
  type PersistSessionEvents,
5
22
  type SessionEvent,
23
+ type WorkspacePathV1,
6
24
  validateToolOccurrenceJournal,
7
25
  type BotCapabilitiesStub,
8
26
  type IsolateModelInvocationV1,
9
- type IsolatePendingDecisionV1,
10
27
  type NormalizedModelRequest,
11
28
  type PackageBundlerBinding,
29
+ type PackageIframeCompositionV1,
30
+ type PackageIframeToolCommandV1,
12
31
  type TurnTypeV1,
13
32
  type WorkspaceFilesV1,
14
33
  } from "@frockbot/kernel-contracts";
34
+ import {
35
+ decodeFrockBotManifest,
36
+ isClientIframeContribution,
37
+ type FrockBotManifest,
38
+ } from "@frockbot/kernel-composition";
15
39
  import type { Plugin } from "cordis";
16
40
  import {
17
41
  ACTIVE_RUN_KEY,
@@ -42,45 +66,43 @@ import {
42
66
  } from "@frockbot/application-foundation/runtime";
43
67
  import {
44
68
  applyBotProfilePatchV1,
45
- capabilityAssignmentFailureV1,
46
69
  configurationCommandFingerprintV1,
47
70
  ConfigurationConflictError,
48
71
  decodeBotConfigurationExecuteRpcV1,
49
72
  decodeBotConfigurationReadRpcV1,
73
+ decodeBotSettingsViewV1,
50
74
  decodeCompositionCommandReceiptV1,
75
+ decodeOperationReceiptV1,
76
+ decodeInstalledPackageSettingIdsV1,
77
+ decodeInstalledPackageSettingsPatchV1,
51
78
  MAX_COMPOSITION_GENERATION_PAGE_V1,
52
79
  type CompositionCommandReceiptV1,
53
80
  type CompositionGenerationListViewV1,
54
81
  type CompositionGenerationViewV1,
55
82
  type RevertCompositionCommandV1,
56
- type ConnectionDependencyRequirementV1,
57
83
  type BotExecutionPlanV1,
58
84
  type BotSelfWriterV1,
59
85
  type BotSettingsViewV1,
60
- type CapabilityAssignmentView,
61
- type ModelAssignment,
86
+ type EnabledCapabilityV1,
62
87
  type ConnectionView,
63
88
  type ConfigurationCommandV1,
64
89
  type OperationReceiptV1,
90
+ type PackageSettingValueV1,
65
91
  type ResolvedModelBindingV1,
66
92
  initializeBotSettingsV1,
93
+ migrateStoredBotSettingsV1,
67
94
  resolvePackageSettingValuesV1,
68
95
  resolveBotExecutionPlanV1,
69
- resolveBotModelBindingV1,
70
96
  resolveEffectiveBotModelV1,
71
97
  type UserSettingsViewV1,
72
98
  } from "@frockbot/configuration-core";
73
99
  import {
74
- createFoundationAssignedRuntimePackages,
100
+ createFoundationEnabledRuntimePackages,
75
101
  mergeFoundationRuntimePackages,
76
102
  createFoundationHostedRuntimePackages,
77
103
  mergeFoundationRuntimePackagesV1,
78
104
  type PackagePublisherAgentHost,
79
105
  } from "@frockbot/application-foundation/runtime";
80
- import {
81
- requireStoredAssignmentSaga,
82
- type StoredAssignmentSaga,
83
- } from "./backend-assignment.js";
84
106
  import {
85
107
  cancelStoredRun,
86
108
  completeStoredRun,
@@ -100,6 +122,7 @@ import {
100
122
  type ShellIsolateMountOptions,
101
123
  type ShellMountedComposition,
102
124
  } from "./backend-composition.js";
125
+ import { compositionFailureTurnTextV1 } from "./backend-composition-input.js";
103
126
  import {
104
127
  activateCompositionV1,
105
128
  type CompositionFailureV1,
@@ -109,7 +132,13 @@ import {
109
132
  import {
110
133
  createPackageAuthoringHost,
111
134
  createR2AuthoringArtifactStore,
135
+ readAuthoredCompositionMemberSourceV1,
112
136
  } from "./backend-authoring.js";
137
+ import {
138
+ type CatalogAwarePackageCatalogHost,
139
+ createPackageCatalogHost,
140
+ createR2BotPackageCatalogReader,
141
+ } from "./backend-package-catalog.js";
113
142
  import { createBotComputerSyncHost } from "./backend-computer.js";
114
143
  import {
115
144
  decodeDirectoryViewV1,
@@ -125,10 +154,7 @@ import {
125
154
  type TemplateShareReceiptV1,
126
155
  } from "@frockbot/plugin-bot-template/shared";
127
156
  import { createBotMemoryHost } from "./backend-memory.js";
128
- import {
129
- createBotImageHost,
130
- type WorkersAiBindingV1,
131
- } from "./backend-image.js";
157
+ import { createBotImageHost, type NativeAiBindingV1 } from "./backend-image.js";
132
158
  import {
133
159
  createBotPluginSkillsSource,
134
160
  createBotSkillCatalogReader,
@@ -288,7 +314,9 @@ import type {
288
314
  RoutineRunListViewV1,
289
315
  } from "@frockbot/plugin-routines/shared";
290
316
  import {
317
+ authorshipManifestKey,
291
318
  decodeAuthoringQuotaReceiptV1,
319
+ type AuthoredManifestRecordV1,
292
320
  type AuthoringQuotaBinding,
293
321
  type PackageAuthoringHost,
294
322
  } from "@frockbot/plugin-authoring";
@@ -298,13 +326,12 @@ import {
298
326
  createR2PackageArtifactStore,
299
327
  isolateBindingDigestV1,
300
328
  type BotCapabilitiesPropsV1,
301
- type IsolateAssignmentV1,
302
329
  type IsolateCapabilityHost,
303
330
  type IsolateModelBindingV1,
304
331
  type IsolateModelPath,
305
332
  type IsolateModelRequestRecordV1,
306
- type IsolatePendingAuthorityDecisionV1,
307
333
  } from "./backend-isolate.js";
334
+ import { memoryScopeRootV1 } from "@frockbot/plugin-memory/roots";
308
335
  import type { BotIsolateLoader } from "@frockbot/kernel-composition/isolate";
309
336
  import type {
310
337
  CompositionGenerationV1,
@@ -317,8 +344,11 @@ import {
317
344
  type McpMountOutcomeReportV1,
318
345
  type McpServerStatusViewV1,
319
346
  } from "@frockbot/plugin-mcp/records";
320
- import { projectCompositionGenerationV1 } from "./composition-views.js";
321
- import { executeBotTurn } from "./backend-runner.js";
347
+ import {
348
+ projectCompositionGenerationV1,
349
+ projectPackageIframeCompositionV1,
350
+ } from "./composition-views.js";
351
+ import { executeBotTurn, executeDirectToolTurn } from "./backend-runner.js";
322
352
  import { shellTerminalRecordsV1 } from "./terminal-records.js";
323
353
  import {
324
354
  CLIENT_RUN_LIST_MAX_BYTES,
@@ -379,10 +409,6 @@ import {
379
409
 
380
410
  export const BOT_CONFIGURATION_KEY = "bot-configuration";
381
411
  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
412
  const STOP_RECEIPT_PREFIX = "stop-receipt:";
387
413
  /**
388
414
  * The Bot's durable announcement log: Session events that happen outside any
@@ -396,18 +422,9 @@ export const BOT_ANNOUNCEMENT_RETENTION = 32;
396
422
  function botAnnouncementKey(seq: number): string {
397
423
  return `${BOT_ANNOUNCEMENT_PREFIX}${String(seq).padStart(12, "0")}`;
398
424
  }
399
- const ASSIGNMENT_SAGA_DEADLINE_MS = 60_000;
400
425
  /** Idempotency records for Composition commands this Package admits. */
401
426
  const COMPOSITION_COMMAND_PREFIX = "composition-command:";
402
427
 
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
428
  interface StoredConfigurationReceipt {
412
429
  commandFingerprint: string;
413
430
  receipt: OperationReceiptV1;
@@ -428,11 +445,22 @@ function isTerminalStoredRunStatus(status: StoredRunStatus): boolean {
428
445
  );
429
446
  }
430
447
 
431
- interface AssignmentActivity {
448
+ interface ConfigurationActivity {
432
449
  commandFingerprint: string;
433
450
  promise: Promise<OperationReceiptV1>;
434
451
  }
435
452
 
453
+ interface IsolateCallScopeV1 {
454
+ userId: string;
455
+ botId: string;
456
+ runId: string;
457
+ sessionId: string;
458
+ turnId: string;
459
+ packageId: string;
460
+ generationId: string;
461
+ request: unknown;
462
+ }
463
+
436
464
  function requireMatchingConfigurationReceipt(
437
465
  stored: StoredConfigurationReceipt,
438
466
  commandFingerprint: string,
@@ -450,6 +478,9 @@ export type { BotIdentity, OwnedBotTurnCommand };
450
478
 
451
479
  export interface BotStateEnv {
452
480
  MEMORY_FILES: R2Bucket;
481
+ /** Object-storage file surfaces constructed by the Cloudflare adapter. */
482
+ WORKSPACE_FILES?: WorkspaceFilesV1;
483
+ MEMORY_WORKSPACE_FILES?: WorkspaceFilesV1;
453
484
  /**
454
485
  * The Bot Package Worker Loader (plan Step 4). Optional so a host without
455
486
  * Bot-authored Packages — tests, the Electron shell — still compiles; a
@@ -471,8 +502,16 @@ export interface BotStateEnv {
471
502
  */
472
503
  PACKAGE_BUNDLER?: PackageBundlerBinding;
473
504
  MEMORY_INDEX: VectorizeIndex;
474
- /** The native Workers AI binding consumed through narrow Package adapters. */
475
- AI?: WorkersAiBindingV1;
505
+ /** The native AI binding consumed through the image Package adapter. */
506
+ AI?: NativeAiBindingV1;
507
+ /** The Flock AI Gateway adapter constructed by the Cloudflare host. */
508
+ FLOCK_AI?: {
509
+ autoRoute: string;
510
+ runChatCompletion(
511
+ gatewayModel: string,
512
+ body: Record<string, unknown>,
513
+ ): Promise<ReadableStream<Uint8Array>>;
514
+ };
476
515
  USER_CONFIGURATIONS: DurableObjectNamespace;
477
516
  /**
478
517
  * The Bot Durable Object namespace, as the Subagent Durable Object namespace
@@ -535,6 +574,29 @@ function optionalStoredRun(input: unknown): StoredRun | undefined {
535
574
  return input === undefined ? undefined : requireStoredRunV1(input);
536
575
  }
537
576
 
577
+ /** Server-side allowlist for the untrusted page's only effectful message. */
578
+ export function requirePackageUiToolDeclarationV1(
579
+ catalog: PackageIframeCompositionV1,
580
+ command: Pick<
581
+ PackageIframeToolCommandV1,
582
+ "generationId" | "packageId" | "name"
583
+ >,
584
+ ): PackageIframeCompositionV1["contributions"][number] {
585
+ const contribution = catalog.contributions.find(
586
+ (candidate) => candidate.packageId === command.packageId,
587
+ );
588
+ if (
589
+ catalog.generationId !== command.generationId ||
590
+ !contribution ||
591
+ !contribution.declaredTools.includes(command.name)
592
+ ) {
593
+ throw new Error(
594
+ `Package "${command.packageId}" did not declare tool "${command.name}" in generation "${command.generationId}"`,
595
+ );
596
+ }
597
+ return contribution;
598
+ }
599
+
538
600
  export class ShellBotBackendContribution {
539
601
  readonly ctx: DurableObjectState;
540
602
  readonly env: BotStateEnv;
@@ -545,10 +607,24 @@ export class ShellBotBackendContribution {
545
607
  Promise<ClientTurnV1>
546
608
  >();
547
609
  private readonly outboundFetch?: typeof fetch;
548
- private readonly assignmentActivities = new Map<string, AssignmentActivity>();
610
+ private readonly configurationActivities = new Map<
611
+ string,
612
+ ConfigurationActivity
613
+ >();
549
614
  /** The Turn currently executing on this object, for durable Stop. */
550
615
  private activeTurn:
551
- { runId: string; sessionId: string; cancel(): void } | undefined;
616
+ | {
617
+ runId: string;
618
+ sessionId: string;
619
+ turnId: string;
620
+ generationId: string;
621
+ turnType: TurnTypeV1;
622
+ subagentRole?: string;
623
+ mounted: ShellMountedComposition;
624
+ signal: AbortSignal;
625
+ cancel(): void;
626
+ }
627
+ | undefined;
552
628
  /**
553
629
  * Admission, the event log, the cursor, idempotency, cancellation, and
554
630
  * durable scheduling are kernel authority; this Package supplies only the
@@ -620,7 +696,7 @@ export class ShellBotBackendContribution {
620
696
  terminalRecords: (input) => this.terminalPackageRecords(input),
621
697
  scheduledDeadlines: (transaction) =>
622
698
  this.scheduledDeadlines(transaction),
623
- scheduledWorkInFlight: () => this.assignmentActivities.size > 0,
699
+ scheduledWorkInFlight: () => false,
624
700
  deferScheduledWork: (transaction) =>
625
701
  this.deferScheduledWork(transaction),
626
702
  settleScheduledWork: () => this.settleScheduledWork(),
@@ -634,12 +710,6 @@ export class ShellBotBackendContribution {
634
710
  name: string;
635
711
  /** The persona the Bot's profile is seeded with, when its creator gave one. */
636
712
  description?: string;
637
- model?: BotSettingsViewV1["model"];
638
- modelBinding?: {
639
- assignment: BotSettingsViewV1["assignments"][number];
640
- generation: string;
641
- };
642
- assignments?: BotSettingsViewV1["assignments"];
643
713
  },
644
714
  ): Promise<BotSettingsViewV1> {
645
715
  return this.ctx.storage.transaction(async (transaction) => {
@@ -651,63 +721,29 @@ export class ShellBotBackendContribution {
651
721
  ) {
652
722
  throw new Error("Bot authority does not match its durable identity");
653
723
  }
654
- const existing = await transaction.get<BotSettingsViewV1>(
655
- BOT_CONFIGURATION_KEY,
656
- );
657
- if (existing) return existing;
724
+ const stored = await transaction.get<unknown>(BOT_CONFIGURATION_KEY);
725
+ if (stored !== undefined) {
726
+ return decodeBotSettingsViewV1(migrateStoredBotSettingsV1(stored));
727
+ }
658
728
  const settings = {
659
- ...this.initialBotSettings(identity.botId, initial.model),
729
+ ...this.initialBotSettings(identity.botId),
660
730
  profile: {
661
731
  name: initial.name,
662
732
  ...(initial.description === undefined
663
733
  ? {}
664
734
  : { description: initial.description }),
665
735
  },
666
- assignments: [
667
- ...(initial.assignments ?? []).map((assignment) =>
668
- structuredClone(assignment),
669
- ),
670
- ...(initial.modelBinding
671
- ? [structuredClone(initial.modelBinding.assignment)]
672
- : []),
673
- ],
674
736
  } satisfies BotSettingsViewV1;
675
737
  await transaction.put({
676
738
  [IDENTITY_KEY]: durableIdentity ?? identity,
677
739
  [BOT_CONFIGURATION_KEY]: settings,
678
- ...(initial.modelBinding
679
- ? {
680
- [assignmentGenerationKey(
681
- initial.modelBinding.assignment.assignmentId,
682
- )]: initial.modelBinding.generation,
683
- }
684
- : {}),
685
740
  });
686
741
  return settings;
687
742
  });
688
743
  }
689
744
 
690
745
  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 };
746
+ return this.ensureBotSettings(identity);
711
747
  }
712
748
 
713
749
  async readConfiguration(input: unknown): Promise<BotSettingsViewV1> {
@@ -729,7 +765,7 @@ export class ShellBotBackendContribution {
729
765
  command: Extract<ConfigurationCommandV1, { botId: string }>,
730
766
  ): Promise<OperationReceiptV1> {
731
767
  const commandFingerprint = configurationCommandFingerprintV1(command);
732
- const active = this.assignmentActivities.get(command.commandId);
768
+ const active = this.configurationActivities.get(command.commandId);
733
769
  if (active) {
734
770
  if (active.commandFingerprint !== commandFingerprint) {
735
771
  throw new Error(
@@ -745,12 +781,13 @@ export class ShellBotBackendContribution {
745
781
  commandFingerprint,
746
782
  ).finally(() => {
747
783
  if (
748
- this.assignmentActivities.get(command.commandId)?.promise === activity
784
+ this.configurationActivities.get(command.commandId)?.promise ===
785
+ activity
749
786
  ) {
750
- this.assignmentActivities.delete(command.commandId);
787
+ this.configurationActivities.delete(command.commandId);
751
788
  }
752
789
  });
753
- this.assignmentActivities.set(command.commandId, {
790
+ this.configurationActivities.set(command.commandId, {
754
791
  commandFingerprint,
755
792
  promise: activity,
756
793
  });
@@ -767,103 +804,18 @@ export class ShellBotBackendContribution {
767
804
  const existing =
768
805
  await this.ctx.storage.get<StoredConfigurationReceipt>(receiptKey);
769
806
  if (existing) {
770
- const receipt = requireMatchingConfigurationReceipt(
807
+ return requireMatchingConfigurationReceipt(
771
808
  existing,
772
809
  commandFingerprint,
773
810
  command.commandId,
774
811
  );
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
812
  }
805
813
  if (command.expectedRevision !== settings.revision) {
806
814
  throw new ConfigurationConflictError(settings.revision);
807
815
  }
808
- let modelCapabilities = new Set<string>();
809
- if (command.type === "bot/select-model") {
810
- const [user, application] = await Promise.all([
811
- this.userConfiguration(identity).readConfiguration({
812
- schemaVersion: 1,
813
- userId: identity.userId,
814
- }),
815
- this.compileApplication(),
816
- ]);
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
- }
816
+ let packageValues: Record<string, unknown> | undefined;
817
+ let packageUnset: string[] | undefined;
818
+ if (command.type === "bot/set-package-settings") {
867
819
  const [user, application] = await Promise.all([
868
820
  this.userConfiguration(identity).readConfiguration({
869
821
  schemaVersion: 1,
@@ -871,186 +823,39 @@ export class ShellBotBackendContribution {
871
823
  }),
872
824
  this.compileApplication(),
873
825
  ]);
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
- })),
826
+ const packages = application.packages.map((pkg) => ({
827
+ packageId: pkg.id,
828
+ version: pkg.version,
829
+ settings: pkg.manifest.configuration?.settings ?? [],
830
+ }));
831
+ if (command.values) {
832
+ packageValues = decodeInstalledPackageSettingsPatchV1({
833
+ packageId: command.packageId,
834
+ values: command.values,
835
+ scope: "bot",
836
+ installations: user.packages,
837
+ packages,
924
838
  });
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
- }
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
839
  }
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
- );
840
+ if (command.unset) {
841
+ packageUnset = decodeInstalledPackageSettingIdsV1({
842
+ packageId: command.packageId,
843
+ unset: command.unset,
844
+ scope: "bot",
845
+ installations: user.packages,
846
+ packages,
847
+ });
981
848
  }
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
849
  }
1007
- return this.executeAssignmentCommand(
850
+ return this.applySimpleConfigurationCommand(
1008
851
  identity,
1009
852
  command,
1010
853
  commandFingerprint,
1011
- {
1012
- ...(command.type === "bot/unassign-capability"
1013
- ? {}
1014
- : { model: command.model }),
1015
- },
854
+ packageValues,
855
+ packageUnset,
1016
856
  );
1017
857
  }
1018
858
 
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
859
  private async applySimpleConfigurationCommand(
1055
860
  identity: BotIdentity,
1056
861
  command: Extract<
@@ -1060,10 +865,12 @@ export class ShellBotBackendContribution {
1060
865
  | "bot/update-profile"
1061
866
  | "bot/set-profile"
1062
867
  | "bot/update-notifications"
1063
- | "bot/select-model";
868
+ | "bot/set-package-settings";
1064
869
  }
1065
870
  >,
1066
871
  commandFingerprint: string,
872
+ packageValues?: Record<string, unknown>,
873
+ packageUnset: readonly string[] = [],
1067
874
  ): Promise<OperationReceiptV1> {
1068
875
  return this.ctx.storage.transaction(async (transaction) => {
1069
876
  await this.lifecycleAdmission?.(transaction, identity.botId);
@@ -1077,15 +884,14 @@ export class ShellBotBackendContribution {
1077
884
  command.commandId,
1078
885
  );
1079
886
  }
887
+ const stored = await transaction.get<unknown>(BOT_CONFIGURATION_KEY);
1080
888
  const current =
1081
- (await transaction.get<BotSettingsViewV1>(BOT_CONFIGURATION_KEY)) ??
1082
- this.initialBotSettings(identity.botId);
889
+ stored === undefined
890
+ ? this.initialBotSettings(identity.botId)
891
+ : decodeBotSettingsViewV1(migrateStoredBotSettingsV1(stored));
1083
892
  if (command.expectedRevision !== current.revision) {
1084
893
  throw new ConfigurationConflictError(current.revision);
1085
894
  }
1086
- if (current.assignmentOperations.length > 0) {
1087
- throw new Error("An Assignment operation is still retrying");
1088
- }
1089
895
  const revision = current.revision + 1;
1090
896
  const next: BotSettingsViewV1 =
1091
897
  command.type === "bot/update-profile"
@@ -1102,7 +908,25 @@ export class ShellBotBackendContribution {
1102
908
  }
1103
909
  : command.type === "bot/update-notifications"
1104
910
  ? { ...current, revision, notifications: command.notifications }
1105
- : { ...current, revision, model: command.model };
911
+ : (() => {
912
+ const values = {
913
+ ...(current.packageValues[command.packageId] ?? {}),
914
+ ...structuredClone(packageValues ?? {}),
915
+ };
916
+ for (const settingId of packageUnset)
917
+ delete values[settingId];
918
+ const nextPackageValues = { ...current.packageValues };
919
+ if (Object.keys(values).length > 0) {
920
+ nextPackageValues[command.packageId] = values;
921
+ } else {
922
+ delete nextPackageValues[command.packageId];
923
+ }
924
+ return {
925
+ ...current,
926
+ revision,
927
+ packageValues: nextPackageValues,
928
+ };
929
+ })();
1106
930
  const receipt: OperationReceiptV1 = {
1107
931
  schemaVersion: 1,
1108
932
  commandId: command.commandId,
@@ -1200,797 +1024,132 @@ export class ShellBotBackendContribution {
1200
1024
  .map(([, value]) => decodeSessionEvent(value));
1201
1025
  }
1202
1026
 
1203
- private async assignmentRequirement(
1027
+ private async refreshRecoveryAlarm(
1028
+ transaction: DurableObjectTransaction,
1029
+ ): Promise<void> {
1030
+ await this.authority.refreshRecoveryAlarm(transaction);
1031
+ }
1032
+
1033
+ async resolveConfiguration(
1204
1034
  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,
1035
+ ): Promise<BotExecutionPlanV1> {
1036
+ return (await this.resolveExecutionContext(identity)).plan;
1037
+ }
1038
+
1039
+ async run(command: OwnedBotTurnCommand): Promise<ClientTurnV1> {
1040
+ return projectClientTurnV1(await this.authority.run(command));
1041
+ }
1042
+
1043
+ async runPackageUiTool(
1044
+ identity: BotIdentity,
1045
+ command: PackageIframeToolCommandV1,
1046
+ ): Promise<ClientTurnV1> {
1047
+ await this.validateIdentity(identity);
1048
+ const catalog = await this.listPackageUi(identity);
1049
+ const contribution = requirePackageUiToolDeclarationV1(catalog, command);
1050
+ return projectClientTurnV1(
1051
+ await this.authority.run({
1052
+ ...identity,
1053
+ runId: command.commandId,
1054
+ sessionId: `${identity.userId}:${identity.botId}`,
1055
+ acceptedAt: new Date().toISOString(),
1056
+ text: `${contribution.displayName} · ${command.name}`,
1057
+ directTool: {
1058
+ generationId: command.generationId,
1059
+ packageId: command.packageId,
1060
+ name: command.name,
1061
+ input: command.input,
1062
+ },
1217
1063
  }),
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
1064
  );
1237
- const pkg = application.packages.find(
1238
- (candidate) =>
1239
- candidate.id === assignment.packageId &&
1240
- candidate.version === installation?.version,
1065
+ }
1066
+
1067
+ /**
1068
+ * The Bot's invocable Skills, for the composer's `/` and `@` popover.
1069
+ *
1070
+ * A read of the same instruction root the Turn loader reads, through the
1071
+ * same `WorkspaceReadsV1`, so the popover can never offer a Skill a Turn
1072
+ * would refuse as an instruction: a refused candidate is not in the catalog
1073
+ * here either. Names and descriptions only — never a body.
1074
+ *
1075
+ * An unbound Workspace surface is an empty catalog, not a failure: the
1076
+ * Skills Package is not mounted in that host either, so "no Skills" is the
1077
+ * true answer rather than an error the composer has to explain.
1078
+ */
1079
+ async listSkills(identity: BotIdentity): Promise<ClientSkillCatalogV1> {
1080
+ await this.validateIdentity(identity);
1081
+ const reads = createBotSkillsReads(this.env);
1082
+ if (!reads) return { schemaVersion: 1, skills: [] };
1083
+ const user = await this.userConfiguration(identity).readConfiguration({
1084
+ schemaVersion: 1,
1085
+ userId: identity.userId,
1086
+ });
1087
+ const pluginSkills = createBotPluginSkillsSource(
1088
+ user.packages,
1089
+ createBotSkillCatalogReader(this.env),
1241
1090
  );
1242
- const capability = pkg?.manifest.configuration?.capabilities.find(
1243
- (candidate) => candidate.id === assignment.capabilityId,
1091
+ const catalog = await loadFullSkillCatalogV1(
1092
+ reads,
1093
+ { userId: identity.userId, botId: identity.botId },
1094
+ { ...(pluginSkills ? { pluginSkills } : {}) },
1244
1095
  );
1245
- if (!installation || !pkg || !capability) {
1246
- return {
1247
- failure: "Capability assignment policy changed during validation",
1248
- };
1096
+ const entries: ClientSkillCatalogEntryV1[] = [];
1097
+ for (const skill of catalog.skills) {
1098
+ const ref = skillRefForLoadedSkillV1(skill);
1099
+ // A Skill whose directory is not a well-formed slug has no ref, so it
1100
+ // cannot be invoked and is not offered. It is still listed to the model
1101
+ // in `<agent_skills>` and still loadable by path.
1102
+ if (!ref) continue;
1103
+ entries.push(
1104
+ clientSkillCatalogEntryV1({
1105
+ skill: ref,
1106
+ name: skill.name,
1107
+ description: skill.description,
1108
+ path: skill.path,
1109
+ }),
1110
+ );
1249
1111
  }
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
- });
1112
+ return { schemaVersion: 1, skills: entries };
1488
1113
  }
1489
1114
 
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 },
1115
+ /** The one durable manifest lookup used by mounts, commands, and UI views. */
1116
+ private async readCompositionMemberManifest(
1117
+ member: CompositionMemberV1,
1118
+ ): Promise<FrockBotManifest | undefined> {
1119
+ const stored = await this.ctx.storage.get<AuthoredManifestRecordV1>(
1120
+ authorshipManifestKey(member.manifestHash),
1534
1121
  );
1122
+ return stored ? decodeFrockBotManifest(stored.manifest) : undefined;
1535
1123
  }
1536
1124
 
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
- ) {
1125
+ private async requireCompositionMemberManifest(
1126
+ member: CompositionMemberV1,
1127
+ ): Promise<FrockBotManifest> {
1128
+ const manifest = await this.readCompositionMemberManifest(member);
1129
+ if (!manifest) {
1724
1130
  throw new Error(
1725
- `Configuration command idempotency key "${commandId}" was reused for a different command`,
1131
+ `package "${member.packageId}" manifest "${member.manifestHash}" is unavailable`,
1726
1132
  );
1727
1133
  }
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
- }
1134
+ return manifest;
1875
1135
  }
1876
1136
 
1877
- private async refreshRecoveryAlarm(
1878
- transaction: DurableObjectTransaction,
1879
- ): Promise<void> {
1880
- await this.authority.refreshRecoveryAlarm(transaction);
1881
- }
1882
- async markConnectionUnavailable(
1137
+ /** Active fail-closed Composition projected as inert iframe metadata. */
1138
+ async listPackageUi(
1883
1139
  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";
1935
- });
1936
- }
1937
-
1938
- async resolveConfiguration(
1939
- identity: BotIdentity,
1940
- ): Promise<BotExecutionPlanV1> {
1941
- return (await this.resolveExecutionContext(identity)).plan;
1942
- }
1943
-
1944
- async run(command: OwnedBotTurnCommand): Promise<ClientTurnV1> {
1945
- return projectClientTurnV1(await this.authority.run(command));
1946
- }
1947
-
1948
- /**
1949
- * The Bot's invocable Skills, for the composer's `/` and `@` popover.
1950
- *
1951
- * A read of the same instruction root the Turn loader reads, through the
1952
- * same `WorkspaceReadsV1`, so the popover can never offer a Skill a Turn
1953
- * would refuse as an instruction: a refused candidate is not in the catalog
1954
- * here either. Names and descriptions only — never a body.
1955
- *
1956
- * An unbound Workspace surface is an empty catalog, not a failure: the
1957
- * Skills Package is not mounted in that host either, so "no Skills" is the
1958
- * true answer rather than an error the composer has to explain.
1959
- */
1960
- async listSkills(identity: BotIdentity): Promise<ClientSkillCatalogV1> {
1140
+ ): Promise<PackageIframeCompositionV1> {
1961
1141
  await this.validateIdentity(identity);
1962
- const reads = createBotSkillsReads(this.env);
1963
- if (!reads) return { schemaVersion: 1, skills: [] };
1964
- const user = await this.userConfiguration(identity).readConfiguration({
1965
- schemaVersion: 1,
1966
- userId: identity.userId,
1142
+ const current = await this.authority.composition.current();
1143
+ const generation =
1144
+ current.status === "active" || current.status === "superseded"
1145
+ ? current
1146
+ : await this.authority.composition.lastKnownGood();
1147
+ return projectPackageIframeCompositionV1({
1148
+ botId: identity.botId,
1149
+ generation,
1150
+ readMemberManifest: (member) =>
1151
+ this.readCompositionMemberManifest(member),
1967
1152
  });
1968
- const pluginSkills = createBotPluginSkillsSource(
1969
- user.packages,
1970
- createBotSkillCatalogReader(this.env),
1971
- );
1972
- const catalog = await loadFullSkillCatalogV1(
1973
- reads,
1974
- { userId: identity.userId, botId: identity.botId },
1975
- { ...(pluginSkills ? { pluginSkills } : {}) },
1976
- );
1977
- const entries: ClientSkillCatalogEntryV1[] = [];
1978
- for (const skill of catalog.skills) {
1979
- const ref = skillRefForLoadedSkillV1(skill);
1980
- // A Skill whose directory is not a well-formed slug has no ref, so it
1981
- // cannot be invoked and is not offered. It is still listed to the model
1982
- // in `<agent_skills>` and still loadable by path.
1983
- if (!ref) continue;
1984
- entries.push(
1985
- clientSkillCatalogEntryV1({
1986
- skill: ref,
1987
- name: skill.name,
1988
- description: skill.description,
1989
- path: skill.path,
1990
- }),
1991
- );
1992
- }
1993
- return { schemaVersion: 1, skills: entries };
1994
1153
  }
1995
1154
 
1996
1155
  /**
@@ -2204,11 +1363,17 @@ export class ShellBotBackendContribution {
2204
1363
  ? { subagentTaskId: input.command.origin.taskId }
2205
1364
  : {}),
2206
1365
  };
1366
+ let mountedRoot: ShellMountedComposition["root"] | undefined;
1367
+ let mountedGeneration: CompositionGenerationV1 | undefined;
1368
+ const currentToolNames = (): readonly string[] =>
1369
+ mountedRoot?.tools.registeredNames?.() ?? [];
2207
1370
  const runtime = await this.agentRuntime(
2208
1371
  input.identity,
2209
1372
  settings,
2210
1373
  input.admittedRequest,
2211
1374
  turn,
1375
+ currentToolNames,
1376
+ () => mountedGeneration,
2212
1377
  );
2213
1378
  const promptParts = [
2214
1379
  `You are ${settings.profile.name}.`,
@@ -2225,9 +1390,9 @@ export class ShellBotBackendContribution {
2225
1390
  runId: input.command.runId,
2226
1391
  sessionId: input.command.sessionId,
2227
1392
  generationId: mounting.generationId,
2228
- assignments: settings.assignments,
1393
+ settings,
2229
1394
  });
2230
- return createShellCompositionHost({
1395
+ const mounted = await createShellCompositionHost({
2231
1396
  botId: input.identity.botId,
2232
1397
  sessionId: input.command.sessionId,
2233
1398
  sessionEvents: input.previousEvents,
@@ -2253,6 +1418,9 @@ export class ShellBotBackendContribution {
2253
1418
  ),
2254
1419
  ...(isolate ? { isolate } : {}),
2255
1420
  }).mount(mounting, signal);
1421
+ mountedRoot = mounted.root;
1422
+ mountedGeneration = mounted.generation;
1423
+ return mounted;
2256
1424
  },
2257
1425
  };
2258
1426
  const controller = new AbortController();
@@ -2292,14 +1460,78 @@ export class ShellBotBackendContribution {
2292
1460
  const active = {
2293
1461
  runId: input.command.runId,
2294
1462
  sessionId: input.command.sessionId,
2295
- cancel: () => activation.mounted.runtime.agent.agent.cancel("user"),
1463
+ turnId: input.command.runId,
1464
+ generationId: activation.mounted.generation.generationId,
1465
+ turnType: input.command.turnType ?? "chat",
1466
+ ...(input.command.subagentRole
1467
+ ? { subagentRole: input.command.subagentRole }
1468
+ : {}),
1469
+ mounted: activation.mounted,
1470
+ signal: controller.signal,
1471
+ cancel: () => {
1472
+ controller.abort("user");
1473
+ activation.mounted.runtime.agent.agent.cancel("user");
1474
+ },
2296
1475
  };
2297
1476
  this.activeTurn = active;
2298
1477
  try {
1478
+ const directTool = input.command.directTool;
1479
+ if (directTool) {
1480
+ if (
1481
+ directTool.generationId !== activation.mounted.generation.generationId
1482
+ ) {
1483
+ throw new Error(
1484
+ "Package UI command does not match the mounted Composition generation",
1485
+ );
1486
+ }
1487
+ const member = activation.mounted.generation.members.find(
1488
+ (candidate) =>
1489
+ candidate.packageId === directTool.packageId &&
1490
+ candidate.provenance.kind !== "first-party",
1491
+ );
1492
+ if (!member)
1493
+ throw new Error("Package UI command names an unavailable Package");
1494
+ const manifest = await this.requireCompositionMemberManifest(member);
1495
+ const client = manifest.contributions.client;
1496
+ if (
1497
+ !client ||
1498
+ !isClientIframeContribution(client) ||
1499
+ !(manifest.tools ?? []).some((tool) => tool.name === directTool.name)
1500
+ ) {
1501
+ throw new Error(
1502
+ `Package "${directTool.packageId}" did not declare tool "${directTool.name}" for its iframe`,
1503
+ );
1504
+ }
1505
+ return await executeDirectToolTurn({
1506
+ command: { ...input.command, directTool },
1507
+ previousEvents: input.previousEvents,
1508
+ composition: activation.mounted,
1509
+ admitEffect: (effect) =>
1510
+ this.admitRunEffect(
1511
+ input.identity,
1512
+ input.command.runId,
1513
+ input.command.sessionId,
1514
+ effect,
1515
+ ),
1516
+ signal: controller.signal,
1517
+ });
1518
+ }
1519
+ const ordinaryInput = await this.turnInputTextV1(input.command);
1520
+ const durableInput =
1521
+ activation.status === "failed-closed"
1522
+ ? compositionFailureTurnTextV1(ordinaryInput, {
1523
+ attemptedGenerationId: input.compositionGenerationId,
1524
+ ...(activation.generation
1525
+ ? { generation: activation.generation }
1526
+ : {}),
1527
+ ...(activation.failure ? { failure: activation.failure } : {}),
1528
+ quarantined: activation.quarantined,
1529
+ })
1530
+ : ordinaryInput;
2299
1531
  return await executeBotTurn({
2300
1532
  command: {
2301
1533
  ...input.command,
2302
- text: await this.turnInputTextV1(input.command),
1534
+ text: durableInput,
2303
1535
  },
2304
1536
  previousEvents: input.previousEvents,
2305
1537
  composition: activation.mounted,
@@ -2376,9 +1608,9 @@ export class ShellBotBackendContribution {
2376
1608
  }
2377
1609
 
2378
1610
  /**
2379
- * Everything a Bot isolate member needs. Returns `undefined` when this host
2380
- * has no Package loader, in which case an isolate member fails verification
2381
- * rather than silently running nowhere.
1611
+ * Everything a Bot isolate member needs. Package identity is attribution
1612
+ * only; Connections and model are resolved once for the Bot and every member
1613
+ * receives the same list.
2382
1614
  */
2383
1615
  private async isolateMountOptions(
2384
1616
  identity: BotIdentity,
@@ -2386,7 +1618,7 @@ export class ShellBotBackendContribution {
2386
1618
  runId: string;
2387
1619
  sessionId: string;
2388
1620
  generationId: string;
2389
- assignments: readonly CapabilityAssignmentView[];
1621
+ settings: BotSettingsViewV1;
2390
1622
  },
2391
1623
  ): Promise<ShellIsolateMountOptions | undefined> {
2392
1624
  const loader = this.env.BOT_PACKAGES;
@@ -2401,172 +1633,659 @@ export class ShellBotBackendContribution {
2401
1633
  }
2402
1634
  ).exports;
2403
1635
  if (!loader || !artifacts || !exports?.BotCapabilities) return undefined;
1636
+ const authority = await this.isolateAuthoritySnapshot(
1637
+ identity,
1638
+ turn.settings,
1639
+ );
2404
1640
  const mintCapabilities = exports.BotCapabilities;
2405
- const assignments = await this.isolateAssignments(turn.assignments);
2406
1641
  return {
2407
1642
  userId: identity.userId,
2408
1643
  runId: turn.runId,
2409
- // One admitted Turn is one run; the Turn ordinal lives in the session log.
2410
1644
  turnId: turn.runId,
2411
1645
  loader,
2412
1646
  artifacts: createR2PackageArtifactStore(artifacts),
1647
+ manifestFor: (member) => this.requireCompositionMemberManifest(member),
2413
1648
  capabilitiesFor: (member) =>
2414
1649
  mintCapabilities({
2415
1650
  props: {
2416
1651
  userId: identity.userId,
2417
1652
  botId: identity.botId,
1653
+ runId: turn.runId,
1654
+ sessionId: turn.sessionId,
1655
+ turnId: turn.runId,
2418
1656
  generationId: turn.generationId,
2419
1657
  packageId: member.packageId,
2420
- assignments: structuredClone(assignments),
1658
+ connections: structuredClone(authority.connections),
1659
+ ...(authority.model
1660
+ ? { model: structuredClone(authority.model) }
1661
+ : {}),
1662
+ memory: authority.memory,
1663
+ workspace: authority.workspace,
2421
1664
  },
2422
1665
  }),
2423
- bindingDigest: await isolateBindingDigestV1(
2424
- assignments,
2425
- turn.generationId,
2426
- ),
1666
+ bindingDigest: await isolateBindingDigestV1({
1667
+ userId: identity.userId,
1668
+ botId: identity.botId,
1669
+ connections: authority.connections,
1670
+ ...(authority.model ? { model: authority.model } : {}),
1671
+ compositionGenerationId: turn.generationId,
1672
+ }),
2427
1673
  compatibilityDate: BOT_ISOLATE_COMPATIBILITY_DATE,
2428
1674
  };
2429
1675
  }
2430
1676
 
2431
- /**
2432
- * The Bot Durable Object side of `CAPABILITIES.requestAuthority`. Never a
2433
- * grant: it records a durable pending decision for the User.
2434
- */
2435
- async isolateRequestAuthority(input: {
2436
- botId: string;
2437
- packageId: string;
2438
- generationId: string;
2439
- request: unknown;
2440
- }): Promise<IsolatePendingDecisionV1> {
2441
- return await this.isolateCapabilities(input, []).requestAuthority(
2442
- input.request,
1677
+ private async isolateAuthoritySnapshot(
1678
+ identity: BotIdentity,
1679
+ settings: BotSettingsViewV1,
1680
+ ): Promise<{
1681
+ connections: IsolateConnectionV1[];
1682
+ model?: IsolateModelBindingV1;
1683
+ memory: boolean;
1684
+ workspace: boolean;
1685
+ }> {
1686
+ const [user, application] = await Promise.all([
1687
+ this.userConfiguration(identity).readConfiguration({
1688
+ schemaVersion: 1,
1689
+ userId: identity.userId,
1690
+ }),
1691
+ this.compileApplication(),
1692
+ ]);
1693
+ const connections = user.connections.flatMap((connection) =>
1694
+ connection.state === "ready" && connection.generation
1695
+ ? [
1696
+ {
1697
+ connectionId: connection.connectionId,
1698
+ packageId: connection.packageId,
1699
+ connectionTypeId: connection.connectionTypeId,
1700
+ displayName: connection.displayName,
1701
+ generation: connection.generation,
1702
+ safeMetadata: structuredClone(connection.safeMetadata),
1703
+ } satisfies IsolateConnectionV1,
1704
+ ]
1705
+ : [],
2443
1706
  );
1707
+ const effective = resolveEffectiveBotModelV1({
1708
+ bot: settings,
1709
+ user,
1710
+ packages: application.packages.map((pkg) => ({
1711
+ packageId: pkg.id,
1712
+ version: pkg.version,
1713
+ settings: pkg.manifest.configuration?.settings ?? [],
1714
+ capabilities: pkg.manifest.configuration?.capabilities ?? [],
1715
+ connectionTypes: pkg.manifest.configuration?.connectionTypes ?? [],
1716
+ })),
1717
+ });
1718
+ const binding = effective.binding;
1719
+ const model =
1720
+ effective.model &&
1721
+ binding?.state === "ready" &&
1722
+ binding.connection?.generation &&
1723
+ binding.packageId &&
1724
+ binding.providerType
1725
+ ? {
1726
+ connectionId: binding.connection.connectionId,
1727
+ packageId: binding.packageId,
1728
+ provider: binding.providerType,
1729
+ providerModelId: effective.model.providerModelId,
1730
+ connectionGeneration: binding.connection.generation,
1731
+ ...(binding.connection.modelCatalog?.generation
1732
+ ? {
1733
+ catalogGeneration: binding.connection.modelCatalog.generation,
1734
+ }
1735
+ : {}),
1736
+ }
1737
+ : undefined;
1738
+ return {
1739
+ connections,
1740
+ ...(model ? { model } : {}),
1741
+ memory: Boolean(this.env.MEMORY_WORKSPACE_FILES),
1742
+ workspace: Boolean(this.env.WORKSPACE_FILES),
1743
+ };
2444
1744
  }
2445
1745
 
2446
- /**
2447
- * The Bot Durable Object side of `CAPABILITIES.invokeModel`. Refuses with a
2448
- * pending decision unless an enabled model Assignment matches; otherwise
2449
- * records the normalized request and streams through the provider path,
2450
- * which takes the credential lease on the way.
2451
- */
2452
1746
  async isolateInvokeModel(
2453
1747
  identity: BotIdentity,
2454
1748
  input: {
1749
+ runId: string;
1750
+ sessionId: string;
1751
+ turnId: string;
2455
1752
  packageId: string;
2456
1753
  generationId: string;
2457
1754
  request: NormalizedModelRequest;
2458
1755
  },
2459
1756
  ): 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>(
2464
- BOT_CONFIGURATION_KEY,
2465
- );
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) {
1757
+ if (!this.activeIsolateTurn(input)) {
1758
+ return {
1759
+ status: "unavailable",
1760
+ reason: "the Package is not running in this Bot's active Composition",
1761
+ };
1762
+ }
1763
+ const settings = await this.ensureBotSettings(identity);
1764
+ const authority = await this.isolateAuthoritySnapshot(identity, settings);
1765
+ let runtime:
1766
+ | {
1767
+ agentPackages: FoundationAgentPackage[];
1768
+ modelSelection: RuntimeModelSelection;
1769
+ }
1770
+ | undefined;
1771
+ if (authority.model) {
2472
1772
  try {
2473
- settings = (await this.resolveExecutionContext(identity)).settings;
1773
+ runtime = await this.agentRuntime(identity, settings);
2474
1774
  } catch {
2475
- settings = stored;
1775
+ runtime = undefined;
2476
1776
  }
2477
1777
  }
2478
- const projected = await this.isolateAssignments(
2479
- settings?.assignments ?? [],
2480
- );
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(
1778
+ return this.isolateCapabilities(
2494
1779
  {
2495
1780
  botId: identity.botId,
2496
1781
  packageId: input.packageId,
2497
1782
  generationId: input.generationId,
2498
1783
  },
2499
- assignments,
2500
- bound
1784
+ authority,
1785
+ runtime && authority.model
2501
1786
  ? {
2502
- binding: bound.binding,
2503
- path: this.isolateModelPath(
2504
- identity,
2505
- bound.runtime,
2506
- input.generationId,
2507
- ),
1787
+ path: this.isolateModelPath(identity, runtime, input.generationId),
2508
1788
  }
2509
1789
  : undefined,
2510
- );
2511
- return await host.invokeModel(input.request);
1790
+ ).invokeModel(input.request);
2512
1791
  }
2513
1792
 
2514
- /**
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.
2521
- */
2522
- private async isolateModelBinding(
2523
- identity: BotIdentity,
2524
- settings: BotSettingsViewV1 | undefined,
2525
- assignments: readonly IsolateAssignmentV1[],
2526
- ): Promise<
2527
- | {
2528
- binding: IsolateModelBindingV1;
2529
- runtime: {
2530
- agentPackages: FoundationAgentPackage[];
2531
- modelSelection: RuntimeModelSelection;
1793
+ async isolateInvokeTool(input: {
1794
+ userId: string;
1795
+ botId: string;
1796
+ runId: string;
1797
+ sessionId: string;
1798
+ turnId: string;
1799
+ packageId: string;
1800
+ generationId: string;
1801
+ request: unknown;
1802
+ }): Promise<IsolateToolOutcomeV1> {
1803
+ const request = decodeIsolateToolRequestV1(input.request);
1804
+ const active = this.activeIsolateTurn(input);
1805
+ if (!active) {
1806
+ return {
1807
+ status: "unavailable",
1808
+ reason: "the Package is not running in this Bot's active Composition",
1809
+ };
1810
+ }
1811
+ const session = active.mounted.runtime.root.sessions.get(input.sessionId);
1812
+ if (!session) {
1813
+ return {
1814
+ status: "unavailable",
1815
+ reason: "the active Session is unavailable",
1816
+ };
1817
+ }
1818
+ const started = session.events.findLast(
1819
+ (event) => event.type === "step/start",
1820
+ );
1821
+ const ended = session.events.findLast((event) => event.type === "step/end");
1822
+ if (
1823
+ started?.type !== "step/start" ||
1824
+ (ended?.type === "step/end" &&
1825
+ ended.turn === started.turn &&
1826
+ ended.step === started.step)
1827
+ ) {
1828
+ return {
1829
+ status: "unavailable",
1830
+ reason: "the active step is unavailable",
1831
+ };
1832
+ }
1833
+ const effectId = await this.isolateToolEffectId(
1834
+ input.packageId,
1835
+ request.callId,
1836
+ );
1837
+ const priorCall = session.events.find(
1838
+ (event) =>
1839
+ event.type === "package/tool-call" && event.effectId === effectId,
1840
+ );
1841
+ const priorResult = session.events.find(
1842
+ (event) =>
1843
+ event.type === "package/tool-result" && event.effectId === effectId,
1844
+ );
1845
+ if (priorResult?.type === "package/tool-result") {
1846
+ return {
1847
+ status: "completed",
1848
+ content: priorResult.content,
1849
+ isError: priorResult.isError,
1850
+ };
1851
+ }
1852
+ if (
1853
+ priorCall?.type === "package/tool-call" &&
1854
+ (priorCall.packageId !== input.packageId ||
1855
+ priorCall.callId !== request.callId ||
1856
+ priorCall.name !== request.name ||
1857
+ JSON.stringify(priorCall.input) !== JSON.stringify(request.input))
1858
+ ) {
1859
+ return {
1860
+ status: "unavailable",
1861
+ reason: "the Package tool idempotency key was reused",
1862
+ };
1863
+ }
1864
+ if (!priorCall) {
1865
+ session.append({
1866
+ type: "package/tool-call",
1867
+ turn: started.turn,
1868
+ step: started.step,
1869
+ effectId,
1870
+ packageId: input.packageId,
1871
+ callId: request.callId,
1872
+ name: request.name,
1873
+ input: request.input,
1874
+ });
1875
+ await session.flush();
1876
+ }
1877
+ const call = {
1878
+ id: request.callId,
1879
+ name: request.name,
1880
+ input: request.input,
1881
+ };
1882
+ const context = {
1883
+ botId: input.botId,
1884
+ agentId: input.botId,
1885
+ sessionId: input.sessionId,
1886
+ compositionGenerationId: input.generationId,
1887
+ effectId,
1888
+ toolCall: call,
1889
+ turnType: active.turnType,
1890
+ ...(active.subagentRole ? { subagentRole: active.subagentRole } : {}),
1891
+ signal: active.signal,
1892
+ };
1893
+ const preparation = await active.mounted.runtime.root.tools.prepare(
1894
+ call,
1895
+ context,
1896
+ );
1897
+ let result: { content: string; isError: boolean };
1898
+ if (preparation.kind === "denied") {
1899
+ result = preparation.result;
1900
+ } else {
1901
+ const admitted = await this.admitRunEffect(
1902
+ { userId: input.userId, botId: input.botId },
1903
+ input.runId,
1904
+ input.sessionId,
1905
+ { kind: "tool", effectId },
1906
+ );
1907
+ if (!admitted) {
1908
+ result = {
1909
+ content: "The tool effect was stopped before it started.",
1910
+ isError: true,
2532
1911
  };
1912
+ } else if (priorCall) {
1913
+ const recovered =
1914
+ await active.mounted.runtime.root.tools.reconcilePrepared(
1915
+ preparation,
1916
+ context,
1917
+ );
1918
+ result =
1919
+ recovered.status === "recovered"
1920
+ ? recovered.result
1921
+ : { content: recovered.reason, isError: true };
1922
+ } else {
1923
+ result = await active.mounted.runtime.root.tools.executePrepared(
1924
+ preparation,
1925
+ context,
1926
+ );
2533
1927
  }
2534
- | undefined
2535
- > {
2536
- if (!settings) return undefined;
2537
- let runtime: {
2538
- agentPackages: FoundationAgentPackage[];
2539
- modelSelection: RuntimeModelSelection;
1928
+ }
1929
+ session.append({
1930
+ type: "package/tool-result",
1931
+ turn: started.turn,
1932
+ step: started.step,
1933
+ effectId,
1934
+ packageId: input.packageId,
1935
+ callId: request.callId,
1936
+ name: request.name,
1937
+ content: result.content,
1938
+ isError: result.isError,
1939
+ });
1940
+ await session.flush();
1941
+ return {
1942
+ status: "completed",
1943
+ content: result.content,
1944
+ isError: result.isError,
2540
1945
  };
2541
- try {
2542
- runtime = await this.agentRuntime(identity, settings);
2543
- } catch {
1946
+ }
1947
+
1948
+ async isolateMemoryRead(input: {
1949
+ userId: string;
1950
+ botId: string;
1951
+ runId: string;
1952
+ sessionId: string;
1953
+ turnId: string;
1954
+ packageId: string;
1955
+ generationId: string;
1956
+ request: unknown;
1957
+ }): Promise<IsolateMemoryOutcomeV1> {
1958
+ const request = decodeIsolateMemoryReadRequestV1(input.request);
1959
+ const memory = await this.isolateMemoryHost(input, request);
1960
+ if (!memory)
1961
+ return { status: "unavailable", reason: "Memory is unavailable" };
1962
+ return {
1963
+ status: "available",
1964
+ value: await memory.store.read(
1965
+ memoryScopeRootV1(request.scope, memory.owner, request.projectId),
1966
+ ),
1967
+ };
1968
+ }
1969
+
1970
+ async isolateMemoryWrite(input: {
1971
+ userId: string;
1972
+ botId: string;
1973
+ runId: string;
1974
+ sessionId: string;
1975
+ turnId: string;
1976
+ packageId: string;
1977
+ generationId: string;
1978
+ request: unknown;
1979
+ }): Promise<IsolateMemoryOutcomeV1> {
1980
+ const request = decodeIsolateMemoryWriteRequestV1(input.request);
1981
+ const memory = await this.isolateMemoryHost(input, request);
1982
+ if (!memory?.writer) {
1983
+ return { status: "unavailable", reason: "Memory is unavailable" };
1984
+ }
1985
+ return {
1986
+ status: "available",
1987
+ value: await memory.store.write({
1988
+ root: memoryScopeRootV1(request.scope, memory.owner, request.projectId),
1989
+ tier: request.tier ?? "log",
1990
+ fact: request.fact,
1991
+ writer: {
1992
+ kind: "bot",
1993
+ botId: input.botId,
1994
+ ...memory.writer,
1995
+ },
1996
+ }),
1997
+ };
1998
+ }
1999
+
2000
+ async isolateMemoryForget(input: {
2001
+ userId: string;
2002
+ botId: string;
2003
+ runId: string;
2004
+ sessionId: string;
2005
+ turnId: string;
2006
+ packageId: string;
2007
+ generationId: string;
2008
+ request: unknown;
2009
+ }): Promise<IsolateMemoryOutcomeV1> {
2010
+ const request = decodeIsolateMemoryWriteRequestV1(input.request);
2011
+ const memory = await this.isolateMemoryHost(input, request);
2012
+ if (!memory?.writer) {
2013
+ return { status: "unavailable", reason: "Memory is unavailable" };
2014
+ }
2015
+ return {
2016
+ status: "available",
2017
+ value: await memory.store.forget({
2018
+ root: memoryScopeRootV1(request.scope, memory.owner, request.projectId),
2019
+ fact: request.fact,
2020
+ writer: {
2021
+ kind: "bot",
2022
+ botId: input.botId,
2023
+ ...memory.writer,
2024
+ },
2025
+ }),
2026
+ };
2027
+ }
2028
+
2029
+ async isolateWorkspaceRead(
2030
+ input: IsolateCallScopeV1,
2031
+ ): Promise<IsolateWorkspaceOutcomeV1> {
2032
+ const active = this.activeIsolateTurn(input);
2033
+ const files = this.env.WORKSPACE_FILES;
2034
+ if (!active || !files) {
2035
+ return { status: "unavailable", reason: "Workspace is unavailable" };
2036
+ }
2037
+ const path = this.isolateWorkspacePath(
2038
+ input.userId,
2039
+ decodeIsolateWorkspacePathV1(input.request),
2040
+ );
2041
+ return { status: "available", value: await files.read(path) };
2042
+ }
2043
+
2044
+ async isolateWorkspaceList(
2045
+ input: IsolateCallScopeV1,
2046
+ ): Promise<IsolateWorkspaceOutcomeV1> {
2047
+ const active = this.activeIsolateTurn(input);
2048
+ const files = this.env.WORKSPACE_FILES;
2049
+ if (!active || !files) {
2050
+ return { status: "unavailable", reason: "Workspace is unavailable" };
2051
+ }
2052
+ const request = decodeIsolateWorkspaceListRequestV1(input.request);
2053
+ const root = this.isolateWorkspaceRoot(input.userId, request.root);
2054
+ return {
2055
+ status: "available",
2056
+ value: await files.list({
2057
+ root,
2058
+ ...(request.prefix === undefined ? {} : { prefix: request.prefix }),
2059
+ ...(request.cursor === undefined ? {} : { cursor: request.cursor }),
2060
+ ...(request.limit === undefined ? {} : { limit: request.limit }),
2061
+ }),
2062
+ };
2063
+ }
2064
+
2065
+ async isolateWorkspaceStat(
2066
+ input: IsolateCallScopeV1,
2067
+ ): Promise<IsolateWorkspaceOutcomeV1> {
2068
+ const active = this.activeIsolateTurn(input);
2069
+ const files = this.env.WORKSPACE_FILES;
2070
+ if (!active || !files) {
2071
+ return { status: "unavailable", reason: "Workspace is unavailable" };
2072
+ }
2073
+ const path = this.isolateWorkspacePath(
2074
+ input.userId,
2075
+ decodeIsolateWorkspacePathV1(input.request),
2076
+ );
2077
+ return { status: "available", value: await files.stat(path) };
2078
+ }
2079
+
2080
+ async isolateWorkspaceWrite(
2081
+ input: IsolateCallScopeV1,
2082
+ ): Promise<IsolateWorkspaceOutcomeV1> {
2083
+ const active = this.activeIsolateTurn(input);
2084
+ const files = this.env.WORKSPACE_FILES;
2085
+ if (!active || !files) {
2086
+ return { status: "unavailable", reason: "Workspace is unavailable" };
2087
+ }
2088
+ const request = decodeIsolateWorkspaceWriteRequestV1(input.request);
2089
+ return {
2090
+ status: "available",
2091
+ value: await files.write({
2092
+ path: this.isolateWorkspacePath(input.userId, request.path),
2093
+ bytes: request.bytes,
2094
+ writer: this.isolateWorkspaceWriter(input),
2095
+ expectedGenerationId: request.expectedGenerationId,
2096
+ ...(request.mediaType ? { mediaType: request.mediaType } : {}),
2097
+ }),
2098
+ };
2099
+ }
2100
+
2101
+ async isolateWorkspaceDelete(
2102
+ input: IsolateCallScopeV1,
2103
+ ): Promise<IsolateWorkspaceOutcomeV1> {
2104
+ const active = this.activeIsolateTurn(input);
2105
+ const files = this.env.WORKSPACE_FILES;
2106
+ if (!active || !files) {
2107
+ return { status: "unavailable", reason: "Workspace is unavailable" };
2108
+ }
2109
+ const request = decodeIsolateWorkspaceDeleteRequestV1(input.request);
2110
+ return {
2111
+ status: "available",
2112
+ value: await files.delete({
2113
+ path: this.isolateWorkspacePath(input.userId, request.path),
2114
+ writer: this.isolateWorkspaceWriter(input),
2115
+ expectedGenerationId: request.expectedGenerationId,
2116
+ }),
2117
+ };
2118
+ }
2119
+
2120
+ async isolateConnection(
2121
+ input: IsolateCallScopeV1,
2122
+ ): Promise<IsolateConnectionOutcomeV1> {
2123
+ if (!this.activeIsolateTurn(input) || typeof input.request !== "string") {
2124
+ return { status: "unavailable", reason: "the Connection is unavailable" };
2125
+ }
2126
+ const identity = { userId: input.userId, botId: input.botId };
2127
+ const user = await this.userConfiguration(identity).readConfiguration({
2128
+ schemaVersion: 1,
2129
+ userId: input.userId,
2130
+ });
2131
+ const connection = user.connections.find(
2132
+ (candidate) =>
2133
+ candidate.connectionId === input.request &&
2134
+ candidate.state === "ready" &&
2135
+ candidate.generation,
2136
+ );
2137
+ if (!connection?.generation) {
2138
+ await this.authority.recordNotification({
2139
+ notificationId: `package-connection-unavailable:${input.runId}:${input.packageId}:${input.request}`,
2140
+ runId: input.runId,
2141
+ createdAt: new Date().toISOString(),
2142
+ title: "Connection unavailable",
2143
+ body: `Package "${input.packageId}" could not use Connection "${input.request}". The User can enable or repair it on Connections.`.slice(
2144
+ 0,
2145
+ 240,
2146
+ ),
2147
+ });
2148
+ return { status: "unavailable", reason: "the Connection is unavailable" };
2149
+ }
2150
+ return {
2151
+ status: "available",
2152
+ leaseId: crypto.randomUUID(),
2153
+ connectionId: connection.connectionId,
2154
+ generation: connection.generation,
2155
+ expiresAt: new Date(Date.now() + 5 * 60_000).toISOString(),
2156
+ };
2157
+ }
2158
+
2159
+ async isolateNotify(
2160
+ input: IsolateCallScopeV1,
2161
+ ): Promise<IsolateNotificationOutcomeV1> {
2162
+ if (!this.activeIsolateTurn(input)) {
2163
+ return { status: "unavailable", reason: "notifications are unavailable" };
2164
+ }
2165
+ const request = decodeIsolateNotificationRequestV1(input.request);
2166
+ await this.authority.recordNotification({
2167
+ notificationId: `package:${input.packageId}:${request.notificationId}`,
2168
+ runId: input.runId,
2169
+ createdAt: new Date().toISOString(),
2170
+ title: request.title,
2171
+ body: request.body,
2172
+ });
2173
+ return { status: "recorded" };
2174
+ }
2175
+
2176
+ async isolateSchedule(
2177
+ input: IsolateCallScopeV1,
2178
+ ): Promise<IsolateToolOutcomeV1> {
2179
+ const request = decodeIsolateScheduleRequestV1(input.request);
2180
+ return this.isolateInvokeTool({
2181
+ ...input,
2182
+ request: {
2183
+ callId: request.callId,
2184
+ name: "routine_manage",
2185
+ input: request.input,
2186
+ },
2187
+ });
2188
+ }
2189
+
2190
+ private async isolateMemoryHost(
2191
+ input: IsolateCallScopeV1,
2192
+ request: { scope: "bot" | "user" | "project"; projectId?: string },
2193
+ ) {
2194
+ if (!this.activeIsolateTurn(input)) return undefined;
2195
+ const host = createBotMemoryHost(
2196
+ { userId: input.userId, botId: input.botId },
2197
+ {
2198
+ runId: input.runId,
2199
+ sessionId: input.sessionId,
2200
+ turnId: input.turnId,
2201
+ },
2202
+ this.env,
2203
+ );
2204
+ if (!host) return undefined;
2205
+ if (request.scope === "project") {
2206
+ const projects = await host.projects?.joined();
2207
+ if (
2208
+ !request.projectId ||
2209
+ !projects?.some((project) => project.projectId === request.projectId)
2210
+ ) {
2211
+ return undefined;
2212
+ }
2213
+ }
2214
+ return host;
2215
+ }
2216
+
2217
+ private activeIsolateTurn(input: {
2218
+ runId: string;
2219
+ sessionId: string;
2220
+ turnId: string;
2221
+ packageId: string;
2222
+ generationId: string;
2223
+ }) {
2224
+ const active = this.activeTurn;
2225
+ if (
2226
+ !active ||
2227
+ active.runId !== input.runId ||
2228
+ active.sessionId !== input.sessionId ||
2229
+ active.turnId !== input.turnId ||
2230
+ active.generationId !== input.generationId ||
2231
+ !active.mounted.generation.members.some(
2232
+ (member) => member.packageId === input.packageId && member.artifact,
2233
+ )
2234
+ ) {
2544
2235
  return undefined;
2545
2236
  }
2546
- const selection = runtime.modelSelection;
2547
- const connectionId = selection.connectionId;
2548
- if (!connectionId) return undefined;
2549
- const assignment = assignments.find(
2550
- (candidate) =>
2551
- candidate.kind === "model" && candidate.connectionId === connectionId,
2237
+ return active;
2238
+ }
2239
+
2240
+ private async isolateToolEffectId(
2241
+ packageId: string,
2242
+ callId: string,
2243
+ ): Promise<string> {
2244
+ const digest = await crypto.subtle.digest(
2245
+ "SHA-256",
2246
+ new TextEncoder().encode(`${packageId}\0${callId}`),
2247
+ );
2248
+ const hex = [...new Uint8Array(digest)]
2249
+ .map((byte) => byte.toString(16).padStart(2, "0"))
2250
+ .join("");
2251
+ return `package-tool:${hex}`;
2252
+ }
2253
+
2254
+ private isolateWorkspaceRoot(
2255
+ userId: string,
2256
+ root: ReturnType<typeof decodeIsolateWorkspacePathV1>["root"],
2257
+ ) {
2258
+ return decodeWorkspaceRootV1(
2259
+ root.kind === "user-instructions"
2260
+ ? { kind: root.kind, userId }
2261
+ : root.kind === "bot-instructions"
2262
+ ? { kind: root.kind, userId, botId: root.botId }
2263
+ : {
2264
+ kind: root.kind,
2265
+ userId,
2266
+ packageId: root.packageId,
2267
+ rootId: root.rootId,
2268
+ },
2552
2269
  );
2553
- if (!assignment) return undefined;
2270
+ }
2271
+
2272
+ private isolateWorkspacePath(
2273
+ userId: string,
2274
+ path: ReturnType<typeof decodeIsolateWorkspacePathV1>,
2275
+ ): WorkspacePathV1 {
2276
+ return decodeWorkspacePathV1({
2277
+ root: this.isolateWorkspaceRoot(userId, path.root),
2278
+ path: path.path,
2279
+ });
2280
+ }
2281
+
2282
+ private isolateWorkspaceWriter(input: IsolateCallScopeV1) {
2554
2283
  return {
2555
- runtime,
2556
- binding: {
2557
- assignmentId: assignment.assignmentId,
2558
- packageId: assignment.packageId,
2559
- capabilityId: assignment.capabilityId,
2560
- connectionId,
2561
- provider: selection.provider,
2562
- providerModelId: selection.model,
2563
- ...(selection.connectionGeneration
2564
- ? { connectionGeneration: selection.connectionGeneration }
2565
- : {}),
2566
- ...(selection.catalogGeneration
2567
- ? { catalogGeneration: selection.catalogGeneration }
2568
- : {}),
2569
- },
2284
+ kind: "bot" as const,
2285
+ botId: input.botId,
2286
+ sessionId: input.sessionId,
2287
+ turnId: input.turnId,
2288
+ runId: input.runId,
2570
2289
  };
2571
2290
  }
2572
2291
 
@@ -2575,58 +2294,35 @@ export class ShellBotBackendContribution {
2575
2294
  botId: string;
2576
2295
  packageId: string;
2577
2296
  generationId: string;
2578
- request?: unknown;
2579
2297
  },
2580
- assignments: readonly IsolateAssignmentV1[],
2581
- model?: { binding: IsolateModelBindingV1; path: IsolateModelPath },
2298
+ authority: {
2299
+ connections: readonly IsolateConnectionV1[];
2300
+ model?: IsolateModelBindingV1;
2301
+ memory: boolean;
2302
+ workspace: boolean;
2303
+ },
2304
+ model?: { path: IsolateModelPath },
2582
2305
  ): IsolateCapabilityHost {
2583
2306
  return createIsolateCapabilityHost({
2584
2307
  storage: {
2585
2308
  put: (key, value) => this.ctx.storage.put(key, value),
2586
- get: (key) => this.ctx.storage.get(key),
2587
2309
  list: (options) => this.ctx.storage.list(options),
2588
2310
  },
2589
2311
  botId: scope.botId,
2590
2312
  packageId: scope.packageId,
2591
2313
  generationId: scope.generationId,
2592
- assignments,
2593
- ...(model ? { modelBinding: model.binding, modelPath: model.path } : {}),
2314
+ connections: authority.connections,
2315
+ ...(authority.model ? { modelBinding: authority.model } : {}),
2316
+ ...(model ? { modelPath: model.path } : {}),
2317
+ memory: authority.memory,
2318
+ workspace: authority.workspace,
2594
2319
  });
2595
2320
  }
2596
2321
 
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
2322
  /**
2625
2323
  * Streams through the pinned Composition's mounted `ctx.llm` — the same
2626
2324
  * provider path a Turn uses, so whichever provider Plugin serves the request
2627
- * 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.
2325
+ * is the one that takes the credential lease.
2630
2326
  */
2631
2327
  private isolateModelPath(
2632
2328
  identity: BotIdentity,
@@ -2652,8 +2348,6 @@ export class ShellBotBackendContribution {
2652
2348
  sessionEvents: [],
2653
2349
  agentPackages: runtime.agentPackages,
2654
2350
  modelSelection: runtime.modelSelection,
2655
- // 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.
2657
2351
  admitEffect: () => Promise.resolve(true),
2658
2352
  }).mount(generation, signal);
2659
2353
  try {
@@ -2676,29 +2370,22 @@ export class ShellBotBackendContribution {
2676
2370
  private async resolveAdmissionSnapshot(
2677
2371
  command: OwnedBotTurnCommand,
2678
2372
  ): Promise<BotSettingsViewV1> {
2679
- const context = await this.resolveExecutionContext(command);
2680
- return {
2681
- ...context.settings,
2682
- assignments: context.plan.assignments,
2683
- } satisfies BotSettingsViewV1;
2373
+ return (await this.resolveExecutionContext(command)).settings;
2684
2374
  }
2685
2375
 
2686
2376
  private async admittedSnapshot(
2687
2377
  transaction: DurableObjectTransaction,
2688
2378
  resolved: BotSettingsViewV1,
2689
2379
  ): Promise<BotSettingsViewV1> {
2690
- return (
2691
- (await transaction.get<BotSettingsViewV1>(BOT_CONFIGURATION_KEY)) ??
2692
- resolved
2693
- );
2380
+ const stored = await transaction.get<unknown>(BOT_CONFIGURATION_KEY);
2381
+ return stored === undefined
2382
+ ? resolved
2383
+ : decodeBotSettingsViewV1(migrateStoredBotSettingsV1(stored));
2694
2384
  }
2695
2385
 
2696
2386
  private async scheduledDeadlines(
2697
2387
  transaction: DurableObjectTransaction,
2698
2388
  ): Promise<number[]> {
2699
- const sagas = await transaction.list<unknown>({
2700
- prefix: ASSIGNMENT_SAGA_PREFIX,
2701
- });
2702
2389
  // A pending approval is a deadline like any other: the object already owns
2703
2390
  // one alarm, and expiry rides it rather than inventing a second clock.
2704
2391
  const approvals = await transaction.list<unknown>({
@@ -2711,9 +2398,6 @@ export class ShellBotBackendContribution {
2711
2398
  expiries.push(Date.parse(approval.expiresAt));
2712
2399
  }
2713
2400
  return [
2714
- ...[...sagas.values()].map(
2715
- (stored) => requireStoredAssignmentSaga(stored).deadlineAt,
2716
- ),
2717
2401
  ...(await this.routineScheduler.deadlines(transaction)),
2718
2402
  ...expiries.filter((at) => Number.isFinite(at)),
2719
2403
  // A dispatched task's 30-minute lifetime, and a child's own owed Turn,
@@ -2761,38 +2445,12 @@ export class ShellBotBackendContribution {
2761
2445
  private async deferScheduledWork(
2762
2446
  transaction: DurableObjectTransaction,
2763
2447
  ): 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.
2448
+ // A Routine's deadline is a debt, so the scheduler holds it rather than
2449
+ // moving it while other durable work remains in flight.
2775
2450
  await this.routineScheduler.defer(transaction);
2776
2451
  }
2777
2452
 
2778
2453
  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
2454
  await this.settleRoutineFirings();
2797
2455
  await this.runOwedSubagentTurns();
2798
2456
  await this.reconcileOverdueTasks();
@@ -2867,7 +2525,7 @@ export class ShellBotBackendContribution {
2867
2525
  *
2868
2526
  * The Bot Durable Object cannot serialize across a User's Bots — they are
2869
2527
  * separate objects — and the User Durable Object owns the Computer
2870
- * assignment but not the desktop. The host's `control` op is already the
2528
+ * allocation but not the desktop. The host's `control` op is already the
2871
2529
  * single writer that serializes human takeover, so it is where a second
2872
2530
  * opinion cannot exist (plan decision 3): the Bot records the intent, the
2873
2531
  * host grants or refuses, and the refusal names the holder.
@@ -3839,12 +3497,15 @@ export class ShellBotBackendContribution {
3839
3497
  private authoringHost(
3840
3498
  identity: BotIdentity,
3841
3499
  turn: { runId: string; turnId: string },
3500
+ currentToolNames: () => readonly string[],
3501
+ mountedGeneration: () => CompositionGenerationV1 | undefined,
3842
3502
  ): PackageAuthoringHost {
3843
3503
  const artifacts = this.env.APPLICATION_ARTIFACTS;
3844
3504
  return createPackageAuthoringHost({
3845
3505
  storage: {
3846
3506
  get: (key) => this.ctx.storage.get(key),
3847
3507
  put: (entries) => this.ctx.storage.put(entries),
3508
+ list: (options) => this.ctx.storage.list(options),
3848
3509
  },
3849
3510
  composition: this.authority.composition,
3850
3511
  ...(this.env.PACKAGE_BUNDLER
@@ -3859,6 +3520,43 @@ export class ShellBotBackendContribution {
3859
3520
  runId: turn.runId,
3860
3521
  turnId: turn.turnId,
3861
3522
  compatibilityDate: BOT_ISOLATE_COMPATIBILITY_DATE,
3523
+ currentToolNames,
3524
+ mountedGeneration,
3525
+ activationFailures: this.authority.compositionFailures,
3526
+ });
3527
+ }
3528
+
3529
+ /** Catalog reads plus the two-authority mutation seam for one admitted Turn. */
3530
+ private packageCatalogHost(
3531
+ identity: BotIdentity,
3532
+ turn: { runId: string; turnId: string },
3533
+ ): CatalogAwarePackageCatalogHost | undefined {
3534
+ if (!this.env.PACKAGE_CATALOG || !this.env.APPLICATION_ARTIFACTS) {
3535
+ return undefined;
3536
+ }
3537
+ const user = this.userConfiguration(identity);
3538
+ return createPackageCatalogHost({
3539
+ storage: {
3540
+ get: (key) => this.ctx.storage.get(key),
3541
+ put: (entries) => this.ctx.storage.put(entries),
3542
+ },
3543
+ composition: this.authority.composition,
3544
+ catalog: createR2BotPackageCatalogReader(
3545
+ this.env.PACKAGE_CATALOG,
3546
+ this.env.APPLICATION_ARTIFACTS,
3547
+ ),
3548
+ user: {
3549
+ read: () =>
3550
+ user.readConfiguration({
3551
+ schemaVersion: 1,
3552
+ userId: identity.userId,
3553
+ }),
3554
+ execute: (command) => user.executeConfiguration(command),
3555
+ },
3556
+ userId: identity.userId,
3557
+ botId: identity.botId,
3558
+ runId: turn.runId,
3559
+ turnId: turn.turnId,
3862
3560
  });
3863
3561
  }
3864
3562
 
@@ -3883,8 +3581,12 @@ export class ShellBotBackendContribution {
3883
3581
  /** The task a child Turn is running, in a Subagent Durable Object. */
3884
3582
  subagentTaskId?: string;
3885
3583
  },
3584
+ currentToolNames: () => readonly string[] = () => [],
3585
+ mountedGeneration: () => CompositionGenerationV1 | undefined = () =>
3586
+ undefined,
3886
3587
  ): Promise<{
3887
3588
  agentPackages: FoundationAgentPackage[];
3589
+ capabilities: EnabledCapabilityV1[];
3888
3590
  modelSelection: RuntimeModelSelection;
3889
3591
  }> {
3890
3592
  const userConfiguration = this.userConfiguration(identity);
@@ -3896,32 +3598,42 @@ export class ShellBotBackendContribution {
3896
3598
  const packageDefinitions = application.packages.map((pkg) => ({
3897
3599
  packageId: pkg.id,
3898
3600
  version: pkg.version,
3601
+ settings: pkg.manifest.configuration?.settings ?? [],
3899
3602
  capabilities: pkg.manifest.configuration?.capabilities ?? [],
3900
3603
  connectionTypes: pkg.manifest.configuration?.connectionTypes ?? [],
3901
3604
  }));
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
- });
3605
+ const plan = resolveBotExecutionPlanV1({
3606
+ bot: settings,
3607
+ user,
3608
+ packages: packageDefinitions,
3609
+ });
3915
3610
  const readSecret = (name: string) => {
3916
3611
  // SAFETY: Worker secrets are dynamic string bindings not enumerable in Env.
3917
3612
  const value = (this.env as unknown as Record<string, unknown>)[name];
3918
3613
  return typeof value === "string" ? value : undefined;
3919
3614
  };
3920
- const authorizeAssignedConnection = admittedRequest
3921
- ? (assignment: BotSettingsViewV1["assignments"][number]) =>
3922
- this.authorizeAdmittedAssignedEffect(identity, assignment)
3923
- : (assignment: BotSettingsViewV1["assignments"][number]) =>
3924
- this.authorizeAssignedEffect(identity, assignment);
3615
+ const authorizeEnabledConnection = (
3616
+ capability: EnabledCapabilityV1,
3617
+ ): Promise<ConnectionView> => {
3618
+ const enabled = plan.capabilities.some(
3619
+ (candidate) =>
3620
+ candidate.packageId === capability.packageId &&
3621
+ candidate.capabilityId === capability.capabilityId &&
3622
+ candidate.connectionId === capability.connectionId,
3623
+ );
3624
+ const connection = user.connections.find(
3625
+ (candidate) =>
3626
+ candidate.connectionId === capability.connectionId &&
3627
+ candidate.packageId === capability.packageId &&
3628
+ candidate.state === "ready",
3629
+ );
3630
+ if (!enabled || !connection) {
3631
+ return Promise.reject(
3632
+ new Error("Enabled effect is no longer authorized"),
3633
+ );
3634
+ }
3635
+ return Promise.resolve(structuredClone(connection));
3636
+ };
3925
3637
  // The Package-level settings this User holds, resolved against the manifest
3926
3638
  // of the Composition this Turn is pinned to. They come from the same `user`
3927
3639
  // read the rest of this Composition uses, so a value the User changed is
@@ -3929,7 +3641,7 @@ export class ShellBotBackendContribution {
3929
3641
  // one already running.
3930
3642
  const packageSettings = (
3931
3643
  packageId: string,
3932
- ): Record<string, string | number | boolean> => {
3644
+ ): Record<string, PackageSettingValueV1> => {
3933
3645
  const installation = user.packages.find(
3934
3646
  (candidate) => candidate.packageId === packageId,
3935
3647
  );
@@ -3943,6 +3655,15 @@ export class ShellBotBackendContribution {
3943
3655
  installation?.values,
3944
3656
  );
3945
3657
  };
3658
+ const primitivePackageSettings = (
3659
+ packageId: string,
3660
+ ): Record<string, string | number | boolean> =>
3661
+ Object.fromEntries(
3662
+ Object.entries(packageSettings(packageId)).filter(
3663
+ (entry): entry is [string, string | number | boolean] =>
3664
+ typeof entry[1] !== "object",
3665
+ ),
3666
+ );
3946
3667
  // The `image.model` Package setting, already checked against the enum the
3947
3668
  // Image Package's manifest declares.
3948
3669
  const configuredImageModel = packageSettings("image").model;
@@ -3953,20 +3674,36 @@ export class ShellBotBackendContribution {
3953
3674
  const machineSeam = turn ? this.machineSeam(identity) : undefined;
3954
3675
  const messagesGate = machineSeam
3955
3676
  ? await resolveBotMachineMessagesGateV1(
3956
- packageSettings("machine-messages"),
3677
+ primitivePackageSettings("machine-messages"),
3957
3678
  () => machineSeam.list(),
3958
3679
  )
3959
3680
  : ({ status: "off" } as const);
3960
3681
  // Filled in once this Turn's model binding is resolved, below. The tool
3961
3682
  // and the prompt section both read it lazily, from inside the Turn.
3962
3683
  const subagentModels: SubagentModelOptionV1[] = [];
3684
+ const packageCatalog = turn
3685
+ ? this.packageCatalogHost(identity, turn)
3686
+ : undefined;
3687
+ const baseAuthoring = turn
3688
+ ? this.authoringHost(identity, turn, currentToolNames, mountedGeneration)
3689
+ : undefined;
3690
+ const authoring: PackageAuthoringHost | undefined =
3691
+ baseAuthoring && packageCatalog
3692
+ ? {
3693
+ ...baseAuthoring,
3694
+ undo: async (request) =>
3695
+ (await packageCatalog.undoCatalogChange(request)) ??
3696
+ baseAuthoring.undo(request),
3697
+ }
3698
+ : baseAuthoring;
3963
3699
  const resolvedAgentPackages: FoundationAgentPackage[] = [
3964
3700
  ...createFoundationHostedRuntimePackages(application, {
3965
3701
  userId: identity.userId,
3966
3702
  readSecret,
3967
3703
  // A Bot authors a Package only inside an admitted Turn, whose run and
3968
3704
  // session the artifact provenance names.
3969
- ...(turn ? { authoring: this.authoringHost(identity, turn) } : {}),
3705
+ ...(authoring ? { authoring } : {}),
3706
+ ...(packageCatalog ? { packageCatalog } : {}),
3970
3707
  ...(turn
3971
3708
  ? {
3972
3709
  skills: createBotSkillsHost(
@@ -4168,55 +3905,53 @@ export class ShellBotBackendContribution {
4168
3905
  ),
4169
3906
  },
4170
3907
  }),
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,
3908
+ ...(await createFoundationEnabledRuntimePackages(application, plan, {
3909
+ userId: identity.userId,
3910
+ readSecret,
3911
+ authorizeConnection: authorizeEnabledConnection,
3912
+ packageSettings,
3913
+ // Enabled Contributions reach the network through the same
3914
+ // outbound seam the model provider uses, so a deployment that stubs
3915
+ // it stubs every one of them.
3916
+ ...(this.outboundFetch ? { fetch: this.outboundFetch } : {}),
3917
+ leaseCredential: async (
3918
+ capability: EnabledCapabilityV1,
3919
+ effectId: string,
3920
+ expectedGeneration?: string,
3921
+ ): Promise<CredentialLeaseV1> => {
3922
+ if (!capability.connectionId || !expectedGeneration) {
3923
+ throw new Error("Enabled Connection generation is unavailable");
3924
+ }
3925
+ return userConfiguration.leaseToolCredential(
3926
+ identity.userId,
3927
+ capability.connectionId,
4186
3928
  effectId,
4187
3929
  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),
3930
+ );
3931
+ },
3932
+ settleCredential: async (
3933
+ capability: EnabledCapabilityV1,
3934
+ effectId: string,
3935
+ ): Promise<void> => {
3936
+ if (!capability.connectionId) return;
3937
+ await userConfiguration.settleToolCredential(
3938
+ identity.userId,
3939
+ capability.connectionId,
3940
+ effectId,
3941
+ );
4212
3942
  },
4213
- )),
3943
+ // A mount that could not reach its server writes that down where
3944
+ // the User can read it. The Bot holds no MCP record; the User
3945
+ // Durable Object that owns the Connection does.
3946
+ recordOutcome: (outcome: McpMountOutcomeReportV1) =>
3947
+ userConfiguration.recordMcpMountOutcome(identity.userId, outcome),
3948
+ })),
4214
3949
  ];
4215
3950
  const agentPackages: FoundationAgentPackage[] =
4216
3951
  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.
3952
+ // One generic resolver owns precedence: enabled Bot-scoped Package value,
3953
+ // enabled User-scoped Package value, then the platform model. The kernel
3954
+ // names no Package (AGENTS.md Configuration shape; ADR 0019).
4220
3955
  const effective = resolveEffectiveBotModelV1({
4221
3956
  bot: settings,
4222
3957
  user,
@@ -4224,64 +3959,37 @@ export class ShellBotBackendContribution {
4224
3959
  });
4225
3960
  const effectiveModel = effective.model;
4226
3961
  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,
3962
+ throw new Error(
3963
+ effective.binding?.failure ??
3964
+ "No model is configured; enable a model Package or restore the platform model",
4243
3965
  );
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
3966
  }
3967
+ const binding: ResolvedModelBindingV1 = effective.binding ?? {
3968
+ model: structuredClone(effectiveModel),
3969
+ state: "unavailable",
3970
+ failure: "The resolved model Connection is unavailable",
3971
+ };
4278
3972
  if (
4279
3973
  binding.state === "unavailable" ||
4280
3974
  !binding.connection ||
4281
3975
  !binding.providerType ||
4282
3976
  !binding.packageId
4283
3977
  ) {
4284
- throw new Error(binding.failure ?? "Bot model Connection is unavailable");
3978
+ throw new Error(
3979
+ binding.failure ?? "The resolved model Connection is unavailable",
3980
+ );
3981
+ }
3982
+ if (
3983
+ admittedRequest &&
3984
+ (admittedRequest.provider !== binding.providerType ||
3985
+ admittedRequest.model !== effectiveModel.providerModelId ||
3986
+ admittedRequest.modelBinding?.connectionId !==
3987
+ binding.connection.connectionId ||
3988
+ !admittedRequest.modelBinding.connectionGeneration ||
3989
+ admittedRequest.modelBinding.connectionGeneration !==
3990
+ binding.connection.generation)
3991
+ ) {
3992
+ throw new Error("Admitted model binding is unavailable");
4285
3993
  }
4286
3994
  const bindingPackageId = binding.packageId;
4287
3995
  agentPackages.push(
@@ -4312,27 +4020,28 @@ export class ShellBotBackendContribution {
4312
4020
  bindingPackageId,
4313
4021
  effectId,
4314
4022
  ),
4315
- ...(this.env.AI
4023
+ ...(this.env.FLOCK_AI
4316
4024
  ? {
4317
- runWorkersAi: (model, input) => this.env.AI!.run(model, input),
4025
+ flockAiAutoRoute: this.env.FLOCK_AI.autoRoute,
4026
+ runFlockAiChatCompletion: (gatewayModel, body) =>
4027
+ this.env.FLOCK_AI!.runChatCompletion(gatewayModel, body),
4318
4028
  }
4319
4029
  : {}),
4320
4030
  fetch: this.outboundFetch,
4321
4031
  }),
4322
4032
  );
4323
4033
  // 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(
4034
+ // `Task` call may name. They come from User enablement as resolved for this
4035
+ // Turn — never anything the Bot claimed about a model.
4036
+ const modelCapability = plan.capabilities.find(
4327
4037
  (candidate) =>
4328
- candidate.state === "enabled" &&
4038
+ candidate.kind === "model" &&
4329
4039
  candidate.connectionId === binding.connection!.connectionId,
4330
4040
  );
4331
- if (modelAssignment) {
4041
+ if (modelCapability) {
4332
4042
  const subagentBinding = {
4333
- assignmentId: modelAssignment.assignmentId,
4334
- packageId: modelAssignment.packageId,
4335
- capabilityId: modelAssignment.capabilityId,
4043
+ packageId: modelCapability.packageId,
4044
+ capabilityId: modelCapability.capabilityId,
4336
4045
  connectionId: binding.connection.connectionId,
4337
4046
  provider: binding.providerType,
4338
4047
  providerModelId: effectiveModel.providerModelId,
@@ -4342,7 +4051,7 @@ export class ShellBotBackendContribution {
4342
4051
  };
4343
4052
  subagentModels.push(
4344
4053
  ...subagentModelCatalogV1({
4345
- assignments: [subagentBinding],
4054
+ bindings: [subagentBinding],
4346
4055
  defaultBinding: subagentBinding,
4347
4056
  turnType: turn?.turnType ?? "chat",
4348
4057
  }),
@@ -4353,6 +4062,7 @@ export class ShellBotBackendContribution {
4353
4062
  // Cloud is both the model provider and the `web_search` Capability — and
4354
4063
  // the runtime resolves one Plugin per Contribution specifier.
4355
4064
  agentPackages: mergeFoundationRuntimePackagesV1(agentPackages),
4065
+ capabilities: structuredClone(plan.capabilities),
4356
4066
  modelSelection: {
4357
4067
  provider: binding.providerType,
4358
4068
  model: effectiveModel.providerModelId,
@@ -4372,77 +4082,6 @@ export class ShellBotBackendContribution {
4372
4082
  };
4373
4083
  }
4374
4084
 
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
4085
  async readDurableIdentity(): Promise<BotIdentity | undefined> {
4447
4086
  return this.authority.readDurableIdentity();
4448
4087
  }
@@ -4883,15 +4522,17 @@ export class ShellBotBackendContribution {
4883
4522
  return quarantine === undefined ? {} : { quarantine };
4884
4523
  }
4885
4524
 
4886
- /**
4887
- * SEAM — plan Step 5 (authoring) owns `authorship:intent:<effectId>` and
4888
- * `artifact:<contentHash>`. Those records do not exist yet, so an isolate
4889
- * member has no recorded source to show and the view carries members only.
4890
- */
4891
- private readCompositionMemberSource(
4892
- _member: CompositionMemberV1,
4525
+ /** Reads the immutable TypeScript source retained beside an authored artifact. */
4526
+ private async readCompositionMemberSource(
4527
+ member: CompositionMemberV1,
4893
4528
  ): Promise<string | undefined> {
4894
- return Promise.resolve(undefined);
4529
+ const bucket = this.env.APPLICATION_ARTIFACTS;
4530
+ if (!bucket) return undefined;
4531
+ return readAuthoredCompositionMemberSourceV1({
4532
+ storage: { get: (key) => this.ctx.storage.get(key) },
4533
+ artifacts: createR2AuthoringArtifactStore(bucket),
4534
+ member,
4535
+ });
4895
4536
  }
4896
4537
 
4897
4538
  /**
@@ -5127,8 +4768,8 @@ export class ShellBotBackendContribution {
5127
4768
  }
5128
4769
 
5129
4770
  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
4771
+ // One alarm: the kernel defers while work is in flight, settles Package
4772
+ // scheduled work, and recovers the active run. A run left
5132
4773
  // durably `reconciliation-required` stays scheduled and visible; only an
5133
4774
  // explicit resume retrieves the original effect, so the alarm never
5134
4775
  // terminalizes an uncertain outcome on its own.
@@ -5426,11 +5067,8 @@ export class ShellBotBackendContribution {
5426
5067
  await this.authority.fenceRunAdmission(identity, query.runId),
5427
5068
  );
5428
5069
  }
5429
- private initialBotSettings(
5430
- botId: string,
5431
- model?: BotSettingsViewV1["model"],
5432
- ): BotSettingsViewV1 {
5433
- return initializeBotSettingsV1(botId, model);
5070
+ private initialBotSettings(botId: string): BotSettingsViewV1 {
5071
+ return initializeBotSettingsV1(botId);
5434
5072
  }
5435
5073
 
5436
5074
  private userConfiguration(identity: BotIdentity): {
@@ -5438,6 +5076,9 @@ export class ShellBotBackendContribution {
5438
5076
  schemaVersion: 1;
5439
5077
  userId: string;
5440
5078
  }): Promise<UserSettingsViewV1>;
5079
+ executeConfiguration(
5080
+ input: Extract<ConfigurationCommandV1, { type: `user/${string}` }>,
5081
+ ): Promise<OperationReceiptV1>;
5441
5082
  readPackageRevisions(
5442
5083
  userId: string,
5443
5084
  ): ReturnType<PackagePublisherAgentHost["read"]>;
@@ -5449,40 +5090,6 @@ export class ShellBotBackendContribution {
5449
5090
  userId: string,
5450
5091
  command: Parameters<PackagePublisherAgentHost["rollback"]>[0],
5451
5092
  ): 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
5093
  leaseModelCredential(
5487
5094
  userId: string,
5488
5095
  connectionId: string,
@@ -5543,6 +5150,7 @@ export class ShellBotBackendContribution {
5543
5150
  // SAFETY: this namespace is bound to UserConfiguration; generated Worker types do not expose its RPC surface.
5544
5151
  const rpc = this.env.USER_CONFIGURATIONS.get(id) as unknown as {
5545
5152
  readConfiguration(input: unknown): Promise<UserSettingsViewV1>;
5153
+ executeConfiguration(input: unknown): Promise<unknown>;
5546
5154
  readPackageRevisions(
5547
5155
  input: unknown,
5548
5156
  ): ReturnType<PackagePublisherAgentHost["read"]>;
@@ -5552,16 +5160,6 @@ export class ShellBotBackendContribution {
5552
5160
  rollbackPackage(
5553
5161
  input: unknown,
5554
5162
  ): 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
5163
  leaseModelCredential(input: unknown): Promise<unknown>;
5566
5164
  settleModelCredential(input: unknown): Promise<void>;
5567
5165
  leaseToolCredential(input: unknown): Promise<unknown>;
@@ -5579,6 +5177,14 @@ export class ShellBotBackendContribution {
5579
5177
  };
5580
5178
  return {
5581
5179
  readConfiguration: (input) => rpc.readConfiguration(input),
5180
+ executeConfiguration: async (command) =>
5181
+ decodeOperationReceiptV1(
5182
+ await rpc.executeConfiguration({
5183
+ schemaVersion: 1,
5184
+ userId: identity.userId,
5185
+ command,
5186
+ }),
5187
+ ),
5582
5188
  // A machine is a User asset, so every one of these crosses the seam and
5583
5189
  // is decoded on arrival rather than trusted in the shape RPC returned.
5584
5190
  listMachines: async (userId) =>
@@ -5617,8 +5223,6 @@ export class ShellBotBackendContribution {
5617
5223
  rpc.publishPackage({ schemaVersion: 1, userId, command }),
5618
5224
  rollbackPackage: (userId, command) =>
5619
5225
  rpc.rollbackPackage({ schemaVersion: 1, userId, command }),
5620
- getConnection: (userId, connectionId) =>
5621
- rpc.getConnection({ schemaVersion: 1, userId, connectionId }),
5622
5226
  // Flock state crosses a Durable Object seam, so it decodes on arrival
5623
5227
  // rather than being trusted in the shape RPC happened to return.
5624
5228
  listBots: async (userId) =>
@@ -5635,57 +5239,6 @@ export class ShellBotBackendContribution {
5635
5239
  command,
5636
5240
  }),
5637
5241
  ),
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
5242
  leaseModelCredential: async (
5690
5243
  userId,
5691
5244
  connectionId,
@@ -5752,94 +5305,10 @@ export class ShellBotBackendContribution {
5752
5305
  identity: BotIdentity,
5753
5306
  ): Promise<BotSettingsViewV1> {
5754
5307
  await this.validateIdentity(identity);
5755
- const existing = await this.ctx.storage.get<BotSettingsViewV1>(
5756
- BOT_CONFIGURATION_KEY,
5757
- );
5758
- if (!existing)
5308
+ const stored = await this.ctx.storage.get<unknown>(BOT_CONFIGURATION_KEY);
5309
+ if (stored === undefined)
5759
5310
  throw new Error(`Bot "${identity.botId}" is not materialized`);
5760
- return existing;
5761
- }
5762
-
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);
5311
+ return decodeBotSettingsViewV1(migrateStoredBotSettingsV1(stored));
5843
5312
  }
5844
5313
 
5845
5314
  private async resolveExecutionContext(identity: BotIdentity): Promise<{
@@ -5847,82 +5316,30 @@ export class ShellBotBackendContribution {
5847
5316
  user: UserSettingsViewV1;
5848
5317
  plan: BotExecutionPlanV1;
5849
5318
  }> {
5850
- let settings = await this.ensureBotSettings(identity);
5319
+ const settings = await this.ensureBotSettings(identity);
5851
5320
  const user = await this.userConfiguration(identity).readConfiguration({
5852
5321
  schemaVersion: 1,
5853
5322
  userId: identity.userId,
5854
5323
  });
5855
5324
  const application = await this.compileApplication();
5856
- let plan = resolveBotExecutionPlanV1({
5325
+ const plan = resolveBotExecutionPlanV1({
5857
5326
  bot: settings,
5858
5327
  user,
5859
5328
  packages: application.packages.map((pkg) => ({
5860
5329
  packageId: pkg.id,
5861
5330
  version: pkg.version,
5331
+ settings: pkg.manifest.configuration?.settings ?? [],
5862
5332
  capabilities: pkg.manifest.configuration?.capabilities ?? [],
5863
5333
  connectionTypes: pkg.manifest.configuration?.connectionTypes ?? [],
5864
5334
  })),
5865
5335
  });
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
5336
  return { settings, user, plan };
5910
5337
  }
5911
5338
 
5912
5339
  async archiveEligible(storage: {
5913
5340
  get<T>(key: string): Promise<T | undefined>;
5914
- list<T>(options: { prefix: string }): Promise<Map<string, T>>;
5915
5341
  }): 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
- );
5342
+ return (await storage.get<string>(ACTIVE_RUN_KEY)) === undefined;
5926
5343
  }
5927
5344
 
5928
5345
  async assertLifecycleActive(botId: string): Promise<void> {