@opengeni/core 1.0.1 → 2.1.0-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/access/index.d.ts +37 -0
  2. package/dist/application/new-session-drafts.d.ts +8 -1
  3. package/dist/application/session-commands.d.ts +18 -11
  4. package/dist/application/session-tenancy.d.ts +18 -0
  5. package/dist/application/user-resource-grants.d.ts +20 -0
  6. package/dist/{chunk-6WKPWE5S.js → chunk-H7X52RI2.js} +27 -18
  7. package/dist/chunk-H7X52RI2.js.map +1 -0
  8. package/dist/{chunk-JJU6I5XD.js → chunk-VCZFFEPB.js} +21 -9
  9. package/dist/chunk-VCZFFEPB.js.map +1 -0
  10. package/dist/{chunk-NYPMQ7NO.js → chunk-ZYCNLYGH.js} +29 -27
  11. package/dist/chunk-ZYCNLYGH.js.map +1 -0
  12. package/dist/dependencies.d.ts +12 -1
  13. package/dist/domain/automations.d.ts +51 -0
  14. package/dist/domain/company-brain-governed-writes.d.ts +70 -0
  15. package/dist/domain/company-profile-agent-admin.d.ts +24 -0
  16. package/dist/domain/connection-authority.d.ts +97 -0
  17. package/dist/domain/connection-authority.js +169 -0
  18. package/dist/domain/connection-authority.js.map +1 -0
  19. package/dist/domain/editable-artifacts/service.d.ts +1 -2
  20. package/dist/domain/environments.d.ts +1 -1
  21. package/dist/domain/github-repository-bindings.d.ts +117 -0
  22. package/dist/domain/governed-learning-activation.d.ts +16 -0
  23. package/dist/domain/governed-learning-evaluator.d.ts +6 -0
  24. package/dist/domain/governed-learning-slack-publication.d.ts +45 -0
  25. package/dist/domain/memory-slack-delivery.d.ts +1 -4
  26. package/dist/domain/organization-membership-lifecycle.d.ts +1 -0
  27. package/dist/domain/packs.d.ts +5 -1
  28. package/dist/domain/personal-connection-delegations.d.ts +34 -2
  29. package/dist/domain/pr-review.d.ts +149 -0
  30. package/dist/domain/remember.d.ts +69 -0
  31. package/dist/domain/resources.d.ts +22 -1
  32. package/dist/domain/scheduled-tasks.d.ts +43 -2
  33. package/dist/domain/session-tool-policy.d.ts +1 -1
  34. package/dist/domain/sessions.d.ts +40 -6
  35. package/dist/editable-artifact-live/application.d.ts +10 -2
  36. package/dist/editable-artifact-live/ports.d.ts +0 -1
  37. package/dist/editable-artifact-live/server.d.ts +0 -1
  38. package/dist/editable-artifact-live/ticket.d.ts +0 -1
  39. package/dist/editable-artifact-live/types.d.ts +5 -2
  40. package/dist/editable-artifact-live.js +2 -2
  41. package/dist/editable-artifacts.js +2 -4
  42. package/dist/index.d.ts +13 -1
  43. package/dist/index.js +4827 -1164
  44. package/dist/index.js.map +1 -1
  45. package/dist/rigs/index.d.ts +22 -5
  46. package/dist/sandbox/fleet.d.ts +34 -6
  47. package/dist/sandbox/routing.d.ts +15 -1
  48. package/dist/sandbox/runtime-settings.d.ts +39 -0
  49. package/package.json +16 -11
  50. package/src/access/index.ts +151 -31
  51. package/src/application/new-session-drafts.ts +24 -2
  52. package/src/application/session-commands.ts +305 -71
  53. package/src/application/session-tenancy.ts +321 -0
  54. package/src/application/user-resource-grants.ts +140 -0
  55. package/src/billing/limits.ts +25 -15
  56. package/src/dependencies.ts +12 -1
  57. package/src/domain/automations.ts +196 -0
  58. package/src/domain/capabilities.ts +7 -1
  59. package/src/domain/company-brain-governed-writes.ts +330 -0
  60. package/src/domain/company-profile-agent-admin.ts +55 -0
  61. package/src/domain/connection-authority.ts +329 -0
  62. package/src/domain/editable-artifacts/agent-application.ts +17 -9
  63. package/src/domain/editable-artifacts/service.ts +12 -1
  64. package/src/domain/editable-artifacts/snapshot-compaction.ts +9 -5
  65. package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +10 -8
  66. package/src/domain/editable-artifacts/snapshot-verifier.ts +10 -3
  67. package/src/domain/environments.ts +16 -3
  68. package/src/domain/github-repository-bindings.ts +295 -0
  69. package/src/domain/governed-learning-activation.ts +44 -0
  70. package/src/domain/governed-learning-evaluator.ts +49 -0
  71. package/src/domain/governed-learning-slack-publication.ts +157 -0
  72. package/src/domain/insights.ts +9 -0
  73. package/src/domain/memory-slack-delivery.ts +0 -30
  74. package/src/domain/organization-membership-lifecycle.ts +9 -0
  75. package/src/domain/packs.ts +106 -3
  76. package/src/domain/personal-connection-delegations.ts +532 -51
  77. package/src/domain/pr-review.ts +671 -0
  78. package/src/domain/remember.ts +549 -0
  79. package/src/domain/resources.ts +131 -4
  80. package/src/domain/scheduled-tasks.ts +497 -76
  81. package/src/domain/session-tool-policy.ts +12 -9
  82. package/src/domain/sessions.ts +613 -166
  83. package/src/editable-artifact-live/application.ts +21 -3
  84. package/src/editable-artifact-live/ports.ts +0 -1
  85. package/src/editable-artifact-live/server.ts +13 -7
  86. package/src/editable-artifact-live/ticket.ts +1 -5
  87. package/src/editable-artifact-live/types.ts +6 -2
  88. package/src/editable-artifact-live/wire.ts +0 -10
  89. package/src/index.ts +13 -1
  90. package/src/rigs/index.ts +165 -48
  91. package/src/sandbox/fleet.ts +376 -30
  92. package/src/sandbox/routing.ts +91 -29
  93. package/src/sandbox/runtime-settings.ts +274 -0
  94. package/src/session-authorization.ts +17 -49
  95. package/dist/chunk-6WKPWE5S.js.map +0 -1
  96. package/dist/chunk-JJU6I5XD.js.map +0 -1
  97. package/dist/chunk-NYPMQ7NO.js.map +0 -1
  98. package/dist/domain/durable-learning-slack-publication.d.ts +0 -72
  99. package/src/domain/durable-learning-slack-publication.ts +0 -202
