@opengeni/api-router 0.12.12 → 0.14.3
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 +18 -38
- package/dist/app.js +3 -1
- package/dist/auth/managed-auth.d.ts +35 -0
- package/dist/{chunk-5TULDPWX.js → chunk-36PW33ND.js} +4111 -1277
- package/dist/chunk-36PW33ND.js.map +1 -0
- package/dist/codex-redemption-security.d.ts +15 -0
- package/dist/github-access.d.ts +12 -0
- package/dist/github-browser-flow.d.ts +14 -0
- package/dist/http/auth.d.ts +3 -0
- package/dist/http/common.d.ts +3 -0
- package/dist/http/sse.d.ts +70 -0
- package/dist/index.d.ts +11 -13
- package/dist/index.js +1 -1
- package/dist/integrations/google-drive.d.ts +76 -0
- package/dist/integrations/oauth-client.d.ts +42 -0
- package/dist/integrations/provider-domain.d.ts +7 -0
- package/dist/integrations/slack-bot.d.ts +343 -0
- package/dist/mcp/documents.d.ts +8 -0
- package/dist/mcp/files.d.ts +5 -0
- package/dist/mcp/server.d.ts +107 -0
- package/dist/mcp/session-view.d.ts +191 -0
- package/dist/mcp/toolspace.d.ts +61 -0
- package/dist/model-catalog.d.ts +39 -0
- package/dist/observability.d.ts +3 -0
- package/dist/routes/api-keys.d.ts +3 -0
- package/dist/routes/billing.d.ts +16 -0
- package/dist/routes/capabilities.d.ts +3 -0
- package/dist/routes/catalog-assets.d.ts +5 -0
- package/dist/routes/codex.d.ts +10 -0
- package/dist/routes/connections.d.ts +3 -0
- package/dist/routes/documents.d.ts +3 -0
- package/dist/routes/enrollments.d.ts +3 -0
- package/dist/routes/environments.d.ts +5 -0
- package/dist/routes/files.d.ts +4 -0
- package/dist/routes/github.d.ts +3 -0
- package/dist/routes/insights.d.ts +3 -0
- package/dist/routes/install.d.ts +5 -0
- package/dist/routes/machines.d.ts +3 -0
- package/dist/routes/packs.d.ts +3 -0
- package/dist/routes/preference-registry.d.ts +5 -0
- package/dist/routes/rigs.d.ts +3 -0
- package/dist/routes/scheduled-tasks.d.ts +3 -0
- package/dist/routes/sessions.d.ts +15 -0
- package/dist/routes/social.d.ts +3 -0
- package/dist/routes/transcriptions.d.ts +3 -0
- package/dist/routes/workspace-capture.d.ts +59 -0
- package/dist/routes/workspace-instruction-policies.d.ts +3 -0
- package/dist/routes/workspace-state.d.ts +3 -0
- package/dist/routes/workspaces.d.ts +5 -0
- package/dist/sandbox/access.d.ts +21 -0
- package/dist/sandbox/auth-callout.d.ts +30 -0
- package/dist/sandbox/channel-a.d.ts +37 -0
- package/dist/sandbox/enrollment.d.ts +120 -0
- package/dist/sandbox/machines.d.ts +29 -0
- package/dist/sandbox/metrics-ingestion.d.ts +128 -0
- package/dist/sandbox/rematerialize.d.ts +24 -0
- package/dist/sandbox/viewer.d.ts +251 -0
- package/dist/transcription/providers/azure-openai.d.ts +9 -0
- package/dist/transcription/providers/codex-subscription.d.ts +9 -0
- package/dist/transcription/providers/openai.d.ts +9 -0
- package/dist/transcription/service.d.ts +10 -0
- package/dist/workspace-state-projection.d.ts +16 -0
- package/package.json +13 -13
- package/src/app.ts +67 -5
- package/src/integrations/google-drive.ts +869 -0
- package/src/integrations/oauth-client.ts +41 -6
- package/src/integrations/slack-bot.ts +756 -26
- package/src/mcp/server.ts +200 -3
- package/src/mcp/session-view.ts +1 -0
- package/src/mcp/toolspace.ts +17 -9
- package/src/routes/connections.ts +137 -7
- package/src/routes/documents.ts +13 -2
- package/src/routes/github.ts +7 -5
- package/src/routes/insights.ts +30 -0
- package/src/routes/preference-registry.ts +482 -0
- package/src/routes/sessions.ts +10 -0
- package/src/routes/transcriptions.ts +155 -0
- package/src/routes/workspace-state.ts +61 -0
- package/src/sandbox/channel-a.ts +2 -0
- package/src/sandbox/rematerialize.ts +111 -4
- package/src/sandbox/viewer.ts +29 -12
- package/src/transcription/providers/azure-openai.ts +45 -0
- package/src/transcription/providers/codex-subscription.ts +100 -0
- package/src/transcription/providers/openai.ts +93 -0
- package/src/transcription/service.ts +121 -0
- package/src/workspace-state-projection.ts +244 -0
- package/dist/chunk-5TULDPWX.js.map +0 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {
|
|
2
|
+
WORKSPACE_STATE_MAX_BASES,
|
|
3
|
+
WORKSPACE_STATE_MAX_TOPICS,
|
|
4
|
+
WORKSPACE_STATE_TOPIC_MAX_CHARS,
|
|
5
|
+
WorkspaceStateResponse,
|
|
6
|
+
} from "@opengeni/contracts";
|
|
7
|
+
import { hasPermission, requireAccessGrant, type ApiRouteDeps } from "@opengeni/core";
|
|
8
|
+
import {
|
|
9
|
+
getWorkspace,
|
|
10
|
+
listWorkspaceStateMemoryRecords,
|
|
11
|
+
listWorkspaceInstructionPolicyRevisions,
|
|
12
|
+
} from "@opengeni/db";
|
|
13
|
+
import { getDocumentInventory } from "@opengeni/documents";
|
|
14
|
+
import type { Hono } from "hono";
|
|
15
|
+
import { HTTPException } from "hono/http-exception";
|
|
16
|
+
|
|
17
|
+
import { projectWorkspaceState } from "../workspace-state-projection";
|
|
18
|
+
|
|
19
|
+
export function registerWorkspaceStateRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
20
|
+
app.get("/v1/workspaces/:workspaceId/workspace-state", async (context) => {
|
|
21
|
+
const workspaceId = context.req.param("workspaceId");
|
|
22
|
+
const grant = await requireAccessGrant(context, deps, workspaceId, "workspace:read");
|
|
23
|
+
const generatedAt = new Date().toISOString();
|
|
24
|
+
const canInspectKnowledge = hasPermission(grant.permissions, "documents:search");
|
|
25
|
+
|
|
26
|
+
const [workspace, policies, knowledge] = await Promise.all([
|
|
27
|
+
getWorkspace(deps.db, workspaceId),
|
|
28
|
+
listWorkspaceInstructionPolicyRevisions(deps.db, workspaceId, { limit: 1 }),
|
|
29
|
+
canInspectKnowledge
|
|
30
|
+
? (async () => {
|
|
31
|
+
const [documents, memories] = await Promise.all([
|
|
32
|
+
getDocumentInventory(deps.db, workspaceId, {
|
|
33
|
+
baseLimit: WORKSPACE_STATE_MAX_BASES,
|
|
34
|
+
topicLimit: WORKSPACE_STATE_MAX_TOPICS,
|
|
35
|
+
topicMaxChars: WORKSPACE_STATE_TOPIC_MAX_CHARS,
|
|
36
|
+
access: { viewerSubjectId: grant.subjectId },
|
|
37
|
+
}),
|
|
38
|
+
listWorkspaceStateMemoryRecords(deps.db, workspaceId),
|
|
39
|
+
]);
|
|
40
|
+
return { documents, memories };
|
|
41
|
+
})()
|
|
42
|
+
: Promise.resolve(null),
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
if (!workspace) {
|
|
46
|
+
throw new HTTPException(404, { message: "workspace not found" });
|
|
47
|
+
}
|
|
48
|
+
context.header("cache-control", "private, no-store");
|
|
49
|
+
return context.json(
|
|
50
|
+
WorkspaceStateResponse.parse(
|
|
51
|
+
projectWorkspaceState({
|
|
52
|
+
workspaceId,
|
|
53
|
+
generatedAt,
|
|
54
|
+
workspaceAgentInstructions: workspace.agentInstructions,
|
|
55
|
+
policies,
|
|
56
|
+
knowledge,
|
|
57
|
+
}),
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
}
|
package/src/sandbox/channel-a.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
applyGitAuthPointerEnvironment,
|
|
22
22
|
hasGitCredentialRepositorySelection,
|
|
23
23
|
hasGitHubRepositorySelection,
|
|
24
|
+
sandboxArchiveCaptureTimeoutMs,
|
|
24
25
|
stableSandboxEnvironmentForRun,
|
|
25
26
|
type Settings,
|
|
26
27
|
} from "@opengeni/config";
|
|
@@ -247,6 +248,7 @@ export async function withChannelA<T>(
|
|
|
247
248
|
os: session.sandboxOs,
|
|
248
249
|
leaseTtlMs,
|
|
249
250
|
warmingLeaseTtlMs: settings.sandboxWarmingTimeoutMs,
|
|
251
|
+
captureWaitMs: sandboxArchiveCaptureTimeoutMs(settings),
|
|
250
252
|
});
|
|
251
253
|
|
|
252
254
|
if (acquired.role === "blocked") {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Settings } from "@opengeni/config";
|
|
2
2
|
import {
|
|
3
|
+
adoptLegacyModalCheckpointArtifact,
|
|
3
4
|
beginSandboxRematerialization,
|
|
4
5
|
commitWarmingToWarm,
|
|
5
6
|
failSandboxRematerialization,
|
|
@@ -12,9 +13,13 @@ import {
|
|
|
12
13
|
type LeaseSnapshot,
|
|
13
14
|
} from "@opengeni/db";
|
|
14
15
|
import {
|
|
16
|
+
describeLegacyNativeSnapshotArchive,
|
|
15
17
|
establishSandboxSessionFromEnvelope,
|
|
16
18
|
isProviderSandboxNotFoundError,
|
|
19
|
+
modalSessionMatchesCheckpointProviderBinding,
|
|
20
|
+
parseWorkspaceArchiveDescriptor,
|
|
17
21
|
requirePersistableReplacementSandboxEnvelope,
|
|
22
|
+
resolveModalCheckpointProviderBindingForSession,
|
|
18
23
|
serializeReplacementSandboxEnvelope,
|
|
19
24
|
tagModalSandbox,
|
|
20
25
|
verifySandboxExecReadiness,
|
|
@@ -33,6 +38,20 @@ function hasWorkspaceArchive(envelope: Record<string, unknown> | null): boolean
|
|
|
33
38
|
);
|
|
34
39
|
}
|
|
35
40
|
|
|
41
|
+
function legacyNativeArchiveFromEnvelope(envelope: Record<string, unknown> | null) {
|
|
42
|
+
const sessionState =
|
|
43
|
+
envelope?.sessionState && typeof envelope.sessionState === "object"
|
|
44
|
+
? (envelope.sessionState as Record<string, unknown>)
|
|
45
|
+
: null;
|
|
46
|
+
if (!sessionState) return null;
|
|
47
|
+
const existing = parseWorkspaceArchiveDescriptor(sessionState.workspaceArchiveMeta);
|
|
48
|
+
if (existing?.version === 2) return null;
|
|
49
|
+
return describeLegacyNativeSnapshotArchive(
|
|
50
|
+
sessionState.workspaceArchive,
|
|
51
|
+
existing?.version === 1 ? Date.parse(existing.capturedAt) : Date.now(),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
36
55
|
function withoutProviderIdentity(
|
|
37
56
|
envelope: Record<string, unknown> | null,
|
|
38
57
|
): Record<string, unknown> | null {
|
|
@@ -48,7 +67,9 @@ function withoutProviderIdentity(
|
|
|
48
67
|
|
|
49
68
|
async function terminateCreated(established: EstablishedSandboxSession | null): Promise<boolean> {
|
|
50
69
|
if (!established) return true;
|
|
51
|
-
const client = established.client as {
|
|
70
|
+
const client = established.client as {
|
|
71
|
+
delete?: (state: unknown) => Promise<unknown>;
|
|
72
|
+
};
|
|
52
73
|
try {
|
|
53
74
|
if (typeof client.delete === "function" && established.sessionState !== undefined) {
|
|
54
75
|
await client.delete(established.sessionState);
|
|
@@ -93,20 +114,30 @@ export async function establishApiSandboxSpawner(input: {
|
|
|
93
114
|
hasWorkspaceArchive(input.fallbackEnvelope)
|
|
94
115
|
? withoutProviderIdentity(input.fallbackEnvelope)
|
|
95
116
|
: null;
|
|
96
|
-
|
|
117
|
+
let spawnEnvelope =
|
|
97
118
|
fallbackArchiveEnvelope ?? input.acquiredLease.resumeState ?? input.fallbackEnvelope;
|
|
98
119
|
const archiveSource =
|
|
99
120
|
input.acquiredLease.recovery.archive.status === "none"
|
|
100
121
|
? fallbackArchiveEnvelope
|
|
101
122
|
: input.acquiredLease.resumeState;
|
|
102
123
|
let established: EstablishedSandboxSession | null = null;
|
|
103
|
-
let rematerialization: {
|
|
124
|
+
let rematerialization: {
|
|
125
|
+
id: string;
|
|
126
|
+
selectedRevision: string;
|
|
127
|
+
workspaceGeneration: number;
|
|
128
|
+
providerBindingKey: string | null;
|
|
129
|
+
legacyCheckpoint: ReturnType<typeof legacyNativeArchiveFromEnvelope>;
|
|
130
|
+
legacyProviderBinding: Awaited<
|
|
131
|
+
ReturnType<typeof resolveModalCheckpointProviderBindingForSession>
|
|
132
|
+
> | null;
|
|
133
|
+
} | null = null;
|
|
104
134
|
try {
|
|
105
135
|
if (
|
|
106
136
|
input.acquiredLease.recovery.archive.status === "available" ||
|
|
107
137
|
hasWorkspaceArchive(archiveSource)
|
|
108
138
|
) {
|
|
109
139
|
const id = crypto.randomUUID();
|
|
140
|
+
const legacyNativeArchive = legacyNativeArchiveFromEnvelope(archiveSource);
|
|
110
141
|
const begun = await beginSandboxRematerialization(input.db, {
|
|
111
142
|
accountId: input.accountId,
|
|
112
143
|
workspaceId: input.workspaceId,
|
|
@@ -114,6 +145,7 @@ export async function establishApiSandboxSpawner(input: {
|
|
|
114
145
|
expectedEpoch: input.expectedEpoch,
|
|
115
146
|
rematerializationId: id,
|
|
116
147
|
archiveSource,
|
|
148
|
+
legacyNativeArchive,
|
|
117
149
|
});
|
|
118
150
|
if (begun.status !== "started") {
|
|
119
151
|
if (begun.code === "stale_epoch" || begun.code === "attempt_conflict") {
|
|
@@ -129,6 +161,7 @@ export async function establishApiSandboxSpawner(input: {
|
|
|
129
161
|
begun.lease?.recovery ?? input.acquiredLease.recovery,
|
|
130
162
|
);
|
|
131
163
|
}
|
|
164
|
+
spawnEnvelope = begun.lease.resumeState ?? spawnEnvelope;
|
|
132
165
|
const selectedRevision = begun.lease.recovery.restore.selectedRevision;
|
|
133
166
|
if (!selectedRevision) {
|
|
134
167
|
throw new WorkspaceArchiveIntegrityError(
|
|
@@ -136,7 +169,14 @@ export async function establishApiSandboxSpawner(input: {
|
|
|
136
169
|
"sandbox rematerialization selected no durable archive revision",
|
|
137
170
|
);
|
|
138
171
|
}
|
|
139
|
-
rematerialization = {
|
|
172
|
+
rematerialization = {
|
|
173
|
+
id,
|
|
174
|
+
selectedRevision,
|
|
175
|
+
workspaceGeneration: begun.lease.workspaceGeneration,
|
|
176
|
+
providerBindingKey: begun.checkpointArtifact?.providerBindingKey ?? null,
|
|
177
|
+
legacyCheckpoint: begun.checkpointArtifact === null ? legacyNativeArchive : null,
|
|
178
|
+
legacyProviderBinding: null,
|
|
179
|
+
};
|
|
140
180
|
} else if (input.acquiredLease.recovery.archive.status !== "none") {
|
|
141
181
|
throw new SandboxLeaseRecoveryBlockedError(
|
|
142
182
|
input.sandboxGroupId,
|
|
@@ -146,6 +186,7 @@ export async function establishApiSandboxSpawner(input: {
|
|
|
146
186
|
);
|
|
147
187
|
}
|
|
148
188
|
|
|
189
|
+
const providerCreateStartedAt = new Date();
|
|
149
190
|
established = await establishSandboxSessionFromEnvelope(input.settings, spawnEnvelope, {
|
|
150
191
|
sessionId: input.sessionId,
|
|
151
192
|
recovery: "create-or-restore",
|
|
@@ -153,6 +194,37 @@ export async function establishApiSandboxSpawner(input: {
|
|
|
153
194
|
environment: input.environment,
|
|
154
195
|
onSandboxCreated: async (created) => {
|
|
155
196
|
established = created;
|
|
197
|
+
if (
|
|
198
|
+
rematerialization &&
|
|
199
|
+
(rematerialization.providerBindingKey || rematerialization.legacyCheckpoint)
|
|
200
|
+
) {
|
|
201
|
+
if (created.backendId !== "modal") {
|
|
202
|
+
throw new WorkspaceArchiveIntegrityError(
|
|
203
|
+
"native_snapshot_reference_invalid",
|
|
204
|
+
"Modal checkpoint restore resolved a non-Modal sandbox backend",
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
if (rematerialization.providerBindingKey) {
|
|
208
|
+
if (
|
|
209
|
+
!(await modalSessionMatchesCheckpointProviderBinding(
|
|
210
|
+
input.settings,
|
|
211
|
+
created.session,
|
|
212
|
+
rematerialization.providerBindingKey,
|
|
213
|
+
))
|
|
214
|
+
) {
|
|
215
|
+
throw new WorkspaceArchiveIntegrityError(
|
|
216
|
+
"native_snapshot_reference_invalid",
|
|
217
|
+
"Modal checkpoint restore refused because the authenticated provider workspace changed",
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
rematerialization.legacyProviderBinding =
|
|
222
|
+
await resolveModalCheckpointProviderBindingForSession(
|
|
223
|
+
input.settings,
|
|
224
|
+
created.session,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
156
228
|
const resumeState = requirePersistableReplacementSandboxEnvelope(
|
|
157
229
|
await serializeReplacementSandboxEnvelope(created, spawnEnvelope),
|
|
158
230
|
created.backendId,
|
|
@@ -166,6 +238,14 @@ export async function establishApiSandboxSpawner(input: {
|
|
|
166
238
|
instanceId: created.instanceId,
|
|
167
239
|
resumeBackendId: created.backendId,
|
|
168
240
|
resumeState,
|
|
241
|
+
...(created.backendId === "modal"
|
|
242
|
+
? {
|
|
243
|
+
providerCreatedAt: providerCreateStartedAt,
|
|
244
|
+
providerDeadlineAt: new Date(
|
|
245
|
+
providerCreateStartedAt.getTime() + input.settings.modalTimeoutSeconds * 1000,
|
|
246
|
+
),
|
|
247
|
+
}
|
|
248
|
+
: {}),
|
|
169
249
|
leaseTtlMs: input.settings.sandboxLeaseTtlMs,
|
|
170
250
|
warmingLeaseTtlMs: input.settings.sandboxWarmingTimeoutMs,
|
|
171
251
|
});
|
|
@@ -187,6 +267,33 @@ export async function establishApiSandboxSpawner(input: {
|
|
|
187
267
|
`hydrated archive revision ${descriptor.revision} does not match the selected rematerialization revision`,
|
|
188
268
|
);
|
|
189
269
|
}
|
|
270
|
+
if (rematerialization.legacyCheckpoint) {
|
|
271
|
+
const binding = rematerialization.legacyProviderBinding;
|
|
272
|
+
if (!binding) {
|
|
273
|
+
throw new WorkspaceArchiveIntegrityError(
|
|
274
|
+
"native_snapshot_reference_invalid",
|
|
275
|
+
"Legacy Modal checkpoint restore produced no authenticated provider identity",
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
const adopted = await adoptLegacyModalCheckpointArtifact(input.db, {
|
|
279
|
+
accountId: input.accountId,
|
|
280
|
+
workspaceId: input.workspaceId,
|
|
281
|
+
sandboxGroupId: input.sandboxGroupId,
|
|
282
|
+
leaseId: input.acquiredLease.id,
|
|
283
|
+
leaseEpoch: input.expectedEpoch,
|
|
284
|
+
workspaceGeneration: rematerialization.workspaceGeneration,
|
|
285
|
+
slot: "current",
|
|
286
|
+
archiveBase64: rematerialization.legacyCheckpoint.archiveBase64,
|
|
287
|
+
descriptor: rematerialization.legacyCheckpoint.descriptor,
|
|
288
|
+
providerBindingKey: binding.key,
|
|
289
|
+
providerBinding: binding.binding,
|
|
290
|
+
rematerializationId: rematerialization.id,
|
|
291
|
+
});
|
|
292
|
+
if (!adopted) {
|
|
293
|
+
throw new SandboxLeaseSupersededError(input.sandboxGroupId, input.expectedEpoch);
|
|
294
|
+
}
|
|
295
|
+
rematerialization.providerBindingKey = binding.key;
|
|
296
|
+
}
|
|
190
297
|
const verifying = await markSandboxRestoreVerifying(input.db, {
|
|
191
298
|
accountId: input.accountId,
|
|
192
299
|
workspaceId: input.workspaceId,
|
package/src/sandbox/viewer.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
hasGitCredentialRepositorySelection,
|
|
25
25
|
hasGitHubRepositorySelection,
|
|
26
26
|
resolveStreamTokenSecret,
|
|
27
|
+
sandboxArchiveCaptureTimeoutMs,
|
|
27
28
|
stableSandboxEnvironmentForRun,
|
|
28
29
|
} from "@opengeni/config";
|
|
29
30
|
import type { Settings } from "@opengeni/config";
|
|
@@ -42,6 +43,7 @@ import {
|
|
|
42
43
|
recordLeaseTerminalDataPlaneUrl,
|
|
43
44
|
releaseLeaseHolder,
|
|
44
45
|
SandboxLeaseSupersededError,
|
|
46
|
+
SandboxViewerAdmissionBlockedError,
|
|
45
47
|
type Database,
|
|
46
48
|
type LeaseSnapshot,
|
|
47
49
|
type SandboxRecord,
|
|
@@ -195,18 +197,33 @@ export async function attachViewer(
|
|
|
195
197
|
});
|
|
196
198
|
};
|
|
197
199
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
200
|
+
let acquired: Awaited<ReturnType<typeof acquireLease>>;
|
|
201
|
+
try {
|
|
202
|
+
acquired = await acquireLease(db, {
|
|
203
|
+
accountId,
|
|
204
|
+
workspaceId,
|
|
205
|
+
sandboxGroupId,
|
|
206
|
+
kind: "viewer",
|
|
207
|
+
holderId: viewerId,
|
|
208
|
+
subjectId: session.id,
|
|
209
|
+
backend: session.sandboxBackend,
|
|
210
|
+
os: session.sandboxOs,
|
|
211
|
+
leaseTtlMs,
|
|
212
|
+
warmingLeaseTtlMs: settings.sandboxWarmingTimeoutMs,
|
|
213
|
+
captureWaitMs: sandboxArchiveCaptureTimeoutMs(settings),
|
|
214
|
+
});
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if (error instanceof SandboxViewerAdmissionBlockedError) {
|
|
217
|
+
throw new HTTPException(error.reason === "balance" ? 402 : 429, {
|
|
218
|
+
message:
|
|
219
|
+
error.reason === "balance"
|
|
220
|
+
? "insufficient OpenGeni credits for an idle sandbox viewer"
|
|
221
|
+
: "workspace sandbox warm allowance exhausted",
|
|
222
|
+
cause: error,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
210
227
|
|
|
211
228
|
// FENCED: a newer epoch re-established the box. Release our just-registered
|
|
212
229
|
// holder and surface a 409 — the client re-reads capabilities and re-attaches.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { TranscriptionProvider } from "@opengeni/core";
|
|
2
|
+
import { fetchError, responseError } from "./openai";
|
|
3
|
+
|
|
4
|
+
export function createAzureOpenAiTranscriptionProvider(input: {
|
|
5
|
+
endpoint: string;
|
|
6
|
+
deployment: string;
|
|
7
|
+
apiVersion: string;
|
|
8
|
+
apiKey: string | null;
|
|
9
|
+
adToken: string | null;
|
|
10
|
+
fetch?: typeof fetch;
|
|
11
|
+
}): TranscriptionProvider {
|
|
12
|
+
const fetchImpl = input.fetch ?? fetch;
|
|
13
|
+
const url = `${input.endpoint}/openai/deployments/${encodeURIComponent(input.deployment)}/audio/transcriptions?api-version=${encodeURIComponent(input.apiVersion)}`;
|
|
14
|
+
return {
|
|
15
|
+
id: "azure-openai",
|
|
16
|
+
available: () => Boolean(input.apiKey || input.adToken),
|
|
17
|
+
async transcribe({ audio, mimeType, filename, signal }) {
|
|
18
|
+
const form = new FormData();
|
|
19
|
+
form.append("file", new Blob([Uint8Array.from(audio).buffer], { type: mimeType }), filename);
|
|
20
|
+
const headers: Record<string, string> = input.apiKey
|
|
21
|
+
? { "api-key": input.apiKey }
|
|
22
|
+
: { Authorization: `Bearer ${input.adToken}` };
|
|
23
|
+
let response: Response;
|
|
24
|
+
try {
|
|
25
|
+
response = await fetchImpl(url, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers,
|
|
28
|
+
body: form,
|
|
29
|
+
...(signal ? { signal } : {}),
|
|
30
|
+
});
|
|
31
|
+
} catch (error) {
|
|
32
|
+
throw fetchError(error);
|
|
33
|
+
}
|
|
34
|
+
if (!response.ok) throw responseError(response.status);
|
|
35
|
+
const body = await response.json().catch(() => null);
|
|
36
|
+
if (!body || typeof body.text !== "string") {
|
|
37
|
+
throw responseError(502);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
text: body.text,
|
|
41
|
+
languages: typeof body.language === "string" && body.language ? [body.language] : [],
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { CODEX_CLIENT_VERSION, CODEX_ORIGINATOR } from "@opengeni/codex/constants";
|
|
2
|
+
import type { Settings } from "@opengeni/config";
|
|
3
|
+
import {
|
|
4
|
+
type TranscriptionAvailabilityContext,
|
|
5
|
+
type TranscriptionProvider,
|
|
6
|
+
TranscriptionServiceError,
|
|
7
|
+
} from "@opengeni/core";
|
|
8
|
+
import { buildCodexTokenResolver, type Database, listCodexAccountStatuses } from "@opengeni/db";
|
|
9
|
+
import { fetchError, responseError } from "./openai";
|
|
10
|
+
|
|
11
|
+
const TRANSCRIBE_URL = "https://chatgpt.com/backend-api/transcribe";
|
|
12
|
+
|
|
13
|
+
async function workspaceHasActiveCodexAccount(db: Database, workspaceId: string): Promise<boolean> {
|
|
14
|
+
const account = (await listCodexAccountStatuses(db, workspaceId)).find(
|
|
15
|
+
(candidate) => candidate.isActive && candidate.status === "active",
|
|
16
|
+
);
|
|
17
|
+
return account != null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createCodexSubscriptionTranscriptionProvider(input: {
|
|
21
|
+
settings: Settings;
|
|
22
|
+
db: Database;
|
|
23
|
+
fetch?: typeof fetch;
|
|
24
|
+
probe?: (context?: TranscriptionAvailabilityContext) => boolean | Promise<boolean>;
|
|
25
|
+
}): TranscriptionProvider {
|
|
26
|
+
const fetchImpl = input.fetch ?? fetch;
|
|
27
|
+
const probe =
|
|
28
|
+
input.probe ??
|
|
29
|
+
(async (context?: TranscriptionAvailabilityContext) => {
|
|
30
|
+
// Deployment-level readiness: registry construction already gated this
|
|
31
|
+
// provider on OPENGENI_CODEX_SUBSCRIPTION_ENABLED. Request selection
|
|
32
|
+
// passes workspaceId so we only claim Codex when a subscription is
|
|
33
|
+
// attached; otherwise OpenAI/Azure remain eligible.
|
|
34
|
+
if (!context?.workspaceId) return true;
|
|
35
|
+
return await workspaceHasActiveCodexAccount(input.db, context.workspaceId);
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
id: "codex-subscription",
|
|
39
|
+
experimental: true,
|
|
40
|
+
available: probe,
|
|
41
|
+
async transcribe({ audio, mimeType, filename, workspaceId, signal }) {
|
|
42
|
+
const account = (await listCodexAccountStatuses(input.db, workspaceId)).find(
|
|
43
|
+
(candidate) => candidate.isActive && candidate.status === "active",
|
|
44
|
+
);
|
|
45
|
+
if (!account) {
|
|
46
|
+
throw new TranscriptionServiceError({
|
|
47
|
+
code: "unavailable",
|
|
48
|
+
message: "Transcription is unavailable.",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const resolver = buildCodexTokenResolver(input.db, input.settings, workspaceId, account.id);
|
|
52
|
+
let token: Awaited<ReturnType<typeof resolver.getToken>>;
|
|
53
|
+
try {
|
|
54
|
+
token = await resolver.getToken();
|
|
55
|
+
} catch {
|
|
56
|
+
throw new TranscriptionServiceError({
|
|
57
|
+
code: "unavailable",
|
|
58
|
+
message: "Transcription is unavailable.",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const request = async (accessToken: string, accountId: string | null) => {
|
|
62
|
+
const form = new FormData();
|
|
63
|
+
form.append(
|
|
64
|
+
"file",
|
|
65
|
+
new Blob([Uint8Array.from(audio).buffer], { type: mimeType }),
|
|
66
|
+
filename,
|
|
67
|
+
);
|
|
68
|
+
return await fetchImpl(TRANSCRIBE_URL, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: {
|
|
71
|
+
Authorization: `Bearer ${accessToken}`,
|
|
72
|
+
...(accountId ? { "ChatGPT-Account-ID": accountId } : {}),
|
|
73
|
+
originator: CODEX_ORIGINATOR,
|
|
74
|
+
"User-Agent": `${CODEX_ORIGINATOR}/${CODEX_CLIENT_VERSION}`,
|
|
75
|
+
version: CODEX_CLIENT_VERSION,
|
|
76
|
+
},
|
|
77
|
+
body: form,
|
|
78
|
+
...(signal ? { signal } : {}),
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
let response: Response;
|
|
82
|
+
try {
|
|
83
|
+
response = await request(token.accessToken, token.chatgptAccountId);
|
|
84
|
+
if (response.status === 401) {
|
|
85
|
+
token = await resolver.refresh();
|
|
86
|
+
response = await request(token.accessToken, token.chatgptAccountId);
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
throw fetchError(error);
|
|
90
|
+
}
|
|
91
|
+
if (!response.ok) throw responseError(response.status);
|
|
92
|
+
const body = await response.json().catch(() => null);
|
|
93
|
+
if (!body || typeof body.text !== "string") throw responseError(502);
|
|
94
|
+
return {
|
|
95
|
+
text: body.text,
|
|
96
|
+
languages: typeof body.language === "string" && body.language ? [body.language] : [],
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { type TranscriptionProvider, TranscriptionServiceError } from "@opengeni/core";
|
|
2
|
+
|
|
3
|
+
export function createOpenAiTranscriptionProvider(input: {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
baseUrl: string;
|
|
6
|
+
model: string;
|
|
7
|
+
fetch?: typeof fetch;
|
|
8
|
+
}): TranscriptionProvider {
|
|
9
|
+
const fetchImpl = input.fetch ?? fetch;
|
|
10
|
+
return {
|
|
11
|
+
id: "openai",
|
|
12
|
+
available: () => true,
|
|
13
|
+
async transcribe({ audio, mimeType, filename, signal }) {
|
|
14
|
+
const form = new FormData();
|
|
15
|
+
form.append("file", audioBlob(audio, mimeType), filename);
|
|
16
|
+
form.append("model", input.model);
|
|
17
|
+
let response: Response;
|
|
18
|
+
try {
|
|
19
|
+
response = await fetchImpl(`${input.baseUrl}/audio/transcriptions`, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: { Authorization: `Bearer ${input.apiKey}` },
|
|
22
|
+
body: form,
|
|
23
|
+
...(signal ? { signal } : {}),
|
|
24
|
+
});
|
|
25
|
+
} catch (error) {
|
|
26
|
+
throw fetchError(error);
|
|
27
|
+
}
|
|
28
|
+
if (!response.ok) throw responseError(response.status);
|
|
29
|
+
const body = await response.json().catch(() => null);
|
|
30
|
+
if (!body || typeof body.text !== "string") {
|
|
31
|
+
throw new TranscriptionServiceError({
|
|
32
|
+
code: "provider",
|
|
33
|
+
message: "Invalid transcription response.",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
text: body.text,
|
|
38
|
+
languages: typeof body.language === "string" && body.language ? [body.language] : [],
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function audioBlob(audio: Uint8Array, mimeType: string): Blob {
|
|
45
|
+
return new Blob([Uint8Array.from(audio).buffer], { type: mimeType });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function responseError(status: number): TranscriptionServiceError {
|
|
49
|
+
if (status === 401 || status === 403) {
|
|
50
|
+
return new TranscriptionServiceError({
|
|
51
|
+
code: "unavailable",
|
|
52
|
+
message: "Transcription is unavailable.",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (status === 413) {
|
|
56
|
+
return new TranscriptionServiceError({
|
|
57
|
+
code: "too_large",
|
|
58
|
+
message: "Audio is too large.",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (status === 400 || status === 422) {
|
|
62
|
+
return new TranscriptionServiceError({
|
|
63
|
+
code: "invalid_audio",
|
|
64
|
+
message: "Audio could not be transcribed.",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (status === 408 || status === 504) {
|
|
68
|
+
return new TranscriptionServiceError({
|
|
69
|
+
code: "timeout",
|
|
70
|
+
message: "Transcription timed out.",
|
|
71
|
+
retryable: true,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return new TranscriptionServiceError({
|
|
75
|
+
code: status >= 500 ? "unavailable" : "provider",
|
|
76
|
+
message: "Transcription provider failed.",
|
|
77
|
+
retryable: status >= 500,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function fetchError(error: unknown): TranscriptionServiceError {
|
|
82
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
83
|
+
return new TranscriptionServiceError({
|
|
84
|
+
code: "cancelled",
|
|
85
|
+
message: "Transcription was cancelled.",
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return new TranscriptionServiceError({
|
|
89
|
+
code: "network",
|
|
90
|
+
message: "Transcription provider is unreachable.",
|
|
91
|
+
retryable: true,
|
|
92
|
+
});
|
|
93
|
+
}
|