@opengeni/api-router 0.30.3 → 2.1.0-canary.0

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 (165) hide show
  1. package/dist/api-websocket.d.ts +18 -0
  2. package/dist/app.js +1 -1
  3. package/dist/auth/managed-auth.d.ts +6 -0
  4. package/dist/{chunk-WCXHI3FF.js → chunk-QKDFBBUE.js} +26385 -15573
  5. package/dist/chunk-QKDFBBUE.js.map +1 -0
  6. package/dist/codemode.d.ts +8 -1
  7. package/dist/company-brain-okf.d.ts +24 -0
  8. package/dist/connected-machine-computer-access.d.ts +11 -0
  9. package/dist/connection-authority-owner.d.ts +59 -0
  10. package/dist/connection-ownership.d.ts +47 -0
  11. package/dist/controller-data-plane.d.ts +16 -0
  12. package/dist/editable-artifact-websocket.d.ts +5 -15
  13. package/dist/editable-artifact-workspace-files.d.ts +1 -1
  14. package/dist/http/api-error.d.ts +10 -0
  15. package/dist/http/cors.d.ts +5 -0
  16. package/dist/http/interaction-control-error.d.ts +13 -0
  17. package/dist/http/sse.d.ts +2 -3
  18. package/dist/index.d.ts +5 -2
  19. package/dist/index.js +326 -82
  20. package/dist/index.js.map +1 -1
  21. package/dist/integrations/atlassian.d.ts +17 -0
  22. package/dist/integrations/fiken.d.ts +7 -2
  23. package/dist/integrations/google-drive.d.ts +18 -0
  24. package/dist/integrations/oauth-client.d.ts +22 -3
  25. package/dist/integrations/oauth-profiles.d.ts +195 -0
  26. package/dist/integrations/personal-github-repositories.d.ts +45 -0
  27. package/dist/integrations/personal-github.d.ts +40 -0
  28. package/dist/integrations/pr-review-provider.d.ts +22 -0
  29. package/dist/integrations/provider-oauth.d.ts +8 -0
  30. package/dist/integrations/slack-app-home.d.ts +24 -0
  31. package/dist/integrations/slack-bot.d.ts +149 -9
  32. package/dist/integrations/slack-interactions.d.ts +4 -3
  33. package/dist/integrations/slack-routing.d.ts +120 -0
  34. package/dist/integrations/social-oauth.d.ts +5 -0
  35. package/dist/interaction-frame-proxy.d.ts +68 -0
  36. package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
  37. package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
  38. package/dist/mcp/documents.d.ts +1 -0
  39. package/dist/mcp/remember.d.ts +28 -0
  40. package/dist/mcp/request-abort.d.ts +19 -0
  41. package/dist/mcp/scheduled-task-view.d.ts +4 -4
  42. package/dist/mcp/server.d.ts +15 -2
  43. package/dist/mcp/session-view.d.ts +4 -0
  44. package/dist/mcp/session-wait.d.ts +137 -0
  45. package/dist/routes/automations.d.ts +4 -0
  46. package/dist/routes/billing.d.ts +5 -0
  47. package/dist/routes/browser-sessions.d.ts +4 -1
  48. package/dist/routes/company-brain.d.ts +3 -0
  49. package/dist/routes/company-profile.d.ts +2 -1
  50. package/dist/routes/connection-authorities.d.ts +3 -0
  51. package/dist/routes/personal-github-git-broker.d.ts +29 -0
  52. package/dist/routes/personal-github.d.ts +3 -0
  53. package/dist/routes/pr-review.d.ts +3 -0
  54. package/dist/routes/sessions.d.ts +1 -0
  55. package/dist/routes/user-resource-authorities.d.ts +3 -0
  56. package/dist/routes/workspace-capture.d.ts +10 -2
  57. package/dist/routes/workspace-learning.d.ts +3 -0
  58. package/dist/routes/workspaces.d.ts +19 -0
  59. package/dist/sandbox/channel-a.d.ts +11 -1
  60. package/dist/sandbox/connection-authority.d.ts +3 -0
  61. package/dist/sandbox/enrollment.d.ts +2 -0
  62. package/dist/sandbox/machines.d.ts +2 -2
  63. package/dist/sandbox/metrics-ingestion.d.ts +10 -20
  64. package/dist/sandbox/viewer.d.ts +24 -1
  65. package/dist/sandbox-file-artifacts.d.ts +11 -0
  66. package/dist/scheduled-task-deletion.d.ts +14 -0
  67. package/dist/slack-reaction-files.d.ts +1 -1
  68. package/dist/temporal-schedule-cleanup.d.ts +1 -0
  69. package/package.json +20 -18
  70. package/src/api-websocket.ts +23 -0
  71. package/src/app.ts +153 -23
  72. package/src/auth/managed-auth.ts +25 -3
  73. package/src/codemode.ts +53 -23
  74. package/src/codex-realtime.ts +8 -2
  75. package/src/company-brain-okf.ts +340 -0
  76. package/src/connected-machine-computer-access.ts +33 -0
  77. package/src/connection-authority-owner.ts +61 -0
  78. package/src/connection-ownership.ts +180 -0
  79. package/src/controller-data-plane.ts +47 -0
  80. package/src/editable-artifact-native-kernel.ts +26 -15
  81. package/src/editable-artifact-office-import.ts +28 -3
  82. package/src/editable-artifact-production.ts +13 -7
  83. package/src/editable-artifact-websocket.ts +11 -18
  84. package/src/editable-artifact-workspace-files.ts +31 -6
  85. package/src/http/api-error.ts +28 -0
  86. package/src/http/auth.ts +4 -0
  87. package/src/http/cors.ts +35 -0
  88. package/src/http/interaction-control-error.ts +164 -0
  89. package/src/http/sse.ts +112 -66
  90. package/src/index.ts +46 -6
  91. package/src/integrations/atlassian.ts +146 -35
  92. package/src/integrations/fiken.ts +102 -22
  93. package/src/integrations/google-drive.ts +301 -92
  94. package/src/integrations/oauth-client.ts +272 -143
  95. package/src/integrations/oauth-profiles.ts +477 -0
  96. package/src/integrations/personal-github-repositories.ts +445 -0
  97. package/src/integrations/personal-github.ts +705 -0
  98. package/src/integrations/pr-review-provider.ts +246 -0
  99. package/src/integrations/provider-oauth.ts +121 -5
  100. package/src/integrations/slack-app-home.ts +300 -0
  101. package/src/integrations/slack-bot.ts +337 -57
  102. package/src/integrations/slack-interactions.ts +1265 -208
  103. package/src/integrations/slack-routing.ts +324 -0
  104. package/src/integrations/social-oauth.ts +25 -0
  105. package/src/interaction-frame-proxy.ts +409 -0
  106. package/src/mcp/company-brain-governed-writes.ts +262 -0
  107. package/src/mcp/company-profile-agent-admin.ts +205 -0
  108. package/src/mcp/documents.ts +37 -6
  109. package/src/mcp/files.ts +16 -1
  110. package/src/mcp/remember.ts +181 -0
  111. package/src/mcp/request-abort.ts +44 -0
  112. package/src/mcp/scheduled-task-view.ts +1 -0
  113. package/src/mcp/server.ts +719 -369
  114. package/src/mcp/session-view.ts +54 -0
  115. package/src/mcp/session-wait.ts +554 -0
  116. package/src/model-catalog.ts +20 -12
  117. package/src/routes/api-integrations.ts +4 -0
  118. package/src/routes/api-keys.ts +1 -0
  119. package/src/routes/automations.ts +534 -0
  120. package/src/routes/billing.ts +57 -1
  121. package/src/routes/browser-sessions.ts +329 -79
  122. package/src/routes/channels.ts +17 -1
  123. package/src/routes/codex.ts +50 -0
  124. package/src/routes/company-brain.ts +367 -0
  125. package/src/routes/company-profile.ts +1 -1
  126. package/src/routes/computer-sessions.ts +320 -93
  127. package/src/routes/connection-authorities.ts +139 -0
  128. package/src/routes/connections.ts +144 -34
  129. package/src/routes/documents.ts +221 -3
  130. package/src/routes/editable-artifacts.ts +11 -3
  131. package/src/routes/enrollments.ts +116 -26
  132. package/src/routes/environments.ts +127 -55
  133. package/src/routes/files.ts +72 -11
  134. package/src/routes/install.ts +82 -28
  135. package/src/routes/integration-facets.ts +29 -0
  136. package/src/routes/interaction-resources.ts +20 -2
  137. package/src/routes/machines.ts +275 -13
  138. package/src/routes/organization-memberships.ts +717 -28
  139. package/src/routes/packs.ts +4 -3
  140. package/src/routes/personal-github-git-broker.ts +785 -0
  141. package/src/routes/personal-github.ts +319 -0
  142. package/src/routes/pr-review.ts +531 -0
  143. package/src/routes/rigs.ts +104 -39
  144. package/src/routes/scheduled-tasks.ts +19 -21
  145. package/src/routes/sessions.ts +775 -38
  146. package/src/routes/social.ts +14 -2
  147. package/src/routes/supergrok.ts +23 -4
  148. package/src/routes/transcription-recordings.ts +8 -2
  149. package/src/routes/user-resource-authorities.ts +138 -0
  150. package/src/routes/workspace-artifacts.ts +4 -1
  151. package/src/routes/workspace-capture.ts +21 -0
  152. package/src/routes/workspace-learning.ts +228 -0
  153. package/src/routes/workspaces.ts +59 -6
  154. package/src/sandbox/auth-callout.ts +38 -12
  155. package/src/sandbox/channel-a.ts +242 -19
  156. package/src/sandbox/connection-authority.ts +3 -0
  157. package/src/sandbox/enrollment.ts +15 -3
  158. package/src/sandbox/machines.ts +186 -64
  159. package/src/sandbox/metrics-ingestion.ts +220 -58
  160. package/src/sandbox/viewer.ts +243 -19
  161. package/src/sandbox-file-artifacts.ts +329 -0
  162. package/src/scheduled-task-deletion.ts +127 -0
  163. package/src/slack-reaction-files.ts +16 -5
  164. package/src/temporal-schedule-cleanup.ts +13 -0
  165. package/dist/chunk-WCXHI3FF.js.map +0 -1
