@opengeni/api-router 0.5.2 → 0.5.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 (48) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/{chunk-YY6OAEL6.js → chunk-DO2G3JSB.js} +5333 -2205
  3. package/dist/chunk-DO2G3JSB.js.map +1 -0
  4. package/dist/index.d.ts +2 -1
  5. package/dist/index.js +297 -54
  6. package/dist/index.js.map +1 -1
  7. package/package.json +21 -21
  8. package/src/app.ts +415 -147
  9. package/src/auth/managed-auth.ts +32 -16
  10. package/src/http/auth.ts +8 -1
  11. package/src/http/common.ts +6 -2
  12. package/src/http/sse.ts +27 -6
  13. package/src/index.ts +196 -74
  14. package/src/integrations/oauth-client.ts +592 -131
  15. package/src/integrations/provider-domain.ts +4 -1
  16. package/src/mcp/documents.ts +173 -94
  17. package/src/mcp/server.ts +1517 -692
  18. package/src/mcp/session-view.ts +8 -2
  19. package/src/mcp/toolspace.ts +175 -84
  20. package/src/observability.ts +7 -1
  21. package/src/routes/api-keys.ts +39 -23
  22. package/src/routes/billing.ts +180 -65
  23. package/src/routes/capabilities.ts +17 -8
  24. package/src/routes/catalog-assets.ts +5 -2
  25. package/src/routes/codex.ts +244 -63
  26. package/src/routes/connections.ts +72 -34
  27. package/src/routes/documents.ts +242 -92
  28. package/src/routes/enrollments.ts +100 -70
  29. package/src/routes/environments.ts +205 -136
  30. package/src/routes/files.ts +164 -39
  31. package/src/routes/github.ts +123 -50
  32. package/src/routes/install.ts +9 -2
  33. package/src/routes/machines.ts +9 -8
  34. package/src/routes/packs.ts +141 -89
  35. package/src/routes/rigs.ts +189 -0
  36. package/src/routes/scheduled-tasks.ts +51 -9
  37. package/src/routes/sessions.ts +839 -329
  38. package/src/routes/social.ts +50 -38
  39. package/src/routes/workspace-capture.ts +238 -0
  40. package/src/routes/workspaces.ts +159 -13
  41. package/src/sandbox/access.ts +11 -3
  42. package/src/sandbox/auth-callout.ts +5 -1
  43. package/src/sandbox/channel-a.ts +104 -27
  44. package/src/sandbox/enrollment.ts +13 -3
  45. package/src/sandbox/machines.ts +68 -59
  46. package/src/sandbox/metrics-ingestion.ts +238 -17
  47. package/src/sandbox/viewer.ts +172 -46
  48. package/dist/chunk-YY6OAEL6.js.map +0 -1
@@ -21,6 +21,9 @@
21
21
  // re-surfaced. Consuming the Hello's `capabilities.desktop` / `display` makes
22
22
  // `has_display` track reality (both directions), which the desktop-capability
23
23
  // gate (packages/runtime capabilities.ts) keys off.
24
+ // refreshEnrollmentOpStream — reconcile `enrollments.op_stream` to the LIVE
25
+ // runner capability the Hello reports, leaving legacy request/reply exec as the
26
+ // fallback unless the runner advertises the streaming engine.
24
27
  //
25
28
  // Both consumers are BEST-EFFORT and fail-soft: a decode/DB error for one message
26
29
  // is logged + swallowed (the bus subscription already swallows handler throws) so
@@ -28,16 +31,27 @@
28
31
  // back-pressures the agent, or breaks its connect.
29
32
 
30
33
  import {
34
+ clearEnrollmentWentOffline,
31
35
  getEnrollment,
32
36
  ingestMachineMetricsSample,
37
+ sessionsWithActiveOpOnEnrollment,
33
38
  setEnrollmentDisplayState,
39
+ setEnrollmentOpStreamState,
40
+ setEnrollmentWentOffline,
34
41
  touchEnrollmentLastSeen,
42
+ type AppendEventInput,
35
43
  type Database,
36
44
  type MachineMetricsSample,
37
45
  } from "@opengeni/db";
38
- import type { EventBus } from "@opengeni/events";
46
+ import { appendAndPublishEvents, type EventBus } from "@opengeni/events";
39
47
  import type { Observability } from "@opengeni/observability";
