@opengeni/db 0.31.1 → 0.36.1

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 (125) hide show
  1. package/dist/browser-auth.d.ts +209 -1
  2. package/dist/browser-downloads.d.ts +51 -0
  3. package/dist/browser-identities.d.ts +11 -2
  4. package/dist/browser-sessions.d.ts +83 -1
  5. package/dist/browser-state-artifacts.d.ts +75 -1
  6. package/dist/canonical-human-identities.d.ts +48 -0
  7. package/dist/canonical-human-identities.js +28 -0
  8. package/dist/canonical-human-identities.js.map +1 -0
  9. package/dist/capability-integrations.d.ts +8 -6
  10. package/dist/{chunk-MWDVXQOL.js → chunk-5KTIEDX7.js} +5773 -4212
  11. package/dist/chunk-5KTIEDX7.js.map +1 -0
  12. package/dist/chunk-FPVAD4CO.js +117 -0
  13. package/dist/chunk-FPVAD4CO.js.map +1 -0
  14. package/dist/chunk-GYO7ED2Q.js +259 -0
  15. package/dist/chunk-GYO7ED2Q.js.map +1 -0
  16. package/dist/{chunk-F5FT7BEZ.js → chunk-HSOWSYOX.js} +2 -2
  17. package/dist/{chunk-YHZXQ7HP.js → chunk-VKLB2ZGI.js} +446 -19
  18. package/dist/chunk-VKLB2ZGI.js.map +1 -0
  19. package/dist/{chunk-NBE6CHRI.js → chunk-VTPRIXKG.js} +49 -9
  20. package/dist/chunk-VTPRIXKG.js.map +1 -0
  21. package/dist/{chunk-ISI2TIUG.js → chunk-Z7VYMNPZ.js} +76 -4
  22. package/dist/chunk-Z7VYMNPZ.js.map +1 -0
  23. package/dist/database.d.ts +5 -0
  24. package/dist/editable-artifact-durable-export.js +2 -2
  25. package/dist/editable-artifacts.js +3 -3
  26. package/dist/index.d.ts +467 -29
  27. package/dist/index.js +23247 -16232
  28. package/dist/index.js.map +1 -1
  29. package/dist/insights.d.ts +23 -7
  30. package/dist/integration-bindings.d.ts +24 -24
  31. package/dist/integration-facets.d.ts +99 -0
  32. package/dist/interaction-revisions.d.ts +11 -0
  33. package/dist/interaction-schema.d.ts +698 -34
  34. package/dist/persistence-errors.d.ts +8 -0
  35. package/dist/provision-roles.js +1 -1
  36. package/dist/runtime-posture.d.ts +8 -7
  37. package/dist/schema.d.ts +9464 -5412
  38. package/dist/schema.js +51 -9
  39. package/dist/session-control.d.ts +29 -3
  40. package/dist/session-tenancy.d.ts +44 -0
  41. package/dist/session-tenancy.js +16 -0
  42. package/dist/session-tenancy.js.map +1 -0
  43. package/dist/slack-task-policy-schema.d.ts +847 -0
  44. package/dist/slack-task-policy.d.ts +65 -0
  45. package/dist/task-notes-schema.d.ts +924 -0
  46. package/dist/task-notes.d.ts +85 -0
  47. package/dist/video-generation.d.ts +9 -0
  48. package/dist/video-generation.js +5 -3
  49. package/dist/xai-subscription.d.ts +326 -0
  50. package/drizzle/0212_browser_state_transfer_hardening.sql +560 -0
  51. package/drizzle/0213_browser_interaction_authority.sql +67 -0
  52. package/drizzle/0214_browser_download_saves.sql +34 -0
  53. package/drizzle/0215_browser_controller_host.sql +385 -0
  54. package/drizzle/0216_browser_auth_health_evidence.sql +137 -0
  55. package/drizzle/0217_external_browser_auth_operations.sql +22 -0
  56. package/drizzle/0218_organization_tenancy_foundation.sql +299 -0
  57. package/drizzle/0219_organization_tenancy_managed_human_provisioning.sql +317 -0
  58. package/drizzle/0219_site_auth_maintenance_sessions.sql +234 -0
  59. package/drizzle/0220_memory_slack_append_only_cascade.sql +38 -0
  60. package/drizzle/0220_session_channels.sql +54 -0
  61. package/drizzle/0221_sessions_channel_index.sql +5 -0
  62. package/drizzle/0222_session_visibility_authority_epochs.sql +158 -0
  63. package/drizzle/0222_sessions_channel_fk.sql +89 -0
  64. package/drizzle/0223_pending_tool_event_output.sql +55 -0
  65. package/drizzle/0223_sessions_channel_fk_validate.sql +118 -0
  66. package/drizzle/0224_slack_post_outcome_reconciliation.sql +123 -0
  67. package/drizzle/0225_session_visibility_fork_activation.sql +1363 -0
  68. package/drizzle/0226_personal_codex_authority_foundation.sql +365 -0
  69. package/drizzle/0227_slack_native_actions.sql +145 -0
  70. package/drizzle/0228_interaction_controller_data_plane.sql +14 -0
  71. package/drizzle/0228_slack_task_policy.sql +400 -0
  72. package/drizzle/0229_slack_inbox_file_fact.sql +10 -0
  73. package/drizzle/0230_user_scoped_variable_sets_rigs.sql +84 -0
  74. package/drizzle/0231_integration_definition_identity_cutover.sql +271 -0
  75. package/drizzle/0232_integration_facet_authority_cutover.sql +610 -0
  76. package/drizzle/0233_skill_and_integration_authority_cutover.sql +1162 -0
  77. package/drizzle/0234_xai_subscription_authority.sql +1139 -0
  78. package/drizzle/0235_canonical_human_login_bindings.sql +920 -0
  79. package/drizzle/0236_browser_identity_lifecycle.sql +39 -0
  80. package/drizzle/0236_session_visibility_slack_policy.sql +72 -0
  81. package/drizzle/0237_interaction_transition_reaper.sql +263 -0
  82. package/drizzle/0238_goal_persistence_policy.sql +471 -0
  83. package/drizzle/0238_recover_unclaimed_session_turns.sql +307 -0
  84. package/drizzle/0238_supergrok_realtime_model.sql +29 -0
  85. package/drizzle/0239_supergrok_video_funding.sql +50 -0
  86. package/drizzle/0239_task_tree_notes.sql +990 -0
  87. package/package.json +13 -5
  88. package/src/browser-auth.ts +2600 -123
  89. package/src/browser-downloads.ts +517 -0
  90. package/src/browser-identities.ts +220 -3
  91. package/src/browser-sessions.ts +334 -45
  92. package/src/browser-state-artifacts.ts +402 -12
  93. package/src/canonical-human-identities.ts +317 -0
  94. package/src/capability-integrations.ts +121 -213
  95. package/src/database.ts +41 -1
  96. package/src/index.ts +4625 -411
  97. package/src/insights.ts +109 -28
  98. package/src/integration-bindings.ts +101 -101
  99. package/src/{integration-features.ts → integration-facets.ts} +187 -184
  100. package/src/interaction-revisions.ts +33 -3
  101. package/src/interaction-schema.ts +311 -8
  102. package/src/pack-components.ts +43 -43
  103. package/src/pack-installations.ts +0 -43
  104. package/src/persistence-errors.ts +60 -1
  105. package/src/plugin-packages.ts +9 -9
  106. package/src/provision-roles.ts +232 -0
  107. package/src/runtime-posture.ts +227 -14
  108. package/src/schema.ts +1188 -75
  109. package/src/session-control.ts +122 -37
  110. package/src/session-queue-commands.ts +78 -1
  111. package/src/session-tenancy.ts +188 -0
  112. package/src/session-tool-call-settlement.ts +9 -3
  113. package/src/slack-task-policy-schema.ts +130 -0
  114. package/src/slack-task-policy.ts +341 -0
  115. package/src/task-notes-schema.ts +135 -0
  116. package/src/task-notes.ts +178 -0
  117. package/src/video-generation.ts +90 -12
  118. package/src/workspace-artifacts.ts +1 -1
  119. package/src/xai-subscription.ts +1887 -0
  120. package/dist/chunk-ISI2TIUG.js.map +0 -1
  121. package/dist/chunk-MWDVXQOL.js.map +0 -1
  122. package/dist/chunk-NBE6CHRI.js.map +0 -1
  123. package/dist/chunk-YHZXQ7HP.js.map +0 -1
  124. package/dist/integration-features.d.ts +0 -99
  125. /package/dist/{chunk-F5FT7BEZ.js.map → chunk-HSOWSYOX.js.map} +0 -0
