@opengeni/api-router 3.2.2 → 3.4.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 (45) hide show
  1. package/dist/access-grant-rls.d.ts +15 -0
  2. package/dist/app.js +1 -1
  3. package/dist/billing-stripe-customer.d.ts +18 -0
  4. package/dist/billing-subscription-store.d.ts +34 -0
  5. package/dist/billing-subscriptions.d.ts +64 -0
  6. package/dist/{chunk-PQBHZ2YH.js → chunk-6DZ4H5JQ.js} +3133 -1711
  7. package/dist/{chunk-PQBHZ2YH.js.map → chunk-6DZ4H5JQ.js.map} +1 -1
  8. package/dist/index.js +4 -2
  9. package/dist/index.js.map +1 -1
  10. package/dist/integrations/marketplace-plugin.d.ts +43 -43
  11. package/dist/mcp/server.d.ts +11 -1
  12. package/dist/routes/browser-sessions.d.ts +23 -2
  13. package/dist/routes/sessions.d.ts +1 -0
  14. package/dist/sandbox/channel-a.d.ts +10 -1
  15. package/dist/site-publishing/agent-data-tools.d.ts +5 -5
  16. package/dist/site-publishing/agent-session.d.ts +3 -2
  17. package/dist/site-publishing/runtime.d.ts +7 -6
  18. package/dist/site-publishing/service.d.ts +8 -11
  19. package/dist/site-publishing/session-request-authority.d.ts +3 -0
  20. package/package.json +24 -24
  21. package/src/access-grant-rls.ts +99 -10
  22. package/src/app.ts +105 -43
  23. package/src/billing-stripe-customer.ts +40 -0
  24. package/src/billing-subscription-store.ts +121 -0
  25. package/src/billing-subscriptions.ts +759 -0
  26. package/src/http/sse.ts +5 -1
  27. package/src/index.ts +3 -0
  28. package/src/mcp/action-catalog.gen.ts +113 -0
  29. package/src/mcp/action-schema-imports.gen.ts +5 -1
  30. package/src/mcp/server.ts +28 -0
  31. package/src/routes/billing.ts +161 -21
  32. package/src/routes/browser-sessions.ts +420 -26
  33. package/src/routes/computer-sessions.ts +102 -39
  34. package/src/routes/insights-usage.ts +22 -0
  35. package/src/routes/organization-memberships.ts +12 -0
  36. package/src/routes/sessions.ts +30 -5
  37. package/src/routes/site-runtime.ts +62 -20
  38. package/src/sandbox/channel-a.ts +18 -1
  39. package/src/sandbox/metrics-ingestion.ts +1 -0
  40. package/src/site-frame-guard.ts +11 -2
  41. package/src/site-publishing/agent-data-tools.ts +16 -8
  42. package/src/site-publishing/agent-session.ts +24 -6
  43. package/src/site-publishing/runtime.ts +29 -7
  44. package/src/site-publishing/service.ts +65 -77
  45. package/src/site-publishing/session-request-authority.ts +14 -0
