@opengeni/core 0.12.7 → 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.
Files changed (39) hide show
  1. package/dist/access/index.d.ts +22 -0
  2. package/dist/application/new-session-drafts.d.ts +14 -0
  3. package/dist/application/session-commands.d.ts +107 -0
  4. package/dist/billing/limits.d.ts +29 -0
  5. package/dist/dependencies.d.ts +137 -0
  6. package/dist/domain/capabilities.d.ts +62 -0
  7. package/dist/domain/environments.d.ts +33 -0
  8. package/dist/domain/insights.d.ts +11 -0
  9. package/dist/domain/packs.d.ts +27 -0
  10. package/dist/domain/resources.d.ts +32 -0
  11. package/dist/domain/scheduled-tasks.d.ts +72 -0
  12. package/dist/domain/session-tool-policy.d.ts +31 -0
  13. package/dist/domain/sessions.d.ts +256 -0
  14. package/dist/domain/slack-bot.d.ts +19 -0
  15. package/dist/domain/workspace-members.d.ts +34 -0
  16. package/dist/index.d.ts +23 -1189
  17. package/dist/index.js +758 -115
  18. package/dist/index.js.map +1 -1
  19. package/dist/managed-auth-type.d.ts +2 -0
  20. package/dist/rigs/index.d.ts +57 -0
  21. package/dist/sandbox/fleet.d.ts +197 -0
  22. package/dist/sandbox/routing.d.ts +55 -0
  23. package/dist/sandbox-types.d.ts +52 -0
  24. package/dist/session-authorization.d.ts +36 -0
  25. package/dist/transcription.d.ts +71 -0
  26. package/dist/workflow-wake-contract.d.ts +4 -0
  27. package/package.json +11 -11
  28. package/src/access/index.ts +73 -2
  29. package/src/application/new-session-drafts.ts +3 -0
  30. package/src/application/session-commands.ts +22 -9
  31. package/src/dependencies.ts +5 -0
  32. package/src/domain/insights.ts +480 -0
  33. package/src/domain/session-tool-policy.ts +22 -39
  34. package/src/domain/sessions.ts +140 -72
  35. package/src/domain/slack-bot.ts +2 -4
  36. package/src/index.ts +2 -0
  37. package/src/sandbox/fleet.ts +96 -33
  38. package/src/sandbox/routing.ts +29 -7
  39. package/src/transcription.ts +142 -0
package/src/index.ts CHANGED
@@ -35,6 +35,7 @@ export * from "./workflow-wake-contract";
35
35
  // structural TYPES live here.
36
36
  export * from "./sandbox-types";
37
37
  export * from "./managed-auth-type";
38
+ export * from "./transcription";
38
39
 
39
40
  // Sandbox fleet/routing service — the closure of `domain/sessions.ts`
40
41
  // (`swapActiveSandbox` + `FleetContext`). apps/api re-imports these for its MCP
@@ -60,6 +61,7 @@ export * from "./domain/resources";
60
61
  export * from "./domain/session-tool-policy";
61
62
  export * from "./domain/scheduled-tasks";
62
63
  export * from "./domain/sessions";
64
+ export * from "./domain/insights";
63
65
  export * from "./domain/slack-bot";
64
66
  export * from "./domain/workspace-members";
65
67
  export * from "./application/new-session-drafts";
@@ -33,6 +33,7 @@ import {
33
33
  type BackendUnresolvableCode,
34
34
  type ControlRpc,
35
35
  type NatsRequestConnection,
36
+ type SelfhostedRelayConfig,
36
37
  } from "@opengeni/runtime/sandbox";
37
38
  import { HTTPException } from "hono/http-exception";
38
39
  import { relayConfigFromSettings } from "./routing";
@@ -512,11 +513,94 @@ export type RunOnResult = {
512
513
  stdout?: string;
513
514
  stderr?: string;
514
515
  exitCode?: number | null;
516
+ /** Exec only: whether the machine killed the child at its process deadline. */
517
+ timedOut?: boolean;
518
+ /** Exec only: the effective clamped process deadline enforced by the machine. */
519
+ deadlineMs?: number;
515
520
  content?: string;
516
521
  bytesWritten?: number;
517
522
  reason?: string;
518
523
  };
