@opengeni/api-router 0.12.12 → 0.14.4

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 (87) hide show
  1. package/dist/app.d.ts +18 -38
  2. package/dist/app.js +3 -1
  3. package/dist/auth/managed-auth.d.ts +35 -0
  4. package/dist/{chunk-5TULDPWX.js → chunk-36PW33ND.js} +4111 -1277
  5. package/dist/chunk-36PW33ND.js.map +1 -0
  6. package/dist/codex-redemption-security.d.ts +15 -0
  7. package/dist/github-access.d.ts +12 -0
  8. package/dist/github-browser-flow.d.ts +14 -0
  9. package/dist/http/auth.d.ts +3 -0
  10. package/dist/http/common.d.ts +3 -0
  11. package/dist/http/sse.d.ts +70 -0
  12. package/dist/index.d.ts +11 -13
  13. package/dist/index.js +1 -1
  14. package/dist/integrations/google-drive.d.ts +76 -0
  15. package/dist/integrations/oauth-client.d.ts +42 -0
  16. package/dist/integrations/provider-domain.d.ts +7 -0
  17. package/dist/integrations/slack-bot.d.ts +343 -0
  18. package/dist/mcp/documents.d.ts +8 -0
  19. package/dist/mcp/files.d.ts +5 -0
  20. package/dist/mcp/server.d.ts +107 -0
  21. package/dist/mcp/session-view.d.ts +191 -0
  22. package/dist/mcp/toolspace.d.ts +61 -0
  23. package/dist/model-catalog.d.ts +39 -0
  24. package/dist/observability.d.ts +3 -0
  25. package/dist/routes/api-keys.d.ts +3 -0
  26. package/dist/routes/billing.d.ts +16 -0
  27. package/dist/routes/capabilities.d.ts +3 -0
  28. package/dist/routes/catalog-assets.d.ts +5 -0
  29. package/dist/routes/codex.d.ts +10 -0
  30. package/dist/routes/connections.d.ts +3 -0
  31. package/dist/routes/documents.d.ts +3 -0
  32. package/dist/routes/enrollments.d.ts +3 -0
  33. package/dist/routes/environments.d.ts +5 -0
  34. package/dist/routes/files.d.ts +4 -0
  35. package/dist/routes/github.d.ts +3 -0
  36. package/dist/routes/insights.d.ts +3 -0
  37. package/dist/routes/install.d.ts +5 -0
  38. package/dist/routes/machines.d.ts +3 -0
  39. package/dist/routes/packs.d.ts +3 -0
  40. package/dist/routes/preference-registry.d.ts +5 -0
  41. package/dist/routes/rigs.d.ts +3 -0
  42. package/dist/routes/scheduled-tasks.d.ts +3 -0
  43. package/dist/routes/sessions.d.ts +15 -0
  44. package/dist/routes/social.d.ts +3 -0
  45. package/dist/routes/transcriptions.d.ts +3 -0
  46. package/dist/routes/workspace-capture.d.ts +59 -0
  47. package/dist/routes/workspace-instruction-policies.d.ts +3 -0
  48. package/dist/routes/workspace-state.d.ts +3 -0
  49. package/dist/routes/workspaces.d.ts +5 -0
  50. package/dist/sandbox/access.d.ts +21 -0
  51. package/dist/sandbox/auth-callout.d.ts +30 -0
  52. package/dist/sandbox/channel-a.d.ts +37 -0
  53. package/dist/sandbox/enrollment.d.ts +120 -0
  54. package/dist/sandbox/machines.d.ts +29 -0
  55. package/dist/sandbox/metrics-ingestion.d.ts +128 -0
  56. package/dist/sandbox/rematerialize.d.ts +24 -0
  57. package/dist/sandbox/viewer.d.ts +251 -0
  58. package/dist/transcription/providers/azure-openai.d.ts +9 -0
  59. package/dist/transcription/providers/codex-subscription.d.ts +9 -0
  60. package/dist/transcription/providers/openai.d.ts +9 -0
  61. package/dist/transcription/service.d.ts +10 -0
  62. package/dist/workspace-state-projection.d.ts +16 -0
  63. package/package.json +13 -13
  64. package/src/app.ts +67 -5
  65. package/src/integrations/google-drive.ts +869 -0
  66. package/src/integrations/oauth-client.ts +41 -6
  67. package/src/integrations/slack-bot.ts +756 -26
  68. package/src/mcp/server.ts +200 -3
  69. package/src/mcp/session-view.ts +1 -0
  70. package/src/mcp/toolspace.ts +17 -9
  71. package/src/routes/connections.ts +137 -7
  72. package/src/routes/documents.ts +13 -2
  73. package/src/routes/github.ts +7 -5
  74. package/src/routes/insights.ts +30 -0
  75. package/src/routes/preference-registry.ts +482 -0
  76. package/src/routes/sessions.ts +10 -0
  77. package/src/routes/transcriptions.ts +155 -0
  78. package/src/routes/workspace-state.ts +61 -0
  79. package/src/sandbox/channel-a.ts +2 -0
  80. package/src/sandbox/rematerialize.ts +111 -4
  81. package/src/sandbox/viewer.ts +29 -12
  82. package/src/transcription/providers/azure-openai.ts +45 -0
  83. package/src/transcription/providers/codex-subscription.ts +100 -0
  84. package/src/transcription/providers/openai.ts +93 -0
  85. package/src/transcription/service.ts +121 -0
  86. package/src/workspace-state-projection.ts +244 -0
  87. package/dist/chunk-5TULDPWX.js.map +0 -1
