@frockbot/plugin-shell 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/frockbot.json +4 -0
- package/package.json +30 -28
- package/src/agent.ts +10 -13
- package/src/backend-authoring.test.ts +356 -2
- package/src/backend-authoring.ts +585 -20
- package/src/backend-composition-input.test.ts +65 -0
- package/src/backend-composition-input.ts +58 -0
- package/src/backend-composition.ts +23 -5
- package/src/backend-configuration.test.ts +549 -1468
- package/src/backend-contracts.test.ts +28 -0
- package/src/backend-contracts.ts +3 -1
- package/src/backend-execution.ts +0 -4
- package/src/backend-iframe-ui.test.ts +131 -0
- package/src/backend-image.test.ts +8 -8
- package/src/backend-image.ts +13 -13
- package/src/backend-isolate.test.ts +230 -89
- package/src/backend-isolate.ts +103 -200
- package/src/backend-package-catalog.test.ts +451 -0
- package/src/backend-package-catalog.ts +923 -0
- package/src/backend-recovery-integration.test.ts +17 -67
- package/src/backend-routines.ts +1 -1
- package/src/backend-runner-iframe.test.ts +74 -0
- package/src/backend-runner.ts +192 -0
- package/src/backend.ts +1198 -1781
- package/src/client/FrockBotApp.vue +44 -73
- package/src/client/PackageIframeHost.vue +218 -0
- package/src/client/PackageIframeSettings.vue +52 -0
- package/src/client/SendPayloadView.vue +0 -60
- package/src/client/index.test.ts +439 -380
- package/src/client/index.ts +163 -257
- package/src/client/model-presentation.test.ts +21 -8
- package/src/client/model-presentation.ts +14 -7
- package/src/client/package-iframe-host-message.test.ts +41 -0
- package/src/client/package-iframe-host-message.ts +27 -0
- package/src/client/styles.css +0 -27
- package/src/composition-views.ts +54 -0
- package/src/settings-links.test.ts +2 -10
- package/src/settings-links.ts +1 -13
- package/src/shared.ts +10 -13
- package/src/backend-assignment.test.ts +0 -161
- package/src/backend-assignment.ts +0 -274
package/src/backend.ts
CHANGED
|
@@ -1,17 +1,41 @@
|
|
|
1
1
|
import type { AgentEffectAdmission } from "@frockbot/kernel-agent-loop/agent";
|
|
2
2
|
import {
|
|
3
|
+
decodeIsolateMemoryReadRequestV1,
|
|
4
|
+
decodeIsolateMemoryWriteRequestV1,
|
|
5
|
+
decodeIsolateNotificationRequestV1,
|
|
6
|
+
decodeIsolateScheduleRequestV1,
|
|
7
|
+
decodeIsolateToolRequestV1,
|
|
8
|
+
decodeIsolateWorkspaceDeleteRequestV1,
|
|
9
|
+
decodeIsolateWorkspaceListRequestV1,
|
|
10
|
+
decodeIsolateWorkspacePathV1,
|
|
11
|
+
decodeIsolateWorkspaceWriteRequestV1,
|
|
12
|
+
decodeWorkspacePathV1,
|
|
13
|
+
decodeWorkspaceRootV1,
|
|
3
14
|
decodeSessionEvent,
|
|
15
|
+
type IsolateConnectionOutcomeV1,
|
|
16
|
+
type IsolateConnectionV1,
|
|
17
|
+
type IsolateMemoryOutcomeV1,
|
|
18
|
+
type IsolateNotificationOutcomeV1,
|
|
19
|
+
type IsolateToolOutcomeV1,
|
|
20
|
+
type IsolateWorkspaceOutcomeV1,
|
|
4
21
|
type PersistSessionEvents,
|
|
5
22
|
type SessionEvent,
|
|
23
|
+
type WorkspacePathV1,
|
|
6
24
|
validateToolOccurrenceJournal,
|
|
7
25
|
type BotCapabilitiesStub,
|
|
8
26
|
type IsolateModelInvocationV1,
|
|
9
|
-
type IsolatePendingDecisionV1,
|
|
10
27
|
type NormalizedModelRequest,
|
|
11
28
|
type PackageBundlerBinding,
|
|
29
|
+
type PackageIframeCompositionV1,
|
|
30
|
+
type PackageIframeToolCommandV1,
|
|
12
31
|
type TurnTypeV1,
|
|
13
32
|
type WorkspaceFilesV1,
|
|
14
33
|
} from "@frockbot/kernel-contracts";
|
|
34
|
+
import {
|
|
35
|
+
decodeFrockBotManifest,
|
|
36
|
+
isClientIframeContribution,
|
|
37
|
+
type FrockBotManifest,
|
|
38
|
+
} from "@frockbot/kernel-composition";
|
|
15
39
|
import type { Plugin } from "cordis";
|
|
16
40
|
import {
|
|
17
41
|
ACTIVE_RUN_KEY,
|
|
@@ -42,45 +66,43 @@ import {
|
|
|
42
66
|
} from "@frockbot/application-foundation/runtime";
|
|
43
67
|
import {
|
|
44
68
|
applyBotProfilePatchV1,
|
|
45
|
-
capabilityAssignmentFailureV1,
|
|
46
69
|
configurationCommandFingerprintV1,
|
|
47
70
|
ConfigurationConflictError,
|
|
48
71
|
decodeBotConfigurationExecuteRpcV1,
|
|
49
72
|
decodeBotConfigurationReadRpcV1,
|
|
73
|
+
decodeBotSettingsViewV1,
|
|
50
74
|
decodeCompositionCommandReceiptV1,
|
|
75
|
+
decodeOperationReceiptV1,
|
|
76
|
+
decodeInstalledPackageSettingIdsV1,
|
|
77
|
+
decodeInstalledPackageSettingsPatchV1,
|
|
51
78
|
MAX_COMPOSITION_GENERATION_PAGE_V1,
|
|
52
79
|
type CompositionCommandReceiptV1,
|
|
53
80
|
type CompositionGenerationListViewV1,
|
|
54
81
|
type CompositionGenerationViewV1,
|
|
55
82
|
type RevertCompositionCommandV1,
|
|
56
|
-
type ConnectionDependencyRequirementV1,
|
|
57
83
|
type BotExecutionPlanV1,
|
|
58
84
|
type BotSelfWriterV1,
|
|
59
85
|
type BotSettingsViewV1,
|
|
60
|
-
type
|
|
61
|
-
type ModelAssignment,
|
|
86
|
+
type EnabledCapabilityV1,
|
|
62
87
|
type ConnectionView,
|
|
63
88
|
type ConfigurationCommandV1,
|
|
64
89
|
type OperationReceiptV1,
|
|
90
|
+
type PackageSettingValueV1,
|
|
65
91
|
type ResolvedModelBindingV1,
|
|
66
92
|
initializeBotSettingsV1,
|
|
93
|
+
migrateStoredBotSettingsV1,
|
|
67
94
|
resolvePackageSettingValuesV1,
|
|
68
95
|
resolveBotExecutionPlanV1,
|
|
69
|
-
resolveBotModelBindingV1,
|
|
70
96
|
resolveEffectiveBotModelV1,
|
|
71
97
|
type UserSettingsViewV1,
|
|
72
98
|
} from "@frockbot/configuration-core";
|
|
73
99
|
import {
|
|
74
|
-
|
|
100
|
+
createFoundationEnabledRuntimePackages,
|
|
75
101
|
mergeFoundationRuntimePackages,
|
|
76
102
|
createFoundationHostedRuntimePackages,
|
|
77
103
|
mergeFoundationRuntimePackagesV1,
|
|
78
104
|
type PackagePublisherAgentHost,
|
|
79
105
|
} from "@frockbot/application-foundation/runtime";
|
|
80
|
-
import {
|
|
81
|
-
requireStoredAssignmentSaga,
|
|
82
|
-
type StoredAssignmentSaga,
|
|
83
|
-
} from "./backend-assignment.js";
|
|
84
106
|
import {
|
|
85
107
|
cancelStoredRun,
|
|
86
108
|
completeStoredRun,
|
|
@@ -100,6 +122,7 @@ import {
|
|
|
100
122
|
type ShellIsolateMountOptions,
|
|
101
123
|
type ShellMountedComposition,
|
|
102
124
|
} from "./backend-composition.js";
|
|
125
|
+
import { compositionFailureTurnTextV1 } from "./backend-composition-input.js";
|
|
103
126
|
import {
|
|
104
127
|
activateCompositionV1,
|
|
105
128
|
type CompositionFailureV1,
|
|
@@ -109,7 +132,13 @@ import {
|
|
|
109
132
|
import {
|
|
110
133
|
createPackageAuthoringHost,
|
|
111
134
|
createR2AuthoringArtifactStore,
|
|
135
|
+
readAuthoredCompositionMemberSourceV1,
|
|
112
136
|
} from "./backend-authoring.js";
|
|
137
|
+
import {
|
|
138
|
+
type CatalogAwarePackageCatalogHost,
|
|
139
|
+
createPackageCatalogHost,
|
|
140
|
+
createR2BotPackageCatalogReader,
|
|
141
|
+
} from "./backend-package-catalog.js";
|
|
113
142
|
import { createBotComputerSyncHost } from "./backend-computer.js";
|
|
114
143
|
import {
|
|
115
144
|
decodeDirectoryViewV1,
|
|
@@ -125,10 +154,7 @@ import {
|
|
|
125
154
|
type TemplateShareReceiptV1,
|
|
126
155
|
} from "@frockbot/plugin-bot-template/shared";
|
|
127
156
|
import { createBotMemoryHost } from "./backend-memory.js";
|
|
128
|
-
import {
|
|
129
|
-
createBotImageHost,
|
|
130
|
-
type WorkersAiBindingV1,
|
|
131
|
-
} from "./backend-image.js";
|
|
157
|
+
import { createBotImageHost, type NativeAiBindingV1 } from "./backend-image.js";
|
|
132
158
|
import {
|
|
133
159
|
createBotPluginSkillsSource,
|
|
134
160
|
createBotSkillCatalogReader,
|
|
@@ -288,7 +314,9 @@ import type {
|
|
|
288
314
|
RoutineRunListViewV1,
|
|
289
315
|
} from "@frockbot/plugin-routines/shared";
|
|
290
316
|
import {
|
|
317
|
+
authorshipManifestKey,
|
|
291
318
|
decodeAuthoringQuotaReceiptV1,
|
|
319
|
+
type AuthoredManifestRecordV1,
|
|
292
320
|
type AuthoringQuotaBinding,
|
|
293
321
|
type PackageAuthoringHost,
|
|
294
322
|
} from "@frockbot/plugin-authoring";
|
|
@@ -298,13 +326,12 @@ import {
|
|
|
298
326
|
createR2PackageArtifactStore,
|
|
299
327
|
isolateBindingDigestV1,
|
|
300
328
|
type BotCapabilitiesPropsV1,
|
|
301
|
-
type IsolateAssignmentV1,
|
|
302
329
|
type IsolateCapabilityHost,
|
|
303
330
|
type IsolateModelBindingV1,
|
|
304
331
|
type IsolateModelPath,
|
|
305
332
|
type IsolateModelRequestRecordV1,
|
|
306
|
-
type IsolatePendingAuthorityDecisionV1,
|
|
307
333
|
} from "./backend-isolate.js";
|
|
334
|
+
import { memoryScopeRootV1 } from "@frockbot/plugin-memory/roots";
|
|
308
335
|
import type { BotIsolateLoader } from "@frockbot/kernel-composition/isolate";
|
|
309
336
|
import type {
|
|
310
337
|
CompositionGenerationV1,
|
|
@@ -317,8 +344,11 @@ import {
|
|
|
317
344
|
type McpMountOutcomeReportV1,
|
|
318
345
|
type McpServerStatusViewV1,
|
|
319
346
|
} from "@frockbot/plugin-mcp/records";
|
|
320
|
-
import {
|
|
321
|
-
|
|
347
|
+
import {
|
|
348
|
+
projectCompositionGenerationV1,
|
|
349
|
+
projectPackageIframeCompositionV1,
|
|
350
|
+
} from "./composition-views.js";
|
|
351
|
+
import { executeBotTurn, executeDirectToolTurn } from "./backend-runner.js";
|
|
322
352
|
import { shellTerminalRecordsV1 } from "./terminal-records.js";
|
|
323
353
|
import {
|
|
324
354
|
CLIENT_RUN_LIST_MAX_BYTES,
|
|
@@ -379,10 +409,6 @@ import {
|
|
|
379
409
|
|
|
380
410
|
export const BOT_CONFIGURATION_KEY = "bot-configuration";
|
|
381
411
|
const CONFIGURATION_RECEIPT_PREFIX = "configuration-receipt:";
|
|
382
|
-
const ASSIGNMENT_GENERATION_PREFIX = "assignment-generation:";
|
|
383
|
-
const ASSIGNMENT_COMPENSATION_PREFIX = "assignment-compensation:";
|
|
384
|
-
const ASSIGNMENT_TOMBSTONE_PREFIX = "assignment-tombstone:";
|
|
385
|
-
const ASSIGNMENT_SAGA_PREFIX = "assignment-saga:";
|
|
386
412
|
const STOP_RECEIPT_PREFIX = "stop-receipt:";
|
|
387
413
|
/**
|
|
388
414
|
* The Bot's durable announcement log: Session events that happen outside any
|
|
@@ -396,18 +422,9 @@ export const BOT_ANNOUNCEMENT_RETENTION = 32;
|
|
|
396
422
|
function botAnnouncementKey(seq: number): string {
|
|
397
423
|
return `${BOT_ANNOUNCEMENT_PREFIX}${String(seq).padStart(12, "0")}`;
|
|
398
424
|
}
|
|
399
|
-
const ASSIGNMENT_SAGA_DEADLINE_MS = 60_000;
|
|
400
425
|
/** Idempotency records for Composition commands this Package admits. */
|
|
401
426
|
const COMPOSITION_COMMAND_PREFIX = "composition-command:";
|
|
402
427
|
|
|
403
|
-
function assignmentGenerationKey(assignmentId: string): string {
|
|
404
|
-
return `${ASSIGNMENT_GENERATION_PREFIX}${assignmentId}`;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
function capabilityKey(packageId: string, capabilityId: string): string {
|
|
408
|
-
return `${packageId}:${capabilityId}`;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
428
|
interface StoredConfigurationReceipt {
|
|
412
429
|
commandFingerprint: string;
|
|
413
430
|
receipt: OperationReceiptV1;
|
|
@@ -428,11 +445,22 @@ function isTerminalStoredRunStatus(status: StoredRunStatus): boolean {
|
|
|
428
445
|
);
|
|
429
446
|
}
|
|
430
447
|
|
|
431
|
-
interface
|
|
448
|
+
interface ConfigurationActivity {
|
|
432
449
|
commandFingerprint: string;
|
|
433
450
|
promise: Promise<OperationReceiptV1>;
|
|
434
451
|
}
|
|
435
452
|
|
|
453
|
+
interface IsolateCallScopeV1 {
|
|
454
|
+
userId: string;
|
|
455
|
+
botId: string;
|
|
456
|
+
runId: string;
|
|
457
|
+
sessionId: string;
|
|
458
|
+
turnId: string;
|
|
459
|
+
packageId: string;
|
|
460
|
+
generationId: string;
|
|
461
|
+
request: unknown;
|
|
462
|
+
}
|
|
463
|
+
|
|
436
464
|
function requireMatchingConfigurationReceipt(
|
|
437
465
|
stored: StoredConfigurationReceipt,
|
|
438
466
|
commandFingerprint: string,
|
|
@@ -450,6 +478,9 @@ export type { BotIdentity, OwnedBotTurnCommand };
|
|
|
450
478
|
|
|
451
479
|
export interface BotStateEnv {
|
|
452
480
|
MEMORY_FILES: R2Bucket;
|
|
481
|
+
/** Object-storage file surfaces constructed by the Cloudflare adapter. */
|
|
482
|
+
WORKSPACE_FILES?: WorkspaceFilesV1;
|
|
483
|
+
MEMORY_WORKSPACE_FILES?: WorkspaceFilesV1;
|
|
453
484
|
/**
|
|
454
485
|
* The Bot Package Worker Loader (plan Step 4). Optional so a host without
|
|
455
486
|
* Bot-authored Packages — tests, the Electron shell — still compiles; a
|
|
@@ -471,8 +502,16 @@ export interface BotStateEnv {
|
|
|
471
502
|
*/
|
|
472
503
|
PACKAGE_BUNDLER?: PackageBundlerBinding;
|
|
473
504
|
MEMORY_INDEX: VectorizeIndex;
|
|
474
|
-
/** The native
|
|
475
|
-
AI?:
|
|
505
|
+
/** The native AI binding consumed through the image Package adapter. */
|
|
506
|
+
AI?: NativeAiBindingV1;
|
|
507
|
+
/** The Flock AI Gateway adapter constructed by the Cloudflare host. */
|
|
508
|
+
FLOCK_AI?: {
|
|
509
|
+
autoRoute: string;
|
|
510
|
+
runChatCompletion(
|
|
511
|
+
gatewayModel: string,
|
|
512
|
+
body: Record<string, unknown>,
|
|
513
|
+
): Promise<ReadableStream<Uint8Array>>;
|
|
514
|
+
};
|
|
476
515
|
USER_CONFIGURATIONS: DurableObjectNamespace;
|
|
477
516
|
/**
|
|
478
517
|
* The Bot Durable Object namespace, as the Subagent Durable Object namespace
|
|
@@ -535,6 +574,29 @@ function optionalStoredRun(input: unknown): StoredRun | undefined {
|
|
|
535
574
|
return input === undefined ? undefined : requireStoredRunV1(input);
|
|
536
575
|
}
|
|
537
576
|
|
|
577
|
+
/** Server-side allowlist for the untrusted page's only effectful message. */
|
|
578
|
+
export function requirePackageUiToolDeclarationV1(
|
|
579
|
+
catalog: PackageIframeCompositionV1,
|
|
580
|
+
command: Pick<
|
|
581
|
+
PackageIframeToolCommandV1,
|
|
582
|
+
"generationId" | "packageId" | "name"
|
|
583
|
+
>,
|
|
584
|
+
): PackageIframeCompositionV1["contributions"][number] {
|
|
585
|
+
const contribution = catalog.contributions.find(
|
|
586
|
+
(candidate) => candidate.packageId === command.packageId,
|
|
587
|
+
);
|
|
588
|
+
if (
|
|
589
|
+
catalog.generationId !== command.generationId ||
|
|
590
|
+
!contribution ||
|
|
591
|
+
!contribution.declaredTools.includes(command.name)
|
|
592
|
+
) {
|
|
593
|
+
throw new Error(
|
|
594
|
+
`Package "${command.packageId}" did not declare tool "${command.name}" in generation "${command.generationId}"`,
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
return contribution;
|
|
598
|
+
}
|
|
599
|
+
|
|
538
600
|
export class ShellBotBackendContribution {
|
|
539
601
|
readonly ctx: DurableObjectState;
|
|
540
602
|
readonly env: BotStateEnv;
|
|
@@ -545,10 +607,24 @@ export class ShellBotBackendContribution {
|
|
|
545
607
|
Promise<ClientTurnV1>
|
|
546
608
|
>();
|
|
547
609
|
private readonly outboundFetch?: typeof fetch;
|
|
548
|
-
private readonly
|
|
610
|
+
private readonly configurationActivities = new Map<
|
|
611
|
+
string,
|
|
612
|
+
ConfigurationActivity
|
|
613
|
+
>();
|
|
549
614
|
/** The Turn currently executing on this object, for durable Stop. */
|
|
550
615
|
private activeTurn:
|
|
551
|
-
|
|
616
|
+
| {
|
|
617
|
+
runId: string;
|
|
618
|
+
sessionId: string;
|
|
619
|
+
turnId: string;
|
|
620
|
+
generationId: string;
|
|
621
|
+
turnType: TurnTypeV1;
|
|
622
|
+
subagentRole?: string;
|
|
623
|
+
mounted: ShellMountedComposition;
|
|
624
|
+
signal: AbortSignal;
|
|
625
|
+
cancel(): void;
|
|
626
|
+
}
|
|
627
|
+
| undefined;
|
|
552
628
|
/**
|
|
553
629
|
* Admission, the event log, the cursor, idempotency, cancellation, and
|
|
554
630
|
* durable scheduling are kernel authority; this Package supplies only the
|
|
@@ -620,7 +696,7 @@ export class ShellBotBackendContribution {
|
|
|
620
696
|
terminalRecords: (input) => this.terminalPackageRecords(input),
|
|
621
697
|
scheduledDeadlines: (transaction) =>
|
|
622
698
|
this.scheduledDeadlines(transaction),
|
|
623
|
-
scheduledWorkInFlight: () =>
|
|
699
|
+
scheduledWorkInFlight: () => false,
|
|
624
700
|
deferScheduledWork: (transaction) =>
|
|
625
701
|
this.deferScheduledWork(transaction),
|
|
626
702
|
settleScheduledWork: () => this.settleScheduledWork(),
|
|
@@ -634,12 +710,6 @@ export class ShellBotBackendContribution {
|
|
|
634
710
|
name: string;
|
|
635
711
|
/** The persona the Bot's profile is seeded with, when its creator gave one. */
|
|
636
712
|
description?: string;
|
|
637
|
-
model?: BotSettingsViewV1["model"];
|
|
638
|
-
modelBinding?: {
|
|
639
|
-
assignment: BotSettingsViewV1["assignments"][number];
|
|
640
|
-
generation: string;
|
|
641
|
-
};
|
|
642
|
-
assignments?: BotSettingsViewV1["assignments"];
|
|
643
713
|
},
|
|
644
714
|
): Promise<BotSettingsViewV1> {
|
|
645
715
|
return this.ctx.storage.transaction(async (transaction) => {
|
|
@@ -651,63 +721,29 @@ export class ShellBotBackendContribution {
|
|
|
651
721
|
) {
|
|
652
722
|
throw new Error("Bot authority does not match its durable identity");
|
|
653
723
|
}
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
724
|
+
const stored = await transaction.get<unknown>(BOT_CONFIGURATION_KEY);
|
|
725
|
+
if (stored !== undefined) {
|
|
726
|
+
return decodeBotSettingsViewV1(migrateStoredBotSettingsV1(stored));
|
|
727
|
+
}
|
|
658
728
|
const settings = {
|
|
659
|
-
...this.initialBotSettings(identity.botId
|
|
729
|
+
...this.initialBotSettings(identity.botId),
|
|
660
730
|
profile: {
|
|
661
731
|
name: initial.name,
|
|
662
732
|
...(initial.description === undefined
|
|
663
733
|
? {}
|
|
664
734
|
: { description: initial.description }),
|
|
665
735
|
},
|
|
666
|
-
assignments: [
|
|
667
|
-
...(initial.assignments ?? []).map((assignment) =>
|
|
668
|
-
structuredClone(assignment),
|
|
669
|
-
),
|
|
670
|
-
...(initial.modelBinding
|
|
671
|
-
? [structuredClone(initial.modelBinding.assignment)]
|
|
672
|
-
: []),
|
|
673
|
-
],
|
|
674
736
|
} satisfies BotSettingsViewV1;
|
|
675
737
|
await transaction.put({
|
|
676
738
|
[IDENTITY_KEY]: durableIdentity ?? identity,
|
|
677
739
|
[BOT_CONFIGURATION_KEY]: settings,
|
|
678
|
-
...(initial.modelBinding
|
|
679
|
-
? {
|
|
680
|
-
[assignmentGenerationKey(
|
|
681
|
-
initial.modelBinding.assignment.assignmentId,
|
|
682
|
-
)]: initial.modelBinding.generation,
|
|
683
|
-
}
|
|
684
|
-
: {}),
|
|
685
740
|
});
|
|
686
741
|
return settings;
|
|
687
742
|
});
|
|
688
743
|
}
|
|
689
744
|
|
|
690
745
|
async getSettings(identity: BotIdentity): Promise<BotSettingsViewV1> {
|
|
691
|
-
|
|
692
|
-
if (settings.assignments.length === 0) return settings;
|
|
693
|
-
const [user, application] = await Promise.all([
|
|
694
|
-
this.userConfiguration(identity).readConfiguration({
|
|
695
|
-
schemaVersion: 1,
|
|
696
|
-
userId: identity.userId,
|
|
697
|
-
}),
|
|
698
|
-
this.compileApplication(),
|
|
699
|
-
]);
|
|
700
|
-
const plan = resolveBotExecutionPlanV1({
|
|
701
|
-
bot: settings,
|
|
702
|
-
user,
|
|
703
|
-
packages: application.packages.map((pkg) => ({
|
|
704
|
-
packageId: pkg.id,
|
|
705
|
-
version: pkg.version,
|
|
706
|
-
capabilities: pkg.manifest.configuration?.capabilities ?? [],
|
|
707
|
-
connectionTypes: pkg.manifest.configuration?.connectionTypes ?? [],
|
|
708
|
-
})),
|
|
709
|
-
});
|
|
710
|
-
return { ...settings, assignments: plan.assignments };
|
|
746
|
+
return this.ensureBotSettings(identity);
|
|
711
747
|
}
|
|
712
748
|
|
|
713
749
|
async readConfiguration(input: unknown): Promise<BotSettingsViewV1> {
|
|
@@ -729,7 +765,7 @@ export class ShellBotBackendContribution {
|
|
|
729
765
|
command: Extract<ConfigurationCommandV1, { botId: string }>,
|
|
730
766
|
): Promise<OperationReceiptV1> {
|
|
731
767
|
const commandFingerprint = configurationCommandFingerprintV1(command);
|
|
732
|
-
const active = this.
|
|
768
|
+
const active = this.configurationActivities.get(command.commandId);
|
|
733
769
|
if (active) {
|
|
734
770
|
if (active.commandFingerprint !== commandFingerprint) {
|
|
735
771
|
throw new Error(
|
|
@@ -745,12 +781,13 @@ export class ShellBotBackendContribution {
|
|
|
745
781
|
commandFingerprint,
|
|
746
782
|
).finally(() => {
|
|
747
783
|
if (
|
|
748
|
-
this.
|
|
784
|
+
this.configurationActivities.get(command.commandId)?.promise ===
|
|
785
|
+
activity
|
|
749
786
|
) {
|
|
750
|
-
this.
|
|
787
|
+
this.configurationActivities.delete(command.commandId);
|
|
751
788
|
}
|
|
752
789
|
});
|
|
753
|
-
this.
|
|
790
|
+
this.configurationActivities.set(command.commandId, {
|
|
754
791
|
commandFingerprint,
|
|
755
792
|
promise: activity,
|
|
756
793
|
});
|
|
@@ -767,103 +804,18 @@ export class ShellBotBackendContribution {
|
|
|
767
804
|
const existing =
|
|
768
805
|
await this.ctx.storage.get<StoredConfigurationReceipt>(receiptKey);
|
|
769
806
|
if (existing) {
|
|
770
|
-
|
|
807
|
+
return requireMatchingConfigurationReceipt(
|
|
771
808
|
existing,
|
|
772
809
|
commandFingerprint,
|
|
773
810
|
command.commandId,
|
|
774
811
|
);
|
|
775
|
-
if (
|
|
776
|
-
command.type === "bot/assign-capability" ||
|
|
777
|
-
command.type === "bot/replace-capability" ||
|
|
778
|
-
command.type === "bot/unassign-capability"
|
|
779
|
-
) {
|
|
780
|
-
try {
|
|
781
|
-
await this.reconcileStoredAssignmentSaga(
|
|
782
|
-
identity,
|
|
783
|
-
command.commandId,
|
|
784
|
-
commandFingerprint,
|
|
785
|
-
);
|
|
786
|
-
} catch {
|
|
787
|
-
// The durable accepted receipt remains replayable while recovery is
|
|
788
|
-
// retrying; alarm reconciliation owns eventual settlement.
|
|
789
|
-
}
|
|
790
|
-
const pending = await this.ctx.storage.get<unknown>(
|
|
791
|
-
`${ASSIGNMENT_SAGA_PREFIX}${command.commandId}`,
|
|
792
|
-
);
|
|
793
|
-
if (pending !== undefined) {
|
|
794
|
-
const saga = requireStoredAssignmentSaga(pending);
|
|
795
|
-
if (saga.commandFingerprint !== commandFingerprint) {
|
|
796
|
-
throw new Error(
|
|
797
|
-
`Configuration command idempotency key "${command.commandId}" was reused for a different command`,
|
|
798
|
-
);
|
|
799
|
-
}
|
|
800
|
-
return saga.acceptedReceipt;
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
return receipt;
|
|
804
812
|
}
|
|
805
813
|
if (command.expectedRevision !== settings.revision) {
|
|
806
814
|
throw new ConfigurationConflictError(settings.revision);
|
|
807
815
|
}
|
|
808
|
-
let
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
this.userConfiguration(identity).readConfiguration({
|
|
812
|
-
schemaVersion: 1,
|
|
813
|
-
userId: identity.userId,
|
|
814
|
-
}),
|
|
815
|
-
this.compileApplication(),
|
|
816
|
-
]);
|
|
817
|
-
modelCapabilities = new Set(
|
|
818
|
-
application.packages.flatMap((pkg) =>
|
|
819
|
-
(pkg.manifest.configuration?.capabilities ?? []).flatMap(
|
|
820
|
-
(capability) =>
|
|
821
|
-
capability.kind === "model"
|
|
822
|
-
? [capabilityKey(pkg.id, capability.id)]
|
|
823
|
-
: [],
|
|
824
|
-
),
|
|
825
|
-
),
|
|
826
|
-
);
|
|
827
|
-
const binding = resolveBotModelBindingV1({
|
|
828
|
-
model: command.model,
|
|
829
|
-
assignments: settings.assignments,
|
|
830
|
-
user,
|
|
831
|
-
packages: application.packages.map((pkg) => ({
|
|
832
|
-
packageId: pkg.id,
|
|
833
|
-
version: pkg.version,
|
|
834
|
-
capabilities: pkg.manifest.configuration?.capabilities ?? [],
|
|
835
|
-
connectionTypes: pkg.manifest.configuration?.connectionTypes ?? [],
|
|
836
|
-
})),
|
|
837
|
-
});
|
|
838
|
-
if (binding.state === "unavailable") {
|
|
839
|
-
return this.rejectConfigurationCommand(
|
|
840
|
-
identity,
|
|
841
|
-
command,
|
|
842
|
-
commandFingerprint,
|
|
843
|
-
binding.failure ?? "Bot model binding is unavailable",
|
|
844
|
-
);
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
if (
|
|
848
|
-
command.type === "bot/assign-capability" ||
|
|
849
|
-
command.type === "bot/replace-capability"
|
|
850
|
-
) {
|
|
851
|
-
const existingAssignment = settings.assignments.find(
|
|
852
|
-
(assignment) =>
|
|
853
|
-
assignment.assignmentId === command.assignment.assignmentId,
|
|
854
|
-
);
|
|
855
|
-
if (
|
|
856
|
-
existingAssignment &&
|
|
857
|
-
(existingAssignment.packageId !== command.assignment.packageId ||
|
|
858
|
-
existingAssignment.capabilityId !== command.assignment.capabilityId)
|
|
859
|
-
) {
|
|
860
|
-
return this.rejectConfigurationCommand(
|
|
861
|
-
identity,
|
|
862
|
-
command,
|
|
863
|
-
commandFingerprint,
|
|
864
|
-
"Assignment ID cannot change Package Capability authority",
|
|
865
|
-
);
|
|
866
|
-
}
|
|
816
|
+
let packageValues: Record<string, unknown> | undefined;
|
|
817
|
+
let packageUnset: string[] | undefined;
|
|
818
|
+
if (command.type === "bot/set-package-settings") {
|
|
867
819
|
const [user, application] = await Promise.all([
|
|
868
820
|
this.userConfiguration(identity).readConfiguration({
|
|
869
821
|
schemaVersion: 1,
|
|
@@ -871,186 +823,39 @@ export class ShellBotBackendContribution {
|
|
|
871
823
|
}),
|
|
872
824
|
this.compileApplication(),
|
|
873
825
|
]);
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
user,
|
|
887
|
-
packages: application.packages.map((pkg) => ({
|
|
888
|
-
packageId: pkg.id,
|
|
889
|
-
version: pkg.version,
|
|
890
|
-
capabilities: pkg.manifest.configuration?.capabilities ?? [],
|
|
891
|
-
connectionTypes: pkg.manifest.configuration?.connectionTypes ?? [],
|
|
892
|
-
})),
|
|
893
|
-
});
|
|
894
|
-
if (failure) {
|
|
895
|
-
return this.rejectConfigurationCommand(
|
|
896
|
-
identity,
|
|
897
|
-
command,
|
|
898
|
-
commandFingerprint,
|
|
899
|
-
failure,
|
|
900
|
-
);
|
|
901
|
-
}
|
|
902
|
-
if (command.model) {
|
|
903
|
-
if (command.model.connectionId !== command.assignment.connectionId) {
|
|
904
|
-
return this.rejectConfigurationCommand(
|
|
905
|
-
identity,
|
|
906
|
-
command,
|
|
907
|
-
commandFingerprint,
|
|
908
|
-
"Model binding must use the assigned Connection",
|
|
909
|
-
);
|
|
910
|
-
}
|
|
911
|
-
const binding = resolveBotModelBindingV1({
|
|
912
|
-
model: command.model,
|
|
913
|
-
assignments: [
|
|
914
|
-
...settings.assignments,
|
|
915
|
-
{ ...command.assignment, state: "enabled" },
|
|
916
|
-
],
|
|
917
|
-
user,
|
|
918
|
-
packages: application.packages.map((pkg) => ({
|
|
919
|
-
packageId: pkg.id,
|
|
920
|
-
version: pkg.version,
|
|
921
|
-
capabilities: pkg.manifest.configuration?.capabilities ?? [],
|
|
922
|
-
connectionTypes: pkg.manifest.configuration?.connectionTypes ?? [],
|
|
923
|
-
})),
|
|
826
|
+
const packages = application.packages.map((pkg) => ({
|
|
827
|
+
packageId: pkg.id,
|
|
828
|
+
version: pkg.version,
|
|
829
|
+
settings: pkg.manifest.configuration?.settings ?? [],
|
|
830
|
+
}));
|
|
831
|
+
if (command.values) {
|
|
832
|
+
packageValues = decodeInstalledPackageSettingsPatchV1({
|
|
833
|
+
packageId: command.packageId,
|
|
834
|
+
values: command.values,
|
|
835
|
+
scope: "bot",
|
|
836
|
+
installations: user.packages,
|
|
837
|
+
packages,
|
|
924
838
|
});
|
|
925
|
-
if (binding.state === "unavailable") {
|
|
926
|
-
return this.rejectConfigurationCommand(
|
|
927
|
-
identity,
|
|
928
|
-
command,
|
|
929
|
-
commandFingerprint,
|
|
930
|
-
binding.failure ?? "Bot model binding is unavailable",
|
|
931
|
-
);
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
if (
|
|
936
|
-
command.type === "bot/select-model" &&
|
|
937
|
-
settings.model?.connectionId &&
|
|
938
|
-
settings.model.connectionId !== command.model.connectionId
|
|
939
|
-
) {
|
|
940
|
-
// Moving the model to another Connection changes Assignment authority,
|
|
941
|
-
// so it is a Replace, not a select. One durable shape, one saga.
|
|
942
|
-
return this.rejectConfigurationCommand(
|
|
943
|
-
identity,
|
|
944
|
-
command,
|
|
945
|
-
commandFingerprint,
|
|
946
|
-
"Selecting a model on another Connection requires Replace",
|
|
947
|
-
);
|
|
948
|
-
}
|
|
949
|
-
if (command.type === "bot/unbind-model") {
|
|
950
|
-
const assignment = settings.assignments.find(
|
|
951
|
-
(candidate) =>
|
|
952
|
-
candidate.assignmentId === command.assignmentId &&
|
|
953
|
-
(candidate.state === "enabled" ||
|
|
954
|
-
candidate.state === "unavailable") &&
|
|
955
|
-
candidate.connectionId === settings.model?.connectionId,
|
|
956
|
-
);
|
|
957
|
-
const application = await this.compileApplication();
|
|
958
|
-
const capability = application.packages
|
|
959
|
-
.find((pkg) => pkg.id === assignment?.packageId)
|
|
960
|
-
?.manifest.configuration?.capabilities.find(
|
|
961
|
-
(candidate) => candidate.id === assignment?.capabilityId,
|
|
962
|
-
);
|
|
963
|
-
if (!assignment?.connectionId || capability?.kind !== "model") {
|
|
964
|
-
return this.rejectConfigurationCommand(
|
|
965
|
-
identity,
|
|
966
|
-
command,
|
|
967
|
-
commandFingerprint,
|
|
968
|
-
"Bot model assignment is unavailable",
|
|
969
|
-
);
|
|
970
839
|
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
"Bot model assignment generation is unavailable",
|
|
980
|
-
);
|
|
840
|
+
if (command.unset) {
|
|
841
|
+
packageUnset = decodeInstalledPackageSettingIdsV1({
|
|
842
|
+
packageId: command.packageId,
|
|
843
|
+
unset: command.unset,
|
|
844
|
+
scope: "bot",
|
|
845
|
+
installations: user.packages,
|
|
846
|
+
packages,
|
|
847
|
+
});
|
|
981
848
|
}
|
|
982
|
-
// Unbinding the model is the Assignment's Unassign: one saga releases
|
|
983
|
-
// the Connection dependency and clears the Bot's model together.
|
|
984
|
-
return this.executeAssignmentCommand(
|
|
985
|
-
identity,
|
|
986
|
-
{
|
|
987
|
-
...command,
|
|
988
|
-
type: "bot/unassign-capability",
|
|
989
|
-
assignmentId: assignment.assignmentId,
|
|
990
|
-
},
|
|
991
|
-
commandFingerprint,
|
|
992
|
-
{ clearModel: true },
|
|
993
|
-
);
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
if (
|
|
997
|
-
command.type !== "bot/assign-capability" &&
|
|
998
|
-
command.type !== "bot/replace-capability" &&
|
|
999
|
-
command.type !== "bot/unassign-capability"
|
|
1000
|
-
) {
|
|
1001
|
-
return this.applySimpleConfigurationCommand(
|
|
1002
|
-
identity,
|
|
1003
|
-
command,
|
|
1004
|
-
commandFingerprint,
|
|
1005
|
-
);
|
|
1006
849
|
}
|
|
1007
|
-
return this.
|
|
850
|
+
return this.applySimpleConfigurationCommand(
|
|
1008
851
|
identity,
|
|
1009
852
|
command,
|
|
1010
853
|
commandFingerprint,
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
? {}
|
|
1014
|
-
: { model: command.model }),
|
|
1015
|
-
},
|
|
854
|
+
packageValues,
|
|
855
|
+
packageUnset,
|
|
1016
856
|
);
|
|
1017
857
|
}
|
|
1018
858
|
|
|
1019
|
-
private async rejectConfigurationCommand(
|
|
1020
|
-
identity: BotIdentity,
|
|
1021
|
-
command: Extract<ConfigurationCommandV1, { botId: string }>,
|
|
1022
|
-
commandFingerprint: string,
|
|
1023
|
-
failure: string,
|
|
1024
|
-
): Promise<OperationReceiptV1> {
|
|
1025
|
-
return this.ctx.storage.transaction(async (transaction) => {
|
|
1026
|
-
const receiptKey = `${CONFIGURATION_RECEIPT_PREFIX}${command.commandId}`;
|
|
1027
|
-
const existing =
|
|
1028
|
-
await transaction.get<StoredConfigurationReceipt>(receiptKey);
|
|
1029
|
-
if (existing) {
|
|
1030
|
-
return requireMatchingConfigurationReceipt(
|
|
1031
|
-
existing,
|
|
1032
|
-
commandFingerprint,
|
|
1033
|
-
command.commandId,
|
|
1034
|
-
);
|
|
1035
|
-
}
|
|
1036
|
-
const current =
|
|
1037
|
-
(await transaction.get<BotSettingsViewV1>(BOT_CONFIGURATION_KEY)) ??
|
|
1038
|
-
this.initialBotSettings(identity.botId);
|
|
1039
|
-
if (command.expectedRevision !== current.revision) {
|
|
1040
|
-
throw new ConfigurationConflictError(current.revision);
|
|
1041
|
-
}
|
|
1042
|
-
const receipt: OperationReceiptV1 = {
|
|
1043
|
-
schemaVersion: 1,
|
|
1044
|
-
commandId: command.commandId,
|
|
1045
|
-
revision: current.revision,
|
|
1046
|
-
status: "rejected",
|
|
1047
|
-
failure,
|
|
1048
|
-
};
|
|
1049
|
-
await transaction.put(receiptKey, { commandFingerprint, receipt });
|
|
1050
|
-
return receipt;
|
|
1051
|
-
});
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
859
|
private async applySimpleConfigurationCommand(
|
|
1055
860
|
identity: BotIdentity,
|
|
1056
861
|
command: Extract<
|
|
@@ -1060,10 +865,12 @@ export class ShellBotBackendContribution {
|
|
|
1060
865
|
| "bot/update-profile"
|
|
1061
866
|
| "bot/set-profile"
|
|
1062
867
|
| "bot/update-notifications"
|
|
1063
|
-
| "bot/
|
|
868
|
+
| "bot/set-package-settings";
|
|
1064
869
|
}
|
|
1065
870
|
>,
|
|
1066
871
|
commandFingerprint: string,
|
|
872
|
+
packageValues?: Record<string, unknown>,
|
|
873
|
+
packageUnset: readonly string[] = [],
|
|
1067
874
|
): Promise<OperationReceiptV1> {
|
|
1068
875
|
return this.ctx.storage.transaction(async (transaction) => {
|
|
1069
876
|
await this.lifecycleAdmission?.(transaction, identity.botId);
|
|
@@ -1077,15 +884,14 @@ export class ShellBotBackendContribution {
|
|
|
1077
884
|
command.commandId,
|
|
1078
885
|
);
|
|
1079
886
|
}
|
|
887
|
+
const stored = await transaction.get<unknown>(BOT_CONFIGURATION_KEY);
|
|
1080
888
|
const current =
|
|
1081
|
-
|
|
1082
|
-
|
|
889
|
+
stored === undefined
|
|
890
|
+
? this.initialBotSettings(identity.botId)
|
|
891
|
+
: decodeBotSettingsViewV1(migrateStoredBotSettingsV1(stored));
|
|
1083
892
|
if (command.expectedRevision !== current.revision) {
|
|
1084
893
|
throw new ConfigurationConflictError(current.revision);
|
|
1085
894
|
}
|
|
1086
|
-
if (current.assignmentOperations.length > 0) {
|
|
1087
|
-
throw new Error("An Assignment operation is still retrying");
|
|
1088
|
-
}
|
|
1089
895
|
const revision = current.revision + 1;
|
|
1090
896
|
const next: BotSettingsViewV1 =
|
|
1091
897
|
command.type === "bot/update-profile"
|
|
@@ -1102,7 +908,25 @@ export class ShellBotBackendContribution {
|
|
|
1102
908
|
}
|
|
1103
909
|
: command.type === "bot/update-notifications"
|
|
1104
910
|
? { ...current, revision, notifications: command.notifications }
|
|
1105
|
-
:
|
|
911
|
+
: (() => {
|
|
912
|
+
const values = {
|
|
913
|
+
...(current.packageValues[command.packageId] ?? {}),
|
|
914
|
+
...structuredClone(packageValues ?? {}),
|
|
915
|
+
};
|
|
916
|
+
for (const settingId of packageUnset)
|
|
917
|
+
delete values[settingId];
|
|
918
|
+
const nextPackageValues = { ...current.packageValues };
|
|
919
|
+
if (Object.keys(values).length > 0) {
|
|
920
|
+
nextPackageValues[command.packageId] = values;
|
|
921
|
+
} else {
|
|
922
|
+
delete nextPackageValues[command.packageId];
|
|
923
|
+
}
|
|
924
|
+
return {
|
|
925
|
+
...current,
|
|
926
|
+
revision,
|
|
927
|
+
packageValues: nextPackageValues,
|
|
928
|
+
};
|
|
929
|
+
})();
|
|
1106
930
|
const receipt: OperationReceiptV1 = {
|
|
1107
931
|
schemaVersion: 1,
|
|
1108
932
|
commandId: command.commandId,
|
|
@@ -1200,797 +1024,132 @@ export class ShellBotBackendContribution {
|
|
|
1200
1024
|
.map(([, value]) => decodeSessionEvent(value));
|
|
1201
1025
|
}
|
|
1202
1026
|
|
|
1203
|
-
private async
|
|
1027
|
+
private async refreshRecoveryAlarm(
|
|
1028
|
+
transaction: DurableObjectTransaction,
|
|
1029
|
+
): Promise<void> {
|
|
1030
|
+
await this.authority.refreshRecoveryAlarm(transaction);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
async resolveConfiguration(
|
|
1204
1034
|
identity: BotIdentity,
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1035
|
+
): Promise<BotExecutionPlanV1> {
|
|
1036
|
+
return (await this.resolveExecutionContext(identity)).plan;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
async run(command: OwnedBotTurnCommand): Promise<ClientTurnV1> {
|
|
1040
|
+
return projectClientTurnV1(await this.authority.run(command));
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
async runPackageUiTool(
|
|
1044
|
+
identity: BotIdentity,
|
|
1045
|
+
command: PackageIframeToolCommandV1,
|
|
1046
|
+
): Promise<ClientTurnV1> {
|
|
1047
|
+
await this.validateIdentity(identity);
|
|
1048
|
+
const catalog = await this.listPackageUi(identity);
|
|
1049
|
+
const contribution = requirePackageUiToolDeclarationV1(catalog, command);
|
|
1050
|
+
return projectClientTurnV1(
|
|
1051
|
+
await this.authority.run({
|
|
1052
|
+
...identity,
|
|
1053
|
+
runId: command.commandId,
|
|
1054
|
+
sessionId: `${identity.userId}:${identity.botId}`,
|
|
1055
|
+
acceptedAt: new Date().toISOString(),
|
|
1056
|
+
text: `${contribution.displayName} · ${command.name}`,
|
|
1057
|
+
directTool: {
|
|
1058
|
+
generationId: command.generationId,
|
|
1059
|
+
packageId: command.packageId,
|
|
1060
|
+
name: command.name,
|
|
1061
|
+
input: command.input,
|
|
1062
|
+
},
|
|
1217
1063
|
}),
|
|
1218
|
-
this.compileApplication(),
|
|
1219
|
-
]);
|
|
1220
|
-
const packages = application.packages.map((pkg) => ({
|
|
1221
|
-
packageId: pkg.id,
|
|
1222
|
-
version: pkg.version,
|
|
1223
|
-
capabilities: pkg.manifest.configuration?.capabilities ?? [],
|
|
1224
|
-
connectionTypes: pkg.manifest.configuration?.connectionTypes ?? [],
|
|
1225
|
-
}));
|
|
1226
|
-
const failure = capabilityAssignmentFailureV1({
|
|
1227
|
-
assignment,
|
|
1228
|
-
user,
|
|
1229
|
-
packages,
|
|
1230
|
-
});
|
|
1231
|
-
if (failure) return { failure };
|
|
1232
|
-
if (!assignment.connectionId) return { user };
|
|
1233
|
-
const installation = user.packages.find(
|
|
1234
|
-
(pkg) =>
|
|
1235
|
-
pkg.packageId === assignment.packageId && pkg.state === "installed",
|
|
1236
1064
|
);
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* The Bot's invocable Skills, for the composer's `/` and `@` popover.
|
|
1069
|
+
*
|
|
1070
|
+
* A read of the same instruction root the Turn loader reads, through the
|
|
1071
|
+
* same `WorkspaceReadsV1`, so the popover can never offer a Skill a Turn
|
|
1072
|
+
* would refuse as an instruction: a refused candidate is not in the catalog
|
|
1073
|
+
* here either. Names and descriptions only — never a body.
|
|
1074
|
+
*
|
|
1075
|
+
* An unbound Workspace surface is an empty catalog, not a failure: the
|
|
1076
|
+
* Skills Package is not mounted in that host either, so "no Skills" is the
|
|
1077
|
+
* true answer rather than an error the composer has to explain.
|
|
1078
|
+
*/
|
|
1079
|
+
async listSkills(identity: BotIdentity): Promise<ClientSkillCatalogV1> {
|
|
1080
|
+
await this.validateIdentity(identity);
|
|
1081
|
+
const reads = createBotSkillsReads(this.env);
|
|
1082
|
+
if (!reads) return { schemaVersion: 1, skills: [] };
|
|
1083
|
+
const user = await this.userConfiguration(identity).readConfiguration({
|
|
1084
|
+
schemaVersion: 1,
|
|
1085
|
+
userId: identity.userId,
|
|
1086
|
+
});
|
|
1087
|
+
const pluginSkills = createBotPluginSkillsSource(
|
|
1088
|
+
user.packages,
|
|
1089
|
+
createBotSkillCatalogReader(this.env),
|
|
1241
1090
|
);
|
|
1242
|
-
const
|
|
1243
|
-
|
|
1091
|
+
const catalog = await loadFullSkillCatalogV1(
|
|
1092
|
+
reads,
|
|
1093
|
+
{ userId: identity.userId, botId: identity.botId },
|
|
1094
|
+
{ ...(pluginSkills ? { pluginSkills } : {}) },
|
|
1244
1095
|
);
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1096
|
+
const entries: ClientSkillCatalogEntryV1[] = [];
|
|
1097
|
+
for (const skill of catalog.skills) {
|
|
1098
|
+
const ref = skillRefForLoadedSkillV1(skill);
|
|
1099
|
+
// A Skill whose directory is not a well-formed slug has no ref, so it
|
|
1100
|
+
// cannot be invoked and is not offered. It is still listed to the model
|
|
1101
|
+
// in `<agent_skills>` and still loadable by path.
|
|
1102
|
+
if (!ref) continue;
|
|
1103
|
+
entries.push(
|
|
1104
|
+
clientSkillCatalogEntryV1({
|
|
1105
|
+
skill: ref,
|
|
1106
|
+
name: skill.name,
|
|
1107
|
+
description: skill.description,
|
|
1108
|
+
path: skill.path,
|
|
1109
|
+
}),
|
|
1110
|
+
);
|
|
1249
1111
|
}
|
|
1250
|
-
return {
|
|
1251
|
-
user,
|
|
1252
|
-
requirement: {
|
|
1253
|
-
schemaVersion: 1,
|
|
1254
|
-
packageId: pkg.id,
|
|
1255
|
-
packageVersion: pkg.version,
|
|
1256
|
-
capabilityId: capability.id,
|
|
1257
|
-
connectionTypeIds: [...capability.connectionTypes],
|
|
1258
|
-
},
|
|
1259
|
-
};
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
private async executeAssignmentCommand(
|
|
1263
|
-
identity: BotIdentity,
|
|
1264
|
-
command: Extract<
|
|
1265
|
-
ConfigurationCommandV1,
|
|
1266
|
-
{
|
|
1267
|
-
type:
|
|
1268
|
-
| "bot/assign-capability"
|
|
1269
|
-
| "bot/replace-capability"
|
|
1270
|
-
| "bot/unassign-capability";
|
|
1271
|
-
}
|
|
1272
|
-
>,
|
|
1273
|
-
commandFingerprint: string,
|
|
1274
|
-
binding: { model?: ModelAssignment; clearModel?: boolean } = {},
|
|
1275
|
-
): Promise<OperationReceiptV1> {
|
|
1276
|
-
try {
|
|
1277
|
-
await this.reconcileStoredAssignmentSaga(
|
|
1278
|
-
identity,
|
|
1279
|
-
command.commandId,
|
|
1280
|
-
commandFingerprint,
|
|
1281
|
-
);
|
|
1282
|
-
} catch (error) {
|
|
1283
|
-
const pending = await this.ctx.storage.get<unknown>(
|
|
1284
|
-
`${ASSIGNMENT_SAGA_PREFIX}${command.commandId}`,
|
|
1285
|
-
);
|
|
1286
|
-
if (pending === undefined) throw error;
|
|
1287
|
-
const saga = requireStoredAssignmentSaga(pending);
|
|
1288
|
-
if (saga.commandFingerprint !== commandFingerprint) throw error;
|
|
1289
|
-
return saga.acceptedReceipt;
|
|
1290
|
-
}
|
|
1291
|
-
const receiptKey = `${CONFIGURATION_RECEIPT_PREFIX}${command.commandId}`;
|
|
1292
|
-
const receipt =
|
|
1293
|
-
await this.ctx.storage.get<StoredConfigurationReceipt>(receiptKey);
|
|
1294
|
-
if (receipt) {
|
|
1295
|
-
return requireMatchingConfigurationReceipt(
|
|
1296
|
-
receipt,
|
|
1297
|
-
commandFingerprint,
|
|
1298
|
-
command.commandId,
|
|
1299
|
-
);
|
|
1300
|
-
}
|
|
1301
|
-
const pending = await this.ctx.storage.get<unknown>(
|
|
1302
|
-
`${ASSIGNMENT_SAGA_PREFIX}${command.commandId}`,
|
|
1303
|
-
);
|
|
1304
|
-
if (pending !== undefined) {
|
|
1305
|
-
const saga = requireStoredAssignmentSaga(pending);
|
|
1306
|
-
if (saga.commandFingerprint !== commandFingerprint) {
|
|
1307
|
-
throw new Error(
|
|
1308
|
-
`Configuration command idempotency key "${command.commandId}" was reused for a different command`,
|
|
1309
|
-
);
|
|
1310
|
-
}
|
|
1311
|
-
return saga.acceptedReceipt;
|
|
1312
|
-
}
|
|
1313
|
-
const current = await this.ensureBotSettings(identity);
|
|
1314
|
-
const assignmentId =
|
|
1315
|
-
command.type === "bot/unassign-capability"
|
|
1316
|
-
? command.assignmentId
|
|
1317
|
-
: command.assignment.assignmentId;
|
|
1318
|
-
const previous = current.assignments.find(
|
|
1319
|
-
(assignment) => assignment.assignmentId === assignmentId,
|
|
1320
|
-
);
|
|
1321
|
-
if (command.type === "bot/assign-capability" && previous) {
|
|
1322
|
-
return this.rejectAssignmentCommand(
|
|
1323
|
-
identity,
|
|
1324
|
-
command.commandId,
|
|
1325
|
-
commandFingerprint,
|
|
1326
|
-
`Assignment "${assignmentId}" already exists; use Replace`,
|
|
1327
|
-
);
|
|
1328
|
-
}
|
|
1329
|
-
if (command.type !== "bot/assign-capability" && !previous) {
|
|
1330
|
-
return this.rejectAssignmentCommand(
|
|
1331
|
-
identity,
|
|
1332
|
-
command.commandId,
|
|
1333
|
-
commandFingerprint,
|
|
1334
|
-
`Assignment "${assignmentId}" does not exist`,
|
|
1335
|
-
);
|
|
1336
|
-
}
|
|
1337
|
-
let targetRequirement: ConnectionDependencyRequirementV1 | undefined;
|
|
1338
|
-
if (command.type !== "bot/unassign-capability") {
|
|
1339
|
-
const validation = await this.assignmentRequirement(
|
|
1340
|
-
identity,
|
|
1341
|
-
command.assignment,
|
|
1342
|
-
);
|
|
1343
|
-
if ("failure" in validation) {
|
|
1344
|
-
return this.rejectAssignmentCommand(
|
|
1345
|
-
identity,
|
|
1346
|
-
command.commandId,
|
|
1347
|
-
commandFingerprint,
|
|
1348
|
-
validation.failure,
|
|
1349
|
-
);
|
|
1350
|
-
}
|
|
1351
|
-
targetRequirement = validation.requirement;
|
|
1352
|
-
}
|
|
1353
|
-
const previousGeneration = previous?.connectionId
|
|
1354
|
-
? await this.ctx.storage.get<string>(
|
|
1355
|
-
`${ASSIGNMENT_GENERATION_PREFIX}${previous.assignmentId}`,
|
|
1356
|
-
)
|
|
1357
|
-
: undefined;
|
|
1358
|
-
const operation =
|
|
1359
|
-
command.type === "bot/assign-capability"
|
|
1360
|
-
? "assigning"
|
|
1361
|
-
: command.type === "bot/replace-capability"
|
|
1362
|
-
? "replacing"
|
|
1363
|
-
: "unassigning";
|
|
1364
|
-
await this.ctx.storage.transaction(async (transaction) => {
|
|
1365
|
-
await this.lifecycleAdmission?.(transaction, identity.botId);
|
|
1366
|
-
const existing = await transaction.get<StoredAssignmentSaga>(
|
|
1367
|
-
`${ASSIGNMENT_SAGA_PREFIX}${command.commandId}`,
|
|
1368
|
-
);
|
|
1369
|
-
if (existing) return;
|
|
1370
|
-
const durable =
|
|
1371
|
-
(await transaction.get<BotSettingsViewV1>(BOT_CONFIGURATION_KEY)) ??
|
|
1372
|
-
this.initialBotSettings(identity.botId);
|
|
1373
|
-
if (durable.revision !== command.expectedRevision) {
|
|
1374
|
-
throw new ConfigurationConflictError(durable.revision);
|
|
1375
|
-
}
|
|
1376
|
-
if (durable.assignmentOperations.length > 0) {
|
|
1377
|
-
throw new Error("Another Assignment operation is already pending");
|
|
1378
|
-
}
|
|
1379
|
-
const target =
|
|
1380
|
-
command.type === "bot/unassign-capability"
|
|
1381
|
-
? undefined
|
|
1382
|
-
: structuredClone(command.assignment);
|
|
1383
|
-
const phase =
|
|
1384
|
-
operation === "unassigning"
|
|
1385
|
-
? "releasing"
|
|
1386
|
-
: target?.connectionId
|
|
1387
|
-
? "claiming"
|
|
1388
|
-
: "committing";
|
|
1389
|
-
const acceptedReceipt: OperationReceiptV1 = {
|
|
1390
|
-
schemaVersion: 1,
|
|
1391
|
-
commandId: command.commandId,
|
|
1392
|
-
revision: durable.revision,
|
|
1393
|
-
status: "pending",
|
|
1394
|
-
};
|
|
1395
|
-
const saga: StoredAssignmentSaga = {
|
|
1396
|
-
schemaVersion: 1,
|
|
1397
|
-
commandId: command.commandId,
|
|
1398
|
-
commandFingerprint,
|
|
1399
|
-
userId: identity.userId,
|
|
1400
|
-
botId: identity.botId,
|
|
1401
|
-
operation,
|
|
1402
|
-
assignmentId,
|
|
1403
|
-
generation: command.commandId,
|
|
1404
|
-
phase,
|
|
1405
|
-
target,
|
|
1406
|
-
targetRequirement,
|
|
1407
|
-
previous: previous ? structuredClone(previous) : undefined,
|
|
1408
|
-
previousGeneration,
|
|
1409
|
-
// The Bot's model commits with the Assignment, in the saga's commit
|
|
1410
|
-
// phase, so the dependency claim and the binding are one durable unit.
|
|
1411
|
-
model: binding.model ? structuredClone(binding.model) : undefined,
|
|
1412
|
-
clearModel: binding.clearModel,
|
|
1413
|
-
deadlineAt: Date.now() + ASSIGNMENT_SAGA_DEADLINE_MS,
|
|
1414
|
-
acceptedReceipt,
|
|
1415
|
-
};
|
|
1416
|
-
await transaction.put({
|
|
1417
|
-
[`${ASSIGNMENT_SAGA_PREFIX}${command.commandId}`]: saga,
|
|
1418
|
-
[BOT_CONFIGURATION_KEY]: {
|
|
1419
|
-
...durable,
|
|
1420
|
-
assignmentOperations: [
|
|
1421
|
-
{
|
|
1422
|
-
commandId: command.commandId,
|
|
1423
|
-
kind: operation,
|
|
1424
|
-
assignmentId,
|
|
1425
|
-
state: "pending",
|
|
1426
|
-
target,
|
|
1427
|
-
},
|
|
1428
|
-
],
|
|
1429
|
-
} satisfies BotSettingsViewV1,
|
|
1430
|
-
});
|
|
1431
|
-
await this.refreshRecoveryAlarm(transaction);
|
|
1432
|
-
});
|
|
1433
|
-
try {
|
|
1434
|
-
await this.reconcileStoredAssignmentSaga(
|
|
1435
|
-
identity,
|
|
1436
|
-
command.commandId,
|
|
1437
|
-
commandFingerprint,
|
|
1438
|
-
);
|
|
1439
|
-
} catch {
|
|
1440
|
-
const pending = requireStoredAssignmentSaga(
|
|
1441
|
-
await this.ctx.storage.get<unknown>(
|
|
1442
|
-
`${ASSIGNMENT_SAGA_PREFIX}${command.commandId}`,
|
|
1443
|
-
),
|
|
1444
|
-
);
|
|
1445
|
-
return pending.acceptedReceipt;
|
|
1446
|
-
}
|
|
1447
|
-
const completed =
|
|
1448
|
-
await this.ctx.storage.get<StoredConfigurationReceipt>(receiptKey);
|
|
1449
|
-
if (!completed) {
|
|
1450
|
-
const pending = requireStoredAssignmentSaga(
|
|
1451
|
-
await this.ctx.storage.get<unknown>(
|
|
1452
|
-
`${ASSIGNMENT_SAGA_PREFIX}${command.commandId}`,
|
|
1453
|
-
),
|
|
1454
|
-
);
|
|
1455
|
-
return pending.acceptedReceipt;
|
|
1456
|
-
}
|
|
1457
|
-
return requireMatchingConfigurationReceipt(
|
|
1458
|
-
completed,
|
|
1459
|
-
commandFingerprint,
|
|
1460
|
-
command.commandId,
|
|
1461
|
-
);
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
private async rejectAssignmentCommand(
|
|
1465
|
-
identity: BotIdentity,
|
|
1466
|
-
commandId: string,
|
|
1467
|
-
commandFingerprint: string,
|
|
1468
|
-
failure: string,
|
|
1469
|
-
): Promise<OperationReceiptV1> {
|
|
1470
|
-
return this.ctx.storage.transaction(async (transaction) => {
|
|
1471
|
-
await this.lifecycleAdmission?.(transaction, identity.botId);
|
|
1472
|
-
const current =
|
|
1473
|
-
(await transaction.get<BotSettingsViewV1>(BOT_CONFIGURATION_KEY)) ??
|
|
1474
|
-
this.initialBotSettings(identity.botId);
|
|
1475
|
-
const receipt: OperationReceiptV1 = {
|
|
1476
|
-
schemaVersion: 1,
|
|
1477
|
-
commandId,
|
|
1478
|
-
revision: current.revision,
|
|
1479
|
-
status: "rejected",
|
|
1480
|
-
failure,
|
|
1481
|
-
};
|
|
1482
|
-
await transaction.put(`${CONFIGURATION_RECEIPT_PREFIX}${commandId}`, {
|
|
1483
|
-
commandFingerprint,
|
|
1484
|
-
receipt,
|
|
1485
|
-
} satisfies StoredConfigurationReceipt);
|
|
1486
|
-
return receipt;
|
|
1487
|
-
});
|
|
1112
|
+
return { schemaVersion: 1, skills: entries };
|
|
1488
1113
|
}
|
|
1489
1114
|
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
const connectionId =
|
|
1497
|
-
target === "new"
|
|
1498
|
-
? saga.target?.connectionId
|
|
1499
|
-
: saga.previous?.connectionId;
|
|
1500
|
-
const generation =
|
|
1501
|
-
target === "new" ? saga.generation : saga.previousGeneration;
|
|
1502
|
-
if (!connectionId) {
|
|
1503
|
-
return { schemaVersion: 1, status: "released" };
|
|
1504
|
-
}
|
|
1505
|
-
if (!generation) {
|
|
1506
|
-
return {
|
|
1507
|
-
schemaVersion: 1,
|
|
1508
|
-
status: "unavailable",
|
|
1509
|
-
failure: `Assignment "${saga.assignmentId}" dependency generation is unavailable`,
|
|
1510
|
-
};
|
|
1511
|
-
}
|
|
1512
|
-
const packageId =
|
|
1513
|
-
target === "new" ? saga.target?.packageId : saga.previous?.packageId;
|
|
1514
|
-
if (!packageId) {
|
|
1515
|
-
return {
|
|
1516
|
-
schemaVersion: 1,
|
|
1517
|
-
status: "unavailable",
|
|
1518
|
-
failure: `Assignment "${saga.assignmentId}" Package identity is unavailable`,
|
|
1519
|
-
};
|
|
1520
|
-
}
|
|
1521
|
-
const base = {
|
|
1522
|
-
schemaVersion: 1 as const,
|
|
1523
|
-
operationId: `${saga.commandId}:${target}`,
|
|
1524
|
-
userId: identity.userId,
|
|
1525
|
-
packageId,
|
|
1526
|
-
connectionId,
|
|
1527
|
-
botId: identity.botId,
|
|
1528
|
-
generation,
|
|
1529
|
-
};
|
|
1530
|
-
return this.userConfiguration(identity).executeConnectionDependency(
|
|
1531
|
-
action === "claim"
|
|
1532
|
-
? { ...base, action, requirement: saga.targetRequirement! }
|
|
1533
|
-
: { ...base, action },
|
|
1115
|
+
/** The one durable manifest lookup used by mounts, commands, and UI views. */
|
|
1116
|
+
private async readCompositionMemberManifest(
|
|
1117
|
+
member: CompositionMemberV1,
|
|
1118
|
+
): Promise<FrockBotManifest | undefined> {
|
|
1119
|
+
const stored = await this.ctx.storage.get<AuthoredManifestRecordV1>(
|
|
1120
|
+
authorshipManifestKey(member.manifestHash),
|
|
1534
1121
|
);
|
|
1122
|
+
return stored ? decodeFrockBotManifest(stored.manifest) : undefined;
|
|
1535
1123
|
}
|
|
1536
1124
|
|
|
1537
|
-
private async
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
const settings = await transaction.get<BotSettingsViewV1>(
|
|
1543
|
-
BOT_CONFIGURATION_KEY,
|
|
1544
|
-
);
|
|
1545
|
-
if (!settings) throw new Error("Bot settings are unavailable");
|
|
1546
|
-
const receipt: OperationReceiptV1 = {
|
|
1547
|
-
schemaVersion: 1,
|
|
1548
|
-
commandId: saga.commandId,
|
|
1549
|
-
revision: settings.revision,
|
|
1550
|
-
status: "rejected",
|
|
1551
|
-
failure,
|
|
1552
|
-
};
|
|
1553
|
-
await transaction.put({
|
|
1554
|
-
[BOT_CONFIGURATION_KEY]: {
|
|
1555
|
-
...settings,
|
|
1556
|
-
assignmentOperations: settings.assignmentOperations.filter(
|
|
1557
|
-
(operation) => operation.commandId !== saga.commandId,
|
|
1558
|
-
),
|
|
1559
|
-
} satisfies BotSettingsViewV1,
|
|
1560
|
-
[`${CONFIGURATION_RECEIPT_PREFIX}${saga.commandId}`]: {
|
|
1561
|
-
commandFingerprint: saga.commandFingerprint,
|
|
1562
|
-
receipt,
|
|
1563
|
-
} satisfies StoredConfigurationReceipt,
|
|
1564
|
-
});
|
|
1565
|
-
await transaction.delete(`${ASSIGNMENT_SAGA_PREFIX}${saga.commandId}`);
|
|
1566
|
-
await this.refreshRecoveryAlarm(transaction);
|
|
1567
|
-
});
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
private async persistSaga(
|
|
1571
|
-
saga: StoredAssignmentSaga,
|
|
1572
|
-
patch: Partial<StoredAssignmentSaga>,
|
|
1573
|
-
retrying = false,
|
|
1574
|
-
): Promise<void> {
|
|
1575
|
-
await this.ctx.storage.transaction(async (transaction) => {
|
|
1576
|
-
const key = `${ASSIGNMENT_SAGA_PREFIX}${saga.commandId}`;
|
|
1577
|
-
const current = await transaction.get<StoredAssignmentSaga>(key);
|
|
1578
|
-
if (!current || current.generation !== saga.generation) return;
|
|
1579
|
-
await transaction.put(key, {
|
|
1580
|
-
...current,
|
|
1581
|
-
...patch,
|
|
1582
|
-
deadlineAt: Date.now() + ASSIGNMENT_SAGA_DEADLINE_MS,
|
|
1583
|
-
} satisfies StoredAssignmentSaga);
|
|
1584
|
-
if (retrying) {
|
|
1585
|
-
const settings = await transaction.get<BotSettingsViewV1>(
|
|
1586
|
-
BOT_CONFIGURATION_KEY,
|
|
1587
|
-
);
|
|
1588
|
-
if (settings) {
|
|
1589
|
-
await transaction.put(BOT_CONFIGURATION_KEY, {
|
|
1590
|
-
...settings,
|
|
1591
|
-
assignmentOperations: settings.assignmentOperations.map(
|
|
1592
|
-
(operation) =>
|
|
1593
|
-
operation.commandId === saga.commandId
|
|
1594
|
-
? { ...operation, state: "retrying" as const }
|
|
1595
|
-
: operation,
|
|
1596
|
-
),
|
|
1597
|
-
} satisfies BotSettingsViewV1);
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
await this.refreshRecoveryAlarm(transaction);
|
|
1601
|
-
});
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
private async commitAssignmentSaga(
|
|
1605
|
-
saga: StoredAssignmentSaga,
|
|
1606
|
-
): Promise<OperationReceiptV1> {
|
|
1607
|
-
return this.ctx.storage.transaction(async (transaction) => {
|
|
1608
|
-
const current = (await transaction.get<BotSettingsViewV1>(
|
|
1609
|
-
BOT_CONFIGURATION_KEY,
|
|
1610
|
-
))!;
|
|
1611
|
-
const existing = await transaction.get<StoredConfigurationReceipt>(
|
|
1612
|
-
`${CONFIGURATION_RECEIPT_PREFIX}${saga.commandId}`,
|
|
1613
|
-
);
|
|
1614
|
-
if (existing) return existing.receipt;
|
|
1615
|
-
const revision = current.revision + 1;
|
|
1616
|
-
const assignments =
|
|
1617
|
-
saga.operation === "unassigning"
|
|
1618
|
-
? current.assignments.filter(
|
|
1619
|
-
(assignment) => assignment.assignmentId !== saga.assignmentId,
|
|
1620
|
-
)
|
|
1621
|
-
: [
|
|
1622
|
-
...current.assignments.filter(
|
|
1623
|
-
(assignment) => assignment.assignmentId !== saga.assignmentId,
|
|
1624
|
-
),
|
|
1625
|
-
{ ...saga.target!, state: "enabled" as const },
|
|
1626
|
-
];
|
|
1627
|
-
const receipt: OperationReceiptV1 = {
|
|
1628
|
-
schemaVersion: 1,
|
|
1629
|
-
commandId: saga.commandId,
|
|
1630
|
-
revision,
|
|
1631
|
-
status: "applied",
|
|
1632
|
-
};
|
|
1633
|
-
await transaction.put({
|
|
1634
|
-
[BOT_CONFIGURATION_KEY]: {
|
|
1635
|
-
...current,
|
|
1636
|
-
revision,
|
|
1637
|
-
assignments,
|
|
1638
|
-
...(saga.clearModel
|
|
1639
|
-
? { model: undefined }
|
|
1640
|
-
: saga.model
|
|
1641
|
-
? { model: structuredClone(saga.model) }
|
|
1642
|
-
: {}),
|
|
1643
|
-
} satisfies BotSettingsViewV1,
|
|
1644
|
-
[`${CONFIGURATION_RECEIPT_PREFIX}${saga.commandId}`]: {
|
|
1645
|
-
commandFingerprint: saga.commandFingerprint,
|
|
1646
|
-
receipt,
|
|
1647
|
-
} satisfies StoredConfigurationReceipt,
|
|
1648
|
-
});
|
|
1649
|
-
if (saga.target?.connectionId) {
|
|
1650
|
-
await transaction.put(
|
|
1651
|
-
`${ASSIGNMENT_GENERATION_PREFIX}${saga.assignmentId}`,
|
|
1652
|
-
saga.generation,
|
|
1653
|
-
);
|
|
1654
|
-
} else if (saga.operation === "unassigning") {
|
|
1655
|
-
await transaction.delete(
|
|
1656
|
-
`${ASSIGNMENT_GENERATION_PREFIX}${saga.assignmentId}`,
|
|
1657
|
-
);
|
|
1658
|
-
}
|
|
1659
|
-
return receipt;
|
|
1660
|
-
});
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
private async markSagaAssignmentUnavailable(
|
|
1664
|
-
saga: StoredAssignmentSaga,
|
|
1665
|
-
): Promise<void> {
|
|
1666
|
-
await this.ctx.storage.transaction(async (transaction) => {
|
|
1667
|
-
const settings = await transaction.get<BotSettingsViewV1>(
|
|
1668
|
-
BOT_CONFIGURATION_KEY,
|
|
1669
|
-
);
|
|
1670
|
-
if (!settings) return;
|
|
1671
|
-
await transaction.put(BOT_CONFIGURATION_KEY, {
|
|
1672
|
-
...settings,
|
|
1673
|
-
assignments: settings.assignments.map((assignment) =>
|
|
1674
|
-
assignment.assignmentId === saga.assignmentId
|
|
1675
|
-
? { ...assignment, state: "unavailable" as const }
|
|
1676
|
-
: assignment,
|
|
1677
|
-
),
|
|
1678
|
-
} satisfies BotSettingsViewV1);
|
|
1679
|
-
});
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
private async finishAssignmentSaga(
|
|
1683
|
-
saga: StoredAssignmentSaga,
|
|
1684
|
-
): Promise<void> {
|
|
1685
|
-
await this.ctx.storage.transaction(async (transaction) => {
|
|
1686
|
-
const key = `${ASSIGNMENT_SAGA_PREFIX}${saga.commandId}`;
|
|
1687
|
-
const current = await transaction.get<StoredAssignmentSaga>(key);
|
|
1688
|
-
if (current?.generation !== saga.generation) return;
|
|
1689
|
-
const settings = await transaction.get<BotSettingsViewV1>(
|
|
1690
|
-
BOT_CONFIGURATION_KEY,
|
|
1691
|
-
);
|
|
1692
|
-
if (settings) {
|
|
1693
|
-
await transaction.put(BOT_CONFIGURATION_KEY, {
|
|
1694
|
-
...settings,
|
|
1695
|
-
assignmentOperations: settings.assignmentOperations.filter(
|
|
1696
|
-
(operation) => operation.commandId !== saga.commandId,
|
|
1697
|
-
),
|
|
1698
|
-
} satisfies BotSettingsViewV1);
|
|
1699
|
-
}
|
|
1700
|
-
await transaction.delete(key);
|
|
1701
|
-
await this.refreshRecoveryAlarm(transaction);
|
|
1702
|
-
});
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
private async reconcileStoredAssignmentSaga(
|
|
1706
|
-
identity: BotIdentity,
|
|
1707
|
-
commandId: string,
|
|
1708
|
-
commandFingerprint?: string,
|
|
1709
|
-
): Promise<void> {
|
|
1710
|
-
const key = `${ASSIGNMENT_SAGA_PREFIX}${commandId}`;
|
|
1711
|
-
const storedSaga = await this.ctx.storage.get<unknown>(key);
|
|
1712
|
-
let saga =
|
|
1713
|
-
storedSaga === undefined
|
|
1714
|
-
? undefined
|
|
1715
|
-
: requireStoredAssignmentSaga(storedSaga);
|
|
1716
|
-
if (!saga) return;
|
|
1717
|
-
if (saga.userId !== identity.userId || saga.botId !== identity.botId) {
|
|
1718
|
-
throw new Error("Assignment saga does not match its durable identity");
|
|
1719
|
-
}
|
|
1720
|
-
if (
|
|
1721
|
-
commandFingerprint !== undefined &&
|
|
1722
|
-
saga.commandFingerprint !== commandFingerprint
|
|
1723
|
-
) {
|
|
1125
|
+
private async requireCompositionMemberManifest(
|
|
1126
|
+
member: CompositionMemberV1,
|
|
1127
|
+
): Promise<FrockBotManifest> {
|
|
1128
|
+
const manifest = await this.readCompositionMemberManifest(member);
|
|
1129
|
+
if (!manifest) {
|
|
1724
1130
|
throw new Error(
|
|
1725
|
-
`
|
|
1131
|
+
`package "${member.packageId}" manifest "${member.manifestHash}" is unavailable`,
|
|
1726
1132
|
);
|
|
1727
1133
|
}
|
|
1728
|
-
|
|
1729
|
-
for (let step = 0; step < 8 && saga; step += 1) {
|
|
1730
|
-
if (saga.phase === "claiming") {
|
|
1731
|
-
if (!saga.claimDispatched) {
|
|
1732
|
-
await this.persistSaga(saga, { claimDispatched: true });
|
|
1733
|
-
saga = { ...saga, claimDispatched: true };
|
|
1734
|
-
}
|
|
1735
|
-
let result = await this.dependencyResult(
|
|
1736
|
-
identity,
|
|
1737
|
-
saga,
|
|
1738
|
-
saga.claimDispatched ? "read" : "claim",
|
|
1739
|
-
"new",
|
|
1740
|
-
);
|
|
1741
|
-
if (result.status === "absent") {
|
|
1742
|
-
result = await this.dependencyResult(
|
|
1743
|
-
identity,
|
|
1744
|
-
saga,
|
|
1745
|
-
"claim",
|
|
1746
|
-
"new",
|
|
1747
|
-
);
|
|
1748
|
-
}
|
|
1749
|
-
if (result.status === "pending" || result.status === "unavailable") {
|
|
1750
|
-
if (result.status === "pending") {
|
|
1751
|
-
await this.dependencyResult(identity, saga, "reconcile", "new");
|
|
1752
|
-
}
|
|
1753
|
-
await this.persistSaga(saga, {}, true);
|
|
1754
|
-
return;
|
|
1755
|
-
}
|
|
1756
|
-
if (result.status !== "claimed" && result.status !== "acknowledged") {
|
|
1757
|
-
await this.rejectPendingAssignmentSaga(
|
|
1758
|
-
saga,
|
|
1759
|
-
("failure" in result ? result.failure : undefined) ??
|
|
1760
|
-
"Connection dependency claim rejected",
|
|
1761
|
-
);
|
|
1762
|
-
return;
|
|
1763
|
-
}
|
|
1764
|
-
await this.persistSaga(saga, { phase: "committing" });
|
|
1765
|
-
saga = { ...saga, phase: "committing" };
|
|
1766
|
-
continue;
|
|
1767
|
-
}
|
|
1768
|
-
if (saga.phase === "committing") {
|
|
1769
|
-
const receipt = await this.commitAssignmentSaga(saga);
|
|
1770
|
-
const phase: StoredAssignmentSaga["phase"] | undefined = saga.target
|
|
1771
|
-
?.connectionId
|
|
1772
|
-
? "acknowledging"
|
|
1773
|
-
: saga.previous?.connectionId
|
|
1774
|
-
? "releasing"
|
|
1775
|
-
: undefined;
|
|
1776
|
-
if (!phase) {
|
|
1777
|
-
await this.finishAssignmentSaga({ ...saga, receipt });
|
|
1778
|
-
return;
|
|
1779
|
-
}
|
|
1780
|
-
await this.persistSaga(saga, { phase, receipt });
|
|
1781
|
-
saga = { ...saga, phase, receipt };
|
|
1782
|
-
continue;
|
|
1783
|
-
}
|
|
1784
|
-
if (saga.phase === "acknowledging") {
|
|
1785
|
-
if (!saga.acknowledgeDispatched) {
|
|
1786
|
-
await this.persistSaga(saga, { acknowledgeDispatched: true });
|
|
1787
|
-
saga = { ...saga, acknowledgeDispatched: true };
|
|
1788
|
-
}
|
|
1789
|
-
let result = await this.dependencyResult(
|
|
1790
|
-
identity,
|
|
1791
|
-
saga,
|
|
1792
|
-
"read",
|
|
1793
|
-
"new",
|
|
1794
|
-
);
|
|
1795
|
-
if (result.status === "claimed") {
|
|
1796
|
-
result = await this.dependencyResult(
|
|
1797
|
-
identity,
|
|
1798
|
-
saga,
|
|
1799
|
-
"acknowledge",
|
|
1800
|
-
"new",
|
|
1801
|
-
);
|
|
1802
|
-
}
|
|
1803
|
-
if (result.status === "pending" || result.status === "unavailable") {
|
|
1804
|
-
if (result.status === "pending") {
|
|
1805
|
-
await this.dependencyResult(identity, saga, "reconcile", "new");
|
|
1806
|
-
}
|
|
1807
|
-
await this.persistSaga(saga, {}, true);
|
|
1808
|
-
return;
|
|
1809
|
-
}
|
|
1810
|
-
if (result.status !== "acknowledged") {
|
|
1811
|
-
if (result.status === "rejected" || result.status === "absent") {
|
|
1812
|
-
await this.markSagaAssignmentUnavailable(saga);
|
|
1813
|
-
if (!saga.previous?.connectionId) {
|
|
1814
|
-
await this.finishAssignmentSaga(saga);
|
|
1815
|
-
return;
|
|
1816
|
-
}
|
|
1817
|
-
await this.persistSaga(saga, { phase: "releasing" });
|
|
1818
|
-
saga = { ...saga, phase: "releasing" };
|
|
1819
|
-
continue;
|
|
1820
|
-
}
|
|
1821
|
-
throw new Error(
|
|
1822
|
-
("failure" in result ? result.failure : undefined) ??
|
|
1823
|
-
"Connection dependency acknowledgement rejected",
|
|
1824
|
-
);
|
|
1825
|
-
}
|
|
1826
|
-
if (!saga.previous?.connectionId) {
|
|
1827
|
-
await this.finishAssignmentSaga(saga);
|
|
1828
|
-
return;
|
|
1829
|
-
}
|
|
1830
|
-
await this.persistSaga(saga, { phase: "releasing" });
|
|
1831
|
-
saga = { ...saga, phase: "releasing" };
|
|
1832
|
-
continue;
|
|
1833
|
-
}
|
|
1834
|
-
if (!saga.previous?.connectionId) {
|
|
1835
|
-
if (saga.operation === "unassigning")
|
|
1836
|
-
await this.commitAssignmentSaga(saga);
|
|
1837
|
-
await this.finishAssignmentSaga(saga);
|
|
1838
|
-
return;
|
|
1839
|
-
}
|
|
1840
|
-
if (!saga.releaseDispatched) {
|
|
1841
|
-
await this.persistSaga(saga, { releaseDispatched: true });
|
|
1842
|
-
saga = { ...saga, releaseDispatched: true };
|
|
1843
|
-
}
|
|
1844
|
-
let result = await this.dependencyResult(identity, saga, "read", "old");
|
|
1845
|
-
if (result.status === "claimed" || result.status === "acknowledged") {
|
|
1846
|
-
result = await this.dependencyResult(
|
|
1847
|
-
identity,
|
|
1848
|
-
saga,
|
|
1849
|
-
"release",
|
|
1850
|
-
"old",
|
|
1851
|
-
);
|
|
1852
|
-
}
|
|
1853
|
-
if (result.status === "pending" || result.status === "unavailable") {
|
|
1854
|
-
if (result.status === "pending") {
|
|
1855
|
-
await this.dependencyResult(identity, saga, "reconcile", "old");
|
|
1856
|
-
}
|
|
1857
|
-
await this.persistSaga(saga, {}, true);
|
|
1858
|
-
return;
|
|
1859
|
-
}
|
|
1860
|
-
if (result.status !== "released" && result.status !== "absent") {
|
|
1861
|
-
throw new Error(
|
|
1862
|
-
("failure" in result ? result.failure : undefined) ??
|
|
1863
|
-
"Connection dependency release rejected",
|
|
1864
|
-
);
|
|
1865
|
-
}
|
|
1866
|
-
if (saga.operation === "unassigning")
|
|
1867
|
-
await this.commitAssignmentSaga(saga);
|
|
1868
|
-
await this.finishAssignmentSaga(saga);
|
|
1869
|
-
return;
|
|
1870
|
-
}
|
|
1871
|
-
} catch (error) {
|
|
1872
|
-
await this.persistSaga(saga, {}, true);
|
|
1873
|
-
throw error;
|
|
1874
|
-
}
|
|
1134
|
+
return manifest;
|
|
1875
1135
|
}
|
|
1876
1136
|
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
): Promise<void> {
|
|
1880
|
-
await this.authority.refreshRecoveryAlarm(transaction);
|
|
1881
|
-
}
|
|
1882
|
-
async markConnectionUnavailable(
|
|
1137
|
+
/** Active fail-closed Composition projected as inert iframe metadata. */
|
|
1138
|
+
async listPackageUi(
|
|
1883
1139
|
identity: BotIdentity,
|
|
1884
|
-
|
|
1885
|
-
compensation: { id: string; expectedGeneration: string },
|
|
1886
|
-
): Promise<"applied" | "stale"> {
|
|
1887
|
-
await this.ensureBotSettings(identity);
|
|
1888
|
-
return this.ctx.storage.transaction(async (transaction) => {
|
|
1889
|
-
const receiptKey = `${ASSIGNMENT_COMPENSATION_PREFIX}${compensation.id}`;
|
|
1890
|
-
const existing = await transaction.get<"applied" | "stale">(receiptKey);
|
|
1891
|
-
if (existing) return existing;
|
|
1892
|
-
const current =
|
|
1893
|
-
(await transaction.get<BotSettingsViewV1>(BOT_CONFIGURATION_KEY)) ??
|
|
1894
|
-
this.initialBotSettings(identity.botId);
|
|
1895
|
-
// Every enabled Assignment on this Connection whose durable generation
|
|
1896
|
-
// is the compensated one becomes unavailable. A compensation that names
|
|
1897
|
-
// a generation no live Assignment holds is stale, not a silent no-op.
|
|
1898
|
-
const matching: string[] = [];
|
|
1899
|
-
for (const assignment of current.assignments) {
|
|
1900
|
-
if (
|
|
1901
|
-
assignment.connectionId !== connectionId ||
|
|
1902
|
-
assignment.state !== "enabled"
|
|
1903
|
-
) {
|
|
1904
|
-
continue;
|
|
1905
|
-
}
|
|
1906
|
-
if (
|
|
1907
|
-
(await transaction.get<string>(
|
|
1908
|
-
assignmentGenerationKey(assignment.assignmentId),
|
|
1909
|
-
)) === compensation.expectedGeneration
|
|
1910
|
-
) {
|
|
1911
|
-
matching.push(assignment.assignmentId);
|
|
1912
|
-
}
|
|
1913
|
-
}
|
|
1914
|
-
await transaction.put(
|
|
1915
|
-
`${ASSIGNMENT_TOMBSTONE_PREFIX}${connectionId}:${compensation.expectedGeneration}`,
|
|
1916
|
-
compensation.id,
|
|
1917
|
-
);
|
|
1918
|
-
if (matching.length === 0) {
|
|
1919
|
-
await transaction.put(receiptKey, "stale");
|
|
1920
|
-
return "stale";
|
|
1921
|
-
}
|
|
1922
|
-
const unavailable = new Set(matching);
|
|
1923
|
-
await transaction.put(BOT_CONFIGURATION_KEY, {
|
|
1924
|
-
...current,
|
|
1925
|
-
revision: current.revision + 1,
|
|
1926
|
-
assignments: current.assignments.map((assignment) =>
|
|
1927
|
-
unavailable.has(assignment.assignmentId)
|
|
1928
|
-
? { ...assignment, state: "unavailable" as const }
|
|
1929
|
-
: assignment,
|
|
1930
|
-
),
|
|
1931
|
-
} satisfies BotSettingsViewV1);
|
|
1932
|
-
await this.refreshRecoveryAlarm(transaction);
|
|
1933
|
-
await transaction.put(receiptKey, "applied");
|
|
1934
|
-
return "applied";
|
|
1935
|
-
});
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
|
-
async resolveConfiguration(
|
|
1939
|
-
identity: BotIdentity,
|
|
1940
|
-
): Promise<BotExecutionPlanV1> {
|
|
1941
|
-
return (await this.resolveExecutionContext(identity)).plan;
|
|
1942
|
-
}
|
|
1943
|
-
|
|
1944
|
-
async run(command: OwnedBotTurnCommand): Promise<ClientTurnV1> {
|
|
1945
|
-
return projectClientTurnV1(await this.authority.run(command));
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
|
-
/**
|
|
1949
|
-
* The Bot's invocable Skills, for the composer's `/` and `@` popover.
|
|
1950
|
-
*
|
|
1951
|
-
* A read of the same instruction root the Turn loader reads, through the
|
|
1952
|
-
* same `WorkspaceReadsV1`, so the popover can never offer a Skill a Turn
|
|
1953
|
-
* would refuse as an instruction: a refused candidate is not in the catalog
|
|
1954
|
-
* here either. Names and descriptions only — never a body.
|
|
1955
|
-
*
|
|
1956
|
-
* An unbound Workspace surface is an empty catalog, not a failure: the
|
|
1957
|
-
* Skills Package is not mounted in that host either, so "no Skills" is the
|
|
1958
|
-
* true answer rather than an error the composer has to explain.
|
|
1959
|
-
*/
|
|
1960
|
-
async listSkills(identity: BotIdentity): Promise<ClientSkillCatalogV1> {
|
|
1140
|
+
): Promise<PackageIframeCompositionV1> {
|
|
1961
1141
|
await this.validateIdentity(identity);
|
|
1962
|
-
const
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1142
|
+
const current = await this.authority.composition.current();
|
|
1143
|
+
const generation =
|
|
1144
|
+
current.status === "active" || current.status === "superseded"
|
|
1145
|
+
? current
|
|
1146
|
+
: await this.authority.composition.lastKnownGood();
|
|
1147
|
+
return projectPackageIframeCompositionV1({
|
|
1148
|
+
botId: identity.botId,
|
|
1149
|
+
generation,
|
|
1150
|
+
readMemberManifest: (member) =>
|
|
1151
|
+
this.readCompositionMemberManifest(member),
|
|
1967
1152
|
});
|
|
1968
|
-
const pluginSkills = createBotPluginSkillsSource(
|
|
1969
|
-
user.packages,
|
|
1970
|
-
createBotSkillCatalogReader(this.env),
|
|
1971
|
-
);
|
|
1972
|
-
const catalog = await loadFullSkillCatalogV1(
|
|
1973
|
-
reads,
|
|
1974
|
-
{ userId: identity.userId, botId: identity.botId },
|
|
1975
|
-
{ ...(pluginSkills ? { pluginSkills } : {}) },
|
|
1976
|
-
);
|
|
1977
|
-
const entries: ClientSkillCatalogEntryV1[] = [];
|
|
1978
|
-
for (const skill of catalog.skills) {
|
|
1979
|
-
const ref = skillRefForLoadedSkillV1(skill);
|
|
1980
|
-
// A Skill whose directory is not a well-formed slug has no ref, so it
|
|
1981
|
-
// cannot be invoked and is not offered. It is still listed to the model
|
|
1982
|
-
// in `<agent_skills>` and still loadable by path.
|
|
1983
|
-
if (!ref) continue;
|
|
1984
|
-
entries.push(
|
|
1985
|
-
clientSkillCatalogEntryV1({
|
|
1986
|
-
skill: ref,
|
|
1987
|
-
name: skill.name,
|
|
1988
|
-
description: skill.description,
|
|
1989
|
-
path: skill.path,
|
|
1990
|
-
}),
|
|
1991
|
-
);
|
|
1992
|
-
}
|
|
1993
|
-
return { schemaVersion: 1, skills: entries };
|
|
1994
1153
|
}
|
|
1995
1154
|
|
|
1996
1155
|
/**
|
|
@@ -2204,11 +1363,17 @@ export class ShellBotBackendContribution {
|
|
|
2204
1363
|
? { subagentTaskId: input.command.origin.taskId }
|
|
2205
1364
|
: {}),
|
|
2206
1365
|
};
|
|
1366
|
+
let mountedRoot: ShellMountedComposition["root"] | undefined;
|
|
1367
|
+
let mountedGeneration: CompositionGenerationV1 | undefined;
|
|
1368
|
+
const currentToolNames = (): readonly string[] =>
|
|
1369
|
+
mountedRoot?.tools.registeredNames?.() ?? [];
|
|
2207
1370
|
const runtime = await this.agentRuntime(
|
|
2208
1371
|
input.identity,
|
|
2209
1372
|
settings,
|
|
2210
1373
|
input.admittedRequest,
|
|
2211
1374
|
turn,
|
|
1375
|
+
currentToolNames,
|
|
1376
|
+
() => mountedGeneration,
|
|
2212
1377
|
);
|
|
2213
1378
|
const promptParts = [
|
|
2214
1379
|
`You are ${settings.profile.name}.`,
|
|
@@ -2225,9 +1390,9 @@ export class ShellBotBackendContribution {
|
|
|
2225
1390
|
runId: input.command.runId,
|
|
2226
1391
|
sessionId: input.command.sessionId,
|
|
2227
1392
|
generationId: mounting.generationId,
|
|
2228
|
-
|
|
1393
|
+
settings,
|
|
2229
1394
|
});
|
|
2230
|
-
|
|
1395
|
+
const mounted = await createShellCompositionHost({
|
|
2231
1396
|
botId: input.identity.botId,
|
|
2232
1397
|
sessionId: input.command.sessionId,
|
|
2233
1398
|
sessionEvents: input.previousEvents,
|
|
@@ -2253,6 +1418,9 @@ export class ShellBotBackendContribution {
|
|
|
2253
1418
|
),
|
|
2254
1419
|
...(isolate ? { isolate } : {}),
|
|
2255
1420
|
}).mount(mounting, signal);
|
|
1421
|
+
mountedRoot = mounted.root;
|
|
1422
|
+
mountedGeneration = mounted.generation;
|
|
1423
|
+
return mounted;
|
|
2256
1424
|
},
|
|
2257
1425
|
};
|
|
2258
1426
|
const controller = new AbortController();
|
|
@@ -2292,14 +1460,78 @@ export class ShellBotBackendContribution {
|
|
|
2292
1460
|
const active = {
|
|
2293
1461
|
runId: input.command.runId,
|
|
2294
1462
|
sessionId: input.command.sessionId,
|
|
2295
|
-
|
|
1463
|
+
turnId: input.command.runId,
|
|
1464
|
+
generationId: activation.mounted.generation.generationId,
|
|
1465
|
+
turnType: input.command.turnType ?? "chat",
|
|
1466
|
+
...(input.command.subagentRole
|
|
1467
|
+
? { subagentRole: input.command.subagentRole }
|
|
1468
|
+
: {}),
|
|
1469
|
+
mounted: activation.mounted,
|
|
1470
|
+
signal: controller.signal,
|
|
1471
|
+
cancel: () => {
|
|
1472
|
+
controller.abort("user");
|
|
1473
|
+
activation.mounted.runtime.agent.agent.cancel("user");
|
|
1474
|
+
},
|
|
2296
1475
|
};
|
|
2297
1476
|
this.activeTurn = active;
|
|
2298
1477
|
try {
|
|
1478
|
+
const directTool = input.command.directTool;
|
|
1479
|
+
if (directTool) {
|
|
1480
|
+
if (
|
|
1481
|
+
directTool.generationId !== activation.mounted.generation.generationId
|
|
1482
|
+
) {
|
|
1483
|
+
throw new Error(
|
|
1484
|
+
"Package UI command does not match the mounted Composition generation",
|
|
1485
|
+
);
|
|
1486
|
+
}
|
|
1487
|
+
const member = activation.mounted.generation.members.find(
|
|
1488
|
+
(candidate) =>
|
|
1489
|
+
candidate.packageId === directTool.packageId &&
|
|
1490
|
+
candidate.provenance.kind !== "first-party",
|
|
1491
|
+
);
|
|
1492
|
+
if (!member)
|
|
1493
|
+
throw new Error("Package UI command names an unavailable Package");
|
|
1494
|
+
const manifest = await this.requireCompositionMemberManifest(member);
|
|
1495
|
+
const client = manifest.contributions.client;
|
|
1496
|
+
if (
|
|
1497
|
+
!client ||
|
|
1498
|
+
!isClientIframeContribution(client) ||
|
|
1499
|
+
!(manifest.tools ?? []).some((tool) => tool.name === directTool.name)
|
|
1500
|
+
) {
|
|
1501
|
+
throw new Error(
|
|
1502
|
+
`Package "${directTool.packageId}" did not declare tool "${directTool.name}" for its iframe`,
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
return await executeDirectToolTurn({
|
|
1506
|
+
command: { ...input.command, directTool },
|
|
1507
|
+
previousEvents: input.previousEvents,
|
|
1508
|
+
composition: activation.mounted,
|
|
1509
|
+
admitEffect: (effect) =>
|
|
1510
|
+
this.admitRunEffect(
|
|
1511
|
+
input.identity,
|
|
1512
|
+
input.command.runId,
|
|
1513
|
+
input.command.sessionId,
|
|
1514
|
+
effect,
|
|
1515
|
+
),
|
|
1516
|
+
signal: controller.signal,
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
const ordinaryInput = await this.turnInputTextV1(input.command);
|
|
1520
|
+
const durableInput =
|
|
1521
|
+
activation.status === "failed-closed"
|
|
1522
|
+
? compositionFailureTurnTextV1(ordinaryInput, {
|
|
1523
|
+
attemptedGenerationId: input.compositionGenerationId,
|
|
1524
|
+
...(activation.generation
|
|
1525
|
+
? { generation: activation.generation }
|
|
1526
|
+
: {}),
|
|
1527
|
+
...(activation.failure ? { failure: activation.failure } : {}),
|
|
1528
|
+
quarantined: activation.quarantined,
|
|
1529
|
+
})
|
|
1530
|
+
: ordinaryInput;
|
|
2299
1531
|
return await executeBotTurn({
|
|
2300
1532
|
command: {
|
|
2301
1533
|
...input.command,
|
|
2302
|
-
text:
|
|
1534
|
+
text: durableInput,
|
|
2303
1535
|
},
|
|
2304
1536
|
previousEvents: input.previousEvents,
|
|
2305
1537
|
composition: activation.mounted,
|
|
@@ -2376,9 +1608,9 @@ export class ShellBotBackendContribution {
|
|
|
2376
1608
|
}
|
|
2377
1609
|
|
|
2378
1610
|
/**
|
|
2379
|
-
* Everything a Bot isolate member needs.
|
|
2380
|
-
*
|
|
2381
|
-
*
|
|
1611
|
+
* Everything a Bot isolate member needs. Package identity is attribution
|
|
1612
|
+
* only; Connections and model are resolved once for the Bot and every member
|
|
1613
|
+
* receives the same list.
|
|
2382
1614
|
*/
|
|
2383
1615
|
private async isolateMountOptions(
|
|
2384
1616
|
identity: BotIdentity,
|
|
@@ -2386,7 +1618,7 @@ export class ShellBotBackendContribution {
|
|
|
2386
1618
|
runId: string;
|
|
2387
1619
|
sessionId: string;
|
|
2388
1620
|
generationId: string;
|
|
2389
|
-
|
|
1621
|
+
settings: BotSettingsViewV1;
|
|
2390
1622
|
},
|
|
2391
1623
|
): Promise<ShellIsolateMountOptions | undefined> {
|
|
2392
1624
|
const loader = this.env.BOT_PACKAGES;
|
|
@@ -2401,172 +1633,659 @@ export class ShellBotBackendContribution {
|
|
|
2401
1633
|
}
|
|
2402
1634
|
).exports;
|
|
2403
1635
|
if (!loader || !artifacts || !exports?.BotCapabilities) return undefined;
|
|
1636
|
+
const authority = await this.isolateAuthoritySnapshot(
|
|
1637
|
+
identity,
|
|
1638
|
+
turn.settings,
|
|
1639
|
+
);
|
|
2404
1640
|
const mintCapabilities = exports.BotCapabilities;
|
|
2405
|
-
const assignments = await this.isolateAssignments(turn.assignments);
|
|
2406
1641
|
return {
|
|
2407
1642
|
userId: identity.userId,
|
|
2408
1643
|
runId: turn.runId,
|
|
2409
|
-
// One admitted Turn is one run; the Turn ordinal lives in the session log.
|
|
2410
1644
|
turnId: turn.runId,
|
|
2411
1645
|
loader,
|
|
2412
1646
|
artifacts: createR2PackageArtifactStore(artifacts),
|
|
1647
|
+
manifestFor: (member) => this.requireCompositionMemberManifest(member),
|
|
2413
1648
|
capabilitiesFor: (member) =>
|
|
2414
1649
|
mintCapabilities({
|
|
2415
1650
|
props: {
|
|
2416
1651
|
userId: identity.userId,
|
|
2417
1652
|
botId: identity.botId,
|
|
1653
|
+
runId: turn.runId,
|
|
1654
|
+
sessionId: turn.sessionId,
|
|
1655
|
+
turnId: turn.runId,
|
|
2418
1656
|
generationId: turn.generationId,
|
|
2419
1657
|
packageId: member.packageId,
|
|
2420
|
-
|
|
1658
|
+
connections: structuredClone(authority.connections),
|
|
1659
|
+
...(authority.model
|
|
1660
|
+
? { model: structuredClone(authority.model) }
|
|
1661
|
+
: {}),
|
|
1662
|
+
memory: authority.memory,
|
|
1663
|
+
workspace: authority.workspace,
|
|
2421
1664
|
},
|
|
2422
1665
|
}),
|
|
2423
|
-
bindingDigest: await isolateBindingDigestV1(
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
1666
|
+
bindingDigest: await isolateBindingDigestV1({
|
|
1667
|
+
userId: identity.userId,
|
|
1668
|
+
botId: identity.botId,
|
|
1669
|
+
connections: authority.connections,
|
|
1670
|
+
...(authority.model ? { model: authority.model } : {}),
|
|
1671
|
+
compositionGenerationId: turn.generationId,
|
|
1672
|
+
}),
|
|
2427
1673
|
compatibilityDate: BOT_ISOLATE_COMPATIBILITY_DATE,
|
|
2428
1674
|
};
|
|
2429
1675
|
}
|
|
2430
1676
|
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
1677
|
+
private async isolateAuthoritySnapshot(
|
|
1678
|
+
identity: BotIdentity,
|
|
1679
|
+
settings: BotSettingsViewV1,
|
|
1680
|
+
): Promise<{
|
|
1681
|
+
connections: IsolateConnectionV1[];
|
|
1682
|
+
model?: IsolateModelBindingV1;
|
|
1683
|
+
memory: boolean;
|
|
1684
|
+
workspace: boolean;
|
|
1685
|
+
}> {
|
|
1686
|
+
const [user, application] = await Promise.all([
|
|
1687
|
+
this.userConfiguration(identity).readConfiguration({
|
|
1688
|
+
schemaVersion: 1,
|
|
1689
|
+
userId: identity.userId,
|
|
1690
|
+
}),
|
|
1691
|
+
this.compileApplication(),
|
|
1692
|
+
]);
|
|
1693
|
+
const connections = user.connections.flatMap((connection) =>
|
|
1694
|
+
connection.state === "ready" && connection.generation
|
|
1695
|
+
? [
|
|
1696
|
+
{
|
|
1697
|
+
connectionId: connection.connectionId,
|
|
1698
|
+
packageId: connection.packageId,
|
|
1699
|
+
connectionTypeId: connection.connectionTypeId,
|
|
1700
|
+
displayName: connection.displayName,
|
|
1701
|
+
generation: connection.generation,
|
|
1702
|
+
safeMetadata: structuredClone(connection.safeMetadata),
|
|
1703
|
+
} satisfies IsolateConnectionV1,
|
|
1704
|
+
]
|
|
1705
|
+
: [],
|
|
2443
1706
|
);
|
|
1707
|
+
const effective = resolveEffectiveBotModelV1({
|
|
1708
|
+
bot: settings,
|
|
1709
|
+
user,
|
|
1710
|
+
packages: application.packages.map((pkg) => ({
|
|
1711
|
+
packageId: pkg.id,
|
|
1712
|
+
version: pkg.version,
|
|
1713
|
+
settings: pkg.manifest.configuration?.settings ?? [],
|
|
1714
|
+
capabilities: pkg.manifest.configuration?.capabilities ?? [],
|
|
1715
|
+
connectionTypes: pkg.manifest.configuration?.connectionTypes ?? [],
|
|
1716
|
+
})),
|
|
1717
|
+
});
|
|
1718
|
+
const binding = effective.binding;
|
|
1719
|
+
const model =
|
|
1720
|
+
effective.model &&
|
|
1721
|
+
binding?.state === "ready" &&
|
|
1722
|
+
binding.connection?.generation &&
|
|
1723
|
+
binding.packageId &&
|
|
1724
|
+
binding.providerType
|
|
1725
|
+
? {
|
|
1726
|
+
connectionId: binding.connection.connectionId,
|
|
1727
|
+
packageId: binding.packageId,
|
|
1728
|
+
provider: binding.providerType,
|
|
1729
|
+
providerModelId: effective.model.providerModelId,
|
|
1730
|
+
connectionGeneration: binding.connection.generation,
|
|
1731
|
+
...(binding.connection.modelCatalog?.generation
|
|
1732
|
+
? {
|
|
1733
|
+
catalogGeneration: binding.connection.modelCatalog.generation,
|
|
1734
|
+
}
|
|
1735
|
+
: {}),
|
|
1736
|
+
}
|
|
1737
|
+
: undefined;
|
|
1738
|
+
return {
|
|
1739
|
+
connections,
|
|
1740
|
+
...(model ? { model } : {}),
|
|
1741
|
+
memory: Boolean(this.env.MEMORY_WORKSPACE_FILES),
|
|
1742
|
+
workspace: Boolean(this.env.WORKSPACE_FILES),
|
|
1743
|
+
};
|
|
2444
1744
|
}
|
|
2445
1745
|
|
|
2446
|
-
/**
|
|
2447
|
-
* The Bot Durable Object side of `CAPABILITIES.invokeModel`. Refuses with a
|
|
2448
|
-
* pending decision unless an enabled model Assignment matches; otherwise
|
|
2449
|
-
* records the normalized request and streams through the provider path,
|
|
2450
|
-
* which takes the credential lease on the way.
|
|
2451
|
-
*/
|
|
2452
1746
|
async isolateInvokeModel(
|
|
2453
1747
|
identity: BotIdentity,
|
|
2454
1748
|
input: {
|
|
1749
|
+
runId: string;
|
|
1750
|
+
sessionId: string;
|
|
1751
|
+
turnId: string;
|
|
2455
1752
|
packageId: string;
|
|
2456
1753
|
generationId: string;
|
|
2457
1754
|
request: NormalizedModelRequest;
|
|
2458
1755
|
},
|
|
2459
1756
|
): Promise<IsolateModelInvocationV1> {
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
1757
|
+
if (!this.activeIsolateTurn(input)) {
|
|
1758
|
+
return {
|
|
1759
|
+
status: "unavailable",
|
|
1760
|
+
reason: "the Package is not running in this Bot's active Composition",
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
const settings = await this.ensureBotSettings(identity);
|
|
1764
|
+
const authority = await this.isolateAuthoritySnapshot(identity, settings);
|
|
1765
|
+
let runtime:
|
|
1766
|
+
| {
|
|
1767
|
+
agentPackages: FoundationAgentPackage[];
|
|
1768
|
+
modelSelection: RuntimeModelSelection;
|
|
1769
|
+
}
|
|
1770
|
+
| undefined;
|
|
1771
|
+
if (authority.model) {
|
|
2472
1772
|
try {
|
|
2473
|
-
|
|
1773
|
+
runtime = await this.agentRuntime(identity, settings);
|
|
2474
1774
|
} catch {
|
|
2475
|
-
|
|
1775
|
+
runtime = undefined;
|
|
2476
1776
|
}
|
|
2477
1777
|
}
|
|
2478
|
-
|
|
2479
|
-
settings?.assignments ?? [],
|
|
2480
|
-
);
|
|
2481
|
-
const bound = await this.isolateModelBinding(identity, settings, projected);
|
|
2482
|
-
const assignments = bound
|
|
2483
|
-
? projected.map((assignment) =>
|
|
2484
|
-
assignment.assignmentId === bound.binding.assignmentId
|
|
2485
|
-
? {
|
|
2486
|
-
...assignment,
|
|
2487
|
-
connectionId: bound.binding.connectionId,
|
|
2488
|
-
providerModelId: bound.binding.providerModelId,
|
|
2489
|
-
}
|
|
2490
|
-
: assignment,
|
|
2491
|
-
)
|
|
2492
|
-
: projected;
|
|
2493
|
-
const host = this.isolateCapabilities(
|
|
1778
|
+
return this.isolateCapabilities(
|
|
2494
1779
|
{
|
|
2495
1780
|
botId: identity.botId,
|
|
2496
1781
|
packageId: input.packageId,
|
|
2497
1782
|
generationId: input.generationId,
|
|
2498
1783
|
},
|
|
2499
|
-
|
|
2500
|
-
|
|
1784
|
+
authority,
|
|
1785
|
+
runtime && authority.model
|
|
2501
1786
|
? {
|
|
2502
|
-
|
|
2503
|
-
path: this.isolateModelPath(
|
|
2504
|
-
identity,
|
|
2505
|
-
bound.runtime,
|
|
2506
|
-
input.generationId,
|
|
2507
|
-
),
|
|
1787
|
+
path: this.isolateModelPath(identity, runtime, input.generationId),
|
|
2508
1788
|
}
|
|
2509
1789
|
: undefined,
|
|
2510
|
-
);
|
|
2511
|
-
return await host.invokeModel(input.request);
|
|
1790
|
+
).invokeModel(input.request);
|
|
2512
1791
|
}
|
|
2513
1792
|
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
1793
|
+
async isolateInvokeTool(input: {
|
|
1794
|
+
userId: string;
|
|
1795
|
+
botId: string;
|
|
1796
|
+
runId: string;
|
|
1797
|
+
sessionId: string;
|
|
1798
|
+
turnId: string;
|
|
1799
|
+
packageId: string;
|
|
1800
|
+
generationId: string;
|
|
1801
|
+
request: unknown;
|
|
1802
|
+
}): Promise<IsolateToolOutcomeV1> {
|
|
1803
|
+
const request = decodeIsolateToolRequestV1(input.request);
|
|
1804
|
+
const active = this.activeIsolateTurn(input);
|
|
1805
|
+
if (!active) {
|
|
1806
|
+
return {
|
|
1807
|
+
status: "unavailable",
|
|
1808
|
+
reason: "the Package is not running in this Bot's active Composition",
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
const session = active.mounted.runtime.root.sessions.get(input.sessionId);
|
|
1812
|
+
if (!session) {
|
|
1813
|
+
return {
|
|
1814
|
+
status: "unavailable",
|
|
1815
|
+
reason: "the active Session is unavailable",
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
const started = session.events.findLast(
|
|
1819
|
+
(event) => event.type === "step/start",
|
|
1820
|
+
);
|
|
1821
|
+
const ended = session.events.findLast((event) => event.type === "step/end");
|
|
1822
|
+
if (
|
|
1823
|
+
started?.type !== "step/start" ||
|
|
1824
|
+
(ended?.type === "step/end" &&
|
|
1825
|
+
ended.turn === started.turn &&
|
|
1826
|
+
ended.step === started.step)
|
|
1827
|
+
) {
|
|
1828
|
+
return {
|
|
1829
|
+
status: "unavailable",
|
|
1830
|
+
reason: "the active step is unavailable",
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
const effectId = await this.isolateToolEffectId(
|
|
1834
|
+
input.packageId,
|
|
1835
|
+
request.callId,
|
|
1836
|
+
);
|
|
1837
|
+
const priorCall = session.events.find(
|
|
1838
|
+
(event) =>
|
|
1839
|
+
event.type === "package/tool-call" && event.effectId === effectId,
|
|
1840
|
+
);
|
|
1841
|
+
const priorResult = session.events.find(
|
|
1842
|
+
(event) =>
|
|
1843
|
+
event.type === "package/tool-result" && event.effectId === effectId,
|
|
1844
|
+
);
|
|
1845
|
+
if (priorResult?.type === "package/tool-result") {
|
|
1846
|
+
return {
|
|
1847
|
+
status: "completed",
|
|
1848
|
+
content: priorResult.content,
|
|
1849
|
+
isError: priorResult.isError,
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
if (
|
|
1853
|
+
priorCall?.type === "package/tool-call" &&
|
|
1854
|
+
(priorCall.packageId !== input.packageId ||
|
|
1855
|
+
priorCall.callId !== request.callId ||
|
|
1856
|
+
priorCall.name !== request.name ||
|
|
1857
|
+
JSON.stringify(priorCall.input) !== JSON.stringify(request.input))
|
|
1858
|
+
) {
|
|
1859
|
+
return {
|
|
1860
|
+
status: "unavailable",
|
|
1861
|
+
reason: "the Package tool idempotency key was reused",
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
if (!priorCall) {
|
|
1865
|
+
session.append({
|
|
1866
|
+
type: "package/tool-call",
|
|
1867
|
+
turn: started.turn,
|
|
1868
|
+
step: started.step,
|
|
1869
|
+
effectId,
|
|
1870
|
+
packageId: input.packageId,
|
|
1871
|
+
callId: request.callId,
|
|
1872
|
+
name: request.name,
|
|
1873
|
+
input: request.input,
|
|
1874
|
+
});
|
|
1875
|
+
await session.flush();
|
|
1876
|
+
}
|
|
1877
|
+
const call = {
|
|
1878
|
+
id: request.callId,
|
|
1879
|
+
name: request.name,
|
|
1880
|
+
input: request.input,
|
|
1881
|
+
};
|
|
1882
|
+
const context = {
|
|
1883
|
+
botId: input.botId,
|
|
1884
|
+
agentId: input.botId,
|
|
1885
|
+
sessionId: input.sessionId,
|
|
1886
|
+
compositionGenerationId: input.generationId,
|
|
1887
|
+
effectId,
|
|
1888
|
+
toolCall: call,
|
|
1889
|
+
turnType: active.turnType,
|
|
1890
|
+
...(active.subagentRole ? { subagentRole: active.subagentRole } : {}),
|
|
1891
|
+
signal: active.signal,
|
|
1892
|
+
};
|
|
1893
|
+
const preparation = await active.mounted.runtime.root.tools.prepare(
|
|
1894
|
+
call,
|
|
1895
|
+
context,
|
|
1896
|
+
);
|
|
1897
|
+
let result: { content: string; isError: boolean };
|
|
1898
|
+
if (preparation.kind === "denied") {
|
|
1899
|
+
result = preparation.result;
|
|
1900
|
+
} else {
|
|
1901
|
+
const admitted = await this.admitRunEffect(
|
|
1902
|
+
{ userId: input.userId, botId: input.botId },
|
|
1903
|
+
input.runId,
|
|
1904
|
+
input.sessionId,
|
|
1905
|
+
{ kind: "tool", effectId },
|
|
1906
|
+
);
|
|
1907
|
+
if (!admitted) {
|
|
1908
|
+
result = {
|
|
1909
|
+
content: "The tool effect was stopped before it started.",
|
|
1910
|
+
isError: true,
|
|
2532
1911
|
};
|
|
1912
|
+
} else if (priorCall) {
|
|
1913
|
+
const recovered =
|
|
1914
|
+
await active.mounted.runtime.root.tools.reconcilePrepared(
|
|
1915
|
+
preparation,
|
|
1916
|
+
context,
|
|
1917
|
+
);
|
|
1918
|
+
result =
|
|
1919
|
+
recovered.status === "recovered"
|
|
1920
|
+
? recovered.result
|
|
1921
|
+
: { content: recovered.reason, isError: true };
|
|
1922
|
+
} else {
|
|
1923
|
+
result = await active.mounted.runtime.root.tools.executePrepared(
|
|
1924
|
+
preparation,
|
|
1925
|
+
context,
|
|
1926
|
+
);
|
|
2533
1927
|
}
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
1928
|
+
}
|
|
1929
|
+
session.append({
|
|
1930
|
+
type: "package/tool-result",
|
|
1931
|
+
turn: started.turn,
|
|
1932
|
+
step: started.step,
|
|
1933
|
+
effectId,
|
|
1934
|
+
packageId: input.packageId,
|
|
1935
|
+
callId: request.callId,
|
|
1936
|
+
name: request.name,
|
|
1937
|
+
content: result.content,
|
|
1938
|
+
isError: result.isError,
|
|
1939
|
+
});
|
|
1940
|
+
await session.flush();
|
|
1941
|
+
return {
|
|
1942
|
+
status: "completed",
|
|
1943
|
+
content: result.content,
|
|
1944
|
+
isError: result.isError,
|
|
2540
1945
|
};
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
async isolateMemoryRead(input: {
|
|
1949
|
+
userId: string;
|
|
1950
|
+
botId: string;
|
|
1951
|
+
runId: string;
|
|
1952
|
+
sessionId: string;
|
|
1953
|
+
turnId: string;
|
|
1954
|
+
packageId: string;
|
|
1955
|
+
generationId: string;
|
|
1956
|
+
request: unknown;
|
|
1957
|
+
}): Promise<IsolateMemoryOutcomeV1> {
|
|
1958
|
+
const request = decodeIsolateMemoryReadRequestV1(input.request);
|
|
1959
|
+
const memory = await this.isolateMemoryHost(input, request);
|
|
1960
|
+
if (!memory)
|
|
1961
|
+
return { status: "unavailable", reason: "Memory is unavailable" };
|
|
1962
|
+
return {
|
|
1963
|
+
status: "available",
|
|
1964
|
+
value: await memory.store.read(
|
|
1965
|
+
memoryScopeRootV1(request.scope, memory.owner, request.projectId),
|
|
1966
|
+
),
|
|
1967
|
+
};
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
async isolateMemoryWrite(input: {
|
|
1971
|
+
userId: string;
|
|
1972
|
+
botId: string;
|
|
1973
|
+
runId: string;
|
|
1974
|
+
sessionId: string;
|
|
1975
|
+
turnId: string;
|
|
1976
|
+
packageId: string;
|
|
1977
|
+
generationId: string;
|
|
1978
|
+
request: unknown;
|
|
1979
|
+
}): Promise<IsolateMemoryOutcomeV1> {
|
|
1980
|
+
const request = decodeIsolateMemoryWriteRequestV1(input.request);
|
|
1981
|
+
const memory = await this.isolateMemoryHost(input, request);
|
|
1982
|
+
if (!memory?.writer) {
|
|
1983
|
+
return { status: "unavailable", reason: "Memory is unavailable" };
|
|
1984
|
+
}
|
|
1985
|
+
return {
|
|
1986
|
+
status: "available",
|
|
1987
|
+
value: await memory.store.write({
|
|
1988
|
+
root: memoryScopeRootV1(request.scope, memory.owner, request.projectId),
|
|
1989
|
+
tier: request.tier ?? "log",
|
|
1990
|
+
fact: request.fact,
|
|
1991
|
+
writer: {
|
|
1992
|
+
kind: "bot",
|
|
1993
|
+
botId: input.botId,
|
|
1994
|
+
...memory.writer,
|
|
1995
|
+
},
|
|
1996
|
+
}),
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
async isolateMemoryForget(input: {
|
|
2001
|
+
userId: string;
|
|
2002
|
+
botId: string;
|
|
2003
|
+
runId: string;
|
|
2004
|
+
sessionId: string;
|
|
2005
|
+
turnId: string;
|
|
2006
|
+
packageId: string;
|
|
2007
|
+
generationId: string;
|
|
2008
|
+
request: unknown;
|
|
2009
|
+
}): Promise<IsolateMemoryOutcomeV1> {
|
|
2010
|
+
const request = decodeIsolateMemoryWriteRequestV1(input.request);
|
|
2011
|
+
const memory = await this.isolateMemoryHost(input, request);
|
|
2012
|
+
if (!memory?.writer) {
|
|
2013
|
+
return { status: "unavailable", reason: "Memory is unavailable" };
|
|
2014
|
+
}
|
|
2015
|
+
return {
|
|
2016
|
+
status: "available",
|
|
2017
|
+
value: await memory.store.forget({
|
|
2018
|
+
root: memoryScopeRootV1(request.scope, memory.owner, request.projectId),
|
|
2019
|
+
fact: request.fact,
|
|
2020
|
+
writer: {
|
|
2021
|
+
kind: "bot",
|
|
2022
|
+
botId: input.botId,
|
|
2023
|
+
...memory.writer,
|
|
2024
|
+
},
|
|
2025
|
+
}),
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
async isolateWorkspaceRead(
|
|
2030
|
+
input: IsolateCallScopeV1,
|
|
2031
|
+
): Promise<IsolateWorkspaceOutcomeV1> {
|
|
2032
|
+
const active = this.activeIsolateTurn(input);
|
|
2033
|
+
const files = this.env.WORKSPACE_FILES;
|
|
2034
|
+
if (!active || !files) {
|
|
2035
|
+
return { status: "unavailable", reason: "Workspace is unavailable" };
|
|
2036
|
+
}
|
|
2037
|
+
const path = this.isolateWorkspacePath(
|
|
2038
|
+
input.userId,
|
|
2039
|
+
decodeIsolateWorkspacePathV1(input.request),
|
|
2040
|
+
);
|
|
2041
|
+
return { status: "available", value: await files.read(path) };
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
async isolateWorkspaceList(
|
|
2045
|
+
input: IsolateCallScopeV1,
|
|
2046
|
+
): Promise<IsolateWorkspaceOutcomeV1> {
|
|
2047
|
+
const active = this.activeIsolateTurn(input);
|
|
2048
|
+
const files = this.env.WORKSPACE_FILES;
|
|
2049
|
+
if (!active || !files) {
|
|
2050
|
+
return { status: "unavailable", reason: "Workspace is unavailable" };
|
|
2051
|
+
}
|
|
2052
|
+
const request = decodeIsolateWorkspaceListRequestV1(input.request);
|
|
2053
|
+
const root = this.isolateWorkspaceRoot(input.userId, request.root);
|
|
2054
|
+
return {
|
|
2055
|
+
status: "available",
|
|
2056
|
+
value: await files.list({
|
|
2057
|
+
root,
|
|
2058
|
+
...(request.prefix === undefined ? {} : { prefix: request.prefix }),
|
|
2059
|
+
...(request.cursor === undefined ? {} : { cursor: request.cursor }),
|
|
2060
|
+
...(request.limit === undefined ? {} : { limit: request.limit }),
|
|
2061
|
+
}),
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
async isolateWorkspaceStat(
|
|
2066
|
+
input: IsolateCallScopeV1,
|
|
2067
|
+
): Promise<IsolateWorkspaceOutcomeV1> {
|
|
2068
|
+
const active = this.activeIsolateTurn(input);
|
|
2069
|
+
const files = this.env.WORKSPACE_FILES;
|
|
2070
|
+
if (!active || !files) {
|
|
2071
|
+
return { status: "unavailable", reason: "Workspace is unavailable" };
|
|
2072
|
+
}
|
|
2073
|
+
const path = this.isolateWorkspacePath(
|
|
2074
|
+
input.userId,
|
|
2075
|
+
decodeIsolateWorkspacePathV1(input.request),
|
|
2076
|
+
);
|
|
2077
|
+
return { status: "available", value: await files.stat(path) };
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
async isolateWorkspaceWrite(
|
|
2081
|
+
input: IsolateCallScopeV1,
|
|
2082
|
+
): Promise<IsolateWorkspaceOutcomeV1> {
|
|
2083
|
+
const active = this.activeIsolateTurn(input);
|
|
2084
|
+
const files = this.env.WORKSPACE_FILES;
|
|
2085
|
+
if (!active || !files) {
|
|
2086
|
+
return { status: "unavailable", reason: "Workspace is unavailable" };
|
|
2087
|
+
}
|
|
2088
|
+
const request = decodeIsolateWorkspaceWriteRequestV1(input.request);
|
|
2089
|
+
return {
|
|
2090
|
+
status: "available",
|
|
2091
|
+
value: await files.write({
|
|
2092
|
+
path: this.isolateWorkspacePath(input.userId, request.path),
|
|
2093
|
+
bytes: request.bytes,
|
|
2094
|
+
writer: this.isolateWorkspaceWriter(input),
|
|
2095
|
+
expectedGenerationId: request.expectedGenerationId,
|
|
2096
|
+
...(request.mediaType ? { mediaType: request.mediaType } : {}),
|
|
2097
|
+
}),
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
async isolateWorkspaceDelete(
|
|
2102
|
+
input: IsolateCallScopeV1,
|
|
2103
|
+
): Promise<IsolateWorkspaceOutcomeV1> {
|
|
2104
|
+
const active = this.activeIsolateTurn(input);
|
|
2105
|
+
const files = this.env.WORKSPACE_FILES;
|
|
2106
|
+
if (!active || !files) {
|
|
2107
|
+
return { status: "unavailable", reason: "Workspace is unavailable" };
|
|
2108
|
+
}
|
|
2109
|
+
const request = decodeIsolateWorkspaceDeleteRequestV1(input.request);
|
|
2110
|
+
return {
|
|
2111
|
+
status: "available",
|
|
2112
|
+
value: await files.delete({
|
|
2113
|
+
path: this.isolateWorkspacePath(input.userId, request.path),
|
|
2114
|
+
writer: this.isolateWorkspaceWriter(input),
|
|
2115
|
+
expectedGenerationId: request.expectedGenerationId,
|
|
2116
|
+
}),
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
async isolateConnection(
|
|
2121
|
+
input: IsolateCallScopeV1,
|
|
2122
|
+
): Promise<IsolateConnectionOutcomeV1> {
|
|
2123
|
+
if (!this.activeIsolateTurn(input) || typeof input.request !== "string") {
|
|
2124
|
+
return { status: "unavailable", reason: "the Connection is unavailable" };
|
|
2125
|
+
}
|
|
2126
|
+
const identity = { userId: input.userId, botId: input.botId };
|
|
2127
|
+
const user = await this.userConfiguration(identity).readConfiguration({
|
|
2128
|
+
schemaVersion: 1,
|
|
2129
|
+
userId: input.userId,
|
|
2130
|
+
});
|
|
2131
|
+
const connection = user.connections.find(
|
|
2132
|
+
(candidate) =>
|
|
2133
|
+
candidate.connectionId === input.request &&
|
|
2134
|
+
candidate.state === "ready" &&
|
|
2135
|
+
candidate.generation,
|
|
2136
|
+
);
|
|
2137
|
+
if (!connection?.generation) {
|
|
2138
|
+
await this.authority.recordNotification({
|
|
2139
|
+
notificationId: `package-connection-unavailable:${input.runId}:${input.packageId}:${input.request}`,
|
|
2140
|
+
runId: input.runId,
|
|
2141
|
+
createdAt: new Date().toISOString(),
|
|
2142
|
+
title: "Connection unavailable",
|
|
2143
|
+
body: `Package "${input.packageId}" could not use Connection "${input.request}". The User can enable or repair it on Connections.`.slice(
|
|
2144
|
+
0,
|
|
2145
|
+
240,
|
|
2146
|
+
),
|
|
2147
|
+
});
|
|
2148
|
+
return { status: "unavailable", reason: "the Connection is unavailable" };
|
|
2149
|
+
}
|
|
2150
|
+
return {
|
|
2151
|
+
status: "available",
|
|
2152
|
+
leaseId: crypto.randomUUID(),
|
|
2153
|
+
connectionId: connection.connectionId,
|
|
2154
|
+
generation: connection.generation,
|
|
2155
|
+
expiresAt: new Date(Date.now() + 5 * 60_000).toISOString(),
|
|
2156
|
+
};
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
async isolateNotify(
|
|
2160
|
+
input: IsolateCallScopeV1,
|
|
2161
|
+
): Promise<IsolateNotificationOutcomeV1> {
|
|
2162
|
+
if (!this.activeIsolateTurn(input)) {
|
|
2163
|
+
return { status: "unavailable", reason: "notifications are unavailable" };
|
|
2164
|
+
}
|
|
2165
|
+
const request = decodeIsolateNotificationRequestV1(input.request);
|
|
2166
|
+
await this.authority.recordNotification({
|
|
2167
|
+
notificationId: `package:${input.packageId}:${request.notificationId}`,
|
|
2168
|
+
runId: input.runId,
|
|
2169
|
+
createdAt: new Date().toISOString(),
|
|
2170
|
+
title: request.title,
|
|
2171
|
+
body: request.body,
|
|
2172
|
+
});
|
|
2173
|
+
return { status: "recorded" };
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
async isolateSchedule(
|
|
2177
|
+
input: IsolateCallScopeV1,
|
|
2178
|
+
): Promise<IsolateToolOutcomeV1> {
|
|
2179
|
+
const request = decodeIsolateScheduleRequestV1(input.request);
|
|
2180
|
+
return this.isolateInvokeTool({
|
|
2181
|
+
...input,
|
|
2182
|
+
request: {
|
|
2183
|
+
callId: request.callId,
|
|
2184
|
+
name: "routine_manage",
|
|
2185
|
+
input: request.input,
|
|
2186
|
+
},
|
|
2187
|
+
});
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
private async isolateMemoryHost(
|
|
2191
|
+
input: IsolateCallScopeV1,
|
|
2192
|
+
request: { scope: "bot" | "user" | "project"; projectId?: string },
|
|
2193
|
+
) {
|
|
2194
|
+
if (!this.activeIsolateTurn(input)) return undefined;
|
|
2195
|
+
const host = createBotMemoryHost(
|
|
2196
|
+
{ userId: input.userId, botId: input.botId },
|
|
2197
|
+
{
|
|
2198
|
+
runId: input.runId,
|
|
2199
|
+
sessionId: input.sessionId,
|
|
2200
|
+
turnId: input.turnId,
|
|
2201
|
+
},
|
|
2202
|
+
this.env,
|
|
2203
|
+
);
|
|
2204
|
+
if (!host) return undefined;
|
|
2205
|
+
if (request.scope === "project") {
|
|
2206
|
+
const projects = await host.projects?.joined();
|
|
2207
|
+
if (
|
|
2208
|
+
!request.projectId ||
|
|
2209
|
+
!projects?.some((project) => project.projectId === request.projectId)
|
|
2210
|
+
) {
|
|
2211
|
+
return undefined;
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
return host;
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
private activeIsolateTurn(input: {
|
|
2218
|
+
runId: string;
|
|
2219
|
+
sessionId: string;
|
|
2220
|
+
turnId: string;
|
|
2221
|
+
packageId: string;
|
|
2222
|
+
generationId: string;
|
|
2223
|
+
}) {
|
|
2224
|
+
const active = this.activeTurn;
|
|
2225
|
+
if (
|
|
2226
|
+
!active ||
|
|
2227
|
+
active.runId !== input.runId ||
|
|
2228
|
+
active.sessionId !== input.sessionId ||
|
|
2229
|
+
active.turnId !== input.turnId ||
|
|
2230
|
+
active.generationId !== input.generationId ||
|
|
2231
|
+
!active.mounted.generation.members.some(
|
|
2232
|
+
(member) => member.packageId === input.packageId && member.artifact,
|
|
2233
|
+
)
|
|
2234
|
+
) {
|
|
2544
2235
|
return undefined;
|
|
2545
2236
|
}
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2237
|
+
return active;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
private async isolateToolEffectId(
|
|
2241
|
+
packageId: string,
|
|
2242
|
+
callId: string,
|
|
2243
|
+
): Promise<string> {
|
|
2244
|
+
const digest = await crypto.subtle.digest(
|
|
2245
|
+
"SHA-256",
|
|
2246
|
+
new TextEncoder().encode(`${packageId}\0${callId}`),
|
|
2247
|
+
);
|
|
2248
|
+
const hex = [...new Uint8Array(digest)]
|
|
2249
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
2250
|
+
.join("");
|
|
2251
|
+
return `package-tool:${hex}`;
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
private isolateWorkspaceRoot(
|
|
2255
|
+
userId: string,
|
|
2256
|
+
root: ReturnType<typeof decodeIsolateWorkspacePathV1>["root"],
|
|
2257
|
+
) {
|
|
2258
|
+
return decodeWorkspaceRootV1(
|
|
2259
|
+
root.kind === "user-instructions"
|
|
2260
|
+
? { kind: root.kind, userId }
|
|
2261
|
+
: root.kind === "bot-instructions"
|
|
2262
|
+
? { kind: root.kind, userId, botId: root.botId }
|
|
2263
|
+
: {
|
|
2264
|
+
kind: root.kind,
|
|
2265
|
+
userId,
|
|
2266
|
+
packageId: root.packageId,
|
|
2267
|
+
rootId: root.rootId,
|
|
2268
|
+
},
|
|
2552
2269
|
);
|
|
2553
|
-
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
private isolateWorkspacePath(
|
|
2273
|
+
userId: string,
|
|
2274
|
+
path: ReturnType<typeof decodeIsolateWorkspacePathV1>,
|
|
2275
|
+
): WorkspacePathV1 {
|
|
2276
|
+
return decodeWorkspacePathV1({
|
|
2277
|
+
root: this.isolateWorkspaceRoot(userId, path.root),
|
|
2278
|
+
path: path.path,
|
|
2279
|
+
});
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
private isolateWorkspaceWriter(input: IsolateCallScopeV1) {
|
|
2554
2283
|
return {
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
connectionId,
|
|
2561
|
-
provider: selection.provider,
|
|
2562
|
-
providerModelId: selection.model,
|
|
2563
|
-
...(selection.connectionGeneration
|
|
2564
|
-
? { connectionGeneration: selection.connectionGeneration }
|
|
2565
|
-
: {}),
|
|
2566
|
-
...(selection.catalogGeneration
|
|
2567
|
-
? { catalogGeneration: selection.catalogGeneration }
|
|
2568
|
-
: {}),
|
|
2569
|
-
},
|
|
2284
|
+
kind: "bot" as const,
|
|
2285
|
+
botId: input.botId,
|
|
2286
|
+
sessionId: input.sessionId,
|
|
2287
|
+
turnId: input.turnId,
|
|
2288
|
+
runId: input.runId,
|
|
2570
2289
|
};
|
|
2571
2290
|
}
|
|
2572
2291
|
|
|
@@ -2575,58 +2294,35 @@ export class ShellBotBackendContribution {
|
|
|
2575
2294
|
botId: string;
|
|
2576
2295
|
packageId: string;
|
|
2577
2296
|
generationId: string;
|
|
2578
|
-
request?: unknown;
|
|
2579
2297
|
},
|
|
2580
|
-
|
|
2581
|
-
|
|
2298
|
+
authority: {
|
|
2299
|
+
connections: readonly IsolateConnectionV1[];
|
|
2300
|
+
model?: IsolateModelBindingV1;
|
|
2301
|
+
memory: boolean;
|
|
2302
|
+
workspace: boolean;
|
|
2303
|
+
},
|
|
2304
|
+
model?: { path: IsolateModelPath },
|
|
2582
2305
|
): IsolateCapabilityHost {
|
|
2583
2306
|
return createIsolateCapabilityHost({
|
|
2584
2307
|
storage: {
|
|
2585
2308
|
put: (key, value) => this.ctx.storage.put(key, value),
|
|
2586
|
-
get: (key) => this.ctx.storage.get(key),
|
|
2587
2309
|
list: (options) => this.ctx.storage.list(options),
|
|
2588
2310
|
},
|
|
2589
2311
|
botId: scope.botId,
|
|
2590
2312
|
packageId: scope.packageId,
|
|
2591
2313
|
generationId: scope.generationId,
|
|
2592
|
-
|
|
2593
|
-
...(model ? { modelBinding:
|
|
2314
|
+
connections: authority.connections,
|
|
2315
|
+
...(authority.model ? { modelBinding: authority.model } : {}),
|
|
2316
|
+
...(model ? { modelPath: model.path } : {}),
|
|
2317
|
+
memory: authority.memory,
|
|
2318
|
+
workspace: authority.workspace,
|
|
2594
2319
|
});
|
|
2595
2320
|
}
|
|
2596
2321
|
|
|
2597
|
-
/** The Bot's enabled Assignments, projected onto the isolate capability DTO. */
|
|
2598
|
-
private async isolateAssignments(
|
|
2599
|
-
assignments: readonly CapabilityAssignmentView[],
|
|
2600
|
-
): Promise<IsolateAssignmentV1[]> {
|
|
2601
|
-
const application = await this.compileApplication();
|
|
2602
|
-
const projected: IsolateAssignmentV1[] = [];
|
|
2603
|
-
for (const assignment of assignments) {
|
|
2604
|
-
if (assignment.state !== "enabled") continue;
|
|
2605
|
-
const capability = application.packages
|
|
2606
|
-
.find((candidate) => candidate.id === assignment.packageId)
|
|
2607
|
-
?.manifest.configuration?.capabilities.find(
|
|
2608
|
-
(candidate) => candidate.id === assignment.capabilityId,
|
|
2609
|
-
);
|
|
2610
|
-
if (!capability) continue;
|
|
2611
|
-
projected.push({
|
|
2612
|
-
assignmentId: assignment.assignmentId,
|
|
2613
|
-
packageId: assignment.packageId,
|
|
2614
|
-
capabilityId: assignment.capabilityId,
|
|
2615
|
-
kind: capability.kind,
|
|
2616
|
-
...(assignment.connectionId
|
|
2617
|
-
? { connectionId: assignment.connectionId }
|
|
2618
|
-
: {}),
|
|
2619
|
-
});
|
|
2620
|
-
}
|
|
2621
|
-
return projected;
|
|
2622
|
-
}
|
|
2623
|
-
|
|
2624
2322
|
/**
|
|
2625
2323
|
* Streams through the pinned Composition's mounted `ctx.llm` — the same
|
|
2626
2324
|
* provider path a Turn uses, so whichever provider Plugin serves the request
|
|
2627
|
-
* is the one that takes the credential lease.
|
|
2628
|
-
* durable model binding resolved to, so the Package that streams is the
|
|
2629
|
-
* Package the Assignment names.
|
|
2325
|
+
* is the one that takes the credential lease.
|
|
2630
2326
|
*/
|
|
2631
2327
|
private isolateModelPath(
|
|
2632
2328
|
identity: BotIdentity,
|
|
@@ -2652,8 +2348,6 @@ export class ShellBotBackendContribution {
|
|
|
2652
2348
|
sessionEvents: [],
|
|
2653
2349
|
agentPackages: runtime.agentPackages,
|
|
2654
2350
|
modelSelection: runtime.modelSelection,
|
|
2655
|
-
// An isolate model invocation is not an admitted Turn, so there is
|
|
2656
|
-
// no run to fence it against; it is admitted by its Assignment.
|
|
2657
2351
|
admitEffect: () => Promise.resolve(true),
|
|
2658
2352
|
}).mount(generation, signal);
|
|
2659
2353
|
try {
|
|
@@ -2676,29 +2370,22 @@ export class ShellBotBackendContribution {
|
|
|
2676
2370
|
private async resolveAdmissionSnapshot(
|
|
2677
2371
|
command: OwnedBotTurnCommand,
|
|
2678
2372
|
): Promise<BotSettingsViewV1> {
|
|
2679
|
-
|
|
2680
|
-
return {
|
|
2681
|
-
...context.settings,
|
|
2682
|
-
assignments: context.plan.assignments,
|
|
2683
|
-
} satisfies BotSettingsViewV1;
|
|
2373
|
+
return (await this.resolveExecutionContext(command)).settings;
|
|
2684
2374
|
}
|
|
2685
2375
|
|
|
2686
2376
|
private async admittedSnapshot(
|
|
2687
2377
|
transaction: DurableObjectTransaction,
|
|
2688
2378
|
resolved: BotSettingsViewV1,
|
|
2689
2379
|
): Promise<BotSettingsViewV1> {
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
resolved
|
|
2693
|
-
|
|
2380
|
+
const stored = await transaction.get<unknown>(BOT_CONFIGURATION_KEY);
|
|
2381
|
+
return stored === undefined
|
|
2382
|
+
? resolved
|
|
2383
|
+
: decodeBotSettingsViewV1(migrateStoredBotSettingsV1(stored));
|
|
2694
2384
|
}
|
|
2695
2385
|
|
|
2696
2386
|
private async scheduledDeadlines(
|
|
2697
2387
|
transaction: DurableObjectTransaction,
|
|
2698
2388
|
): Promise<number[]> {
|
|
2699
|
-
const sagas = await transaction.list<unknown>({
|
|
2700
|
-
prefix: ASSIGNMENT_SAGA_PREFIX,
|
|
2701
|
-
});
|
|
2702
2389
|
// A pending approval is a deadline like any other: the object already owns
|
|
2703
2390
|
// one alarm, and expiry rides it rather than inventing a second clock.
|
|
2704
2391
|
const approvals = await transaction.list<unknown>({
|
|
@@ -2711,9 +2398,6 @@ export class ShellBotBackendContribution {
|
|
|
2711
2398
|
expiries.push(Date.parse(approval.expiresAt));
|
|
2712
2399
|
}
|
|
2713
2400
|
return [
|
|
2714
|
-
...[...sagas.values()].map(
|
|
2715
|
-
(stored) => requireStoredAssignmentSaga(stored).deadlineAt,
|
|
2716
|
-
),
|
|
2717
2401
|
...(await this.routineScheduler.deadlines(transaction)),
|
|
2718
2402
|
...expiries.filter((at) => Number.isFinite(at)),
|
|
2719
2403
|
// A dispatched task's 30-minute lifetime, and a child's own owed Turn,
|
|
@@ -2761,38 +2445,12 @@ export class ShellBotBackendContribution {
|
|
|
2761
2445
|
private async deferScheduledWork(
|
|
2762
2446
|
transaction: DurableObjectTransaction,
|
|
2763
2447
|
): Promise<void> {
|
|
2764
|
-
|
|
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.
|
|
2448
|
+
// A Routine's deadline is a debt, so the scheduler holds it rather than
|
|
2449
|
+
// moving it while other durable work remains in flight.
|
|
2775
2450
|
await this.routineScheduler.defer(transaction);
|
|
2776
2451
|
}
|
|
2777
2452
|
|
|
2778
2453
|
private async settleScheduledWork(): Promise<void> {
|
|
2779
|
-
const stored = await this.ctx.storage.list<unknown>({
|
|
2780
|
-
prefix: ASSIGNMENT_SAGA_PREFIX,
|
|
2781
|
-
});
|
|
2782
|
-
for (const value of stored.values()) {
|
|
2783
|
-
const saga = requireStoredAssignmentSaga(value);
|
|
2784
|
-
try {
|
|
2785
|
-
await this.reconcileStoredAssignmentSaga(
|
|
2786
|
-
{ userId: saga.userId, botId: saga.botId },
|
|
2787
|
-
saga.commandId,
|
|
2788
|
-
);
|
|
2789
|
-
} catch (error) {
|
|
2790
|
-
console.error(
|
|
2791
|
-
"Assignment saga remains durably scheduled after reconciliation failure",
|
|
2792
|
-
error instanceof Error ? error.message : "unknown failure",
|
|
2793
|
-
);
|
|
2794
|
-
}
|
|
2795
|
-
}
|
|
2796
2454
|
await this.settleRoutineFirings();
|
|
2797
2455
|
await this.runOwedSubagentTurns();
|
|
2798
2456
|
await this.reconcileOverdueTasks();
|
|
@@ -2867,7 +2525,7 @@ export class ShellBotBackendContribution {
|
|
|
2867
2525
|
*
|
|
2868
2526
|
* The Bot Durable Object cannot serialize across a User's Bots — they are
|
|
2869
2527
|
* separate objects — and the User Durable Object owns the Computer
|
|
2870
|
-
*
|
|
2528
|
+
* allocation but not the desktop. The host's `control` op is already the
|
|
2871
2529
|
* single writer that serializes human takeover, so it is where a second
|
|
2872
2530
|
* opinion cannot exist (plan decision 3): the Bot records the intent, the
|
|
2873
2531
|
* host grants or refuses, and the refusal names the holder.
|
|
@@ -3839,12 +3497,15 @@ export class ShellBotBackendContribution {
|
|
|
3839
3497
|
private authoringHost(
|
|
3840
3498
|
identity: BotIdentity,
|
|
3841
3499
|
turn: { runId: string; turnId: string },
|
|
3500
|
+
currentToolNames: () => readonly string[],
|
|
3501
|
+
mountedGeneration: () => CompositionGenerationV1 | undefined,
|
|
3842
3502
|
): PackageAuthoringHost {
|
|
3843
3503
|
const artifacts = this.env.APPLICATION_ARTIFACTS;
|
|
3844
3504
|
return createPackageAuthoringHost({
|
|
3845
3505
|
storage: {
|
|
3846
3506
|
get: (key) => this.ctx.storage.get(key),
|
|
3847
3507
|
put: (entries) => this.ctx.storage.put(entries),
|
|
3508
|
+
list: (options) => this.ctx.storage.list(options),
|
|
3848
3509
|
},
|
|
3849
3510
|
composition: this.authority.composition,
|
|
3850
3511
|
...(this.env.PACKAGE_BUNDLER
|
|
@@ -3859,6 +3520,43 @@ export class ShellBotBackendContribution {
|
|
|
3859
3520
|
runId: turn.runId,
|
|
3860
3521
|
turnId: turn.turnId,
|
|
3861
3522
|
compatibilityDate: BOT_ISOLATE_COMPATIBILITY_DATE,
|
|
3523
|
+
currentToolNames,
|
|
3524
|
+
mountedGeneration,
|
|
3525
|
+
activationFailures: this.authority.compositionFailures,
|
|
3526
|
+
});
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
/** Catalog reads plus the two-authority mutation seam for one admitted Turn. */
|
|
3530
|
+
private packageCatalogHost(
|
|
3531
|
+
identity: BotIdentity,
|
|
3532
|
+
turn: { runId: string; turnId: string },
|
|
3533
|
+
): CatalogAwarePackageCatalogHost | undefined {
|
|
3534
|
+
if (!this.env.PACKAGE_CATALOG || !this.env.APPLICATION_ARTIFACTS) {
|
|
3535
|
+
return undefined;
|
|
3536
|
+
}
|
|
3537
|
+
const user = this.userConfiguration(identity);
|
|
3538
|
+
return createPackageCatalogHost({
|
|
3539
|
+
storage: {
|
|
3540
|
+
get: (key) => this.ctx.storage.get(key),
|
|
3541
|
+
put: (entries) => this.ctx.storage.put(entries),
|
|
3542
|
+
},
|
|
3543
|
+
composition: this.authority.composition,
|
|
3544
|
+
catalog: createR2BotPackageCatalogReader(
|
|
3545
|
+
this.env.PACKAGE_CATALOG,
|
|
3546
|
+
this.env.APPLICATION_ARTIFACTS,
|
|
3547
|
+
),
|
|
3548
|
+
user: {
|
|
3549
|
+
read: () =>
|
|
3550
|
+
user.readConfiguration({
|
|
3551
|
+
schemaVersion: 1,
|
|
3552
|
+
userId: identity.userId,
|
|
3553
|
+
}),
|
|
3554
|
+
execute: (command) => user.executeConfiguration(command),
|
|
3555
|
+
},
|
|
3556
|
+
userId: identity.userId,
|
|
3557
|
+
botId: identity.botId,
|
|
3558
|
+
runId: turn.runId,
|
|
3559
|
+
turnId: turn.turnId,
|
|
3862
3560
|
});
|
|
3863
3561
|
}
|
|
3864
3562
|
|
|
@@ -3883,8 +3581,12 @@ export class ShellBotBackendContribution {
|
|
|
3883
3581
|
/** The task a child Turn is running, in a Subagent Durable Object. */
|
|
3884
3582
|
subagentTaskId?: string;
|
|
3885
3583
|
},
|
|
3584
|
+
currentToolNames: () => readonly string[] = () => [],
|
|
3585
|
+
mountedGeneration: () => CompositionGenerationV1 | undefined = () =>
|
|
3586
|
+
undefined,
|
|
3886
3587
|
): Promise<{
|
|
3887
3588
|
agentPackages: FoundationAgentPackage[];
|
|
3589
|
+
capabilities: EnabledCapabilityV1[];
|
|
3888
3590
|
modelSelection: RuntimeModelSelection;
|
|
3889
3591
|
}> {
|
|
3890
3592
|
const userConfiguration = this.userConfiguration(identity);
|
|
@@ -3896,32 +3598,42 @@ export class ShellBotBackendContribution {
|
|
|
3896
3598
|
const packageDefinitions = application.packages.map((pkg) => ({
|
|
3897
3599
|
packageId: pkg.id,
|
|
3898
3600
|
version: pkg.version,
|
|
3601
|
+
settings: pkg.manifest.configuration?.settings ?? [],
|
|
3899
3602
|
capabilities: pkg.manifest.configuration?.capabilities ?? [],
|
|
3900
3603
|
connectionTypes: pkg.manifest.configuration?.connectionTypes ?? [],
|
|
3901
3604
|
}));
|
|
3902
|
-
const plan =
|
|
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
|
-
});
|
|
3605
|
+
const plan = resolveBotExecutionPlanV1({
|
|
3606
|
+
bot: settings,
|
|
3607
|
+
user,
|
|
3608
|
+
packages: packageDefinitions,
|
|
3609
|
+
});
|
|
3915
3610
|
const readSecret = (name: string) => {
|
|
3916
3611
|
// SAFETY: Worker secrets are dynamic string bindings not enumerable in Env.
|
|
3917
3612
|
const value = (this.env as unknown as Record<string, unknown>)[name];
|
|
3918
3613
|
return typeof value === "string" ? value : undefined;
|
|
3919
3614
|
};
|
|
3920
|
-
const
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3615
|
+
const authorizeEnabledConnection = (
|
|
3616
|
+
capability: EnabledCapabilityV1,
|
|
3617
|
+
): Promise<ConnectionView> => {
|
|
3618
|
+
const enabled = plan.capabilities.some(
|
|
3619
|
+
(candidate) =>
|
|
3620
|
+
candidate.packageId === capability.packageId &&
|
|
3621
|
+
candidate.capabilityId === capability.capabilityId &&
|
|
3622
|
+
candidate.connectionId === capability.connectionId,
|
|
3623
|
+
);
|
|
3624
|
+
const connection = user.connections.find(
|
|
3625
|
+
(candidate) =>
|
|
3626
|
+
candidate.connectionId === capability.connectionId &&
|
|
3627
|
+
candidate.packageId === capability.packageId &&
|
|
3628
|
+
candidate.state === "ready",
|
|
3629
|
+
);
|
|
3630
|
+
if (!enabled || !connection) {
|
|
3631
|
+
return Promise.reject(
|
|
3632
|
+
new Error("Enabled effect is no longer authorized"),
|
|
3633
|
+
);
|
|
3634
|
+
}
|
|
3635
|
+
return Promise.resolve(structuredClone(connection));
|
|
3636
|
+
};
|
|
3925
3637
|
// The Package-level settings this User holds, resolved against the manifest
|
|
3926
3638
|
// of the Composition this Turn is pinned to. They come from the same `user`
|
|
3927
3639
|
// read the rest of this Composition uses, so a value the User changed is
|
|
@@ -3929,7 +3641,7 @@ export class ShellBotBackendContribution {
|
|
|
3929
3641
|
// one already running.
|
|
3930
3642
|
const packageSettings = (
|
|
3931
3643
|
packageId: string,
|
|
3932
|
-
): Record<string,
|
|
3644
|
+
): Record<string, PackageSettingValueV1> => {
|
|
3933
3645
|
const installation = user.packages.find(
|
|
3934
3646
|
(candidate) => candidate.packageId === packageId,
|
|
3935
3647
|
);
|
|
@@ -3943,6 +3655,15 @@ export class ShellBotBackendContribution {
|
|
|
3943
3655
|
installation?.values,
|
|
3944
3656
|
);
|
|
3945
3657
|
};
|
|
3658
|
+
const primitivePackageSettings = (
|
|
3659
|
+
packageId: string,
|
|
3660
|
+
): Record<string, string | number | boolean> =>
|
|
3661
|
+
Object.fromEntries(
|
|
3662
|
+
Object.entries(packageSettings(packageId)).filter(
|
|
3663
|
+
(entry): entry is [string, string | number | boolean] =>
|
|
3664
|
+
typeof entry[1] !== "object",
|
|
3665
|
+
),
|
|
3666
|
+
);
|
|
3946
3667
|
// The `image.model` Package setting, already checked against the enum the
|
|
3947
3668
|
// Image Package's manifest declares.
|
|
3948
3669
|
const configuredImageModel = packageSettings("image").model;
|
|
@@ -3953,20 +3674,36 @@ export class ShellBotBackendContribution {
|
|
|
3953
3674
|
const machineSeam = turn ? this.machineSeam(identity) : undefined;
|
|
3954
3675
|
const messagesGate = machineSeam
|
|
3955
3676
|
? await resolveBotMachineMessagesGateV1(
|
|
3956
|
-
|
|
3677
|
+
primitivePackageSettings("machine-messages"),
|
|
3957
3678
|
() => machineSeam.list(),
|
|
3958
3679
|
)
|
|
3959
3680
|
: ({ status: "off" } as const);
|
|
3960
3681
|
// Filled in once this Turn's model binding is resolved, below. The tool
|
|
3961
3682
|
// and the prompt section both read it lazily, from inside the Turn.
|
|
3962
3683
|
const subagentModels: SubagentModelOptionV1[] = [];
|
|
3684
|
+
const packageCatalog = turn
|
|
3685
|
+
? this.packageCatalogHost(identity, turn)
|
|
3686
|
+
: undefined;
|
|
3687
|
+
const baseAuthoring = turn
|
|
3688
|
+
? this.authoringHost(identity, turn, currentToolNames, mountedGeneration)
|
|
3689
|
+
: undefined;
|
|
3690
|
+
const authoring: PackageAuthoringHost | undefined =
|
|
3691
|
+
baseAuthoring && packageCatalog
|
|
3692
|
+
? {
|
|
3693
|
+
...baseAuthoring,
|
|
3694
|
+
undo: async (request) =>
|
|
3695
|
+
(await packageCatalog.undoCatalogChange(request)) ??
|
|
3696
|
+
baseAuthoring.undo(request),
|
|
3697
|
+
}
|
|
3698
|
+
: baseAuthoring;
|
|
3963
3699
|
const resolvedAgentPackages: FoundationAgentPackage[] = [
|
|
3964
3700
|
...createFoundationHostedRuntimePackages(application, {
|
|
3965
3701
|
userId: identity.userId,
|
|
3966
3702
|
readSecret,
|
|
3967
3703
|
// A Bot authors a Package only inside an admitted Turn, whose run and
|
|
3968
3704
|
// session the artifact provenance names.
|
|
3969
|
-
...(
|
|
3705
|
+
...(authoring ? { authoring } : {}),
|
|
3706
|
+
...(packageCatalog ? { packageCatalog } : {}),
|
|
3970
3707
|
...(turn
|
|
3971
3708
|
? {
|
|
3972
3709
|
skills: createBotSkillsHost(
|
|
@@ -4168,55 +3905,53 @@ export class ShellBotBackendContribution {
|
|
|
4168
3905
|
),
|
|
4169
3906
|
},
|
|
4170
3907
|
}),
|
|
4171
|
-
...(await
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
3908
|
+
...(await createFoundationEnabledRuntimePackages(application, plan, {
|
|
3909
|
+
userId: identity.userId,
|
|
3910
|
+
readSecret,
|
|
3911
|
+
authorizeConnection: authorizeEnabledConnection,
|
|
3912
|
+
packageSettings,
|
|
3913
|
+
// Enabled Contributions reach the network through the same
|
|
3914
|
+
// outbound seam the model provider uses, so a deployment that stubs
|
|
3915
|
+
// it stubs every one of them.
|
|
3916
|
+
...(this.outboundFetch ? { fetch: this.outboundFetch } : {}),
|
|
3917
|
+
leaseCredential: async (
|
|
3918
|
+
capability: EnabledCapabilityV1,
|
|
3919
|
+
effectId: string,
|
|
3920
|
+
expectedGeneration?: string,
|
|
3921
|
+
): Promise<CredentialLeaseV1> => {
|
|
3922
|
+
if (!capability.connectionId || !expectedGeneration) {
|
|
3923
|
+
throw new Error("Enabled Connection generation is unavailable");
|
|
3924
|
+
}
|
|
3925
|
+
return userConfiguration.leaseToolCredential(
|
|
3926
|
+
identity.userId,
|
|
3927
|
+
capability.connectionId,
|
|
4186
3928
|
effectId,
|
|
4187
3929
|
expectedGeneration,
|
|
4188
|
-
)
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
if (!assignment.connectionId) return;
|
|
4201
|
-
await userConfiguration.settleToolCredential(
|
|
4202
|
-
identity.userId,
|
|
4203
|
-
assignment.connectionId,
|
|
4204
|
-
effectId,
|
|
4205
|
-
);
|
|
4206
|
-
},
|
|
4207
|
-
// A mount that could not reach its server writes that down where
|
|
4208
|
-
// the User can read it. The Bot holds no MCP record; the User
|
|
4209
|
-
// Durable Object that owns the Connection does.
|
|
4210
|
-
recordOutcome: (outcome) =>
|
|
4211
|
-
userConfiguration.recordMcpMountOutcome(identity.userId, outcome),
|
|
3930
|
+
);
|
|
3931
|
+
},
|
|
3932
|
+
settleCredential: async (
|
|
3933
|
+
capability: EnabledCapabilityV1,
|
|
3934
|
+
effectId: string,
|
|
3935
|
+
): Promise<void> => {
|
|
3936
|
+
if (!capability.connectionId) return;
|
|
3937
|
+
await userConfiguration.settleToolCredential(
|
|
3938
|
+
identity.userId,
|
|
3939
|
+
capability.connectionId,
|
|
3940
|
+
effectId,
|
|
3941
|
+
);
|
|
4212
3942
|
},
|
|
4213
|
-
|
|
3943
|
+
// A mount that could not reach its server writes that down where
|
|
3944
|
+
// the User can read it. The Bot holds no MCP record; the User
|
|
3945
|
+
// Durable Object that owns the Connection does.
|
|
3946
|
+
recordOutcome: (outcome: McpMountOutcomeReportV1) =>
|
|
3947
|
+
userConfiguration.recordMcpMountOutcome(identity.userId, outcome),
|
|
3948
|
+
})),
|
|
4214
3949
|
];
|
|
4215
3950
|
const agentPackages: FoundationAgentPackage[] =
|
|
4216
3951
|
mergeFoundationRuntimePackages(resolvedAgentPackages);
|
|
4217
|
-
//
|
|
4218
|
-
//
|
|
4219
|
-
//
|
|
3952
|
+
// One generic resolver owns precedence: enabled Bot-scoped Package value,
|
|
3953
|
+
// enabled User-scoped Package value, then the platform model. The kernel
|
|
3954
|
+
// names no Package (AGENTS.md Configuration shape; ADR 0019).
|
|
4220
3955
|
const effective = resolveEffectiveBotModelV1({
|
|
4221
3956
|
bot: settings,
|
|
4222
3957
|
user,
|
|
@@ -4224,64 +3959,37 @@ export class ShellBotBackendContribution {
|
|
|
4224
3959
|
});
|
|
4225
3960
|
const effectiveModel = effective.model;
|
|
4226
3961
|
if (!effectiveModel) {
|
|
4227
|
-
throw new Error(
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
let binding: ResolvedModelBindingV1;
|
|
4231
|
-
if (admittedRequest) {
|
|
4232
|
-
const admittedBinding = admittedRequest.modelBinding;
|
|
4233
|
-
const assignment = settings.assignments.find(
|
|
4234
|
-
(candidate) =>
|
|
4235
|
-
candidate.connectionId === admittedBinding?.connectionId &&
|
|
4236
|
-
candidate.state === "enabled",
|
|
4237
|
-
);
|
|
4238
|
-
const pkg = application.packages.find(
|
|
4239
|
-
(candidate) => candidate.id === assignment?.packageId,
|
|
4240
|
-
);
|
|
4241
|
-
const capability = pkg?.manifest.configuration?.capabilities.find(
|
|
4242
|
-
(candidate) => candidate.id === assignment?.capabilityId,
|
|
3962
|
+
throw new Error(
|
|
3963
|
+
effective.binding?.failure ??
|
|
3964
|
+
"No model is configured; enable a model Package or restore the platform model",
|
|
4243
3965
|
);
|
|
4244
|
-
const connectionTypeId = capability?.connectionTypes[0];
|
|
4245
|
-
if (
|
|
4246
|
-
!admittedBinding?.connectionGeneration ||
|
|
4247
|
-
!assignment?.connectionId ||
|
|
4248
|
-
!pkg ||
|
|
4249
|
-
!connectionTypeId ||
|
|
4250
|
-
effectiveModel.connectionId !== admittedBinding.connectionId ||
|
|
4251
|
-
effectiveModel.providerModelId !== admittedRequest.model
|
|
4252
|
-
) {
|
|
4253
|
-
throw new Error("Admitted model binding is unavailable");
|
|
4254
|
-
}
|
|
4255
|
-
binding = {
|
|
4256
|
-
state: "ready",
|
|
4257
|
-
assignment: structuredClone(effectiveModel),
|
|
4258
|
-
packageId: pkg.id,
|
|
4259
|
-
providerType: admittedRequest.provider,
|
|
4260
|
-
connection: {
|
|
4261
|
-
connectionId: admittedBinding.connectionId,
|
|
4262
|
-
packageId: pkg.id,
|
|
4263
|
-
connectionTypeId,
|
|
4264
|
-
displayName: "Admitted model Connection",
|
|
4265
|
-
state: "ready",
|
|
4266
|
-
providerType: admittedRequest.provider,
|
|
4267
|
-
generation: admittedBinding.connectionGeneration,
|
|
4268
|
-
safeMetadata: {},
|
|
4269
|
-
},
|
|
4270
|
-
};
|
|
4271
|
-
} else {
|
|
4272
|
-
binding = effective.binding ?? {
|
|
4273
|
-
assignment: structuredClone(effectiveModel),
|
|
4274
|
-
state: "unavailable",
|
|
4275
|
-
failure: "Bot model Connection is unavailable",
|
|
4276
|
-
};
|
|
4277
3966
|
}
|
|
3967
|
+
const binding: ResolvedModelBindingV1 = effective.binding ?? {
|
|
3968
|
+
model: structuredClone(effectiveModel),
|
|
3969
|
+
state: "unavailable",
|
|
3970
|
+
failure: "The resolved model Connection is unavailable",
|
|
3971
|
+
};
|
|
4278
3972
|
if (
|
|
4279
3973
|
binding.state === "unavailable" ||
|
|
4280
3974
|
!binding.connection ||
|
|
4281
3975
|
!binding.providerType ||
|
|
4282
3976
|
!binding.packageId
|
|
4283
3977
|
) {
|
|
4284
|
-
throw new Error(
|
|
3978
|
+
throw new Error(
|
|
3979
|
+
binding.failure ?? "The resolved model Connection is unavailable",
|
|
3980
|
+
);
|
|
3981
|
+
}
|
|
3982
|
+
if (
|
|
3983
|
+
admittedRequest &&
|
|
3984
|
+
(admittedRequest.provider !== binding.providerType ||
|
|
3985
|
+
admittedRequest.model !== effectiveModel.providerModelId ||
|
|
3986
|
+
admittedRequest.modelBinding?.connectionId !==
|
|
3987
|
+
binding.connection.connectionId ||
|
|
3988
|
+
!admittedRequest.modelBinding.connectionGeneration ||
|
|
3989
|
+
admittedRequest.modelBinding.connectionGeneration !==
|
|
3990
|
+
binding.connection.generation)
|
|
3991
|
+
) {
|
|
3992
|
+
throw new Error("Admitted model binding is unavailable");
|
|
4285
3993
|
}
|
|
4286
3994
|
const bindingPackageId = binding.packageId;
|
|
4287
3995
|
agentPackages.push(
|
|
@@ -4312,27 +4020,28 @@ export class ShellBotBackendContribution {
|
|
|
4312
4020
|
bindingPackageId,
|
|
4313
4021
|
effectId,
|
|
4314
4022
|
),
|
|
4315
|
-
...(this.env.
|
|
4023
|
+
...(this.env.FLOCK_AI
|
|
4316
4024
|
? {
|
|
4317
|
-
|
|
4025
|
+
flockAiAutoRoute: this.env.FLOCK_AI.autoRoute,
|
|
4026
|
+
runFlockAiChatCompletion: (gatewayModel, body) =>
|
|
4027
|
+
this.env.FLOCK_AI!.runChatCompletion(gatewayModel, body),
|
|
4318
4028
|
}
|
|
4319
4029
|
: {}),
|
|
4320
4030
|
fetch: this.outboundFetch,
|
|
4321
4031
|
}),
|
|
4322
4032
|
);
|
|
4323
4033
|
// The slugs `<available_subagent_models>` renders, and the only ones a
|
|
4324
|
-
// `Task` call may name. They
|
|
4325
|
-
//
|
|
4326
|
-
const
|
|
4034
|
+
// `Task` call may name. They come from User enablement as resolved for this
|
|
4035
|
+
// Turn — never anything the Bot claimed about a model.
|
|
4036
|
+
const modelCapability = plan.capabilities.find(
|
|
4327
4037
|
(candidate) =>
|
|
4328
|
-
candidate.
|
|
4038
|
+
candidate.kind === "model" &&
|
|
4329
4039
|
candidate.connectionId === binding.connection!.connectionId,
|
|
4330
4040
|
);
|
|
4331
|
-
if (
|
|
4041
|
+
if (modelCapability) {
|
|
4332
4042
|
const subagentBinding = {
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
capabilityId: modelAssignment.capabilityId,
|
|
4043
|
+
packageId: modelCapability.packageId,
|
|
4044
|
+
capabilityId: modelCapability.capabilityId,
|
|
4336
4045
|
connectionId: binding.connection.connectionId,
|
|
4337
4046
|
provider: binding.providerType,
|
|
4338
4047
|
providerModelId: effectiveModel.providerModelId,
|
|
@@ -4342,7 +4051,7 @@ export class ShellBotBackendContribution {
|
|
|
4342
4051
|
};
|
|
4343
4052
|
subagentModels.push(
|
|
4344
4053
|
...subagentModelCatalogV1({
|
|
4345
|
-
|
|
4054
|
+
bindings: [subagentBinding],
|
|
4346
4055
|
defaultBinding: subagentBinding,
|
|
4347
4056
|
turnType: turn?.turnType ?? "chat",
|
|
4348
4057
|
}),
|
|
@@ -4353,6 +4062,7 @@ export class ShellBotBackendContribution {
|
|
|
4353
4062
|
// Cloud is both the model provider and the `web_search` Capability — and
|
|
4354
4063
|
// the runtime resolves one Plugin per Contribution specifier.
|
|
4355
4064
|
agentPackages: mergeFoundationRuntimePackagesV1(agentPackages),
|
|
4065
|
+
capabilities: structuredClone(plan.capabilities),
|
|
4356
4066
|
modelSelection: {
|
|
4357
4067
|
provider: binding.providerType,
|
|
4358
4068
|
model: effectiveModel.providerModelId,
|
|
@@ -4372,77 +4082,6 @@ export class ShellBotBackendContribution {
|
|
|
4372
4082
|
};
|
|
4373
4083
|
}
|
|
4374
4084
|
|
|
4375
|
-
private async authorizeAdmittedAssignedEffect(
|
|
4376
|
-
identity: BotIdentity,
|
|
4377
|
-
assignment: BotSettingsViewV1["assignments"][number],
|
|
4378
|
-
): Promise<ConnectionView> {
|
|
4379
|
-
const user = await this.userConfiguration(identity).readConfiguration({
|
|
4380
|
-
schemaVersion: 1,
|
|
4381
|
-
userId: identity.userId,
|
|
4382
|
-
});
|
|
4383
|
-
const application = await this.compileApplication();
|
|
4384
|
-
const connection = user.connections.find(
|
|
4385
|
-
(candidate) =>
|
|
4386
|
-
candidate.connectionId === assignment.connectionId &&
|
|
4387
|
-
candidate.packageId === assignment.packageId &&
|
|
4388
|
-
candidate.state === "ready",
|
|
4389
|
-
);
|
|
4390
|
-
const pkg = application.packages.find(
|
|
4391
|
-
(candidate) => candidate.id === assignment.packageId,
|
|
4392
|
-
);
|
|
4393
|
-
const capability = pkg?.manifest.configuration?.capabilities.find(
|
|
4394
|
-
(candidate) =>
|
|
4395
|
-
candidate.id === assignment.capabilityId &&
|
|
4396
|
-
connection !== undefined &&
|
|
4397
|
-
candidate.connectionTypes.includes(connection.connectionTypeId),
|
|
4398
|
-
);
|
|
4399
|
-
if (assignment.state !== "enabled" || !connection || !capability) {
|
|
4400
|
-
throw new Error("Admitted assigned effect is unavailable");
|
|
4401
|
-
}
|
|
4402
|
-
return structuredClone(connection);
|
|
4403
|
-
}
|
|
4404
|
-
|
|
4405
|
-
private async authorizeAssignedEffect(
|
|
4406
|
-
identity: BotIdentity,
|
|
4407
|
-
admittedAssignment: BotSettingsViewV1["assignments"][number],
|
|
4408
|
-
): Promise<ConnectionView> {
|
|
4409
|
-
const user = await this.userConfiguration(identity).readConfiguration({
|
|
4410
|
-
schemaVersion: 1,
|
|
4411
|
-
userId: identity.userId,
|
|
4412
|
-
});
|
|
4413
|
-
const application = await this.compileApplication();
|
|
4414
|
-
const admittedBot = {
|
|
4415
|
-
...this.initialBotSettings(identity.botId),
|
|
4416
|
-
assignments: [admittedAssignment],
|
|
4417
|
-
} satisfies BotSettingsViewV1;
|
|
4418
|
-
const plan = resolveBotExecutionPlanV1({
|
|
4419
|
-
bot: admittedBot,
|
|
4420
|
-
user,
|
|
4421
|
-
packages: application.packages.map((pkg) => ({
|
|
4422
|
-
packageId: pkg.id,
|
|
4423
|
-
version: pkg.version,
|
|
4424
|
-
capabilities: pkg.manifest.configuration?.capabilities ?? [],
|
|
4425
|
-
connectionTypes: pkg.manifest.configuration?.connectionTypes ?? [],
|
|
4426
|
-
})),
|
|
4427
|
-
});
|
|
4428
|
-
const assignment = plan.assignments.find(
|
|
4429
|
-
(candidate) =>
|
|
4430
|
-
candidate.assignmentId === admittedAssignment.assignmentId &&
|
|
4431
|
-
candidate.packageId === admittedAssignment.packageId &&
|
|
4432
|
-
candidate.capabilityId === admittedAssignment.capabilityId &&
|
|
4433
|
-
candidate.connectionId === admittedAssignment.connectionId &&
|
|
4434
|
-
candidate.state === "enabled",
|
|
4435
|
-
);
|
|
4436
|
-
const connection = user.connections.find(
|
|
4437
|
-
(candidate) =>
|
|
4438
|
-
candidate.connectionId === assignment?.connectionId &&
|
|
4439
|
-
candidate.packageId === admittedAssignment.packageId &&
|
|
4440
|
-
candidate.state === "ready",
|
|
4441
|
-
);
|
|
4442
|
-
if (!connection) throw new Error("Assigned effect is no longer authorized");
|
|
4443
|
-
return connection;
|
|
4444
|
-
}
|
|
4445
|
-
|
|
4446
4085
|
async readDurableIdentity(): Promise<BotIdentity | undefined> {
|
|
4447
4086
|
return this.authority.readDurableIdentity();
|
|
4448
4087
|
}
|
|
@@ -4883,15 +4522,17 @@ export class ShellBotBackendContribution {
|
|
|
4883
4522
|
return quarantine === undefined ? {} : { quarantine };
|
|
4884
4523
|
}
|
|
4885
4524
|
|
|
4886
|
-
/**
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
* member has no recorded source to show and the view carries members only.
|
|
4890
|
-
*/
|
|
4891
|
-
private readCompositionMemberSource(
|
|
4892
|
-
_member: CompositionMemberV1,
|
|
4525
|
+
/** Reads the immutable TypeScript source retained beside an authored artifact. */
|
|
4526
|
+
private async readCompositionMemberSource(
|
|
4527
|
+
member: CompositionMemberV1,
|
|
4893
4528
|
): Promise<string | undefined> {
|
|
4894
|
-
|
|
4529
|
+
const bucket = this.env.APPLICATION_ARTIFACTS;
|
|
4530
|
+
if (!bucket) return undefined;
|
|
4531
|
+
return readAuthoredCompositionMemberSourceV1({
|
|
4532
|
+
storage: { get: (key) => this.ctx.storage.get(key) },
|
|
4533
|
+
artifacts: createR2AuthoringArtifactStore(bucket),
|
|
4534
|
+
member,
|
|
4535
|
+
});
|
|
4895
4536
|
}
|
|
4896
4537
|
|
|
4897
4538
|
/**
|
|
@@ -5127,8 +4768,8 @@ export class ShellBotBackendContribution {
|
|
|
5127
4768
|
}
|
|
5128
4769
|
|
|
5129
4770
|
async alarm(): Promise<void> {
|
|
5130
|
-
// One alarm: the kernel defers while work is in flight, settles
|
|
5131
|
-
//
|
|
4771
|
+
// One alarm: the kernel defers while work is in flight, settles Package
|
|
4772
|
+
// scheduled work, and recovers the active run. A run left
|
|
5132
4773
|
// durably `reconciliation-required` stays scheduled and visible; only an
|
|
5133
4774
|
// explicit resume retrieves the original effect, so the alarm never
|
|
5134
4775
|
// terminalizes an uncertain outcome on its own.
|
|
@@ -5426,11 +5067,8 @@ export class ShellBotBackendContribution {
|
|
|
5426
5067
|
await this.authority.fenceRunAdmission(identity, query.runId),
|
|
5427
5068
|
);
|
|
5428
5069
|
}
|
|
5429
|
-
private initialBotSettings(
|
|
5430
|
-
botId
|
|
5431
|
-
model?: BotSettingsViewV1["model"],
|
|
5432
|
-
): BotSettingsViewV1 {
|
|
5433
|
-
return initializeBotSettingsV1(botId, model);
|
|
5070
|
+
private initialBotSettings(botId: string): BotSettingsViewV1 {
|
|
5071
|
+
return initializeBotSettingsV1(botId);
|
|
5434
5072
|
}
|
|
5435
5073
|
|
|
5436
5074
|
private userConfiguration(identity: BotIdentity): {
|
|
@@ -5438,6 +5076,9 @@ export class ShellBotBackendContribution {
|
|
|
5438
5076
|
schemaVersion: 1;
|
|
5439
5077
|
userId: string;
|
|
5440
5078
|
}): Promise<UserSettingsViewV1>;
|
|
5079
|
+
executeConfiguration(
|
|
5080
|
+
input: Extract<ConfigurationCommandV1, { type: `user/${string}` }>,
|
|
5081
|
+
): Promise<OperationReceiptV1>;
|
|
5441
5082
|
readPackageRevisions(
|
|
5442
5083
|
userId: string,
|
|
5443
5084
|
): ReturnType<PackagePublisherAgentHost["read"]>;
|
|
@@ -5449,40 +5090,6 @@ export class ShellBotBackendContribution {
|
|
|
5449
5090
|
userId: string,
|
|
5450
5091
|
command: Parameters<PackagePublisherAgentHost["rollback"]>[0],
|
|
5451
5092
|
): ReturnType<PackagePublisherAgentHost["rollback"]>;
|
|
5452
|
-
getConnection(
|
|
5453
|
-
userId: string,
|
|
5454
|
-
connectionId: string,
|
|
5455
|
-
): Promise<ConnectionView | undefined>;
|
|
5456
|
-
executeConnectionDependency(
|
|
5457
|
-
input: import("@frockbot/connection-core").ConnectionDependencyCommandV1,
|
|
5458
|
-
): Promise<
|
|
5459
|
-
import("@frockbot/connection-core").ConnectionDependencyResultV1
|
|
5460
|
-
>;
|
|
5461
|
-
claimConnectionDependency(
|
|
5462
|
-
userId: string,
|
|
5463
|
-
connectionId: string,
|
|
5464
|
-
botId: string,
|
|
5465
|
-
generation: string,
|
|
5466
|
-
requirement: ConnectionDependencyRequirementV1,
|
|
5467
|
-
): Promise<boolean>;
|
|
5468
|
-
acknowledgeConnectionDependency(
|
|
5469
|
-
userId: string,
|
|
5470
|
-
connectionId: string,
|
|
5471
|
-
botId: string,
|
|
5472
|
-
generation: string,
|
|
5473
|
-
): Promise<boolean>;
|
|
5474
|
-
releaseConnectionDependency(
|
|
5475
|
-
userId: string,
|
|
5476
|
-
connectionId: string,
|
|
5477
|
-
botId: string,
|
|
5478
|
-
generation: string,
|
|
5479
|
-
): Promise<boolean>;
|
|
5480
|
-
compensateConnectionDependency(
|
|
5481
|
-
userId: string,
|
|
5482
|
-
connectionId: string,
|
|
5483
|
-
botId: string,
|
|
5484
|
-
generation: string,
|
|
5485
|
-
): Promise<boolean>;
|
|
5486
5093
|
leaseModelCredential(
|
|
5487
5094
|
userId: string,
|
|
5488
5095
|
connectionId: string,
|
|
@@ -5543,6 +5150,7 @@ export class ShellBotBackendContribution {
|
|
|
5543
5150
|
// SAFETY: this namespace is bound to UserConfiguration; generated Worker types do not expose its RPC surface.
|
|
5544
5151
|
const rpc = this.env.USER_CONFIGURATIONS.get(id) as unknown as {
|
|
5545
5152
|
readConfiguration(input: unknown): Promise<UserSettingsViewV1>;
|
|
5153
|
+
executeConfiguration(input: unknown): Promise<unknown>;
|
|
5546
5154
|
readPackageRevisions(
|
|
5547
5155
|
input: unknown,
|
|
5548
5156
|
): ReturnType<PackagePublisherAgentHost["read"]>;
|
|
@@ -5552,16 +5160,6 @@ export class ShellBotBackendContribution {
|
|
|
5552
5160
|
rollbackPackage(
|
|
5553
5161
|
input: unknown,
|
|
5554
5162
|
): ReturnType<PackagePublisherAgentHost["rollback"]>;
|
|
5555
|
-
getConnection(input: unknown): Promise<ConnectionView | undefined>;
|
|
5556
|
-
executeConnectionDependency(
|
|
5557
|
-
input: unknown,
|
|
5558
|
-
): Promise<
|
|
5559
|
-
import("@frockbot/connection-core").ConnectionDependencyResultV1
|
|
5560
|
-
>;
|
|
5561
|
-
claimConnectionDependency(input: unknown): Promise<boolean>;
|
|
5562
|
-
acknowledgeConnectionDependency(input: unknown): Promise<boolean>;
|
|
5563
|
-
releaseConnectionDependency(input: unknown): Promise<boolean>;
|
|
5564
|
-
compensateConnectionDependency(input: unknown): Promise<boolean>;
|
|
5565
5163
|
leaseModelCredential(input: unknown): Promise<unknown>;
|
|
5566
5164
|
settleModelCredential(input: unknown): Promise<void>;
|
|
5567
5165
|
leaseToolCredential(input: unknown): Promise<unknown>;
|
|
@@ -5579,6 +5177,14 @@ export class ShellBotBackendContribution {
|
|
|
5579
5177
|
};
|
|
5580
5178
|
return {
|
|
5581
5179
|
readConfiguration: (input) => rpc.readConfiguration(input),
|
|
5180
|
+
executeConfiguration: async (command) =>
|
|
5181
|
+
decodeOperationReceiptV1(
|
|
5182
|
+
await rpc.executeConfiguration({
|
|
5183
|
+
schemaVersion: 1,
|
|
5184
|
+
userId: identity.userId,
|
|
5185
|
+
command,
|
|
5186
|
+
}),
|
|
5187
|
+
),
|
|
5582
5188
|
// A machine is a User asset, so every one of these crosses the seam and
|
|
5583
5189
|
// is decoded on arrival rather than trusted in the shape RPC returned.
|
|
5584
5190
|
listMachines: async (userId) =>
|
|
@@ -5617,8 +5223,6 @@ export class ShellBotBackendContribution {
|
|
|
5617
5223
|
rpc.publishPackage({ schemaVersion: 1, userId, command }),
|
|
5618
5224
|
rollbackPackage: (userId, command) =>
|
|
5619
5225
|
rpc.rollbackPackage({ schemaVersion: 1, userId, command }),
|
|
5620
|
-
getConnection: (userId, connectionId) =>
|
|
5621
|
-
rpc.getConnection({ schemaVersion: 1, userId, connectionId }),
|
|
5622
5226
|
// Flock state crosses a Durable Object seam, so it decodes on arrival
|
|
5623
5227
|
// rather than being trusted in the shape RPC happened to return.
|
|
5624
5228
|
listBots: async (userId) =>
|
|
@@ -5635,57 +5239,6 @@ export class ShellBotBackendContribution {
|
|
|
5635
5239
|
command,
|
|
5636
5240
|
}),
|
|
5637
5241
|
),
|
|
5638
|
-
executeConnectionDependency: (input) =>
|
|
5639
|
-
rpc.executeConnectionDependency(input),
|
|
5640
|
-
claimConnectionDependency: (
|
|
5641
|
-
userId,
|
|
5642
|
-
connectionId,
|
|
5643
|
-
botId,
|
|
5644
|
-
generation,
|
|
5645
|
-
requirement,
|
|
5646
|
-
) =>
|
|
5647
|
-
rpc.claimConnectionDependency({
|
|
5648
|
-
schemaVersion: 1,
|
|
5649
|
-
userId,
|
|
5650
|
-
connectionId,
|
|
5651
|
-
botId,
|
|
5652
|
-
generation,
|
|
5653
|
-
requirement,
|
|
5654
|
-
}),
|
|
5655
|
-
acknowledgeConnectionDependency: (
|
|
5656
|
-
userId,
|
|
5657
|
-
connectionId,
|
|
5658
|
-
botId,
|
|
5659
|
-
generation,
|
|
5660
|
-
) =>
|
|
5661
|
-
rpc.acknowledgeConnectionDependency({
|
|
5662
|
-
schemaVersion: 1,
|
|
5663
|
-
userId,
|
|
5664
|
-
connectionId,
|
|
5665
|
-
botId,
|
|
5666
|
-
generation,
|
|
5667
|
-
}),
|
|
5668
|
-
releaseConnectionDependency: (userId, connectionId, botId, generation) =>
|
|
5669
|
-
rpc.releaseConnectionDependency({
|
|
5670
|
-
schemaVersion: 1,
|
|
5671
|
-
userId,
|
|
5672
|
-
connectionId,
|
|
5673
|
-
botId,
|
|
5674
|
-
generation,
|
|
5675
|
-
}),
|
|
5676
|
-
compensateConnectionDependency: (
|
|
5677
|
-
userId,
|
|
5678
|
-
connectionId,
|
|
5679
|
-
botId,
|
|
5680
|
-
generation,
|
|
5681
|
-
) =>
|
|
5682
|
-
rpc.compensateConnectionDependency({
|
|
5683
|
-
schemaVersion: 1,
|
|
5684
|
-
userId,
|
|
5685
|
-
connectionId,
|
|
5686
|
-
botId,
|
|
5687
|
-
generation,
|
|
5688
|
-
}),
|
|
5689
5242
|
leaseModelCredential: async (
|
|
5690
5243
|
userId,
|
|
5691
5244
|
connectionId,
|
|
@@ -5752,94 +5305,10 @@ export class ShellBotBackendContribution {
|
|
|
5752
5305
|
identity: BotIdentity,
|
|
5753
5306
|
): Promise<BotSettingsViewV1> {
|
|
5754
5307
|
await this.validateIdentity(identity);
|
|
5755
|
-
const
|
|
5756
|
-
|
|
5757
|
-
);
|
|
5758
|
-
if (!existing)
|
|
5308
|
+
const stored = await this.ctx.storage.get<unknown>(BOT_CONFIGURATION_KEY);
|
|
5309
|
+
if (stored === undefined)
|
|
5759
5310
|
throw new Error(`Bot "${identity.botId}" is not materialized`);
|
|
5760
|
-
return
|
|
5761
|
-
}
|
|
5762
|
-
|
|
5763
|
-
/**
|
|
5764
|
-
* A Bot that follows the User's default model still needs its own durable
|
|
5765
|
-
* Assignment: authority reaches a Bot only through an explicit Assignment
|
|
5766
|
-
* and, when required, a Connection (ADR 0003). The Assignment is claimed
|
|
5767
|
-
* lazily the first time the Bot resolves its execution context under a
|
|
5768
|
-
* default it has not yet claimed, exactly as Flock claims one when a Bot is
|
|
5769
|
-
* created, so the User Connection's dependency ledger stays accurate and
|
|
5770
|
-
* revocation still fails closed.
|
|
5771
|
-
*/
|
|
5772
|
-
private async claimDefaultModelAssignment(
|
|
5773
|
-
identity: BotIdentity,
|
|
5774
|
-
settings: BotSettingsViewV1,
|
|
5775
|
-
user: UserSettingsViewV1,
|
|
5776
|
-
application: Awaited<ReturnType<typeof compileFoundationApplication>>,
|
|
5777
|
-
): Promise<BotSettingsViewV1> {
|
|
5778
|
-
const model = user.newBotModelTemplate;
|
|
5779
|
-
if (settings.model || !model) return settings;
|
|
5780
|
-
// One Assignment operation at a time: a claim still reconciling owns the
|
|
5781
|
-
// Bot's Assignment authority until it settles.
|
|
5782
|
-
if (settings.assignmentOperations.length > 0) return settings;
|
|
5783
|
-
const connection = user.connections.find(
|
|
5784
|
-
(candidate) => candidate.connectionId === model.connectionId,
|
|
5785
|
-
);
|
|
5786
|
-
const installation = user.packages.find(
|
|
5787
|
-
(candidate) =>
|
|
5788
|
-
candidate.packageId === connection?.packageId &&
|
|
5789
|
-
candidate.state === "installed",
|
|
5790
|
-
);
|
|
5791
|
-
const pkg = application.packages.find(
|
|
5792
|
-
(candidate) =>
|
|
5793
|
-
candidate.id === connection?.packageId &&
|
|
5794
|
-
candidate.version === installation?.version,
|
|
5795
|
-
);
|
|
5796
|
-
const connectionType = pkg?.manifest.configuration?.connectionTypes.find(
|
|
5797
|
-
(candidate) => candidate.id === connection?.connectionTypeId,
|
|
5798
|
-
);
|
|
5799
|
-
const capability = pkg?.manifest.configuration?.capabilities.find(
|
|
5800
|
-
(candidate) =>
|
|
5801
|
-
candidate.kind === "model" &&
|
|
5802
|
-
connectionType?.capabilities.includes(candidate.id) &&
|
|
5803
|
-
candidate.connectionTypes.includes(connectionType.id),
|
|
5804
|
-
);
|
|
5805
|
-
if (connection?.state !== "ready" || !pkg || !capability) return settings;
|
|
5806
|
-
if (
|
|
5807
|
-
settings.assignments.some(
|
|
5808
|
-
(assignment) =>
|
|
5809
|
-
assignment.packageId === pkg.id &&
|
|
5810
|
-
assignment.capabilityId === capability.id &&
|
|
5811
|
-
assignment.connectionId === connection.connectionId,
|
|
5812
|
-
)
|
|
5813
|
-
) {
|
|
5814
|
-
return settings;
|
|
5815
|
-
}
|
|
5816
|
-
const commandId = crypto.randomUUID();
|
|
5817
|
-
try {
|
|
5818
|
-
const receipt = await this.executeConfigurationCommand(identity, {
|
|
5819
|
-
schemaVersion: 1,
|
|
5820
|
-
type: "bot/assign-capability",
|
|
5821
|
-
commandId,
|
|
5822
|
-
expectedRevision: settings.revision,
|
|
5823
|
-
botId: identity.botId,
|
|
5824
|
-
assignment: {
|
|
5825
|
-
assignmentId: commandId,
|
|
5826
|
-
packageId: pkg.id,
|
|
5827
|
-
capabilityId: capability.id,
|
|
5828
|
-
connectionId: connection.connectionId,
|
|
5829
|
-
},
|
|
5830
|
-
});
|
|
5831
|
-
if (receipt.status !== "applied") return settings;
|
|
5832
|
-
} catch (error) {
|
|
5833
|
-
// The default model is not the Bot's own binding: a claim that cannot be
|
|
5834
|
-
// made leaves the Bot without a model, visibly, rather than failing the
|
|
5835
|
-
// caller that only wanted to read the plan.
|
|
5836
|
-
console.error(
|
|
5837
|
-
"Default model Assignment claim failed",
|
|
5838
|
-
error instanceof Error ? error.message : "unknown failure",
|
|
5839
|
-
);
|
|
5840
|
-
return settings;
|
|
5841
|
-
}
|
|
5842
|
-
return this.ensureBotSettings(identity);
|
|
5311
|
+
return decodeBotSettingsViewV1(migrateStoredBotSettingsV1(stored));
|
|
5843
5312
|
}
|
|
5844
5313
|
|
|
5845
5314
|
private async resolveExecutionContext(identity: BotIdentity): Promise<{
|
|
@@ -5847,82 +5316,30 @@ export class ShellBotBackendContribution {
|
|
|
5847
5316
|
user: UserSettingsViewV1;
|
|
5848
5317
|
plan: BotExecutionPlanV1;
|
|
5849
5318
|
}> {
|
|
5850
|
-
|
|
5319
|
+
const settings = await this.ensureBotSettings(identity);
|
|
5851
5320
|
const user = await this.userConfiguration(identity).readConfiguration({
|
|
5852
5321
|
schemaVersion: 1,
|
|
5853
5322
|
userId: identity.userId,
|
|
5854
5323
|
});
|
|
5855
5324
|
const application = await this.compileApplication();
|
|
5856
|
-
|
|
5325
|
+
const plan = resolveBotExecutionPlanV1({
|
|
5857
5326
|
bot: settings,
|
|
5858
5327
|
user,
|
|
5859
5328
|
packages: application.packages.map((pkg) => ({
|
|
5860
5329
|
packageId: pkg.id,
|
|
5861
5330
|
version: pkg.version,
|
|
5331
|
+
settings: pkg.manifest.configuration?.settings ?? [],
|
|
5862
5332
|
capabilities: pkg.manifest.configuration?.capabilities ?? [],
|
|
5863
5333
|
connectionTypes: pkg.manifest.configuration?.connectionTypes ?? [],
|
|
5864
5334
|
})),
|
|
5865
5335
|
});
|
|
5866
|
-
settings = await this.claimDefaultModelAssignment(
|
|
5867
|
-
identity,
|
|
5868
|
-
settings,
|
|
5869
|
-
user,
|
|
5870
|
-
application,
|
|
5871
|
-
);
|
|
5872
|
-
if (settings.revision !== plan.revision) {
|
|
5873
|
-
plan = resolveBotExecutionPlanV1({
|
|
5874
|
-
bot: settings,
|
|
5875
|
-
user,
|
|
5876
|
-
packages: application.packages.map((pkg) => ({
|
|
5877
|
-
packageId: pkg.id,
|
|
5878
|
-
version: pkg.version,
|
|
5879
|
-
capabilities: pkg.manifest.configuration?.capabilities ?? [],
|
|
5880
|
-
connectionTypes: pkg.manifest.configuration?.connectionTypes ?? [],
|
|
5881
|
-
})),
|
|
5882
|
-
});
|
|
5883
|
-
}
|
|
5884
|
-
const changed = settings.assignments.some(
|
|
5885
|
-
(assignment, index) =>
|
|
5886
|
-
assignment.state !== plan.assignments[index]?.state,
|
|
5887
|
-
);
|
|
5888
|
-
if (changed) {
|
|
5889
|
-
settings = await this.ctx.storage.transaction(async (transaction) => {
|
|
5890
|
-
const current =
|
|
5891
|
-
(await transaction.get<BotSettingsViewV1>(BOT_CONFIGURATION_KEY)) ??
|
|
5892
|
-
settings;
|
|
5893
|
-
if (current.revision !== settings.revision) return current;
|
|
5894
|
-
const next = {
|
|
5895
|
-
...current,
|
|
5896
|
-
revision: current.revision + 1,
|
|
5897
|
-
assignments: plan.assignments,
|
|
5898
|
-
} satisfies BotSettingsViewV1;
|
|
5899
|
-
await transaction.put(BOT_CONFIGURATION_KEY, next);
|
|
5900
|
-
await this.refreshRecoveryAlarm(transaction);
|
|
5901
|
-
return next;
|
|
5902
|
-
});
|
|
5903
|
-
plan = {
|
|
5904
|
-
...plan,
|
|
5905
|
-
revision: settings.revision,
|
|
5906
|
-
assignments: settings.assignments,
|
|
5907
|
-
};
|
|
5908
|
-
}
|
|
5909
5336
|
return { settings, user, plan };
|
|
5910
5337
|
}
|
|
5911
5338
|
|
|
5912
5339
|
async archiveEligible(storage: {
|
|
5913
5340
|
get<T>(key: string): Promise<T | undefined>;
|
|
5914
|
-
list<T>(options: { prefix: string }): Promise<Map<string, T>>;
|
|
5915
5341
|
}): Promise<boolean> {
|
|
5916
|
-
|
|
5917
|
-
storage.get<string>(ACTIVE_RUN_KEY),
|
|
5918
|
-
storage.get<BotSettingsViewV1>(BOT_CONFIGURATION_KEY),
|
|
5919
|
-
storage.list<unknown>({ prefix: ASSIGNMENT_SAGA_PREFIX }),
|
|
5920
|
-
]);
|
|
5921
|
-
return (
|
|
5922
|
-
activeRunId === undefined &&
|
|
5923
|
-
(settings?.assignmentOperations.length ?? 0) === 0 &&
|
|
5924
|
-
sagas.size === 0
|
|
5925
|
-
);
|
|
5342
|
+
return (await storage.get<string>(ACTIVE_RUN_KEY)) === undefined;
|
|
5926
5343
|
}
|
|
5927
5344
|
|
|
5928
5345
|
async assertLifecycleActive(botId: string): Promise<void> {
|