@opengeni/worker-bundle 2.2.0 → 2.2.1-canary.36927100590001

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 (88) hide show
  1. package/dist/activities/agent-run-admission.d.ts +3 -1
  2. package/dist/activities/agent-turn/admission.d.ts +24 -3
  3. package/dist/activities/agent-turn/agent-build.d.ts +1 -0
  4. package/dist/activities/agent-turn/claude-usage-observer.d.ts +13 -1
  5. package/dist/activities/agent-turn/compaction-prep.d.ts +20 -0
  6. package/dist/activities/agent-turn/credential-restriction.d.ts +3 -0
  7. package/dist/activities/agent-turn/final-reply.d.ts +18 -0
  8. package/dist/activities/agent-turn/governance-model.d.ts +6 -0
  9. package/dist/activities/agent-turn/model-call-admission.d.ts +15 -0
  10. package/dist/activities/agent-turn/run-credentials.d.ts +4 -0
  11. package/dist/activities/agent-turn/sandbox-establish.d.ts +1 -0
  12. package/dist/activities/agent-turn/session-title.d.ts +3 -1
  13. package/dist/activities/agent-turn/stream-attempt.d.ts +1 -0
  14. package/dist/activities/agent-turn/turn-context.d.ts +1 -0
  15. package/dist/activities/automations.d.ts +3 -1
  16. package/dist/activities/browser-deadline-checkpoint.d.ts +29 -0
  17. package/dist/activities/goals.d.ts +8 -0
  18. package/dist/activities/run-credentials.d.ts +4 -2
  19. package/dist/activities/scheduled-tasks.d.ts +4 -35
  20. package/dist/activities/types.d.ts +4 -1
  21. package/dist/activities/workspace-capture.d.ts +2 -0
  22. package/dist/{activities-control-MHNA3CYV.js → activities-control-4F2A734B.js} +502 -133
  23. package/dist/activities-control-4F2A734B.js.map +1 -0
  24. package/dist/activities-control.d.ts +30 -0
  25. package/dist/{activities-turn-FQ5OO2WW.js → activities-turn-TESOIOEX.js} +696 -251
  26. package/dist/activities-turn-TESOIOEX.js.map +1 -0
  27. package/dist/activities.d.ts +30 -0
  28. package/dist/browser-deadline-checkpoint-contract.d.ts +2 -0
  29. package/dist/{chunk-RZAAGRFQ.js → chunk-4PJTUBWQ.js} +220 -88
  30. package/dist/chunk-4PJTUBWQ.js.map +1 -0
  31. package/dist/{chunk-IJZU752L.js → chunk-YG27NEMX.js} +34 -1
  32. package/dist/chunk-YG27NEMX.js.map +1 -0
  33. package/dist/index.d.ts +15 -0
  34. package/dist/index.js +46 -24
  35. package/dist/index.js.map +1 -1
  36. package/dist/observability-metrics.d.ts +16 -1
  37. package/dist/sandbox-resume.d.ts +4 -1
  38. package/dist/workflow-bundle.js +89 -21
  39. package/dist/workflows/browser-deadline-checkpoint.d.ts +9 -0
  40. package/dist/workflows/scheduled-tasks.d.ts +4 -0
  41. package/dist/workflows.d.ts +1 -0
  42. package/package.json +21 -20
  43. package/src/activities/agent-run-admission.ts +17 -1
  44. package/src/activities/agent-turn/admission.ts +64 -1
  45. package/src/activities/agent-turn/agent-build.ts +24 -3
  46. package/src/activities/agent-turn/claim.ts +69 -15
  47. package/src/activities/agent-turn/claude-usage-observer.ts +33 -1
  48. package/src/activities/agent-turn/compaction-prep.ts +67 -16
  49. package/src/activities/agent-turn/credential-restriction.ts +14 -0
  50. package/src/activities/agent-turn/errors.ts +55 -0
  51. package/src/activities/agent-turn/failure-settlement.ts +26 -6
  52. package/src/activities/agent-turn/final-reply.ts +46 -0
  53. package/src/activities/agent-turn/governance-model.ts +12 -0
  54. package/src/activities/agent-turn/model-call-admission.ts +94 -0
  55. package/src/activities/agent-turn/run-credentials.ts +20 -3
  56. package/src/activities/agent-turn/run.ts +44 -5
  57. package/src/activities/agent-turn/sandbox-establish.ts +9 -5
  58. package/src/activities/agent-turn/session-title.ts +8 -2
  59. package/src/activities/agent-turn/stream-attempt.ts +144 -38
  60. package/src/activities/agent-turn/tool-environment.ts +55 -39
  61. package/src/activities/agent-turn/turn-context.ts +1 -0
  62. package/src/activities/automations.ts +78 -4
  63. package/src/activities/browser-deadline-checkpoint.ts +279 -0
  64. package/src/activities/capabilities.ts +52 -14
  65. package/src/activities/goals.ts +95 -98
  66. package/src/activities/knowledge-indexing.ts +29 -0
  67. package/src/activities/open-suffix-resume.ts +2 -1
  68. package/src/activities/run-credentials.ts +22 -16
  69. package/src/activities/sandbox-lease.ts +22 -0
  70. package/src/activities/scheduled-tasks.ts +172 -38
  71. package/src/activities/site-auth-maintenance.ts +1 -0
  72. package/src/activities/types.ts +4 -0
  73. package/src/activities/workspace-capture.ts +13 -2
  74. package/src/activities/workspace-credential-provider.ts +12 -0
  75. package/src/activities-control.ts +2 -0
  76. package/src/activities.ts +3 -0
  77. package/src/browser-deadline-checkpoint-contract.ts +5 -0
  78. package/src/index.ts +53 -25
  79. package/src/observability-metrics.ts +55 -1
  80. package/src/sandbox-resume.ts +23 -8
  81. package/src/sandbox-routing.ts +4 -2
  82. package/src/workflows/browser-deadline-checkpoint.ts +50 -0
  83. package/src/workflows/scheduled-tasks.ts +26 -13
  84. package/src/workflows.ts +4 -0
  85. package/dist/activities-control-MHNA3CYV.js.map +0 -1
  86. package/dist/activities-turn-FQ5OO2WW.js.map +0 -1
  87. package/dist/chunk-IJZU752L.js.map +0 -1
  88. package/dist/chunk-RZAAGRFQ.js.map +0 -1
