@opengeni/api-router 0.30.3 → 2.2.0-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/dist/api-websocket.d.ts +18 -0
  2. package/dist/app.js +1 -1
  3. package/dist/auth/managed-auth.d.ts +6 -0
  4. package/dist/{chunk-WCXHI3FF.js → chunk-3TP54PPX.js} +26692 -15389
  5. package/dist/chunk-3TP54PPX.js.map +1 -0
  6. package/dist/codemode.d.ts +8 -1
  7. package/dist/company-brain-okf.d.ts +24 -0
  8. package/dist/connected-machine-computer-access.d.ts +11 -0
  9. package/dist/connection-authority-owner.d.ts +59 -0
  10. package/dist/connection-ownership.d.ts +47 -0
  11. package/dist/controller-data-plane.d.ts +16 -0
  12. package/dist/editable-artifact-websocket.d.ts +5 -15
  13. package/dist/editable-artifact-workspace-files.d.ts +1 -1
  14. package/dist/http/api-error.d.ts +10 -0
  15. package/dist/http/cors.d.ts +5 -0
  16. package/dist/http/interaction-control-error.d.ts +13 -0
  17. package/dist/http/sse.d.ts +2 -3
  18. package/dist/index.d.ts +5 -2
  19. package/dist/index.js +326 -82
  20. package/dist/index.js.map +1 -1
  21. package/dist/integrations/atlassian.d.ts +17 -0
  22. package/dist/integrations/fiken.d.ts +7 -2
  23. package/dist/integrations/google-drive.d.ts +18 -0
  24. package/dist/integrations/oauth-client.d.ts +22 -3
  25. package/dist/integrations/oauth-profiles.d.ts +195 -0
  26. package/dist/integrations/personal-github-repositories.d.ts +45 -0
  27. package/dist/integrations/personal-github.d.ts +40 -0
  28. package/dist/integrations/pr-review-provider.d.ts +22 -0
  29. package/dist/integrations/provider-oauth.d.ts +8 -0
  30. package/dist/integrations/slack-app-home.d.ts +24 -0
  31. package/dist/integrations/slack-bot.d.ts +157 -9
  32. package/dist/integrations/slack-interactions.d.ts +39 -5
  33. package/dist/integrations/slack-routing.d.ts +120 -0
  34. package/dist/integrations/social-oauth.d.ts +5 -0
  35. package/dist/interaction-frame-proxy.d.ts +68 -0
  36. package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
  37. package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
  38. package/dist/mcp/documents.d.ts +1 -0
  39. package/dist/mcp/remember.d.ts +28 -0
  40. package/dist/mcp/request-abort.d.ts +19 -0
  41. package/dist/mcp/scheduled-task-view.d.ts +4 -4
  42. package/dist/mcp/server.d.ts +15 -2
  43. package/dist/mcp/session-view.d.ts +4 -0
  44. package/dist/mcp/session-wait.d.ts +137 -0
  45. package/dist/routes/automations.d.ts +4 -0
  46. package/dist/routes/billing.d.ts +5 -0
  47. package/dist/routes/browser-sessions.d.ts +4 -1
  48. package/dist/routes/company-brain.d.ts +3 -0
  49. package/dist/routes/company-profile.d.ts +2 -1
  50. package/dist/routes/connection-authorities.d.ts +3 -0
  51. package/dist/routes/personal-github-git-broker.d.ts +29 -0
  52. package/dist/routes/personal-github.d.ts +3 -0
  53. package/dist/routes/pr-review.d.ts +3 -0
  54. package/dist/routes/sessions.d.ts +1 -0
  55. package/dist/routes/user-resource-authorities.d.ts +3 -0
  56. package/dist/routes/workspace-capture.d.ts +10 -2
  57. package/dist/routes/workspace-learning.d.ts +3 -0
  58. package/dist/routes/workspaces.d.ts +19 -0
  59. package/dist/sandbox/channel-a.d.ts +11 -1
  60. package/dist/sandbox/connection-authority.d.ts +3 -0
  61. package/dist/sandbox/enrollment.d.ts +2 -0
  62. package/dist/sandbox/machines.d.ts +2 -2
  63. package/dist/sandbox/metrics-ingestion.d.ts +10 -20
  64. package/dist/sandbox/viewer.d.ts +24 -1
  65. package/dist/sandbox-file-artifacts.d.ts +11 -0
  66. package/dist/scheduled-task-deletion.d.ts +14 -0
  67. package/dist/slack-reaction-files.d.ts +1 -1
  68. package/dist/temporal-schedule-cleanup.d.ts +1 -0
  69. package/package.json +20 -18
  70. package/src/api-websocket.ts +23 -0
  71. package/src/app.ts +161 -23
  72. package/src/auth/managed-auth.ts +25 -3
  73. package/src/codemode.ts +53 -23
  74. package/src/codex-realtime.ts +8 -2
  75. package/src/company-brain-okf.ts +340 -0
  76. package/src/connected-machine-computer-access.ts +33 -0
  77. package/src/connection-authority-owner.ts +61 -0
  78. package/src/connection-ownership.ts +180 -0
  79. package/src/controller-data-plane.ts +47 -0
  80. package/src/editable-artifact-native-kernel.ts +26 -15
  81. package/src/editable-artifact-office-import.ts +28 -3
  82. package/src/editable-artifact-production.ts +13 -7
  83. package/src/editable-artifact-websocket.ts +11 -18
  84. package/src/editable-artifact-workspace-files.ts +31 -6
  85. package/src/http/api-error.ts +28 -0
  86. package/src/http/auth.ts +4 -0
  87. package/src/http/cors.ts +35 -0
  88. package/src/http/interaction-control-error.ts +164 -0
  89. package/src/http/sse.ts +112 -66
  90. package/src/index.ts +46 -6
  91. package/src/integrations/atlassian.ts +146 -35
  92. package/src/integrations/fiken.ts +102 -22
  93. package/src/integrations/google-drive.ts +301 -92
  94. package/src/integrations/oauth-client.ts +272 -143
  95. package/src/integrations/oauth-profiles.ts +477 -0
  96. package/src/integrations/personal-github-repositories.ts +445 -0
  97. package/src/integrations/personal-github.ts +705 -0
  98. package/src/integrations/pr-review-provider.ts +246 -0
  99. package/src/integrations/provider-oauth.ts +121 -5
  100. package/src/integrations/slack-app-home.ts +300 -0
  101. package/src/integrations/slack-bot.ts +342 -57
  102. package/src/integrations/slack-interactions.ts +1898 -259
  103. package/src/integrations/slack-routing.ts +337 -0
  104. package/src/integrations/social-oauth.ts +25 -0
  105. package/src/interaction-frame-proxy.ts +409 -0
  106. package/src/mcp/company-brain-governed-writes.ts +262 -0
  107. package/src/mcp/company-profile-agent-admin.ts +205 -0
  108. package/src/mcp/documents.ts +37 -6
  109. package/src/mcp/files.ts +16 -1
  110. package/src/mcp/remember.ts +181 -0
  111. package/src/mcp/request-abort.ts +44 -0
  112. package/src/mcp/scheduled-task-view.ts +1 -0
  113. package/src/mcp/server.ts +719 -369
  114. package/src/mcp/session-view.ts +54 -0
  115. package/src/mcp/session-wait.ts +554 -0
  116. package/src/model-catalog.ts +20 -12
  117. package/src/routes/api-integrations.ts +4 -0
  118. package/src/routes/api-keys.ts +1 -0
  119. package/src/routes/automations.ts +534 -0
  120. package/src/routes/billing.ts +57 -1
  121. package/src/routes/browser-sessions.ts +329 -79
  122. package/src/routes/channels.ts +17 -1
  123. package/src/routes/codex.ts +50 -0
  124. package/src/routes/company-brain.ts +367 -0
  125. package/src/routes/company-profile.ts +1 -1
  126. package/src/routes/computer-sessions.ts +320 -93
  127. package/src/routes/connection-authorities.ts +139 -0
  128. package/src/routes/connections.ts +144 -34
  129. package/src/routes/documents.ts +353 -4
  130. package/src/routes/editable-artifacts.ts +11 -3
  131. package/src/routes/enrollments.ts +116 -26
  132. package/src/routes/environments.ts +127 -55
  133. package/src/routes/files.ts +72 -11
  134. package/src/routes/install.ts +82 -28
  135. package/src/routes/integration-facets.ts +29 -0
  136. package/src/routes/interaction-resources.ts +20 -2
  137. package/src/routes/machines.ts +275 -13
  138. package/src/routes/organization-memberships.ts +717 -28
  139. package/src/routes/packs.ts +4 -3
  140. package/src/routes/personal-github-git-broker.ts +785 -0
  141. package/src/routes/personal-github.ts +319 -0
  142. package/src/routes/pr-review.ts +531 -0
  143. package/src/routes/rigs.ts +104 -39
  144. package/src/routes/scheduled-tasks.ts +19 -21
  145. package/src/routes/sessions.ts +775 -38
  146. package/src/routes/social.ts +14 -2
  147. package/src/routes/supergrok.ts +23 -4
  148. package/src/routes/transcription-recordings.ts +8 -2
  149. package/src/routes/user-resource-authorities.ts +138 -0
  150. package/src/routes/workspace-artifacts.ts +4 -1
  151. package/src/routes/workspace-capture.ts +21 -0
  152. package/src/routes/workspace-learning.ts +228 -0
  153. package/src/routes/workspaces.ts +59 -6
  154. package/src/sandbox/auth-callout.ts +38 -12
  155. package/src/sandbox/channel-a.ts +242 -19
  156. package/src/sandbox/connection-authority.ts +3 -0
  157. package/src/sandbox/enrollment.ts +15 -3
  158. package/src/sandbox/machines.ts +186 -64
  159. package/src/sandbox/metrics-ingestion.ts +220 -58
  160. package/src/sandbox/viewer.ts +243 -19
  161. package/src/sandbox-file-artifacts.ts +329 -0
  162. package/src/scheduled-task-deletion.ts +127 -0
  163. package/src/slack-reaction-files.ts +16 -5
  164. package/src/temporal-schedule-cleanup.ts +13 -0
  165. package/dist/chunk-WCXHI3FF.js.map +0 -1
