@opengeni/api-router 0.5.3 → 0.5.5

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 (49) hide show
  1. package/dist/app.d.ts +9 -1
  2. package/dist/app.js +7 -1
  3. package/dist/{chunk-3HIA43CC.js → chunk-HBEJMWD3.js} +5470 -2223
  4. package/dist/chunk-HBEJMWD3.js.map +1 -0
  5. package/dist/index.d.ts +2 -1
  6. package/dist/index.js +279 -55
  7. package/dist/index.js.map +1 -1
  8. package/package.json +20 -20
  9. package/src/app.ts +583 -166
  10. package/src/auth/managed-auth.ts +32 -16
  11. package/src/http/auth.ts +8 -1
  12. package/src/http/common.ts +6 -2
  13. package/src/http/sse.ts +84 -8
  14. package/src/index.ts +178 -75
  15. package/src/integrations/oauth-client.ts +403 -120
  16. package/src/integrations/provider-domain.ts +4 -1
  17. package/src/mcp/documents.ts +173 -94
  18. package/src/mcp/server.ts +1600 -693
  19. package/src/mcp/session-view.ts +8 -2
  20. package/src/mcp/toolspace.ts +175 -84
  21. package/src/observability.ts +7 -1
  22. package/src/routes/api-keys.ts +39 -23
  23. package/src/routes/billing.ts +180 -65
  24. package/src/routes/capabilities.ts +17 -8
  25. package/src/routes/catalog-assets.ts +5 -2
  26. package/src/routes/codex.ts +244 -63
  27. package/src/routes/connections.ts +71 -33
  28. package/src/routes/documents.ts +242 -92
  29. package/src/routes/enrollments.ts +100 -70
  30. package/src/routes/environments.ts +205 -136
  31. package/src/routes/files.ts +164 -39
  32. package/src/routes/github.ts +123 -50
  33. package/src/routes/install.ts +9 -2
  34. package/src/routes/machines.ts +9 -8
  35. package/src/routes/packs.ts +141 -89
  36. package/src/routes/rigs.ts +189 -0
  37. package/src/routes/scheduled-tasks.ts +51 -9
  38. package/src/routes/sessions.ts +870 -328
  39. package/src/routes/social.ts +50 -38
  40. package/src/routes/workspace-capture.ts +238 -0
  41. package/src/routes/workspaces.ts +146 -13
  42. package/src/sandbox/access.ts +11 -3
  43. package/src/sandbox/auth-callout.ts +5 -1
  44. package/src/sandbox/channel-a.ts +104 -27
  45. package/src/sandbox/enrollment.ts +13 -3
  46. package/src/sandbox/machines.ts +68 -59
  47. package/src/sandbox/metrics-ingestion.ts +238 -17
  48. package/src/sandbox/viewer.ts +172 -46
  49. package/dist/chunk-3HIA43CC.js.map +0 -1
package/dist/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { getSettings } from '@opengeni/config';
2
2
  import { ScheduledTaskOverlapPolicy, ScheduledTaskScheduleSpec } from '@opengeni/contracts';
3
+ import { Database } from '@opengeni/db';
3
4
  import { ScheduleOverlapPolicy, ScheduleSpec } from '@temporalio/client';
4
5
  import { SessionWorkflowClient, DocumentIndexClient } from '@opengeni/core';
5
6
 