@@ -16,6 +16,11 @@ import {
16
16
  type InteractionLifecycleOperationKind,
17
17
  type InteractionLifecycleOperationReceipt as InteractionLifecycleOperationReceiptValue,
18
18
  type InteractionPlacement,
19
+ NetworkRouteConfiguration,
20
+ NetworkRouteConsistency,
21
+ networkRoutePlacementCompatibilityIssue,
22
+ type NetworkRouteConfiguration as NetworkRouteConfigurationValue,
23
+ type NetworkRouteConsistency as NetworkRouteConsistencyValue,
19
24
  } from "@opengeni/contracts";
20
25
  import { and, desc, eq, inArray, isNull, sql } from "drizzle-orm";
21
26
  import { type Database, withRlsContext } from "./database";
@@ -25,6 +30,9 @@ import {
25
30
  } from "./interaction-revisions";
26
31
  import { safeDatabaseErrorFacts } from "./persistence-errors";
27
32
  import {
33
+ commitBrowserStateUploadInTransaction,
34
+ markBrowserStateUploadsDeletePendingInTransaction,
35
+ prepareBrowserStateUploadInTransaction,
28
36
  validateBrowserStateArtifactCommitInput,
29
37
  type BrowserStateArtifactCommitInput,
30
38
  } from "./browser-state-artifacts";