package/src/http/cors.ts CHANGED
@@ -1,3 +1,38 @@
1
+ import { HTTPException } from "hono/http-exception";
2
+
1
3
  export function allowedCorsOrigin(pattern: string, origin: string): boolean {
2
4
  return new RegExp(`^(?:${pattern})$`).test(origin);
3
5
  }
6
+
7
+ export function validateInteractionRequestOrigin(
8
+ value: string | undefined,
9
+ input: {
10
+ corsAllowOriginRegex: string;
11
+ publicBaseUrl?: string | undefined;
12
+ webBaseUrl?: string | undefined;
13
+ },
14
+ ): string | null {
15
+ if (!value) return null;
16
+ let url: URL;
17
+ try {
18
+ url = new URL(value);
19
+ } catch {
20
+ throw new HTTPException(400, { message: "invalid request origin" });
21
+ }
22
+ if (
23
+ url.origin === "null" ||
24
+ (url.protocol !== "http:" && url.protocol !== "https:") ||
25
+ url.origin !== value
26
+ ) {
27
+ throw new HTTPException(400, { message: "invalid request origin" });
28
+ }
29
+ if (
30
+ allowedCorsOrigin(input.corsAllowOriginRegex, url.origin) ||
31
+ [input.publicBaseUrl, input.webBaseUrl].some((baseUrl) =>
32
+ baseUrl ? new URL(baseUrl).origin === url.origin : false,
33
+ )
34
+ ) {
35
+ return url.origin;
36
+ }
37
+ throw new HTTPException(403, { message: "request origin is not allowed" });
38
+ }
@@ -0,0 +1,164 @@
1
+ import { ErrorCode as AgentErrorCode } from "@opengeni/agent-proto";
2
+ import {
3
+ InteractionControlFailureDetails,
4
+ type ErrorCode,
5
+ type InteractionControlFailureCode,
6
+ } from "@opengeni/contracts";
7
+ import { SelfhostedControlError } from "@opengeni/runtime/sandbox";
8
+ import { HTTPException } from "hono/http-exception";
9
+ import { ApiHttpError } from "./api-error";
10
+
11
+ export type InteractionControlSurface = "browser" | "computer";
12
+
13
+ /**
14
+ * Projects an internal connected-machine control fault into the public API
15
+ * vocabulary. This is deliberately an allowlist: the agent's free-form message
16
+ * and detail map may contain paths, process output, or provider diagnostics and
17
+ * never cross this boundary. The exact inner request id is safe opaque
18
+ * correlation and links API logs back to the machine operation.
19
+ */
20
+ export function interactionControlApiError(
21
+ error: unknown,
22
+ surface: InteractionControlSurface,
23
+ ): ApiHttpError | null {
24
+ const controlError =
25
+ error instanceof SelfhostedControlError
26
+ ? error
27
+ : error instanceof HTTPException && error.cause instanceof SelfhostedControlError
28
+ ? error.cause
29
+ : null;
30
+ if (!controlError) return null;
31
+ const controlFailureCode = publicControlFailureCode(controlError.code);
32
+ const details = InteractionControlFailureDetails.parse({
33
+ interactionLayer: "connected_machine",
34
+ interactionSurface: surface,
35
+ controlFailureCode,
36
+ ...(safeControlRequestId(controlError.controlRequestId)
37
+ ? { controlRequestId: controlError.controlRequestId }
38
+ : {}),
39
+ });
40
+ const ruling = publicControlFailureRuling(controlError, surface, controlFailureCode);
41
+ return new ApiHttpError(ruling.status, {
42
+ code: ruling.code,
43
+ message: ruling.message,
44
+ retryable: controlError.retryable,
45
+ outcomeUnknown:
46
+ controlError.code === AgentErrorCode.ERROR_CODE_TIMEOUT && !controlError.neverSent,
47
+ details,
48
+ });
49
+ }
50
+
51
+ export function publicControlFailureCode(code: AgentErrorCode): InteractionControlFailureCode {
52
+ switch (code) {
53
+ case AgentErrorCode.ERROR_CODE_UNSUPPORTED:
54
+ return "unsupported";
55
+ case AgentErrorCode.ERROR_CODE_OS:
56
+ return "os";
57
+ case AgentErrorCode.ERROR_CODE_NOT_FOUND:
58
+ return "not_found";
59
+ case AgentErrorCode.ERROR_CODE_CONSENT_REQUIRED:
60
+ return "consent_required";
61
+ case AgentErrorCode.ERROR_CODE_TIMEOUT:
62
+ return "timeout";
63
+ case AgentErrorCode.ERROR_CODE_DRAINING:
64
+ return "draining";
65
+ case AgentErrorCode.ERROR_CODE_PROTOCOL:
66
+ return "protocol";
67
+ case AgentErrorCode.ERROR_CODE_STREAM:
68
+ return "stream";
69
+ case AgentErrorCode.ERROR_CODE_AGENT_OFFLINE:
70
+ return "agent_offline";
71
+ case AgentErrorCode.ERROR_CODE_FENCED:
72
+ return "fenced";
73
+ case AgentErrorCode.ERROR_CODE_PAYLOAD_TOO_LARGE:
74
+ return "payload_too_large";
75
+ default:
76
+ return "unknown";
77
+ }
78
+ }
79
+
80
+ function publicControlFailureRuling(
81
+ error: SelfhostedControlError,
82
+ surface: InteractionControlSurface,
83
+ code: InteractionControlFailureCode,
84
+ ): { status: number; code: ErrorCode; message: string } {
85
+ const liveView = surface === "browser" ? "browser live view" : "computer live view";
86
+ switch (code) {
87
+ case "consent_required":
88
+ return {
89
+ status: 403,
90
+ code: "forbidden",
91
+ message: `Screen access is not enabled for this connected machine's ${liveView}.`,
92
+ };
93
+ case "unsupported":
94
+ return {
95
+ status: 409,
96
+ code: "conflict",
97
+ message: `This connected machine does not support the ${liveView}.`,
98
+ };
99
+ case "not_found":
100
+ return {
101
+ status: 404,
102
+ code: "not_found",
103
+ message: `The connected-machine resource for this ${liveView} no longer exists.`,
104
+ };
105
+ case "fenced":
106
+ return {
107
+ status: 409,
108
+ code: "conflict",
109
+ message: `The connected-machine ${liveView} changed while it was opening.`,
110
+ };
111
+ case "agent_offline":
112
+ return {
113
+ status: 503,
114
+ code: "upstream_unavailable",
115
+ message: `The connected machine is offline, so its ${liveView} is unavailable.`,
116
+ };
117
+ case "draining":
118
+ return {
119
+ status: 503,
120
+ code: "upstream_unavailable",
121
+ message: `The connected machine is busy and could not open the ${liveView}.`,
122
+ };
123
+ case "timeout":
124
+ return {
125
+ status: 504,
126
+ code: "upstream_unavailable",
127
+ message: `The connected machine did not finish opening the ${liveView} in time.`,
128
+ };
129
+ case "payload_too_large":
130
+ return {
131
+ status: 502,
132
+ code: "upstream_unavailable",
133
+ message: `The connected machine could not deliver the ${liveView} within its transport limit.`,
134
+ };
135
+ case "stream":
136
+ return {
137
+ status: error.retryable ? 503 : 502,
138
+ code: "upstream_unavailable",
139
+ message: `The connected machine could not open the ${liveView} stream.`,
140
+ };
141
+ case "protocol":
142
+ return {
143
+ status: 502,
144
+ code: "upstream_unavailable",
145
+ message: `The connected machine returned an invalid ${liveView} response.`,
146
+ };
147
+ case "os":
148
+ return {
149
+ status: 502,
150
+ code: "upstream_unavailable",
151
+ message: `The connected machine could not prepare the ${liveView}.`,
152
+ };
153
+ case "unknown":
154
+ return {
155
+ status: 502,
156
+ code: "upstream_unavailable",
157
+ message: `The connected machine could not open the ${liveView}.`,
158
+ };
159
+ }
160
+ }
161
+
162
+ function safeControlRequestId(value: string | null): value is string {
163
+ return value !== null && value.length <= 128 && /^[A-Za-z0-9._:-]+$/u.test(value);
164
+ }
package/src/http/sse.ts CHANGED
@@ -10,9 +10,11 @@ import {
10
10
  type Database,
11
11
  } from "@opengeni/db";
