@opengeni/core 0.20.16 → 0.21.10

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.
@@ -0,0 +1,36 @@
1
+ import type { Context } from "hono";
2
+ import type { ManagedAuth } from "./managed-auth-type";
3
+
4
+ /**
5
+ * Read a Better Auth session without bypassing its sliding-cookie renewal.
6
+ *
7
+ * Better Auth can refresh the durable session while resolving `getSession`.
8
+ * Programmatic callers must explicitly request and forward the returned cookie
9
+ * headers; the HTTP handler does this automatically, but direct API calls do not.
10
+ */
11
+ export async function getManagedSession(c: Context, auth: ManagedAuth) {
12
+ const result = await auth.api.getSession({
13
+ headers: c.req.raw.headers,
14
+ returnHeaders: true,
15
+ });
16
+
17
+ for (const cookie of setCookieHeaders(result.headers)) {
18
+ c.header("set-cookie", cookie, { append: true });
19
+ }
20
+
21
+ return result.response;
22
+ }
23
+
24
+ function setCookieHeaders(headers: Headers): string[] {
25
+ const getSetCookie = (
26
+ headers as Headers & {
27
+ getSetCookie?: () => string[];
28
+ }
29
+ ).getSetCookie;
30
+ if (getSetCookie) {
31
+ return getSetCookie.call(headers);
32
+ }
33
+
34
+ const cookie = headers.get("set-cookie");
35
+ return cookie ? [cookie] : [];
36
+ }
@@ -27,6 +27,7 @@ import {
27
27
  import type { EventBus } from "@opengeni/events";
28
28
  import {
29
29
  NatsControlRpc,
30
+ NatsOpStreamTransport,
30
31
  selfhostedLiveness,
31
32
  SelfhostedSession,
32
33
  swapTargetEstablishability,
@@ -34,6 +35,7 @@ import {
34
35
  type ControlRpc,
35
36
  type NatsRequestConnection,
36
37
  type SelfhostedRelayConfig,
38
+ type SelfhostedOpStreamDeps,
37
39
  } from "@opengeni/runtime/sandbox";
38
40
  import { HTTPException } from "hono/http-exception";
39
41
  import { relayConfigFromSettings } from "./routing";
@@ -95,6 +97,7 @@ export async function buildFleetContextForSession(
95
97
 
96
98
  /** The dominant liveness of a fleet member, surfaced to the dock + the agent. */
97
99
  export type FleetLiveness = "online" | "reconnecting" | "offline";
100
+ export type FleetOperationAvailability = "ready" | "wakeable" | "recovering" | "unavailable";
98
101
 
99
102
  /**
100
103
  * A fleet member as the agent + the dock see it (the M8b/M9 UI seam — the
@@ -117,6 +120,11 @@ export type FleetSandboxEntry = {
117
120
  enrollmentId: string | null;
118
121
  /** Whether this target can be attached/swapped to right now (live + addressable). */
119
122
  attachable: boolean;
123
+ /** Whether an ordinary shell/files operation can use this target. This is
124
+ * deliberately separate from `attachable`: an idle managed home sandbox can
125
+ * be wakeable even while its holderless lease is cold/draining and therefore
126
+ * not an already-live swap target. */
127
+ operationAvailability: FleetOperationAvailability;
120
128
  /** Selfhosted only: whether whole-machine + screen-control consent is acked. */
121
129
  consented?: boolean;
122
130
  /** Selfhosted only: whether a display (real/Xvfb) is present. */
@@ -254,6 +262,22 @@ export async function listFleet(
254
262
  groupLease.recovery.restore.status === "restoring" ||
255
263
  groupLease.recovery.restore.status === "verifying"),
256
264
  );
265
+ const groupRecoveryUnavailable = Boolean(
266
+ groupLease &&
267
+ (groupLease.recovery.restore.status === "degraded" ||
268
+ groupLease.recovery.restore.status === "unrecoverable" ||
269
+ groupLease.recovery.workspace.status === "degraded" ||
270
+ groupLease.recovery.workspace.status === "unrecoverable"),
271
+ );
272
+ const groupOperationAvailability: FleetOperationAvailability = groupOnline
273
+ ? "ready"
274
+ : groupRecoveryUnavailable
275
+ ? "unavailable"
276
+ : groupRecovering
277
+ ? "recovering"
278
+ : ctx.sessionBackend === "selfhosted"
279
+ ? "unavailable"
280
+ : "wakeable";
257
281
  entries.push({
258
282
  id: ctx.sessionGroupId,
259
283
  kind: ctx.sessionBackend === "selfhosted" ? "selfhosted" : "modal",
@@ -263,6 +287,7 @@ export async function listFleet(
263
287
  isSessionGroup: true,
264
288
  enrollmentId: null,
265
289
  attachable: groupOnline,
290
+ operationAvailability: groupOperationAvailability,
266
291
  providerStatus: groupLease?.recovery.provider.status ?? "not_created",
267
292
  leaseLiveness: groupLease?.liveness ?? null,
268
293
  routeStatus: groupActive ? "attached" : "detached",
@@ -284,6 +309,11 @@ export async function listFleet(
284
309
  continue;
285
310
  }
286
311
  const enrollment = await getEnrollment(db, ctx.workspaceId, sandbox.enrollmentId);
312
+ if (!enrollment || enrollment.status !== "active") {
313
+ // Revoked enrollments remain durable audit/history records, but they are
314
+ // intentionally absent from the normal attach/run picker.
315
+ continue;
316
+ }
287
317
  const probe = enrollment
288
318
  ? await probeEnrollment(services, ctx.workspaceId, enrollment)
289
319
  : { liveness: "offline" as FleetLiveness, consented: false, hasDisplay: false };
@@ -296,6 +326,12 @@ export async function listFleet(
296
326
  isSessionGroup: false,
297
327
  enrollmentId: sandbox.enrollmentId,
298
328
  attachable: probe.liveness === "online",
329
+ operationAvailability:
330
+ probe.liveness === "online"
331
+ ? "ready"
332
+ : probe.liveness === "reconnecting"
333
+ ? "recovering"
334
+ : "unavailable",
299
335
  consented: probe.consented,
300
336
  hasDisplay: probe.hasDisplay,
301
337
  lastSeenAt: enrollment?.lastSeenAt ?? null,
@@ -533,6 +569,8 @@ export type RunOnSelfhostedMachine = {
533
569
  controlTimeoutMs: number;
534
570
  /** Longer agent-side process deadline for exec. */
535
571
  execTimeoutMs: number;
572
+ /** Streaming transport required when execTimeoutMs is 0 (unbounded). */
573
+ opStream?: SelfhostedOpStreamDeps;
536
574
  };
537
575
 
538
576
  /**
@@ -553,6 +591,7 @@ export async function executeRunOnSelfhostedMachine(
553
591
  relay: machine.relay,
554
592
  timeoutMs: machine.controlTimeoutMs,
555
593
  execTimeoutMs: machine.execTimeoutMs,
594
+ ...(machine.opStream !== undefined ? { opStream: machine.opStream } : {}),
556
595
  });
557
596
 
558
597
  try {
@@ -600,6 +639,11 @@ export async function executeRunOnSelfhostedMachine(
600
639
  // so leave `timedOut` absent while still reporting the enforced deadline.
601
640
  ...(op.kind === "exec" ? { deadlineMs: session.effectiveExecDeadlineMs } : {}),
602
641
  };
642
+ } finally {
643
+ // This one-off has no turn journal. Once its result has been accepted by
644
+ // this call, final-ack any settled stream so the runner can immediately
645
+ // release replay/output retention instead of waiting for TTL cleanup.
646
+ await session.finalizeOpStreamOps().catch(() => undefined);
603
647
  }
604
648
  }
605
649
 
@@ -655,6 +699,17 @@ export async function runOnSandbox(
655
699
  relay: relayConfigFromSettings(services.settings),
656
700
  controlTimeoutMs: services.settings.sandboxSelfhostedControlTimeoutMs,
657
701
  execTimeoutMs: services.settings.sandboxSelfhostedExecTimeoutMs,
702
+ ...(services.settings.agentOpStreamEnabled === true &&
703
+ enrollment.opStream === true &&
704
+ services.bus?.getOpStreamConnection
705
+ ? {
706
+ opStream: {
707
+ transport: new NatsOpStreamTransport(
708
+ async () => services.bus?.getOpStreamConnection?.() ?? null,
709
+ ),
710
+ },
711
+ }
712
+ : {}),
658
713
  },
659
714
  target,
660
715
  op,
@@ -692,7 +747,7 @@ export async function provisionSandbox(
692
747
  return {
693
748
  kind: "selfhosted",
694
749
  instructions:
695
- "Share these instructions with a human operator. They install the OpenGeni agent on the machine, run `opengeni-agent enroll`, complete the device-flow at the verification URL (the loud whole-machine + screen-control consent), and the machine then appears here as an attachable selfhosted sandbox.",
750
+ "Share these instructions with a human operator. They install the OpenGeni agent on the machine, run `opengeni-agent connect`, complete the device-flow at the verification URL (the loud whole-machine + screen-control consent), and the machine then appears here as an attachable selfhosted sandbox. Existing connections to other OpenGeni workspaces or deployments are preserved.",
696
751
  // Install from THIS control plane's origin (not a hardcoded public CDN): the
697
752
  // served install script is rewritten to pull the per-SHA agent baked into
698
753
  // this exact deployment (see apps/api/src/routes/install.ts), so a deployed
@@ -15,6 +15,7 @@ import { sandboxArchiveCaptureTimeoutMs, type Settings } from "@opengeni/config"
15
15
  import {
16
16
  advanceWorkspaceGenerationForDirectRequest,
17
17
  advanceWorkspaceGenerationForRetainedProcess,
18
+ getEnrollment,
18
19
  getRetainedProcess,
19
20
  getSandbox,
20
21
  markWarmLeaseInstanceLost,
@@ -32,6 +33,7 @@ import {
32
33
  isProviderSandboxGoneDuringRoutedOperation,
33
34
  makeActiveBackendResolver,
34
35
  NatsControlRpc,
36
+ NatsOpStreamTransport,
35
37
  RoutingSandboxSession,
36
38
  resolveModalCheckpointProviderBindingForSession,
37
39
  type ControlRpc,
@@ -44,6 +46,7 @@ import {
44
46
  type RoutingRetainedProcessTerminalProof,
45
47
  type RoutingSandboxOperationObserver,
46
48
  type SelfhostedRelayConfig,
49
+ type SelfhostedOpStreamDeps,
47
50
  } from "@opengeni/runtime/sandbox";
48
51
 
49
52
  type PersistableMutationAdmission = {
@@ -114,6 +117,27 @@ function controlRpcFactory(bus: EventBus | undefined): () => ControlRpc {
114
117
  });
115
118
  }
116
119
 
120
+ async function resolveSelfhostedOpStream(
121
+ services: ChannelARoutingServices,
122
+ workspaceId: string,
123
+ sandbox: RoutableSandbox,
124
+ ): Promise<SelfhostedOpStreamDeps | undefined> {
125
+ if (
126
+ services.settings.agentOpStreamEnabled !== true ||
127
+ !services.bus?.getOpStreamConnection ||
128
+ !sandbox.enrollmentId
129
+ ) {
130
+ return undefined;
131
+ }
132
+ const enrollment = await getEnrollment(services.db, workspaceId, sandbox.enrollmentId);
133
+ if (enrollment?.opStream !== true) return undefined;
134
+ return {
135
+ transport: new NatsOpStreamTransport(
136
+ async () => services.bus?.getOpStreamConnection?.() ?? null,
137
+ ),
138
+ };
139
+ }
140
+
117
141
  /** Whether the routing proxy should wrap the Channel-A box: gated by the
118
142
  * selfhosted flag (the active pointer + swap are only meaningful then). */
119
143
  export function routingEnabled(settings: Settings): boolean {
@@ -389,6 +413,8 @@ export function wrapChannelABoxWithRouting(
389
413
  : null;
390
414
  },
391
415
  controlRpcFactory: controlRpcFactory(bus),
416
+ resolveSelfhostedOpStream: (sandbox) =>
417
+ resolveSelfhostedOpStream(services, ids.workspaceId, sandbox),
392
418
  relay: relayConfigFromSettings(settings),
393
419
  selfhostedTimeoutMs: settings.sandboxSelfhostedControlTimeoutMs,
394
420
  selfhostedExecTimeoutMs: settings.sandboxSelfhostedExecTimeoutMs,
@@ -46,6 +46,23 @@ export type ResolvedSessionAuthorization = {
46
46
  reauthorizeAfterMs: number | null;
47
47
  };
48
48
 
49
+ /**
50
+ * Prove that a first-party request belongs to the exact currently active
51
+ * attempt of the named caller session. Unlike the optional embedding-host ACL
52
+ * port, this database fence is mandatory for high-trust operations.
53
+ */
54
+ export async function requireLiveAgentAttemptAuthorization(
55
+ db: Database,
56
+ grant: AccessGrant,
57
+ callerSessionId: string,
58
+ ): Promise<Extract<SessionAuthorizationActor, { kind: "agent_attempt" }>> {
59
+ const actor = await resolveSessionAuthorizationActor(db, grant);
60
+ if (actor.kind !== "agent_attempt" || actor.callerSessionId !== callerSessionId) {
61
+ throw new SessionAuthorizationDeniedError("caller_stale");
62
+ }
63
+ return actor;
64
+ }
65
+
49
66
  /**
50
67
  * Resolve and enforce the host ACL for one session. The target and agent actor
51
68
  * are reconstructed from workspace-scoped durable state. A request can supply
@@ -1,5 +1,15 @@
1
1
  import type { TranscribeAudioResponse, VoiceInputErrorCode } from "@opengeni/contracts";
2
2
 
3
+ /**
4
+ * Server-owned upstream budget for one provider attempt. Resumable recording
5
+ * claims remain fenced for longer than this budget before another worker may
6
+ * reclaim them. Provider adapters must honor the supplied AbortSignal and must
7
+ * not return while their upstream request is still live; OpenGeni does not
8
+ * claim remote-side idempotency or cancellation for vendors that cannot meet
9
+ * that adapter contract.
10
+ */
11
+ export const TRANSCRIPTION_PROVIDER_REQUEST_TIMEOUT_MILLISECONDS = 10 * 60 * 1_000;
12
+
3
13
  export type TranscriptionLimits = {
4
14
  maxDurationSeconds: number;
5
15
  maxSizeBytes: number;
@@ -15,6 +25,10 @@ export type TranscriptionRequest = {
15
25
  durationSeconds?: number | undefined;
16
26
  signal?: AbortSignal | undefined;
17
27
  requestId: string;
28
+ /** Absolute server-owned provider deadline persisted for resumable attempts. */
29
+ providerDeadlineAt?: Date | undefined;
30
+ /** Exact provider selected before a resumable segment is first sent upstream. */
31
+ providerId?: string | undefined;
18
32
  };
19
33
 
20
34
  export type TranscriptionResult = TranscribeAudioResponse & {
@@ -82,6 +96,8 @@ export type TranscriptionAvailabilityContext = {
82
96
  */
83
97
  export type TranscriptionProvider = {
84
98
  readonly id: string;
99
+ /** The adapter guarantees that its upstream transport honors AbortSignal. */
100
+ readonly supportsServerDeadline: true;
85
101
  readonly experimental?: boolean | undefined;
86
102
  /**
87
103
  * Deployment readiness when called without a workspace. When `workspaceId` is
@@ -93,6 +109,7 @@ export type TranscriptionProvider = {
93
109
  mimeType: string;
94
110
  filename: string;
95
111
  workspaceId: string;
112
+ requestId: string;
96
113
  signal?: AbortSignal | undefined;
97
114
  }): Promise<{ text: string; languages: string[] }>;
98
115
  };
@@ -101,9 +118,32 @@ export type TranscriptionService = {
101
118
  limits(): TranscriptionLimits;
102
119
  /** True when at least one ready provider can serve requests. */
103
120
  available(context?: TranscriptionAvailabilityContext): boolean | Promise<boolean>;
121
+ /** Select one provider before a durable segment attempt; retries pin this id. */
122
+ selectProvider?(
123
+ context: TranscriptionAvailabilityContext,
124
+ ): string | null | Promise<string | null>;
104
125
  transcribe(request: TranscriptionRequest): Promise<TranscriptionResult>;
105
126
  };
106
127
 
128
+ export type PreparedTranscriptionSegment = {
129
+ segmentNumber: number;
130
+ startMilliseconds: number;
131
+ durationMilliseconds: number;
132
+ mimeType: "audio/wav";
133
+ bytes: Uint8Array;
134
+ };
135
+
136
+ export type TranscriptionSegmenter = {
137
+ available(): boolean | Promise<boolean>;
138
+ segment(input: {
139
+ sourceMimeType: string;
140
+ totalDurationMilliseconds: number;
141
+ providerSegmentSeconds: number;
142
+ chunks: AsyncIterable<Uint8Array>;
143
+ signal?: AbortSignal | undefined;
144
+ }): AsyncIterable<PreparedTranscriptionSegment>;
145
+ };
146
+
107
147
  export function normalizeMimeType(mimeType: string): string {
108
148
  return mimeType.trim().toLowerCase();
109
149
  }