@@ -40,8 +40,12 @@ import {
40
40
  BrowserSessionLifecycleRequest,
41
41
  BrowserSessionListResponse,
42
42
  BrowserSessionMutationResponse,
43
+ type BrowserTarget,
43
44
  BrowserTargetListResponse,
44
45
  BrowserTargetState,
46
+ BrowserWorkingCopyAdoptionRequest,
47
+ BrowserWorkingCopyAdoptionResponse,
48
+ BrowserWorkingCopyInspection,
45
49
  CreateBrowserSessionRequest,
46
50
  ExternalAuthInteractiveRequest,
47
51
  ExternalAuthInteractiveResponse,
@@ -57,6 +61,8 @@ import {
57
61
  type BrowserActionRequest as BrowserActionRequestValue,
58
62
  type BrowserDownload as BrowserDownloadValue,
59
63
  type BrowserSession as BrowserSessionValue,
64
+ type BrowserWorkingCopyAdoption as BrowserWorkingCopyAdoptionValue,
65
+ type BrowserWorkingCopyRefusal as BrowserWorkingCopyRefusalValue,
60
66
  type FileAsset,
61
67
  type CreateBrowserSessionRequest as CreateBrowserSessionRequestValue,
62
68
  type InteractionPlacement,
@@ -143,6 +149,7 @@ import {
143
149
  verifyAuthRun,
144
150
  settleBrowserDownloadSaveFailure,
145
151
  terminalizeStaleConnectedInteractionPlacement,
152
+ recordAuditEvent,
146
153
  type BrowserSessionControlRecord,
147
154
  type BrowserPrivateCheckpointAuthority,
148
155
  type BrowserRevisionArtifactAuthority,
@@ -151,6 +158,7 @@ import {
151
158
  import {
152
159
  requireAccessGrant,
153
160
  requireSessionAuthorization,
161
+ withSessionAuthorizationReadReuse,
154
162
  recordWorkspaceUsage,
155
163
  requireLimit,
156
164
  relayConfigFromSettings,
@@ -181,6 +189,9 @@ import {
181
189
  type BrowserControlScreenshotOptions,
182
190
  type BrowserControlFrame,
183
191
  type PlacementBrowserStateCaptureReceipt,
192
+ type PlacementBrowserWorkingCopyAdoption,
193
+ type PlacementBrowserWorkingCopyAuthority,
194
+ type PlacementBrowserWorkingCopyInspection,
184
195
  type PlacementBrowserNetworkRoute,
185
196
  type PlacementBrowserTransport,
186
197
  type RestorePlacementBrowserStateInput,
@@ -351,22 +362,36 @@ export function registerBrowserSessionRoutes(
351
362
  const request = await parseJsonBody(context, CreateBrowserSessionRequest);
352
363
  const startedAtMs = performance.now();
353
364
  const stages = new InteractionRouteStages("create");
354
- await authorizeSourceSession(deps, grant, request.sessionId, "session.control");
365
+ // The replay lookup and the source session are read alongside the
366
+ // authorization check. A denial still wins, and neither read is used
367
+ // until the request is authorized; each failure surfaces where it did.
368
+ const [authorized, existingRead, sourceRead] = await Promise.allSettled([
369
+ authorizeSourceSession(deps, grant, request.sessionId, "session.control"),
370
+ findBrowserSessionControlRecordByOperation(deps.db, {
371
+ accountId: grant.accountId,
372
+ workspaceId,
373
+ operationId: request.operationId,
374
+ }),
375
+ requireSourceSession(deps, workspaceId, request.sessionId),
376
+ ]);
377
+ if (authorized.status === "rejected") throw authorized.reason;
355
378
  stages.mark("authorize");
356
379
  const origin = requestOrigin(context, deps.settings);
357
380
  const authority = browserAuthorityRoot(deps);
358
381
  let prepared: Awaited<ReturnType<typeof prepareBrowserSessionCreate>> | null = null;
382
+ // What the controller showed when it launched this browser, returned so
383
+ // the caller can present the opened tab without two more reads.
384
+ const launch: { result: Awaited<ReturnType<BrowserControlClient["createSession"]>> | null } = {
385
+ result: null,
386
+ };
359
387
 
360
388
  try {
361
389
  assertEphemeralBrowserCreateEnabled(
362
390
  request,
363
391
  deps.settings.experimentalBrowserContextPoolEnabled,
364
392
  );
365
- const existing = await findBrowserSessionControlRecordByOperation(deps.db, {
366
- accountId: grant.accountId,
367
- workspaceId,
368
- operationId: request.operationId,
369
- });
393
+ if (existingRead.status === "rejected") throw existingRead.reason;
394
+ const existing = existingRead.value;
370
395
  if (existing && existing.sourceSessionId !== request.sessionId) {
371
396
  throw new BrowserSessionOperationConflictError(
372
397
  "BrowserSession create operation belongs to another source session",
@@ -400,7 +425,8 @@ export function registerBrowserSessionRoutes(
400
425
  }
401
426
  }
402
427
 
403
- const sourceSession = await requireSourceSession(deps, workspaceId, request.sessionId);
428
+ if (sourceRead.status === "rejected") throw sourceRead.reason;
429
+ const sourceSession = sourceRead.value;
404
430
  stages.mark("source_session");
405
431
  const response = await withBrowserPlacement(
406
432
  sourceSession,
@@ -535,7 +561,7 @@ export function registerBrowserSessionRoutes(
535
561
  linkedComputer !== null,
536
562
  );
537
563
  stages.mark("display");
538
- return await client.createSession({
564
+ launch.result = await client.createSession({
539
565
  browserSessionId: preparedSession.id,
540
566
  controllerGeneration,
541
567
  tokenGeneration: record.tokenGeneration,
@@ -552,6 +578,7 @@ export function registerBrowserSessionRoutes(
552
578
  ...(request.initialUrl ? { initialUrl: request.initialUrl } : {}),
553
579
  ...(restore ? { restore } : {}),
554
580
  });
581
+ return launch.result;
555
582
  },
556
583
  );
557
584
  },
@@ -613,7 +640,7 @@ export function registerBrowserSessionRoutes(
613
640
  }
614
641
  },
615
642
  );
616
- const parsed = BrowserSessionMutationResponse.parse(response);
643
+ const parsed = BrowserSessionMutationResponse.parse(withInitialView(response, launch.result));
617
644
  observeLifecycleResult(deps.observability, startedAtMs, parsed);
618
645
  reportInteractionRouteStages(context, deps.observability, stages);
619
646
  return context.json(
@@ -2611,6 +2638,217 @@ export function registerBrowserSessionRoutes(
2611
2638
  },
2612
2639
  );
2613
2640
 
2641
+ /** Run one working-copy request against an active browser's own controller
2642
+ * generation, with the same session.control and placement authority as any
2643
+ * browser control. The controller binds every request to the exact
2644
+ * controller authority the copy was launched under. Never replayed. */
2645
+ async function withPreservedWorkingCopy<T>(
2646
+ context: Context,
2647
+ callback: (input: {
2648
+ grant: AccessGrant;
2649
+ workspaceId: string;
2650
+ browserSessionId: string;
2651
+ client: BrowserControlClient;
2652
+ authority: PlacementBrowserWorkingCopyAuthority;
2653
+ }) => Promise<T>,
2654
+ ): Promise<T> {
2655
+ const { workspaceId, grant, browserSessionId } = await browserRoutePreamble(
2656
+ context,
2657
+ "sessions:control",
2658
+ );
2659
+ return await withActiveBrowserController(
2660
+ context,
2661
+ grant,
2662
+ workspaceId,
2663
+ browserSessionId,
2664
+ "session.control",
2665
+ "browser.control",
2666
+ async ({ client, record, binding, placement }) => {
2667
+ const transport = browserRuntimeTransport(record.session, placement.transport);
2668
+ if (
2669
+ transport.kind !== "managed" ||
2670
+ transport.engine !== "chromium" ||
2671
+ transport.ephemeralPartition ||
2672
+ browserSessionStorageMode(record.session) !== "private_profile"
2673
+ ) {
2674
+ throw new BrowserSessionStateError(WORKING_COPY_REFUSAL_EXPLANATIONS.unsupported_browser);
2675
+ }
2676
+ const tokens = deriveBrowserSessionControllerTokens({
2677
+ rootSecret: browserAuthorityRoot(deps),
2678
+ accountId: grant.accountId,
2679
+ workspaceId,
2680
+ placement: record.session.placement,
2681
+ placementInstanceId: placement.placementInstanceId,
2682
+ browserSessionId,
2683
+ controllerGeneration: binding.controllerGeneration,
2684
+ tokenGeneration: record.tokenGeneration,
2685
+ });
2686
+ return await callback({
2687
+ grant,
2688
+ workspaceId,
2689
+ browserSessionId,
2690
+ client,
2691
+ authority: {
2692
+ browserSessionId,
2693
+ controllerGeneration: binding.controllerGeneration,
2694
+ tokenGeneration: record.tokenGeneration,
2695
+ ...tokens,
2696
+ },
2697
+ });
2698
+ },
2699
+ // A missing controller session is the state being inspected; never
2700
+ // launch anything to "recover" it here.
2701
+ false,
2702
+ );
2703
+ }
2704
+
2705
+ app.post(
2706
+ "/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/working-copy/inspection",
2707
+ async (context) => {
2708
+ await parseEmptyJsonBody(context);
2709
+ const result = await withPreservedWorkingCopy(
2710
+ context,
2711
+ async ({ grant, workspaceId, browserSessionId, client, authority }) => {
2712
+ const inspection = await client.inspectWorkingCopy(authority);
2713
+ const checkpoint = await getBrowserPrivateCheckpointAuthority(deps.db, {
2714
+ accountId: grant.accountId,
2715
+ workspaceId,
2716
+ browserSessionId,
2717
+ }).catch(() => null);
2718
+ return BrowserWorkingCopyInspection.parse({
2719
+ browserSessionId,
2720
+ controllerGeneration: authority.controllerGeneration,
2721
+ adoptable: inspection.refusal === null && inspection.manifest !== null,
2722
+ refusal: inspection.refusal,
2723
+ explanation: workingCopyInspectionExplanation(
2724
+ inspection,
2725
+ checkpoint?.createdAt ?? null,
2726
+ ),
2727
+ manifest: inspection.manifest,
2728
+ platformSupported: inspection.platformSupported,
2729
+ profileIdentityMatches: inspection.profileIdentityMatches,
2730
+ profileInUse: inspection.profileInUse,
2731
+ receipt: inspection.receipt,
2732
+ freshness: { status: "unknown", latestCheckpointAt: checkpoint?.createdAt ?? null },
2733
+ activeAdoption: inspection.activeAdoption,
2734
+ });
2735
+ },
2736
+ );
2737
+ return context.json(result);
2738
+ },
2739
+ );
2740
+
2741
+ app.post(
2742
+ "/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/working-copy/adoptions",
2743
+ async (context) => {
2744
+ const request = await parseJsonBody(context, BrowserWorkingCopyAdoptionRequest);
2745
+ const result = await withPreservedWorkingCopy(
2746
+ context,
2747
+ async ({ grant, workspaceId, browserSessionId, client, authority }) => {
2748
+ // Durable attribution before any effect: who selected which exact
2749
+ // bytes as this browser's state. Freshness stays unproven.
2750
+ await recordAuditEvent(deps.db, {
2751
+ accountId: grant.accountId,
2752
+ workspaceId,
2753
+ subjectId: grant.subjectId,
2754
+ action: "browserSession.working_copy.adoption_requested",
2755
+ targetType: "browser_session",
2756
+ targetId: browserSessionId,
2757
+ metadata: {
2758
+ operationId: request.operationId,
2759
+ manifestDigest: request.manifestDigest,
2760
+ controllerGeneration: authority.controllerGeneration,
2761
+ },
2762
+ });
2763
+ const adoption = await client.adoptWorkingCopy({
2764
+ ...authority,
2765
+ operationId: request.operationId,
2766
+ manifestDigest: request.manifestDigest,
2767
+ });
2768
+ await recordAuditEvent(deps.db, {
2769
+ accountId: grant.accountId,
2770
+ workspaceId,
2771
+ subjectId: grant.subjectId,
2772
+ action: "browserSession.working_copy.adoption_settled",
2773
+ targetType: "browser_session",
2774
+ targetId: browserSessionId,
2775
+ metadata: {
2776
+ operationId: adoption.operationId,
2777
+ manifestDigest: adoption.manifestDigest,
2778
+ controllerGeneration: adoption.controllerGeneration,
2779
+ phase: adoption.phase,
2780
+ failureCode: adoption.failureCode,
2781
+ },
2782
+ }).catch(() => undefined);
2783
+ return BrowserWorkingCopyAdoptionResponse.parse({
2784
+ inFlight: false,
2785
+ adoption: publicWorkingCopyAdoption(adoption),
2786
+ explanation: workingCopyAdoptionExplanation(false, adoption),
2787
+ });
2788
+ },
2789
+ );
2790
+ return context.json(result);
2791
+ },
2792
+ );
2793
+
2794
+ app.get(
2795
+ "/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/working-copy/adoptions/:operationId",
2796
+ async (context) => {
2797
+ const operationId = requireUuidParam(context, "operationId");
2798
+ const result = await withPreservedWorkingCopy(context, async ({ client, authority }) => {
2799
+ const status = await client.workingCopyAdoption(authority, operationId);
2800
+ return BrowserWorkingCopyAdoptionResponse.parse({
2801
+ inFlight: status.inFlight,
2802
+ adoption: status.adoption ? publicWorkingCopyAdoption(status.adoption) : null,
2803
+ explanation: workingCopyAdoptionExplanation(status.inFlight, status.adoption),
2804
+ });
2805
+ });
2806
+ return context.json(result);
2807
+ },
2808
+ );
2809
+
2810
+ app.post(
2811
+ "/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/working-copy/adoptions/:operationId/revert",
2812
+ async (context) => {
2813
+ const operationId = requireUuidParam(context, "operationId");
2814
+ const result = await withPreservedWorkingCopy(
2815
+ context,
2816
+ async ({ grant, workspaceId, browserSessionId, client, authority }) => {
2817
+ await recordAuditEvent(deps.db, {
2818
+ accountId: grant.accountId,
2819
+ workspaceId,
2820
+ subjectId: grant.subjectId,
2821
+ action: "browserSession.working_copy.revert_requested",
2822
+ targetType: "browser_session",
2823
+ targetId: browserSessionId,
2824
+ metadata: { operationId, controllerGeneration: authority.controllerGeneration },
2825
+ });
2826
+ const status = await client.revertWorkingCopyAdoption({ ...authority, operationId });
2827
+ await recordAuditEvent(deps.db, {
2828
+ accountId: grant.accountId,
2829
+ workspaceId,
2830
+ subjectId: grant.subjectId,
2831
+ action: "browserSession.working_copy.revert_settled",
2832
+ targetType: "browser_session",
2833
+ targetId: browserSessionId,
2834
+ metadata: {
2835
+ operationId,
2836
+ controllerGeneration: authority.controllerGeneration,
2837
+ phase: status.adoption?.phase ?? null,
2838
+ failureCode: status.adoption?.failureCode ?? null,
2839
+ },
2840
+ }).catch(() => undefined);
2841
+ return BrowserWorkingCopyAdoptionResponse.parse({
2842
+ inFlight: status.inFlight,
2843
+ adoption: status.adoption ? publicWorkingCopyAdoption(status.adoption) : null,
2844
+ explanation: workingCopyAdoptionExplanation(status.inFlight, status.adoption),
2845
+ });
2846
+ },
2847
+ );
2848
+ return context.json(result);
2849
+ },
2850
+ );
2851
+
2614
2852
  app.post(
2615
2853
  "/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/end",
2616
2854
  async (context) => {
@@ -2834,6 +3072,7 @@ export function registerBrowserSessionRoutes(
2834
3072
  enrollment.agentCapabilities.operationResourcePolicy === true,
2835
3073
  operationCpuQuotaSupported: enrollment.agentCapabilities.operationCpuQuota === true,
2836
3074
  transactionalFsWriteSupported: enrollment.agentCapabilities.transactionalFsWrite === true,
3075
+ browserControlHttpSupported: enrollment.agentCapabilities.browserControlHttp === true,
2837
3076
  ...(deps.settings.agentOpStreamEnabled === true &&
2838
3077
  enrollment.opStream === true &&
2839
3078
  deps.bus.getOpStreamConnection
@@ -3157,16 +3396,40 @@ export function registerBrowserSessionRoutes(
3157
3396
  browserSessionId: string,
3158
3397
  authorizationOperation: SessionAuthorizationOperation,
3159
3398
  channelOperation: ChannelAOperation,
3160
- callback: (input: {
3161
- client: BrowserControlClient;
3162
- sessionClient: ReturnType<BrowserControlClient["sessionClient"]>;
3163
- record: BrowserSessionControlRecord;
3164
- binding: NonNullable<BrowserSessionControlRecord["session"]["controller"]>;
3165
- placement: BrowserPlacement;
3166
- sourceAuthorization: ResolvedSessionAuthorization | null;
3167
- }) => Promise<T>,
3399
+ callback: ActiveBrowserControllerCallback<T>,
3168
3400
  recoverMissing = true,
3169
3401
  stages?: InteractionRouteStages,
3402
+ ): Promise<T> {
3403
+ // The source-session check and the machine-route check that follows it
3404
+ // prove the same live agent attempt; they share those reads. Everything
3405
+ // from the controller call onward reads fresh.
3406
+ return await withSessionAuthorizationReadReuse((authorizationReads) =>
3407
+ withActiveBrowserControllerReads(
3408
+ context,
3409
+ grant,
3410
+ workspaceId,
3411
+ browserSessionId,
3412
+ authorizationOperation,
3413
+ channelOperation,
3414
+ callback,
3415
+ recoverMissing,
3416
+ stages,
3417
+ authorizationReads,
3418
+ ),
3419
+ );
3420
+ }
3421
+
3422
+ async function withActiveBrowserControllerReads<T>(
3423
+ context: Context,
3424
+ grant: AccessGrant,
3425
+ workspaceId: string,
3426
+ browserSessionId: string,
3427
+ authorizationOperation: SessionAuthorizationOperation,
3428
+ channelOperation: ChannelAOperation,
3429
+ callback: ActiveBrowserControllerCallback<T>,
3430
+ recoverMissing: boolean,
3431
+ stages: InteractionRouteStages | undefined,
3432
+ authorizationReads: { handOffToToolDispatch(): void },
3170
3433
  ): Promise<T> {
3171
3434
  try {
3172
3435
  const record = await getBrowserSessionControlRecord(deps.db, {
@@ -3175,17 +3438,25 @@ export function registerBrowserSessionRoutes(
3175
3438
  browserSessionId,
3176
3439
  });
3177
3440
  stages?.mark("record");
3178
- const sourceAuthorization = await authorizeSourceSession(
3179
- deps,
3180
- grant,
3181
- record.sourceSessionId,
3182
- authorizationOperation,
3183
- );
3441
+ // A Connected Machine (or any placement without a cached controller URL)
3442
+ // always needs the source session; read it alongside authorization. A
3443
+ // denial still wins, and the session is only used once authorized.
3444
+ const sourceSessionNeeded =
3445
+ record.session.placement?.kind === "connected_machine" ||
3446
+ !record.controllerHostSandboxGroupId;
3447
+ const [authorized, sourceRead] = await Promise.allSettled([
3448
+ authorizeSourceSession(deps, grant, record.sourceSessionId, authorizationOperation),
3449
+ sourceSessionNeeded
3450
+ ? requireSourceSession(deps, workspaceId, record.sourceSessionId)
3451
+ : Promise.resolve(null),
3452
+ ]);
3453
+ if (authorized.status === "rejected") throw authorized.reason;
3454
+ if (sourceRead.status === "rejected") throw sourceRead.reason;
3455
+ const sourceAuthorization = authorized.value;
3184
3456
  stages?.mark("authorize");
3185
- let sourceSession: Session | null = null;
3457
+ let sourceSession: Session | null = sourceRead.value;
3186
3458
  if (record.session.placement?.kind === "connected_machine") {
3187
- sourceSession = await requireSourceSession(deps, workspaceId, record.sourceSessionId);
3188
- if (sourceSession.activeSandboxId !== record.session.placement.sandboxId) {
3459
+ if (sourceSession?.activeSandboxId !== record.session.placement.sandboxId) {
3189
3460
  return await throwBrowserSourcePlacementChanged(
3190
3461
  grant,
3191
3462
  record.sourceSessionId,
@@ -3217,6 +3488,7 @@ export function registerBrowserSessionRoutes(
3217
3488
  placement: BrowserPlacement,
3218
3489
  provisionedClient?: BrowserControlClient,
3219
3490
  ): Promise<T> => {
3491
+ authorizationReads.handOffToToolDispatch();
3220
3492
  // An active binding already proves browserd was provisioned. Reusing
3221
3493
  // it avoids restarting/checking the sidecar on every live input.
3222
3494
  const client = provisionedClient ?? connectController(deps, grant, record, placement);
@@ -4422,6 +4694,34 @@ function requireOperationGeneration(record: BrowserSessionControlRecord): string
4422
4694
  return generation;
4423
4695
  }
4424
4696
 
4697
+ /** Attach the controller's launch view to a create that activated exactly that
4698
+ * launch in this request. Replays, failures and other bindings get nothing, so
4699
+ * a caller then reads the targets and observes as before. */
4700
+ export function withInitialView<
4701
+ T extends {
4702
+ session: { id: string; lifecycle: string; controller: { controllerGeneration: string } | null };
4703
+ operation: { state: string; replayed: boolean };
4704
+ },
4705
+ >(
4706
+ response: T,
4707
+ launched: Awaited<ReturnType<BrowserControlClient["createSession"]>> | null,
4708
+ ): T | (T & { initialView: { targets: BrowserTarget[]; observation: BrowserObservation } }) {
4709
+ if (
4710
+ !launched?.targets ||
4711
+ response.operation.state !== "completed" ||
4712
+ response.operation.replayed ||
4713
+ response.session.lifecycle !== "active" ||
4714
+ response.session.id !== launched.browserSessionId ||
4715
+ response.session.controller?.controllerGeneration !== launched.controllerGeneration
4716
+ ) {
4717
+ return response;
4718
+ }
4719
+ return {
4720
+ ...response,
4721
+ initialView: { targets: launched.targets, observation: launched.observation },
4722
+ };
4723
+ }
4724
+
4425
4725
  async function provisionController(
4426
4726
  deps: ApiRouteDeps,
4427
4727
  grant: AccessGrant,
@@ -4631,6 +4931,15 @@ async function settleBrowserRevisionCaptureFailure(
4631
4931
  });
4632
4932
  }
4633
4933
 
4934
+ type ActiveBrowserControllerCallback<T> = (input: {
4935
+ client: BrowserControlClient;
4936
+ sessionClient: ReturnType<BrowserControlClient["sessionClient"]>;
4937
+ record: BrowserSessionControlRecord;
4938
+ binding: NonNullable<BrowserSessionControlRecord["session"]["controller"]>;
4939
+ placement: BrowserPlacement;
4940
+ sourceAuthorization: ResolvedSessionAuthorization | null;
4941
+ }) => Promise<T>;
4942
+
4634
4943
  async function requireSourceSession(
4635
4944
  deps: ApiRouteDeps,
4636
4945
  workspaceId: string,
@@ -5332,6 +5641,91 @@ function recordAttachedBrowserUnavailable(
5332
5641
  }
5333
5642
  }
5334
5643
 
5644
+ const WORKING_COPY_REFUSAL_EXPLANATIONS: Record<BrowserWorkingCopyRefusalValue, string> = {
5645
+ unsupported_platform:
5646
+ "Adopting a working copy is supported only for browsers on Linux machines, where the controller can prove no process uses the copy. Nothing was changed.",
5647
+ unsupported_browser:
5648
+ "Only a managed Chromium browser with a private profile keeps a working copy that can be adopted. Nothing was changed.",
5649
+ browser_running:
5650
+ "The browser is running, starting or being ended on its controller, so there is no stopped working copy to adopt now. If it is running, use it normally; otherwise inspect again once it has stopped.",
5651
+ working_copy_invalid:
5652
+ "The browser's working directory on its machine is missing, incomplete or not a plain directory, so it cannot be adopted. Nothing was changed.",
5653
+ receipt_not_adoptable:
5654
+ "The working copy has no completed record of a browser launch, so it cannot be adopted. Its record and recovery state are preserved.",
5655
+ authority_changed:
5656
+ "The working copy was launched under a different controller authority than this browser now has, so it cannot be adopted for it. Nothing was changed.",
5657
+ process_state_unknown:
5658
+ "It cannot be proven that no process uses the copy's browser profile. Close any browser using it, then inspect again. Nothing was changed.",
5659
+ adoption_not_needed:
5660
+ "The working copy is the same directory the browser was launched in, so it needs no adoption; the browser continues from it when it is next used.",
5661
+ adoption_unresolved:
5662
+ "An earlier adoption for this browser has not finished. Check it with getBrowserSessionWorkingCopyAdoption and its operation id (see activeAdoption). Inspecting and checking never change it; retrying that same adoption with the same operationId and manifestDigest settles it. Do not start another.",
5663
+ };
5664
+
5665
+ function workingCopyInspectionExplanation(
5666
+ inspection: PlacementBrowserWorkingCopyInspection,
5667
+ latestCheckpointAt: string | null,
5668
+ ): string {
5669
+ if (inspection.refusal) return WORKING_COPY_REFUSAL_EXPLANATIONS[inspection.refusal];
5670
+ const manifest = inspection.manifest!;
5671
+ return [
5672
+ `This working copy can be adopted: ${manifest.entries} entries, ${manifest.bytes} bytes, manifest digest ${manifest.digest}.`,
5673
+ manifest.excluded.length > 0
5674
+ ? `Left out because the controller rebuilds them: ${manifest.excluded.join(", ")}. Everything else is in the digest.`
5675
+ : "Every entry is in the digest.",
5676
+ "It is not the directory the browser was launched in (it was moved, copied or restored), so the browser refuses to continue from it until it is adopted.",
5677
+ latestCheckpointAt
5678
+ ? `How recent its contents are is not proven; the browser's latest saved checkpoint is from ${latestCheckpointAt}.`
5679
+ : "How recent its contents are is not proven.",
5680
+ "Adopting selects exactly these bytes as the browser's state and is never replaced by an older checkpoint afterwards; the directory it replaces is kept.",
5681
+ "To adopt, call adoptBrowserSessionWorkingCopy with this manifestDigest and a new operationId. If the bytes change first, the adoption is refused.",
5682
+ ].join(" ");
5683
+ }
5684
+
5685
+ function publicWorkingCopyAdoption(
5686
+ adoption: PlacementBrowserWorkingCopyAdoption,
5687
+ ): BrowserWorkingCopyAdoptionValue {
5688
+ return {
5689
+ operationId: adoption.operationId,
5690
+ browserSessionId: adoption.browserSessionId,
5691
+ controllerGeneration: adoption.controllerGeneration,
5692
+ manifestDigest: adoption.manifestDigest,
5693
+ phase: adoption.phase,
5694
+ failureCode: adoption.failureCode,
5695
+ retainedDirectories: adoption.retainedDirectories,
5696
+ createdAt: adoption.createdAt,
5697
+ updatedAt: adoption.updatedAt,
5698
+ };
5699
+ }
5700
+
5701
+ function workingCopyAdoptionExplanation(
5702
+ inFlight: boolean,
5703
+ adoption: PlacementBrowserWorkingCopyAdoption | null,
5704
+ ): string {
5705
+ const kept = adoption?.retainedDirectories.length
5706
+ ? ` Kept on the machine for separate cleanup: ${adoption.retainedDirectories.join(", ")}.`
5707
+ : "";
5708
+ if (inFlight)
5709
+ return "The adoption is still running on the controller. Check again with getBrowserSessionWorkingCopyAdoption; do not start another.";
5710
+ if (!adoption)
5711
+ return "The controller has no record of this adoption, which proves it changed nothing there.";
5712
+ switch (adoption.phase) {
5713
+ case "adopted":
5714
+ return `Adopted. The browser continues from the copy with manifest digest ${adoption.manifestDigest} the next time it is used, and is never replaced by an older checkpoint.${kept}`;
5715
+ case "interrupted":
5716
+ case "failed":
5717
+ return `The adoption stopped before the copy took the browser's place${adoption.failureCode ? ` (${adoption.failureCode})` : ""}; the browser's own directory is unchanged.${kept}`;
5718
+ case "collision":
5719
+ return `The adoption found the browser's directories in an unexpected state (${adoption.failureCode ?? "unknown"}) and stopped without moving or removing anything more. The browser stays unavailable until this adoption is reverted with revertBrowserSessionWorkingCopyAdoption, which puts the browser's own directory back and keeps the staged copy. If the revert refuses, the directories need a person on that machine.${kept}`;
5720
+ case "reverting":
5721
+ return "A revert of this adoption stopped part way and is not running. Retry revertBrowserSessionWorkingCopyAdoption with the same operationId to finish it; a retry repeats nothing already done.";
5722
+ case "reverted":
5723
+ return `Reverted. The browser's own directory is back in place and the staged copy was not used.${kept}`;
5724
+ default:
5725
+ return "The adoption stopped part way and is not running. Checking it never changes it. To finish it, retry the same adoption with the same operationId and manifestDigest under the authority that approved it; a retry repeats nothing already done. To undo it instead, revert it with revertBrowserSessionWorkingCopyAdoption. Do not start another.";
5726
+ }
5727
+ }
5728
+
5335
5729
  function browserRouteError(error: unknown): HTTPException {
5336
5730
  const tabDiagnosis = browserTabDiagnosisError(error);
5337
5731
  if (tabDiagnosis instanceof ApiHttpError) return tabDiagnosis;