@@ -45,9 +53,10 @@ export const MANAGED_BROWSER_SESSION_CAPABILITIES = BrowserSessionCapabilities.p
45
53
  liveFrames: true,
46
54
  humanInput: true,
47
55
  tabs: true,
48
- downloads: false,
49
- uploads: false,
50
- clipboard: false,
56
+ downloads: true,
57
+ uploads: true,
58
+ clipboard: true,
59
+ permissions: true,
51
60
  diagnostics: true,
52
61
  rawCdp: false,
53
62
  linkedComputer: false,
@@ -64,7 +73,8 @@ export const ATTACHED_BROWSER_SESSION_CAPABILITIES = BrowserSessionCapabilities.
64
73
  tabs: true,
65
74
  downloads: false,
66
75
  uploads: false,
67
- clipboard: false,
76
+ clipboard: true,
77
+ permissions: false,
68
78
  diagnostics: true,
69
79
  rawCdp: false,
70
80
  linkedComputer: false,
@@ -73,6 +83,46 @@ export const ATTACHED_BROWSER_SESSION_CAPABILITIES = BrowserSessionCapabilities.
73
83
  parallelTargets: true,
74
84
  });
75
85
 
86
+ /** Remote Chromium controlled through the same native CDP driver. Files and
87
+ * portable profile state remain false because their bytes live on another host. */
88
+ export const EXTERNAL_BROWSER_SESSION_CAPABILITIES = BrowserSessionCapabilities.parse({
89
+ semanticObservation: true,
90
+ screenshots: true,
91
+ liveFrames: true,
92
+ humanInput: true,
93
+ tabs: true,
94
+ downloads: false,
95
+ uploads: false,
96
+ clipboard: true,
97
+ permissions: true,
98
+ diagnostics: true,
99
+ rawCdp: false,
100
+ linkedComputer: false,
101
+ privateCheckpoint: false,
102
+ identityPublication: false,
103
+ parallelTargets: true,
104
+ });
105
+
106
+ /** Measured against Lightpanda 0.3.5. Unsupported Chromium-only affordances
107
+ * remain false instead of being emulated or silently routed elsewhere. */
108
+ export const LIGHTPANDA_BROWSER_SESSION_CAPABILITIES = BrowserSessionCapabilities.parse({
109
+ semanticObservation: true,
110
+ screenshots: true,
111
+ liveFrames: false,
112
+ humanInput: false,
113
+ tabs: false,
114
+ downloads: false,
115
+ uploads: true,
116
+ clipboard: false,
117
+ permissions: false,
118
+ diagnostics: true,
119
+ rawCdp: false,
120
+ linkedComputer: false,
121
+ privateCheckpoint: false,
122
+ identityPublication: false,
123
+ parallelTargets: false,
124
+ });
125
+
76
126
  export class BrowserSessionNotFoundError extends Error {
77
127
  readonly name = "BrowserSessionNotFoundError";
78
128
  }