@@ -1,11 +1,11 @@
1
1
  // apps/api/src/sandbox/metrics-ingestion.ts — the M10 metrics INGESTION consumer
2
2
  // + the connect-Hello DISPLAY-REFRESH consumer. The
3
3
  // enrolled agent piggybacks a `MetricsSample` on its ~5s heartbeat (an
4
- // `AgentEvent` published one-way on `agent.<ws>.<id>.events`) and publishes a
5
- // `Hello` (its live self-description) on `agent.<ws>.<id>.hello` on every connect
4
+ // `AgentEvent` published one-way on the exact process events subject) and publishes a
5
+ // `Hello` (its live self-description) on the exact process hello subject on every connect
6
6
  // /reconnect. This module owns the two agent→control-plane inbound consumers:
7
7
  //
8
- // `agent.*.*.events` (heartbeat) →
8
+ // `agent.*.*.connection.*.events` (heartbeat) →
9
9
  // 1. touchEnrollmentLastSeen — the liveness cursor (online/reconnecting/offline
10
10
  // derivation + the M3 probe disambiguation).
11
11
  // 2. ingestMachineMetricsSample — UPSERT machine_metrics_latest (the "now" row)
@@ -13,7 +13,7 @@
13
13
  // A GOING-OFFLINE event is not a metrics point — liveness flips via the lease/