@@ -4,18 +4,23 @@ import {
4
4
  WORKSPACE_OPENROUTER_MODEL_ID_PREFIX,
5
5
  } from "@opengeni/config";
6
6
  import {
7
+ agentConfigMayResolve,
8
+ applySessionAgentConfigWriteThrough,
7
9
  assertWorkspaceModelPolicyAllows,
8
10
  canonicalConfiguredModel,
11
+ resolveSessionAgentConfigForCreate,
9
12
  createAndStartSessionWithOutcome,
10
13
  recordWorkspaceUsage,
11
14
  resolveCatalogSettings,
12
15
  resolveWorkspaceCatalogSettings,
16
+ isDeveloperSetupDelegatedPermissionAllowed,
13
17
  } from "@opengeni/core";
14
18
  import {
15
19
  AutomationAuthorityRevokedError,
16
20
  assertAutomationRunAuthorityInTransaction,
17
21
  claimAutomationRun,
18
22
  settleAutomationRun,
23
+ requireWorkspace,
19
24
  } from "@opengeni/db";
20
25
  import { SessionSkills } from "@opengeni/contracts";
21
26
  import { agentRunAdmissionDenial } from "./agent-run-admission";
@@ -30,6 +35,8 @@ export type AutomationActivityOptions = {
30
35
  settle?: typeof settleAutomationRun;
31
36
  assertAuthority?: typeof assertAutomationRunAuthorityInTransaction;
32
37
  createSession?: typeof createAndStartSessionWithOutcome;
38
+ /** Workspace read for agent-configuration defaults (tests inject it). */
39
+ readWorkspace?: typeof requireWorkspace;
33
40
  admit?: typeof agentRunAdmissionDenial;
34
41
  assertModelPolicy?: typeof assertWorkspaceModelPolicyAllows;
35
42
  recordUsage?: typeof recordWorkspaceUsage;
@@ -43,6 +50,7 @@ export function createAutomationActivities(
43
50
  const settle = options.settle ?? settleAutomationRun;
44
51
  const assertAuthority = options.assertAuthority ?? assertAutomationRunAuthorityInTransaction;
45
52
  const createSession = options.createSession ?? createAndStartSessionWithOutcome;
53
+ const readWorkspace = options.readWorkspace ?? requireWorkspace;
46
54
  const admit = options.admit ?? agentRunAdmissionDenial;
47
55
  const assertModelPolicy = options.assertModelPolicy ?? assertWorkspaceModelPolicyAllows;
48
56
  const recordUsage = options.recordUsage ?? recordWorkspaceUsage;
@@ -100,6 +108,20 @@ export function createAutomationActivities(
100
108
  }
101
109
 
102
110
  const template = accepted.sessionTemplate;
111
+ if (
112
+ template.credentialRestriction === "developer_setup" &&
113
+ template.firstPartyMcpPermissions.some(
114
+ (permission) => !isDeveloperSetupDelegatedPermissionAllowed(permission),
115
+ )
116
+ ) {
117
+ await settle(service.db, {
118
+ workspaceId: input.workspaceId,
119
+ runId: run.id,
120
+ status: "failed",
121
+ errorCode: "credential_restriction_violation",
122
+ });
123
+ return { action: "failed", reason: "credential_restriction_violation" };
124
+ }
103
125
  const requestedModel = template.model ?? deploymentCatalogSettings.openaiModel;
104
126
  const catalogSettings =
105
127
  requestedModel.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX) ||
@@ -158,7 +180,51 @@ export function createAutomationActivities(
158
180
  reason: "interactive_compute_not_allowed",
159
181
  };
160
182
  }
