@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.
- package/dist/api-websocket.d.ts +18 -0
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth.d.ts +6 -0
- package/dist/{chunk-WCXHI3FF.js → chunk-3TP54PPX.js} +26692 -15389
- package/dist/chunk-3TP54PPX.js.map +1 -0
- package/dist/codemode.d.ts +8 -1
- package/dist/company-brain-okf.d.ts +24 -0
- package/dist/connected-machine-computer-access.d.ts +11 -0
- package/dist/connection-authority-owner.d.ts +59 -0
- package/dist/connection-ownership.d.ts +47 -0
- package/dist/controller-data-plane.d.ts +16 -0
- package/dist/editable-artifact-websocket.d.ts +5 -15
- package/dist/editable-artifact-workspace-files.d.ts +1 -1
- package/dist/http/api-error.d.ts +10 -0
- package/dist/http/cors.d.ts +5 -0
- package/dist/http/interaction-control-error.d.ts +13 -0
- package/dist/http/sse.d.ts +2 -3
- package/dist/index.d.ts +5 -2
- package/dist/index.js +326 -82
- package/dist/index.js.map +1 -1
- package/dist/integrations/atlassian.d.ts +17 -0
- package/dist/integrations/fiken.d.ts +7 -2
- package/dist/integrations/google-drive.d.ts +18 -0
- package/dist/integrations/oauth-client.d.ts +22 -3
- package/dist/integrations/oauth-profiles.d.ts +195 -0
- package/dist/integrations/personal-github-repositories.d.ts +45 -0
- package/dist/integrations/personal-github.d.ts +40 -0
- package/dist/integrations/pr-review-provider.d.ts +22 -0
- package/dist/integrations/provider-oauth.d.ts +8 -0
- package/dist/integrations/slack-app-home.d.ts +24 -0
- package/dist/integrations/slack-bot.d.ts +157 -9
- package/dist/integrations/slack-interactions.d.ts +39 -5
- package/dist/integrations/slack-routing.d.ts +120 -0
- package/dist/integrations/social-oauth.d.ts +5 -0
- package/dist/interaction-frame-proxy.d.ts +68 -0
- package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
- package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
- package/dist/mcp/documents.d.ts +1 -0
- package/dist/mcp/remember.d.ts +28 -0
- package/dist/mcp/request-abort.d.ts +19 -0
- package/dist/mcp/scheduled-task-view.d.ts +4 -4
- package/dist/mcp/server.d.ts +15 -2
- package/dist/mcp/session-view.d.ts +4 -0
- package/dist/mcp/session-wait.d.ts +137 -0
- package/dist/routes/automations.d.ts +4 -0
- package/dist/routes/billing.d.ts +5 -0
- package/dist/routes/browser-sessions.d.ts +4 -1
- package/dist/routes/company-brain.d.ts +3 -0
- package/dist/routes/company-profile.d.ts +2 -1
- package/dist/routes/connection-authorities.d.ts +3 -0
- package/dist/routes/personal-github-git-broker.d.ts +29 -0
- package/dist/routes/personal-github.d.ts +3 -0
- package/dist/routes/pr-review.d.ts +3 -0
- package/dist/routes/sessions.d.ts +1 -0
- package/dist/routes/user-resource-authorities.d.ts +3 -0
- package/dist/routes/workspace-capture.d.ts +10 -2
- package/dist/routes/workspace-learning.d.ts +3 -0
- package/dist/routes/workspaces.d.ts +19 -0
- package/dist/sandbox/channel-a.d.ts +11 -1
- package/dist/sandbox/connection-authority.d.ts +3 -0
- package/dist/sandbox/enrollment.d.ts +2 -0
- package/dist/sandbox/machines.d.ts +2 -2
- package/dist/sandbox/metrics-ingestion.d.ts +10 -20
- package/dist/sandbox/viewer.d.ts +24 -1
- package/dist/sandbox-file-artifacts.d.ts +11 -0
- package/dist/scheduled-task-deletion.d.ts +14 -0
- package/dist/slack-reaction-files.d.ts +1 -1
- package/dist/temporal-schedule-cleanup.d.ts +1 -0
- package/package.json +20 -18
- package/src/api-websocket.ts +23 -0
- package/src/app.ts +161 -23
- package/src/auth/managed-auth.ts +25 -3
- package/src/codemode.ts +53 -23
- package/src/codex-realtime.ts +8 -2
- package/src/company-brain-okf.ts +340 -0
- package/src/connected-machine-computer-access.ts +33 -0
- package/src/connection-authority-owner.ts +61 -0
- package/src/connection-ownership.ts +180 -0
- package/src/controller-data-plane.ts +47 -0
- package/src/editable-artifact-native-kernel.ts +26 -15
- package/src/editable-artifact-office-import.ts +28 -3
- package/src/editable-artifact-production.ts +13 -7
- package/src/editable-artifact-websocket.ts +11 -18
- package/src/editable-artifact-workspace-files.ts +31 -6
- package/src/http/api-error.ts +28 -0
- package/src/http/auth.ts +4 -0
- package/src/http/cors.ts +35 -0
- package/src/http/interaction-control-error.ts +164 -0
- package/src/http/sse.ts +112 -66
- package/src/index.ts +46 -6
- package/src/integrations/atlassian.ts +146 -35
- package/src/integrations/fiken.ts +102 -22
- package/src/integrations/google-drive.ts +301 -92
- package/src/integrations/oauth-client.ts +272 -143
- package/src/integrations/oauth-profiles.ts +477 -0
- package/src/integrations/personal-github-repositories.ts +445 -0
- package/src/integrations/personal-github.ts +705 -0
- package/src/integrations/pr-review-provider.ts +246 -0
- package/src/integrations/provider-oauth.ts +121 -5
- package/src/integrations/slack-app-home.ts +300 -0
- package/src/integrations/slack-bot.ts +342 -57
- package/src/integrations/slack-interactions.ts +1898 -259
- package/src/integrations/slack-routing.ts +337 -0
- package/src/integrations/social-oauth.ts +25 -0
- package/src/interaction-frame-proxy.ts +409 -0
- package/src/mcp/company-brain-governed-writes.ts +262 -0
- package/src/mcp/company-profile-agent-admin.ts +205 -0
- package/src/mcp/documents.ts +37 -6
- package/src/mcp/files.ts +16 -1
- package/src/mcp/remember.ts +181 -0
- package/src/mcp/request-abort.ts +44 -0
- package/src/mcp/scheduled-task-view.ts +1 -0
- package/src/mcp/server.ts +719 -369
- package/src/mcp/session-view.ts +54 -0
- package/src/mcp/session-wait.ts +554 -0
- package/src/model-catalog.ts +20 -12
- package/src/routes/api-integrations.ts +4 -0
- package/src/routes/api-keys.ts +1 -0
- package/src/routes/automations.ts +534 -0
- package/src/routes/billing.ts +57 -1
- package/src/routes/browser-sessions.ts +329 -79
- package/src/routes/channels.ts +17 -1
- package/src/routes/codex.ts +50 -0
- package/src/routes/company-brain.ts +367 -0
- package/src/routes/company-profile.ts +1 -1
- package/src/routes/computer-sessions.ts +320 -93
- package/src/routes/connection-authorities.ts +139 -0
- package/src/routes/connections.ts +144 -34
- package/src/routes/documents.ts +353 -4
- package/src/routes/editable-artifacts.ts +11 -3
- package/src/routes/enrollments.ts +116 -26
- package/src/routes/environments.ts +127 -55
- package/src/routes/files.ts +72 -11
- package/src/routes/install.ts +82 -28
- package/src/routes/integration-facets.ts +29 -0
- package/src/routes/interaction-resources.ts +20 -2
- package/src/routes/machines.ts +275 -13
- package/src/routes/organization-memberships.ts +717 -28
- package/src/routes/packs.ts +4 -3
- package/src/routes/personal-github-git-broker.ts +785 -0
- package/src/routes/personal-github.ts +319 -0
- package/src/routes/pr-review.ts +531 -0
- package/src/routes/rigs.ts +104 -39
- package/src/routes/scheduled-tasks.ts +19 -21
- package/src/routes/sessions.ts +775 -38
- package/src/routes/social.ts +14 -2
- package/src/routes/supergrok.ts +23 -4
- package/src/routes/transcription-recordings.ts +8 -2
- package/src/routes/user-resource-authorities.ts +138 -0
- package/src/routes/workspace-artifacts.ts +4 -1
- package/src/routes/workspace-capture.ts +21 -0
- package/src/routes/workspace-learning.ts +228 -0
- package/src/routes/workspaces.ts +59 -6
- package/src/sandbox/auth-callout.ts +38 -12
- package/src/sandbox/channel-a.ts +242 -19
- package/src/sandbox/connection-authority.ts +3 -0
- package/src/sandbox/enrollment.ts +15 -3
- package/src/sandbox/machines.ts +186 -64
- package/src/sandbox/metrics-ingestion.ts +220 -58
- package/src/sandbox/viewer.ts +243 -19
- package/src/sandbox-file-artifacts.ts +329 -0
- package/src/scheduled-task-deletion.ts +127 -0
- package/src/slack-reaction-files.ts +16 -5
- package/src/temporal-schedule-cleanup.ts +13 -0
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
375
|
-
|
|
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
|
|
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(() =>
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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) =>
|
|
404
|
-
message: (socket, message) =>
|
|
405
|
-
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
|
},
|