14
14
  // probe path; we skip it here (no-op).
15
15
  //
16
- // `agent.*.*.hello` (connect) →
16
+ // `agent.*.*.connection.*.hello` (connect) →
17
17
  // refreshEnrollmentDisplay — reconcile `enrollments.has_display` to the LIVE
18
18
  // capability the Hello reports. `has_display` was previously FROZEN at the
19
19
  // enroll-time offer snapshot; a machine that GAINS a display later (a Mac that
@@ -22,8 +22,8 @@
22
22
  // `has_display` track reality (both directions), which the desktop-capability
23
23
  // gate (packages/runtime capabilities.ts) keys off.
24
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.
25
+ // runner capability the Hello reports. Exec is unavailable unless the runner
26
+ // advertises the streaming engine.
27
27
  //
28
28
  // Both consumers are BEST-EFFORT and fail-soft: a decode/DB error for one message
29
29
  // is logged + swallowed (the bus subscription already swallows handler throws) so
@@ -33,14 +33,17 @@
33
33
  import {
34
34
  clearEnrollmentWentOffline,
35
35
  disconnectAttachedBrowserDevices,
36
+ advanceEnrollmentAgentUpdate,
36
37
  getEnrollment,
38
+ getLiveEnrollmentConnection,
37
39
  ingestMachineMetricsSample,
38
40
  reconcileAttachedBrowserInventory,
41
+ releaseEnrollmentConnection,
42
+ renewEnrollmentConnection,
39
43
  sessionsWithActiveOpOnEnrollment,
40
44
  setEnrollmentDisplayState,
45
+ setEnrollmentAgentRuntime,
41
46
  setEnrollmentOpStreamState,
42
- setEnrollmentWentOffline,
43
- touchEnrollmentLastSeen,
44
47
  type AppendEventInput,
45
48
  type Database,
46
49
  type MachineMetricsSample,
@@ -51,8 +54,10 @@ import {
51
54
  } from "@opengeni/contracts";
52
55
  import { appendAndPublishEvents, type EventBus } from "@opengeni/events";
53
56
  import type { Observability } from "@opengeni/observability";
57
+ import { normalizeConnectedMachineWorkspaceRoot } from "@opengeni/runtime/sandbox";
54
58
  import {
55
59
  AgentEvent,
60
+ AgentUpdateStage,
56
61
  Arch,
57
62
  GoingOfflineReason,
58
63
  Hello,
@@ -61,40 +66,51 @@ import {
61
66
  type AttachedBrowserInventorySnapshot as WireAttachedBrowserInventorySnapshot,
62
67
  type MetricsSample,
63
68
  } from "@opengeni/agent-proto";
69
+ import { AGENT_CONNECTION_LEASE_MS } from "./connection-authority";
64
70
 
65
71
  /** The wildcard subject the agent event plane publishes heartbeats on. */
66
- export const AGENT_EVENTS_SUBJECT = "agent.*.*.events";
72
+ export const AGENT_EVENTS_SUBJECT = "agent.*.*.connection.*.events";
67
73
 
68
74
  /** The wildcard subject the agent publishes its connect Hello on. */
69
- export const AGENT_HELLO_SUBJECT = "agent.*.*.hello";
75
+ export const AGENT_HELLO_SUBJECT = "agent.*.*.connection.*.hello";
70
76
 
71
77
  /**
72
- * Parse `agent.<ws>.<id>.<tail>` `{ workspaceId, agentId }`, requiring the
78
+ * Parse `agent.<ws>.<id>.connection.<instance>.<tail>` into its exact authority,
73
79
  * expected tail token. Returns null for a subject that does not match the shape
74
80
  * (defensive — the subscription pattern already constrains it).
75
81
  */
76
82
  function parseAgentSubject(
77
83
  subject: string,
78
84
  tail: "events" | "hello",
79
- ): { workspaceId: string; agentId: string } | null {
85
+ ): { workspaceId: string; agentId: string; connectionInstanceId: string } | null {
80
86
  const parts = subject.split(".");
81
- if (parts.length !== 4 || parts[0] !== "agent" || parts[3] !== tail) {
87
+ if (
88
+ parts.length !== 6 ||
89
+ parts[0] !== "agent" ||
90
+ parts[3] !== "connection" ||
91
+ !parts[4] ||
92
+ parts[5] !== tail
93
+ ) {
82
94
  return null;
83
95
  }
84
- return { workspaceId: parts[1]!, agentId: parts[2]! };
96
+ return {
97
+ workspaceId: parts[1]!,
98
+ agentId: parts[2]!,
99
+ connectionInstanceId: parts[4]!,
100
+ };
85
101
  }
86
102
 
87
- /** Parse `agent.<ws>.<id>.events` `{ workspaceId, agentId }` (heartbeat plane). */
103
+ /** Parse the exact process events subject (heartbeat plane). */
88
104
  export function parseAgentEventSubject(
89
105
  subject: string,
90
- ): { workspaceId: string; agentId: string } | null {
106
+ ): { workspaceId: string; agentId: string; connectionInstanceId: string } | null {
91
107
  return parseAgentSubject(subject, "events");
92
108
  }
93
109
 
94
- /** Parse `agent.<ws>.<id>.hello` `{ workspaceId, agentId }` (connect plane). */
110
+ /** Parse the exact process hello subject (connect plane). */
95
111
  export function parseAgentHelloSubject(
96
112
  subject: string,
97
- ): { workspaceId: string; agentId: string } | null {
113
+ ): { workspaceId: string; agentId: string; connectionInstanceId: string } | null {
98
114
  return parseAgentSubject(subject, "hello");
99
115
  }
100
116
 
@@ -218,30 +234,23 @@ async function ingestAttachedBrowserInventory(
218
234
  }
219
235
 
220
236
  /**
221
- * Ingest ONE decoded heartbeat for an enrolled machine. Resolves the enrollment's
222
- * accountId (needed for the RLS-scoped writes) from the enrollment row; an
223
- * unknown/cross-workspace agentId is ignored (no row no write). Touches
224
- * last-seen + upserts latest + downsamples the series.
237
+ * Ingest the OPTIONAL metrics sample carried by an already-authorized heartbeat.
238
+ * Connection renewal is deliberately owned by the outer event handler and runs
239
+ * before optional telemetry/inventory work, so telemetry absence or failure can
240
+ * never make a healthy runner lose authority.
225
241
  */
226
- export async function ingestHeartbeat(
242
+ async function ingestHeartbeatMetrics(
227
243
  db: Database,
228
- input: { workspaceId: string; agentId: string; sample: MetricsSample },
244
+ input: {
245
+ accountId: string;
246
+ workspaceId: string;
247
+ agentId: string;
248
+ sample: MetricsSample;
249
+ },
229
250
  ): Promise<{ ingested: boolean; seriesAppended: boolean }> {
230
- // The enrollment row is the source of the accountId (the RLS principal) and the
231
- // existence check. A revoked machine still reports its accountId, so we ingest
232
- // (the dashboard shows its last sample); a truly unknown id is a no-op.
233
- const enrollment = await getEnrollment(db, input.workspaceId, input.agentId);
234
- if (!enrollment) {
235
- return { ingested: false, seriesAppended: false };
236
- }
237
251
  const sample = wireSampleToDbSample(input.sample);
238
- await touchEnrollmentLastSeen(db, {
239
- accountId: enrollment.accountId,
240
- workspaceId: input.workspaceId,
241
- enrollmentId: input.agentId,
242
- });
243
252
  const result = await ingestMachineMetricsSample(db, {
244
- accountId: enrollment.accountId,
253
+ accountId: input.accountId,
245
254
  workspaceId: input.workspaceId,
246
255
  enrollmentId: input.agentId,
247
256
  sample,
@@ -340,12 +349,16 @@ export async function handleAgentEventPayload(
340
349
  try {
341
350
  const enrollment = await getEnrollment(db, ids.workspaceId, ids.agentId);
342
351
  if (enrollment) {
343
- await setEnrollmentWentOffline(db, {
352
+ const released = await releaseEnrollmentConnection(db, {
344
353
  accountId: enrollment.accountId,
345
354
  workspaceId: ids.workspaceId,
346
355
  enrollmentId: ids.agentId,
356
+ connectionInstanceId: ids.connectionInstanceId,
347
357
  reason,
348
358
  });
359
+ // A late goodbye from a superseded process is not the current machine
360
+ // going offline. It must not disconnect browsers or fan out link loss.
361
+ if (!released.released) return;
349
362
  try {
350
363
  await disconnectAttachedBrowserDevices(db, {
351
364
  accountId: enrollment.accountId,
@@ -396,10 +409,77 @@ export async function handleAgentEventPayload(
396
409
  }
397
410
  return;
398
411
  }
412
+ if (event.event?.$case === "agentUpdateProgress") {
413
+ const enrollment = await getEnrollment(db, ids.workspaceId, ids.agentId).catch(() => null);
414
+ if (!enrollment || enrollment.connectionInstanceId !== ids.connectionInstanceId) return;
415
+ const progress = event.event.agentUpdateProgress;
416
+ const status = (() => {
417
+ switch (progress.stage) {
418
+ case AgentUpdateStage.AGENT_UPDATE_STAGE_ACCEPTED:
419
+ return "accepted" as const;
420
+ case AgentUpdateStage.AGENT_UPDATE_STAGE_WAITING_FOR_IDLE:
421
+ return "waiting_for_idle" as const;
422
+ case AgentUpdateStage.AGENT_UPDATE_STAGE_DOWNLOADING:
423
+ return "downloading" as const;
424
+ case AgentUpdateStage.AGENT_UPDATE_STAGE_VERIFYING:
425
+ return "verifying" as const;
426
+ case AgentUpdateStage.AGENT_UPDATE_STAGE_APPLYING:
427
+ return "applying" as const;
428
+ case AgentUpdateStage.AGENT_UPDATE_STAGE_RESTARTING:
429
+ return "restarting" as const;
430
+ case AgentUpdateStage.AGENT_UPDATE_STAGE_FAILED:
431
+ return "failed" as const;
432
+ default:
433
+ return null;
434
+ }
435
+ })();
436
+ if (!status) return;
437
+ try {
438
+ await advanceEnrollmentAgentUpdate(db, {
439
+ accountId: enrollment.accountId,
440
+ workspaceId: ids.workspaceId,
441
+ enrollmentId: ids.agentId,
442
+ connectionInstanceId: ids.connectionInstanceId,
443
+ connectionGeneration: enrollment.connectionGeneration,
444
+ operationId: progress.operationId,
445
+ status,
446
+ expectedBinarySha256: /^[0-9a-f]{64}$/.test(progress.expectedBinarySha256)
447
+ ? progress.expectedBinarySha256
448
+ : null,
449
+ errorCode: progress.errorCode || null,
450
+ retryable: progress.retryable,
451
+ rolledBack: progress.rolledBack,
452
+ });
453
+ } catch (error) {
454
+ observability?.warn?.("Failed to ingest a machine self-update progress event", {
455
+ subject,
456
+ error: error instanceof Error ? error.message : String(error),
457
+ });
458
+ }
459
+ return;
460
+ }
399
461
  if (event.event?.$case !== "heartbeat") {
400
462
  return; // an unknown event kind → not a metrics point.
401
463
  }
402
464
  const heartbeat = event.event.heartbeat;
465
+ const enrollment = await getEnrollment(db, ids.workspaceId, ids.agentId).catch(() => null);
466
+ if (!enrollment || enrollment.connectionInstanceId !== ids.connectionInstanceId) return;
467
+ try {
468
+ const renewed = await renewEnrollmentConnection(db, {
469
+ accountId: enrollment.accountId,
470
+ workspaceId: ids.workspaceId,
471
+ enrollmentId: ids.agentId,
472
+ connectionInstanceId: ids.connectionInstanceId,
473
+ leaseMs: AGENT_CONNECTION_LEASE_MS,
474
+ });
475
+ if (!renewed.renewed) return;
476
+ } catch (error) {
477
+ observability?.warn?.("Failed to renew a machine connection heartbeat", {
478
+ subject,
479
+ error: error instanceof Error ? error.message : String(error),
480
+ });
481
+ return;
482
+ }
403
483
  if (heartbeat.attachedBrowserInventory) {
404
484
  try {
405
485
  await ingestAttachedBrowserInventory(db, {
@@ -417,7 +497,8 @@ export async function handleAgentEventPayload(
417
497
  const metrics = heartbeat.metrics;
418
498
  if (!metrics) return;
419
499
  try {
420
- await ingestHeartbeat(db, {
500
+ await ingestHeartbeatMetrics(db, {
501
+ accountId: enrollment.accountId,
421
502
  workspaceId: ids.workspaceId,
422
503
  agentId: ids.agentId,
423
504
  sample: metrics,
@@ -431,7 +512,7 @@ export async function handleAgentEventPayload(
431
512
  }
432
513
 
433
514
  /**
434
- * Start the metrics-ingestion consumer: subscribe `agent.*.*.events` and ingest
515
+ * Start the metrics-ingestion consumer: subscribe exact process events and ingest
435
516
  * every heartbeat. Gated by sandboxSelfhostedEnabled (the caller checks the flag;
436
517
  * a disabled deployment never starts the consumer). Returns the unsubscribe fn.
437
518
  */
@@ -490,6 +571,46 @@ export function helloReportsOpStream(hello: Hello): boolean {
490
571
  return hello.capabilities?.opStream === true;
491
572
  }
492
573
 
574
+ function helloRuntimeCapabilities(hello: Hello): Record<string, boolean> {
575
+ const caps = hello.capabilities;
576
+ // Absence is an older-agent/unknown signal, not a fabricated set of false claims.
577
+ // Keep the durable cursor empty so reconnects from legacy agents remain a
578
+ // no-op and future capability additions do not get silently fabricated.
579
+ if (!caps) return {};
580
+ return {
581
+ exec: caps.exec === true,
582
+ filesystem: caps.filesystem === true,
583
+ git: caps.git === true,
584
+ pty: caps.pty === true,
585
+ desktop: caps.desktop === true,
586
+ opStream: caps.opStream === true,
587
+ browserBridge: caps.browserBridge === true,
588
+ operationResourcePolicy: caps.operationResourcePolicy === true,
589
+ operationCpuQuota: caps.operationCpuQuota === true,
590
+ };
591
+ }
592
+
593
+ function helloCompletedUpdate(hello: Hello): {
594
+ operationId: string;
595
+ targetVersion: string;
596
+ binarySha256: string;
597
+ } | null {
598
+ if (
599
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
600
+ hello.completedUpdateOperationId,
601
+ ) ||
602
+ !/^(?:v)?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(hello.completedUpdateTargetVersion) ||
603
+ !/^[0-9a-f]{64}$/.test(hello.completedUpdateBinarySha256)
604
+ ) {
605
+ return null;
606
+ }
607
+ return {
608
+ operationId: hello.completedUpdateOperationId,
609
+ targetVersion: hello.completedUpdateTargetVersion,
610
+ binarySha256: hello.completedUpdateBinarySha256,
611
+ };
612
+ }
613
+
493
614
  /**
494
615
  * Reconcile `enrollments.has_display` (+ the capture-blocked reason) to what a Hello
495
616
  * reports. Resolves the enrollment (the accountId is the RLS principal + the
@@ -505,6 +626,7 @@ export async function refreshEnrollmentDisplay(
505
626
  agentId: string;
506
627
  hasDisplay: boolean;
507
628
  desktopUnavailableReason?: string | null;
629
+ connectionInstanceId?: string;
508
630
  },
509
631
  ): Promise<{ updated: boolean }> {
510
632
  const desktopUnavailableReason = input.desktopUnavailableReason ?? null;
@@ -526,6 +648,7 @@ export async function refreshEnrollmentDisplay(
526
648
  enrollmentId: input.agentId,
527
649
  hasDisplay: input.hasDisplay,
528
650
  desktopUnavailableReason,
651
+ ...(input.connectionInstanceId ? { connectionInstanceId: input.connectionInstanceId } : {}),
529
652
  });
530
653
  }
531
654
 
@@ -541,6 +664,7 @@ export async function refreshEnrollmentOpStream(
541
664
  workspaceId: string;
542
665
  agentId: string;
543
666
  opStream: boolean;
667
+ connectionInstanceId?: string;
544
668
  },
545
669
  ): Promise<{ updated: boolean }> {
546
670
  const enrollment = await getEnrollment(db, input.workspaceId, input.agentId);
@@ -557,6 +681,7 @@ export async function refreshEnrollmentOpStream(
557
681
  workspaceId: input.workspaceId,
558
682
  enrollmentId: input.agentId,
559
683
  opStream: input.opStream,
684
+ ...(input.connectionInstanceId ? { connectionInstanceId: input.connectionInstanceId } : {}),
560
685
  });
561
686
  }
562
687
 
@@ -589,17 +714,56 @@ export async function handleHelloPayload(
589
714
  });
590
715
  return;
591
716
  }
717
+ const authority = await getLiveEnrollmentConnection(db, ids.workspaceId, ids.agentId).catch(
718
+ () => null,
719
+ );
720
+ if (!authority || authority.connectionInstanceId !== ids.connectionInstanceId) {
721
+ observability?.warn?.("Ignored a Hello from a superseded runner instance", {
722
+ workspaceId: ids.workspaceId,
723
+ agentId: ids.agentId,
724
+ });
725
+ return;
726
+ }
727
+ let workspaceRoot: string | undefined;
728
+ if (hello.workspaceRoot.length > 0) {
729
+ try {
730
+ workspaceRoot = normalizeConnectedMachineWorkspaceRoot(hello.workspaceRoot, authority.os);
731
+ } catch (error) {
732
+ observability?.warn?.("Ignored an invalid Connected Machine workspace root", {
733
+ workspaceId: ids.workspaceId,
734
+ agentId: ids.agentId,
735
+ error: error instanceof Error ? error.message : String(error),
736
+ });
737
+ }
738
+ }
592
739
  try {
593
740
  await refreshEnrollmentDisplay(db, {
594
741
  workspaceId: ids.workspaceId,
595
742
  agentId: ids.agentId,
596
743
  hasDisplay: helloReportsDisplay(hello),
597
744
  desktopUnavailableReason: helloDesktopUnavailableReason(hello),
745
+ connectionInstanceId: ids.connectionInstanceId,
598
746
  });
599
747
  await refreshEnrollmentOpStream(db, {
600
748
  workspaceId: ids.workspaceId,
601
749
  agentId: ids.agentId,
602
750
  opStream: helloReportsOpStream(hello),
751
+ connectionInstanceId: ids.connectionInstanceId,
752
+ });
753
+ await setEnrollmentAgentRuntime(db, {
754
+ accountId: authority.accountId,
755
+ workspaceId: ids.workspaceId,
756
+ enrollmentId: ids.agentId,
757
+ connectionInstanceId: ids.connectionInstanceId,
758
+ agentVersion: hello.agentVersion.trim() || null,
759
+ binarySha256: /^[0-9a-f]{64}$/.test(hello.binarySha256) ? hello.binarySha256 : null,
760
+ updateChannel:
761
+ hello.updateChannel === "stable" || hello.updateChannel === "beta"
762
+ ? hello.updateChannel
763
+ : null,
764
+ capabilities: helloRuntimeCapabilities(hello),
765
+ ...(workspaceRoot !== undefined ? { workspaceRoot } : {}),
766
+ completedUpdate: helloCompletedUpdate(hello),
603
767
  });
604
768
  } catch (error) {
605
769
  observability?.warn?.("Failed to refresh an enrollment's capabilities from a Hello", {
@@ -616,23 +780,21 @@ export async function handleHelloPayload(
616
780
  // sessions with an active op on it — a restored only ever pairs a prior lost, so
617
781
  // a routine connect Hello (no marker) emits nothing.
618
782
  try {
619
- const enrollment = await getEnrollment(db, ids.workspaceId, ids.agentId);
620
- if (enrollment) {
621
- const { cleared } = await clearEnrollmentWentOffline(db, {
622
- accountId: enrollment.accountId,
623
- workspaceId: ids.workspaceId,
624
- enrollmentId: ids.agentId,
625
- });
626
- if (cleared && bus) {
627
- await fanOutMachineLinkEvents(
628
- db,
629
- bus,
630
- observability,
631
- ids.workspaceId,
632
- ids.agentId,
633
- (activeTurnId) => [{ type: "machine.link.restored", turnId: activeTurnId, payload: {} }],
634
- );
635
- }
783
+ const { cleared } = await clearEnrollmentWentOffline(db, {
784
+ accountId: authority.accountId,
785
+ workspaceId: ids.workspaceId,
786
+ enrollmentId: ids.agentId,
787
+ connectionInstanceId: ids.connectionInstanceId,
788
+ });
789
+ if (cleared && bus) {
790
+ await fanOutMachineLinkEvents(
791
+ db,
792
+ bus,
793
+ observability,
794
+ ids.workspaceId,
795
+ ids.agentId,
796
+ (activeTurnId) => [{ type: "machine.link.restored", turnId: activeTurnId, payload: {} }],
797
+ );
636
798
  }
637
799
  } catch (error) {
638
800
  observability?.warn?.("Failed to clear a machine going-offline marker on a Hello", {
@@ -643,7 +805,7 @@ export async function handleHelloPayload(
643
805
  }
644
806
 
645
807
  /**
646
- * Start the Hello display-refresh consumer: subscribe `agent.*.*.hello` and
808
+ * Start the Hello display-refresh consumer: subscribe exact process hellos and
647
809
  * reconcile `has_display` to the live capability the agent reports on every
648
810
  * connect. Gated by sandboxSelfhostedEnabled (the caller checks the flag). Returns
649
811
  * the unsubscribe fn.