12
12
  import {
13
+ coalesceSessionEventDeltas,
13
14
  formatSessionEventSse,
14
15
  formatWorkspaceControlEventSse,
15
16
  SESSION_EVENT_SSE_FRAME_MAX_BYTES,
17
+ sessionEventResumeSequence,
16
18
  type EventBus,
17
19
  } from "@opengeni/events";
18
20
  import type { Observability } from "@opengeni/observability";
@@ -268,31 +270,19 @@ export async function sseSessionStream(
268
270
  options: SessionSseDeliveryOptions = {},
269
271
  ): Promise<Response> {
270
272
  const heartbeatIntervalMs = resolveHeartbeatInterval(options.heartbeatIntervalMs);
271
- if (
272
- options.reauthorize &&
273
- options.reauthorizeAfterMs !== undefined &&
274
- (!Number.isSafeInteger(options.reauthorizeAfterMs) ||
275
- options.reauthorizeAfterMs < 1_000 ||
276
- options.reauthorizeAfterMs > 60_000)
277
- ) {
278
- throw new RangeError("session stream reauthorization must be between 1000 and 60000ms");
279
- }
280
273
  let lastSent = after;
281
274
  let bootstrapping = true;
282
275
  let newestBuffered: SessionEvent | null = null;
283
276
  let unsubscribe: (() => void) | null = null;
284
277
  let delivery: LatestWinsDelivery<SessionEvent> | null = null;
285
- let reauthorizationTimer: ReturnType<typeof setTimeout> | null = null;
278
+ let stopReauthorization = () => {};
286
279
  let heartbeatTimer: ReturnType<typeof setTimeout> | null = null;
287
280
  let detachAbortListener = () => {};
288
281
  let closeMetrics = () => {};
289
282
  const stopUpstream = () => {
290
283
  closeMetrics();
291
284
  detachAbortListener();
292
- if (reauthorizationTimer) {
293
- clearTimeout(reauthorizationTimer);
294
- reauthorizationTimer = null;
295
- }
285
+ stopReauthorization();
296
286
  if (heartbeatTimer) {
297
287
  clearTimeout(heartbeatTimer);
298
288
  heartbeatTimer = null;
@@ -313,20 +303,14 @@ export async function sseSessionStream(
313
303
  const fail = (error: unknown) => {
314
304
  channel.fail(retryableSseFailure("session event stream delivery failed", error));
315
305
  };
316
- const scheduleReauthorization = () => {
317
- if (!options.reauthorize || channel.stopped()) return;
318
- const interval = options.reauthorizeAfterMs ?? 15_000;
319
- reauthorizationTimer = setTimeout(() => {
320
- reauthorizationTimer = null;
321
- void options.reauthorize!()
322
- .then(scheduleReauthorization)
323
- .catch((error) => fail(error));
324
- }, interval);
325
- };
326
- scheduleReauthorization();
306
+ stopReauthorization = startSseReauthorization(options, channel.stopped, fail);
327
307
  let writeTail = Promise.resolve();
328
308
  const writeFrame = (frame: string): Promise<void> => {
329
309
  const write = writeTail.then(async () => {
310
+ // A periodic check closes an idle stream, while this exact pre-delivery
311
+ // check prevents a buffered/replayed event from crossing a revocation
312
+ // boundary merely because its timer has not fired yet.
313
+ await options.reauthorize?.();
330
314
  if (!(await channel.write(frame))) throw new SseStreamStoppedError();
331
315
  });
332
316
  writeTail = write.catch(() => {});
@@ -343,36 +327,47 @@ export async function sseSessionStream(
343
327
  });
344
328
  }, heartbeatIntervalMs);
345
329
  };
346
- const send = async (event: SessionEvent) => {
347
- if (event.sequence <= lastSent) return;
348
- if (event.sequence > lastSent + 1) {
349
- while (lastSent + 1 < event.sequence) {
350
- const previousLastSent = lastSent;
351
- const missing = await listSessionEvents(db, workspaceId, sessionId, {
352
- after: lastSent,
353
- limit: Math.min(SESSION_REPLAY_PAGE_SIZE, event.sequence - lastSent - 1),
354
- });
355
- if (missing.length === 0) {
356
- throw new Error(
357
- `Session event replay stalled before sequence ${event.sequence}; last sent ${lastSent}`,
358
- );
359
- }
360
- for (const missed of missing) {
361
- if (missed.sequence >= event.sequence) break;
362
- if (missed.sequence > lastSent) {
363
- await writeFrame(formatSessionEventSse(missed));
364
- lastSent = missed.sequence;
365
- }
366
- }
367
- if (lastSent === previousLastSent) {
368
- throw new Error(
369
- `Session event replay made no progress before sequence ${event.sequence}; last sent ${lastSent}`,
370
- );
371
- }
330
+ const deliverDurableThrough = async (targetSequence?: number) => {
331
+ while (true) {
332
+ if (targetSequence !== undefined && lastSent >= targetSequence) return;
333
+ const previousLastSent = lastSent;
334
+ const limit =
335
+ targetSequence === undefined
336
+ ? SESSION_REPLAY_PAGE_SIZE
337
+ : Math.min(SESSION_REPLAY_PAGE_SIZE, targetSequence - lastSent);
338
+ const page = await listSessionEvents(db, workspaceId, sessionId, {
339
+ after: lastSent,
340
+ limit,
341
+ });
342
+ const eligible =
343
+ targetSequence === undefined
344
+ ? page
345
+ : page.filter((event) => event.sequence <= targetSequence);
346
+ if (eligible.length === 0) {
347
+ if (targetSequence === undefined) return;
348
+ throw new Error(
349
+ `Session event replay stalled before sequence ${targetSequence}; last sent ${lastSent}`,
350
+ );
351
+ }
352
+ // The durable audit log remains exact. The browser transport combines
353
+ // adjacent text deltas into bounded frames carrying `coalescedUntil`, so
354
+ // a long answer cannot create thousands of React renders and starve
355
+ // command acknowledgements behind its own token stream.
356
+ for (const projected of coalesceSessionEventDeltas(eligible)) {
357
+ await writeFrame(formatSessionEventSse(projected));
358
+ lastSent = sessionEventResumeSequence(projected);
372
359
  }
360
+ if (lastSent <= previousLastSent) {
361
+ throw new Error(`Session event replay made no progress after sequence ${lastSent}`);
362
+ }
363
+ if (targetSequence !== undefined && lastSent >= targetSequence) return;
364
+ if (targetSequence === undefined && page.length < limit) return;
373
365
  }
374
- await writeFrame(formatSessionEventSse(event));
375
- lastSent = event.sequence;
366
+ };
367
+ const send = async (event: SessionEvent) => {
368
+ const targetSequence = sessionEventResumeSequence(event);
369
+ if (targetSequence <= lastSent) return;
370
+ await deliverDurableThrough(targetSequence);
376
371
  };
377
372
  delivery = createLatestWinsDelivery(send, fail);
378
373
 
@@ -394,12 +389,7 @@ export async function sseSessionStream(
394
389
  }
395
390
  unsubscribe = release;
396
391
 
397
- await replaySessionEvents(
398
- (cursor, limit) => listSessionEvents(db, workspaceId, sessionId, cursor, limit),
399
- send,
400
- after,
401
- SESSION_REPLAY_PAGE_SIZE,
402
- );
392
+ await deliverDurableThrough();
403
393
  await writeFrame(": connected\n\n");
404
394
  scheduleHeartbeat();
405
395
  bootstrapping = false;
@@ -471,12 +461,14 @@ export async function sseWorkspaceControlStream(
471
461
  let newestBuffered: WorkspaceControlEvent | null = null;
472
462
  let unsubscribe: (() => void) | null = null;
473
463
  let delivery: LatestWinsDelivery<WorkspaceControlEvent> | null = null;
464
+ let stopReauthorization = () => {};
474
465
  let heartbeatTimer: ReturnType<typeof setTimeout> | null = null;
475
466
  let detachAbortListener = () => {};
476
467
  let closeMetrics = () => {};
477
468
  const stopUpstream = () => {
478
469
  closeMetrics();
479
470
  detachAbortListener();
471
+ stopReauthorization();
480
472
  if (heartbeatTimer) {
481
473
  clearTimeout(heartbeatTimer);
482
474
  heartbeatTimer = null;
@@ -497,9 +489,11 @@ export async function sseWorkspaceControlStream(
497
489
  const fail = (error: unknown) => {
498
490
  channel.fail(retryableSseFailure("workspace control stream delivery failed", error));
499
491
  };
492
+ stopReauthorization = startSseReauthorization(options, channel.stopped, fail);
500
493
  let writeTail = Promise.resolve();
501
494
  const writeFrame = (frame: string): Promise<void> => {
502
495
  const write = writeTail.then(async () => {
496
+ await options.reauthorize?.();
503
497
  if (!(await channel.write(frame))) throw new SseStreamStoppedError();
504
498
  });
505
499
  writeTail = write.catch(() => {});
@@ -619,11 +613,13 @@ export async function sseWorkspaceLiveStream(
619
613
  options: WorkspaceInteractionSseOptions = {},
620
614
  ): Promise<Response> {
621
615
  const upstream = new AbortController();
616
+ let stopReauthorization = () => {};
622
617
  const channel = createByteBoundedSseStream({
623
618
  maxQueuedBytes: options.maxQueuedBytes ?? SESSION_EVENT_SSE_FRAME_MAX_BYTES,
624
619
  ...(options.stallTimeoutMs === undefined ? {} : { stallTimeoutMs: options.stallTimeoutMs }),
625
620
  onObservation: sseObservationReporter("workspace_interaction", options),
626
621
  onStop: () => {
622
+ stopReauthorization();
627
623
  signal.removeEventListener("abort", abort);
628
624
  upstream.abort();
629
625
  },
@@ -634,10 +630,22 @@ export async function sseWorkspaceLiveStream(
634
630
  };
635
631
  if (signal.aborted) abort();
636
632
  else signal.addEventListener("abort", abort, { once: true });
633
+ stopReauthorization = startSseReauthorization(options, channel.stopped, (error) => {
634
+ channel.fail(retryableSseFailure("workspace live stream authorization failed", error));
635
+ });
636
+
637
+ const upstreamOptions: WorkspaceInteractionSseOptions = {
638
+ ...options,
639
+ reauthorize: undefined,
640
+ reauthorizeAfterMs: undefined,
641
+ };
637
642
 
638
643
  let writeTail = Promise.resolve(true);
639
644
  const write = (frame: string): Promise<boolean> => {
640
- const pending = writeTail.then(() => channel.write(frame));
645
+ const pending = writeTail.then(async () => {
646
+ await options.reauthorize?.();
647
+ return await channel.write(frame);
648
+ });
641
649
  writeTail = pending.catch(() => false);
642
650
  return pending;
643
651
  };
@@ -665,14 +673,21 @@ export async function sseWorkspaceLiveStream(
665
673
  void (async () => {
666
674
  try {
667
675
  const [control, interaction] = await Promise.all([
668
- sseWorkspaceControlStream(db, bus, workspaceId, controlAfter, upstream.signal, options),
676
+ sseWorkspaceControlStream(
677
+ db,
678
+ bus,
679
+ workspaceId,
680
+ controlAfter,
681
+ upstream.signal,
682
+ upstreamOptions,
683
+ ),
669
684
  sseWorkspaceInteractionRevisionStream(
670
685
  db,
671
686
  accountId,
672
687
  workspaceId,
673
688
  interactionAfter,
674
689
  upstream.signal,
675
- options,
690
+ upstreamOptions,
676
691
  ),
677
692
  ]);
678
693
  const pumps = [pump(control), pump(interaction)];
@@ -721,6 +736,7 @@ export async function sseWorkspaceInteractionRevisionStream(
721
736
  let lastSent = after;
722
737
  let lastWriteAt = Date.now();
723
738
  let stopRequested = false;
739
+ let stopReauthorization = () => {};
724
740
  let detachAbortListener = () => {};
725
741
  let closeMetrics = () => {};
726
742
  const channel = createByteBoundedSseStream({
@@ -729,13 +745,18 @@ export async function sseWorkspaceInteractionRevisionStream(
729
745
  onObservation: sseObservationReporter("workspace_interaction", options),
730
746
  onStop: () => {
731
747
  stopRequested = true;
748
+ stopReauthorization();
732
749
  closeMetrics();
733
750
  detachAbortListener();
734
751
  },
735
752
  });
736
753
  closeMetrics = observeSseConnection("workspace_interaction", after, options.observability);
754
+ stopReauthorization = startSseReauthorization(options, channel.stopped, (error) => {
755
+ channel.fail(retryableSseFailure("workspace interaction stream authorization failed", error));
756
+ });
737
757
 
738
758
  const write = async (frame: string): Promise<boolean> => {
759
+ await options.reauthorize?.();
739
760
  const accepted = await channel.write(frame);
740
761
  if (accepted) lastWriteAt = Date.now();
741
762
  return accepted;
@@ -849,14 +870,13 @@ export type SseDeliveryOptions = {
849
870
  heartbeatIntervalMs?: number;
850
871
  observability?: Observability | undefined;
851
872
  onObservation?: ((observation: SseDeliveryBoundObservation) => void) | undefined;
852
- };
853
-
854
- export type SessionSseDeliveryOptions = SseDeliveryOptions & {
855
- /** Host ACL re-check, run even while the event stream is otherwise idle. */
873
+ /** Current ACL re-check, run even while the event stream is idle. */
856
874
  reauthorize?: (() => Promise<void>) | undefined;
857
875
  reauthorizeAfterMs?: number | undefined;
858
876
  };
859
877
 
878
+ export type SessionSseDeliveryOptions = SseDeliveryOptions;
879
+
860
880
  function sseObservationReporter(
861
881
  stream: SseStreamKind,
862
882
  options: SseDeliveryOptions,
@@ -900,6 +920,32 @@ function resolveInteractionPollInterval(value: number | undefined): number {
900
920
  return interval;
901
921
  }
902
922
 
923
+ function startSseReauthorization(
924
+ options: SseDeliveryOptions,
925
+ stopped: () => boolean,
926
+ fail: (error: unknown) => void,
927
+ ): () => void {
928
+ if (!options.reauthorize) return () => {};
929
+ const interval = options.reauthorizeAfterMs ?? 15_000;
930
+ if (!Number.isSafeInteger(interval) || interval < 1_000 || interval > 60_000) {
931
+ throw new RangeError("SSE reauthorization must be between 1000 and 60000ms");
932
+ }
933
+ let timer: ReturnType<typeof setTimeout> | null = null;
934
+ const schedule = () => {
935
+ if (stopped()) return;
936
+ timer = setTimeout(() => {
937
+ timer = null;
938
+ void options.reauthorize!().then(schedule).catch(fail);
939
+ }, interval);
940
+ };
941
+ schedule();
942
+ return () => {
943
+ if (!timer) return;
944
+ clearTimeout(timer);
945
+ timer = null;
946
+ };
947
+ }
948
+
903
949
  function formatWorkspaceInteractionRevisionSse(event: WorkspaceInteractionRevisionEvent): string {
904
950
  return `id: ${event.sequence}\nevent: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`;
905
951
  }
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  dbSearchPath,
3
3
  getSettings,
4
+ resolveFirstPartyDelegationSecret,
4
5
  resolveNatsCalloutConfig,
5
6
  resolveNatsControlPlaneAuth,
6
7
  retryStartupDependency,
@@ -39,11 +40,13 @@ import { startHelloIngestion, startMetricsIngestion } from "./sandbox/metrics-in
39
40
  import { startSlackInteractionPump } from "./integrations/slack-interactions";
40
41
  import { startMemorySlackPublicationPump } from "./memory-slack-delivery";
41
42
  import { startTemporalScheduleCleanupPump } from "./temporal-schedule-cleanup";
43
+ import { cleanupScheduledTaskConnectorAuthorization } from "./scheduled-task-deletion";
42
44
  import {
43
45
  EDITABLE_ARTIFACT_LIVE_WEBSOCKET_MAX_MESSAGE_BYTES,
44
46
  EditableArtifactWebSocketTransport,
45
- type EditableArtifactWebSocketConnection,
46
47
  } from "./editable-artifact-websocket";
48
+ import type { ApiWebSocketConnection } from "./api-websocket";
49
+ import { InteractionFrameProxyTransport } from "./interaction-frame-proxy";
47
50
  import {
48
51
  createStandaloneEditableArtifactApplication,
49
52
  type StandaloneEditableArtifactApplication,
@@ -88,6 +91,19 @@ export async function createTemporalWorkflowClient(
88
91
  namespace: settings.temporalNamespace,
89
92
  });
90
93
  const client: SessionWorkflowClient = {
94
+ triggerAutomationRun: async ({ accountId, workspaceId, runId }) => {
95
+ try {
96
+ await temporal.workflow.start("automationRunWorkflow", {
97
+ taskQueue: settings.temporalTaskQueue,
98
+ workflowId: `automation-run:${runId}`,
99
+ workflowIdReusePolicy: "REJECT_DUPLICATE",
100
+ args: [{ accountId, workspaceId, runId }],
101
+ });
102
+ } catch (error) {
103
+ if (isWorkflowAlreadyStarted(error)) return;
104
+ throw error;
105
+ }
106
+ },
91
107
  signalUserMessage: async ({ eventId, workflowId }) => {
92
108
  await temporal.workflow.getHandle(workflowId).signal("userMessage", eventId);
93
109
  },
@@ -291,6 +307,7 @@ export async function startApi() {
291
307
  // strategy.
292
308
  const searchPath = dbSearchPath(settings);
293
309
  const dbClient = createDb(settings.databaseUrl, {
310
+ max: 32,
294
311
  ...(searchPath ? { searchPath } : {}),
295
312
  rlsStrategy: settings.rlsStrategy,
296
313
  });
@@ -303,12 +320,27 @@ export async function startApi() {
303
320
  rlsStrategy: settings.rlsStrategy,
304
321
  expectedRole: settings.runtimeDatabaseRole,
305
322
  targetSchema: settings.dbSchema.trim() || "public",
323
+ organizationTenancyCanonicalActivationEnabled:
324
+ settings.organizationTenancyCanonicalActivationEnabled,
306
325
  } as const;
307
326
  // The PRIVILEGED control-plane NATS login (M-AUTH): when the server runs with
308
327
  // auth_callout, api/worker authenticate as a static account user permitted to
309
- // request `agent.*.rpc`. Null in local dev (anonymous connect — the bus default).
328
+ // request exact generation-fenced agent RPC subjects. Null in local dev
329
+ // (anonymous connect — the bus default).
310
330
  const controlPlaneAuth = resolveNatsControlPlaneAuth(settings);
311
331
  try {
332
+ // Browser, Computer, Terminal, and Files can cold-create the home sandbox
333
+ // directly from this API process. Resolve the same private-registry Modal
334
+ // image before the API becomes reachable so the first human interaction
335
+ // does not pay the provider import/build latency. This mirrors the turn
336
+ // worker's startup boundary and is a no-op for provider-native image IDs,
337
+ // public images, and non-Modal backends.
338
+ const { ensureModalRegistryImage } = await import("@opengeni/runtime/sandbox");
339
+ await retryStartupDependency(
340
+ "Modal private-registry image",
341
+ () => ensureModalRegistryImage(settings),
342
+ { ...retryOptions, onRetry },
343
+ );
312
344
  await retryStartupDependency(
313
345
  "PostgreSQL runtime posture",
314
346
  () => assertRuntimeDatabasePosture(dbClient.db, databasePosture),
@@ -386,11 +418,17 @@ export async function startApi() {
386
418
  });
387
419
  }
388
420
  const artifactWebSockets = new EditableArtifactWebSocketTransport(routeDeps.editableArtifacts);
389
- const server = Bun.serve<EditableArtifactWebSocketConnection>({
421
+ const interactionFrameProxies = new InteractionFrameProxyTransport(
422
+ resolveFirstPartyDelegationSecret(settings),
423
+ );
424
+ const server = Bun.serve<ApiWebSocketConnection>({
390
425
  hostname: settings.apiHost,
391
426
  port: settings.apiPort,
392
427
  idleTimeout: 255,
393
428
  fetch: (request, bunServer) => {
429
+ if (interactionFrameProxies.handles(request)) {
430
+ return interactionFrameProxies.upgrade(request, bunServer);
431
+ }
394
432
  if (artifactWebSockets.handles(request)) {
395
433
  return artifactWebSockets.upgrade(request, bunServer);
396
434
  }
@@ -400,9 +438,9 @@ export async function startApi() {
400
438
  maxPayloadLength: EDITABLE_ARTIFACT_LIVE_WEBSOCKET_MAX_MESSAGE_BYTES,
401
439
  backpressureLimit: 16 * 1024 * 1024,
402
440
  closeOnBackpressureLimit: true,
403
- open: (socket) => artifactWebSockets.websocket.open(socket),
404
- message: (socket, message) => artifactWebSockets.websocket.message(socket, message),
405
- close: (socket) => artifactWebSockets.websocket.close(socket),
441
+ open: (socket) => socket.data.attach(socket),
442
+ message: (socket, message) => socket.data.receive(message),
443
+ close: (socket) => socket.data.transportClosed(),
406
444
  },
407
445
  });
408
446
  const stopSlackInteractionPump = settings.slackSigningSecret
@@ -411,6 +449,8 @@ export async function startApi() {
411
449
  const stopMemorySlackPublicationPump = startMemorySlackPublicationPump(routeDeps);
412
450
  const stopTemporalScheduleCleanupPump = startTemporalScheduleCleanupPump({
413
451
  db: dbClient.db,
452
+ cleanupConnectorAuthorization: async (claim) =>
453
+ await cleanupScheduledTaskConnectorAuthorization(routeDeps, claim),
414
454
  deleteSchedule: async (temporalScheduleId) => {
415
455
  await workflowClient.client.deleteScheduledTaskSchedule({ temporalScheduleId });
416
456
  },