@frockbot/plugin-shell 0.1.4 → 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/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
- type IsolateModelOutcomeV1,
9
- type IsolatePendingDecisionV1,
26
+ type IsolateModelInvocationV1,
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,
@@ -46,7 +70,9 @@ import {
46
70
  ConfigurationConflictError,
47
71
  decodeBotConfigurationExecuteRpcV1,
48
72
  decodeBotConfigurationReadRpcV1,
73
+ decodeBotSettingsViewV1,
49
74
  decodeCompositionCommandReceiptV1,
75
+ decodeOperationReceiptV1,
50
76
  decodeInstalledPackageSettingIdsV1,
51
77
  decodeInstalledPackageSettingsPatchV1,
52
78
  MAX_COMPOSITION_GENERATION_PAGE_V1,
@@ -64,6 +90,7 @@ import {
64
90
  type PackageSettingValueV1,
65
91
  type ResolvedModelBindingV1,
66
92
  initializeBotSettingsV1,
93
+ migrateStoredBotSettingsV1,
67
94
  resolvePackageSettingValuesV1,
68
95
  resolveBotExecutionPlanV1,
69
96
  resolveEffectiveBotModelV1,
@@ -95,6 +122,7 @@ import {
95
122
  type ShellIsolateMountOptions,
96
123
  type ShellMountedComposition,
97
124
  } from "./backend-composition.js";
125
+ import { compositionFailureTurnTextV1 } from "./backend-composition-input.js";
98
126
  import {
99
127
  activateCompositionV1,
100
128
  type CompositionFailureV1,
@@ -104,7 +132,13 @@ import {
104
132
  import {
105
133
  createPackageAuthoringHost,
106
134
  createR2AuthoringArtifactStore,
135
+ readAuthoredCompositionMemberSourceV1,
107
136
  } from "./backend-authoring.js";
137
+ import {
138
+ type CatalogAwarePackageCatalogHost,
139
+ createPackageCatalogHost,
140
+ createR2BotPackageCatalogReader,
141
+ } from "./backend-package-catalog.js";
108
142
  import { createBotComputerSyncHost } from "./backend-computer.js";
109
143
  import {
110
144
  decodeDirectoryViewV1,
@@ -280,7 +314,9 @@ import type {
280
314
  RoutineRunListViewV1,
281
315
  } from "@frockbot/plugin-routines/shared";
282
316
  import {
317
+ authorshipManifestKey,
283
318
  decodeAuthoringQuotaReceiptV1,
319
+ type AuthoredManifestRecordV1,
284
320
  type AuthoringQuotaBinding,
285
321
  type PackageAuthoringHost,
286
322
  } from "@frockbot/plugin-authoring";
@@ -290,14 +326,12 @@ import {
290
326
  createR2PackageArtifactStore,
291
327
  isolateBindingDigestV1,
292
328
  type BotCapabilitiesPropsV1,
293
- type IsolateCapabilityV1,
294
329
  type IsolateCapabilityHost,
295
330
  type IsolateModelBindingV1,
296
331
  type IsolateModelPath,
297
332
  type IsolateModelRequestRecordV1,
298
- type IsolatePendingAuthorityDecisionV1,
299
- type IsolateUnavailableModelBindingV1,
300
333
  } from "./backend-isolate.js";
334
+ import { memoryScopeRootV1 } from "@frockbot/plugin-memory/roots";
301
335
  import type { BotIsolateLoader } from "@frockbot/kernel-composition/isolate";
302
336
  import type {
303
337
  CompositionGenerationV1,
@@ -310,8 +344,11 @@ import {
310
344
  type McpMountOutcomeReportV1,
311
345
  type McpServerStatusViewV1,
312
346
  } from "@frockbot/plugin-mcp/records";
313
- import { projectCompositionGenerationV1 } from "./composition-views.js";
314
- import { executeBotTurn } from "./backend-runner.js";
347
+ import {
348
+ projectCompositionGenerationV1,
349
+ projectPackageIframeCompositionV1,
350
+ } from "./composition-views.js";
351
+ import { executeBotTurn, executeDirectToolTurn } from "./backend-runner.js";
315
352
  import { shellTerminalRecordsV1 } from "./terminal-records.js";
316
353
  import {
317
354
  CLIENT_RUN_LIST_MAX_BYTES,
@@ -413,6 +450,17 @@ interface ConfigurationActivity {
413
450
  promise: Promise<OperationReceiptV1>;
414
451
  }
415
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
+
416
464
  function requireMatchingConfigurationReceipt(
417
465
  stored: StoredConfigurationReceipt,
418
466
  commandFingerprint: string,
@@ -430,6 +478,9 @@ export type { BotIdentity, OwnedBotTurnCommand };
430
478
 
431
479
  export interface BotStateEnv {
432
480
  MEMORY_FILES: R2Bucket;
481
+ /** Object-storage file surfaces constructed by the Cloudflare adapter. */
482
+ WORKSPACE_FILES?: WorkspaceFilesV1;
483
+ MEMORY_WORKSPACE_FILES?: WorkspaceFilesV1;
433
484
  /**
434
485
  * The Bot Package Worker Loader (plan Step 4). Optional so a host without
435
486
  * Bot-authored Packages — tests, the Electron shell — still compiles; a
@@ -523,6 +574,29 @@ function optionalStoredRun(input: unknown): StoredRun | undefined {
523
574
  return input === undefined ? undefined : requireStoredRunV1(input);
524
575
  }
525
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
+
526
600
  export class ShellBotBackendContribution {
527
601
  readonly ctx: DurableObjectState;
528
602
  readonly env: BotStateEnv;
@@ -539,7 +613,18 @@ export class ShellBotBackendContribution {
539
613
  >();
540
614
  /** The Turn currently executing on this object, for durable Stop. */
541
615
  private activeTurn:
542
- { runId: string; sessionId: string; cancel(): void } | undefined;
616
+ | {
617
+ runId: string;
618
+ sessionId: string;
619
+ turnId: string;
620
+ generationId: string;
621
+ turnType: TurnTypeV1;
622
+ subagentRole?: string;
623
+ mounted: ShellMountedComposition;
624
+ signal: AbortSignal;
625
+ cancel(): void;
626
+ }
627
+ | undefined;
543
628
  /**
544
629
  * Admission, the event log, the cursor, idempotency, cancellation, and
545
630
  * durable scheduling are kernel authority; this Package supplies only the
@@ -636,10 +721,10 @@ export class ShellBotBackendContribution {
636
721
  ) {
637
722
  throw new Error("Bot authority does not match its durable identity");
638
723
  }
639
- const existing = await transaction.get<BotSettingsViewV1>(
640
- BOT_CONFIGURATION_KEY,
641
- );
642
- if (existing) return existing;
724
+ const stored = await transaction.get<unknown>(BOT_CONFIGURATION_KEY);
725
+ if (stored !== undefined) {
726
+ return decodeBotSettingsViewV1(migrateStoredBotSettingsV1(stored));
727
+ }
643
728
  const settings = {
644
729
  ...this.initialBotSettings(identity.botId),
645
730
  profile: {
@@ -799,9 +884,11 @@ export class ShellBotBackendContribution {
799
884
  command.commandId,
800
885
  );
801
886
  }
887
+ const stored = await transaction.get<unknown>(BOT_CONFIGURATION_KEY);
802
888
  const current =
803
- (await transaction.get<BotSettingsViewV1>(BOT_CONFIGURATION_KEY)) ??
804
- this.initialBotSettings(identity.botId);
889
+ stored === undefined
890
+ ? this.initialBotSettings(identity.botId)
891
+ : decodeBotSettingsViewV1(migrateStoredBotSettingsV1(stored));
805
892
  if (command.expectedRevision !== current.revision) {
806
893
  throw new ConfigurationConflictError(current.revision);
807
894
  }
@@ -953,6 +1040,30 @@ export class ShellBotBackendContribution {
953
1040
  return projectClientTurnV1(await this.authority.run(command));
954
1041
  }
955
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
+ },
1063
+ }),
1064
+ );
1065
+ }
1066
+
956
1067
  /**
957
1068
  * The Bot's invocable Skills, for the composer's `/` and `@` popover.
958
1069
  *
@@ -1001,6 +1112,46 @@ export class ShellBotBackendContribution {
1001
1112
  return { schemaVersion: 1, skills: entries };
1002
1113
  }
1003
1114
 
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),
1121
+ );
1122
+ return stored ? decodeFrockBotManifest(stored.manifest) : undefined;
1123
+ }
1124
+
1125
+ private async requireCompositionMemberManifest(
1126
+ member: CompositionMemberV1,
1127
+ ): Promise<FrockBotManifest> {
1128
+ const manifest = await this.readCompositionMemberManifest(member);
1129
+ if (!manifest) {
1130
+ throw new Error(
1131
+ `package "${member.packageId}" manifest "${member.manifestHash}" is unavailable`,
1132
+ );
1133
+ }
1134
+ return manifest;
1135
+ }
1136
+
1137
+ /** Active fail-closed Composition projected as inert iframe metadata. */
1138
+ async listPackageUi(
1139
+ identity: BotIdentity,
1140
+ ): Promise<PackageIframeCompositionV1> {
1141
+ await this.validateIdentity(identity);
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),
1152
+ });
1153
+ }
1154
+
1004
1155
  /**
1005
1156
  * Write one Skill into this Bot's own instruction root as its **User**.
1006
1157
  *
@@ -1212,11 +1363,17 @@ export class ShellBotBackendContribution {
1212
1363
  ? { subagentTaskId: input.command.origin.taskId }
1213
1364
  : {}),
1214
1365
  };
1366
+ let mountedRoot: ShellMountedComposition["root"] | undefined;
1367
+ let mountedGeneration: CompositionGenerationV1 | undefined;
1368
+ const currentToolNames = (): readonly string[] =>
1369
+ mountedRoot?.tools.registeredNames?.() ?? [];
1215
1370
  const runtime = await this.agentRuntime(
1216
1371
  input.identity,
1217
1372
  settings,
1218
1373
  input.admittedRequest,
1219
1374
  turn,
1375
+ currentToolNames,
1376
+ () => mountedGeneration,
1220
1377
  );
1221
1378
  const promptParts = [
1222
1379
  `You are ${settings.profile.name}.`,
@@ -1233,9 +1390,9 @@ export class ShellBotBackendContribution {
1233
1390
  runId: input.command.runId,
1234
1391
  sessionId: input.command.sessionId,
1235
1392
  generationId: mounting.generationId,
1236
- capabilities: runtime.capabilities,
1393
+ settings,
1237
1394
  });
1238
- return createShellCompositionHost({
1395
+ const mounted = await createShellCompositionHost({
1239
1396
  botId: input.identity.botId,
1240
1397
  sessionId: input.command.sessionId,
1241
1398
  sessionEvents: input.previousEvents,
@@ -1261,6 +1418,9 @@ export class ShellBotBackendContribution {
1261
1418
  ),
1262
1419
  ...(isolate ? { isolate } : {}),
1263
1420
  }).mount(mounting, signal);
1421
+ mountedRoot = mounted.root;
1422
+ mountedGeneration = mounted.generation;
1423
+ return mounted;
1264
1424
  },
1265
1425
  };
1266
1426
  const controller = new AbortController();
@@ -1300,14 +1460,78 @@ export class ShellBotBackendContribution {
1300
1460
  const active = {
1301
1461
  runId: input.command.runId,
1302
1462
  sessionId: input.command.sessionId,
1303
- cancel: () => activation.mounted.runtime.agent.agent.cancel("user"),
1463
+ turnId: input.command.runId,
1464
+ generationId: activation.mounted.generation.generationId,
1465
+ turnType: input.command.turnType ?? "chat",
1466
+ ...(input.command.subagentRole
1467
+ ? { subagentRole: input.command.subagentRole }
1468
+ : {}),
1469
+ mounted: activation.mounted,
1470
+ signal: controller.signal,
1471
+ cancel: () => {
1472
+ controller.abort("user");
1473
+ activation.mounted.runtime.agent.agent.cancel("user");
1474
+ },
1304
1475
  };
1305
1476
  this.activeTurn = active;
1306
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;
1307
1531
  return await executeBotTurn({
1308
1532
  command: {
1309
1533
  ...input.command,
1310
- text: await this.turnInputTextV1(input.command),
1534
+ text: durableInput,
1311
1535
  },
1312
1536
  previousEvents: input.previousEvents,
1313
1537
  composition: activation.mounted,
@@ -1384,9 +1608,9 @@ export class ShellBotBackendContribution {
1384
1608
  }
1385
1609
 
1386
1610
  /**
1387
- * Everything a Bot isolate member needs. Returns `undefined` when this host
1388
- * has no Package loader, in which case an isolate member fails verification
1389
- * rather than silently running nowhere.
1611
+ * Everything a Bot isolate member needs. Package identity is attribution
1612
+ * only; Connections and model are resolved once for the Bot and every member
1613
+ * receives the same list.
1390
1614
  */
1391
1615
  private async isolateMountOptions(
1392
1616
  identity: BotIdentity,
@@ -1394,7 +1618,7 @@ export class ShellBotBackendContribution {
1394
1618
  runId: string;
1395
1619
  sessionId: string;
1396
1620
  generationId: string;
1397
- capabilities: readonly EnabledCapabilityV1[];
1621
+ settings: BotSettingsViewV1;
1398
1622
  },
1399
1623
  ): Promise<ShellIsolateMountOptions | undefined> {
1400
1624
  const loader = this.env.BOT_PACKAGES;
@@ -1409,248 +1633,696 @@ export class ShellBotBackendContribution {
1409
1633
  }
1410
1634
  ).exports;
1411
1635
  if (!loader || !artifacts || !exports?.BotCapabilities) return undefined;
1412
- const mintCapabilities = exports.BotCapabilities;
1413
- const capabilities = turn.capabilities.map((capability) =>
1414
- structuredClone(capability),
1636
+ const authority = await this.isolateAuthoritySnapshot(
1637
+ identity,
1638
+ turn.settings,
1415
1639
  );
1640
+ const mintCapabilities = exports.BotCapabilities;
1416
1641
  return {
1417
1642
  userId: identity.userId,
1418
1643
  runId: turn.runId,
1419
- // One admitted Turn is one run; the Turn ordinal lives in the session log.
1420
1644
  turnId: turn.runId,
1421
1645
  loader,
1422
1646
  artifacts: createR2PackageArtifactStore(artifacts),
1647
+ manifestFor: (member) => this.requireCompositionMemberManifest(member),
1423
1648
  capabilitiesFor: (member) =>
1424
1649
  mintCapabilities({
1425
1650
  props: {
1426
1651
  userId: identity.userId,
1427
1652
  botId: identity.botId,
1653
+ runId: turn.runId,
1654
+ sessionId: turn.sessionId,
1655
+ turnId: turn.runId,
1428
1656
  generationId: turn.generationId,
1429
1657
  packageId: member.packageId,
1430
- capabilities: structuredClone(capabilities),
1658
+ connections: structuredClone(authority.connections),
1659
+ ...(authority.model
1660
+ ? { model: structuredClone(authority.model) }
1661
+ : {}),
1662
+ memory: authority.memory,
1663
+ workspace: authority.workspace,
1431
1664
  },
1432
1665
  }),
1433
1666
  bindingDigest: await isolateBindingDigestV1({
1434
1667
  userId: identity.userId,
1435
1668
  botId: identity.botId,
1436
- generationId: turn.generationId,
1437
- capabilities,
1669
+ connections: authority.connections,
1670
+ ...(authority.model ? { model: authority.model } : {}),
1671
+ compositionGenerationId: turn.generationId,
1438
1672
  }),
1439
1673
  compatibilityDate: BOT_ISOLATE_COMPATIBILITY_DATE,
1440
1674
  };
1441
1675
  }
1442
1676
 
1443
- /**
1444
- * The Bot Durable Object side of `CAPABILITIES.requestAuthority`. Never a
1445
- * grant: it records a durable pending decision for the User.
1446
- */
1447
- async isolateRequestAuthority(input: {
1448
- botId: string;
1449
- packageId: string;
1450
- generationId: string;
1451
- request: unknown;
1452
- }): Promise<IsolatePendingDecisionV1> {
1453
- return await this.isolateCapabilityHost(input, []).requestAuthority(
1454
- input.request,
1677
+ private async isolateAuthoritySnapshot(
1678
+ identity: BotIdentity,
1679
+ settings: BotSettingsViewV1,
1680
+ ): Promise<{
1681
+ connections: IsolateConnectionV1[];
1682
+ model?: IsolateModelBindingV1;
1683
+ memory: boolean;
1684
+ workspace: boolean;
1685
+ }> {
1686
+ const [user, application] = await Promise.all([
1687
+ this.userConfiguration(identity).readConfiguration({
1688
+ schemaVersion: 1,
1689
+ userId: identity.userId,
1690
+ }),
1691
+ this.compileApplication(),
1692
+ ]);
1693
+ const connections = user.connections.flatMap((connection) =>
1694
+ connection.state === "ready" && connection.generation
1695
+ ? [
1696
+ {
1697
+ connectionId: connection.connectionId,
1698
+ packageId: connection.packageId,
1699
+ connectionTypeId: connection.connectionTypeId,
1700
+ displayName: connection.displayName,
1701
+ generation: connection.generation,
1702
+ safeMetadata: structuredClone(connection.safeMetadata),
1703
+ } satisfies IsolateConnectionV1,
1704
+ ]
1705
+ : [],
1455
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
+ };
1456
1744
  }
1457
1745
 
1458
- /**
1459
- * The Bot Durable Object side of `CAPABILITIES.invokeModel`. Refuses with a
1460
- * pending decision unless an enabled model Capability matches; otherwise
1461
- * records the normalized request and streams through the provider path,
1462
- * which takes the credential lease on the way.
1463
- */
1464
1746
  async isolateInvokeModel(
1465
1747
  identity: BotIdentity,
1466
1748
  input: {
1749
+ runId: string;
1750
+ sessionId: string;
1751
+ turnId: string;
1467
1752
  packageId: string;
1468
1753
  generationId: string;
1469
1754
  request: NormalizedModelRequest;
1470
1755
  },
1471
- ): Promise<IsolateModelOutcomeV1> {
1472
- // The effective binding and enabled set both come from authority-owned
1473
- // User and Bot state. Nothing the Bot supplied is read.
1474
- const durableSettings = await this.ctx.storage.get<BotSettingsViewV1>(
1475
- BOT_CONFIGURATION_KEY,
1476
- );
1477
- if (!durableSettings) {
1478
- return await this.isolateCapabilityHost(
1479
- {
1480
- botId: identity.botId,
1481
- packageId: input.packageId,
1482
- generationId: input.generationId,
1483
- },
1484
- [],
1485
- ).invokeModel(input.request);
1756
+ ): Promise<IsolateModelInvocationV1> {
1757
+ if (!this.activeIsolateTurn(input)) {
1758
+ return {
1759
+ status: "unavailable",
1760
+ reason: "the Package is not running in this Bot's active Composition",
1761
+ };
1486
1762
  }
1487
- const context = await this.resolveExecutionContext(identity);
1488
- const capabilities = structuredClone(context.plan.capabilities);
1489
- const bound = await this.isolateModelBinding(
1490
- identity,
1491
- context.settings,
1492
- context.user,
1493
- capabilities,
1494
- );
1495
- const host = this.isolateCapabilityHost(
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) {
1772
+ try {
1773
+ runtime = await this.agentRuntime(identity, settings);
1774
+ } catch {
1775
+ runtime = undefined;
1776
+ }
1777
+ }
1778
+ return this.isolateCapabilities(
1496
1779
  {
1497
1780
  botId: identity.botId,
1498
1781
  packageId: input.packageId,
1499
1782
  generationId: input.generationId,
1500
1783
  },
1501
- capabilities,
1502
- bound?.state === "ready"
1784
+ authority,
1785
+ runtime && authority.model
1503
1786
  ? {
1504
- binding: bound.binding,
1505
- path: this.isolateModelPath(
1506
- identity,
1507
- bound.runtime,
1508
- input.generationId,
1509
- ),
1787
+ path: this.isolateModelPath(identity, runtime, input.generationId),
1510
1788
  }
1511
1789
  : undefined,
1512
- bound?.state === "unavailable" ? bound.binding : undefined,
1513
- );
1514
- return await host.invokeModel(input.request);
1790
+ ).invokeModel(input.request);
1515
1791
  }
1516
1792
 
1517
- /**
1518
- * The effective model binding, projected onto the isolate view. It resolves
1519
- * generic Package settings and the platform fallback through the User's
1520
- * Connection exactly as an admitted Turn does. No configured binding means
1521
- * an authority-widening request and a pending decision; a configured binding
1522
- * whose Connection cannot be resolved stays distinct as unavailable.
1523
- */
1524
- private async isolateModelBinding(
1525
- identity: BotIdentity,
1526
- settings: BotSettingsViewV1 | undefined,
1527
- user: UserSettingsViewV1,
1528
- capabilities: readonly IsolateCapabilityV1[],
1529
- ): Promise<
1530
- | {
1531
- state: "ready";
1532
- binding: IsolateModelBindingV1;
1533
- runtime: {
1534
- agentPackages: FoundationAgentPackage[];
1535
- modelSelection: RuntimeModelSelection;
1793
+ async isolateInvokeTool(input: {
1794
+ userId: string;
1795
+ botId: string;
1796
+ runId: string;
1797
+ sessionId: string;
1798
+ turnId: string;
1799
+ packageId: string;
1800
+ generationId: string;
1801
+ request: unknown;
1802
+ }): Promise<IsolateToolOutcomeV1> {
1803
+ const request = decodeIsolateToolRequestV1(input.request);
1804
+ const active = this.activeIsolateTurn(input);
1805
+ if (!active) {
1806
+ return {
1807
+ status: "unavailable",
1808
+ reason: "the Package is not running in this Bot's active Composition",
1809
+ };
1810
+ }
1811
+ const session = active.mounted.runtime.root.sessions.get(input.sessionId);
1812
+ if (!session) {
1813
+ return {
1814
+ status: "unavailable",
1815
+ reason: "the active Session is unavailable",
1816
+ };
1817
+ }
1818
+ const started = session.events.findLast(
1819
+ (event) => event.type === "step/start",
1820
+ );
1821
+ const ended = session.events.findLast((event) => event.type === "step/end");
1822
+ if (
1823
+ started?.type !== "step/start" ||
1824
+ (ended?.type === "step/end" &&
1825
+ ended.turn === started.turn &&
1826
+ ended.step === started.step)
1827
+ ) {
1828
+ return {
1829
+ status: "unavailable",
1830
+ reason: "the active step is unavailable",
1831
+ };
1832
+ }
1833
+ const effectId = await this.isolateToolEffectId(
1834
+ input.packageId,
1835
+ request.callId,
1836
+ );
1837
+ const priorCall = session.events.find(
1838
+ (event) =>
1839
+ event.type === "package/tool-call" && event.effectId === effectId,
1840
+ );
1841
+ const priorResult = session.events.find(
1842
+ (event) =>
1843
+ event.type === "package/tool-result" && event.effectId === effectId,
1844
+ );
1845
+ if (priorResult?.type === "package/tool-result") {
1846
+ return {
1847
+ status: "completed",
1848
+ content: priorResult.content,
1849
+ isError: priorResult.isError,
1850
+ };
1851
+ }
1852
+ if (
1853
+ priorCall?.type === "package/tool-call" &&
1854
+ (priorCall.packageId !== input.packageId ||
1855
+ priorCall.callId !== request.callId ||
1856
+ priorCall.name !== request.name ||
1857
+ JSON.stringify(priorCall.input) !== JSON.stringify(request.input))
1858
+ ) {
1859
+ return {
1860
+ status: "unavailable",
1861
+ reason: "the Package tool idempotency key was reused",
1862
+ };
1863
+ }
1864
+ if (!priorCall) {
1865
+ session.append({
1866
+ type: "package/tool-call",
1867
+ turn: started.turn,
1868
+ step: started.step,
1869
+ effectId,
1870
+ packageId: input.packageId,
1871
+ callId: request.callId,
1872
+ name: request.name,
1873
+ input: request.input,
1874
+ });
1875
+ await session.flush();
1876
+ }
1877
+ const call = {
1878
+ id: request.callId,
1879
+ name: request.name,
1880
+ input: request.input,
1881
+ };
1882
+ const context = {
1883
+ botId: input.botId,
1884
+ agentId: input.botId,
1885
+ sessionId: input.sessionId,
1886
+ compositionGenerationId: input.generationId,
1887
+ effectId,
1888
+ toolCall: call,
1889
+ turnType: active.turnType,
1890
+ ...(active.subagentRole ? { subagentRole: active.subagentRole } : {}),
1891
+ signal: active.signal,
1892
+ };
1893
+ const preparation = await active.mounted.runtime.root.tools.prepare(
1894
+ call,
1895
+ context,
1896
+ );
1897
+ let result: { content: string; isError: boolean };
1898
+ if (preparation.kind === "denied") {
1899
+ result = preparation.result;
1900
+ } else {
1901
+ const admitted = await this.admitRunEffect(
1902
+ { userId: input.userId, botId: input.botId },
1903
+ input.runId,
1904
+ input.sessionId,
1905
+ { kind: "tool", effectId },
1906
+ );
1907
+ if (!admitted) {
1908
+ result = {
1909
+ content: "The tool effect was stopped before it started.",
1910
+ isError: true,
1536
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
+ );
1537
1927
  }
1538
- | {
1539
- state: "unavailable";
1540
- binding: IsolateUnavailableModelBindingV1;
1541
- }
1542
- | undefined
1543
- > {
1544
- if (!settings) return undefined;
1545
- const application = await this.compileApplication();
1546
- const packageDefinitions = application.packages.map((pkg) => ({
1547
- packageId: pkg.id,
1548
- version: pkg.version,
1549
- settings: pkg.manifest.configuration?.settings ?? [],
1550
- capabilities: pkg.manifest.configuration?.capabilities ?? [],
1551
- connectionTypes: pkg.manifest.configuration?.connectionTypes ?? [],
1552
- }));
1553
- const effective = resolveEffectiveBotModelV1({
1554
- bot: settings,
1555
- user,
1556
- packages: packageDefinitions,
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,
1557
1939
  });
1558
- const effectiveModel = effective.model;
1559
- if (!effectiveModel) return undefined;
1560
- const configuredConnection = user.connections.find(
1561
- (connection) => connection.connectionId === effectiveModel.connectionId,
1940
+ await session.flush();
1941
+ return {
1942
+ status: "completed",
1943
+ content: result.content,
1944
+ isError: result.isError,
1945
+ };
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),
1562
2040
  );
1563
- const unavailable = (): {
1564
- state: "unavailable";
1565
- binding: IsolateUnavailableModelBindingV1;
1566
- } => ({
1567
- state: "unavailable",
1568
- binding: {
1569
- ...(effective.binding?.providerType
1570
- ? { provider: effective.binding.providerType }
1571
- : configuredConnection?.providerType
1572
- ? { provider: configuredConnection.providerType }
1573
- : {}),
1574
- providerModelId: effectiveModel.providerModelId,
1575
- },
1576
- });
1577
- if (!effective.binding || effective.binding.state === "unavailable") {
1578
- return unavailable();
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" };
1579
2051
  }
1580
- const connection = effective.binding.connection;
1581
- const provider = effective.binding.providerType;
1582
- const packageId = effective.binding.packageId;
1583
- if (!connection || !provider || !packageId) return unavailable();
1584
- const capability = capabilities.find(
1585
- (candidate) =>
1586
- candidate.kind === "model" &&
1587
- candidate.packageId === packageId &&
1588
- candidate.connectionId === connection.connectionId,
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),
1589
2076
  );
1590
- if (!capability) return undefined;
1591
- let runtime: {
1592
- agentPackages: FoundationAgentPackage[];
1593
- modelSelection: RuntimeModelSelection;
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
+ }),
1594
2098
  };
1595
- try {
1596
- runtime = await this.agentRuntime(identity, settings);
1597
- } catch {
1598
- return unavailable();
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" };
1599
2108
  }
1600
- const selection = runtime.modelSelection;
1601
- const connectionId = selection.connectionId;
1602
- if (!connectionId) return unavailable();
2109
+ const request = decodeIsolateWorkspaceDeleteRequestV1(input.request);
1603
2110
  return {
1604
- state: "ready",
1605
- runtime,
1606
- binding: {
1607
- packageId: capability.packageId,
1608
- capabilityId: capability.capabilityId,
1609
- connectionId,
1610
- provider: selection.provider,
1611
- providerModelId: selection.model,
1612
- ...(selection.connectionGeneration
1613
- ? { connectionGeneration: selection.connectionGeneration }
1614
- : {}),
1615
- ...(selection.catalogGeneration
1616
- ? { catalogGeneration: selection.catalogGeneration }
1617
- : {}),
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,
1618
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
+ ) {
2235
+ return undefined;
2236
+ }
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
+ },
2269
+ );
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) {
2283
+ return {
2284
+ kind: "bot" as const,
2285
+ botId: input.botId,
2286
+ sessionId: input.sessionId,
2287
+ turnId: input.turnId,
2288
+ runId: input.runId,
1619
2289
  };
1620
2290
  }
1621
2291
 
1622
- private isolateCapabilityHost(
2292
+ private isolateCapabilities(
1623
2293
  scope: {
1624
2294
  botId: string;
1625
2295
  packageId: string;
1626
2296
  generationId: string;
1627
- request?: unknown;
1628
2297
  },
1629
- capabilities: readonly IsolateCapabilityV1[],
1630
- model?: { binding: IsolateModelBindingV1; path: IsolateModelPath },
1631
- unavailableModelBinding?: IsolateUnavailableModelBindingV1,
2298
+ authority: {
2299
+ connections: readonly IsolateConnectionV1[];
2300
+ model?: IsolateModelBindingV1;
2301
+ memory: boolean;
2302
+ workspace: boolean;
2303
+ },
2304
+ model?: { path: IsolateModelPath },
1632
2305
  ): IsolateCapabilityHost {
1633
2306
  return createIsolateCapabilityHost({
1634
2307
  storage: {
1635
2308
  put: (key, value) => this.ctx.storage.put(key, value),
1636
- get: (key) => this.ctx.storage.get(key),
1637
2309
  list: (options) => this.ctx.storage.list(options),
1638
2310
  },
1639
2311
  botId: scope.botId,
1640
2312
  packageId: scope.packageId,
1641
2313
  generationId: scope.generationId,
1642
- capabilities,
1643
- ...(model ? { modelBinding: model.binding, modelPath: model.path } : {}),
1644
- ...(unavailableModelBinding ? { unavailableModelBinding } : {}),
2314
+ connections: authority.connections,
2315
+ ...(authority.model ? { modelBinding: authority.model } : {}),
2316
+ ...(model ? { modelPath: model.path } : {}),
2317
+ memory: authority.memory,
2318
+ workspace: authority.workspace,
1645
2319
  });
1646
2320
  }
1647
2321
 
1648
2322
  /**
1649
2323
  * Streams through the pinned Composition's mounted `ctx.llm` — the same
1650
2324
  * provider path a Turn uses, so whichever provider Plugin serves the request
1651
- * is the one that takes the credential lease. The runtime is the one the
1652
- * effective model binding resolved to, so the Package that streams is the
1653
- * Package the enabled Capability names.
2325
+ * is the one that takes the credential lease.
1654
2326
  */
1655
2327
  private isolateModelPath(
1656
2328
  identity: BotIdentity,
@@ -1676,8 +2348,6 @@ export class ShellBotBackendContribution {
1676
2348
  sessionEvents: [],
1677
2349
  agentPackages: runtime.agentPackages,
1678
2350
  modelSelection: runtime.modelSelection,
1679
- // An isolate model invocation is not an admitted Turn, so there is
1680
- // no run to fence it against; User enablement admitted it.
1681
2351
  admitEffect: () => Promise.resolve(true),
1682
2352
  }).mount(generation, signal);
1683
2353
  try {
@@ -1707,10 +2377,10 @@ export class ShellBotBackendContribution {
1707
2377
  transaction: DurableObjectTransaction,
1708
2378
  resolved: BotSettingsViewV1,
1709
2379
  ): Promise<BotSettingsViewV1> {
1710
- return (
1711
- (await transaction.get<BotSettingsViewV1>(BOT_CONFIGURATION_KEY)) ??
1712
- resolved
1713
- );
2380
+ const stored = await transaction.get<unknown>(BOT_CONFIGURATION_KEY);
2381
+ return stored === undefined
2382
+ ? resolved
2383
+ : decodeBotSettingsViewV1(migrateStoredBotSettingsV1(stored));
1714
2384
  }
1715
2385
 
1716
2386
  private async scheduledDeadlines(
@@ -2827,12 +3497,15 @@ export class ShellBotBackendContribution {
2827
3497
  private authoringHost(
2828
3498
  identity: BotIdentity,
2829
3499
  turn: { runId: string; turnId: string },
3500
+ currentToolNames: () => readonly string[],
3501
+ mountedGeneration: () => CompositionGenerationV1 | undefined,
2830
3502
  ): PackageAuthoringHost {
2831
3503
  const artifacts = this.env.APPLICATION_ARTIFACTS;
2832
3504
  return createPackageAuthoringHost({
2833
3505
  storage: {
2834
3506
  get: (key) => this.ctx.storage.get(key),
2835
3507
  put: (entries) => this.ctx.storage.put(entries),
3508
+ list: (options) => this.ctx.storage.list(options),
2836
3509
  },
2837
3510
  composition: this.authority.composition,
2838
3511
  ...(this.env.PACKAGE_BUNDLER
@@ -2847,6 +3520,43 @@ export class ShellBotBackendContribution {
2847
3520
  runId: turn.runId,
2848
3521
  turnId: turn.turnId,
2849
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,
2850
3560
  });
2851
3561
  }
2852
3562
 
@@ -2871,6 +3581,9 @@ export class ShellBotBackendContribution {
2871
3581
  /** The task a child Turn is running, in a Subagent Durable Object. */
2872
3582
  subagentTaskId?: string;
2873
3583
  },
3584
+ currentToolNames: () => readonly string[] = () => [],
3585
+ mountedGeneration: () => CompositionGenerationV1 | undefined = () =>
3586
+ undefined,
2874
3587
  ): Promise<{
2875
3588
  agentPackages: FoundationAgentPackage[];
2876
3589
  capabilities: EnabledCapabilityV1[];
@@ -2968,13 +3681,29 @@ export class ShellBotBackendContribution {
2968
3681
  // Filled in once this Turn's model binding is resolved, below. The tool
2969
3682
  // and the prompt section both read it lazily, from inside the Turn.
2970
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;
2971
3699
  const resolvedAgentPackages: FoundationAgentPackage[] = [
2972
3700
  ...createFoundationHostedRuntimePackages(application, {
2973
3701
  userId: identity.userId,
2974
3702
  readSecret,
2975
3703
  // A Bot authors a Package only inside an admitted Turn, whose run and
2976
3704
  // session the artifact provenance names.
2977
- ...(turn ? { authoring: this.authoringHost(identity, turn) } : {}),
3705
+ ...(authoring ? { authoring } : {}),
3706
+ ...(packageCatalog ? { packageCatalog } : {}),
2978
3707
  ...(turn
2979
3708
  ? {
2980
3709
  skills: createBotSkillsHost(
@@ -3793,15 +4522,17 @@ export class ShellBotBackendContribution {
3793
4522
  return quarantine === undefined ? {} : { quarantine };
3794
4523
  }
3795
4524
 
3796
- /**
3797
- * SEAM — plan Step 5 (authoring) owns `authorship:intent:<effectId>` and
3798
- * `artifact:<contentHash>`. Those records do not exist yet, so an isolate
3799
- * member has no recorded source to show and the view carries members only.
3800
- */
3801
- private readCompositionMemberSource(
3802
- _member: CompositionMemberV1,
4525
+ /** Reads the immutable TypeScript source retained beside an authored artifact. */
4526
+ private async readCompositionMemberSource(
4527
+ member: CompositionMemberV1,
3803
4528
  ): Promise<string | undefined> {
3804
- return Promise.resolve(undefined);
4529
+ const bucket = this.env.APPLICATION_ARTIFACTS;
4530
+ if (!bucket) return undefined;
4531
+ return readAuthoredCompositionMemberSourceV1({
4532
+ storage: { get: (key) => this.ctx.storage.get(key) },
4533
+ artifacts: createR2AuthoringArtifactStore(bucket),
4534
+ member,
4535
+ });
3805
4536
  }
3806
4537
 
3807
4538
  /**
@@ -4345,6 +5076,9 @@ export class ShellBotBackendContribution {
4345
5076
  schemaVersion: 1;
4346
5077
  userId: string;
4347
5078
  }): Promise<UserSettingsViewV1>;
5079
+ executeConfiguration(
5080
+ input: Extract<ConfigurationCommandV1, { type: `user/${string}` }>,
5081
+ ): Promise<OperationReceiptV1>;
4348
5082
  readPackageRevisions(
4349
5083
  userId: string,
4350
5084
  ): ReturnType<PackagePublisherAgentHost["read"]>;
@@ -4416,6 +5150,7 @@ export class ShellBotBackendContribution {
4416
5150
  // SAFETY: this namespace is bound to UserConfiguration; generated Worker types do not expose its RPC surface.
4417
5151
  const rpc = this.env.USER_CONFIGURATIONS.get(id) as unknown as {
4418
5152
  readConfiguration(input: unknown): Promise<UserSettingsViewV1>;
5153
+ executeConfiguration(input: unknown): Promise<unknown>;
4419
5154
  readPackageRevisions(
4420
5155
  input: unknown,
4421
5156
  ): ReturnType<PackagePublisherAgentHost["read"]>;
@@ -4442,6 +5177,14 @@ export class ShellBotBackendContribution {
4442
5177
  };
4443
5178
  return {
4444
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
+ ),
4445
5188
  // A machine is a User asset, so every one of these crosses the seam and
4446
5189
  // is decoded on arrival rather than trusted in the shape RPC returned.
4447
5190
  listMachines: async (userId) =>
@@ -4562,12 +5305,10 @@ export class ShellBotBackendContribution {
4562
5305
  identity: BotIdentity,
4563
5306
  ): Promise<BotSettingsViewV1> {
4564
5307
  await this.validateIdentity(identity);
4565
- const existing = await this.ctx.storage.get<BotSettingsViewV1>(
4566
- BOT_CONFIGURATION_KEY,
4567
- );
4568
- if (!existing)
5308
+ const stored = await this.ctx.storage.get<unknown>(BOT_CONFIGURATION_KEY);
5309
+ if (stored === undefined)
4569
5310
  throw new Error(`Bot "${identity.botId}" is not materialized`);
4570
- return existing;
5311
+ return decodeBotSettingsViewV1(migrateStoredBotSettingsV1(stored));
4571
5312
  }
4572
5313
 
4573
5314
  private async resolveExecutionContext(identity: BotIdentity): Promise<{