40
- import { AgentEvent, Hello, type MetricsSample } from "@opengeni/agent-proto";
48
+ import {
49
+ AgentEvent,
50
+ GoingOfflineReason,
51
+ Hello,
52
+ goingOfflineReasonToJSON,
53
+ type MetricsSample,
54
+ } from "@opengeni/agent-proto";
41
55
 
42
56
  /** The wildcard subject the agent event plane publishes heartbeats on. */
43
57
  export const AGENT_EVENTS_SUBJECT = "agent.*.*.events";
@@ -50,7 +64,10 @@ export const AGENT_HELLO_SUBJECT = "agent.*.*.hello";
50
64
  * expected tail token. Returns null for a subject that does not match the shape
51
65
  * (defensive — the subscription pattern already constrains it).
52
66
  */
53
- function parseAgentSubject(subject: string, tail: "events" | "hello"): { workspaceId: string; agentId: string } | null {
67
+ function parseAgentSubject(
68
+ subject: string,
69
+ tail: "events" | "hello",
70
+ ): { workspaceId: string; agentId: string } | null {
54
71
  const parts = subject.split(".");
55
72
  if (parts.length !== 4 || parts[0] !== "agent" || parts[3] !== tail) {
56
73
  return null;
@@ -59,12 +76,16 @@ function parseAgentSubject(subject: string, tail: "events" | "hello"): { workspa
59
76
  }
60
77
 
61
78
  /** Parse `agent.<ws>.<id>.events` → `{ workspaceId, agentId }` (heartbeat plane). */
62
- export function parseAgentEventSubject(subject: string): { workspaceId: string; agentId: string } | null {
79
+ export function parseAgentEventSubject(
80
+ subject: string,
81
+ ): { workspaceId: string; agentId: string } | null {
63
82
  return parseAgentSubject(subject, "events");
64
83
  }
65
84
 
66
85
  /** Parse `agent.<ws>.<id>.hello` → `{ workspaceId, agentId }` (connect plane). */
67
- export function parseAgentHelloSubject(subject: string): { workspaceId: string; agentId: string } | null {
86
+ export function parseAgentHelloSubject(
87
+ subject: string,
88
+ ): { workspaceId: string; agentId: string } | null {
68
89
  return parseAgentSubject(subject, "hello");
69
90
  }
70
91
 
@@ -96,7 +117,10 @@ export function wireSampleToDbSample(wire: MetricsSample): MachineMetricsSample
96
117
  contention: wire.runQueue,
97
118
  // The sample carries its own wall-clock stamp (epoch ms); fall back to now on
98
119
  // a missing/zero stamp so a series row is never NULL-dated.
99
- sampledAt: wire.sampledAtMs && Number(wire.sampledAtMs) > 0 ? new Date(Number(wire.sampledAtMs)) : new Date(),
120
+ sampledAt:
121
+ wire.sampledAtMs && Number(wire.sampledAtMs) > 0
122
+ ? new Date(Number(wire.sampledAtMs))
123
+ : new Date(),
100
124
  };
101
125
  }
102
126
 
@@ -132,16 +156,61 @@ export async function ingestHeartbeat(
132
156
  return { ingested: true, seriesAppended: result.seriesAppended };
133
157
  }
134
158
 
159
+ /**
160
+ * Fan out one or more machine-LINK session events to the sessions that had an
161
+ * active op running on the machine when its control link changed (per
162
+ * `sessionsWithActiveOpOnEnrollment`) — the announce-only failure-visibility
163
+ * plane. Each session's events are stamped on its OWN active turn. No matching
164
+ * session ⇒ nothing is emitted (an idle-machine blip must never spam idle /
165
+ * historical sessions). Called best-effort inside the handlers' fail-soft blocks.
166
+ *
167
+ * Each session's emission is ISOLATED: one session's append failing (a
168
+ * session-specific constraint like a sequence collision from a racing writer, a
169
+ * transient write error) is logged with that sessionId and skipped, never
170
+ * aborting the fan-out — one session's failure must never cost the OTHER matching
171
+ * sessions their events. A partial fan-out stays visible per-session in the logs.
172
+ */
173
+ async function fanOutMachineLinkEvents(
174
+ db: Database,
175
+ bus: EventBus,
176
+ observability: Observability | undefined,
177
+ workspaceId: string,
178
+ enrollmentId: string,
179
+ build: (activeTurnId: string) => AppendEventInput[],
180
+ ): Promise<void> {
181
+ const sessions = await sessionsWithActiveOpOnEnrollment(db, { workspaceId, enrollmentId });
182
+ for (const session of sessions) {
183
+ try {
184
+ await appendAndPublishEvents(
185
+ db,
186
+ bus,
187
+ workspaceId,
188
+ session.sessionId,
189
+ build(session.activeTurnId),
190
+ );
191
+ } catch (error) {
192
+ observability?.warn?.("Failed to fan out a machine-link event to a session", {
193
+ workspaceId,
194
+ sessionId: session.sessionId,
195
+ error: error instanceof Error ? error.message : String(error),
196
+ });
197
+ }
198
+ }
199
+ }
200
+
135
201
  /**
136
202
  * Decode a raw `AgentEvent` payload + ingest it (the per-message handler). A
137
- * heartbeat carrying a metrics sample is ingested; a going-offline (or a
138
- * heartbeat without metrics) is a no-op. Decode failures are reported + swallowed.
203
+ * heartbeat carrying a metrics sample is ingested; a going-offline records the
204
+ * machine-plane marker + fans out the link-plane session events. Decode failures
205
+ * are reported + swallowed. `bus` (when present) enables the session-event
206
+ * fan-out; the live consumer always supplies it, pure unit tests may omit it.
139
207
  */
140
208
  export async function handleAgentEventPayload(
141
209
  db: Database,
142
210
  observability: Observability | undefined,
143
211
  payload: Uint8Array,
144
212
  subject: string,
213
+ bus?: EventBus,
145
214
  ): Promise<void> {
146
215
  const ids = parseAgentEventSubject(subject);
147
216
  if (!ids) {
@@ -157,15 +226,84 @@ export async function handleAgentEventPayload(
157
226
  });
158
227
  return;
159
228
  }
229
+ // A clean GoingOffline is the machine-plane's typed shutdown signal. Two things
230
+ // happen, in this order:
231
+ // 1. Record it ALWAYS on the machine plane (a Prometheus counter keyed by the
232
+ // typed reason) so a fleet operator can see clean stops / self-updates /
233
+ // host shutdowns. This fires unconditionally, independent of the DB.
234
+ // 2. Stamp the enrollment's clean going-offline marker so the liveness
235
+ // derivation reads the machine OFFLINE immediately instead of waiting out
236
+ // the last_seen dead-detect window. Best-effort + fail-soft (like the rest
237
+ // of this module): an unknown enrollment is a no-op and a DB error is
238
+ // swallowed so a bad write never tears down the consumer. Deliberately does
239
+ // NOT touch last-seen (a shutdown must not look "more recently alive").
240
+ if (event.event?.$case === "goingOffline") {
241
+ const reason = goingOfflineReasonToJSON(event.event.goingOffline.reason);
242
+ observability?.incrementCounter({
243
+ name: "opengeni_machine_going_offline_total",
244
+ help: "Total Connected Machine clean GoingOffline signals by typed reason.",
245
+ labels: { reason },
246
+ });
247
+ try {
248
+ const enrollment = await getEnrollment(db, ids.workspaceId, ids.agentId);
249
+ if (enrollment) {
250
+ await setEnrollmentWentOffline(db, {
251
+ accountId: enrollment.accountId,
252
+ workspaceId: ids.workspaceId,
253
+ enrollmentId: ids.agentId,
254
+ reason,
255
+ });
256
+ // Fan out the link-plane events to the sessions with an active op on this
257
+ // machine: machine.link.lost (its control link is going away) for every
258
+ // clean going-offline, PLUS machine.runner.restarted when the reason is a
259
+ // self-update restart specifically (link.lost fires for it too; this
260
+ // distinguishes a restart from a plain stop / host shutdown).
261
+ if (bus) {
262
+ const isSelfUpdate =
263
+ event.event.goingOffline.reason === GoingOfflineReason.GOING_OFFLINE_REASON_UPDATE;
264
+ await fanOutMachineLinkEvents(
265
+ db,
266
+ bus,
267
+ observability,
268
+ ids.workspaceId,
269
+ ids.agentId,
270
+ (activeTurnId) => {
271
+ const events: AppendEventInput[] = [
272
+ { type: "machine.link.lost", turnId: activeTurnId, payload: { reason } },
273
+ ];
274
+ if (isSelfUpdate) {
275
+ events.push({
276
+ type: "machine.runner.restarted",
277
+ turnId: activeTurnId,
278
+ payload: {},
279
+ });
280
+ }
281
+ return events;
282
+ },
283
+ );
284
+ }
285
+ }
286
+ } catch (error) {
287
+ observability?.warn?.("Failed to record a machine clean going-offline", {
288
+ subject,
289
+ error: error instanceof Error ? error.message : String(error),
290
+ });
291
+ }
292
+ return;
293
+ }
160
294
  if (event.event?.$case !== "heartbeat") {
161
- return; // going-offline / unknown → not a metrics point.
295
+ return; // an unknown event kind → not a metrics point.
162
296
  }
163
297
  const metrics = event.event.heartbeat.metrics;
164
298
  if (!metrics) {
165
299
  return; // a heartbeat without a sample → liveness already touched elsewhere.
166
300
  }
167
301
  try {
168
- await ingestHeartbeat(db, { workspaceId: ids.workspaceId, agentId: ids.agentId, sample: metrics });
302
+ await ingestHeartbeat(db, {
303
+ workspaceId: ids.workspaceId,
304
+ agentId: ids.agentId,
305
+ sample: metrics,
306
+ });
169
307
  } catch (error) {
170
308
  observability?.warn?.("Failed to ingest a machine metrics heartbeat", {
171
309
  subject,
@@ -185,7 +323,7 @@ export function startMetricsIngestion(deps: {
185
323
  observability?: Observability;
186
324
  }): () => void {
187
325
  return deps.bus.subscribeAgentEvents(AGENT_EVENTS_SUBJECT, (payload, subject) =>
188
- handleAgentEventPayload(deps.db, deps.observability, payload, subject),
326
+ handleAgentEventPayload(deps.db, deps.observability, payload, subject, deps.bus),
189
327
  );
190
328
  }
191
329
 
@@ -229,6 +367,11 @@ export function helloDesktopUnavailableReason(hello: Hello): string | null {
229
367
  return reason ? reason : null;
230
368
  }
231
369
 
370
+ /** Whether the runner's current Hello advertises the op-stream engine. */
371
+ export function helloReportsOpStream(hello: Hello): boolean {
372
+ return hello.capabilities?.opStream === true;
373
+ }
374
+
232
375
  /**
233
376
  * Reconcile `enrollments.has_display` (+ the capture-blocked reason) to what a Hello
234
377
  * reports. Resolves the enrollment (the accountId is the RLS principal + the
@@ -239,7 +382,12 @@ export function helloDesktopUnavailableReason(hello: Hello): string | null {
239
382
  */
240
383
  export async function refreshEnrollmentDisplay(
241
384
  db: Database,
242
- input: { workspaceId: string; agentId: string; hasDisplay: boolean; desktopUnavailableReason?: string | null },
385
+ input: {
386
+ workspaceId: string;
387
+ agentId: string;
388
+ hasDisplay: boolean;
389
+ desktopUnavailableReason?: string | null;
390
+ },
243
391
  ): Promise<{ updated: boolean }> {
244
392
  const desktopUnavailableReason = input.desktopUnavailableReason ?? null;
245
393
  const enrollment = await getEnrollment(db, input.workspaceId, input.agentId);
@@ -264,15 +412,50 @@ export async function refreshEnrollmentDisplay(
264
412
  }
265
413
 
266
414
  /**
267
- * Decode a raw `Hello` payload + refresh the enrollment's display cursor (the
268
- * per-message handler for the hello plane). Decode failures + write failures are
269
- * reported + swallowed a display refresh must NEVER break the agent's connect.
415
+ * Reconcile `enrollments.op_stream` to what a Hello reports. Resolves the
416
+ * enrollment first so the accountId remains the RLS principal and so a no-change
417
+ * Hello short-circuits BEFORE issuing any write (the DB writer is itself
418
+ * change-guarded as a backstop). An unknown/cross-workspace agentId is a no-op.
419
+ */
420
+ export async function refreshEnrollmentOpStream(
421
+ db: Database,
422
+ input: {
423
+ workspaceId: string;
424
+ agentId: string;
425
+ opStream: boolean;
426
+ },
427
+ ): Promise<{ updated: boolean }> {
428
+ const enrollment = await getEnrollment(db, input.workspaceId, input.agentId);
429
+ if (!enrollment) {
430
+ return { updated: false };
431
+ }
432
+ if (enrollment.opStream === input.opStream) {
433
+ // The capability is unchanged — do not even issue the UPDATE (no churn on a
434
+ // steady-state Hello).
435
+ return { updated: false };
436
+ }
437
+ return await setEnrollmentOpStreamState(db, {
438
+ accountId: enrollment.accountId,
439
+ workspaceId: input.workspaceId,
440
+ enrollmentId: input.agentId,
441
+ opStream: input.opStream,
442
+ });
443
+ }
444
+
445
+ /**
446
+ * Decode a raw `Hello` payload + refresh the enrollment's display cursor + clear
447
+ * any pending clean going-offline marker and, when the reconnect actually cleared
448
+ * one, fan out machine.link.restored to the sessions with an active op on the
449
+ * machine (the per-message handler for the hello plane). Decode failures + write
450
+ * failures are reported + swallowed — a Hello must NEVER break the agent's connect.
451
+ * `bus` (when present) enables the link.restored fan-out.
270
452
  */
271
453
  export async function handleHelloPayload(
272
454
  db: Database,
273
455
  observability: Observability | undefined,
274
456
  payload: Uint8Array,
275
457
  subject: string,
458
+ bus?: EventBus,
276
459
  ): Promise<void> {
277
460
  const ids = parseAgentHelloSubject(subject);
278
461
  if (!ids) {
@@ -295,8 +478,46 @@ export async function handleHelloPayload(
295
478
  hasDisplay: helloReportsDisplay(hello),
296
479
  desktopUnavailableReason: helloDesktopUnavailableReason(hello),
297
480
  });
481
+ await refreshEnrollmentOpStream(db, {
482
+ workspaceId: ids.workspaceId,
483
+ agentId: ids.agentId,
484
+ opStream: helloReportsOpStream(hello),
485
+ });
486
+ } catch (error) {
487
+ observability?.warn?.("Failed to refresh an enrollment's capabilities from a Hello", {
488
+ subject,
489
+ error: error instanceof Error ? error.message : String(error),
490
+ });
491
+ }
492
+ // A reconnect Hello re-announces the machine, so any pending clean going-offline
493
+ // marker no longer holds — clear it so the liveness derivation stops reading the
494
+ // machine offline. Best-effort + fail-soft, and change-guarded in the DB (a
495
+ // steady-state Hello with no marker writes nothing), so this never breaks the
496
+ // agent's connect and never churns. When a marker was ACTUALLY cleared (the
497
+ // machine had been reported link.lost), fan out machine.link.restored to the
498
+ // sessions with an active op on it — a restored only ever pairs a prior lost, so
499
+ // a routine connect Hello (no marker) emits nothing.
500
+ try {
501
+ const enrollment = await getEnrollment(db, ids.workspaceId, ids.agentId);
502
+ if (enrollment) {
503
+ const { cleared } = await clearEnrollmentWentOffline(db, {
504
+ accountId: enrollment.accountId,
505
+ workspaceId: ids.workspaceId,
506
+ enrollmentId: ids.agentId,
507
+ });
508
+ if (cleared && bus) {
509
+ await fanOutMachineLinkEvents(
510
+ db,
511
+ bus,
512
+ observability,
513
+ ids.workspaceId,
514
+ ids.agentId,
515
+ (activeTurnId) => [{ type: "machine.link.restored", turnId: activeTurnId, payload: {} }],
516
+ );
517
+ }
518
+ }
298
519
  } catch (error) {
299
- observability?.warn?.("Failed to refresh an enrollment's display from a Hello", {
520
+ observability?.warn?.("Failed to clear a machine going-offline marker on a Hello", {
300
521
  subject,
301
522
  error: error instanceof Error ? error.message : String(error),
302
523
  });
@@ -315,6 +536,6 @@ export function startHelloIngestion(deps: {
315
536
  observability?: Observability;
316
537
  }): () => void {
317
538
  return deps.bus.subscribeAgentEvents(AGENT_HELLO_SUBJECT, (payload, subject) =>
318
- handleHelloPayload(deps.db, deps.observability, payload, subject),
539
+ handleHelloPayload(deps.db, deps.observability, payload, subject, deps.bus),
319
540
  );
320
541
  }