@@ -15,7 +15,7 @@ import { sandboxLifecycleTransitionWaitMs, type Settings } from "@opengeni/confi
15
15
  import {
16
16
  advanceWorkspaceGenerationForDirectRequest,
17
17
  advanceWorkspaceGenerationForRetainedProcess,
18
- getEnrollment,
18
+ getLiveEnrollmentConnection,
19
19
  getRetainedProcess,
20
20
  getSandbox,
21
21
  markWarmLeaseInstanceLost,
@@ -28,6 +28,7 @@ import {
28
28
  type Database,
29
29
  type SandboxWorkspaceMutationAdmission,
30
30
  } from "@opengeni/db";
31
+ import { settleSessionBackgroundCommandForRetainedProcess } from "@opengeni/db/session-background-commands";
31
32
  import { appendAndPublishEvents, type EventBus } from "@opengeni/events";
32
33
  import {
33
34
  isProviderSandboxGoneDuringRoutedOperation,
@@ -46,6 +47,7 @@ import {
46
47
  type RoutingRetainedProcessTerminalProof,
47
48
  type RoutingSandboxOperationObserver,
48
49
  type SelfhostedRelayConfig,
50
+ type SelfhostedConnectionBinding,
49
51
  type SelfhostedOpStreamDeps,
50
52
  } from "@opengeni/runtime/sandbox";
51
53
 
@@ -56,6 +58,34 @@ type PersistableMutationAdmission = {
56
58
  > | null;
57
59
  };
58
60
 
61
+ type DirectRetainedProcessRoute = {
62
+ providerSessionId: number;
63
+ providerBackend: string;
64
+ providerInstanceId: string;
65
+ leaseEpoch: number;
66
+ routeKind: "home" | "active";
67
+ routeTargetId: string | null;
68
+ routeEpoch: number;
69
+ };
70
+
71
+ /** API-direct requests always use active-route authority. The default active
72
+ * pointer is represented by a null target id; it is not a home-route write. */
73
+ export function directRetainedProcessMatchesBackend(
74
+ durable: DirectRetainedProcessRoute,
75
+ process: RoutingRetainedProcess,
76
+ backend: ResolvedActiveBackend,
77
+ ): boolean {
78
+ return (
79
+ durable.providerSessionId === process.providerSessionId &&
80
+ durable.providerBackend === backend.kind &&
81
+ durable.providerInstanceId === backend.providerInstanceId &&
82
+ durable.leaseEpoch === backend.leaseEpoch &&
83
+ durable.routeKind === "active" &&
84
+ durable.routeTargetId === backend.sandboxId &&
85
+ durable.routeEpoch === backend.activeEpoch
86
+ );
87
+ }
88
+
59
89
  export type ChannelARoutingServices = {
60
90
  db: Database;
61
91
  settings: Settings;
@@ -118,24 +148,31 @@ function controlRpcFactory(bus: EventBus | undefined): () => ControlRpc {
118
148
  });
119
149
  }
120
150
 
121
- async function resolveSelfhostedOpStream(
151
+ async function resolveSelfhostedConnection(
122
152
  services: ChannelARoutingServices,
123
- workspaceId: string,
153
+ access: string | { accountId: string; workspaceId: string; subjectId: string },
124
154
  sandbox: RoutableSandbox,
125
- ): Promise<SelfhostedOpStreamDeps | undefined> {
126
- if (
127
- services.settings.agentOpStreamEnabled !== true ||
128
- !services.bus?.getOpStreamConnection ||
129
- !sandbox.enrollmentId
130
- ) {
131
- return undefined;
132
- }
133
- const enrollment = await getEnrollment(services.db, workspaceId, sandbox.enrollmentId);
134
- if (enrollment?.opStream !== true) return undefined;
155
+ ): Promise<SelfhostedConnectionBinding | null> {
156
+ if (!sandbox.enrollmentId) return null;
157
+ const enrollment = await getLiveEnrollmentConnection(services.db, access, sandbox.enrollmentId);
158
+ if (!enrollment?.connectionInstanceId) return null;
159
+ const opStream: SelfhostedOpStreamDeps | undefined =
160
+ services.settings.agentOpStreamEnabled === true &&
161
+ services.bus?.getOpStreamConnection &&
162
+ enrollment.opStream === true
163
+ ? {
164
+ transport: new NatsOpStreamTransport(
165
+ async () => services.bus?.getOpStreamConnection?.() ?? null,
166
+ ),
167
+ }
168
+ : undefined;
135
169
  return {
136
- transport: new NatsOpStreamTransport(
137
- async () => services.bus?.getOpStreamConnection?.() ?? null,
138
- ),
170
+ connectionInstanceId: enrollment.connectionInstanceId,
171
+ workspaceRoot: enrollment.workspaceRoot,
172
+ ...(opStream ? { opStream } : {}),
173
+ operationResourcePolicy: enrollment.operationPolicy,
174
+ operationResourcePolicySupported: enrollment.agentCapabilities.operationResourcePolicy === true,
175
+ operationCpuQuotaSupported: enrollment.agentCapabilities.operationCpuQuota === true,
139
176
  };
140
177
  }
141
178
 
@@ -157,6 +194,7 @@ export function wrapChannelABoxWithRouting(
157
194
  accountId: string;
158
195
  workspaceId: string;
159
196
  sessionId: string;
197
+ resourceSubjectId: string;
160
198
  homeLease?: {
161
199
  sandboxGroupId: string;
162
200
  leaseEpoch: number;
@@ -209,6 +247,7 @@ export function wrapChannelABoxWithRouting(
209
247
  sessionId: ids.sessionId,
210
248
  requestId: ids.directRequest.requestId,
211
249
  holderId: ids.directRequest.holderId,
250
+ initiatorSubjectId: ids.resourceSubjectId,
212
251
  sandboxGroupId: homeLease.sandboxGroupId,
213
252
  expectedEpoch: backend.leaseEpoch,
214
253
  expectedInstanceId: backend.providerInstanceId,
@@ -267,10 +306,15 @@ export function wrapChannelABoxWithRouting(
267
306
  admittedWorkspaceGeneration: exactAdmission.workspaceGeneration,
268
307
  operation: op,
269
308
  providerBinding: boundAdmission.providerBinding ?? null,
309
+ backgroundCommand: {
310
+ commandId: retainedProcess.id,
311
+ command: op,
312
+ },
270
313
  owner: {
271
314
  kind: "direct",
272
315
  requestId: ids.directRequest.requestId,
273
316
  holderId: ids.directRequest.holderId,
317
+ initiatorSubjectId: ids.resourceSubjectId,
274
318
  sandboxGroupId: homeLease.sandboxGroupId,
275
319
  expectedEpoch: backend.leaseEpoch,
276
320
  expectedInstanceId: backend.providerInstanceId,
@@ -286,6 +330,7 @@ export function wrapChannelABoxWithRouting(
286
330
  sessionId: ids.sessionId,
287
331
  requestId: ids.directRequest.requestId,
288
332
  holderId: ids.directRequest.holderId,
333
+ initiatorSubjectId: ids.resourceSubjectId,
289
334
  sandboxGroupId: homeLease.sandboxGroupId,
290
335
  expectedEpoch: backend.leaseEpoch,
291
336
  expectedInstanceId: backend.providerInstanceId,
@@ -373,16 +418,7 @@ export function wrapChannelABoxWithRouting(
373
418
  sessionId: ids.sessionId,
374
419
  processId: process.id,
375
420
  });
376
- if (
377
- !durable ||
378
- durable.providerSessionId !== process.providerSessionId ||
379
- durable.providerBackend !== backend.kind ||
380
- durable.providerInstanceId !== backend.providerInstanceId ||
381
- durable.leaseEpoch !== backend.leaseEpoch ||
382
- durable.routeKind !== (backend.sandboxId === null ? "home" : "active") ||
383
- durable.routeTargetId !== backend.sandboxId ||
384
- durable.routeEpoch !== backend.activeEpoch
385
- ) {
421
+ if (!durable || !directRetainedProcessMatchesBackend(durable, process, backend)) {
386
422
  throw new Error(
387
423
  "API retained-process settlement lost its exact durable backend identity",
388
424
  );
@@ -398,6 +434,15 @@ export function wrapChannelABoxWithRouting(
398
434
  reason: proof.reason,
399
435
  idleGraceMs: settings.sandboxIdleGraceMs,
400
436
  });
437
+ await settleSessionBackgroundCommandForRetainedProcess(db, {
438
+ accountId: ids.accountId,
439
+ workspaceId: ids.workspaceId,
440
+ sessionId: ids.sessionId,
441
+ retainedProcessId: process.id,
442
+ outcome: proof.outcome,
443
+ exitCode: proof.exitCode,
444
+ reason: proof.reason,
445
+ });
401
446
  }
402
447
  : undefined;
403
448
  const resolver = makeActiveBackendResolver({
@@ -405,10 +450,19 @@ export function wrapChannelABoxWithRouting(
405
450
  defaultBackend: established.session as RoutableBackendSession,
406
451
  defaultKind: established.backendId,
407
452
  getSandbox: async (sandboxId): Promise<RoutableSandbox | null> => {
408
- const sandbox = await getSandbox(db, ids.workspaceId, sandboxId);
453
+ const sandbox = await getSandbox(
454
+ db,
455
+ {
456
+ accountId: ids.accountId,
457
+ workspaceId: ids.workspaceId,
458
+ subjectId: ids.resourceSubjectId,
459
+ },
460
+ sandboxId,
461
+ );
409
462
  return sandbox
410
463
  ? {
411
464
  id: sandbox.id,
465
+ workspaceId: sandbox.workspaceId,
412
466
  kind: sandbox.kind,
413
467
  name: sandbox.name,
414
468
  enrollmentId: sandbox.enrollmentId,
@@ -416,8 +470,16 @@ export function wrapChannelABoxWithRouting(
416
470
  : null;
417
471
  },
418
472
  controlRpcFactory: controlRpcFactory(bus),
419
- resolveSelfhostedOpStream: (sandbox) =>
420
- resolveSelfhostedOpStream(services, ids.workspaceId, sandbox),
473
+ resolveSelfhostedConnection: (sandbox) =>
474
+ resolveSelfhostedConnection(
475
+ services,
476
+ {
477
+ accountId: ids.accountId,
478
+ workspaceId: ids.workspaceId,
479
+ subjectId: ids.resourceSubjectId,
480
+ },
481
+ sandbox,
482
+ ),
421
483
  relay: relayConfigFromSettings(settings),
422
484
  selfhostedTimeoutMs: settings.sandboxSelfhostedControlTimeoutMs,
423
485
  selfhostedExecTimeoutMs: settings.sandboxSelfhostedExecTimeoutMs,
@@ -0,0 +1,274 @@
1
+ import type { Settings } from "@opengeni/config";
2
+ import {
3
+ CapabilityPack,
4
+ type RigVersion,
5
+ type Session,
6
+ type SandboxBackend,
7
+ } from "@opengeni/contracts";
8
+ import {
9
+ getRigVersion,
10
+ getWorkspacePack,
11
+ listPackInstallations,
12
+ type Database,
13
+ } from "@opengeni/db";
14
+ import { resolveModalCheckpointProviderBinding } from "@opengeni/runtime/sandbox";
15
+ import {
16
+ rigProviderImageContentHash,
17
+ rigProviderImageMatchesDefinition,
18
+ rigProviderImageProviderBindingKeyHash,
19
+ } from "../rigs/provider-images";
20
+
21
+ /** Pre-V2 Packs are the sole compatibility path where a Pack can still own a
22
+ * sandbox image. V2 installations select a Rig instead. Keep this predicate at
23
+ * the shared runtime boundary so turns, API-direct tools, viewers, Browser and
24
+ * Computer cannot resolve different physical machines for the same session. */
25
+ export function packInstallationUsesLegacyRuntime(input: {
26
+ manifestSnapshot: unknown | null;
27
+ manifestDigest: string | null;
28
+ }): boolean {
29
+ return input.manifestSnapshot === null && input.manifestDigest === null;
30
+ }
31
+
32
+ export async function resolveWorkspaceLegacyRuntimePacks(
33
+ db: Database,
34
+ workspaceId: string,
35
+ ): Promise<CapabilityPack[]> {
36
+ const installations = await listPackInstallations(db, workspaceId);
37
+ const packs: CapabilityPack[] = [];
38
+ for (const installation of installations) {
39
+ if (installation.status !== "active" || !packInstallationUsesLegacyRuntime(installation)) {
40
+ continue;
41
+ }
42
+ const registration = await getWorkspacePack(db, workspaceId, installation.packId);
43
+ const parsed = CapabilityPack.safeParse(registration?.pack);
44
+ if (parsed.success) packs.push(parsed.data);
45
+ }
46
+ return packs;
47
+ }
48
+
49
+ export function legacySandboxRuntimeFromPacks(packs: readonly CapabilityPack[]): {
50
+ sandboxImage: string | null;
51
+ sandboxProviderImages: CapabilityPack["sandboxProviderImages"] | null;
52
+ } {
53
+ const imagePacks = packs.filter(
54
+ (pack) => typeof pack.sandboxImage === "string" && pack.sandboxImage.trim().length > 0,
55
+ );
56
+ if (imagePacks.length > 1) {
57
+ const ids = imagePacks
58
+ .map((pack) => pack.id)
59
+ .sort()
60
+ .join(", ");
61
+ throw new Error(
62
+ `Multiple enabled packs declare a sandbox image (${ids}). Only one enabled pack per workspace may declare sandboxImage; disable the others and retry.`,
63
+ );
64
+ }
65
+ return {
66
+ sandboxImage: imagePacks[0]?.sandboxImage?.trim() ?? null,
67
+ sandboxProviderImages: imagePacks[0]?.sandboxProviderImages ?? null,
68
+ };
69
+ }
70
+
71
+ export function settingsWithPackSandboxImage(
72
+ settings: Settings,
73
+ sandboxImage: string | null,
74
+ sandboxProviderImages: CapabilityPack["sandboxProviderImages"] | null = null,
75
+ ): Settings {
76
+ if (!sandboxImage) return settings;
77
+ return {
78
+ ...settings,
79
+ dockerImage: sandboxImage,
80
+ modalImageRef: sandboxImage,
81
+ modalImageId: sandboxProviderImages?.modal?.imageId,
82
+ };
83
+ }
84
+
85
+ export function settingsWithRigImage(settings: Settings, rigImage: string | null): Settings {
86
+ if (!rigImage) return settings;
87
+ return {
88
+ ...settings,
89
+ dockerImage: rigImage,
90
+ modalImageRef: rigImage,
91
+ modalImageId: undefined,
92
+ };
93
+ }
94
+
95
+ export function rigProviderImageSourceImage(
96
+ settings: Settings,
97
+ backend: SandboxBackend,
98
+ ): string | null {
99
+ if (backend === "modal") return settings.modalImageId ?? settings.modalImageRef ?? null;
100
+ if (backend === "docker") return settings.dockerImage ?? null;
101
+ return null;
102
+ }
103
+
104
+ export type RigProviderImageSelectionReason =
105
+ | "selected"
106
+ | "missing"
107
+ | "provider_unsupported"
108
+ | "not_ready"
109
+ | "not_cold_boot_validated"
110
+ | "content_mismatch"
111
+ | "provider_binding_unavailable"
112
+ | "provider_binding_mismatch";
113
+
114
+ export function resolveRigProviderImageSelection(
115
+ settings: Settings,
116
+ version: RigVersion | null,
117
+ backend: SandboxBackend,
118
+ currentProviderBindingKeyHash: string | null,
119
+ ): {
120
+ settings: Settings;
121
+ reason: RigProviderImageSelectionReason;
122
+ contentHash: string | null;
123
+ imageId: string | null;
124
+ } {
125
+ if (!version) return { settings, reason: "missing", contentHash: null, imageId: null };
126
+ if (backend !== "modal") {
127
+ return { settings, reason: "provider_unsupported", contentHash: null, imageId: null };
128
+ }
129
+ const image = version.providerImages[backend];
130
+ if (!image) return { settings, reason: "missing", contentHash: null, imageId: null };
131
+ if (image.status !== "ready" || !image.imageId) {
132
+ return {
133
+ settings,
134
+ reason: "not_ready",
135
+ contentHash: image.contentHash,
136
+ imageId: image.imageId,
137
+ };
138
+ }
139
+ if (image.coldBootValidation?.version !== 1) {
140
+ return {
141
+ settings,
142
+ reason: "not_cold_boot_validated",
143
+ contentHash: image.contentHash,
144
+ imageId: null,
145
+ };
146
+ }
147
+ const sourceImage = rigProviderImageSourceImage(settings, backend);
148
+ const expectedContentHash = rigProviderImageContentHash({
149
+ backend,
150
+ sourceImage,
151
+ definition: version,
152
+ });
153
+ if (
154
+ image.sourceImage !== sourceImage ||
155
+ image.contentHash !== expectedContentHash ||
156
+ !rigProviderImageMatchesDefinition(image, version)
157
+ ) {
158
+ return {
159
+ settings,
160
+ reason: "content_mismatch",
161
+ contentHash: expectedContentHash,
162
+ imageId: null,
163
+ };
164
+ }
165
+ if (!image.providerBindingKeyHash || !currentProviderBindingKeyHash) {
166
+ return {
167
+ settings,
168
+ reason: "provider_binding_unavailable",
169
+ contentHash: expectedContentHash,
170
+ imageId: null,
171
+ };
172
+ }
173
+ if (image.providerBindingKeyHash !== currentProviderBindingKeyHash) {
174
+ return {
175
+ settings,
176
+ reason: "provider_binding_mismatch",
177
+ contentHash: expectedContentHash,
178
+ imageId: null,
179
+ };
180
+ }
181
+ return {
182
+ settings: { ...settings, modalImageId: image.imageId },
183
+ reason: "selected",
184
+ contentHash: expectedContentHash,
185
+ imageId: image.imageId,
186
+ };
187
+ }
188
+
189
+ export async function resolveRigProviderImageForRun(
190
+ settings: Settings,
191
+ version: RigVersion | null,
192
+ backend: SandboxBackend,
193
+ resolveBinding: typeof resolveModalCheckpointProviderBinding = resolveModalCheckpointProviderBinding,
194
+ ): Promise<ReturnType<typeof resolveRigProviderImageSelection>> {
195
+ const image = backend === "modal" ? version?.providerImages.modal : null;
196
+ if (image?.status !== "ready" || !image.providerBindingKeyHash) {
197
+ return resolveRigProviderImageSelection(settings, version, backend, null);
198
+ }
199
+ const structural = resolveRigProviderImageSelection(
200
+ settings,
201
+ version,
202
+ backend,
203
+ image.providerBindingKeyHash,
204
+ );
205
+ if (structural.reason !== "selected") return structural;
206
+ let currentProviderBindingKeyHash: string | null = null;
207
+ try {
208
+ const binding = await resolveBinding(settings);
209
+ currentProviderBindingKeyHash = rigProviderImageProviderBindingKeyHash(binding.key);
210
+ } catch {
211
+ // A provider-native image is an optimization. The exact logical image is
212
+ // still the correct cold-create fallback when provider identity is absent.
213
+ }
214
+ return resolveRigProviderImageSelection(
215
+ settings,
216
+ version,
217
+ backend,
218
+ currentProviderBindingKeyHash,
219
+ );
220
+ }
221
+
222
+ export async function settingsWithRigProviderImage(
223
+ settings: Settings,
224
+ version: RigVersion | null,
225
+ backend: SandboxBackend,
226
+ resolveBinding: typeof resolveModalCheckpointProviderBinding = resolveModalCheckpointProviderBinding,
227
+ ): Promise<Settings> {
228
+ return (await resolveRigProviderImageForRun(settings, version, backend, resolveBinding)).settings;
229
+ }
230
+
231
+ export type SessionSandboxRuntime = {
232
+ /** Exact logical image/rig settings used to fence the durable group lease. */
233
+ settings: Settings;
234
+ image: string | null;
235
+ rigVersion: RigVersion | null;
236
+ };
237
+
238
+ export async function resolveSessionSandboxRuntime(
239
+ db: Database,
240
+ settings: Settings,
241
+ session: Pick<Session, "workspaceId" | "sandboxBackend" | "rigId" | "rigVersionId">,
242
+ ): Promise<SessionSandboxRuntime> {
243
+ const [packs, rigVersion] = await Promise.all([
244
+ resolveWorkspaceLegacyRuntimePacks(db, session.workspaceId),
245
+ session.rigId && session.rigVersionId
246
+ ? getRigVersion(db, session.workspaceId, session.rigId, session.rigVersionId)
247
+ : Promise.resolve(null),
248
+ ]);
249
+ if (session.rigVersionId && !rigVersion) {
250
+ throw new Error(`Frozen rig version ${session.rigVersionId} is unavailable`);
251
+ }
252
+ const legacy = legacySandboxRuntimeFromPacks(packs);
253
+ const logicalSettings = settingsWithRigImage(
254
+ settingsWithPackSandboxImage(settings, legacy.sandboxImage, legacy.sandboxProviderImages),
255
+ rigVersion?.image ?? null,
256
+ );
257
+ return {
258
+ settings: {
259
+ ...logicalSettings,
260
+ sandboxBackend: session.sandboxBackend,
261
+ },
262
+ image: rigProviderImageSourceImage(logicalSettings, session.sandboxBackend),
263
+ rigVersion,
264
+ };
265
+ }
266
+
267
+ /** Resolve provider-native cold-create optimization only for the single CAS
268
+ * winner. Ordinary API calls never perform provider identity I/O. */
269
+ export async function providerSettingsForSessionSandboxRuntime(
270
+ runtime: SessionSandboxRuntime,
271
+ backend: SandboxBackend,
272
+ ): Promise<Settings> {
273
+ return await settingsWithRigProviderImage(runtime.settings, runtime.rigVersion, backend);
274
+ }
@@ -67,52 +67,16 @@ function grantHasAgentAttemptAuthority(grant: AccessGrant): boolean {
67
67
  }
68
68
 
69
69
  /**
70
- * Read-only access an immediate child may use against its parent. Upstream
71
- * mutation is deliberately limited to `session.append`: letting a child Pause,
72
- * Steer, or otherwise mutate its parent would let it influence siblings through
73
- * the parent's recursive control and shared state.
70
+ * Live agent attempts may address any workspace session that later private,
71
+ * Slack-owner, and optional host checks still allow. Parent/child lineage is
72
+ * not an access deny. Cross-session projections stay exact-target so a peer
73
+ * read does not receive another session's derived metadata.
74
74
  */
75
- const AGENT_PARENT_READ_OPERATIONS = new Set<SessionAuthorizationOperation>([
76
- "session.read",
77
- "session.events.read",
78
- "session.stream.read",
79
- "session.turns.read",
80
- "session.queue.read",
81
- "session.composer.read",
82
- "session.lineage.read",
83
- "session.capture.read",
84
- "session.files.read",
85
- "session.git.read",
86
- "session.terminal.read",
87
- "session.viewer.read",
88
- "session.goal.read",
89
- "session.human_input.read",
90
- ]);
91
-
92
- function enforceAgentSessionHierarchy(
75
+ function relatedSessionAccessForAgentAttempt(
93
76
  actor: Extract<SessionAuthorizationActor, { kind: "agent_attempt" }>,
94
- callerParentSessionId: string | null,
95
- target: ResolvedSessionAuthorizationTarget,
96
- operation: SessionAuthorizationOperation,
77
+ targetSessionId: string,
97
78
  ): "target" | "root" {
98
- if (target.target.sessionId === actor.callerSessionId) return "root";
99
-
100
- // A manager may inspect and operate an immediate child. Existing permission
101
- // and host-policy checks still apply; lineage never grants a capability.
102
- if (target.parentSessionId === actor.callerSessionId) return "target";
103
-
104
- // A child may report to and inspect its immediate parent, but cannot mutate
105
- // the parent except through the canonical machine-input message boundary.
106
- if (callerParentSessionId === target.target.sessionId) {
107
- if (operation === "session.append" || AGENT_PARENT_READ_OPERATIONS.has(operation)) {
108
- return "target";
109
- }
110
- throw new SessionAuthorizationDeniedError("forbidden");
111
- }
112
-
113
- // Siblings, skipped generations, other branches, and unrelated roots are
114
- // never cross-session authority for a live agent attempt.
115
- throw new SessionAuthorizationDeniedError("forbidden");
79
+ return targetSessionId === actor.callerSessionId ? "root" : "target";
116
80
  }
117
81
 
118
82
  export function sessionRlsActorForAuthorization(
@@ -201,14 +165,18 @@ export async function requireSessionAuthorization(
201
165
  const target = resolvedTarget.target;
202
166
  const agentRelatedSessionAccess =
203
167
  actor.kind === "agent_attempt"
204
- ? enforceAgentSessionHierarchy(
205
- actor,
206
- resolvedActor.callerParentSessionId,
207
- resolvedTarget,
208
- input.operation,
209
- )
168
+ ? relatedSessionAccessForAgentAttempt(actor, target.sessionId)
210
169
  : null;
211
170
 
171
+ // Tool approvals are a human decision. An agent attempt may answer another
172
+ // session's structured human-input request (`session.human_input.write`,
173
+ // through `session_human_input_respond`) but may never approve or reject a
174
+ // pending tool approval on any session, including a child it spawned; an
175
+ // embedding-host port cannot widen this.
176
+ if (actor.kind === "agent_attempt" && input.operation === "session.approval.write") {
177
+ throw new SessionAuthorizationDeniedError("forbidden");
178
+ }
179
+
212
180
  if (authority.visibility === "user_private") {
213
181
  const allowed =
214
182
  authority.ownerSubjectId !== null &&