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