@@ -0,0 +1,21 @@
1
+ import type { Settings } from "@opengeni/config";
2
+ export type { ApiSandboxSession, ApiSandboxClient, ResumeBoxByIdInput, ResumedSandboxSession, } from "@opengeni/core";
3
+ import type { ApiSandboxClient, ResumeBoxByIdInput, ResumedSandboxSession } from "@opengeni/core";
4
+ export declare class SandboxResumeError extends Error {
5
+ readonly cause?: unknown;
6
+ constructor(message: string, cause?: unknown);
7
+ }
8
+ /**
9
+ * Construct the API process's own sandbox client from settings, agent-loop-free.
10
+ * Returns undefined when `sandboxBackend=none` (no box to touch). The Modal
11
+ * token + app name are read from settings (already parsed by getSettings and
12
+ * present in the API runtime env), so the client can resume Modal boxes by id.
13
+ */
14
+ export declare function createApiSandboxClient(settings: Settings): ApiSandboxClient | undefined;
15
+ /**
16
+ * Build the `resumeBoxById` helper bound to the API's sandbox client. Given a
17
+ * backend + a serialized resume_state envelope, it resumes the box and returns
18
+ * a live session for one in-process op. The caller drives exec/readFile and then
19
+ * drops the handle (resume → use → drop); it does NOT own the box.
20
+ */
21
+ export declare function makeResumeBoxById(client: ApiSandboxClient | undefined): (input: ResumeBoxByIdInput) => Promise<ResumedSandboxSession>;
@@ -0,0 +1,30 @@
1
+ import { type NatsCalloutConfig, type Settings } from "@opengeni/config";
2
+ import { type Database } from "@opengeni/db";
3
+ import { type ResponderConnection } from "@opengeni/events";
4
+ import type { Observability } from "@opengeni/observability";
5
+ /** The NATS subject nats-server publishes authorization requests on (ADR-26). */
6
+ export declare const AUTH_CALLOUT_SUBJECT = "$SYS.REQ.USER.AUTH";
7
+ export interface AuthCalloutDeps {
8
+ db: Database;
9
+ settings: Settings;
10
+ callout: NatsCalloutConfig;
11
+ observability?: Observability;
12
+ }
13
+ /**
14
+ * The pure validate→scoped-JWT decision, isolated from the NATS transport so it is
15
+ * unit-testable. Given the raw authorization-request JWT bytes, returns the signed
16
+ * authorization-response JWT bytes to reply with — a GRANT (embedding a scoped user
17
+ * JWT) on success, a DENIAL (carrying `nats.error`, no user JWT) otherwise. NEVER
18
+ * throws on a bad/invalid request: every failure becomes a signed denial (the
19
+ * server then refuses the connection cleanly).
20
+ */
21
+ export declare function handleAuthorizationRequest(deps: AuthCalloutDeps, requestBytes: Uint8Array): Promise<Uint8Array>;
22
+ /**
23
+ * Start the auth-callout responder: open a SEPARATE NATS connection authenticated
24
+ * as the callout `auth_users` user, subscribe $SYS.REQ.USER.AUTH, and answer every
25
+ * authorization request via {@link handleAuthorizationRequest}. Returns a handle
26
+ * whose `close()` drains the connection. Gated by the caller (sandboxSelfhostedEnabled
27
+ * + a resolvable callout config); a deployment without the callout plane never starts
28
+ * it.
29
+ */
30
+ export declare function startAuthCalloutResponder(deps: AuthCalloutDeps, natsUrl: string): Promise<ResponderConnection>;
@@ -0,0 +1,37 @@
1
+ import { type Settings } from "@opengeni/config";
2
+ import type { Session } from "@opengeni/contracts";
3
+ import { type Database, type LeaseSnapshot } from "@opengeni/db";
4
+ import { type EventBus } from "@opengeni/events";
5
+ import { SandboxChannelAService, type RoutingSandboxSession } from "@opengeni/runtime/sandbox";
6
+ export type ChannelAServices = {
7
+ db: Database;
8
+ settings: Settings;
9
+ bus: EventBus;
10
+ };
11
+ export type ChannelAContext = {
12
+ accountId: string;
13
+ workspaceId: string;
14
+ session: Session;
15
+ subjectId: string;
16
+ };
17
+ export type ChannelAHandle = {
18
+ service: SandboxChannelAService;
19
+ /** Connected Machine homes deliberately have no cloud lease. Durable PTYs
20
+ * require a real home-provider lease and reject this null case. */
21
+ lease: LeaseSnapshot | null;
22
+ routingSession: RoutingSandboxSession;
23
+ requestId: string;
24
+ };
25
+ /**
26
+ * Run a Channel-A op against a live box, API-direct. Acquires an exact direct holder
27
+ * (warming the box when cold), resumes by id, builds the service, runs `fn`, and
28
+ * ALWAYS releases the holder + drops the handle in `finally`. Maps the service's
29
+ * typed errors to HTTP status (the route never sees a raw ChannelA*Error).
30
+ *
31
+ * Gated behind sandboxOwnershipEnabled at the route (the lease is dormant
32
+ * otherwise). A `backend:none` session has no box -> 409 before touching it.
33
+ */
34
+ export declare function withChannelA<T>(services: ChannelAServices, ctx: ChannelAContext, fn: (handle: ChannelAHandle) => Promise<T>): Promise<T>;
35
+ /** Map the service's typed errors to HTTP status (the §5.3 matrix). Re-throws an
36
+ * already-HTTPException unchanged. */
37
+ export declare function mapChannelAError(error: unknown): unknown;
@@ -0,0 +1,120 @@
1
+ import { type Settings } from "@opengeni/config";
2
+ import { type DeviceEnrollmentLookupResponse, type DeviceEnrollmentPollResponse, type DeviceEnrollmentStartResponse, type EnrollTokenExchangeResponse, type MintEnrollTokenResponse } from "@opengeni/contracts";
3
+ import { type Database, type DeviceEnrollmentRequestRecord, type EnrollmentOs } from "@opengeni/db";
4
+ export declare const DEVICE_CODE_TTL_SECONDS = 600;
5
+ export declare const DEVICE_POLL_INTERVAL_SECONDS = 5;
6
+ export declare const ENROLLMENT_BEARER_TTL_SECONDS: number;
7
+ export declare const RELAY_TOKEN_TTL_SECONDS: number;
8
+ export declare const ENROLL_TOKEN_TTL_SECONDS = 3600;
9
+ export type EnrollmentServices = {
10
+ db: Database;
11
+ settings: Settings;
12
+ };
13
+ /** A workspace-scoped flow START context (the route resolves the workspace the
14
+ * agent's flow binds to from the deployment edge / a workspace hint). */
15
+ export type DeviceStartInput = {
16
+ accountId: string;
17
+ workspaceId: string;
18
+ publicKey: string;
19
+ os: EnrollmentOs;
20
+ arch: string;
21
+ machineName?: string | null;
22
+ canOfferDisplay: boolean;
23
+ requestsScreenControl: boolean;
24
+ verificationOrigin: string;
25
+ };
26
+ /**
27
+ * START a device-flow: persist a short-TTL single-use pending request + return the
28
+ * DeviceAuthStart. Retries the user_code mint on the (astronomically rare) partial-
29
+ * unique collision among live pending rows.
30
+ */
31
+ export declare function startDeviceEnrollment(services: EnrollmentServices, input: DeviceStartInput): Promise<DeviceEnrollmentStartResponse>;
32
+ /** APPROVE a flow by user_code (the LOUD consent step). Returns the resulting
33
+ * enrollment + sandbox ids, or null when no LIVE pending request matches the code
34
+ * in this workspace (an unknown/expired/already-terminal code). */
35
+ export declare function approveDeviceEnrollment(services: EnrollmentServices, input: {
36
+ accountId: string;
37
+ workspaceId: string;
38
+ userCode: string;
39
+ allowScreenControl: boolean;
40
+ approvedBySubjectId: string;
41
+ approvedBySubjectLabel?: string | null;
42
+ }): Promise<{
43
+ enrollmentId: string;
44
+ sandboxId: string;
45
+ allowScreenControl: boolean;
46
+ } | null>;
47
+ /** LOOK UP a pending flow by user_code GLOBALLY (the click-Grant approve page;
48
+ * design 11 §B.1). Returns the resolved pending record (carrying its workspaceId)
49
+ * or null when no live pending row matches the code. The ROUTE authorizes the
50
+ * caller against the resolved workspaceId (enrollments:read) BEFORE exposing
51
+ * anything — a failed grant OR a null here both surface as 404 so cross-workspace
52
+ * existence is never revealed. This does NOT consume the request. */
53
+ export declare function lookupDeviceEnrollment(services: EnrollmentServices, input: {
54
+ userCode: string;
55
+ }): Promise<DeviceEnrollmentRequestRecord | null>;
56
+ /** Project a resolved pending record to the presentational lookup response (no
57
+ * secrets, no device_code) the approve screen (EnrollmentConsent) renders. */
58
+ export declare function toLookupResponse(record: DeviceEnrollmentRequestRecord): DeviceEnrollmentLookupResponse;
59
+ /** DENY a flow by user_code (the explicit "no" at the approve page; design 11
60
+ * §B.2). Workspace-scoped (the route asserts the grant). Returns whether a pending
61
+ * row was flipped to denied (false for an unknown / already-terminal code). */
62
+ export declare function denyDeviceEnrollment(services: EnrollmentServices, input: {
63
+ accountId: string;
64
+ workspaceId: string;
65
+ userCode: string;
66
+ }): Promise<{
67
+ denied: boolean;
68
+ }>;
69
+ /** MINT a headless enroll token (design 11 §A2.2). Signs an `oget_` token bound to
70
+ * the workspace + account + the screen-control consent, with a 1h TTL. Returns
71
+ * null when the credential plane is disabled (no signing secret) so the route can
72
+ * mirror poll's "disabled" handling. The token value is NEVER logged. */
73
+ export declare function mintEnrollToken(services: EnrollmentServices, input: {
74
+ accountId: string;
75
+ workspaceId: string;
76
+ allowScreenControl: boolean;
77
+ }): Promise<MintEnrollTokenResponse | null>;
78
+ /** Distinguishes the two exchange failure modes for the route. */
79
+ export type ExchangeEnrollTokenResult = {
80
+ ok: true;
81
+ credentials: EnrollTokenExchangeResponse["credentials"];
82
+ } | {
83
+ ok: false;
84
+ reason: "disabled";
85
+ } | {
86
+ ok: false;
87
+ reason: "invalid";
88
+ };
89
+ /** EXCHANGE a headless enroll token (design 11 §A2.3) — the UNAUTHENTICATED path
90
+ * where the token IS the auth. Verifies the `oget_` token, then performs the SAME
91
+ * finalize as approve (upsert enrollment + ensure selfhosted sandbox,
92
+ * consentedWholeMachine=true, consentedScreenControl=token.allowScreenControl) and
93
+ * builds the IDENTICAL EnrollmentCredentials the poll authorized branch returns.
94
+ * Returns reason "disabled" when no signing secret (mirror poll), "invalid" when
95
+ * the token fails verification (the route 401s). */
96
+ export declare function exchangeEnrollToken(services: EnrollmentServices, input: {
97
+ token: string;
98
+ publicKey: string;
99
+ os: EnrollmentOs;
100
+ arch: string;
101
+ machineName?: string | null;
102
+ canOfferDisplay: boolean;
103
+ }): Promise<ExchangeEnrollTokenResult>;
104
+ /**
105
+ * POLL a flow by device_code. Resolves the state machine:
106
+ * - unknown code → "expired" (do not leak existence; an unknown code
107
+ * behaves like an expired one to the agent).
108
+ * - pending + within TTL → "pending".
109
+ * - pending + past TTL → "expired".
110
+ * - denied → "denied".
111
+ * - approved | consumed → "authorized" + the EnrollmentCredentials (the
112
+ * approved row is flipped to consumed; a legitimate
113
+ * re-poll of a consumed row still returns the creds).
114
+ * When the credential plane is disabled (no resolvable signing secret), an
115
+ * otherwise-authorized poll returns "disabled" so the agent surfaces a clear reason
116
+ * rather than half-enrolling.
117
+ */
118
+ export declare function pollDeviceEnrollment(services: EnrollmentServices, input: {
119
+ deviceCode: string;
120
+ }): Promise<DeviceEnrollmentPollResponse>;
@@ -0,0 +1,29 @@
1
+ import type { Settings } from "@opengeni/config";
2
+ import { type Database, type MachineMetricsRow } from "@opengeni/db";
3
+ import type { EventBus } from "@opengeni/events";
4
+ import { MetricSample, type MachinesResponse } from "@opengeni/contracts";
5
+ export type MachinesServices = {
6
+ db: Database;
7
+ settings: Settings;
8
+ bus?: EventBus;
9
+ };
10
+ /**
11
+ * Project a stored `machine_metrics_latest` row to the contract `MetricSample`.
12
+ * The DB carries `gpuUtilPercent` + `gpuMemUsedBytes`/`gpuMemTotalBytes`; the wire
13
+ * `MetricSample` exposes the single `gpuUtilPct` + `gpuMemBytes` (USED bytes — the
14
+ * "how much VRAM is in use" the dashboard reads). A null any-numeric stays null
15
+ * (the not-reported contract); the byte/load fields default to 0 when a sample
16
+ * carried no value (the agent reports 0 == not-reported for those).
17
+ */
18
+ export declare function metricRowToSample(row: MachineMetricsRow): MetricSample;
19
+ /**
20
+ * Build the Machines dashboard response for a workspace. When `sessionId` is
21
+ * supplied (an in-session view) the session's synthetic Modal group box is
22
+ * prepended (`isSessionGroup:true`) and the active-sandbox pointer is echoed;
23
+ * without it (the pure workspace dashboard) `activeSandboxId` is null and only
24
+ * the enrolled machines are listed.
25
+ */
26
+ export declare function listMachines(services: MachinesServices, input: {
27
+ workspaceId: string;
28
+ sessionId?: string | null;
29
+ }): Promise<MachinesResponse>;
@@ -0,0 +1,128 @@
1
+ import { type Database, type MachineMetricsSample } from "@opengeni/db";
2
+ import { type EventBus } from "@opengeni/events";
3
+ import type { Observability } from "@opengeni/observability";
4
+ import { Hello, type MetricsSample } from "@opengeni/agent-proto";
5
+ /** The wildcard subject the agent event plane publishes heartbeats on. */
6
+ export declare const AGENT_EVENTS_SUBJECT = "agent.*.*.events";
7
+ /** The wildcard subject the agent publishes its connect Hello on. */
8
+ export declare const AGENT_HELLO_SUBJECT = "agent.*.*.hello";
9
+ /** Parse `agent.<ws>.<id>.events` → `{ workspaceId, agentId }` (heartbeat plane). */
10
+ export declare function parseAgentEventSubject(subject: string): {
11
+ workspaceId: string;
12
+ agentId: string;
13
+ } | null;
14
+ /** Parse `agent.<ws>.<id>.hello` → `{ workspaceId, agentId }` (connect plane). */
15
+ export declare function parseAgentHelloSubject(subject: string): {
16
+ workspaceId: string;
17
+ agentId: string;
18
+ } | null;
19
+ /**
20
+ * Project a wire `MetricsSample` (proto, ms-stamped, GPU as a repeated list) to
21
+ * the DB `MachineMetricsSample`. The proto byte/count fields are protobuf-encoded
22
+ * as decimal strings (uint64) on the TS side (ts-proto `string`); coerce to
23
+ * numbers. The DB carries a single `gpuUtilPercent` + `gpuMemUsedBytes`/Total —
24
+ * we take the FIRST GPU (the dashboard surfaces the primary accelerator); absent
25
+ * GPUs stay null (the not-reported contract). A zero on a non-GPU field is the
26
+ * agent's "not reported" (we keep it null-friendly via `nullIfZero` only for the
27
+ * GPU plane; cpu/mem/disk 0 is a legitimate reading the dashboard shows as 0).
28
+ */
29
+ export declare function wireSampleToDbSample(wire: MetricsSample): MachineMetricsSample;
30
+ /**
31
+ * Ingest ONE decoded heartbeat for an enrolled machine. Resolves the enrollment's
32
+ * accountId (needed for the RLS-scoped writes) from the enrollment row; an
33
+ * unknown/cross-workspace agentId is ignored (no row → no write). Touches
34
+ * last-seen + upserts latest + downsamples the series.
35
+ */
36
+ export declare function ingestHeartbeat(db: Database, input: {
37
+ workspaceId: string;
38
+ agentId: string;
39
+ sample: MetricsSample;
40
+ }): Promise<{
41
+ ingested: boolean;
42
+ seriesAppended: boolean;
43
+ }>;
44
+ /**
45
+ * Decode a raw `AgentEvent` payload + ingest it (the per-message handler). A
46
+ * heartbeat carrying a metrics sample is ingested; a going-offline records the
47
+ * machine-plane marker + fans out the link-plane session events. Decode failures
48
+ * are reported + swallowed. `bus` (when present) enables the session-event
49
+ * fan-out; the live consumer always supplies it, pure unit tests may omit it.
50
+ */
51
+ export declare function handleAgentEventPayload(db: Database, observability: Observability | undefined, payload: Uint8Array, subject: string, bus?: EventBus): Promise<void>;
52
+ /**
53
+ * Start the metrics-ingestion consumer: subscribe `agent.*.*.events` and ingest
54
+ * every heartbeat. Gated by sandboxSelfhostedEnabled (the caller checks the flag;
55
+ * a disabled deployment never starts the consumer). Returns the unsubscribe fn.
56
+ */
57
+ export declare function startMetricsIngestion(deps: {
58
+ db: Database;
59
+ bus: EventBus;
60
+ observability?: Observability;
61
+ }): () => void;
62
+ /**
63
+ * The LIVE display presence the agent's Hello reports: a desktop framebuffer is
64
+ * available (`capabilities.desktop`, which the agent sets true only when a display
65
+ * probes AND it can stream it) OR a `Display` detail is present. An unset
66
+ * Capabilities (or a headless machine) → false. This is what `has_display` should
67
+ * track, replacing the enroll-time snapshot.
68
+ */
69
+ export declare function helloReportsDisplay(hello: Hello): boolean;
70
+ /**
71
+ * The human, actionable reason a display is present but UNUSABLE (macOS Screen
72
+ * Recording / TCC not granted), or null when capture is permitted / the machine is
73
+ * headless. Normalizes the proto's non-optional "" empty string to null so the DB
74
+ * carries a clean tri-state (a real reason vs. no reason) — the Machines dashboard
75
+ * shows "display: capture not granted" only when this is non-null.
76
+ */
77
+ export declare function helloDesktopUnavailableReason(hello: Hello): string | null;
78
+ /** Whether the runner's current Hello advertises the op-stream engine. */
79
+ export declare function helloReportsOpStream(hello: Hello): boolean;
80
+ /**
81
+ * Reconcile `enrollments.has_display` (+ the capture-blocked reason) to what a Hello
82
+ * reports. Resolves the enrollment (the accountId is the RLS principal + the
83
+ * existence check + the current values). A no-change Hello short-circuits BEFORE
84
+ * issuing any write (and the DB writer is itself change-guarded on BOTH fields as a
85
+ * backstop), so a steady state never churns. An unknown/cross-workspace agentId is a
86
+ * no-op.
87
+ */
88
+ export declare function refreshEnrollmentDisplay(db: Database, input: {
89
+ workspaceId: string;
90
+ agentId: string;
91
+ hasDisplay: boolean;
92
+ desktopUnavailableReason?: string | null;
93
+ }): Promise<{
94
+ updated: boolean;
95
+ }>;
96
+ /**
97
+ * Reconcile `enrollments.op_stream` to what a Hello reports. Resolves the
98
+ * enrollment first so the accountId remains the RLS principal and so a no-change
99
+ * Hello short-circuits BEFORE issuing any write (the DB writer is itself
100
+ * change-guarded as a backstop). An unknown/cross-workspace agentId is a no-op.
101
+ */
102
+ export declare function refreshEnrollmentOpStream(db: Database, input: {
103
+ workspaceId: string;
104
+ agentId: string;
105
+ opStream: boolean;
106
+ }): Promise<{
107
+ updated: boolean;
108
+ }>;
109
+ /**
110
+ * Decode a raw `Hello` payload + refresh the enrollment's display cursor + clear
111
+ * any pending clean going-offline marker and, when the reconnect actually cleared
112
+ * one, fan out machine.link.restored to the sessions with an active op on the
113
+ * machine (the per-message handler for the hello plane). Decode failures + write
114
+ * failures are reported + swallowed — a Hello must NEVER break the agent's connect.
115
+ * `bus` (when present) enables the link.restored fan-out.
116
+ */
117
+ export declare function handleHelloPayload(db: Database, observability: Observability | undefined, payload: Uint8Array, subject: string, bus?: EventBus): Promise<void>;
118
+ /**
119
+ * Start the Hello display-refresh consumer: subscribe `agent.*.*.hello` and
120
+ * reconcile `has_display` to the live capability the agent reports on every
121
+ * connect. Gated by sandboxSelfhostedEnabled (the caller checks the flag). Returns
122
+ * the unsubscribe fn.
123
+ */
124
+ export declare function startHelloIngestion(deps: {
125
+ db: Database;
126
+ bus: EventBus;
127
+ observability?: Observability;
128
+ }): () => void;
@@ -0,0 +1,24 @@
1
+ import type { Settings } from "@opengeni/config";
2
+ import { type Database, type LeaseSnapshot } from "@opengeni/db";
3
+ import { type EstablishedSandboxSession } from "@opengeni/runtime/sandbox";
4
+ /** The sole API-direct cold->warming owner path used by Channel A and viewer
5
+ * attach. It never publishes warm until archive identity, hydrated tree, command
6
+ * routing, provider identity, and the selected rematerialization attempt all
7
+ * agree under one lease epoch. */
8
+ export declare function establishApiSandboxSpawner(input: {
9
+ db: Database;
10
+ settings: Settings;
11
+ accountId: string;
12
+ workspaceId: string;
13
+ sandboxGroupId: string;
14
+ sessionId: string;
15
+ backend: string;
16
+ environment: Record<string, string>;
17
+ expectedEpoch: number;
18
+ acquiredLease: LeaseSnapshot;
19
+ fallbackEnvelope: Record<string, unknown> | null;
20
+ dataPlaneUrl: string | null;
21
+ }): Promise<{
22
+ established: EstablishedSandboxSession;
23
+ lease: LeaseSnapshot;
24
+ }>;
@@ -0,0 +1,251 @@
1
+ import type { Settings } from "@opengeni/config";
2
+ import { type Session } from "@opengeni/contracts";
3
+ import { type Database, type LeaseSnapshot, type SandboxRecord } from "@opengeni/db";
4
+ import { type EventBus } from "@opengeni/events";
5
+ import { establishSandboxSessionFromEnvelope, type EstablishedSandboxSession } from "@opengeni/runtime/sandbox";
6
+ /** The minimal services a viewer op needs: the DB + settings (lease cadence +
7
+ * the sandbox client construction the leaf reads from settings). The bus is
8
+ * optional — only the rotation path (emitting stream.url.rotated to OTHER
9
+ * viewers) needs it. */
10
+ export type ViewerServices = {
11
+ db: Database;
12
+ settings: Settings;
13
+ bus?: EventBus;
14
+ /** Provider-establish dependency used by API-direct readiness and stream
15
+ * operations. Production uses the runtime leaf; isolated tests may supply a
16
+ * deterministic provider without replacing a process-global module. */
17
+ establishSandboxSession?: typeof establishSandboxSessionFromEnvelope;
18
+ };
19
+ /** A coherent snapshot the routes echo back: the holder id (the viewer's fence-
20
+ * carrying handle), the lease liveness/epoch, and the recorded data-plane URL
21
+ * (null until P4 mints the desktop tunnel). */
22
+ export type ViewerAttachResult = {
23
+ viewerId: string;
24
+ liveness: LeaseSnapshot["liveness"];
25
+ leaseEpoch: number;
26
+ workspaceGeneration: number | null;
27
+ archiveGeneration: number | null;
28
+ archiveComplete: boolean;
29
+ sandboxGroupId: string;
30
+ viewerHeartbeatIntervalMs: number;
31
+ dataPlaneUrl: string | null;
32
+ };
33
+ /**
34
+ * The STABLE run-scoped sandbox environment a COLD box must be created with so
35
+ * that — whether the box is first warmed by an API-direct ATTACH (here) or by the
36
+ * worker TURN — its manifest environment matches the environment the agent later
37
+ * declares for a turn. Without this, an attach-warmed box was created with the
38
+ * BASE allowlist env only (establishSandboxSessionFromEnvelope's
39
+ * collectSandboxEnvironment default), so the next turn's fuller env (git identity
40
+ * + workspace environment + HOME) introduced a delta and the SDK's
41
+ * `validateNoEnvironmentDelta` threw "Live sandbox sessions cannot change manifest
42
+ * environment variables" — the BLOCKING error this fixes.
43
+ *
44
+ * Mirrors the worker turn's STABLE env (config.stableSandboxEnvironmentForRun +
45
+ * the session's attached, decrypted workspace environment + — for a repo-attached
46
+ * session — the stable git-auth POINTERS the turn declares since the token-broker:
47
+ * GIT_ASKPASS / GIT_TERMINAL_PROMPT / bot identity). The pointers carry NO rotating
48
+ * value (the token lives in the box FILE the clone hook seeds), so they are
49
+ * attach-reproducible; omitting them cold-created a box whose env lacked keys the
50
+ * next repo turn's manifest declares → the SDK guard threw "Live sandbox sessions
51
+ * cannot change manifest environment variables" whenever a viewer attach (an open
52
+ * session page) won the cold-create race against the first turn.
53
+ */
54
+ export declare function sessionAttachEnvironment(services: ViewerServices, workspaceId: string, session: Session): Promise<Record<string, string>>;
55
+ /**
56
+ * Acquire a `viewer` holder on the group lease, spinning up the box IN-PROCESS
57
+ * when cold. Mirrors the worker's resumeBoxForTurn spawner/attached branches,
58
+ * but with kind:'viewer' and run by the API process — no Temporal, no worker.
59
+ *
60
+ * `viewerId` is the unique-per-connection holder id (a uuid the client carries
61
+ * through heartbeats + detach); generated when absent.
62
+ */
63
+ export declare function attachViewer(services: ViewerServices, input: {
64
+ accountId: string;
65
+ workspaceId: string;
66
+ session: Session;
67
+ viewerId?: string;
68
+ }): Promise<ViewerAttachResult>;
69
+ export type SessionGroupReadinessHold = {
70
+ lease: LeaseSnapshot;
71
+ /** Idempotently release the viewer holder after route publication settles. */
72
+ release: () => Promise<void>;
73
+ };
74
+ /** Readiness callback for fleet attach/swap. It acquires one disposable viewer
75
+ * holder through the same provider-verifying path as the UI and RETURNS that
76
+ * holder to the swap owner. The holder must remain live until route publication
77
+ * settles, otherwise the reaper can drain the just-verified target between the
78
+ * readiness probe and the route CAS. A first 409 may have fenced a missing warm
79
+ * provider; one bounded retry lets the normal cold->warming election rematerialize
80
+ * it. */
81
+ export declare function ensureSessionGroupReady(services: ViewerServices, input: {
82
+ accountId: string;
83
+ workspaceId: string;
84
+ session: Session;
85
+ }): Promise<SessionGroupReadinessHold>;
86
+ /**
87
+ * Refresh a viewer holder's TTL (the app-level viewer heartbeat). Epoch-fenced:
88
+ * a stale-epoch heartbeat (a box re-established under a newer epoch) returns
89
+ * false and the client must re-attach. Returns whether the holder is still live.
90
+ */
91
+ export declare function heartbeatViewer(services: ViewerServices, input: {
92
+ accountId: string;
93
+ workspaceId: string;
94
+ sandboxGroupId: string;
95
+ viewerId: string;
96
+ expectedEpoch: number;
97
+ }): Promise<boolean>;
98
+ /**
99
+ * Release a viewer holder (the client disconnected). Idempotent: a double
100
+ * detach (or a detach after the reaper already TTL-reaped the holder) is a
101
+ * no-op. The box drains/stops only when no turn AND no viewer holds it.
102
+ */
103
+ export declare function detachViewer(services: ViewerServices, input: {
104
+ accountId: string;
105
+ workspaceId: string;
106
+ sandboxGroupId: string;
107
+ viewerId: string;
108
+ }): Promise<{
109
+ liveness: LeaseSnapshot["liveness"];
110
+ refcount: number;
111
+ } | null>;
112
+ /** Non-locking lease snapshot for the capability-negotiation read. */
113
+ export declare function readGroupLease(services: ViewerServices, input: {
114
+ workspaceId: string;
115
+ sandboxGroupId: string;
116
+ }): Promise<LeaseSnapshot | null>;
117
+ export declare function viewerHeartbeatIntervalMs(settings: Settings): number;
118
+ /**
119
+ * The desktop WIRE transport for the session's ACTIVE sandbox — the single
120
+ * invariant that keeps the advertised transport in lockstep with where
121
+ * mintDesktopStream routed the pixels. It MUST be derived from the ACTIVE sandbox
122
+ * (not the session's HOME backend that negotiateCapabilities keys on): a selfhosted
123
+ * machine serves the RELAY framebuffer (PNG-per-frame → "relay-frames"/"frames",
124
+ * view-only in v1), and a Modal group box serves noVNC/RFB over the 6080 tunnel
125
+ * (→ "vnc-ws"/"novnc", take-control unless the deployment disabled it). Advertising
126
+ * relay-frames for a Modal box (the swap-away case) hands the client a dead relay
127
+ * socket → "desktop stream closed before it opened"; the reverse hands a machine's
128
+ * relay URL to the noVNC renderer. `selfhostedActive` == (activeSandboxId set AND the
129
+ * active sandbox kind is "selfhosted") — EXACTLY mintDesktopStream's routing predicate.
130
+ */
131
+ export declare function resolveActiveDesktopTransport(selfhostedActive: boolean, interactive: boolean): {
132
+ transport: "relay-frames" | "vnc-ws";
133
+ client: "frames" | "novnc";
134
+ mode: "read-only" | "interactive";
135
+ };
136
+ /** The minted pixel cell the handshake/attach folds into the DesktopStream
137
+ * capability. Null when degraded (no secret, headless backend, display-stack
138
+ * failure, provider tunnel failure) — degradation is a value, never a throw. */
139
+ export type DesktopStreamMint = {
140
+ url: string;
141
+ token: string;
142
+ expiresAt: string;
143
+ resolution: [number, number];
144
+ leaseEpoch: number;
145
+ };
146
+ export type MintDesktopStreamInput = {
147
+ accountId: string;
148
+ workspaceId: string;
149
+ session: Session;
150
+ /** The viewer holder id the scoped token is minted for. */
151
+ viewerId: string;
152
+ /** The live lease (must be warm/draining — the box is up). A selfhosted-active
153
+ * session may have no Modal group lease; omit and the selfhosted branch handles it. */
154
+ lease?: LeaseSnapshot;
155
+ /** The epoch the CALLER last observed the URL minted under. When the live
156
+ * lease epoch is greater, the box rolled over → emit stream.url.rotated to the
157
+ * other viewers. Omit on a first mint (no prior URL to rotate from). */
158
+ previousEpoch?: number;
159
+ /** Test seam: override how the box is re-established by id. Defaults to the
160
+ * real leaf `establishSandboxSessionFromEnvelope`. Production NEVER passes
161
+ * this; it exists so a real-lease integration test can inject a fake provider
162
+ * session carrying `resolveExposedPort` without a live cloud box. */
163
+ establish?: (envelope: Record<string, unknown> | null) => Promise<EstablishedSandboxSession>;
164
+ /** Test seam: inject a fake relay-resolving session for the selfhosted-active
165
+ * branch. Production NEVER passes this. */
166
+ resolveSelfhostedSession?: (sandbox: SandboxRecord) => Promise<{
167
+ resolveExposedPort?: (port: number) => Promise<unknown>;
168
+ }>;
169
+ };
170
+ /**
171
+ * Mint (or re-mint) the desktop pixel cell for a viewer against a WARM box,
172
+ * IN-PROCESS. Returns the minted cell, or null when the desktop tier degrades
173
+ * (no resolvable stream-token secret, a headless backend, a display-stack
174
+ * failure, or a provider-tunnel failure) — the caller surfaces transport:null,
175
+ * never an exception to the user.
176
+ *
177
+ * Idempotent display-stack + resolveExposedPort are safe to call N times. The
178
+ * resolved URL is recorded on the lease (data_plane_url) under the epoch fence; a
179
+ * stale-epoch write (the box re-established under a newer epoch mid-call) is a
180
+ * no-op and the mint returns null rather than disclosing a capability for the
181
+ * superseded provider epoch.
182
+ */
183
+ export declare function mintDesktopStream(services: ViewerServices, input: MintDesktopStreamInput): Promise<DesktopStreamMint | null>;
184
+ /** The minted terminal cell the handshake/attach folds into the Terminal
185
+ * capability (pty-ws). Null when degraded — the caller surfaces transport
186
+ * "sse-events" (the read-only firehose), never an exception. */
187
+ export type TerminalStreamMint = {
188
+ url: string;
189
+ token: string;
190
+ expiresAt: string;
191
+ leaseEpoch: number;
192
+ };
193
+ export type MintTerminalStreamInput = {
194
+ accountId: string;
195
+ workspaceId: string;
196
+ session: Session;
197
+ /** The viewer holder / principal id the scoped token is minted for. */
198
+ viewerId: string;
199
+ /** The live lease (must be warm/draining — the box is up). A selfhosted-active
200
+ * session may have no Modal group lease; omit and the selfhosted branch handles it. */
201
+ lease?: LeaseSnapshot;
202
+ /** Test seam: override how the box is re-established by id (see
203
+ * MintDesktopStreamInput.establish). Production NEVER passes this. */
204
+ establish?: (envelope: Record<string, unknown> | null) => Promise<EstablishedSandboxSession>;
205
+ /** Test seam: inject a fake relay-resolving session for the selfhosted-active
206
+ * branch. Production NEVER passes this. */
207
+ resolveSelfhostedSession?: (sandbox: SandboxRecord) => Promise<{
208
+ resolveExposedPort?: (port: number) => Promise<unknown>;
209
+ }>;
210
+ };
211
+ /**
212
+ * Mint (or re-mint) the REAL PTY (ttyd pty-ws) terminal cell for a viewer against
213
+ * a WARM box, IN-PROCESS. Returns the minted cell, or null when the terminal tier
214
+ * degrades (terminal off, no resolvable stream-token secret, a headless backend,
215
+ * a ttyd-launch failure, or a provider-tunnel failure) — the caller surfaces the
216
+ * sse-events firehose, never an exception to the user. Mirrors mintDesktopStream.
217
+ */
218
+ export declare function mintTerminalStream(services: ViewerServices, input: MintTerminalStreamInput): Promise<TerminalStreamMint | null>;
219
+ /** The structural slice of a selfhosted session the relay stream mint needs. */
220
+ type RelayResolvableSession = {
221
+ resolveExposedPort?: (port: number) => Promise<unknown>;
222
+ };
223
+ export type MintSelfhostedStreamInput = {
224
+ workspaceId: string;
225
+ sessionId: string;
226
+ /** The viewer holder / principal id the scoped token is minted for. */
227
+ viewerId: string;
228
+ /** The swap fence: the session's `active_epoch`. The minted `ogs_` token carries
229
+ * THIS as its leaseEpoch claim so the relay rejects a stale-epoch (swapped-away)
230
+ * viewer. */
231
+ activeEpoch: number;
232
+ /** The exposed stream port (6080 desktop / 7681 terminal). */
233
+ port: number;
234
+ /** The resolvable selfhosted session (the routing proxy resolves the active
235
+ * selfhosted backend; its `resolveExposedPort` returns the relay endpoint). */
236
+ session: RelayResolvableSession;
237
+ };
238
+ /**
239
+ * Mint the selfhosted relay stream cell for a viewer against the session's ACTIVE
240
+ * selfhosted machine, IN-PROCESS. Resolves the relay endpoint via the selfhosted
241
+ * session's `resolveExposedPort` and mints the scoped `ogs_` token FENCED BY THE
242
+ * SWAP `active_epoch`. Returns null when the stream tier degrades (no stream-token
243
+ * secret, the agent is offline / cannot ensure a channel) — the caller surfaces
244
+ * transport:null, never an exception.
245
+ *
246
+ * The token is RECORDED against the viewer holder by the caller and is NEVER a URL
247
+ * query param (the relay validates the in-band token); the relay's stale-viewer
248
+ * fence uses the token's leaseEpoch claim (== activeEpoch here).
249
+ */
250
+ export declare function mintSelfhostedStream(services: ViewerServices, input: MintSelfhostedStreamInput): Promise<TerminalStreamMint | null>;
251
+ export {};
@@ -0,0 +1,9 @@
1
+ import type { TranscriptionProvider } from "@opengeni/core";
2
+ export declare function createAzureOpenAiTranscriptionProvider(input: {
3
+ endpoint: string;
4
+ deployment: string;
5
+ apiVersion: string;
6
+ apiKey: string | null;
7
+ adToken: string | null;
8
+ fetch?: typeof fetch;
9
+ }): TranscriptionProvider;
@@ -0,0 +1,9 @@
1
+ import type { Settings } from "@opengeni/config";
2
+ import { type TranscriptionAvailabilityContext, type TranscriptionProvider } from "@opengeni/core";
3
+ import { type Database } from "@opengeni/db";
4
+ export declare function createCodexSubscriptionTranscriptionProvider(input: {
5
+ settings: Settings;
6
+ db: Database;
7
+ fetch?: typeof fetch;
8
+ probe?: (context?: TranscriptionAvailabilityContext) => boolean | Promise<boolean>;
9
+ }): TranscriptionProvider;