161
- const turnExecutionPolicy = resolveTurnExecutionPolicyV1(catalogSettings, {
183
+ // Agent configuration for the generated session (null = legacy). The
184
+ // template's explicit tools are its baseline; capabilities only narrow.
185
+ let agentWriteThrough: {
186
+ config: import("@opengeni/contracts").ResolvedAgentConfig | null;
187
+ instructions: string | undefined;
188
+ firstPartyMcpTools: typeof template.firstPartyMcpTools;
189
+ tools: typeof template.tools;
190
+ };
191
+ try {
192
+ const workspaceSettings = agentConfigMayResolve(catalogSettings, template.agent)
193
+ ? (await readWorkspace(service.db, input.workspaceId)).settings
194
+ : {};
195
+ const resolution = resolveSessionAgentConfigForCreate({
196
+ settings: catalogSettings,
197
+ creator: "automation",
198
+ request: template.agent,
199
+ instructions: template.instructions ?? undefined,
200
+ workspaceSettings,
201
+ parent: null,
202
+ goal: false,
203
+ });
204
+ const written = applySessionAgentConfigWriteThrough({
205
+ config: resolution.config,
206
+ firstPartyMcpTools: template.firstPartyMcpTools,
207
+ tools: template.tools,
208
+ toolPolicy: { mode: "explicit", inheritedFromSessionId: null },
209
+ productServerIds: template.tools.map((tool) => tool.id),
210
+ });
211
+ agentWriteThrough = {
212
+ config: resolution.config,
213
+ instructions: resolution.instructions,
214
+ firstPartyMcpTools: written.firstPartyMcpTools,
215
+ tools: written.tools,
216
+ };
217
+ } catch (error) {
218
+ if (!isUnprocessableEntity(error)) throw error;
219
+ await settle(service.db, {
220
+ workspaceId: input.workspaceId,
221
+ runId: run.id,
222
+ status: "failed",
223
+ errorCode: "dispatch_failed",
224
+ });
225
+ return { action: "failed", reason: "dispatch_failed" };
226
+ }
227
+ const resolvedTurnExecutionPolicy = resolveTurnExecutionPolicyV1(catalogSettings, {
162
228
  modelId: model,
163
229
  requestedModelId: template.model,
164
230
  modelSource: template.model ? "explicit" : "deployment",
@@ -167,6 +233,12 @@ export function createAutomationActivities(
167
233
  latencyMode: "standard",
168
234
  latencyModeSource: "deployment",
169
235
  });
236
+ const turnExecutionPolicy = template.credentialRestriction
237
+ ? {
238
+ ...resolvedTurnExecutionPolicy,
239
+ credentialRestriction: template.credentialRestriction,
240
+ }
241
+ : resolvedTurnExecutionPolicy;
170
242
  const created = await createSession({
171
243
  db: service.db,
172
244
  bus: service.bus,
@@ -190,10 +262,11 @@ export function createAutomationActivities(
190
262
  workspaceId: input.workspaceId,
191
263
  initialMessage: accepted.initialMessage,
192
264
  surface: "automation",
265
+ metrics: service.observability,
193
266
  resources: template.resources,
194
267
  skills: SessionSkills.parse(template.skills),
195
268
  bundledSkillIds: template.bundledSkillIds,
196
- tools: template.tools,
269
+ tools: agentWriteThrough.tools,
197
270
  toolPolicy: { mode: "explicit", inheritedFromSessionId: null },
198
271
  model,
199
272
  reasoningEffort,
@@ -212,10 +285,11 @@ export function createAutomationActivities(
212
285
  label: accepted.serviceLabel,
213
286
  },
214
287
  createdByContext: accepted.provenance,
215
- instructions: template.instructions,
288
+ instructions: agentWriteThrough.instructions ?? template.instructions,
216
289
  policyRole: template.policyRole,
217
290
  firstPartyMcpPermissions: template.firstPartyMcpPermissions,
218
- firstPartyMcpTools: template.firstPartyMcpTools,
291
+ firstPartyMcpTools: agentWriteThrough.firstPartyMcpTools,
292
+ ...(agentWriteThrough.config ? { agentConfig: agentWriteThrough.config } : {}),
219
293
  retainWorkspaceCustomModel:
220
294
  model.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX) ||
221
295
  model.startsWith(WORKSPACE_OPENROUTER_MODEL_ID_PREFIX),
@@ -0,0 +1,279 @@
1
+ import { Context } from "@temporalio/activity";
2
+ import {
3
+ environmentsEncryptionKeyBytes,
4
+ resolveFirstPartyDelegationSecret,
5
+ type Settings,
6
+ } from "@opengeni/config";
7
+ import { BROWSER_STATE_ARTIFACT_CONTENT_TYPE } from "@opengeni/contracts";
8
+ import {
9
+ browserDeadlineCheckpoint,
10
+ clearSuspendedBrowserSessionController,
11
+ commitBrowserSessionSuspension,
12
+ dispatchBrowserSessionOperation,
13
+ failBrowserSessionSuspension,
14
+ listBrowserDeadlineCheckpoints,
15
+ readLease,
16
+ releaseLeaseHolder,
17
+ withRlsContext,
18
+ type BrowserDeadlineCheckpointTarget,
19
+ } from "@opengeni/db";
20
+ import { deriveBrowserControllerAdminToken } from "@opengeni/interaction/browser-controller-authority";
21
+ import {
22
+ browserStateArtifactAad,
23
+ browserStateManifestDigest,
24
+ browserStateObjectKey,
25
+ deriveBrowserStateDataKey,
26
+ wrapBrowserStateDataKey,
27
+ } from "@opengeni/interaction/browser-state-authority";
28
+ import {
29
+ BrowserControlClient,
30
+ BrowserControlRequestError,
31
+ createSandboxClientForBackend,
32
+ deserializeSandboxSessionStateEnvelope,
33
+ resumeExactSandboxSession,
34
+ type BrowserControlPlacementSession,
35
+ } from "@opengeni/runtime";
36
+ import type { ControlActivityServices } from "./types";
37
+
38
+ const UPLOAD_CLEANUP_GRACE_MS = 24 * 60 * 60_000;
39
+
40
+ function heartbeat(): void {
41
+ try {
42
+ Context.current().heartbeat();
43
+ } catch {
44
+ // Direct embedded calls have no Temporal context. In-flight controller
45
+ // operations still settle under their exact durable database claim.
46
+ }
47
+ }
48
+
49
+ async function withActivityHeartbeat<T>(run: () => Promise<T>): Promise<T> {
50
+ heartbeat();
51
+ const timer = setInterval(heartbeat, 5_000);
52
+ try {
53
+ return await run();
54
+ } finally {
55
+ clearInterval(timer);
56
+ }
57
+ }
58
+
59
+ type CheckpointController = Pick<BrowserControlClient, "captureState" | "endSession">;
60
+ type CheckpointPlacement = {
61
+ target: BrowserDeadlineCheckpointTarget;
62
+ lease: NonNullable<Awaited<ReturnType<typeof readLease>>>;
63
+ settings: Settings;
64
+ controllerRoot: string;
65
+ };
66
+
67
+ async function resumeCheckpointController({
68
+ target,
69
+ lease,
70
+ settings,
71
+ controllerRoot,
72
+ }: CheckpointPlacement): Promise<CheckpointController> {
73
+ // This edge cannot create a box or route through a user's current machine.
74
+ const provider = createSandboxClientForBackend("modal", settings) as Parameters<
75
+ typeof deserializeSandboxSessionStateEnvelope
76
+ >[0];
77
+ const envelope = lease.resumeState as { sessionState?: unknown };
78
+ const state = await deserializeSandboxSessionStateEnvelope(
79
+ provider,
80
+ envelope.sessionState ?? lease.resumeState,
81
+ target.instanceId,
82
+ );
83
+ const resumed = await resumeExactSandboxSession(provider, "modal", state, target.instanceId);
84
+ return new BrowserControlClient(resumed.session as BrowserControlPlacementSession, {
85
+ adminToken: deriveBrowserControllerAdminToken({
86
+ rootSecret: controllerRoot,
87
+ accountId: target.accountId,
88
+ workspaceId: target.workspaceId,
89
+ placement: { kind: "sandbox_group", sandboxGroupId: target.sandboxGroupId },
90
+ placementInstanceId: target.instanceId,
91
+ }),
92
+ });
93
+ }
94
+
95
+ export function createBrowserDeadlineCheckpointActivities(
96
+ services: () => Promise<ControlActivityServices>,
97
+ connectController: (
98
+ placement: CheckpointPlacement,
99
+ ) => Promise<CheckpointController> = resumeCheckpointController,
100
+ ) {
101
+ return {
102
+ async listDueBrowserCheckpoints() {
103
+ return await withActivityHeartbeat(async () => {
104
+ const { db, settings, objectStorage } = await services();
105
+ if (
106
+ !settings.sandboxOwnershipEnabled ||
107
+ !objectStorage ||
108
+ !resolveFirstPartyDelegationSecret(settings)
109
+ )
110
+ return [];
111
+ const root = environmentsEncryptionKeyBytes(settings);
112
+ if (!root) return [];
113
+ root.fill(0);
114
+ return await listBrowserDeadlineCheckpoints(db);
115
+ });
116
+ },
117
+ async checkpointBrowserBeforeDeadline(target: BrowserDeadlineCheckpointTarget) {
118
+ return await withActivityHeartbeat(async () => {
119
+ const { db, settings, objectStorage } = await services();
120
+ const rootKey = environmentsEncryptionKeyBytes(settings);
121
+ const controllerRoot = resolveFirstPartyDelegationSecret(settings);
122
+ if (!rootKey || !objectStorage || !controllerRoot) {
123
+ rootKey?.fill(0);
124
+ throw new Error("browser deadline checkpoint storage or encryption is unavailable");
125
+ }
126
+ let dataKey: Buffer | null = null;
127
+ let aad: Buffer | null = null;
128
+ let timer: ReturnType<typeof setInterval> | null = null;
129
+ try {
130
+ const claim = await browserDeadlineCheckpoint(db, target, { prepare: true, touch: true });
131
+ if (!claim) return { status: "skipped" as const };
132
+ let pulseRunning = false;
133
+ let authorityLost = false;
134
+ const pulse = async () => {
135
+ if (pulseRunning) return;
136
+ pulseRunning = true;
137
+ try {
138
+ if (!(await browserDeadlineCheckpoint(db, target, { touch: true })))
139
+ authorityLost = true;
140
+ } finally {
141
+ pulseRunning = false;
142
+ }
143
+ };
144
+ timer = setInterval(() => {
145
+ void pulse().catch(() => undefined);
146
+ }, 5_000);
147
+ const lease = await readLease(db, target.workspaceId, target.sandboxGroupId);
148
+ if (
149
+ !lease ||
150
+ lease.instanceId !== target.instanceId ||
151
+ lease.leaseEpoch !== target.leaseEpoch ||
152
+ !lease.resumeState
153
+ ) {
154
+ throw new Error("browser deadline checkpoint placement changed");
155
+ }
156
+ const placement = {
157
+ kind: "sandbox_group" as const,
158
+ sandboxGroupId: target.sandboxGroupId,
159
+ };
160
+ const client = await connectController({ target, lease, settings, controllerRoot });
161
+ if (authorityLost || !(await browserDeadlineCheckpoint(db, target, { touch: true }))) {
162
+ throw new Error("browser deadline checkpoint authority changed");
163
+ }
164
+ if (claim.state !== "completed") {
165
+ const scope = { ...target, operationId: claim.operationId };
166
+ const objectKey = browserStateObjectKey(target.workspaceId, claim.operationId);
167
+ dataKey = deriveBrowserStateDataKey(rootKey, { ...scope, objectKey });
168
+ aad = browserStateArtifactAad({ ...scope, objectKey });
169
+ await dispatchBrowserSessionOperation(db, {
170
+ ...scope,
171
+ deadlineTarget: target,
172
+ stateUpload: {
173
+ objectKey,
174
+ cleanupAfter: new Date(Date.now() + UPLOAD_CLEANUP_GRACE_MS),
175
+ },
176
+ });
177
+ const signed = await objectStorage.createPutUrl({
178
+ key: objectKey,
179
+ contentType: BROWSER_STATE_ARTIFACT_CONTENT_TYPE,
180
+ audience: "sandbox",
181
+ });
182
+ const receipt = await client
183
+ .captureState({
184
+ ...scope,
185
+ objectKey,
186
+ afterCapture: "stop",
187
+ dataKey,
188
+ aad,
189
+ upload: {
190
+ url: signed.url,
191
+ requiredHeaders: signed.requiredHeaders,
192
+ expiresAt: signed.expiresAt.toISOString(),
193
+ },
194
+ })
195
+ .catch(async (error: unknown) => {
196
+ if (
197
+ error instanceof BrowserControlRequestError &&
198
+ (error.error.code === "outcome_unknown" || !error.error.retryable)
199
+ ) {
200
+ await failBrowserSessionSuspension(db, { ...scope, error: error.error });
201
+ }
202
+ throw error;
203
+ });
204
+ if (authorityLost) throw new Error("browser deadline checkpoint authority changed");
205
+ const portable = receipt.manifest.profileCrypto !== "platform_bound";
206
+ await commitBrowserSessionSuspension(db, {
207
+ ...scope,
208
+ deadlineTarget: target,
209
+ artifact: {
210
+ kind: "chromium_profile",
211
+ format: receipt.format,
212
+ objectKey,
213
+ artifactDigest: receipt.artifactDigest,
214
+ contentDigest: receipt.contentDigest,
215
+ manifestDigest: browserStateManifestDigest(receipt.manifest),
216
+ sizeBytes: receipt.sizeBytes,
217
+ encryptedDataKey: wrapBrowserStateDataKey(rootKey, dataKey, {
218
+ ...scope,
219
+ objectKey,
220
+ artifactDigest: receipt.artifactDigest,
221
+ contentDigest: receipt.contentDigest,
222
+ }),
223
+ materialization: {
224
+ portability: portable ? "portable" : "placement_bound",
225
+ reason: portable
226
+ ? null
227
+ : "Profile encryption depends on the source operating-system credential store.",
228
+ platform: receipt.manifest.platform,
229
+ architecture: receipt.manifest.architecture,
230
+ engine: receipt.manifest.engine,
231
+ engineVersion: receipt.manifest.engineVersion,
232
+ driverId: receipt.manifest.driverId,
233
+ driverSchemaVersion: receipt.manifest.driverSchemaVersion,
234
+ profileCrypto: receipt.manifest.profileCrypto,
235
+ providerId: null,
236
+ placement: portable ? null : placement,
237
+ },
238
+ },
239
+ });
240
+ }
241
+ // Only remove the local profile after the encrypted artifact and its
242
+ // authority committed. A crash resumes this cleanup, never capture.
243
+ await client.endSession(target, { removeState: true }).catch((error: unknown) => {
244
+ if (!(error instanceof BrowserControlRequestError && error.status === 404)) throw error;
245
+ });
246
+ await withRlsContext(
247
+ db,
248
+ target,
249
+ async (tx) => {
250
+ if ((await browserDeadlineCheckpoint(tx, target))?.state !== "completed") {
251
+ throw new Error("browser deadline checkpoint cleanup authority changed");
252
+ }
253
+ await clearSuspendedBrowserSessionController(tx, {
254
+ ...target,
255
+ expectedControllerGeneration: target.controllerGeneration,
256
+ });
257
+ // One transaction prevents a human resume from acquiring a new
258
+ // same-browser holder between clearing and releasing the old one.
259
+ await releaseLeaseHolder(tx, {
260
+ ...target,
261
+ kind: "interaction",
262
+ holderId: `browser-session:${target.browserSessionId}`,
263
+ idleGraceMs: settings.sandboxIdleGraceMs,
264
+ });
265
+ },
266
+ undefined,
267
+ "none",
268
+ );
269
+ return { status: "suspended" as const };
270
+ } finally {
271
+ if (timer) clearInterval(timer);
272
+ dataKey?.fill(0);
273
+ aad?.fill(0);
274
+ rootKey.fill(0);
275
+ }
276
+ });
277
+ },
278
+ };
279
+ }
@@ -40,6 +40,8 @@ import {
40
40
  listWorkspaceProviderCustomModels,
41
41
  getWorkspaceProviderCustomModelForExecution,
42
42
  loadWorkspaceProviderApiKey,
43
+ loadClaudeSubscriptionUsageCredential,
44
+ assertModelConnectionAllowsTurn,
43
45
  type Database,
44
46
  type SessionMcpServerForRun,
45
47
  } from "@opengeni/db";
@@ -303,12 +305,30 @@ export async function settingsWithOrganizationProviderCredentials(
303
305
  if (kind === "claude_subscription" && !settings.claudeSubscriptionEnabled) continue;
304
306
  const models = await buildModels(kind, claudeProviderId(kind) + "/");
305
307
  result = withClaudeConnectionCatalog(result, { [kind]: { models } });
306
- const credential = await loadOrganizationModelProviderApiKey(db, settings, {
307
- accountId,
308
- workspaceId,
309
- providerKind: kind,
310
- });
311
- if (credential) result = withClaudeConnectionCredential(result, kind, credential);
308
+ const binding =
309
+ kind === "claude_subscription"
310
+ ? await loadClaudeSubscriptionUsageCredential(db, settings, {
311
+ accountId,
312
+ workspaceId,
313
+ scope: "organization",
314
+ })
315
+ : null;
316
+ const credential =
317
+ kind === "claude_subscription"
318
+ ? binding?.serializedCredential
319
+ : await loadOrganizationModelProviderApiKey(db, settings, {
320
+ accountId,
321
+ workspaceId,
322
+ providerKind: kind,
323
+ });
324
+ if (credential)
325
+ result = withClaudeConnectionCredential(
326
+ result,
327
+ kind,
328
+ credential,
329
+ "organization",
330
+ binding ?? undefined,
331
+ );
312
332
  const workspaceModels = await listWorkspaceProviderCustomModels(db, {
313
333
  accountId,
314
334
  workspaceId,
@@ -333,15 +353,33 @@ export async function settingsWithOrganizationProviderCredentials(
333
353
  { [kind]: { models: workspaceModels } },
334
354
  "workspace",
335
355
  );
336
- const workspaceCredential = await loadWorkspaceProviderApiKey(
337
- db,
338
- settings,
339
- workspaceId,
340
- kind,
341
- workspaceModelId,
342
- );
356
+ const workspaceBinding =
357
+ kind === "claude_subscription"
358
+ ? await loadClaudeSubscriptionUsageCredential(db, settings, {
359
+ accountId,
360
+ workspaceId,
361
+ scope: "workspace",
362
+ })
363
+ : null;
364
+ if (workspaceBinding && workspaceModelId)
365
+ await assertModelConnectionAllowsTurn(db, {
366
+ workspaceId,
367
+ subjectId: "worker:model-access",
368
+ modelId: workspaceModelId,
369
+ workspaceProviderConnectionId: workspaceBinding.connectionId,
370
+ });
371
+ const workspaceCredential =
372
+ kind === "claude_subscription"
373
+ ? workspaceBinding?.serializedCredential
374
+ : await loadWorkspaceProviderApiKey(db, settings, workspaceId, kind, workspaceModelId);
343
375
  if (workspaceCredential)
344
- result = withClaudeConnectionCredential(result, kind, workspaceCredential, "workspace");
376
+ result = withClaudeConnectionCredential(
377
+ result,
378
+ kind,
379
+ workspaceCredential,
380
+ "workspace",
381
+ workspaceBinding ?? undefined,
382
+ );
345
383
  }
346
384
  return result;
347
385
  }