@@ -135,7 +185,11 @@ export type BrowserSessionControlRecord = {
135
185
  session: BrowserSession;
136
186
  tokenGeneration: number;
137
187
  sourceSessionId: string;
188
+ /** Placement hosting browserd. It equals the logical sandbox placement for
189
+ * managed browsers and remains the source home sandbox for remote browsers. */
190
+ controllerHostSandboxGroupId: string | null;
138
191
  createOperationId: string;
192
+ networkRouteAuthority: BrowserSessionNetworkRouteAuthority | null;
139
193
  operation: null | {
140
194
  operationId: string;
141
195
  kind: InteractionLifecycleOperationKind;
@@ -145,6 +199,15 @@ export type BrowserSessionControlRecord = {
145
199
  };
146
200
  };
147
201
 
202
+ export type BrowserSessionNetworkRouteAuthority = {
203
+ routeId: string;
204
+ routeVersion: number;
205
+ credentialVersion: number | null;
206
+ authorityDigest: string | null;
207
+ configuration: NetworkRouteConfigurationValue;
208
+ consistency: NetworkRouteConsistencyValue;
209
+ };
210
+
148
211
  function iso(value: Date): string {
149
212
  return value.toISOString();
150
213
  }
@@ -329,7 +392,7 @@ export function browserSessionCreateRequestDigest(input: PrepareBrowserSessionCr
329
392
  : { kind: "blank_identity" as const }
330
393
  : { kind: "none" as const };
331
394
  return requestDigest({
332
- version: 3,
395
+ version: 4,
333
396
  associatedSessionId: input.associatedSessionId,
334
397
  name: input.name,
335
398
  initialUrl: input.initialUrl,
@@ -502,7 +565,8 @@ export async function getBrowserPrivateCheckpointAuthority(
502
565
  !artifact ||
503
566
  artifact.sourceBrowserSessionId !== session.id ||
504
567
  artifact.purpose !== "private_checkpoint" ||
505
- artifact.state !== "available"
568
+ artifact.state !== "available" ||
569
+ artifact.encryptedDataKey === null
506
570
  ) {
507
571
  throw new BrowserSessionStateError(
508
572
  "BrowserSession private checkpoint authority is inconsistent",
@@ -578,7 +642,9 @@ async function controlRecordFromRows(
578
642
  session: browserSessionFromRows(row, associations),
579
643
  tokenGeneration: row.tokenGeneration,
580
644
  sourceSessionId: createdAssociations[0]!.sessionId,
645
+ controllerHostSandboxGroupId: row.controllerHostSandboxGroupId,
581
646
  createOperationId: row.createOperationId,
647
+ networkRouteAuthority: networkRouteAuthorityFromRow(row),
582
648
  operation: operation
583
649
  ? {
584
650
  operationId: operation.operationId,
@@ -591,6 +657,23 @@ async function controlRecordFromRows(
591
657
  };
592
658
  }
593
659
 
660
+ function networkRouteAuthorityFromRow(
661
+ row: BrowserSessionRow,
662
+ ): BrowserSessionNetworkRouteAuthority | null {
663
+ if (!row.networkRouteId) return null;
664
+ if (!row.networkRouteVersion || !row.networkRouteConfiguration || !row.networkRouteConsistency) {
665
+ throw new Error("BrowserSession network route is missing its pinned authority");
666
+ }
667
+ return {
668
+ routeId: row.networkRouteId,
669
+ routeVersion: row.networkRouteVersion,
670
+ credentialVersion: row.networkRouteCredentialVersion,
671
+ authorityDigest: row.networkRouteAuthorityDigest,
672
+ configuration: NetworkRouteConfiguration.parse(row.networkRouteConfiguration),
673
+ consistency: NetworkRouteConsistency.parse(row.networkRouteConsistency),
674
+ };
675
+ }
676
+
594
677
  export async function prepareBrowserSessionCreate(
595
678
  db: Database,
596
679
  input: PrepareBrowserSessionCreateInput,
@@ -617,7 +700,7 @@ export async function prepareBrowserSessionCreate(
617
700
  });
618
701
  }
619
702
  const [sourceSession] = await tx
620
- .select({ id: schema.sessions.id })
703
+ .select({ id: schema.sessions.id, sandboxGroupId: schema.sessions.sandboxGroupId })
621
704
  .from(schema.sessions)
622
705
  .where(
623
706
  and(
@@ -628,7 +711,7 @@ export async function prepareBrowserSessionCreate(
628
711
  .limit(1);
629
712
  if (!sourceSession) throw new BrowserSessionNotFoundError("Associated session not found");
630
713
  await assertLinkedComputerAvailable(tx, input);
631
- await assertNetworkRouteAvailable(tx, input);
714
+ const networkRoute = await assertNetworkRouteAvailable(tx, input);
632
715
  const identitySelection = await resolveCreateIdentitySelection(tx, input);
633
716
 
634
717
  const [insertedOperation] = await tx
@@ -644,7 +727,9 @@ export async function prepareBrowserSessionCreate(
644
727
  state: "prepared",
645
728
  actorSubjectId: input.actorSubjectId,
646
729
  })
647
- .onConflictDoNothing({ target: schema.interactionOperations.operationId })
730
+ .onConflictDoNothing({
731
+ target: schema.interactionOperations.operationId,
732
+ })
648
733
  .returning();
649
734
  if (!insertedOperation) {
650
735
  const existing = await loadOperation(tx, input.workspaceId, input.operationId);
@@ -660,6 +745,12 @@ export async function prepareBrowserSessionCreate(
660
745
  }
661
746
 
662
747
  const placementColumns = placementToColumns(input.placement);
748
+ const controllerHostSandboxGroupId =
749
+ input.placement.kind === "sandbox_group"
750
+ ? input.placement.sandboxGroupId
751
+ : input.placement.kind === "external_provider"
752
+ ? sourceSession.sandboxGroupId
753
+ : null;
663
754
  const [insertedSession] = await tx
664
755
  .insert(schema.browserSessions)
665
756
  .values({
@@ -669,12 +760,16 @@ export async function prepareBrowserSessionCreate(
669
760
  name: input.name,
670
761
  lifecycle: "starting",
671
762
  ...placementColumns,
763
+ controllerHostSandboxGroupId,
672
764
  driverId: input.driverId,
673
765
  engine: input.engine,
674
766
  headless: input.headless,
675
767
  identityId: identitySelection.identityId,
676
768
  baseRevisionId: identitySelection.baseRevisionId,
677
769
  networkRouteId: input.networkRouteId ?? null,
770
+ networkRouteVersion: networkRoute?.version ?? null,
771
+ networkRouteConfiguration: networkRoute?.configuration ?? null,
772
+ networkRouteConsistency: networkRoute?.consistency ?? null,
678
773
  linkedComputerSessionId: input.linkedComputerSessionId ?? null,
679
774
  capabilities,
680
775
  createOperationId: input.operationId,
@@ -705,6 +800,120 @@ export async function prepareBrowserSessionCreate(
705
800
  );
706
801
  }
707
802
 
803
+ /**
804
+ * Bind the broker-resolved, secret-free launch authority before controller
805
+ * dispatch. A prepared operation may refresh this binding while no physical
806
+ * controller owns it; after dispatch it is immutable and replay-fenced.
807
+ */
808
+ export async function bindBrowserSessionNetworkRouteAuthority(
809
+ db: Database,
810
+ input: {
811
+ accountId: string;
812
+ workspaceId: string;
813
+ browserSessionId: string;
814
+ operationId: string;
815
+ routeVersion: number;
816
+ credentialVersion: number | null;
817
+ authorityDigest: string;
818
+ },
819
+ ): Promise<BrowserSessionNetworkRouteAuthority> {
820
+ if (!Number.isSafeInteger(input.routeVersion) || input.routeVersion < 1) {
821
+ throw new BrowserSessionStateError("Network route version is invalid");
822
+ }
823
+ if (
824
+ input.credentialVersion !== null &&
825
+ (!Number.isSafeInteger(input.credentialVersion) || input.credentialVersion < 1)
826
+ ) {
827
+ throw new BrowserSessionStateError("Network route credential version is invalid");
828
+ }
829
+ if (!/^[A-Za-z0-9._~-]{16,256}$/u.test(input.authorityDigest)) {
830
+ throw new BrowserSessionStateError("Network route authority digest is invalid");
831
+ }
832
+ return await withRlsContext(
833
+ db,
834
+ input,
835
+ async (scopedDb) =>
836
+ await scopedDb.transaction(async (txRaw) => {
837
+ const tx = txRaw as unknown as Database;
838
+ await lockOperation(tx, input.workspaceId, input.operationId);
839
+ const operation = await loadOperation(tx, input.workspaceId, input.operationId);
840
+ assertOperationResource(operation, input.browserSessionId);
841
+ if (operation!.kind !== "create" && operation!.kind !== "resume") {
842
+ throw new BrowserSessionOperationConflictError(
843
+ "Network route authority belongs to another browser operation",
844
+ );
845
+ }
846
+ const [current] = await tx
847
+ .select()
848
+ .from(schema.browserSessions)
849
+ .where(
850
+ and(
851
+ eq(schema.browserSessions.accountId, input.accountId),
852
+ eq(schema.browserSessions.workspaceId, input.workspaceId),
853
+ eq(schema.browserSessions.id, input.browserSessionId),
854
+ ),
855
+ )
856
+ .for("update")
857
+ .limit(1);
858
+ if (!current) throw new BrowserSessionNotFoundError("BrowserSession not found");
859
+ if (
860
+ !current.networkRouteId ||
861
+ current.networkRouteVersion !== input.routeVersion ||
862
+ !current.networkRouteConfiguration ||
863
+ !current.networkRouteConsistency
864
+ ) {
865
+ throw new BrowserSessionOperationConflictError(
866
+ "BrowserSession route does not match this launch authority",
867
+ );
868
+ }
869
+ const configuration = NetworkRouteConfiguration.parse(current.networkRouteConfiguration);
870
+ const hasCredential = "credential" in configuration && configuration.credential !== null;
871
+ if (hasCredential !== (input.credentialVersion !== null)) {
872
+ throw new BrowserSessionStateError(
873
+ "Network route credential authority does not match its configuration",
874
+ );
875
+ }
876
+ if (
877
+ current.networkRouteAuthorityDigest === input.authorityDigest &&
878
+ current.networkRouteCredentialVersion === input.credentialVersion
879
+ ) {
880
+ return networkRouteAuthorityFromRow(current)!;
881
+ }
882
+ if (
883
+ operation!.state !== "prepared" ||
884
+ operation!.controllerGeneration !== null ||
885
+ current.controllerGeneration !== null
886
+ ) {
887
+ throw new BrowserSessionOperationConflictError(
888
+ "Dispatched BrowserSession route authority is immutable",
889
+ );
890
+ }
891
+ const [updated] = await tx
892
+ .update(schema.browserSessions)
893
+ .set({
894
+ networkRouteCredentialVersion: input.credentialVersion,
895
+ networkRouteAuthorityDigest: input.authorityDigest,
896
+ updatedAt: new Date(),
897
+ })
898
+ .where(
899
+ and(
900
+ eq(schema.browserSessions.workspaceId, input.workspaceId),
901
+ eq(schema.browserSessions.id, input.browserSessionId),
902
+ eq(schema.browserSessions.networkRouteVersion, input.routeVersion),
903
+ isNull(schema.browserSessions.controllerGeneration),
904
+ ),
905
+ )
906
+ .returning();
907
+ if (!updated) {
908
+ throw new BrowserSessionOperationConflictError(
909
+ "Network route authority binding lost its fence",
910
+ );
911
+ }
912
+ return networkRouteAuthorityFromRow(updated)!;
913
+ }),
914
+ );
915
+ }
916
+
708
917
  function normalizedBrowserCapabilities(
709
918
  input: PrepareBrowserSessionCreateInput,
710
919
  ): BrowserSessionCapabilitiesValue {
@@ -717,9 +926,13 @@ function normalizedBrowserCapabilities(
717
926
  async function assertNetworkRouteAvailable(
718
927
  db: Database,
719
928
  input: PrepareBrowserSessionCreateInput,
720
- ): Promise<void> {
929
+ ): Promise<{
930
+ version: number;
931
+ configuration: NetworkRouteConfigurationValue;
932
+ consistency: NetworkRouteConsistencyValue;
933
+ } | null> {
721
934
  const networkRouteId = input.networkRouteId ?? null;
722
- if (!networkRouteId) return;
935
+ if (!networkRouteId) return null;
723
936
  const [route] = await db
724
937
  .select()
725
938
  .from(schema.networkRoutes)
@@ -731,35 +944,24 @@ async function assertNetworkRouteAvailable(
731
944
  ),
732
945
  )
733
946
  .limit(1)
734
- .for("key share");
947
+ .for("update");
735
948
  if (!route) throw new BrowserSessionNotFoundError("Network route not found");
736
949
  if (route.status !== "active") {
737
950
  throw new BrowserSessionStateError("Network route is archived");
738
951
  }
739
- if (
740
- route.configuration.kind === "tunnel" &&
741
- !placementsEqual(route.configuration.placement, input.placement)
742
- ) {
743
- throw new BrowserSessionStateError("Tunnel route is bound to another placement");
744
- }
745
- }
746
-
747
- function placementsEqual(left: InteractionPlacement, right: InteractionPlacement): boolean {
748
- if (left.kind !== right.kind) return false;
749
- switch (left.kind) {
750
- case "sandbox_group":
751
- return right.kind === "sandbox_group" && left.sandboxGroupId === right.sandboxGroupId;
752
- case "connected_machine":
753
- return right.kind === "connected_machine" && left.sandboxId === right.sandboxId;
754
- case "attached_device":
755
- return right.kind === "attached_device" && left.deviceId === right.deviceId;
756
- case "external_provider":
757
- return (
758
- right.kind === "external_provider" &&
759
- left.providerId === right.providerId &&
760
- left.placementId === right.placementId
761
- );
762
- }
952
+ const configuration = NetworkRouteConfiguration.parse(route.configuration);
953
+ const consistency = NetworkRouteConsistency.parse(route.consistency);
954
+ const compatibilityIssue = networkRoutePlacementCompatibilityIssue(
955
+ configuration,
956
+ consistency,
957
+ input.placement,
958
+ );
959
+ if (compatibilityIssue) throw new BrowserSessionStateError(compatibilityIssue);
960
+ return {
961
+ version: safeInteger(route.version, "network route version"),
962
+ configuration,
963
+ consistency,
964
+ };
763
965
  }
764
966
 
765
967
  async function assertLinkedComputerAvailable(
@@ -771,11 +973,6 @@ async function assertLinkedComputerAvailable(
771
973
  if (input.headless) {
772
974
  throw new BrowserSessionStateError("A linked ComputerSession requires a headed browser");
773
975
  }
774
- if (input.placement.kind === "attached_device") {
775
- throw new BrowserSessionStateError(
776
- "Attached Chrome does not expose an exact linked ComputerSession yet",
777
- );
778
- }
779
976
  const [computer] = await db
780
977
  .select()
781
978
  .from(schema.computerSessions)
@@ -911,6 +1108,7 @@ export async function dispatchBrowserSessionOperation(
911
1108
  browserSessionId: string;
912
1109
  controllerGeneration: string;
913
1110
  controller?: InteractionControllerBinding;
1111
+ stateUpload?: { objectKey: string; cleanupAfter: Date };
914
1112
  },
915
1113
  ): Promise<InteractionLifecycleOperationReceiptValue> {
916
1114
  const controller = input.controller ? InteractionControllerBinding.parse(input.controller) : null;
@@ -929,6 +1127,16 @@ export async function dispatchBrowserSessionOperation(
929
1127
  const operation = await loadOperation(tx, input.workspaceId, input.operationId);
930
1128
  assertOperationResource(operation, input.browserSessionId);
931
1129
  if (operation!.state === "prepared") {
1130
+ if (input.stateUpload) {
1131
+ await prepareBrowserStateUploadInTransaction(tx, {
1132
+ accountId: input.accountId,
1133
+ workspaceId: input.workspaceId,
1134
+ operationId: input.operationId,
1135
+ sourceBrowserSessionId: input.browserSessionId,
1136
+ purpose: "private_checkpoint",
1137
+ ...input.stateUpload,
1138
+ });
1139
+ }
932
1140
  if (controller) {
933
1141
  await lockBrowserSession(tx, input.workspaceId, input.browserSessionId);
934
1142
  const now = new Date();
@@ -999,11 +1207,54 @@ export async function dispatchBrowserSessionOperation(
999
1207
  );
1000
1208
  }
1001
1209
  }
1210
+ if (input.stateUpload && operation!.state === "dispatched") {
1211
+ await prepareBrowserStateUploadInTransaction(tx, {
1212
+ accountId: input.accountId,
1213
+ workspaceId: input.workspaceId,
1214
+ operationId: input.operationId,
1215
+ sourceBrowserSessionId: input.browserSessionId,
1216
+ purpose: "private_checkpoint",
1217
+ ...input.stateUpload,
1218
+ });
1219
+ }
1002
1220
  return operationReceipt(operation!, true);
1003
1221
  }),
1004
1222
  );
1005
1223
  }
1006
1224
 
1225
+ /** Pulse one exact dispatched lifecycle operation while its controller mutation
1226
+ * is still in flight. This is not viewer or resource activity: it exists only
1227
+ * so the global reaper can distinguish a long profile transfer from an API
1228
+ * caller that disappeared after dispatch. */
1229
+ export async function touchInteractionOperation(
1230
+ db: Database,
1231
+ input: {
1232
+ accountId: string;
1233
+ workspaceId: string;
1234
+ operationId: string;
1235
+ resourceId: string;
1236
+ controllerGeneration: string;
1237
+ },
1238
+ ): Promise<boolean> {
1239
+ return await withRlsContext(db, input, async (scopedDb) => {
1240
+ const [row] = await scopedDb
1241
+ .update(schema.interactionOperations)
1242
+ .set({ updatedAt: new Date() })
1243
+ .where(
1244
+ and(
1245
+ eq(schema.interactionOperations.accountId, input.accountId),
1246
+ eq(schema.interactionOperations.workspaceId, input.workspaceId),
1247
+ eq(schema.interactionOperations.operationId, input.operationId),
1248
+ eq(schema.interactionOperations.resourceId, input.resourceId),
1249
+ eq(schema.interactionOperations.state, "dispatched"),
1250
+ eq(schema.interactionOperations.controllerGeneration, input.controllerGeneration),
1251
+ ),
1252
+ )
1253
+ .returning({ operationId: schema.interactionOperations.operationId });
1254
+ return Boolean(row);
1255
+ });
1256
+ }
1257
+
1007
1258
  export async function activateBrowserSession(
1008
1259
  db: Database,
1009
1260
  input: {
@@ -1042,6 +1293,25 @@ export async function activateBrowserSession(
1042
1293
  );
1043
1294
  }
1044
1295
  await lockBrowserSession(tx, input.workspaceId, input.browserSessionId);
1296
+ const [currentSession] = await tx
1297
+ .select({
1298
+ networkRouteId: schema.browserSessions.networkRouteId,
1299
+ networkRouteAuthorityDigest: schema.browserSessions.networkRouteAuthorityDigest,
1300
+ })
1301
+ .from(schema.browserSessions)
1302
+ .where(
1303
+ and(
1304
+ eq(schema.browserSessions.workspaceId, input.workspaceId),
1305
+ eq(schema.browserSessions.id, input.browserSessionId),
1306
+ ),
1307
+ )
1308
+ .limit(1);
1309
+ if (!currentSession) throw new BrowserSessionNotFoundError("BrowserSession not found");
1310
+ if (currentSession.networkRouteId && !currentSession.networkRouteAuthorityDigest) {
1311
+ throw new BrowserSessionStateError(
1312
+ "BrowserSession network route authority was not bound before activation",
1313
+ );
1314
+ }
1045
1315
  const now = new Date();
1046
1316
  const [sessionRow] = await tx
1047
1317
  .update(schema.browserSessions)
@@ -1287,6 +1557,12 @@ async function prepareBrowserSessionLifecycleTransition(
1287
1557
  .set({
1288
1558
  lifecycle: transition.targetLifecycle,
1289
1559
  failureCode: null,
1560
+ ...(transition.kind === "resume"
1561
+ ? {
1562
+ networkRouteCredentialVersion: null,
1563
+ networkRouteAuthorityDigest: null,
1564
+ }
1565
+ : {}),
1290
1566
  updatedAt: now,
1291
1567
  })
1292
1568
  .where(
@@ -1398,6 +1674,14 @@ export async function commitBrowserSessionSuspension(
1398
1674
  })
1399
1675
  .returning({ id: schema.browserStateArtifacts.id });
1400
1676
  if (!artifactRow) throw new Error("Browser checkpoint artifact insert was lost");
1677
+ await commitBrowserStateUploadInTransaction(tx, {
1678
+ workspaceId: input.workspaceId,
1679
+ operationId: input.operationId,
1680
+ sourceBrowserSessionId: input.browserSessionId,
1681
+ purpose: "private_checkpoint",
1682
+ objectKey: artifact.objectKey,
1683
+ artifactId,
1684
+ });
1401
1685
 
1402
1686
  if (current.privateCheckpointArtifactId) {
1403
1687
  const [retired] = await tx
@@ -1669,6 +1953,9 @@ async function failBrowserSessionTransition(
1669
1953
  if (!sessionRow || !operationRow) {
1670
1954
  throw new Error("BrowserSession lifecycle failure settlement was lost");
1671
1955
  }
1956
+ if (input.kind === "suspend") {
1957
+ await markBrowserStateUploadsDeletePendingInTransaction(tx, input);
1958
+ }
1672
1959
  const associations = await loadAssociations(tx, input.workspaceId, [
1673
1960
  input.browserSessionId,
1674
1961
  ]);
@@ -1712,7 +1999,9 @@ export async function prepareBrowserSessionEnd(
1712
1999
  actorSubjectId: input.actorSubjectId,
1713
2000
  ...(terminal ? { dispatchedAt: now, settledAt: now } : {}),
1714
2001
  })
1715
- .onConflictDoNothing({ target: schema.interactionOperations.operationId })
2002
+ .onConflictDoNothing({
2003
+ target: schema.interactionOperations.operationId,
2004
+ })
1716
2005
  .returning();
1717
2006
  if (!insertedOperation) {
1718
2007
  const existing = await loadOperation(tx, input.workspaceId, input.operationId);
@@ -1905,7 +2194,7 @@ export async function touchBrowserSessionController(
1905
2194
  )
1906
2195
  .limit(1);
1907
2196
  if (!observed) return false;
1908
- if (observed.placementKind === "sandbox_group") {
2197
+ if (observed.controllerHostSandboxGroupId) {
1909
2198
  // Match the reaper's exact lease -> holder -> BrowserSession lock
1910
2199
  // order. The pre-lock observation is only a locator; every authority
1911
2200
  // predicate is repeated below while the corresponding row is locked.
@@ -1915,7 +2204,7 @@ export async function touchBrowserSessionController(
1915
2204
  join browser_sessions browser
1916
2205
  on browser.account_id = lease.account_id
1917
2206
  and browser.workspace_id = lease.workspace_id
1918
- and browser.sandbox_group_id = lease.sandbox_group_id
2207
+ and browser.controller_host_sandbox_group_id = lease.sandbox_group_id
1919
2208
  where browser.account_id = ${input.accountId}
1920
2209
  and browser.workspace_id = ${input.workspaceId}
1921
2210
  and browser.id = ${input.browserSessionId}
@@ -1934,7 +2223,7 @@ export async function touchBrowserSessionController(
1934
2223
  and holder.kind = 'interaction'
1935
2224
  and holder.holder_id = ${`browser-session:${input.browserSessionId}`}
1936
2225
  and lease.workspace_id = ${input.workspaceId}
1937
- and lease.sandbox_group_id = ${observed.sandboxGroupId}
2226
+ and lease.sandbox_group_id = ${observed.controllerHostSandboxGroupId}
1938
2227
  and lease.instance_id = ${observed.placementInstanceId}
1939
2228
  returning holder.id
1940
2229
  `);