@opengeni/api-router 0.26.1 → 0.30.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/app.d.ts +1 -0
- package/dist/app.js +5 -3
- package/dist/auth/canonical-human-session-admission.d.ts +27 -0
- package/dist/browser-auth-broker.d.ts +20 -0
- package/dist/browser-controller-authority.d.ts +19 -1
- package/dist/browser-network-route.d.ts +8 -0
- package/dist/{chunk-JIKNR5YL.js → chunk-2PQMSRCB.js} +15518 -6863
- package/dist/chunk-2PQMSRCB.js.map +1 -0
- package/dist/controller-data-plane.d.ts +12 -0
- package/dist/http/auth.d.ts +1 -1
- package/dist/http/sse.d.ts +18 -1
- package/dist/index.js +1 -1
- package/dist/integrations/api-integrations.d.ts +2 -2
- package/dist/integrations/fiken.d.ts +233 -0
- package/dist/integrations/google-drive.d.ts +6 -6
- package/dist/integrations/provider-oauth.d.ts +0 -2
- package/dist/integrations/slack-bot.d.ts +171 -1
- package/dist/integrations/slack-interactions.d.ts +17 -5
- package/dist/interaction-holder-heartbeat.d.ts +17 -0
- package/dist/interaction-metrics.d.ts +11 -0
- package/dist/mcp/server.d.ts +3 -2
- package/dist/model-catalog.d.ts +1 -0
- package/dist/routes/canonical-human-identities.d.ts +3 -0
- package/dist/routes/channels.d.ts +3 -0
- package/dist/routes/integration-facets.d.ts +3 -0
- package/dist/routes/interaction-resources.d.ts +5 -0
- package/dist/routes/organization-memberships.d.ts +3 -0
- package/dist/routes/sessions.d.ts +2 -1
- package/dist/routes/slack-task-policy.d.ts +3 -0
- package/dist/routes/supergrok.d.ts +3 -0
- package/dist/sandbox/channel-a.d.ts +8 -1
- package/dist/sandbox/machines.d.ts +2 -2
- package/dist/sandbox/viewer.d.ts +3 -1
- package/dist/slack-reaction-files.d.ts +27 -0
- package/dist/transcription/providers/xai-subscription.d.ts +8 -0
- package/dist/xai-realtime.d.ts +26 -0
- package/dist/xai-subscription-auth.d.ts +23 -0
- package/package.json +17 -16
- package/src/app.ts +352 -49
- package/src/auth/canonical-human-session-admission.ts +116 -0
- package/src/auth/managed-auth.ts +101 -0
- package/src/browser-auth-broker.ts +155 -0
- package/src/browser-controller-authority.ts +50 -1
- package/src/browser-network-route.ts +34 -0
- package/src/controller-data-plane.ts +35 -0
- package/src/http/auth.ts +27 -9
- package/src/http/sse.ts +229 -5
- package/src/integrations/api-integrations.ts +57 -42
- package/src/integrations/fiken.ts +1230 -0
- package/src/integrations/google-drive.ts +31 -26
- package/src/integrations/provider-oauth.ts +70 -91
- package/src/integrations/slack-bot.ts +638 -9
- package/src/integrations/slack-interactions.ts +1583 -73
- package/src/interaction-holder-heartbeat.ts +95 -0
- package/src/interaction-metrics.ts +159 -0
- package/src/mcp/documents.ts +115 -6
- package/src/mcp/server.ts +865 -242
- package/src/model-catalog.ts +19 -6
- package/src/routes/api-integrations.ts +32 -29
- package/src/routes/browser-identities.ts +24 -0
- package/src/routes/browser-sessions.ts +2122 -183
- package/src/routes/canonical-human-identities.ts +156 -0
- package/src/routes/channels.ts +90 -0
- package/src/routes/computer-sessions.ts +441 -86
- package/src/routes/connections.ts +141 -4
- package/src/routes/{integration-features.ts → integration-facets.ts} +63 -63
- package/src/routes/interaction-resources.ts +595 -0
- package/src/routes/organization-memberships.ts +53 -0
- package/src/routes/plugins.ts +2 -1
- package/src/routes/sessions.ts +419 -43
- package/src/routes/skills.ts +92 -10
- package/src/routes/slack-task-policy.ts +115 -0
- package/src/routes/supergrok.ts +717 -0
- package/src/routes/transcription-recordings.ts +9 -2
- package/src/routes/transcriptions.ts +2 -1
- package/src/routes/video-generation.ts +34 -4
- package/src/routes/workspaces.ts +22 -4
- package/src/sandbox/channel-a.ts +29 -2
- package/src/sandbox/machines.ts +5 -5
- package/src/sandbox/viewer.ts +10 -3
- package/src/slack-reaction-files.ts +181 -0
- package/src/transcription/providers/xai-subscription.ts +110 -0
- package/src/transcription/service.ts +17 -2
- package/src/xai-realtime.ts +216 -0
- package/src/xai-subscription-auth.ts +132 -0
- package/dist/chunk-JIKNR5YL.js.map +0 -1
- package/dist/routes/integration-features.d.ts +0 -3
|
@@ -362,12 +362,18 @@ export function registerResumableTranscriptionRoutes(app: Hono, deps: ApiRouteDe
|
|
|
362
362
|
if (
|
|
363
363
|
!deps.objectStorage ||
|
|
364
364
|
!service ||
|
|
365
|
-
!(await service.available({
|
|
365
|
+
!(await service.available({
|
|
366
|
+
workspaceId: authority.workspaceId,
|
|
367
|
+
subjectId: authority.subjectId,
|
|
368
|
+
}))
|
|
366
369
|
) {
|
|
367
370
|
return c.json({ code: "unavailable" }, 503);
|
|
368
371
|
}
|
|
369
372
|
const selectedProvider = service.selectProvider
|
|
370
|
-
? await service.selectProvider({
|
|
373
|
+
? await service.selectProvider({
|
|
374
|
+
workspaceId: authority.workspaceId,
|
|
375
|
+
subjectId: authority.subjectId,
|
|
376
|
+
})
|
|
371
377
|
: "host";
|
|
372
378
|
if (!selectedProvider) return c.json({ code: "unavailable" }, 503);
|
|
373
379
|
attemptId = correlationId(c);
|
|
@@ -418,6 +424,7 @@ export function registerResumableTranscriptionRoutes(app: Hono, deps: ApiRouteDe
|
|
|
418
424
|
const result = await service.transcribe({
|
|
419
425
|
workspaceId: authority.workspaceId,
|
|
420
426
|
accountId: authority.accountId,
|
|
427
|
+
subjectId: authority.subjectId,
|
|
421
428
|
audio: stored.bytes,
|
|
422
429
|
mimeType: "audio/wav",
|
|
423
430
|
durationSeconds: claim.segment.durationMilliseconds / 1_000,
|
|
@@ -18,7 +18,7 @@ export function registerTranscriptionRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
18
18
|
return c.json({ code: "policy_blocked" }, 403);
|
|
19
19
|
}
|
|
20
20
|
const service = deps.transcription;
|
|
21
|
-
if (!service || !(await service.available())) {
|
|
21
|
+
if (!service || !(await service.available({ workspaceId, subjectId: grant.subjectId }))) {
|
|
22
22
|
return c.json({ code: "unavailable" }, 503);
|
|
23
23
|
}
|
|
24
24
|
try {
|
|
@@ -26,6 +26,7 @@ export function registerTranscriptionRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
26
26
|
const result = await service.transcribe({
|
|
27
27
|
workspaceId,
|
|
28
28
|
accountId: grant.accountId,
|
|
29
|
+
subjectId: grant.subjectId,
|
|
29
30
|
audio: body.audio,
|
|
30
31
|
mimeType: body.mimeType,
|
|
31
32
|
durationSeconds: body.durationSeconds,
|
|
@@ -10,10 +10,12 @@ import {
|
|
|
10
10
|
getWorkspaceVideoGenerationPolicy,
|
|
11
11
|
updateWorkspaceVideoGenerationPolicy,
|
|
12
12
|
VideoGenerationConflictError,
|
|
13
|
+
workspaceXaiSubscriptionActive,
|
|
13
14
|
} from "@opengeni/db";
|
|
14
15
|
import {
|
|
15
16
|
requireAccessGrant,
|
|
16
17
|
VIDEO_GENERATION_MODEL_CATALOG,
|
|
18
|
+
videoGenerationModelSupportsFundingSource,
|
|
17
19
|
videoGenerationCapabilitiesForPolicy,
|
|
18
20
|
type ApiRouteDeps,
|
|
19
21
|
} from "@opengeni/core";
|
|
@@ -23,14 +25,16 @@ import { HTTPException } from "hono/http-exception";
|
|
|
23
25
|
export function registerVideoGenerationRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
24
26
|
app.get("/v1/workspaces/:workspaceId/video-generation", async (c) => {
|
|
25
27
|
const workspaceId = c.req.param("workspaceId");
|
|
26
|
-
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
27
|
-
const [policy, connection] = await Promise.all([
|
|
28
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
29
|
+
const [policy, connection, supergrokConfigured] = await Promise.all([
|
|
28
30
|
getWorkspaceVideoGenerationPolicy(deps.db, workspaceId),
|
|
29
31
|
getWorkspaceVercelAiGatewayConnectionMetadata(deps.db, workspaceId),
|
|
32
|
+
workspaceXaiSubscriptionActive(deps.db, deps.settings, workspaceId, grant.subjectId),
|
|
30
33
|
]);
|
|
31
34
|
const fundingOptions = videoGenerationFundingOptions({
|
|
32
35
|
managedConfigured: managedVideoGenerationConfigured(deps),
|
|
33
36
|
workspaceGatewayConfigured: connection !== null,
|
|
37
|
+
supergrokConfigured,
|
|
34
38
|
});
|
|
35
39
|
const selectedFunding = fundingOptions.find((option) => option.source === policy.fundingSource);
|
|
36
40
|
const capabilities =
|
|
@@ -56,10 +60,14 @@ export function registerVideoGenerationRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
56
60
|
const workspaceId = c.req.param("workspaceId");
|
|
57
61
|
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
58
62
|
const payload = UpdateVideoGenerationPolicyRequest.parse(await c.req.json());
|
|
59
|
-
const connection = await
|
|
63
|
+
const [connection, supergrokConfigured] = await Promise.all([
|
|
64
|
+
getWorkspaceVercelAiGatewayConnectionMetadata(deps.db, workspaceId),
|
|
65
|
+
workspaceXaiSubscriptionActive(deps.db, deps.settings, workspaceId, grant.subjectId),
|
|
66
|
+
]);
|
|
60
67
|
const fundingOptions = videoGenerationFundingOptions({
|
|
61
68
|
managedConfigured: managedVideoGenerationConfigured(deps),
|
|
62
69
|
workspaceGatewayConfigured: connection !== null,
|
|
70
|
+
supergrokConfigured,
|
|
63
71
|
});
|
|
64
72
|
const selectedFunding = fundingOptions.find(
|
|
65
73
|
(option) => option.source === payload.fundingSource,
|
|
@@ -69,6 +77,15 @@ export function registerVideoGenerationRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
69
77
|
message: selectedFunding?.unavailableReason ?? "Video generation funding is unavailable",
|
|
70
78
|
});
|
|
71
79
|
}
|
|
80
|
+
if (
|
|
81
|
+
payload.enabledModelIds.some(
|
|
82
|
+
(modelId) => !videoGenerationModelSupportsFundingSource(modelId, payload.fundingSource),
|
|
83
|
+
)
|
|
84
|
+
) {
|
|
85
|
+
throw new HTTPException(422, {
|
|
86
|
+
message: "The selected video model is unavailable for this funding source",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
72
89
|
try {
|
|
73
90
|
const policy = await updateWorkspaceVideoGenerationPolicy(deps.db, {
|
|
74
91
|
accountId: grant.accountId,
|
|
@@ -96,7 +113,10 @@ export function registerVideoGenerationRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
96
113
|
workspaceId,
|
|
97
114
|
c.req.param("operationId"),
|
|
98
115
|
);
|
|
99
|
-
if (!summary)
|
|
116
|
+
if (!summary)
|
|
117
|
+
throw new HTTPException(404, {
|
|
118
|
+
message: "video generation operation not found",
|
|
119
|
+
});
|
|
100
120
|
return c.json(VideoGenerationOperationSummary.parse(summary));
|
|
101
121
|
});
|
|
102
122
|
}
|
|
@@ -108,6 +128,7 @@ function managedVideoGenerationConfigured(deps: ApiRouteDeps): boolean {
|
|
|
108
128
|
function videoGenerationFundingOptions(input: {
|
|
109
129
|
managedConfigured: boolean;
|
|
110
130
|
workspaceGatewayConfigured: boolean;
|
|
131
|
+
supergrokConfigured: boolean;
|
|
111
132
|
}) {
|
|
112
133
|
return [
|
|
113
134
|
{
|
|
@@ -119,6 +140,15 @@ function videoGenerationFundingOptions(input: {
|
|
|
119
140
|
? null
|
|
120
141
|
: "OpenGeni-managed video generation is not configured.",
|
|
121
142
|
},
|
|
143
|
+
{
|
|
144
|
+
source: "supergrok_subscription" as const,
|
|
145
|
+
label: "SuperGrok",
|
|
146
|
+
description: "Uses your connected SuperGrok subscription.",
|
|
147
|
+
available: input.supergrokConfigured,
|
|
148
|
+
unavailableReason: input.supergrokConfigured
|
|
149
|
+
? null
|
|
150
|
+
: "Connect an eligible SuperGrok account first.",
|
|
151
|
+
},
|
|
122
152
|
{
|
|
123
153
|
source: "workspace_gateway" as const,
|
|
124
154
|
label: "Your Gateway",
|
package/src/routes/workspaces.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
updateWorkspaceSettings,
|
|
36
36
|
upsertWorkspaceModelPolicy,
|
|
37
37
|
workspaceCodexSubscriptionActive,
|
|
38
|
+
workspaceXaiSubscriptionActive,
|
|
38
39
|
workspaceVercelAiGatewayConnectionActive,
|
|
39
40
|
} from "@opengeni/db";
|
|
40
41
|
import { boundWorkspaceControlHttpPage } from "@opengeni/events";
|
|
@@ -55,6 +56,7 @@ import { processTemporalScheduleCleanupClaims } from "../temporal-schedule-clean
|
|
|
55
56
|
import {
|
|
56
57
|
AI_GATEWAY_REALTIME_MODELS,
|
|
57
58
|
CODEX_REALTIME_MODEL_ID,
|
|
59
|
+
SUPERGROK_REALTIME_MODEL_ID,
|
|
58
60
|
canonicalizeConfiguredModelId,
|
|
59
61
|
type Settings,
|
|
60
62
|
} from "@opengeni/config";
|
|
@@ -165,10 +167,16 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
165
167
|
// so bearer/federated definitions intentionally fail closed as not ready.
|
|
166
168
|
app.get("/v1/workspaces/:workspaceId/model-catalog", async (c) => {
|
|
167
169
|
const workspaceId = c.req.param("workspaceId");
|
|
168
|
-
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
169
|
-
const [
|
|
170
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
171
|
+
const [
|
|
172
|
+
policy,
|
|
173
|
+
codexSubscriptionActive,
|
|
174
|
+
xaiSubscriptionActive,
|
|
175
|
+
workspaceGatewayConnectionActive,
|
|
176
|
+
] = await Promise.all([
|
|
170
177
|
getWorkspaceModelPolicy(deps.db, workspaceId),
|
|
171
178
|
workspaceCodexSubscriptionActive(deps.db, deps.settings, workspaceId),
|
|
179
|
+
workspaceXaiSubscriptionActive(deps.db, deps.settings, workspaceId, grant.subjectId),
|
|
172
180
|
workspaceVercelAiGatewayConnectionActive(deps.db, workspaceId),
|
|
173
181
|
]);
|
|
174
182
|
c.header("cache-control", "private, no-store");
|
|
@@ -178,6 +186,7 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
178
186
|
settings: deps.settings,
|
|
179
187
|
policy,
|
|
180
188
|
codexSubscriptionActive,
|
|
189
|
+
xaiSubscriptionActive,
|
|
181
190
|
workspaceGatewayConnectionActive,
|
|
182
191
|
}),
|
|
183
192
|
),
|
|
@@ -186,9 +195,10 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
186
195
|
|
|
187
196
|
app.get("/v1/workspaces/:workspaceId/realtime-model-catalog", async (c) => {
|
|
188
197
|
const workspaceId = c.req.param("workspaceId");
|
|
189
|
-
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
190
|
-
const [codexConnected, workspaceGatewayConnected] = await Promise.all([
|
|
198
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
199
|
+
const [codexConnected, supergrokConnected, workspaceGatewayConnected] = await Promise.all([
|
|
191
200
|
workspaceCodexSubscriptionActive(deps.db, deps.settings, workspaceId),
|
|
201
|
+
workspaceXaiSubscriptionActive(deps.db, deps.settings, workspaceId, grant.subjectId),
|
|
192
202
|
workspaceVercelAiGatewayConnectionActive(deps.db, workspaceId),
|
|
193
203
|
]);
|
|
194
204
|
const availability = (
|
|
@@ -220,6 +230,14 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
220
230
|
...availability(codexConnected, "Connect Codex to use this voice model"),
|
|
221
231
|
recommended: false,
|
|
222
232
|
},
|
|
233
|
+
{
|
|
234
|
+
id: SUPERGROK_REALTIME_MODEL_ID,
|
|
235
|
+
label: "Grok Voice Think Fast 2.0",
|
|
236
|
+
provider: "Connected SuperGrok" as const,
|
|
237
|
+
description: "Direct SuperGrok speech-to-speech",
|
|
238
|
+
...availability(supergrokConnected, "Connect SuperGrok to use this voice model"),
|
|
239
|
+
recommended: false,
|
|
240
|
+
},
|
|
223
241
|
...gatewayModels.map((model) => ({
|
|
224
242
|
id: model.workspaceModelId,
|
|
225
243
|
label: model.label,
|
package/src/sandbox/channel-a.ts
CHANGED
|
@@ -64,6 +64,8 @@ import {
|
|
|
64
64
|
ChannelAUnsupportedError,
|
|
65
65
|
ChannelAUnavailableError,
|
|
66
66
|
ChannelAValidationError,
|
|
67
|
+
BrowserControlRequestError,
|
|
68
|
+
BrowserControlTransportError,
|
|
67
69
|
codemodeTokenFileFromEnvironment,
|
|
68
70
|
withCodemodeTokenSession,
|
|
69
71
|
withRunCredentialsSession,
|
|
@@ -104,11 +106,14 @@ export type ChannelAOperation =
|
|
|
104
106
|
| "browser.suspend"
|
|
105
107
|
| "browser.end"
|
|
106
108
|
| "browser.read"
|
|
109
|
+
| "browser.action"
|
|
107
110
|
| "browser.control"
|
|
111
|
+
| "browser.download.save"
|
|
108
112
|
| "browser.attach"
|
|
109
113
|
| "computer.create"
|
|
110
114
|
| "computer.end"
|
|
111
115
|
| "computer.read"
|
|
116
|
+
| "computer.action"
|
|
112
117
|
| "computer.control"
|
|
113
118
|
| "computer.attach";
|
|
114
119
|
|
|
@@ -122,6 +127,11 @@ export type ChannelAContext = {
|
|
|
122
127
|
waitSignal?: AbortSignal | undefined;
|
|
123
128
|
/** Bounded route identity for metrics and safe operator diagnostics. */
|
|
124
129
|
operation?: ChannelAOperation | undefined;
|
|
130
|
+
/** The callback is an interaction-controller read or an exactly-once action.
|
|
131
|
+
* A controller transport failure may therefore rebuild the exact fenced
|
|
132
|
+
* provider handle and replay the request. Tab/lifecycle mutations that lack
|
|
133
|
+
* a controller operation id must never opt into this recovery. */
|
|
134
|
+
retryControllerTransport?: boolean | undefined;
|
|
125
135
|
};
|
|
126
136
|
|
|
127
137
|
export type ChannelAOperationFailureReason =
|
|
@@ -371,6 +381,8 @@ type ChannelAReadRecoveryOptions = {
|
|
|
371
381
|
maxFreshHandleRetries?: 1 | 2;
|
|
372
382
|
/** Never start another provider attempt after the originating request ends. */
|
|
373
383
|
waitSignal?: AbortSignal | undefined;
|
|
384
|
+
/** Additional callback-specific failure that is safe to replay. */
|
|
385
|
+
retryableError?: ((error: unknown) => boolean) | undefined;
|
|
374
386
|
};
|
|
375
387
|
|
|
376
388
|
/** Retry a side-effect-free Channel-A read only after the caller has discarded
|
|
@@ -391,7 +403,9 @@ export async function runChannelAReadWithFreshHandleRetry<T>(
|
|
|
391
403
|
try {
|
|
392
404
|
return await run();
|
|
393
405
|
} catch (error) {
|
|
394
|
-
|
|
406
|
+
const retryable =
|
|
407
|
+
error instanceof ChannelAUnavailableError || options.retryableError?.(error) === true;
|
|
408
|
+
if (!retryable || retries >= maxFreshHandleRetries) {
|
|
395
409
|
throw error;
|
|
396
410
|
}
|
|
397
411
|
options.waitSignal?.throwIfAborted();
|
|
@@ -405,7 +419,17 @@ export function shouldEvictChannelAHandleAfterError(
|
|
|
405
419
|
error: unknown,
|
|
406
420
|
cacheKind: EstablishedHandleCacheKind,
|
|
407
421
|
): boolean {
|
|
408
|
-
return
|
|
422
|
+
return (
|
|
423
|
+
cacheKind === "read" &&
|
|
424
|
+
(error instanceof ChannelAUnavailableError || isRetryableControllerTransport(error))
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function isRetryableControllerTransport(error: unknown): boolean {
|
|
429
|
+
return (
|
|
430
|
+
error instanceof BrowserControlTransportError ||
|
|
431
|
+
(error instanceof BrowserControlRequestError && error.retryable)
|
|
432
|
+
);
|
|
409
433
|
}
|
|
410
434
|
|
|
411
435
|
function evictEstablishedHandle(key: string, cacheKind: EstablishedHandleCacheKind): void {
|
|
@@ -941,6 +965,9 @@ async function withChannelAOperation<T>(
|
|
|
941
965
|
{
|
|
942
966
|
maxFreshHandleRetries: session.sandboxBackend === "modal" ? 2 : 1,
|
|
943
967
|
...(ctx.waitSignal ? { waitSignal: ctx.waitSignal } : {}),
|
|
968
|
+
...(ctx.retryControllerTransport
|
|
969
|
+
? { retryableError: isRetryableControllerTransport }
|
|
970
|
+
: {}),
|
|
944
971
|
},
|
|
945
972
|
)
|
|
946
973
|
: await runProviderOperation();
|
package/src/sandbox/machines.ts
CHANGED
|
@@ -156,8 +156,8 @@ function machineStateFor(
|
|
|
156
156
|
|
|
157
157
|
/**
|
|
158
158
|
* Build the Machines dashboard response for a workspace. When `sessionId` is
|
|
159
|
-
* supplied (an in-session view) the session's synthetic
|
|
160
|
-
* prepended (`isSessionGroup:true`) and the active-sandbox pointer is echoed;
|
|
159
|
+
* supplied (an in-session view) the session's synthetic home group box is
|
|
160
|
+
* prepended when one exists (`isSessionGroup:true`) and the active-sandbox pointer is echoed;
|
|
161
161
|
* without it (the pure workspace dashboard) `activeSandboxId` is null and only
|
|
162
162
|
* the enrolled machines are listed.
|
|
163
163
|
*/
|
|
@@ -184,9 +184,9 @@ export async function listMachines(
|
|
|
184
184
|
|
|
185
185
|
const machines: MachineView[] = [];
|
|
186
186
|
|
|
187
|
-
// The session's own
|
|
188
|
-
//
|
|
189
|
-
if (session) {
|
|
187
|
+
// The session's own group box (synthetic): the default/home sandbox a null
|
|
188
|
+
// active pointer routes to. A backend:none session has no home box.
|
|
189
|
+
if (session && session.sandboxBackend !== "none") {
|
|
190
190
|
const groupActive = activeSandboxId === null;
|
|
191
191
|
const groupLease = await readLease(db, workspaceId, session.sandboxGroupId);
|
|
192
192
|
machines.push(
|
package/src/sandbox/viewer.ts
CHANGED
|
@@ -891,8 +891,11 @@ export type TerminalStreamMint = {
|
|
|
891
891
|
token: string;
|
|
892
892
|
expiresAt: string;
|
|
893
893
|
leaseEpoch: number;
|
|
894
|
+
transport: "pty-ws" | "relay-pty";
|
|
894
895
|
};
|
|
895
896
|
|
|
897
|
+
type SelfhostedStreamMint = Omit<TerminalStreamMint, "transport">;
|
|
898
|
+
|
|
896
899
|
export type MintTerminalStreamInput = {
|
|
897
900
|
accountId: string;
|
|
898
901
|
workspaceId: string;
|
|
@@ -950,7 +953,7 @@ export async function mintTerminalStream(
|
|
|
950
953
|
if (session.activeSandboxId) {
|
|
951
954
|
const active = await getSandbox(db, workspaceId, session.activeSandboxId);
|
|
952
955
|
if (active?.kind === "selfhosted") {
|
|
953
|
-
|
|
956
|
+
const stream = await tryMintActiveSelfhostedStream(
|
|
954
957
|
services,
|
|
955
958
|
{
|
|
956
959
|
session,
|
|
@@ -961,6 +964,7 @@ export async function mintTerminalStream(
|
|
|
961
964
|
},
|
|
962
965
|
input.resolveSelfhostedSession,
|
|
963
966
|
);
|
|
967
|
+
return stream ? { ...stream, transport: "relay-pty" } : null;
|
|
964
968
|
}
|
|
965
969
|
// A Modal swap target (or unknown) falls through to the existing group-box path
|
|
966
970
|
// (unchanged — Modal swap-target streaming is out of scope for this fix).
|
|
@@ -1052,6 +1056,7 @@ export async function mintTerminalStream(
|
|
|
1052
1056
|
token: exposed.token,
|
|
1053
1057
|
expiresAt: exposed.expiresAt,
|
|
1054
1058
|
leaseEpoch: lease.leaseEpoch,
|
|
1059
|
+
transport: "pty-ws",
|
|
1055
1060
|
};
|
|
1056
1061
|
} catch {
|
|
1057
1062
|
// Any other failure degrades the terminal pty-ws cell to the sse-events
|
|
@@ -1107,7 +1112,7 @@ async function tryMintActiveSelfhostedStream(
|
|
|
1107
1112
|
resolveSelfhostedSession?: (
|
|
1108
1113
|
sandbox: SandboxRecord,
|
|
1109
1114
|
) => Promise<{ resolveExposedPort?: (port: number) => Promise<unknown> }>,
|
|
1110
|
-
): Promise<
|
|
1115
|
+
): Promise<SelfhostedStreamMint | null> {
|
|
1111
1116
|
const { settings, bus } = services;
|
|
1112
1117
|
const { session, workspaceId, port, sandbox } = input;
|
|
1113
1118
|
if (!sandbox.enrollmentId) {
|
|
@@ -1128,6 +1133,8 @@ async function tryMintActiveSelfhostedStream(
|
|
|
1128
1133
|
controlRpcFactory: () => controlRpc(bus),
|
|
1129
1134
|
agentId: sandbox.enrollmentId,
|
|
1130
1135
|
epoch: session.activeEpoch,
|
|
1136
|
+
// Stream authority rotates; terminal process identity does not.
|
|
1137
|
+
terminalScopeId: session.id,
|
|
1131
1138
|
});
|
|
1132
1139
|
shSession = await client.resume({ agentId: sandbox.enrollmentId });
|
|
1133
1140
|
}
|
|
@@ -1185,7 +1192,7 @@ export type MintSelfhostedStreamInput = {
|
|
|
1185
1192
|
export async function mintSelfhostedStream(
|
|
1186
1193
|
services: ViewerServices,
|
|
1187
1194
|
input: MintSelfhostedStreamInput,
|
|
1188
|
-
): Promise<
|
|
1195
|
+
): Promise<SelfhostedStreamMint | null> {
|
|
1189
1196
|
const { settings } = services;
|
|
1190
1197
|
const secret = resolveStreamTokenSecret(settings);
|
|
1191
1198
|
if (!secret) {
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { FileResourceRef } from "@opengeni/contracts";
|
|
3
|
+
import { completeFileUpload, prepareGeneratedWorkspaceFile, type Database } from "@opengeni/db";
|
|
4
|
+
import type { ObjectHead, ObjectStorage } from "@opengeni/storage";
|
|
5
|
+
import type { DownloadedSlackReactionImage } from "./integrations/slack-bot";
|
|
6
|
+
|
|
7
|
+
const IMPORT_IDENTITY_VERSION = "slack-reaction-image-v1";
|
|
8
|
+
const UPLOAD_INTENT_TTL_MS = 60 * 60_000;
|
|
9
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
10
|
+
|
|
11
|
+
export type SlackReactionImageImportSource = Readonly<{
|
|
12
|
+
accountId: string;
|
|
13
|
+
workspaceId: string;
|
|
14
|
+
connectionId: string;
|
|
15
|
+
slackTeamId: string;
|
|
16
|
+
slackChannelId: string;
|
|
17
|
+
slackMessageTs: string;
|
|
18
|
+
}>;
|
|
19
|
+
|
|
20
|
+
export type ImportedSlackReactionImage = Readonly<{
|
|
21
|
+
resource: FileResourceRef;
|
|
22
|
+
filename: string;
|
|
23
|
+
contentType: "image/png" | "image/jpeg" | "image/webp";
|
|
24
|
+
sizeBytes: number;
|
|
25
|
+
}>;
|
|
26
|
+
|
|
27
|
+
export async function importSlackReactionImage(
|
|
28
|
+
dependencies: Readonly<{
|
|
29
|
+
db: Database;
|
|
30
|
+
objectStorage: ObjectStorage;
|
|
31
|
+
now?: () => Date;
|
|
32
|
+
prepareFile?: typeof prepareGeneratedWorkspaceFile;
|
|
33
|
+
completeFile?: typeof completeFileUpload;
|
|
34
|
+
}>,
|
|
35
|
+
source: SlackReactionImageImportSource,
|
|
36
|
+
image: DownloadedSlackReactionImage,
|
|
37
|
+
ordinal: number,
|
|
38
|
+
): Promise<ImportedSlackReactionImage> {
|
|
39
|
+
if (!Number.isSafeInteger(ordinal) || ordinal < 1 || ordinal > 4) {
|
|
40
|
+
throw new RangeError("Slack reaction attachment ordinal is invalid");
|
|
41
|
+
}
|
|
42
|
+
const storage = dependencies.objectStorage;
|
|
43
|
+
if (!storage.headObject) {
|
|
44
|
+
throw new Error("Object storage lacks immutable object verification support");
|
|
45
|
+
}
|
|
46
|
+
if (image.bytes.byteLength < 1 || image.bytes.byteLength > 4 * 1024 * 1024) {
|
|
47
|
+
throw new Error("Slack reaction image size is invalid");
|
|
48
|
+
}
|
|
49
|
+
const sha256 = createHash("sha256").update(image.bytes).digest("hex");
|
|
50
|
+
const identity = slackReactionImageIdentity(source, image.fileId);
|
|
51
|
+
const fileId = deterministicUuid(`${identity}:file`);
|
|
52
|
+
const uploadId = deterministicUuid(`${identity}:upload`);
|
|
53
|
+
const safeFilename = safeSlackImageFilename(image.filename, image.contentType);
|
|
54
|
+
const objectKey = `workspaces/${source.workspaceId}/files/${fileId}/slack-reactions/${safeFilename}`;
|
|
55
|
+
const filename = safeFilename;
|
|
56
|
+
const prepared = await (dependencies.prepareFile ?? prepareGeneratedWorkspaceFile)(
|
|
57
|
+
dependencies.db,
|
|
58
|
+
{
|
|
59
|
+
accountId: source.accountId,
|
|
60
|
+
workspaceId: source.workspaceId,
|
|
61
|
+
fileId,
|
|
62
|
+
uploadId,
|
|
63
|
+
filename,
|
|
64
|
+
safeFilename,
|
|
65
|
+
contentType: image.contentType,
|
|
66
|
+
sizeBytes: image.bytes.byteLength,
|
|
67
|
+
sha256,
|
|
68
|
+
bucket: storage.bucket,
|
|
69
|
+
objectKey,
|
|
70
|
+
expiresAt: new Date((dependencies.now?.() ?? new Date()).getTime() + UPLOAD_INTENT_TTL_MS),
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
let file = prepared.file;
|
|
75
|
+
if (file.status !== "ready") {
|
|
76
|
+
const existing = await storage.headObject(objectKey);
|
|
77
|
+
if (existing) {
|
|
78
|
+
assertStoredSlackImage(existing, image, sha256);
|
|
79
|
+
} else if (storage.putObjectIfAbsent) {
|
|
80
|
+
await storage.putObjectIfAbsent({
|
|
81
|
+
key: objectKey,
|
|
82
|
+
contentType: image.contentType,
|
|
83
|
+
body: image.bytes,
|
|
84
|
+
sha256,
|
|
85
|
+
});
|
|
86
|
+
assertStoredSlackImage(await storage.headObject(objectKey), image, sha256);
|
|
87
|
+
} else {
|
|
88
|
+
await storage.putObject({
|
|
89
|
+
key: objectKey,
|
|
90
|
+
contentType: image.contentType,
|
|
91
|
+
body: image.bytes,
|
|
92
|
+
sha256,
|
|
93
|
+
});
|
|
94
|
+
assertStoredSlackImage(await storage.headObject(objectKey), image, sha256);
|
|
95
|
+
}
|
|
96
|
+
file = await (dependencies.completeFile ?? completeFileUpload)(
|
|
97
|
+
dependencies.db,
|
|
98
|
+
source.workspaceId,
|
|
99
|
+
uploadId,
|
|
100
|
+
);
|
|
101
|
+
} else {
|
|
102
|
+
assertStoredSlackImage(await storage.headObject(objectKey), image, sha256);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (
|
|
106
|
+
file.id !== fileId ||
|
|
107
|
+
file.status !== "ready" ||
|
|
108
|
+
file.filename !== filename ||
|
|
109
|
+
file.safeFilename !== safeFilename ||
|
|
110
|
+
file.contentType !== image.contentType ||
|
|
111
|
+
file.sizeBytes !== image.bytes.byteLength ||
|
|
112
|
+
file.sha256 !== sha256
|
|
113
|
+
) {
|
|
114
|
+
throw new Error("Imported Slack image differs from its immutable workspace file");
|
|
115
|
+
}
|
|
116
|
+
const mountPath = `attachments/slack/${String(ordinal).padStart(2, "0")}-${safeFilename}`;
|
|
117
|
+
return Object.freeze({
|
|
118
|
+
resource: Object.freeze({ kind: "file", fileId, mountPath }),
|
|
119
|
+
filename,
|
|
120
|
+
contentType: image.contentType,
|
|
121
|
+
sizeBytes: image.bytes.byteLength,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function slackReactionImageIdentity(
|
|
126
|
+
source: SlackReactionImageImportSource,
|
|
127
|
+
slackFileId: string,
|
|
128
|
+
): string {
|
|
129
|
+
return [
|
|
130
|
+
IMPORT_IDENTITY_VERSION,
|
|
131
|
+
source.workspaceId,
|
|
132
|
+
source.connectionId,
|
|
133
|
+
source.slackTeamId,
|
|
134
|
+
source.slackChannelId,
|
|
135
|
+
source.slackMessageTs,
|
|
136
|
+
slackFileId,
|
|
137
|
+
].join(":");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function safeSlackImageFilename(
|
|
141
|
+
value: string,
|
|
142
|
+
contentType: "image/png" | "image/jpeg" | "image/webp",
|
|
143
|
+
): string {
|
|
144
|
+
const extension =
|
|
145
|
+
contentType === "image/png" ? ".png" : contentType === "image/jpeg" ? ".jpg" : ".webp";
|
|
146
|
+
const normalized = value
|
|
147
|
+
.normalize("NFKC")
|
|
148
|
+
.replace(/[\\/:*?"<>|\u0000-\u001f\u007f]+/gu, "-")
|
|
149
|
+
.replace(/\s+/gu, " ")
|
|
150
|
+
.trim();
|
|
151
|
+
const withoutKnownExtension = normalized.replace(/\.(?:png|jpe?g|webp)$/iu, "");
|
|
152
|
+
const stem = withoutKnownExtension.replace(/^[. -]+|[. ]+$/gu, "").slice(0, 180) || "slack-image";
|
|
153
|
+
return `${stem}${extension}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function assertStoredSlackImage(
|
|
157
|
+
head: ObjectHead | null,
|
|
158
|
+
image: DownloadedSlackReactionImage,
|
|
159
|
+
sha256: string,
|
|
160
|
+
): void {
|
|
161
|
+
if (
|
|
162
|
+
!head ||
|
|
163
|
+
head.ContentLength !== image.bytes.byteLength ||
|
|
164
|
+
head.ContentType !== image.contentType ||
|
|
165
|
+
head.Metadata?.sha256 !== sha256 ||
|
|
166
|
+
typeof head.VersionToken !== "string" ||
|
|
167
|
+
head.VersionToken.length < 1
|
|
168
|
+
) {
|
|
169
|
+
throw new Error("Stored Slack image differs from its immutable import");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function deterministicUuid(seed: string): string {
|
|
174
|
+
const bytes = createHash("sha256").update(seed, "utf8").digest().subarray(0, 16);
|
|
175
|
+
bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x50;
|
|
176
|
+
bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
|
|
177
|
+
const hex = bytes.toString("hex");
|
|
178
|
+
const value = `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
179
|
+
if (!UUID_PATTERN.test(value)) throw new Error("Slack reaction file identity is invalid");
|
|
180
|
+
return value;
|
|
181
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { type Settings } from "@opengeni/config";
|
|
2
|
+
import {
|
|
3
|
+
type TranscriptionAvailabilityContext,
|
|
4
|
+
type TranscriptionProvider,
|
|
5
|
+
TranscriptionServiceError,
|
|
6
|
+
} from "@opengeni/core";
|
|
7
|
+
import { workspaceXaiSubscriptionActive, type Database } from "@opengeni/db";
|
|
8
|
+
import {
|
|
9
|
+
XAI_CLIENT_MODE,
|
|
10
|
+
XAI_CLIENT_VERSION,
|
|
11
|
+
XAI_PUBLIC_API_BASE_URL,
|
|
12
|
+
type XaiFetch,
|
|
13
|
+
} from "@opengeni/xai-subscription";
|
|
14
|
+
import { buildXaiSubscriptionAuthorization } from "../../xai-subscription-auth";
|
|
15
|
+
import { fetchError, responseError } from "./openai";
|
|
16
|
+
|
|
17
|
+
const TRANSCRIBE_URL = `${XAI_PUBLIC_API_BASE_URL}/stt`;
|
|
18
|
+
|
|
19
|
+
export function createXaiSubscriptionTranscriptionProvider(input: {
|
|
20
|
+
settings: Settings;
|
|
21
|
+
db: Database;
|
|
22
|
+
fetch?: typeof fetch;
|
|
23
|
+
}): TranscriptionProvider {
|
|
24
|
+
const fetchImpl = input.fetch ?? fetch;
|
|
25
|
+
return {
|
|
26
|
+
id: "supergrok-subscription",
|
|
27
|
+
supportsServerDeadline: true,
|
|
28
|
+
experimental: true,
|
|
29
|
+
async available(context?: TranscriptionAvailabilityContext) {
|
|
30
|
+
if (!context?.workspaceId || !context.subjectId) {
|
|
31
|
+
return input.settings.supergrokSubscriptionEnabled;
|
|
32
|
+
}
|
|
33
|
+
return await workspaceXaiSubscriptionActive(
|
|
34
|
+
input.db,
|
|
35
|
+
input.settings,
|
|
36
|
+
context.workspaceId,
|
|
37
|
+
context.subjectId,
|
|
38
|
+
);
|
|
39
|
+
},
|
|
40
|
+
async transcribe({
|
|
41
|
+
audio,
|
|
42
|
+
mimeType,
|
|
43
|
+
filename,
|
|
44
|
+
workspaceId,
|
|
45
|
+
accountId,
|
|
46
|
+
subjectId,
|
|
47
|
+
requestId,
|
|
48
|
+
signal,
|
|
49
|
+
}) {
|
|
50
|
+
let auth: Awaited<ReturnType<typeof buildXaiSubscriptionAuthorization>>;
|
|
51
|
+
try {
|
|
52
|
+
auth = await buildXaiSubscriptionAuthorization({
|
|
53
|
+
db: input.db,
|
|
54
|
+
settings: input.settings,
|
|
55
|
+
accountId,
|
|
56
|
+
workspaceId,
|
|
57
|
+
subjectId,
|
|
58
|
+
shardKey: requestId,
|
|
59
|
+
sessionId: requestId,
|
|
60
|
+
...(input.fetch ? { fetch: input.fetch as XaiFetch } : {}),
|
|
61
|
+
});
|
|
62
|
+
} catch {
|
|
63
|
+
throw new TranscriptionServiceError({
|
|
64
|
+
code: "unavailable",
|
|
65
|
+
message: "Transcription is unavailable.",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const request = async (accessToken: string) => {
|
|
69
|
+
const form = new FormData();
|
|
70
|
+
form.append(
|
|
71
|
+
"file",
|
|
72
|
+
new Blob([Uint8Array.from(audio).buffer], { type: mimeType }),
|
|
73
|
+
filename,
|
|
74
|
+
);
|
|
75
|
+
return await fetchImpl(TRANSCRIBE_URL, {
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: {
|
|
78
|
+
Authorization: `Bearer ${accessToken}`,
|
|
79
|
+
"User-Agent": `opengeni/${XAI_CLIENT_VERSION}`,
|
|
80
|
+
"x-grok-client-version": XAI_CLIENT_VERSION,
|
|
81
|
+
"x-grok-client-identifier": "opengeni",
|
|
82
|
+
"x-grok-client-mode": XAI_CLIENT_MODE,
|
|
83
|
+
"x-grok-session-id": requestId,
|
|
84
|
+
},
|
|
85
|
+
body: form,
|
|
86
|
+
...(signal ? { signal } : {}),
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
let response: Response;
|
|
90
|
+
try {
|
|
91
|
+
let token = await auth.context.getToken();
|
|
92
|
+
response = await request(token.accessToken);
|
|
93
|
+
if (response.status === 401) {
|
|
94
|
+
await response.body?.cancel().catch(() => undefined);
|
|
95
|
+
token = await auth.context.refresh();
|
|
96
|
+
response = await request(token.accessToken);
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
throw fetchError(error);
|
|
100
|
+
}
|
|
101
|
+
if (!response.ok) throw responseError(response.status);
|
|
102
|
+
const body = (await response.json().catch(() => null)) as Record<string, unknown> | null;
|
|
103
|
+
if (!body || typeof body.text !== "string") throw responseError(502);
|
|
104
|
+
return {
|
|
105
|
+
text: body.text,
|
|
106
|
+
languages: typeof body.language === "string" && body.language ? [body.language] : [],
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|