6
- declare function createTemporalWorkflowClient(settings: ReturnType<typeof getSettings>): Promise<{
7
+ declare function createTemporalWorkflowClient(settings: ReturnType<typeof getSettings>, db: Database): Promise<{
7
8
  client: SessionWorkflowClient;
8
9
  documentIndexer: DocumentIndexClient;
9
10
  close: () => Promise<void>;
package/dist/index.js CHANGED
@@ -1,13 +1,27 @@
1
1
  import {
2
2
  createApp
3
- } from "./chunk-3HIA43CC.js";
3
+ } from "./chunk-HBEJMWD3.js";
4
4
 
5
5
  // src/index.ts
6
- import { dbSearchPath, getSettings, resolveNatsCalloutConfig, resolveNatsControlPlaneAuth, retryStartupDependency, startupRetryOptions } from "@opengeni/config";
7
- import { createDb } from "@opengeni/db";
6
+ import {
7
+ dbSearchPath,
8
+ getSettings,
9
+ resolveNatsCalloutConfig,
10
+ resolveNatsControlPlaneAuth,
11
+ retryStartupDependency,
12
+ startupRetryOptions
13
+ } from "@opengeni/config";
14
+ import { createDb, markSessionWorkflowWakeDelivered } from "@opengeni/db";
8
15
  import { createNatsEventBus } from "@opengeni/events";
9
16
  import { createObservability, logStartupDependencyRetry } from "@opengeni/observability";
10
- import { Connection, Client as TemporalClient, ScheduleNotFoundError, ScheduleOverlapPolicy, WorkflowExecutionAlreadyStartedError } from "@temporalio/client";
17
+ import { SESSION_WORKFLOW_WAKE_DISPATCHER_SCHEDULE_ID } from "@opengeni/core";
18
+ import {
19
+ Connection,
20
+ Client as TemporalClient,
21
+ ScheduleNotFoundError,
22
+ ScheduleOverlapPolicy,
23
+ WorkflowExecutionAlreadyStartedError
24
+ } from "@temporalio/client";
11
25
 
12
26
  // src/observability.ts
13
27
  function observabilityEventLogger(observability) {
@@ -38,7 +52,9 @@ function eventAttributeValue(value) {
38
52
  }
39
53
 
40
54
  // src/sandbox/auth-callout.ts
41
- import { resolveEnrollmentSigningSecret } from "@opengeni/config";
55
+ import {
56
+ resolveEnrollmentSigningSecret
57
+ } from "@opengeni/config";
42
58
  import { verifyEnrollmentBearer } from "@opengeni/contracts";
43
59
  import { getEnrollment } from "@opengeni/db";
44
60
  import {
@@ -135,12 +151,22 @@ async function startAuthCalloutResponder(deps, natsUrl) {
135
151
 
136
152
  // src/sandbox/metrics-ingestion.ts
137
153
  import {
154
+ clearEnrollmentWentOffline,
138
155
  getEnrollment as getEnrollment2,
139
156
  ingestMachineMetricsSample,
157
+ sessionsWithActiveOpOnEnrollment,
140
158
  setEnrollmentDisplayState,
159
+ setEnrollmentOpStreamState,
160
+ setEnrollmentWentOffline,
141
161
  touchEnrollmentLastSeen
142
162
  } from "@opengeni/db";
143
- import { AgentEvent, Hello } from "@opengeni/agent-proto";
163
+ import { appendAndPublishEvents } from "@opengeni/events";
164
+ import {
165
+ AgentEvent,
166
+ GoingOfflineReason,
167
+ Hello,
168
+ goingOfflineReasonToJSON
169
+ } from "@opengeni/agent-proto";
144
170
  var AGENT_EVENTS_SUBJECT = "agent.*.*.events";
145
171
  var AGENT_HELLO_SUBJECT = "agent.*.*.hello";
146
172
  function parseAgentSubject(subject, tail) {
@@ -196,7 +222,27 @@ async function ingestHeartbeat(db, input) {
196
222
  });
197
223
  return { ingested: true, seriesAppended: result.seriesAppended };
198
224
  }
199
- async function handleAgentEventPayload(db, observability, payload, subject) {
225
+ async function fanOutMachineLinkEvents(db, bus, observability, workspaceId, enrollmentId, build) {
226
+ const sessions = await sessionsWithActiveOpOnEnrollment(db, { workspaceId, enrollmentId });
227
+ for (const session of sessions) {
228
+ try {
229
+ await appendAndPublishEvents(
230
+ db,
231
+ bus,
232
+ workspaceId,
233
+ session.sessionId,
234
+ build(session.activeTurnId)
235
+ );
236
+ } catch (error) {
237
+ observability?.warn?.("Failed to fan out a machine-link event to a session", {
238
+ workspaceId,
239
+ sessionId: session.sessionId,
240
+ error: error instanceof Error ? error.message : String(error)
241
+ });
242
+ }
243
+ }
244
+ }
245
+ async function handleAgentEventPayload(db, observability, payload, subject, bus) {
200
246
  const ids = parseAgentEventSubject(subject);
201
247
  if (!ids) {
202
248
  return;
@@ -211,6 +257,54 @@ async function handleAgentEventPayload(db, observability, payload, subject) {
211
257
  });
212
258
  return;
213
259
  }
260
+ if (event.event?.$case === "goingOffline") {
261
+ const reason = goingOfflineReasonToJSON(event.event.goingOffline.reason);
262
+ observability?.incrementCounter({
263
+ name: "opengeni_machine_going_offline_total",
264
+ help: "Total Connected Machine clean GoingOffline signals by typed reason.",
265
+ labels: { reason }
266
+ });
267
+ try {
268
+ const enrollment = await getEnrollment2(db, ids.workspaceId, ids.agentId);
269
+ if (enrollment) {
270
+ await setEnrollmentWentOffline(db, {
271
+ accountId: enrollment.accountId,
272
+ workspaceId: ids.workspaceId,
273
+ enrollmentId: ids.agentId,
274
+ reason
275
+ });
276
+ if (bus) {
277
+ const isSelfUpdate = event.event.goingOffline.reason === GoingOfflineReason.GOING_OFFLINE_REASON_UPDATE;
278
+ await fanOutMachineLinkEvents(
279
+ db,
280
+ bus,
281
+ observability,
282
+ ids.workspaceId,
283
+ ids.agentId,
284
+ (activeTurnId) => {
285
+ const events = [
286
+ { type: "machine.link.lost", turnId: activeTurnId, payload: { reason } }
287
+ ];
288
+ if (isSelfUpdate) {
289
+ events.push({
290
+ type: "machine.runner.restarted",
291
+ turnId: activeTurnId,
292
+ payload: {}
293
+ });
294
+ }
295
+ return events;
296
+ }
297
+ );
298
+ }
299
+ }
300
+ } catch (error) {
301
+ observability?.warn?.("Failed to record a machine clean going-offline", {
302
+ subject,
303
+ error: error instanceof Error ? error.message : String(error)
304
+ });
305
+ }
306
+ return;
307
+ }
214
308
  if (event.event?.$case !== "heartbeat") {
215
309
  return;
216
310
  }
@@ -219,7 +313,11 @@ async function handleAgentEventPayload(db, observability, payload, subject) {
219
313
  return;
220
314
  }
221
315
  try {
222
- await ingestHeartbeat(db, { workspaceId: ids.workspaceId, agentId: ids.agentId, sample: metrics });
316
+ await ingestHeartbeat(db, {
317
+ workspaceId: ids.workspaceId,
318
+ agentId: ids.agentId,
319
+ sample: metrics
320
+ });
223
321
  } catch (error) {
224
322
  observability?.warn?.("Failed to ingest a machine metrics heartbeat", {
225
323
  subject,
@@ -230,7 +328,7 @@ async function handleAgentEventPayload(db, observability, payload, subject) {
230
328
  function startMetricsIngestion(deps) {
231
329
  return deps.bus.subscribeAgentEvents(
232
330
  AGENT_EVENTS_SUBJECT,
233
- (payload, subject) => handleAgentEventPayload(deps.db, deps.observability, payload, subject)
331
+ (payload, subject) => handleAgentEventPayload(deps.db, deps.observability, payload, subject, deps.bus)
234
332
  );
235
333
  }
236
334
  function helloReportsDisplay(hello) {
@@ -247,6 +345,9 @@ function helloDesktopUnavailableReason(hello) {
247
345
  const reason = hello.capabilities?.desktopUnavailableReason;
248
346
  return reason ? reason : null;
249
347
  }
348
+ function helloReportsOpStream(hello) {
349
+ return hello.capabilities?.opStream === true;
350
+ }
250
351
  async function refreshEnrollmentDisplay(db, input) {
251
352
  const desktopUnavailableReason = input.desktopUnavailableReason ?? null;
252
353
  const enrollment = await getEnrollment2(db, input.workspaceId, input.agentId);
@@ -264,7 +365,22 @@ async function refreshEnrollmentDisplay(db, input) {
264
365
  desktopUnavailableReason
265
366
  });
266
367
  }
267
- async function handleHelloPayload(db, observability, payload, subject) {
368
+ async function refreshEnrollmentOpStream(db, input) {
369
+ const enrollment = await getEnrollment2(db, input.workspaceId, input.agentId);
370
+ if (!enrollment) {
371
+ return { updated: false };
372
+ }
373
+ if (enrollment.opStream === input.opStream) {
374
+ return { updated: false };
375
+ }
376
+ return await setEnrollmentOpStreamState(db, {
377
+ accountId: enrollment.accountId,
378
+ workspaceId: input.workspaceId,
379
+ enrollmentId: input.agentId,
380
+ opStream: input.opStream
381
+ });
382
+ }
383
+ async function handleHelloPayload(db, observability, payload, subject, bus) {
268
384
  const ids = parseAgentHelloSubject(subject);
269
385
  if (!ids) {
270
386
  return;
@@ -286,8 +402,38 @@ async function handleHelloPayload(db, observability, payload, subject) {
286
402
  hasDisplay: helloReportsDisplay(hello),
287
403
  desktopUnavailableReason: helloDesktopUnavailableReason(hello)
288
404
  });
405
+ await refreshEnrollmentOpStream(db, {
406
+ workspaceId: ids.workspaceId,
407
+ agentId: ids.agentId,
408
+ opStream: helloReportsOpStream(hello)
409
+ });
289
410
  } catch (error) {
290
- observability?.warn?.("Failed to refresh an enrollment's display from a Hello", {
411
+ observability?.warn?.("Failed to refresh an enrollment's capabilities from a Hello", {
412
+ subject,
413
+ error: error instanceof Error ? error.message : String(error)
414
+ });
415
+ }
416
+ try {
417
+ const enrollment = await getEnrollment2(db, ids.workspaceId, ids.agentId);
418
+ if (enrollment) {
419
+ const { cleared } = await clearEnrollmentWentOffline(db, {
420
+ accountId: enrollment.accountId,
421
+ workspaceId: ids.workspaceId,
422
+ enrollmentId: ids.agentId
423
+ });
424
+ if (cleared && bus) {
425
+ await fanOutMachineLinkEvents(
426
+ db,
427
+ bus,
428
+ observability,
429
+ ids.workspaceId,
430
+ ids.agentId,
431
+ (activeTurnId) => [{ type: "machine.link.restored", turnId: activeTurnId, payload: {} }]
432
+ );
433
+ }
434
+ }
435
+ } catch (error) {
436
+ observability?.warn?.("Failed to clear a machine going-offline marker on a Hello", {
291
437
  subject,
292
438
  error: error instanceof Error ? error.message : String(error)
293
439
  });
@@ -296,7 +442,7 @@ async function handleHelloPayload(db, observability, payload, subject) {
296
442
  function startHelloIngestion(deps) {
297
443
  return deps.bus.subscribeAgentEvents(
298
444
  AGENT_HELLO_SUBJECT,
299
- (payload, subject) => handleHelloPayload(deps.db, deps.observability, payload, subject)
445
+ (payload, subject) => handleHelloPayload(deps.db, deps.observability, payload, subject, deps.bus)
300
446
  );
301
447
  }
302
448
 
@@ -318,7 +464,7 @@ var TEMPORAL_MONTHS = [
318
464
  "NOVEMBER",
319
465
  "DECEMBER"
320
466
  ];
321
- async function createTemporalWorkflowClient(settings) {
467
+ async function createTemporalWorkflowClient(settings, db) {
322
468
  const connection = await Connection.connect({ address: settings.temporalHost });
323
469
  const temporal = new TemporalClient({
324
470
  connection,
@@ -328,27 +474,79 @@ async function createTemporalWorkflowClient(settings) {
328
474
  signalUserMessage: async ({ eventId, workflowId }) => {
329
475
  await temporal.workflow.getHandle(workflowId).signal("userMessage", eventId);
330
476
  },
331
- wakeSessionWorkflow: async ({ accountId, workspaceId, sessionId, workflowId }) => {
477
+ wakeSessionWorkflow: async ({
478
+ accountId,
479
+ workspaceId,
480
+ sessionId,
481
+ workflowId,
482
+ wakeRevision,
483
+ interruptionRequested
484
+ }) => {
332
485
  await temporal.workflow.signalWithStart("sessionWorkflow", {
333
486
  taskQueue: settings.temporalTaskQueue,
334
487
  workflowId,
335
488
  workflowIdReusePolicy: "ALLOW_DUPLICATE",
336
489
  args: [{ accountId, workspaceId, sessionId }],
337
- signal: "queueChanged"
490
+ signal: interruptionRequested ? "sessionControl" : "queueChanged"
491
+ });
492
+ await markSessionWorkflowWakeDelivered(db, {
493
+ accountId,
494
+ workspaceId,
495
+ sessionId,
496
+ temporalWorkflowId: workflowId,
497
+ wakeRevision
338
498
  });
339
499
  },
340
- signalApprovalDecision: async ({ eventId, workflowId }) => {
341
- await temporal.workflow.getHandle(workflowId).signal("approvalDecision", eventId);
500
+ requestSessionWorkflowWakeDispatch: async () => {
501
+ await temporal.schedule.getHandle(SESSION_WORKFLOW_WAKE_DISPATCHER_SCHEDULE_ID).trigger(ScheduleOverlapPolicy.BUFFER_ONE);
342
502
  },
343
- signalInterrupt: async ({ accountId, workspaceId, sessionId, eventId, workflowId }) => {
503
+ signalCodexCapacity: async ({
504
+ accountId,
505
+ workspaceId,
506
+ sessionId,
507
+ workflowId,
508
+ wakeRevision,
509
+ workflowWakeRevision
510
+ }) => {
344
511
  await temporal.workflow.signalWithStart("sessionWorkflow", {
345
512
  taskQueue: settings.temporalTaskQueue,
346
513
  workflowId,
347
514
  workflowIdReusePolicy: "ALLOW_DUPLICATE",
348
515
  args: [{ accountId, workspaceId, sessionId }],
349
- signal: "interrupt",
516
+ signal: "codexCapacityChanged",
517
+ signalArgs: [wakeRevision]
518
+ });
519
+ await markSessionWorkflowWakeDelivered(db, {
520
+ accountId,
521
+ workspaceId,
522
+ sessionId,
523
+ temporalWorkflowId: workflowId,
524
+ wakeRevision: workflowWakeRevision
525
+ });
526
+ },
527
+ signalApprovalDecision: async ({
528
+ accountId,
529
+ workspaceId,
530
+ sessionId,
531
+ eventId,
532
+ workflowId,
533
+ workflowWakeRevision
534
+ }) => {
535
+ await temporal.workflow.signalWithStart("sessionWorkflow", {
536
+ taskQueue: settings.temporalTaskQueue,
537
+ workflowId,
538
+ workflowIdReusePolicy: "ALLOW_DUPLICATE",
539
+ args: [{ accountId, workspaceId, sessionId }],
540
+ signal: "approvalDecision",
350
541
  signalArgs: [eventId]
351
542
  });
543
+ await markSessionWorkflowWakeDelivered(db, {
544
+ accountId,
545
+ workspaceId,
546
+ sessionId,
547
+ temporalWorkflowId: workflowId,
548
+ wakeRevision: workflowWakeRevision
549
+ });
352
550
  },
353
551
  syncScheduledTask: async ({ task }) => {
354
552
  const schedule = temporal.schedule.getHandle(task.temporalScheduleId);
@@ -372,13 +570,15 @@ async function createTemporalWorkflowClient(settings) {
372
570
  taskQueue: settings.temporalTaskQueue,
373
571
  workflowId,
374
572
  workflowIdReusePolicy: "REJECT_DUPLICATE",
375
- args: [{
376
- accountId: task.accountId,
377
- workspaceId: task.workspaceId,
378
- taskId: task.id,
379
- triggerType: "manual",
380
- agentRunUsageIdempotencyKey
381
- }]
573
+ args: [
574
+ {
575
+ accountId: task.accountId,
576
+ workspaceId: task.workspaceId,
577
+ taskId: task.id,
578
+ triggerType: "manual",
579
+ agentRunUsageIdempotencyKey
580
+ }
581
+ ]
382
582
  });
383
583
  } catch (error) {
384
584
  if (isWorkflowAlreadyStarted(error)) {
@@ -387,6 +587,24 @@ async function createTemporalWorkflowClient(settings) {
387
587
  throw error;
388
588
  }
389
589
  },
590
+ startRigVerification: async ({ workspaceId, changeId, versionId, workflowId }) => {
591
+ const targetId = changeId ?? versionId;
592
+ if (!targetId) {
593
+ throw new Error("rig verification requires changeId or versionId");
594
+ }
595
+ await temporal.workflow.start("rigVerificationWorkflow", {
596
+ taskQueue: settings.temporalTaskQueue,
597
+ workflowId: workflowId ?? `rig-verification-${targetId}-${crypto.randomUUID()}`,
598
+ workflowIdReusePolicy: "ALLOW_DUPLICATE",
599
+ args: [
600
+ {
601
+ workspaceId,
602
+ ...changeId ? { changeId } : {},
603
+ ...versionId ? { versionId } : {}
604
+ }
605
+ ]
606
+ });
607
+ },
390
608
  check: async () => {
391
609
  await connection.workflowService.getSystemInfo({});
392
610
  }
@@ -435,16 +653,16 @@ async function startApi() {
435
653
  onRetry
436
654
  }
437
655
  );
438
- workflowClient = await retryStartupDependency("Temporal", () => createTemporalWorkflowClient(settings), {
439
- ...retryOptions,
440
- onRetry
441
- });
656
+ workflowClient = await retryStartupDependency(
657
+ "Temporal",
658
+ () => createTemporalWorkflowClient(settings, dbClient.db),
659
+ {
660
+ ...retryOptions,
661
+ onRetry
662
+ }
663
+ );
442
664
  } catch (error) {
443
- await Promise.allSettled([
444
- bus?.close(),
445
- workflowClient?.close(),
446
- dbClient.close()
447
- ]);
665
+ await Promise.allSettled([bus?.close(), workflowClient?.close(), dbClient.close()]);
448
666
  throw error;
449
667
  }
450
668
  if (!bus || !workflowClient) {
@@ -535,25 +753,29 @@ function temporalScheduleSpec(schedule) {
535
753
  }
536
754
  if (schedule.type === "calendar") {
537
755
  return {
538
- calendars: [{
539
- hour: schedule.hour,
540
- minute: schedule.minute,
541
- second: 0,
542
- ...schedule.daysOfWeek ? { dayOfWeek: schedule.daysOfWeek } : {}
543
- }],
756
+ calendars: [
757
+ {
758
+ hour: schedule.hour,
759
+ minute: schedule.minute,
760
+ second: 0,
761
+ ...schedule.daysOfWeek ? { dayOfWeek: schedule.daysOfWeek } : {}
762
+ }
763
+ ],
544
764
  timezone: schedule.timeZone
545
765
  };
546
766
  }
547
767
  const runAt = new Date(schedule.runAt);
548
768
  return {
549
- calendars: [{
550
- year: runAt.getUTCFullYear(),
551
- month: temporalMonth(runAt.getUTCMonth()),
552
- dayOfMonth: runAt.getUTCDate(),
553
- hour: runAt.getUTCHours(),
554
- minute: runAt.getUTCMinutes(),
555
- second: runAt.getUTCSeconds()
556
- }],
769
+ calendars: [
770
+ {
771
+ year: runAt.getUTCFullYear(),
772
+ month: temporalMonth(runAt.getUTCMonth()),
773
+ dayOfMonth: runAt.getUTCDate(),
774
+ hour: runAt.getUTCHours(),
775
+ minute: runAt.getUTCMinutes(),
776
+ second: runAt.getUTCSeconds()
777
+ }
778
+ ],
557
779
  timezone: "UTC"
558
780
  };
559
781
  }
@@ -568,12 +790,14 @@ function temporalScheduleOptions(task, taskQueue) {
568
790
  type: "startWorkflow",
569
791
  workflowType: "scheduledTaskFireWorkflow",
570
792
  taskQueue,
571
- args: [{
572
- accountId: task.accountId,
573
- workspaceId: task.workspaceId,
574
- taskId: task.id,
575
- triggerType: "scheduled"
576
- }]
793
+ args: [
794
+ {
795
+ accountId: task.accountId,
796
+ workspaceId: task.workspaceId,
797
+ taskId: task.id,
798
+ triggerType: "scheduled"
799
+ }
800
+ ]
577
801
  },
578
802
  policies: {
579
803
  overlap: temporalOverlapPolicy(task.overlapPolicy),