519
524
 
525
+ export type RunOnSelfhostedMachine = {
526
+ workspaceId: string;
527
+ agentId: string;
528
+ controlRpc: ControlRpc;
529
+ relay: SelfhostedRelayConfig;
530
+ /** Short request/reply deadline for read/write and other control operations. */
531
+ controlTimeoutMs: number;
532
+ /** Longer agent-side process deadline for exec. */
533
+ execTimeoutMs: number;
534
+ };
535
+
536
+ /**
537
+ * Execute the one-off machine operation once the workspace/enrollment lookup has
538
+ * succeeded. Kept separate from {@link runOnSandbox} so the command/deadline
539
+ * contract is deterministic against an in-memory ControlRpc without weakening
540
+ * the production ownership lookup or requiring a real machine.
541
+ */
542
+ export async function executeRunOnSelfhostedMachine(
543
+ machine: RunOnSelfhostedMachine,
544
+ target: string,
545
+ op: RunOnOp,
546
+ ): Promise<RunOnResult> {
547
+ const session = new SelfhostedSession({
548
+ workspaceId: machine.workspaceId,
549
+ agentId: machine.agentId,
550
+ controlRpc: machine.controlRpc,
551
+ relay: machine.relay,
552
+ timeoutMs: machine.controlTimeoutMs,
553
+ execTimeoutMs: machine.execTimeoutMs,
554
+ });
555
+
556
+ try {
557
+ if (op.kind === "exec") {
558
+ const deadlineMs = session.effectiveExecDeadlineMs;
559
+ const res = await session.exec({
560
+ cmd: op.cmd,
561
+ ...(op.workdir ? { workdir: op.workdir } : {}),
562
+ });
563
+ const timedOut = res.timedOut === true;
564
+ const hasTerminalExit = res.exitCode !== null;
565
+ return {
566
+ target,
567
+ kind: "exec",
568
+ // `ok` means the one-off operation reached a terminal response. Preserve
569
+ // the established non-zero-exit behavior, but never claim success when
570
+ // the machine killed the child or returned no terminal exit proof.
571
+ ok: !timedOut && hasTerminalExit,
572
+ stdout: res.stdout,
573
+ stderr: res.stderr,
574
+ exitCode: res.exitCode,
575
+ timedOut,
576
+ deadlineMs,
577
+ ...(timedOut
578
+ ? { reason: `command exceeded the ${deadlineMs} ms execution deadline` }
579
+ : !hasTerminalExit
580
+ ? { reason: "machine returned no terminal exit code" }
581
+ : {}),
582
+ };
583
+ }
584
+ if (op.kind === "read") {
585
+ const bytes = await session.readFile({ path: op.path });
586
+ return { target, kind: "read", ok: true, content: new TextDecoder().decode(bytes) };
587
+ }
588
+ const bytesWritten = await session.writeFile({ path: op.path, content: op.content });
589
+ return { target, kind: "write", ok: true, bytesWritten };
590
+ } catch (error) {
591
+ const reason = error instanceof Error ? error.message : String(error);
592
+ return {
593
+ target,
594
+ kind: op.kind,
595
+ ok: false,
596
+ reason,
597
+ // A transport failure is not evidence that the process itself timed out,
598
+ // so leave `timedOut` absent while still reporting the enforced deadline.
599
+ ...(op.kind === "exec" ? { deadlineMs: session.effectiveExecDeadlineMs } : {}),
600
+ };
601
+ }
602
+ }
603
+
520
604
  /**
521
605
  * Run a ONE-OFF op against a SPECIFIC target WITHOUT changing the active pointer
522
606
  * (the design `run_on`). Only selfhosted targets are routable as a one-off here
@@ -554,39 +638,18 @@ export async function runOnSandbox(
554
638
  return { target, kind: op.kind, ok: false, reason: `sandbox ${target} is not enrolled/active` };
555
639
  }
556
640
 
557
- const session = new SelfhostedSession({
558
- workspaceId: ctx.workspaceId,
559
- agentId: sandbox.enrollmentId,
560
- controlRpc: controlRpc(services.bus),
561
- relay: relayConfigFromSettings(services.settings),
562
- });
563
-
564
- try {
565
- if (op.kind === "exec") {
566
- const res = await session.exec({
567
- cmd: op.cmd,
568
- ...(op.workdir ? { workdir: op.workdir } : {}),
569
- });
570
- return {
571
- target,
572
- kind: "exec",
573
- ok: true,
574
- stdout: res.stdout,
575
- stderr: res.stderr,
576
- exitCode: res.exitCode,
577
- };
578
- }
579
- if (op.kind === "read") {
580
- const bytes = await session.readFile({ path: op.path });
581
- return { target, kind: "read", ok: true, content: new TextDecoder().decode(bytes) };
582
- }
583
- // write
584
- const bytesWritten = await session.writeFile({ path: op.path, content: op.content });
585
- return { target, kind: "write", ok: true, bytesWritten };
586
- } catch (error) {
587
- const reason = error instanceof Error ? error.message : String(error);
588
- return { target, kind: op.kind, ok: false, reason };
589
- }
641
+ return executeRunOnSelfhostedMachine(
642
+ {
643
+ workspaceId: ctx.workspaceId,
644
+ agentId: sandbox.enrollmentId,
645
+ controlRpc: controlRpc(services.bus),
646
+ relay: relayConfigFromSettings(services.settings),
647
+ controlTimeoutMs: services.settings.sandboxSelfhostedControlTimeoutMs,
648
+ execTimeoutMs: services.settings.sandboxSelfhostedExecTimeoutMs,
649
+ },
650
+ target,
651
+ op,
652
+ );
590
653
  }
591
654
 
592
655
  export type ProvisionResult =
@@ -11,7 +11,7 @@
11
11
  // The DB-coupled glue (readActiveSandbox / getSandbox / the selfhosted ControlRpc
12
12
  // over the events bus) lives here, not in the leaf (which stays db-free).
13
13
 
14
- import type { Settings } from "@opengeni/config";
14
+ import { sandboxArchiveCaptureTimeoutMs, type Settings } from "@opengeni/config";
15
15
  import {
16
16
  advanceWorkspaceGenerationForDirectRequest,
17
17
  advanceWorkspaceGenerationForRetainedProcess,
@@ -33,6 +33,7 @@ import {
33
33
  makeActiveBackendResolver,
34
34
  NatsControlRpc,
35
35
  RoutingSandboxSession,
36
+ resolveModalCheckpointProviderBindingForSession,
36
37
  type ControlRpc,
37
38
  type EstablishedSandboxSession,
38
39
  type NatsRequestConnection,
@@ -44,6 +45,13 @@ import {
44
45
  type SelfhostedRelayConfig,
45
46
  } from "@opengeni/runtime/sandbox";
46
47
 
48
+ type PersistableMutationAdmission = {
49
+ admission: SandboxWorkspaceMutationAdmission;
50
+ providerBinding: Awaited<
51
+ ReturnType<typeof resolveModalCheckpointProviderBindingForSession>
52
+ > | null;
53
+ };
54
+
47
55
  export type ChannelARoutingServices = {
48
56
  db: Database;
49
57
  settings: Settings;
@@ -151,7 +159,7 @@ export function wrapChannelABoxWithRouting(
151
159
  }: {
152
160
  op: string;
153
161
  backend: ResolvedActiveBackend;
154
- }): Promise<SandboxWorkspaceMutationAdmission | null> => {
162
+ }): Promise<PersistableMutationAdmission | null> => {
155
163
  // Connected Machines and other non-persistable targets intentionally do
156
164
  // not dirty or advance the cloud-home archive generation.
157
165
  if (
@@ -164,7 +172,11 @@ export function wrapChannelABoxWithRouting(
164
172
  if (backend.activeEpoch === undefined) {
165
173
  throw new Error("API-direct workspace mutation resolved without an active route epoch");
166
174
  }
167
- return await advanceWorkspaceGenerationForDirectRequest(db, {
175
+ const providerBinding =
176
+ homeLease.backend === "modal"
177
+ ? await resolveModalCheckpointProviderBindingForSession(settings, backend.session)
178
+ : null;
179
+ const admission = await advanceWorkspaceGenerationForDirectRequest(db, {
168
180
  accountId: ids.accountId,
169
181
  workspaceId: ids.workspaceId,
170
182
  sessionId: ids.sessionId,
@@ -176,7 +188,9 @@ export function wrapChannelABoxWithRouting(
176
188
  routeTargetId: backend.sandboxId,
177
189
  routeEpoch: backend.activeEpoch,
178
190
  operation: op,
191
+ captureWaitMs: sandboxArchiveCaptureTimeoutMs(settings),
179
192
  });
193
+ return { admission, providerBinding };
180
194
  }
181
195
  : undefined;
182
196
  const afterMutation = homeLease
@@ -198,16 +212,22 @@ export function wrapChannelABoxWithRouting(
198
212
  if (
199
213
  !admission ||
200
214
  typeof admission !== "object" ||
201
- typeof (admission as Partial<SandboxWorkspaceMutationAdmission>).id !== "string" ||
202
- typeof (admission as Partial<SandboxWorkspaceMutationAdmission>).workspaceGeneration !==
203
- "number" ||
204
215
  backend.leaseEpoch === undefined ||
205
216
  backend.providerInstanceId === undefined ||
206
217
  backend.activeEpoch === undefined
207
218
  ) {
208
219
  throw new Error("API-direct workspace mutation settlement lacked its exact admission");
209
220
  }
210
- const exactAdmission = admission as SandboxWorkspaceMutationAdmission;
221
+ const boundAdmission = admission as Partial<PersistableMutationAdmission>;
222
+ const exactAdmission = boundAdmission.admission;
223
+ if (
224
+ !exactAdmission ||
225
+ typeof exactAdmission.id !== "string" ||
226
+ typeof exactAdmission.workspaceGeneration !== "number" ||
227
+ !("providerBinding" in boundAdmission)
228
+ ) {
229
+ throw new Error("API-direct workspace mutation settlement lacked its bound admission");
230
+ }
211
231
  if (outcome === "resolved" && retainedProcess) {
212
232
  await retainWorkspaceMutationProcess(db, {
213
233
  accountId: ids.accountId,
@@ -218,6 +238,7 @@ export function wrapChannelABoxWithRouting(
218
238
  admissionId: exactAdmission.id,
219
239
  admittedWorkspaceGeneration: exactAdmission.workspaceGeneration,
220
240
  operation: op,
241
+ providerBinding: boundAdmission.providerBinding ?? null,
221
242
  owner: {
222
243
  kind: "direct",
223
244
  requestId: ids.directRequest.requestId,
@@ -263,6 +284,7 @@ export function wrapChannelABoxWithRouting(
263
284
  sessionId: ids.sessionId,
264
285
  processId: process.id,
265
286
  operation: op,
287
+ captureWaitMs: sandboxArchiveCaptureTimeoutMs(settings),
266
288
  })
267
289
  : undefined;
268
290
  const afterProcessMutation = homeLease
@@ -0,0 +1,142 @@
1
+ import type { TranscribeAudioResponse, VoiceInputErrorCode } from "@opengeni/contracts";
2
+
3
+ export type TranscriptionLimits = {
4
+ maxDurationSeconds: number;
5
+ maxSizeBytes: number;
6
+ acceptedMimeTypes: readonly string[];
7
+ };
8
+
9
+ export type TranscriptionRequest = {
10
+ workspaceId: string;
11
+ accountId: string;
12
+ audio: Uint8Array;
13
+ mimeType: string;
14
+ /** Optional client-reported duration; enforced as a soft ceiling before upstream. */
15
+ durationSeconds?: number | undefined;
16
+ signal?: AbortSignal | undefined;
17
+ requestId: string;
18
+ };
19
+
20
+ export type TranscriptionResult = TranscribeAudioResponse & {
21
+ /** Server-private provider id for operational metrics only. Never returned to clients. */
22
+ providerId: string;
23
+ audioSeconds: number;
24
+ latencyMs: number;
25
+ };
26
+
27
+ export class TranscriptionServiceError extends Error {
28
+ readonly code: VoiceInputErrorCode;
29
+ readonly status: number;
30
+ readonly retryable: boolean;
31
+
32
+ constructor(input: {
33
+ code: VoiceInputErrorCode;
34
+ message: string;
35
+ status?: number;
36
+ retryable?: boolean;
37
+ }) {
38
+ super(input.message);
39
+ this.name = "TranscriptionServiceError";
40
+ this.code = input.code;
41
+ this.status = input.status ?? statusForVoiceInputError(input.code);
42
+ this.retryable = input.retryable ?? false;
43
+ }
44
+ }
45
+
46
+ export function statusForVoiceInputError(code: VoiceInputErrorCode): number {
47
+ switch (code) {
48
+ case "permission_denied":
49
+ return 403;
50
+ case "policy_blocked":
51
+ return 403;
52
+ case "not_supported":
53
+ return 415;
54
+ case "unavailable":
55
+ return 503;
56
+ case "too_large":
57
+ return 413;
58
+ case "invalid_audio":
59
+ return 400;
60
+ case "timeout":
61
+ return 504;
62
+ case "cancelled":
63
+ return 499;
64
+ case "network":
65
+ case "provider":
66
+ return 502;
67
+ case "unknown":
68
+ default:
69
+ return 500;
70
+ }
71
+ }
72
+
73
+ /** Optional workspace scope for readiness checks during provider selection. */
74
+ export type TranscriptionAvailabilityContext = {
75
+ workspaceId?: string | undefined;
76
+ };
77
+
78
+ /**
79
+ * Extensible transcription provider port. Implementations own credentials and
80
+ * upstream request shape. Selection happens before audio is sent; providers must
81
+ * not fall back to another vendor after an upstream request may have started.
82
+ */
83
+ export type TranscriptionProvider = {
84
+ readonly id: string;
85
+ readonly experimental?: boolean | undefined;
86
+ /**
87
+ * Deployment readiness when called without a workspace. When `workspaceId` is
88
+ * provided, providers may require a workspace-attached credential (e.g. Codex).
89
+ */
90
+ available(context?: TranscriptionAvailabilityContext): boolean | Promise<boolean>;
91
+ transcribe(input: {
92
+ audio: Uint8Array;
93
+ mimeType: string;
94
+ filename: string;
95
+ workspaceId: string;
96
+ signal?: AbortSignal | undefined;
97
+ }): Promise<{ text: string; languages: string[] }>;
98
+ };
99
+
100
+ export type TranscriptionService = {
101
+ limits(): TranscriptionLimits;
102
+ /** True when at least one ready provider can serve requests. */
103
+ available(context?: TranscriptionAvailabilityContext): boolean | Promise<boolean>;
104
+ transcribe(request: TranscriptionRequest): Promise<TranscriptionResult>;
105
+ };
106
+
107
+ export function normalizeMimeType(mimeType: string): string {
108
+ return mimeType.trim().toLowerCase();
109
+ }
110
+
111
+ export function isAcceptedMimeType(mimeType: string, accepted: readonly string[]): boolean {
112
+ const normalized = normalizeMimeType(mimeType);
113
+ if (accepted.some((candidate) => normalizeMimeType(candidate) === normalized)) {
114
+ return true;
115
+ }
116
+ // Allow bare type matches against codec-qualified allowlist entries.
117
+ const bare = normalized.split(";")[0]?.trim() ?? normalized;
118
+ return accepted.some((candidate) => {
119
+ const allowed = normalizeMimeType(candidate);
120
+ return allowed === bare || allowed.split(";")[0]?.trim() === bare;
121
+ });
122
+ }
123
+
124
+ export function filenameForMimeType(mimeType: string): string {
125
+ const bare = normalizeMimeType(mimeType).split(";")[0] ?? "audio/webm";
126
+ switch (bare) {
127
+ case "audio/mp4":
128
+ case "audio/m4a":
129
+ return "audio.mp4";
130
+ case "audio/ogg":
131
+ return "audio.ogg";
132
+ case "audio/mpeg":
133
+ case "audio/mp3":
134
+ return "audio.mp3";
135
+ case "audio/wav":
136
+ case "audio/x-wav":
137
+ return "audio.wav";
138
+ case "audio/webm":
139
+ default:
140
+ return "audio.webm";
141
+ }
142
+ }