@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.
- package/dist/api-websocket.d.ts +18 -0
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth.d.ts +6 -0
- package/dist/{chunk-WCXHI3FF.js → chunk-QKDFBBUE.js} +26385 -15573
- package/dist/chunk-QKDFBBUE.js.map +1 -0
- package/dist/codemode.d.ts +8 -1
- package/dist/company-brain-okf.d.ts +24 -0
- package/dist/connected-machine-computer-access.d.ts +11 -0
- package/dist/connection-authority-owner.d.ts +59 -0
- package/dist/connection-ownership.d.ts +47 -0
- package/dist/controller-data-plane.d.ts +16 -0
- package/dist/editable-artifact-websocket.d.ts +5 -15
- package/dist/editable-artifact-workspace-files.d.ts +1 -1
- package/dist/http/api-error.d.ts +10 -0
- package/dist/http/cors.d.ts +5 -0
- package/dist/http/interaction-control-error.d.ts +13 -0
- package/dist/http/sse.d.ts +2 -3
- package/dist/index.d.ts +5 -2
- package/dist/index.js +326 -82
- package/dist/index.js.map +1 -1
- package/dist/integrations/atlassian.d.ts +17 -0
- package/dist/integrations/fiken.d.ts +7 -2
- package/dist/integrations/google-drive.d.ts +18 -0
- package/dist/integrations/oauth-client.d.ts +22 -3
- package/dist/integrations/oauth-profiles.d.ts +195 -0
- package/dist/integrations/personal-github-repositories.d.ts +45 -0
- package/dist/integrations/personal-github.d.ts +40 -0
- package/dist/integrations/pr-review-provider.d.ts +22 -0
- package/dist/integrations/provider-oauth.d.ts +8 -0
- package/dist/integrations/slack-app-home.d.ts +24 -0
- package/dist/integrations/slack-bot.d.ts +149 -9
- package/dist/integrations/slack-interactions.d.ts +4 -3
- package/dist/integrations/slack-routing.d.ts +120 -0
- package/dist/integrations/social-oauth.d.ts +5 -0
- package/dist/interaction-frame-proxy.d.ts +68 -0
- package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
- package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
- package/dist/mcp/documents.d.ts +1 -0
- package/dist/mcp/remember.d.ts +28 -0
- package/dist/mcp/request-abort.d.ts +19 -0
- package/dist/mcp/scheduled-task-view.d.ts +4 -4
- package/dist/mcp/server.d.ts +15 -2
- package/dist/mcp/session-view.d.ts +4 -0
- package/dist/mcp/session-wait.d.ts +137 -0
- package/dist/routes/automations.d.ts +4 -0
- package/dist/routes/billing.d.ts +5 -0
- package/dist/routes/browser-sessions.d.ts +4 -1
- package/dist/routes/company-brain.d.ts +3 -0
- package/dist/routes/company-profile.d.ts +2 -1
- package/dist/routes/connection-authorities.d.ts +3 -0
- package/dist/routes/personal-github-git-broker.d.ts +29 -0
- package/dist/routes/personal-github.d.ts +3 -0
- package/dist/routes/pr-review.d.ts +3 -0
- package/dist/routes/sessions.d.ts +1 -0
- package/dist/routes/user-resource-authorities.d.ts +3 -0
- package/dist/routes/workspace-capture.d.ts +10 -2
- package/dist/routes/workspace-learning.d.ts +3 -0
- package/dist/routes/workspaces.d.ts +19 -0
- package/dist/sandbox/channel-a.d.ts +11 -1
- package/dist/sandbox/connection-authority.d.ts +3 -0
- package/dist/sandbox/enrollment.d.ts +2 -0
- package/dist/sandbox/machines.d.ts +2 -2
- package/dist/sandbox/metrics-ingestion.d.ts +10 -20
- package/dist/sandbox/viewer.d.ts +24 -1
- package/dist/sandbox-file-artifacts.d.ts +11 -0
- package/dist/scheduled-task-deletion.d.ts +14 -0
- package/dist/slack-reaction-files.d.ts +1 -1
- package/dist/temporal-schedule-cleanup.d.ts +1 -0
- package/package.json +20 -18
- package/src/api-websocket.ts +23 -0
- package/src/app.ts +153 -23
- package/src/auth/managed-auth.ts +25 -3
- package/src/codemode.ts +53 -23
- package/src/codex-realtime.ts +8 -2
- package/src/company-brain-okf.ts +340 -0
- package/src/connected-machine-computer-access.ts +33 -0
- package/src/connection-authority-owner.ts +61 -0
- package/src/connection-ownership.ts +180 -0
- package/src/controller-data-plane.ts +47 -0
- package/src/editable-artifact-native-kernel.ts +26 -15
- package/src/editable-artifact-office-import.ts +28 -3
- package/src/editable-artifact-production.ts +13 -7
- package/src/editable-artifact-websocket.ts +11 -18
- package/src/editable-artifact-workspace-files.ts +31 -6
- package/src/http/api-error.ts +28 -0
- package/src/http/auth.ts +4 -0
- package/src/http/cors.ts +35 -0
- package/src/http/interaction-control-error.ts +164 -0
- package/src/http/sse.ts +112 -66
- package/src/index.ts +46 -6
- package/src/integrations/atlassian.ts +146 -35
- package/src/integrations/fiken.ts +102 -22
- package/src/integrations/google-drive.ts +301 -92
- package/src/integrations/oauth-client.ts +272 -143
- package/src/integrations/oauth-profiles.ts +477 -0
- package/src/integrations/personal-github-repositories.ts +445 -0
- package/src/integrations/personal-github.ts +705 -0
- package/src/integrations/pr-review-provider.ts +246 -0
- package/src/integrations/provider-oauth.ts +121 -5
- package/src/integrations/slack-app-home.ts +300 -0
- package/src/integrations/slack-bot.ts +337 -57
- package/src/integrations/slack-interactions.ts +1265 -208
- package/src/integrations/slack-routing.ts +324 -0
- package/src/integrations/social-oauth.ts +25 -0
- package/src/interaction-frame-proxy.ts +409 -0
- package/src/mcp/company-brain-governed-writes.ts +262 -0
- package/src/mcp/company-profile-agent-admin.ts +205 -0
- package/src/mcp/documents.ts +37 -6
- package/src/mcp/files.ts +16 -1
- package/src/mcp/remember.ts +181 -0
- package/src/mcp/request-abort.ts +44 -0
- package/src/mcp/scheduled-task-view.ts +1 -0
- package/src/mcp/server.ts +719 -369
- package/src/mcp/session-view.ts +54 -0
- package/src/mcp/session-wait.ts +554 -0
- package/src/model-catalog.ts +20 -12
- package/src/routes/api-integrations.ts +4 -0
- package/src/routes/api-keys.ts +1 -0
- package/src/routes/automations.ts +534 -0
- package/src/routes/billing.ts +57 -1
- package/src/routes/browser-sessions.ts +329 -79
- package/src/routes/channels.ts +17 -1
- package/src/routes/codex.ts +50 -0
- package/src/routes/company-brain.ts +367 -0
- package/src/routes/company-profile.ts +1 -1
- package/src/routes/computer-sessions.ts +320 -93
- package/src/routes/connection-authorities.ts +139 -0
- package/src/routes/connections.ts +144 -34
- package/src/routes/documents.ts +221 -3
- package/src/routes/editable-artifacts.ts +11 -3
- package/src/routes/enrollments.ts +116 -26
- package/src/routes/environments.ts +127 -55
- package/src/routes/files.ts +72 -11
- package/src/routes/install.ts +82 -28
- package/src/routes/integration-facets.ts +29 -0
- package/src/routes/interaction-resources.ts +20 -2
- package/src/routes/machines.ts +275 -13
- package/src/routes/organization-memberships.ts +717 -28
- package/src/routes/packs.ts +4 -3
- package/src/routes/personal-github-git-broker.ts +785 -0
- package/src/routes/personal-github.ts +319 -0
- package/src/routes/pr-review.ts +531 -0
- package/src/routes/rigs.ts +104 -39
- package/src/routes/scheduled-tasks.ts +19 -21
- package/src/routes/sessions.ts +775 -38
- package/src/routes/social.ts +14 -2
- package/src/routes/supergrok.ts +23 -4
- package/src/routes/transcription-recordings.ts +8 -2
- package/src/routes/user-resource-authorities.ts +138 -0
- package/src/routes/workspace-artifacts.ts +4 -1
- package/src/routes/workspace-capture.ts +21 -0
- package/src/routes/workspace-learning.ts +228 -0
- package/src/routes/workspaces.ts +59 -6
- package/src/sandbox/auth-callout.ts +38 -12
- package/src/sandbox/channel-a.ts +242 -19
- package/src/sandbox/connection-authority.ts +3 -0
- package/src/sandbox/enrollment.ts +15 -3
- package/src/sandbox/machines.ts +186 -64
- package/src/sandbox/metrics-ingestion.ts +220 -58
- package/src/sandbox/viewer.ts +243 -19
- package/src/sandbox-file-artifacts.ts +329 -0
- package/src/scheduled-task-deletion.ts +127 -0
- package/src/slack-reaction-files.ts +16 -5
- package/src/temporal-schedule-cleanup.ts +13 -0
- 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
|
|
5
|
-
// `Hello` (its live self-description) on
|
|
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
|
|
26
|
-
//
|
|
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>`
|
|
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 (
|
|
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 {
|
|
96
|
+
return {
|
|
97
|
+
workspaceId: parts[1]!,
|
|
98
|
+
agentId: parts[2]!,
|
|
99
|
+
connectionInstanceId: parts[4]!,
|
|
100
|
+
};
|
|
85
101
|
}
|
|
86
102
|
|
|
87
|
-
/** Parse
|
|
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
|
|
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
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
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
|
-
|
|
242
|
+
async function ingestHeartbeatMetrics(
|
|
227
243
|
db: Database,
|
|
228
|
-
input: {
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
|
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.
|