@opengeni/db 0.28.1 → 0.31.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.
- package/dist/attached-browser-devices.d.ts +42 -0
- package/dist/attempt-tool-catalogs.d.ts +16 -0
- package/dist/browser-auth.d.ts +91 -0
- package/dist/browser-identities.d.ts +128 -0
- package/dist/browser-sessions.d.ts +272 -0
- package/dist/browser-state-artifacts.d.ts +17 -0
- package/dist/capability-components.d.ts +37 -0
- package/dist/capability-integrations.d.ts +192 -0
- package/dist/chunk-F5FT7BEZ.js +3535 -0
- package/dist/chunk-F5FT7BEZ.js.map +1 -0
- package/dist/chunk-ISI2TIUG.js +639 -0
- package/dist/chunk-ISI2TIUG.js.map +1 -0
- package/dist/chunk-MWDVXQOL.js +11639 -0
- package/dist/chunk-MWDVXQOL.js.map +1 -0
- package/dist/chunk-NBE6CHRI.js +955 -0
- package/dist/chunk-NBE6CHRI.js.map +1 -0
- package/dist/{chunk-DBS5HPEW.js → chunk-YHZXQ7HP.js} +760 -11
- package/dist/chunk-YHZXQ7HP.js.map +1 -0
- package/dist/codemode-operations.d.ts +117 -0
- package/dist/codex-token-resolver.d.ts +2 -2
- package/dist/company-profile-schema.d.ts +798 -0
- package/dist/company-profile.d.ts +268 -0
- package/dist/computer-sessions.d.ts +109 -0
- package/dist/connection-token-resolver.d.ts +9 -4
- package/dist/database.d.ts +52 -3
- package/dist/editable-artifact-durable-export.d.ts +153 -0
- package/dist/editable-artifact-durable-export.js +788 -0
- package/dist/editable-artifact-durable-export.js.map +1 -0
- package/dist/editable-artifact-materialization.d.ts +123 -0
- package/dist/editable-artifacts-schema.d.ts +4152 -0
- package/dist/editable-artifacts.d.ts +610 -0
- package/dist/editable-artifacts.js +26 -0
- package/dist/editable-artifacts.js.map +1 -0
- package/dist/environment-crypto.d.ts +8 -4
- package/dist/generated-images.d.ts +139 -0
- package/dist/index.d.ts +958 -126
- package/dist/index.js +31204 -12172
- package/dist/index.js.map +1 -1
- package/dist/insights.d.ts +51 -4
- package/dist/integration-bindings.d.ts +70 -0
- package/dist/integration-features.d.ts +99 -0
- package/dist/interaction-revisions.d.ts +3 -0
- package/dist/interaction-schema.d.ts +5372 -0
- package/dist/knowledge-source-sync-schema.d.ts +1679 -0
- package/dist/knowledge-source-sync.d.ts +274 -0
- package/dist/lossless-columns.d.ts +58 -0
- package/dist/lossless-json.d.ts +39 -0
- package/dist/memory-domain.d.ts +3 -6
- package/dist/memory-governance.d.ts +2 -2
- package/dist/memory-slack-delivery.d.ts +101 -0
- package/dist/new-session-drafts.d.ts +2 -2
- package/dist/pack-components.d.ts +76 -0
- package/dist/pack-installations.d.ts +86 -0
- package/dist/persistence-errors.d.ts +7 -14
- package/dist/plugin-packages.d.ts +126 -0
- package/dist/preference-registry.d.ts +1 -1
- package/dist/provision-roles.d.ts +9 -1
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +18 -6
- package/dist/schema.d.ts +19122 -8731
- package/dist/schema.js +173 -1
- package/dist/scoped-knowledge.d.ts +97 -1
- package/dist/session-control.d.ts +4 -4
- package/dist/session-queue-commands.d.ts +19 -11
- package/dist/session-realtime-context.d.ts +2 -2
- package/dist/session-realtime-ledger.d.ts +4 -4
- package/dist/session-realtime-mirror.d.ts +2 -2
- package/dist/session-realtime-state.d.ts +1 -1
- package/dist/session-realtime-terminal.d.ts +8 -1
- package/dist/session-realtime.d.ts +7 -7
- package/dist/session-tool-call-settlement.d.ts +1 -1
- package/dist/slack-user-link-access.d.ts +54 -0
- package/dist/transcription-recordings-schema.d.ts +44 -6
- package/dist/transcription-recordings.d.ts +3 -3
- package/dist/turn-initiator.d.ts +2 -2
- package/dist/video-generation.d.ts +216 -0
- package/dist/video-generation.js +66 -0
- package/dist/video-generation.js.map +1 -0
- package/dist/workspace-artifacts.d.ts +1 -1
- package/dist/workspace-instruction-policies.d.ts +1 -1
- package/dist/workspace-learning-policy-schema.d.ts +932 -0
- package/dist/workspace-learning-policy.d.ts +161 -0
- package/drizzle/0065_enrollment_credential_generation.sql +10 -0
- package/drizzle/0140_retained_screenshot_artifacts.sql +192 -0
- package/drizzle/0176_lossless_canonical_json.sql +975 -0
- package/drizzle/0177_session_events_workspace_turn_type_index.sql +5 -0
- package/drizzle/0178_permissioned_secret_reads.sql +14 -0
- package/drizzle/0179_slack_private_shortcut_delivery_gate.sql +85 -0
- package/drizzle/0180_retained_screenshot_lifecycle_fences.sql +273 -0
- package/drizzle/0181_connected_machine_removal.sql +52 -0
- package/drizzle/0182_connected_machine_remove_session_default.sql +77 -0
- package/drizzle/0183_model_call_provider_cost_estimates.sql +39 -0
- package/drizzle/0184_sandbox_drain_teardown_fence.sql +682 -0
- package/drizzle/0185_temporal_schedule_cleanup_outbox.sql +168 -0
- package/drizzle/0186_sandbox_capture_provider_contract.sql +400 -0
- package/drizzle/0187_generated_image_artifacts.sql +173 -0
- package/drizzle/0188_image_generation_retention_failure.sql +25 -0
- package/drizzle/0189_retained_session_image_formats.sql +13 -0
- package/drizzle/0190_timeline_annotations.sql +31 -0
- package/drizzle/0191_editable_artifact_engine.sql +3853 -0
- package/drizzle/0192_editable_artifact_live_tickets.sql +338 -0
- package/drizzle/0193_editable_artifact_authorization.sql +190 -0
- package/drizzle/0194_editable_artifact_durable_exports.sql +160 -0
- package/drizzle/0195_editable_artifact_import_authorization.sql +203 -0
- package/drizzle/0196_rig_provider_images.sql +152 -0
- package/drizzle/0197_knowledge_source_sync_schedules.sql +523 -0
- package/drizzle/0198_memory_slack_publication_delivery.sql +393 -0
- package/drizzle/0199_workspace_learning_policy.sql +980 -0
- package/drizzle/0201_company_profile_authority.sql +849 -0
- package/drizzle/0202_document_index_checkpoints.sql +85 -0
- package/drizzle/0203_durable_video_generation.sql +353 -0
- package/drizzle/0204_video_generation_funding.sql +44 -0
- package/drizzle/0205_attempt_tool_catalogs.sql +230 -0
- package/drizzle/0206_browser_sessions.sql +646 -0
- package/drizzle/0207_browser_identities.sql +379 -0
- package/drizzle/0208_attached_browser_devices.sql +165 -0
- package/drizzle/0209_computer_sessions.sql +575 -0
- package/drizzle/0210_browser_auth_network_interventions.sql +441 -0
- package/drizzle/0211_editable_artifact_session_links.sql +58 -0
- package/drizzle/0212_slack_installation_bindings.sql +348 -0
- package/drizzle/0213_slack_user_link_access_requests.sql +177 -0
- package/drizzle/0214_session_activity_commit_gate.sql +262 -0
- package/drizzle/0215_capabilities_platform.sql +980 -0
- package/drizzle/0216_pack_component_ownership.sql +235 -0
- package/drizzle/0217_capability_definition_delete_authority.sql +32 -0
- package/package.json +18 -5
- package/src/attached-browser-devices.ts +371 -0
- package/src/attempt-tool-catalogs.ts +131 -0
- package/src/browser-auth.ts +1896 -0
- package/src/browser-identities.ts +1168 -0
- package/src/browser-sessions.ts +2005 -0
- package/src/browser-state-artifacts.ts +59 -0
- package/src/capability-components.ts +166 -0
- package/src/capability-integrations.ts +1663 -0
- package/src/codemode-operations.ts +674 -0
- package/src/company-profile-schema.ts +143 -0
- package/src/company-profile.ts +968 -0
- package/src/computer-sessions.ts +1130 -0
- package/src/connection-token-resolver.ts +270 -25
- package/src/database.ts +434 -24
- package/src/editable-artifact-durable-export.ts +1201 -0
- package/src/editable-artifact-materialization.ts +893 -0
- package/src/editable-artifacts-schema.ts +879 -0
- package/src/editable-artifacts.ts +5404 -0
- package/src/environment-crypto.ts +22 -9
- package/src/generated-images.ts +740 -0
- package/src/index.ts +10117 -2730
- package/src/insights.ts +160 -13
- package/src/integration-bindings.ts +455 -0
- package/src/integration-features.ts +859 -0
- package/src/interaction-revisions.ts +40 -0
- package/src/interaction-schema.ts +1303 -0
- package/src/knowledge-source-sync-schema.ts +192 -0
- package/src/knowledge-source-sync.ts +1922 -0
- package/src/lossless-columns.ts +35 -0
- package/src/lossless-json.ts +322 -0
- package/src/memory-domain.ts +5 -44
- package/src/memory-slack-delivery.ts +801 -0
- package/src/pack-components.ts +954 -0
- package/src/pack-installations.ts +929 -0
- package/src/persistence-errors.ts +26 -34
- package/src/plugin-packages.ts +1186 -0
- package/src/provision-roles.ts +302 -1
- package/src/runtime-posture-cli.ts +9 -0
- package/src/runtime-posture.ts +514 -5
- package/src/schema.ts +5177 -2957
- package/src/scoped-knowledge-schema.ts +1 -1
- package/src/scoped-knowledge.ts +655 -3
- package/src/session-control.ts +133 -81
- package/src/session-queue-commands.ts +511 -247
- package/src/session-realtime-context.ts +20 -7
- package/src/session-realtime-ledger.ts +32 -10
- package/src/session-realtime-mirror.ts +4 -2
- package/src/session-realtime-terminal.ts +89 -21
- package/src/session-realtime.ts +10 -9
- package/src/session-tool-call-settlement.ts +38 -15
- package/src/slack-user-link-access.ts +702 -0
- package/src/transcription-recordings-schema.ts +5 -2
- package/src/transcription-recordings.ts +70 -40
- package/src/video-generation.ts +1452 -0
- package/src/workspace-learning-policy-schema.ts +139 -0
- package/src/workspace-learning-policy.ts +595 -0
- package/dist/chunk-DBS5HPEW.js.map +0 -1
- package/dist/chunk-HT5T62CC.js +0 -6803
- package/dist/chunk-HT5T62CC.js.map +0 -1
- package/dist/event-payload-sanitizer.d.ts +0 -32
- package/src/event-payload-sanitizer.ts +0 -377
|
@@ -0,0 +1,1130 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
ComputerSession,
|
|
4
|
+
ComputerSessionCapabilities,
|
|
5
|
+
ComputerSessionListResponse,
|
|
6
|
+
ComputerSessionMutationResponse,
|
|
7
|
+
InteractionControllerBinding,
|
|
8
|
+
InteractionError,
|
|
9
|
+
InteractionLifecycleOperationReceipt,
|
|
10
|
+
type ComputerSessionCapabilities as ComputerSessionCapabilitiesValue,
|
|
11
|
+
type ComputerSessionListResponse as ComputerSessionListResponseValue,
|
|
12
|
+
type ComputerSessionMutationResponse as ComputerSessionMutationResponseValue,
|
|
13
|
+
type InteractionControllerBinding as InteractionControllerBindingValue,
|
|
14
|
+
type InteractionError as InteractionErrorValue,
|
|
15
|
+
type InteractionLifecycleOperationKind,
|
|
16
|
+
type InteractionLifecycleOperationReceipt as InteractionLifecycleOperationReceiptValue,
|
|
17
|
+
type InteractionPlacement,
|
|
18
|
+
} from "@opengeni/contracts";
|
|
19
|
+
import { and, desc, eq, inArray, sql } from "drizzle-orm";
|
|
20
|
+
import { type Database, withRlsContext } from "./database";
|
|
21
|
+
import {
|
|
22
|
+
advanceWorkspaceInteractionRevision,
|
|
23
|
+
readWorkspaceInteractionRevision,
|
|
24
|
+
} from "./interaction-revisions";
|
|
25
|
+
import { safeDatabaseErrorFacts } from "./persistence-errors";
|
|
26
|
+
import * as schema from "./schema";
|
|
27
|
+
|
|
28
|
+
type ComputerSessionRow = typeof schema.computerSessions.$inferSelect;
|
|
29
|
+
type ComputerAssociationRow = typeof schema.computerSessionAssociations.$inferSelect;
|
|
30
|
+
type InteractionOperationRow = typeof schema.interactionOperations.$inferSelect;
|
|
31
|
+
|
|
32
|
+
const CONSISTENT_READ = {
|
|
33
|
+
isolationLevel: "repeatable read",
|
|
34
|
+
accessMode: "read only",
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
37
|
+
export class ComputerSessionNotFoundError extends Error {
|
|
38
|
+
readonly name = "ComputerSessionNotFoundError";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class ComputerSessionOperationConflictError extends Error {
|
|
42
|
+
readonly name = "ComputerSessionOperationConflictError";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class ComputerSessionStateError extends Error {
|
|
46
|
+
readonly name = "ComputerSessionStateError";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type PrepareComputerSessionCreateInput = {
|
|
50
|
+
accountId: string;
|
|
51
|
+
workspaceId: string;
|
|
52
|
+
operationId: string;
|
|
53
|
+
associatedSessionId: string;
|
|
54
|
+
actorSubjectId: string;
|
|
55
|
+
name: string;
|
|
56
|
+
placement: InteractionPlacement;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type PrepareComputerSessionEndInput = {
|
|
60
|
+
accountId: string;
|
|
61
|
+
workspaceId: string;
|
|
62
|
+
computerSessionId: string;
|
|
63
|
+
operationId: string;
|
|
64
|
+
actorSubjectId: string;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** Internal executor view. Plaintext controller credentials are derived from
|
|
68
|
+
* this durable fence and never stored here. */
|
|
69
|
+
export type ComputerSessionControlRecord = {
|
|
70
|
+
session: ComputerSession;
|
|
71
|
+
tokenGeneration: number;
|
|
72
|
+
sourceSessionId: string;
|
|
73
|
+
createOperationId: string;
|
|
74
|
+
operation: null | {
|
|
75
|
+
operationId: string;
|
|
76
|
+
kind: InteractionLifecycleOperationKind;
|
|
77
|
+
state: InteractionOperationRow["state"];
|
|
78
|
+
controllerGeneration: string | null;
|
|
79
|
+
actorSubjectId: string;
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
function iso(value: Date): string {
|
|
84
|
+
return value.toISOString();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function placementFromRow(row: ComputerSessionRow): InteractionPlacement {
|
|
88
|
+
switch (row.placementKind) {
|
|
89
|
+
case "sandbox_group":
|
|
90
|
+
if (!row.sandboxGroupId) throw new Error("ComputerSession sandbox placement is incomplete");
|
|
91
|
+
return { kind: "sandbox_group", sandboxGroupId: row.sandboxGroupId };
|
|
92
|
+
case "connected_machine":
|
|
93
|
+
if (!row.connectedSandboxId) {
|
|
94
|
+
throw new Error("ComputerSession connected-machine placement is incomplete");
|
|
95
|
+
}
|
|
96
|
+
return { kind: "connected_machine", sandboxId: row.connectedSandboxId };
|
|
97
|
+
case "attached_device":
|
|
98
|
+
if (!row.deviceId) throw new Error("ComputerSession attached-device placement is incomplete");
|
|
99
|
+
return { kind: "attached_device", deviceId: row.deviceId };
|
|
100
|
+
case "external_provider":
|
|
101
|
+
if (!row.externalProviderId || !row.externalPlacementId) {
|
|
102
|
+
throw new Error("ComputerSession external placement is incomplete");
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
kind: "external_provider",
|
|
106
|
+
providerId: row.externalProviderId,
|
|
107
|
+
placementId: row.externalPlacementId,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function controllerFromRow(row: ComputerSessionRow): InteractionControllerBindingValue | null {
|
|
113
|
+
if (!row.controllerId && !row.controllerGeneration && !row.placementInstanceId) return null;
|
|
114
|
+
if (!row.controllerId || !row.controllerGeneration || !row.placementInstanceId) {
|
|
115
|
+
throw new Error("ComputerSession controller binding is incomplete");
|
|
116
|
+
}
|
|
117
|
+
return InteractionControllerBinding.parse({
|
|
118
|
+
controllerId: row.controllerId,
|
|
119
|
+
controllerGeneration: row.controllerGeneration,
|
|
120
|
+
placementInstanceId: row.placementInstanceId,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function associationFromRow(row: ComputerAssociationRow) {
|
|
125
|
+
return {
|
|
126
|
+
sessionId: row.sessionId,
|
|
127
|
+
turnId: row.turnId,
|
|
128
|
+
attemptId: row.attemptId,
|
|
129
|
+
relationship: row.relationship,
|
|
130
|
+
actorSubjectId: row.actorSubjectId,
|
|
131
|
+
lastUsedAt: iso(row.lastUsedAt),
|
|
132
|
+
} as const;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function computerSessionFromRows(
|
|
136
|
+
row: ComputerSessionRow,
|
|
137
|
+
associations: readonly ComputerAssociationRow[],
|
|
138
|
+
): ComputerSession {
|
|
139
|
+
return ComputerSession.parse({
|
|
140
|
+
id: row.id,
|
|
141
|
+
accountId: row.accountId,
|
|
142
|
+
workspaceId: row.workspaceId,
|
|
143
|
+
name: row.name,
|
|
144
|
+
lifecycle: row.lifecycle,
|
|
145
|
+
placement: placementFromRow(row),
|
|
146
|
+
controller: controllerFromRow(row),
|
|
147
|
+
platform: row.platform,
|
|
148
|
+
adapter: row.adapter,
|
|
149
|
+
seatId: row.seatId,
|
|
150
|
+
displayId: row.displayId,
|
|
151
|
+
capabilities: row.capabilities,
|
|
152
|
+
associations: associations.map(associationFromRow),
|
|
153
|
+
createdBySubjectId: row.createdBySubjectId,
|
|
154
|
+
createdAt: iso(row.createdAt),
|
|
155
|
+
lastUsedAt: iso(row.lastUsedAt),
|
|
156
|
+
failureCode: row.failureCode,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function operationReceipt(
|
|
161
|
+
row: InteractionOperationRow,
|
|
162
|
+
replayed: boolean,
|
|
163
|
+
): InteractionLifecycleOperationReceiptValue {
|
|
164
|
+
const error: InteractionErrorValue | null = row.errorCode
|
|
165
|
+
? InteractionError.parse({
|
|
166
|
+
code: row.errorCode,
|
|
167
|
+
message: row.errorMessage,
|
|
168
|
+
retryable: row.errorRetryable,
|
|
169
|
+
...(row.errorDetails ? { details: row.errorDetails } : {}),
|
|
170
|
+
})
|
|
171
|
+
: null;
|
|
172
|
+
return InteractionLifecycleOperationReceipt.parse({
|
|
173
|
+
operationId: row.operationId,
|
|
174
|
+
resourceKind: row.resourceKind,
|
|
175
|
+
resourceId: row.resourceId,
|
|
176
|
+
kind: row.kind,
|
|
177
|
+
state: row.state,
|
|
178
|
+
replayed,
|
|
179
|
+
error,
|
|
180
|
+
createdAt: iso(row.createdAt),
|
|
181
|
+
dispatchedAt: row.dispatchedAt ? iso(row.dispatchedAt) : null,
|
|
182
|
+
settledAt: row.settledAt ? iso(row.settledAt) : null,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function loadAssociations(
|
|
187
|
+
db: Database,
|
|
188
|
+
workspaceId: string,
|
|
189
|
+
computerSessionIds: readonly string[],
|
|
190
|
+
): Promise<ComputerAssociationRow[]> {
|
|
191
|
+
if (computerSessionIds.length === 0) return [];
|
|
192
|
+
return await db
|
|
193
|
+
.select()
|
|
194
|
+
.from(schema.computerSessionAssociations)
|
|
195
|
+
.where(
|
|
196
|
+
and(
|
|
197
|
+
eq(schema.computerSessionAssociations.workspaceId, workspaceId),
|
|
198
|
+
inArray(schema.computerSessionAssociations.computerSessionId, [...computerSessionIds]),
|
|
199
|
+
),
|
|
200
|
+
)
|
|
201
|
+
.orderBy(
|
|
202
|
+
desc(schema.computerSessionAssociations.lastUsedAt),
|
|
203
|
+
desc(schema.computerSessionAssociations.createdAt),
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function loadComputerSession(
|
|
208
|
+
db: Database,
|
|
209
|
+
workspaceId: string,
|
|
210
|
+
computerSessionId: string,
|
|
211
|
+
): Promise<ComputerSession | null> {
|
|
212
|
+
const [row] = await db
|
|
213
|
+
.select()
|
|
214
|
+
.from(schema.computerSessions)
|
|
215
|
+
.where(
|
|
216
|
+
and(
|
|
217
|
+
eq(schema.computerSessions.workspaceId, workspaceId),
|
|
218
|
+
eq(schema.computerSessions.id, computerSessionId),
|
|
219
|
+
),
|
|
220
|
+
)
|
|
221
|
+
.limit(1);
|
|
222
|
+
if (!row) return null;
|
|
223
|
+
return computerSessionFromRows(row, await loadAssociations(db, workspaceId, [computerSessionId]));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function loadOperation(
|
|
227
|
+
db: Database,
|
|
228
|
+
workspaceId: string,
|
|
229
|
+
operationId: string,
|
|
230
|
+
): Promise<InteractionOperationRow | null> {
|
|
231
|
+
const [row] = await db
|
|
232
|
+
.select()
|
|
233
|
+
.from(schema.interactionOperations)
|
|
234
|
+
.where(
|
|
235
|
+
and(
|
|
236
|
+
eq(schema.interactionOperations.workspaceId, workspaceId),
|
|
237
|
+
eq(schema.interactionOperations.operationId, operationId),
|
|
238
|
+
),
|
|
239
|
+
)
|
|
240
|
+
.limit(1);
|
|
241
|
+
return row ?? null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function requestDigest(value: Record<string, unknown>): string {
|
|
245
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function computerSessionCreateRequestDigest(
|
|
249
|
+
input: PrepareComputerSessionCreateInput,
|
|
250
|
+
): string {
|
|
251
|
+
return requestDigest({
|
|
252
|
+
version: 1,
|
|
253
|
+
associatedSessionId: input.associatedSessionId,
|
|
254
|
+
name: input.name,
|
|
255
|
+
placement: input.placement,
|
|
256
|
+
actorSubjectId: input.actorSubjectId,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function lifecycleRequestDigest(
|
|
261
|
+
computerSessionId: string,
|
|
262
|
+
kind: InteractionLifecycleOperationKind,
|
|
263
|
+
actorSubjectId: string,
|
|
264
|
+
): string {
|
|
265
|
+
return requestDigest({ version: 1, computerSessionId, kind, actorSubjectId });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function replayedMutation(
|
|
269
|
+
db: Database,
|
|
270
|
+
workspaceId: string,
|
|
271
|
+
operation: InteractionOperationRow,
|
|
272
|
+
expected: { kind: InteractionLifecycleOperationKind; digest: string },
|
|
273
|
+
): Promise<ComputerSessionMutationResponseValue> {
|
|
274
|
+
if (
|
|
275
|
+
operation.resourceKind !== "computer_session" ||
|
|
276
|
+
operation.kind !== expected.kind ||
|
|
277
|
+
operation.requestDigest !== expected.digest
|
|
278
|
+
) {
|
|
279
|
+
throw new ComputerSessionOperationConflictError(
|
|
280
|
+
"Interaction operation id is already bound to another request",
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
const session = await loadComputerSession(db, workspaceId, operation.resourceId);
|
|
284
|
+
if (!session) throw new Error("Interaction operation references a missing ComputerSession");
|
|
285
|
+
return ComputerSessionMutationResponse.parse({
|
|
286
|
+
session,
|
|
287
|
+
operation: operationReceipt(operation, true),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export async function listComputerSessions(
|
|
292
|
+
db: Database,
|
|
293
|
+
input: { accountId: string; workspaceId: string },
|
|
294
|
+
): Promise<ComputerSessionListResponseValue> {
|
|
295
|
+
return await withRlsContext(
|
|
296
|
+
db,
|
|
297
|
+
input,
|
|
298
|
+
async (scopedDb) => {
|
|
299
|
+
const rows = await scopedDb
|
|
300
|
+
.select()
|
|
301
|
+
.from(schema.computerSessions)
|
|
302
|
+
.where(eq(schema.computerSessions.workspaceId, input.workspaceId))
|
|
303
|
+
.orderBy(desc(schema.computerSessions.lastUsedAt), desc(schema.computerSessions.id));
|
|
304
|
+
const associations = await loadAssociations(
|
|
305
|
+
scopedDb,
|
|
306
|
+
input.workspaceId,
|
|
307
|
+
rows.map((row) => row.id),
|
|
308
|
+
);
|
|
309
|
+
const associationsByResource = new Map<string, ComputerAssociationRow[]>();
|
|
310
|
+
for (const association of associations) {
|
|
311
|
+
const current = associationsByResource.get(association.computerSessionId) ?? [];
|
|
312
|
+
current.push(association);
|
|
313
|
+
associationsByResource.set(association.computerSessionId, current);
|
|
314
|
+
}
|
|
315
|
+
return ComputerSessionListResponse.parse({
|
|
316
|
+
revision: await readWorkspaceInteractionRevision(scopedDb, input.workspaceId),
|
|
317
|
+
sessions: rows.map((row) =>
|
|
318
|
+
computerSessionFromRows(row, associationsByResource.get(row.id) ?? []),
|
|
319
|
+
),
|
|
320
|
+
});
|
|
321
|
+
},
|
|
322
|
+
CONSISTENT_READ,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export async function getComputerSession(
|
|
327
|
+
db: Database,
|
|
328
|
+
input: { accountId: string; workspaceId: string; computerSessionId: string },
|
|
329
|
+
): Promise<ComputerSession> {
|
|
330
|
+
return await withRlsContext(
|
|
331
|
+
db,
|
|
332
|
+
input,
|
|
333
|
+
async (scopedDb) => {
|
|
334
|
+
const session = await loadComputerSession(
|
|
335
|
+
scopedDb,
|
|
336
|
+
input.workspaceId,
|
|
337
|
+
input.computerSessionId,
|
|
338
|
+
);
|
|
339
|
+
if (!session) throw new ComputerSessionNotFoundError("ComputerSession not found");
|
|
340
|
+
return session;
|
|
341
|
+
},
|
|
342
|
+
CONSISTENT_READ,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function controlRecordFromRows(
|
|
347
|
+
db: Database,
|
|
348
|
+
workspaceId: string,
|
|
349
|
+
row: ComputerSessionRow,
|
|
350
|
+
operation: InteractionOperationRow | null,
|
|
351
|
+
): Promise<ComputerSessionControlRecord> {
|
|
352
|
+
const associations = await loadAssociations(db, workspaceId, [row.id]);
|
|
353
|
+
const created = associations.filter((association) => association.relationship === "created");
|
|
354
|
+
if (created.length !== 1) {
|
|
355
|
+
throw new Error("ComputerSession must have exactly one creation association");
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
session: computerSessionFromRows(row, associations),
|
|
359
|
+
tokenGeneration: row.tokenGeneration,
|
|
360
|
+
sourceSessionId: created[0]!.sessionId,
|
|
361
|
+
createOperationId: row.createOperationId,
|
|
362
|
+
operation: operation
|
|
363
|
+
? {
|
|
364
|
+
operationId: operation.operationId,
|
|
365
|
+
kind: operation.kind,
|
|
366
|
+
state: operation.state,
|
|
367
|
+
controllerGeneration: operation.controllerGeneration,
|
|
368
|
+
actorSubjectId: operation.actorSubjectId,
|
|
369
|
+
}
|
|
370
|
+
: null,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export async function getComputerSessionControlRecord(
|
|
375
|
+
db: Database,
|
|
376
|
+
input: {
|
|
377
|
+
accountId: string;
|
|
378
|
+
workspaceId: string;
|
|
379
|
+
computerSessionId: string;
|
|
380
|
+
operationId?: string;
|
|
381
|
+
},
|
|
382
|
+
): Promise<ComputerSessionControlRecord> {
|
|
383
|
+
return await withRlsContext(
|
|
384
|
+
db,
|
|
385
|
+
input,
|
|
386
|
+
async (scopedDb) => {
|
|
387
|
+
const [row] = await scopedDb
|
|
388
|
+
.select()
|
|
389
|
+
.from(schema.computerSessions)
|
|
390
|
+
.where(
|
|
391
|
+
and(
|
|
392
|
+
eq(schema.computerSessions.accountId, input.accountId),
|
|
393
|
+
eq(schema.computerSessions.workspaceId, input.workspaceId),
|
|
394
|
+
eq(schema.computerSessions.id, input.computerSessionId),
|
|
395
|
+
),
|
|
396
|
+
)
|
|
397
|
+
.limit(1);
|
|
398
|
+
if (!row) throw new ComputerSessionNotFoundError("ComputerSession not found");
|
|
399
|
+
const operation = input.operationId
|
|
400
|
+
? await loadOperation(scopedDb, input.workspaceId, input.operationId)
|
|
401
|
+
: null;
|
|
402
|
+
if (input.operationId) assertOperationResource(operation, input.computerSessionId);
|
|
403
|
+
return await controlRecordFromRows(scopedDb, input.workspaceId, row, operation);
|
|
404
|
+
},
|
|
405
|
+
CONSISTENT_READ,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export async function findComputerSessionControlRecordByOperation(
|
|
410
|
+
db: Database,
|
|
411
|
+
input: { accountId: string; workspaceId: string; operationId: string },
|
|
412
|
+
): Promise<ComputerSessionControlRecord | null> {
|
|
413
|
+
return await withRlsContext(
|
|
414
|
+
db,
|
|
415
|
+
input,
|
|
416
|
+
async (scopedDb) => {
|
|
417
|
+
const operation = await loadOperation(scopedDb, input.workspaceId, input.operationId);
|
|
418
|
+
if (!operation) return null;
|
|
419
|
+
if (operation.resourceKind !== "computer_session") {
|
|
420
|
+
throw new ComputerSessionOperationConflictError(
|
|
421
|
+
"Interaction operation is bound to another resource",
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
const [row] = await scopedDb
|
|
425
|
+
.select()
|
|
426
|
+
.from(schema.computerSessions)
|
|
427
|
+
.where(
|
|
428
|
+
and(
|
|
429
|
+
eq(schema.computerSessions.accountId, input.accountId),
|
|
430
|
+
eq(schema.computerSessions.workspaceId, input.workspaceId),
|
|
431
|
+
eq(schema.computerSessions.id, operation.resourceId),
|
|
432
|
+
),
|
|
433
|
+
)
|
|
434
|
+
.limit(1);
|
|
435
|
+
if (!row) throw new Error("ComputerSession operation references a missing resource");
|
|
436
|
+
return await controlRecordFromRows(scopedDb, input.workspaceId, row, operation);
|
|
437
|
+
},
|
|
438
|
+
CONSISTENT_READ,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function placementToColumns(placement: InteractionPlacement): {
|
|
443
|
+
placementKind: ComputerSessionRow["placementKind"];
|
|
444
|
+
sandboxGroupId: string | null;
|
|
445
|
+
connectedSandboxId: string | null;
|
|
446
|
+
deviceId: string | null;
|
|
447
|
+
externalProviderId: string | null;
|
|
448
|
+
externalPlacementId: string | null;
|
|
449
|
+
} {
|
|
450
|
+
return {
|
|
451
|
+
placementKind: placement.kind,
|
|
452
|
+
sandboxGroupId: placement.kind === "sandbox_group" ? placement.sandboxGroupId : null,
|
|
453
|
+
connectedSandboxId: placement.kind === "connected_machine" ? placement.sandboxId : null,
|
|
454
|
+
deviceId: placement.kind === "attached_device" ? placement.deviceId : null,
|
|
455
|
+
externalProviderId: placement.kind === "external_provider" ? placement.providerId : null,
|
|
456
|
+
externalPlacementId: placement.kind === "external_provider" ? placement.placementId : null,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export async function prepareComputerSessionCreate(
|
|
461
|
+
db: Database,
|
|
462
|
+
input: PrepareComputerSessionCreateInput,
|
|
463
|
+
): Promise<ComputerSessionMutationResponseValue> {
|
|
464
|
+
const digest = computerSessionCreateRequestDigest(input);
|
|
465
|
+
const computerSessionId = randomUUID();
|
|
466
|
+
return await withRlsContext(db, input, async (scopedDb) =>
|
|
467
|
+
scopedDb.transaction(async (txRaw) => {
|
|
468
|
+
const tx = txRaw as unknown as Database;
|
|
469
|
+
await tx.execute(sql`
|
|
470
|
+
select id from workspaces
|
|
471
|
+
where id = ${input.workspaceId} and account_id = ${input.accountId}
|
|
472
|
+
for key share
|
|
473
|
+
`);
|
|
474
|
+
const existingOperation = await loadOperation(tx, input.workspaceId, input.operationId);
|
|
475
|
+
if (existingOperation) {
|
|
476
|
+
return await replayedMutation(tx, input.workspaceId, existingOperation, {
|
|
477
|
+
kind: "create",
|
|
478
|
+
digest,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
const [sourceSession] = await tx
|
|
482
|
+
.select({ id: schema.sessions.id })
|
|
483
|
+
.from(schema.sessions)
|
|
484
|
+
.where(
|
|
485
|
+
and(
|
|
486
|
+
eq(schema.sessions.workspaceId, input.workspaceId),
|
|
487
|
+
eq(schema.sessions.id, input.associatedSessionId),
|
|
488
|
+
),
|
|
489
|
+
)
|
|
490
|
+
.limit(1);
|
|
491
|
+
if (!sourceSession) throw new ComputerSessionNotFoundError("Associated session not found");
|
|
492
|
+
|
|
493
|
+
const [insertedOperation] = await tx
|
|
494
|
+
.insert(schema.interactionOperations)
|
|
495
|
+
.values({
|
|
496
|
+
operationId: input.operationId,
|
|
497
|
+
accountId: input.accountId,
|
|
498
|
+
workspaceId: input.workspaceId,
|
|
499
|
+
resourceKind: "computer_session",
|
|
500
|
+
resourceId: computerSessionId,
|
|
501
|
+
kind: "create",
|
|
502
|
+
requestDigest: digest,
|
|
503
|
+
state: "prepared",
|
|
504
|
+
actorSubjectId: input.actorSubjectId,
|
|
505
|
+
})
|
|
506
|
+
.onConflictDoNothing({ target: schema.interactionOperations.operationId })
|
|
507
|
+
.returning();
|
|
508
|
+
if (!insertedOperation) {
|
|
509
|
+
const existing = await loadOperation(tx, input.workspaceId, input.operationId);
|
|
510
|
+
if (!existing) {
|
|
511
|
+
throw new ComputerSessionOperationConflictError(
|
|
512
|
+
"Interaction operation id belongs to another workspace",
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
return await replayedMutation(tx, input.workspaceId, existing, {
|
|
516
|
+
kind: "create",
|
|
517
|
+
digest,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const [insertedSession] = await tx
|
|
522
|
+
.insert(schema.computerSessions)
|
|
523
|
+
.values({
|
|
524
|
+
id: computerSessionId,
|
|
525
|
+
accountId: input.accountId,
|
|
526
|
+
workspaceId: input.workspaceId,
|
|
527
|
+
name: input.name,
|
|
528
|
+
lifecycle: "starting",
|
|
529
|
+
...placementToColumns(input.placement),
|
|
530
|
+
createOperationId: input.operationId,
|
|
531
|
+
createdBySubjectId: input.actorSubjectId,
|
|
532
|
+
})
|
|
533
|
+
.returning();
|
|
534
|
+
if (!insertedSession) throw new Error("ComputerSession insert did not return its row");
|
|
535
|
+
const [association] = await tx
|
|
536
|
+
.insert(schema.computerSessionAssociations)
|
|
537
|
+
.values({
|
|
538
|
+
accountId: input.accountId,
|
|
539
|
+
workspaceId: input.workspaceId,
|
|
540
|
+
computerSessionId,
|
|
541
|
+
sessionId: input.associatedSessionId,
|
|
542
|
+
turnId: null,
|
|
543
|
+
attemptId: null,
|
|
544
|
+
relationship: "created",
|
|
545
|
+
actorSubjectId: input.actorSubjectId,
|
|
546
|
+
})
|
|
547
|
+
.returning();
|
|
548
|
+
if (!association) throw new Error("ComputerSession association insert failed");
|
|
549
|
+
await advanceWorkspaceInteractionRevision(tx, input.accountId, input.workspaceId);
|
|
550
|
+
return ComputerSessionMutationResponse.parse({
|
|
551
|
+
session: computerSessionFromRows(insertedSession, [association]),
|
|
552
|
+
operation: operationReceipt(insertedOperation, false),
|
|
553
|
+
});
|
|
554
|
+
}),
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export async function dispatchComputerSessionOperation(
|
|
559
|
+
db: Database,
|
|
560
|
+
input: {
|
|
561
|
+
accountId: string;
|
|
562
|
+
workspaceId: string;
|
|
563
|
+
operationId: string;
|
|
564
|
+
computerSessionId: string;
|
|
565
|
+
controllerGeneration: string;
|
|
566
|
+
controller?: InteractionControllerBindingValue;
|
|
567
|
+
},
|
|
568
|
+
): Promise<InteractionLifecycleOperationReceiptValue> {
|
|
569
|
+
const controller = input.controller ? InteractionControllerBinding.parse(input.controller) : null;
|
|
570
|
+
if (controller && controller.controllerGeneration !== input.controllerGeneration) {
|
|
571
|
+
throw new ComputerSessionOperationConflictError(
|
|
572
|
+
"ComputerSession dispatch controller generations disagree",
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
return await withRlsContext(db, input, async (scopedDb) =>
|
|
576
|
+
scopedDb.transaction(async (txRaw) => {
|
|
577
|
+
const tx = txRaw as unknown as Database;
|
|
578
|
+
await lockOperation(tx, input.workspaceId, input.operationId);
|
|
579
|
+
const operation = await loadOperation(tx, input.workspaceId, input.operationId);
|
|
580
|
+
assertOperationResource(operation, input.computerSessionId);
|
|
581
|
+
if (operation.state === "prepared") {
|
|
582
|
+
if (controller) {
|
|
583
|
+
await lockComputerSession(tx, input.workspaceId, input.computerSessionId);
|
|
584
|
+
const now = new Date();
|
|
585
|
+
const [bound] = await tx
|
|
586
|
+
.update(schema.computerSessions)
|
|
587
|
+
.set({
|
|
588
|
+
controllerId: controller.controllerId,
|
|
589
|
+
controllerGeneration: controller.controllerGeneration,
|
|
590
|
+
placementInstanceId: controller.placementInstanceId,
|
|
591
|
+
controllerHeartbeatAt: now,
|
|
592
|
+
updatedAt: now,
|
|
593
|
+
})
|
|
594
|
+
.where(
|
|
595
|
+
and(
|
|
596
|
+
eq(schema.computerSessions.workspaceId, input.workspaceId),
|
|
597
|
+
eq(schema.computerSessions.id, input.computerSessionId),
|
|
598
|
+
eq(schema.computerSessions.lifecycle, "starting"),
|
|
599
|
+
),
|
|
600
|
+
)
|
|
601
|
+
.returning();
|
|
602
|
+
if (!bound) {
|
|
603
|
+
throw new ComputerSessionStateError(
|
|
604
|
+
"ComputerSession cannot accept a controller dispatch binding",
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
const now = new Date();
|
|
609
|
+
const [updated] = await tx
|
|
610
|
+
.update(schema.interactionOperations)
|
|
611
|
+
.set({
|
|
612
|
+
state: "dispatched",
|
|
613
|
+
controllerGeneration: input.controllerGeneration,
|
|
614
|
+
dispatchedAt: now,
|
|
615
|
+
updatedAt: now,
|
|
616
|
+
})
|
|
617
|
+
.where(eq(schema.interactionOperations.operationId, input.operationId))
|
|
618
|
+
.returning();
|
|
619
|
+
if (!updated) throw new Error("ComputerSession operation dispatch was lost");
|
|
620
|
+
return operationReceipt(updated, false);
|
|
621
|
+
}
|
|
622
|
+
if (
|
|
623
|
+
operation.controllerGeneration !== null &&
|
|
624
|
+
operation.controllerGeneration !== input.controllerGeneration
|
|
625
|
+
) {
|
|
626
|
+
throw new ComputerSessionOperationConflictError(
|
|
627
|
+
"ComputerSession operation belongs to another controller generation",
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
if (controller) {
|
|
631
|
+
const [row] = await tx
|
|
632
|
+
.select()
|
|
633
|
+
.from(schema.computerSessions)
|
|
634
|
+
.where(
|
|
635
|
+
and(
|
|
636
|
+
eq(schema.computerSessions.workspaceId, input.workspaceId),
|
|
637
|
+
eq(schema.computerSessions.id, input.computerSessionId),
|
|
638
|
+
),
|
|
639
|
+
)
|
|
640
|
+
.limit(1);
|
|
641
|
+
const current = row ? controllerFromRow(row) : null;
|
|
642
|
+
if (
|
|
643
|
+
!current ||
|
|
644
|
+
current.controllerId !== controller.controllerId ||
|
|
645
|
+
current.controllerGeneration !== controller.controllerGeneration ||
|
|
646
|
+
current.placementInstanceId !== controller.placementInstanceId
|
|
647
|
+
) {
|
|
648
|
+
throw new ComputerSessionOperationConflictError(
|
|
649
|
+
"ComputerSession dispatch belongs to another controller binding",
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return operationReceipt(operation, true);
|
|
654
|
+
}),
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
export async function activateComputerSession(
|
|
659
|
+
db: Database,
|
|
660
|
+
input: {
|
|
661
|
+
accountId: string;
|
|
662
|
+
workspaceId: string;
|
|
663
|
+
operationId: string;
|
|
664
|
+
computerSessionId: string;
|
|
665
|
+
controller: InteractionControllerBindingValue;
|
|
666
|
+
platform: "linux" | "macos" | "windows";
|
|
667
|
+
adapter: string;
|
|
668
|
+
seatId: string;
|
|
669
|
+
displayId: string;
|
|
670
|
+
capabilities: ComputerSessionCapabilitiesValue;
|
|
671
|
+
},
|
|
672
|
+
): Promise<ComputerSessionMutationResponseValue> {
|
|
673
|
+
const controller = InteractionControllerBinding.parse(input.controller);
|
|
674
|
+
const capabilities = ComputerSessionCapabilities.parse(input.capabilities);
|
|
675
|
+
return await withRlsContext(db, input, async (scopedDb) =>
|
|
676
|
+
scopedDb.transaction(async (txRaw) => {
|
|
677
|
+
const tx = txRaw as unknown as Database;
|
|
678
|
+
await lockOperation(tx, input.workspaceId, input.operationId);
|
|
679
|
+
const operation = await loadOperation(tx, input.workspaceId, input.operationId);
|
|
680
|
+
assertOperationResource(operation, input.computerSessionId);
|
|
681
|
+
if (operation.state === "completed") {
|
|
682
|
+
return await replayedMutation(tx, input.workspaceId, operation, {
|
|
683
|
+
kind: operation.kind,
|
|
684
|
+
digest: operation.requestDigest,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
if (operation.state !== "prepared" && operation.state !== "dispatched") {
|
|
688
|
+
throw new ComputerSessionStateError("ComputerSession operation is already terminal");
|
|
689
|
+
}
|
|
690
|
+
if (
|
|
691
|
+
operation.controllerGeneration !== null &&
|
|
692
|
+
operation.controllerGeneration !== controller.controllerGeneration
|
|
693
|
+
) {
|
|
694
|
+
throw new ComputerSessionOperationConflictError(
|
|
695
|
+
"ComputerSession activation has a stale controller generation",
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
await lockComputerSession(tx, input.workspaceId, input.computerSessionId);
|
|
699
|
+
const now = new Date();
|
|
700
|
+
const [sessionRow] = await tx
|
|
701
|
+
.update(schema.computerSessions)
|
|
702
|
+
.set({
|
|
703
|
+
lifecycle: "active",
|
|
704
|
+
controllerId: controller.controllerId,
|
|
705
|
+
controllerGeneration: controller.controllerGeneration,
|
|
706
|
+
placementInstanceId: controller.placementInstanceId,
|
|
707
|
+
platform: input.platform,
|
|
708
|
+
adapter: input.adapter,
|
|
709
|
+
seatId: input.seatId,
|
|
710
|
+
displayId: input.displayId,
|
|
711
|
+
capabilities,
|
|
712
|
+
controllerHeartbeatAt: now,
|
|
713
|
+
lastUsedAt: now,
|
|
714
|
+
failureCode: null,
|
|
715
|
+
updatedAt: now,
|
|
716
|
+
})
|
|
717
|
+
.where(
|
|
718
|
+
and(
|
|
719
|
+
eq(schema.computerSessions.workspaceId, input.workspaceId),
|
|
720
|
+
eq(schema.computerSessions.id, input.computerSessionId),
|
|
721
|
+
inArray(schema.computerSessions.lifecycle, ["starting", "active"]),
|
|
722
|
+
),
|
|
723
|
+
)
|
|
724
|
+
.returning();
|
|
725
|
+
if (!sessionRow) throw new ComputerSessionStateError("ComputerSession cannot be activated");
|
|
726
|
+
const [operationRow] = await tx
|
|
727
|
+
.update(schema.interactionOperations)
|
|
728
|
+
.set({
|
|
729
|
+
state: "completed",
|
|
730
|
+
controllerGeneration: controller.controllerGeneration,
|
|
731
|
+
dispatchedAt: operation.dispatchedAt ?? now,
|
|
732
|
+
settledAt: now,
|
|
733
|
+
updatedAt: now,
|
|
734
|
+
})
|
|
735
|
+
.where(eq(schema.interactionOperations.operationId, input.operationId))
|
|
736
|
+
.returning();
|
|
737
|
+
if (!operationRow) throw new Error("ComputerSession operation completion was lost");
|
|
738
|
+
const associations = await loadAssociations(tx, input.workspaceId, [input.computerSessionId]);
|
|
739
|
+
await advanceWorkspaceInteractionRevision(tx, input.accountId, input.workspaceId);
|
|
740
|
+
return ComputerSessionMutationResponse.parse({
|
|
741
|
+
session: computerSessionFromRows(sessionRow, associations),
|
|
742
|
+
operation: operationReceipt(operationRow, false),
|
|
743
|
+
});
|
|
744
|
+
}),
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
export async function failComputerSessionOperation(
|
|
749
|
+
db: Database,
|
|
750
|
+
input: {
|
|
751
|
+
accountId: string;
|
|
752
|
+
workspaceId: string;
|
|
753
|
+
operationId: string;
|
|
754
|
+
computerSessionId: string;
|
|
755
|
+
state?: "failed" | "outcome_unknown";
|
|
756
|
+
error: InteractionErrorValue;
|
|
757
|
+
},
|
|
758
|
+
): Promise<ComputerSessionMutationResponseValue> {
|
|
759
|
+
const failure = InteractionError.parse(input.error);
|
|
760
|
+
return await withRlsContext(db, input, async (scopedDb) =>
|
|
761
|
+
scopedDb.transaction(async (txRaw) => {
|
|
762
|
+
const tx = txRaw as unknown as Database;
|
|
763
|
+
await lockOperation(tx, input.workspaceId, input.operationId);
|
|
764
|
+
const operation = await loadOperation(tx, input.workspaceId, input.operationId);
|
|
765
|
+
assertOperationResource(operation, input.computerSessionId);
|
|
766
|
+
if (["completed", "failed", "outcome_unknown"].includes(operation.state)) {
|
|
767
|
+
return await replayedMutation(tx, input.workspaceId, operation, {
|
|
768
|
+
kind: operation.kind,
|
|
769
|
+
digest: operation.requestDigest,
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
await lockComputerSession(tx, input.workspaceId, input.computerSessionId);
|
|
773
|
+
const now = new Date();
|
|
774
|
+
const uncertain = input.state === "outcome_unknown";
|
|
775
|
+
const [sessionRow] = await tx
|
|
776
|
+
.update(schema.computerSessions)
|
|
777
|
+
.set({
|
|
778
|
+
lifecycle: uncertain ? "lost" : "failed",
|
|
779
|
+
...(uncertain
|
|
780
|
+
? {}
|
|
781
|
+
: {
|
|
782
|
+
controllerId: null,
|
|
783
|
+
controllerGeneration: null,
|
|
784
|
+
placementInstanceId: null,
|
|
785
|
+
controllerHeartbeatAt: null,
|
|
786
|
+
}),
|
|
787
|
+
failureCode: failure.code,
|
|
788
|
+
updatedAt: now,
|
|
789
|
+
})
|
|
790
|
+
.where(
|
|
791
|
+
and(
|
|
792
|
+
eq(schema.computerSessions.workspaceId, input.workspaceId),
|
|
793
|
+
eq(schema.computerSessions.id, input.computerSessionId),
|
|
794
|
+
),
|
|
795
|
+
)
|
|
796
|
+
.returning();
|
|
797
|
+
const [operationRow] = await tx
|
|
798
|
+
.update(schema.interactionOperations)
|
|
799
|
+
.set({
|
|
800
|
+
state: input.state ?? "failed",
|
|
801
|
+
errorCode: failure.code,
|
|
802
|
+
errorMessage: failure.message,
|
|
803
|
+
errorRetryable: failure.retryable,
|
|
804
|
+
errorDetails: failure.details ?? null,
|
|
805
|
+
settledAt: now,
|
|
806
|
+
updatedAt: now,
|
|
807
|
+
})
|
|
808
|
+
.where(eq(schema.interactionOperations.operationId, input.operationId))
|
|
809
|
+
.returning();
|
|
810
|
+
if (!sessionRow || !operationRow) {
|
|
811
|
+
throw new Error("ComputerSession failure settlement was lost");
|
|
812
|
+
}
|
|
813
|
+
const associations = await loadAssociations(tx, input.workspaceId, [input.computerSessionId]);
|
|
814
|
+
await advanceWorkspaceInteractionRevision(tx, input.accountId, input.workspaceId);
|
|
815
|
+
return ComputerSessionMutationResponse.parse({
|
|
816
|
+
session: computerSessionFromRows(sessionRow, associations),
|
|
817
|
+
operation: operationReceipt(operationRow, false),
|
|
818
|
+
});
|
|
819
|
+
}),
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
export async function prepareComputerSessionEnd(
|
|
824
|
+
db: Database,
|
|
825
|
+
input: PrepareComputerSessionEndInput,
|
|
826
|
+
): Promise<ComputerSessionMutationResponseValue> {
|
|
827
|
+
const digest = lifecycleRequestDigest(input.computerSessionId, "end", input.actorSubjectId);
|
|
828
|
+
try {
|
|
829
|
+
return await withRlsContext(db, input, async (scopedDb) =>
|
|
830
|
+
scopedDb.transaction(async (txRaw) => {
|
|
831
|
+
const tx = txRaw as unknown as Database;
|
|
832
|
+
await lockComputerSession(tx, input.workspaceId, input.computerSessionId);
|
|
833
|
+
const session = await loadComputerSession(tx, input.workspaceId, input.computerSessionId);
|
|
834
|
+
if (!session) throw new ComputerSessionNotFoundError("ComputerSession not found");
|
|
835
|
+
const [dependentBrowser] = await tx
|
|
836
|
+
.select({ id: schema.browserSessions.id })
|
|
837
|
+
.from(schema.browserSessions)
|
|
838
|
+
.where(
|
|
839
|
+
and(
|
|
840
|
+
eq(schema.browserSessions.workspaceId, input.workspaceId),
|
|
841
|
+
eq(schema.browserSessions.linkedComputerSessionId, input.computerSessionId),
|
|
842
|
+
inArray(schema.browserSessions.lifecycle, [
|
|
843
|
+
"starting",
|
|
844
|
+
"active",
|
|
845
|
+
"suspending",
|
|
846
|
+
"suspended",
|
|
847
|
+
"restoring",
|
|
848
|
+
"repair_required",
|
|
849
|
+
"lost",
|
|
850
|
+
"ending",
|
|
851
|
+
]),
|
|
852
|
+
),
|
|
853
|
+
)
|
|
854
|
+
.limit(1);
|
|
855
|
+
if (dependentBrowser) {
|
|
856
|
+
throw new ComputerSessionStateError(
|
|
857
|
+
"ComputerSession is still used by a BrowserSession; end the browser first",
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
const now = new Date();
|
|
861
|
+
const terminal = session.lifecycle === "ended";
|
|
862
|
+
const [insertedOperation] = await tx
|
|
863
|
+
.insert(schema.interactionOperations)
|
|
864
|
+
.values({
|
|
865
|
+
operationId: input.operationId,
|
|
866
|
+
accountId: input.accountId,
|
|
867
|
+
workspaceId: input.workspaceId,
|
|
868
|
+
resourceKind: "computer_session",
|
|
869
|
+
resourceId: input.computerSessionId,
|
|
870
|
+
kind: "end",
|
|
871
|
+
requestDigest: digest,
|
|
872
|
+
state: terminal ? "completed" : "prepared",
|
|
873
|
+
actorSubjectId: input.actorSubjectId,
|
|
874
|
+
...(terminal ? { dispatchedAt: now, settledAt: now } : {}),
|
|
875
|
+
})
|
|
876
|
+
.onConflictDoNothing({ target: schema.interactionOperations.operationId })
|
|
877
|
+
.returning();
|
|
878
|
+
if (!insertedOperation) {
|
|
879
|
+
const existing = await loadOperation(tx, input.workspaceId, input.operationId);
|
|
880
|
+
if (!existing) {
|
|
881
|
+
throw new ComputerSessionOperationConflictError(
|
|
882
|
+
"Interaction operation id belongs to another workspace",
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
return await replayedMutation(tx, input.workspaceId, existing, {
|
|
886
|
+
kind: "end",
|
|
887
|
+
digest,
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
if (terminal) {
|
|
891
|
+
return ComputerSessionMutationResponse.parse({
|
|
892
|
+
session,
|
|
893
|
+
operation: operationReceipt(insertedOperation, false),
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
const [sessionRow] = await tx
|
|
897
|
+
.update(schema.computerSessions)
|
|
898
|
+
.set({ lifecycle: "ending", failureCode: null, updatedAt: now })
|
|
899
|
+
.where(
|
|
900
|
+
and(
|
|
901
|
+
eq(schema.computerSessions.workspaceId, input.workspaceId),
|
|
902
|
+
eq(schema.computerSessions.id, input.computerSessionId),
|
|
903
|
+
),
|
|
904
|
+
)
|
|
905
|
+
.returning();
|
|
906
|
+
if (!sessionRow) throw new ComputerSessionNotFoundError("ComputerSession not found");
|
|
907
|
+
const associations = await loadAssociations(tx, input.workspaceId, [
|
|
908
|
+
input.computerSessionId,
|
|
909
|
+
]);
|
|
910
|
+
await advanceWorkspaceInteractionRevision(tx, input.accountId, input.workspaceId);
|
|
911
|
+
return ComputerSessionMutationResponse.parse({
|
|
912
|
+
session: computerSessionFromRows(sessionRow, associations),
|
|
913
|
+
operation: operationReceipt(insertedOperation, false),
|
|
914
|
+
});
|
|
915
|
+
}),
|
|
916
|
+
);
|
|
917
|
+
} catch (error) {
|
|
918
|
+
if (safeDatabaseErrorFacts(error).constraint === "interaction_operations_active_resource_uq") {
|
|
919
|
+
throw new ComputerSessionOperationConflictError(
|
|
920
|
+
"ComputerSession already has an active lifecycle operation",
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
throw error;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
export async function completeComputerSessionEnd(
|
|
928
|
+
db: Database,
|
|
929
|
+
input: {
|
|
930
|
+
accountId: string;
|
|
931
|
+
workspaceId: string;
|
|
932
|
+
operationId: string;
|
|
933
|
+
computerSessionId: string;
|
|
934
|
+
expectedControllerGeneration: string | null;
|
|
935
|
+
},
|
|
936
|
+
): Promise<ComputerSessionMutationResponseValue> {
|
|
937
|
+
return await withRlsContext(db, input, async (scopedDb) =>
|
|
938
|
+
scopedDb.transaction(async (txRaw) => {
|
|
939
|
+
const tx = txRaw as unknown as Database;
|
|
940
|
+
await lockOperation(tx, input.workspaceId, input.operationId);
|
|
941
|
+
const operation = await loadOperation(tx, input.workspaceId, input.operationId);
|
|
942
|
+
assertOperationResource(operation, input.computerSessionId, "end");
|
|
943
|
+
if (operation.state === "completed") {
|
|
944
|
+
return await replayedMutation(tx, input.workspaceId, operation, {
|
|
945
|
+
kind: "end",
|
|
946
|
+
digest: operation.requestDigest,
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
if (operation.state !== "prepared" && operation.state !== "dispatched") {
|
|
950
|
+
throw new ComputerSessionStateError("ComputerSession end operation is already terminal");
|
|
951
|
+
}
|
|
952
|
+
await lockComputerSession(tx, input.workspaceId, input.computerSessionId);
|
|
953
|
+
const [current] = await tx
|
|
954
|
+
.select()
|
|
955
|
+
.from(schema.computerSessions)
|
|
956
|
+
.where(
|
|
957
|
+
and(
|
|
958
|
+
eq(schema.computerSessions.workspaceId, input.workspaceId),
|
|
959
|
+
eq(schema.computerSessions.id, input.computerSessionId),
|
|
960
|
+
),
|
|
961
|
+
)
|
|
962
|
+
.limit(1);
|
|
963
|
+
if (!current) throw new ComputerSessionNotFoundError("ComputerSession not found");
|
|
964
|
+
if (current.controllerGeneration !== input.expectedControllerGeneration) {
|
|
965
|
+
throw new ComputerSessionOperationConflictError(
|
|
966
|
+
"ComputerSession end has a stale controller generation",
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
const now = new Date();
|
|
970
|
+
const [sessionRow] = await tx
|
|
971
|
+
.update(schema.computerSessions)
|
|
972
|
+
.set({
|
|
973
|
+
lifecycle: "ended",
|
|
974
|
+
controllerId: null,
|
|
975
|
+
controllerGeneration: null,
|
|
976
|
+
placementInstanceId: null,
|
|
977
|
+
controllerHeartbeatAt: null,
|
|
978
|
+
failureCode: null,
|
|
979
|
+
updatedAt: now,
|
|
980
|
+
})
|
|
981
|
+
.where(
|
|
982
|
+
and(
|
|
983
|
+
eq(schema.computerSessions.workspaceId, input.workspaceId),
|
|
984
|
+
eq(schema.computerSessions.id, input.computerSessionId),
|
|
985
|
+
),
|
|
986
|
+
)
|
|
987
|
+
.returning();
|
|
988
|
+
const [operationRow] = await tx
|
|
989
|
+
.update(schema.interactionOperations)
|
|
990
|
+
.set({
|
|
991
|
+
state: "completed",
|
|
992
|
+
controllerGeneration: input.expectedControllerGeneration,
|
|
993
|
+
dispatchedAt: operation.dispatchedAt ?? now,
|
|
994
|
+
settledAt: now,
|
|
995
|
+
updatedAt: now,
|
|
996
|
+
})
|
|
997
|
+
.where(
|
|
998
|
+
and(
|
|
999
|
+
eq(schema.interactionOperations.workspaceId, input.workspaceId),
|
|
1000
|
+
eq(schema.interactionOperations.operationId, input.operationId),
|
|
1001
|
+
),
|
|
1002
|
+
)
|
|
1003
|
+
.returning();
|
|
1004
|
+
if (!sessionRow || !operationRow) throw new Error("ComputerSession end completion was lost");
|
|
1005
|
+
const associations = await loadAssociations(tx, input.workspaceId, [input.computerSessionId]);
|
|
1006
|
+
await advanceWorkspaceInteractionRevision(tx, input.accountId, input.workspaceId);
|
|
1007
|
+
return ComputerSessionMutationResponse.parse({
|
|
1008
|
+
session: computerSessionFromRows(sessionRow, associations),
|
|
1009
|
+
operation: operationReceipt(operationRow, false),
|
|
1010
|
+
});
|
|
1011
|
+
}),
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
export async function touchComputerSessionController(
|
|
1016
|
+
db: Database,
|
|
1017
|
+
input: {
|
|
1018
|
+
accountId: string;
|
|
1019
|
+
workspaceId: string;
|
|
1020
|
+
computerSessionId: string;
|
|
1021
|
+
controllerGeneration: string;
|
|
1022
|
+
},
|
|
1023
|
+
): Promise<boolean> {
|
|
1024
|
+
return await withRlsContext(db, input, async (scopedDb) =>
|
|
1025
|
+
scopedDb.transaction(async (txRaw) => {
|
|
1026
|
+
const tx = txRaw as unknown as Database;
|
|
1027
|
+
const [observed] = await tx
|
|
1028
|
+
.select()
|
|
1029
|
+
.from(schema.computerSessions)
|
|
1030
|
+
.where(
|
|
1031
|
+
and(
|
|
1032
|
+
eq(schema.computerSessions.workspaceId, input.workspaceId),
|
|
1033
|
+
eq(schema.computerSessions.id, input.computerSessionId),
|
|
1034
|
+
eq(schema.computerSessions.lifecycle, "active"),
|
|
1035
|
+
eq(schema.computerSessions.controllerGeneration, input.controllerGeneration),
|
|
1036
|
+
),
|
|
1037
|
+
)
|
|
1038
|
+
.limit(1);
|
|
1039
|
+
if (!observed) return false;
|
|
1040
|
+
if (observed.placementKind === "sandbox_group") {
|
|
1041
|
+
const lease = await tx.execute<{ id: string }>(sql`
|
|
1042
|
+
select lease.id
|
|
1043
|
+
from sandbox_leases lease
|
|
1044
|
+
join computer_sessions computer
|
|
1045
|
+
on computer.account_id = lease.account_id
|
|
1046
|
+
and computer.workspace_id = lease.workspace_id
|
|
1047
|
+
and computer.sandbox_group_id = lease.sandbox_group_id
|
|
1048
|
+
where computer.account_id = ${input.accountId}
|
|
1049
|
+
and computer.workspace_id = ${input.workspaceId}
|
|
1050
|
+
and computer.id = ${input.computerSessionId}
|
|
1051
|
+
and computer.lifecycle = 'active'
|
|
1052
|
+
and computer.controller_generation = ${input.controllerGeneration}
|
|
1053
|
+
and computer.placement_instance_id = lease.instance_id
|
|
1054
|
+
for update of lease
|
|
1055
|
+
`);
|
|
1056
|
+
if (!lease[0]) return false;
|
|
1057
|
+
const holder = await tx.execute<{ id: string }>(sql`
|
|
1058
|
+
update sandbox_lease_holders holder set last_heartbeat_at = now()
|
|
1059
|
+
from sandbox_leases lease
|
|
1060
|
+
where lease.id = holder.lease_id
|
|
1061
|
+
and holder.account_id = ${input.accountId}
|
|
1062
|
+
and holder.workspace_id = ${input.workspaceId}
|
|
1063
|
+
and holder.kind = 'interaction'
|
|
1064
|
+
and holder.holder_id = ${`computer-session:${input.computerSessionId}`}
|
|
1065
|
+
and lease.workspace_id = ${input.workspaceId}
|
|
1066
|
+
and lease.sandbox_group_id = ${observed.sandboxGroupId}
|
|
1067
|
+
and lease.instance_id = ${observed.placementInstanceId}
|
|
1068
|
+
returning holder.id
|
|
1069
|
+
`);
|
|
1070
|
+
if (!holder[0]) return false;
|
|
1071
|
+
}
|
|
1072
|
+
await lockComputerSession(tx, input.workspaceId, input.computerSessionId);
|
|
1073
|
+
const now = new Date();
|
|
1074
|
+
const [updated] = await tx
|
|
1075
|
+
.update(schema.computerSessions)
|
|
1076
|
+
.set({ controllerHeartbeatAt: now, lastUsedAt: now, updatedAt: now })
|
|
1077
|
+
.where(
|
|
1078
|
+
and(
|
|
1079
|
+
eq(schema.computerSessions.workspaceId, input.workspaceId),
|
|
1080
|
+
eq(schema.computerSessions.id, input.computerSessionId),
|
|
1081
|
+
eq(schema.computerSessions.lifecycle, "active"),
|
|
1082
|
+
eq(schema.computerSessions.controllerGeneration, input.controllerGeneration),
|
|
1083
|
+
),
|
|
1084
|
+
)
|
|
1085
|
+
.returning({ id: schema.computerSessions.id });
|
|
1086
|
+
return updated !== undefined;
|
|
1087
|
+
}),
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
async function lockOperation(
|
|
1092
|
+
db: Database,
|
|
1093
|
+
workspaceId: string,
|
|
1094
|
+
operationId: string,
|
|
1095
|
+
): Promise<void> {
|
|
1096
|
+
await db.execute(sql`
|
|
1097
|
+
select operation_id from interaction_operations
|
|
1098
|
+
where workspace_id = ${workspaceId} and operation_id = ${operationId}
|
|
1099
|
+
for update
|
|
1100
|
+
`);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
async function lockComputerSession(
|
|
1104
|
+
db: Database,
|
|
1105
|
+
workspaceId: string,
|
|
1106
|
+
computerSessionId: string,
|
|
1107
|
+
): Promise<void> {
|
|
1108
|
+
await db.execute(sql`
|
|
1109
|
+
select id from computer_sessions
|
|
1110
|
+
where workspace_id = ${workspaceId} and id = ${computerSessionId}
|
|
1111
|
+
for update
|
|
1112
|
+
`);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
function assertOperationResource(
|
|
1116
|
+
operation: InteractionOperationRow | null,
|
|
1117
|
+
computerSessionId: string,
|
|
1118
|
+
kind?: InteractionLifecycleOperationKind,
|
|
1119
|
+
): asserts operation is InteractionOperationRow {
|
|
1120
|
+
if (!operation) throw new ComputerSessionNotFoundError("ComputerSession operation not found");
|
|
1121
|
+
if (
|
|
1122
|
+
operation.resourceKind !== "computer_session" ||
|
|
1123
|
+
operation.resourceId !== computerSessionId ||
|
|
1124
|
+
(kind !== undefined && operation.kind !== kind)
|
|
1125
|
+
) {
|
|
1126
|
+
throw new ComputerSessionOperationConflictError(
|
|
1127
|
+
"Interaction operation is bound to another resource",
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
}
|