@opengeni/core 1.0.1 → 2.2.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/access/index.d.ts +37 -0
- package/dist/application/new-session-drafts.d.ts +8 -1
- package/dist/application/session-commands.d.ts +18 -11
- package/dist/application/session-tenancy.d.ts +18 -0
- package/dist/application/user-resource-grants.d.ts +20 -0
- package/dist/{chunk-6WKPWE5S.js → chunk-H7X52RI2.js} +27 -18
- package/dist/chunk-H7X52RI2.js.map +1 -0
- package/dist/{chunk-JJU6I5XD.js → chunk-VCZFFEPB.js} +21 -9
- package/dist/chunk-VCZFFEPB.js.map +1 -0
- package/dist/{chunk-NYPMQ7NO.js → chunk-ZYCNLYGH.js} +29 -27
- package/dist/chunk-ZYCNLYGH.js.map +1 -0
- package/dist/dependencies.d.ts +12 -1
- package/dist/domain/automations.d.ts +51 -0
- package/dist/domain/company-brain-governed-writes.d.ts +70 -0
- package/dist/domain/company-profile-agent-admin.d.ts +24 -0
- package/dist/domain/connection-authority.d.ts +97 -0
- package/dist/domain/connection-authority.js +169 -0
- package/dist/domain/connection-authority.js.map +1 -0
- package/dist/domain/editable-artifacts/service.d.ts +1 -2
- package/dist/domain/environments.d.ts +1 -1
- package/dist/domain/github-repository-bindings.d.ts +117 -0
- package/dist/domain/governed-learning-activation.d.ts +16 -0
- package/dist/domain/governed-learning-evaluator.d.ts +6 -0
- package/dist/domain/governed-learning-slack-publication.d.ts +45 -0
- package/dist/domain/memory-slack-delivery.d.ts +1 -4
- package/dist/domain/organization-membership-lifecycle.d.ts +1 -0
- package/dist/domain/packs.d.ts +5 -1
- package/dist/domain/personal-connection-delegations.d.ts +34 -2
- package/dist/domain/pr-review.d.ts +149 -0
- package/dist/domain/remember.d.ts +69 -0
- package/dist/domain/resources.d.ts +22 -1
- package/dist/domain/scheduled-tasks.d.ts +43 -2
- package/dist/domain/session-tool-policy.d.ts +1 -1
- package/dist/domain/sessions.d.ts +40 -6
- package/dist/editable-artifact-live/application.d.ts +10 -2
- package/dist/editable-artifact-live/ports.d.ts +0 -1
- package/dist/editable-artifact-live/server.d.ts +0 -1
- package/dist/editable-artifact-live/ticket.d.ts +0 -1
- package/dist/editable-artifact-live/types.d.ts +5 -2
- package/dist/editable-artifact-live.js +2 -2
- package/dist/editable-artifacts.js +2 -4
- package/dist/index.d.ts +13 -1
- package/dist/index.js +4827 -1164
- package/dist/index.js.map +1 -1
- package/dist/rigs/index.d.ts +22 -5
- package/dist/sandbox/fleet.d.ts +34 -6
- package/dist/sandbox/routing.d.ts +15 -1
- package/dist/sandbox/runtime-settings.d.ts +39 -0
- package/package.json +16 -11
- package/src/access/index.ts +157 -31
- package/src/application/new-session-drafts.ts +24 -2
- package/src/application/session-commands.ts +305 -71
- package/src/application/session-tenancy.ts +321 -0
- package/src/application/user-resource-grants.ts +140 -0
- package/src/billing/limits.ts +25 -15
- package/src/dependencies.ts +12 -1
- package/src/domain/automations.ts +196 -0
- package/src/domain/capabilities.ts +7 -1
- package/src/domain/company-brain-governed-writes.ts +330 -0
- package/src/domain/company-profile-agent-admin.ts +55 -0
- package/src/domain/connection-authority.ts +329 -0
- package/src/domain/editable-artifacts/agent-application.ts +17 -9
- package/src/domain/editable-artifacts/service.ts +12 -1
- package/src/domain/editable-artifacts/snapshot-compaction.ts +9 -5
- package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +10 -8
- package/src/domain/editable-artifacts/snapshot-verifier.ts +10 -3
- package/src/domain/environments.ts +16 -3
- package/src/domain/github-repository-bindings.ts +295 -0
- package/src/domain/governed-learning-activation.ts +44 -0
- package/src/domain/governed-learning-evaluator.ts +49 -0
- package/src/domain/governed-learning-slack-publication.ts +157 -0
- package/src/domain/insights.ts +9 -0
- package/src/domain/memory-slack-delivery.ts +0 -30
- package/src/domain/organization-membership-lifecycle.ts +9 -0
- package/src/domain/packs.ts +106 -3
- package/src/domain/personal-connection-delegations.ts +532 -51
- package/src/domain/pr-review.ts +671 -0
- package/src/domain/remember.ts +549 -0
- package/src/domain/resources.ts +131 -4
- package/src/domain/scheduled-tasks.ts +497 -76
- package/src/domain/session-tool-policy.ts +12 -9
- package/src/domain/sessions.ts +613 -166
- package/src/editable-artifact-live/application.ts +21 -3
- package/src/editable-artifact-live/ports.ts +0 -1
- package/src/editable-artifact-live/server.ts +13 -7
- package/src/editable-artifact-live/ticket.ts +1 -5
- package/src/editable-artifact-live/types.ts +6 -2
- package/src/editable-artifact-live/wire.ts +0 -10
- package/src/index.ts +13 -1
- package/src/rigs/index.ts +165 -48
- package/src/sandbox/fleet.ts +376 -30
- package/src/sandbox/routing.ts +91 -29
- package/src/sandbox/runtime-settings.ts +274 -0
- package/src/session-authorization.ts +17 -49
- package/dist/chunk-6WKPWE5S.js.map +0 -1
- package/dist/chunk-JJU6I5XD.js.map +0 -1
- package/dist/chunk-NYPMQ7NO.js.map +0 -1
- package/dist/domain/durable-learning-slack-publication.d.ts +0 -72
- package/src/domain/durable-learning-slack-publication.ts +0 -202
package/src/sandbox/fleet.ts
CHANGED
|
@@ -13,11 +13,15 @@
|
|
|
13
13
|
|
|
14
14
|
import type { Settings } from "@opengeni/config";
|
|
15
15
|
import {
|
|
16
|
+
authorizePersonalMachineForAttempt,
|
|
16
17
|
getEnrollment,
|
|
18
|
+
getLiveEnrollmentConnection,
|
|
17
19
|
getSandbox,
|
|
20
|
+
listEnrollments,
|
|
18
21
|
listSandboxes,
|
|
19
22
|
readActiveSandbox,
|
|
20
23
|
readLease,
|
|
24
|
+
resolvePersonalMachineConnectionForAttempt,
|
|
21
25
|
requireSession,
|
|
22
26
|
setActiveSandbox,
|
|
23
27
|
type Database,
|
|
@@ -30,12 +34,15 @@ import {
|
|
|
30
34
|
NatsOpStreamTransport,
|
|
31
35
|
selfhostedLiveness,
|
|
32
36
|
SelfhostedSession,
|
|
37
|
+
resolveConnectedMachineWorkspaceRoot,
|
|
33
38
|
swapTargetEstablishability,
|
|
34
39
|
type BackendUnresolvableCode,
|
|
35
40
|
type ControlRpc,
|
|
36
41
|
type NatsRequestConnection,
|
|
37
42
|
type SelfhostedRelayConfig,
|
|
38
43
|
type SelfhostedOpStreamDeps,
|
|
44
|
+
type SelfhostedOperationAdmission,
|
|
45
|
+
type SelfhostedOperationResourcePolicy,
|
|
39
46
|
} from "@opengeni/runtime/sandbox";
|
|
40
47
|
import { relayConfigFromSettings } from "./routing";
|
|
41
48
|
|
|
@@ -57,6 +64,13 @@ export type FleetReadinessHold = {
|
|
|
57
64
|
export type FleetContext = {
|
|
58
65
|
accountId: string;
|
|
59
66
|
workspaceId: string;
|
|
67
|
+
subjectId?: string;
|
|
68
|
+
attemptAuthority?: {
|
|
69
|
+
turnId: string;
|
|
70
|
+
attemptId: string;
|
|
71
|
+
executionGeneration: number;
|
|
72
|
+
initiatingHumanSubjectId: string;
|
|
73
|
+
};
|
|
60
74
|
/** The calling session (the pointer the attach/swap mutates + whose group box
|
|
61
75
|
* is the default fleet member). */
|
|
62
76
|
sessionId: string;
|
|
@@ -78,12 +92,20 @@ export type FleetContext = {
|
|
|
78
92
|
*/
|
|
79
93
|
export async function buildFleetContextForSession(
|
|
80
94
|
deps: { db: Database },
|
|
81
|
-
ctx: {
|
|
95
|
+
ctx: {
|
|
96
|
+
accountId: string;
|
|
97
|
+
workspaceId: string;
|
|
98
|
+
sessionId: string;
|
|
99
|
+
subjectId?: string;
|
|
100
|
+
attemptAuthority?: FleetContext["attemptAuthority"];
|
|
101
|
+
},
|
|
82
102
|
): Promise<FleetContext> {
|
|
83
103
|
const session = await requireSession(deps.db, ctx.workspaceId, ctx.sessionId);
|
|
84
104
|
return {
|
|
85
105
|
accountId: ctx.accountId,
|
|
86
106
|
workspaceId: ctx.workspaceId,
|
|
107
|
+
...(ctx.subjectId ? { subjectId: ctx.subjectId } : {}),
|
|
108
|
+
...(ctx.attemptAuthority ? { attemptAuthority: ctx.attemptAuthority } : {}),
|
|
87
109
|
sessionId: ctx.sessionId,
|
|
88
110
|
sessionBackend: session.sandboxBackend,
|
|
89
111
|
sessionGroupId: session.sandboxGroupId,
|
|
@@ -98,14 +120,15 @@ export type FleetOperationAvailability = "ready" | "wakeable" | "recovering" | "
|
|
|
98
120
|
* A fleet member as the agent + the dock see it (the M8b/M9 UI seam — the
|
|
99
121
|
* `sandboxes_list` response entry the dock renders). STABLE shape: the dock keys
|
|
100
122
|
* on `id`, renders `name`/`kind`/`liveness`, and marks `active`. The session's own
|
|
101
|
-
*
|
|
102
|
-
* null `enrollmentId`; an enrolled machine
|
|
123
|
+
* managed group box is a synthetic entry with `id: groupId`, its actual
|
|
124
|
+
* provisioned backend kind, and a null `enrollmentId`; an enrolled machine
|
|
125
|
+
* carries its sandbox + enrollment ids.
|
|
103
126
|
*/
|
|
104
127
|
export type FleetSandboxEntry = {
|
|
105
128
|
/** The sandbox id used as the attach/swap/run_on `target`. For the session's
|
|
106
129
|
* own group box this is the group id (a null active pointer == this box). */
|
|
107
130
|
id: string;
|
|
108
|
-
kind: "modal" | "selfhosted";
|
|
131
|
+
kind: "modal" | "selfhosted" | "opensandbox";
|
|
109
132
|
name: string;
|
|
110
133
|
liveness: FleetLiveness;
|
|
111
134
|
/** True for the session's currently-active sandbox (the routing target). */
|
|
@@ -165,6 +188,7 @@ export type FleetSwapResult = {
|
|
|
165
188
|
reason?: string;
|
|
166
189
|
code?:
|
|
167
190
|
| BackendUnresolvableCode
|
|
191
|
+
| "invalid_working_directory"
|
|
168
192
|
| "concurrent_swap"
|
|
169
193
|
| "recovery_in_progress"
|
|
170
194
|
| "recovery_degraded"
|
|
@@ -191,13 +215,22 @@ async function probeEnrollment(
|
|
|
191
215
|
services: FleetServices,
|
|
192
216
|
workspaceId: string,
|
|
193
217
|
enrollment: EnrollmentRecord,
|
|
194
|
-
|
|
218
|
+
liveConnection: EnrollmentRecord | null = enrollment,
|
|
219
|
+
): Promise<{
|
|
220
|
+
liveness: FleetLiveness;
|
|
221
|
+
consented: boolean;
|
|
222
|
+
hasDisplay: boolean;
|
|
223
|
+
}> {
|
|
195
224
|
const { settings, bus } = services;
|
|
196
225
|
let probeResponded = false;
|
|
197
|
-
if (
|
|
226
|
+
if (liveConnection?.connectionInstanceId) {
|
|
198
227
|
const session = new SelfhostedSession({
|
|
199
228
|
workspaceId,
|
|
200
229
|
agentId: enrollment.id,
|
|
230
|
+
connectionInstanceId: liveConnection.connectionInstanceId,
|
|
231
|
+
// Ping does not touch the filesystem. Use the reported root when present;
|
|
232
|
+
// older agents can still answer liveness before they reconnect and report it.
|
|
233
|
+
workspaceRoot: enrollment.workspaceRoot ?? "/",
|
|
201
234
|
controlRpc: controlRpc(bus),
|
|
202
235
|
relay: relayConfigFromSettings(settings),
|
|
203
236
|
timeoutMs: PROBE_TIMEOUT_MS,
|
|
@@ -220,7 +253,11 @@ async function probeEnrollment(
|
|
|
220
253
|
},
|
|
221
254
|
probeResponded,
|
|
222
255
|
});
|
|
223
|
-
return {
|
|
256
|
+
return {
|
|
257
|
+
liveness: state.state,
|
|
258
|
+
consented: state.consented,
|
|
259
|
+
hasDisplay: state.hasDisplay,
|
|
260
|
+
};
|
|
224
261
|
}
|
|
225
262
|
|
|
226
263
|
/**
|
|
@@ -235,6 +272,13 @@ export async function listFleet(
|
|
|
235
272
|
ctx: FleetContext,
|
|
236
273
|
): Promise<FleetListResult> {
|
|
237
274
|
const { db } = services;
|
|
275
|
+
const resourceAccess = ctx.subjectId
|
|
276
|
+
? {
|
|
277
|
+
accountId: ctx.accountId,
|
|
278
|
+
workspaceId: ctx.workspaceId,
|
|
279
|
+
subjectId: ctx.subjectId,
|
|
280
|
+
}
|
|
281
|
+
: ctx.workspaceId;
|
|
238
282
|
const pointer = (await readActiveSandbox(db, ctx.workspaceId, ctx.sessionId)) ?? {
|
|
239
283
|
activeSandboxId: null,
|
|
240
284
|
activeEpoch: 0,
|
|
@@ -278,7 +322,12 @@ export async function listFleet(
|
|
|
278
322
|
: "wakeable";
|
|
279
323
|
entries.push({
|
|
280
324
|
id: ctx.sessionGroupId,
|
|
281
|
-
kind:
|
|
325
|
+
kind:
|
|
326
|
+
ctx.sessionBackend === "selfhosted"
|
|
327
|
+
? "selfhosted"
|
|
328
|
+
: ctx.sessionBackend === "opensandbox"
|
|
329
|
+
? "opensandbox"
|
|
330
|
+
: "modal",
|
|
282
331
|
name: "session sandbox",
|
|
283
332
|
liveness: groupOnline ? "online" : groupRecovering ? "reconnecting" : "offline",
|
|
284
333
|
active: groupActive,
|
|
@@ -302,20 +351,65 @@ export async function listFleet(
|
|
|
302
351
|
|
|
303
352
|
// The workspace's first-class selfhosted sandboxes (enrolled machines). Probe
|
|
304
353
|
// each for liveness; a missing enrollment is offline.
|
|
305
|
-
const
|
|
354
|
+
const legacySandboxes = await listSandboxes(db, ctx.workspaceId);
|
|
355
|
+
const enrollments = await listEnrollments(
|
|
356
|
+
db,
|
|
357
|
+
ctx.subjectId
|
|
358
|
+
? {
|
|
359
|
+
accountId: ctx.accountId,
|
|
360
|
+
workspaceId: ctx.workspaceId,
|
|
361
|
+
subjectId: ctx.subjectId,
|
|
362
|
+
}
|
|
363
|
+
: ctx.workspaceId,
|
|
364
|
+
{ status: "active" },
|
|
365
|
+
);
|
|
366
|
+
const enrollmentById = new Map(enrollments.map((enrollment) => [enrollment.id, enrollment]));
|
|
367
|
+
const scopedSandboxes: SandboxRecord[] = enrollments.flatMap((enrollment) =>
|
|
368
|
+
enrollment.sandboxId
|
|
369
|
+
? [
|
|
370
|
+
{
|
|
371
|
+
id: enrollment.sandboxId,
|
|
372
|
+
accountId: enrollment.accountId,
|
|
373
|
+
workspaceId: enrollment.workspaceId,
|
|
374
|
+
kind: "selfhosted" as const,
|
|
375
|
+
name: enrollment.sandboxName ?? `${enrollment.os} machine`,
|
|
376
|
+
enrollmentId: enrollment.id,
|
|
377
|
+
createdAt: enrollment.createdAt,
|
|
378
|
+
updatedAt: enrollment.updatedAt,
|
|
379
|
+
},
|
|
380
|
+
]
|
|
381
|
+
: [],
|
|
382
|
+
);
|
|
383
|
+
const sandboxes = [
|
|
384
|
+
...legacySandboxes.filter(
|
|
385
|
+
(sandbox) => !scopedSandboxes.some((scoped) => scoped.id === sandbox.id),
|
|
386
|
+
),
|
|
387
|
+
...scopedSandboxes,
|
|
388
|
+
];
|
|
306
389
|
for (const sandbox of sandboxes) {
|
|
307
390
|
if (sandbox.kind !== "selfhosted" || !sandbox.enrollmentId) {
|
|
308
391
|
continue;
|
|
309
392
|
}
|
|
310
|
-
const enrollment =
|
|
393
|
+
const enrollment =
|
|
394
|
+
enrollmentById.get(sandbox.enrollmentId) ??
|
|
395
|
+
(await getEnrollment(db, resourceAccess, sandbox.enrollmentId));
|
|
311
396
|
if (!enrollment || enrollment.status !== "active") {
|
|
312
397
|
// Revoked enrollments remain durable audit/history records, but they are
|
|
313
398
|
// intentionally absent from the normal attach/run picker.
|
|
314
399
|
continue;
|
|
315
400
|
}
|
|
401
|
+
const liveConnection = await getLiveEnrollmentConnection(
|
|
402
|
+
db,
|
|
403
|
+
resourceAccess,
|
|
404
|
+
sandbox.enrollmentId,
|
|
405
|
+
);
|
|
316
406
|
const probe = enrollment
|
|
317
|
-
? await probeEnrollment(services,
|
|
318
|
-
: {
|
|
407
|
+
? await probeEnrollment(services, sandbox.workspaceId, enrollment, liveConnection)
|
|
408
|
+
: {
|
|
409
|
+
liveness: "offline" as FleetLiveness,
|
|
410
|
+
consented: false,
|
|
411
|
+
hasDisplay: false,
|
|
412
|
+
};
|
|
319
413
|
entries.push({
|
|
320
414
|
id: sandbox.id,
|
|
321
415
|
kind: "selfhosted",
|
|
@@ -368,7 +462,7 @@ async function resolveTarget(
|
|
|
368
462
|
ctx: FleetContext,
|
|
369
463
|
target: string,
|
|
370
464
|
): Promise<
|
|
371
|
-
| { ok: true; targetSandboxId: string | null }
|
|
465
|
+
| { ok: true; targetSandboxId: string | null; workspaceRoot?: string }
|
|
372
466
|
| { ok: false; reason: string; code: BackendUnresolvableCode }
|
|
373
467
|
> {
|
|
374
468
|
// The session's own group box → the default pointer (null).
|
|
@@ -382,7 +476,17 @@ async function resolveTarget(
|
|
|
382
476
|
}
|
|
383
477
|
return { ok: true, targetSandboxId: null };
|
|
384
478
|
}
|
|
385
|
-
const sandbox = await getSandbox(
|
|
479
|
+
const sandbox = await getSandbox(
|
|
480
|
+
services.db,
|
|
481
|
+
ctx.subjectId
|
|
482
|
+
? {
|
|
483
|
+
accountId: ctx.accountId,
|
|
484
|
+
workspaceId: ctx.workspaceId,
|
|
485
|
+
subjectId: ctx.subjectId,
|
|
486
|
+
}
|
|
487
|
+
: ctx.workspaceId,
|
|
488
|
+
target,
|
|
489
|
+
);
|
|
386
490
|
if (!sandbox) {
|
|
387
491
|
return {
|
|
388
492
|
ok: false,
|
|
@@ -402,8 +506,13 @@ async function resolveTarget(
|
|
|
402
506
|
isSessionGroup: false,
|
|
403
507
|
});
|
|
404
508
|
if (!establishable.ok) {
|
|
405
|
-
return {
|
|
509
|
+
return {
|
|
510
|
+
ok: false,
|
|
511
|
+
reason: establishable.reason,
|
|
512
|
+
code: establishable.code,
|
|
513
|
+
};
|
|
406
514
|
}
|
|
515
|
+
let targetWorkspaceRoot: string | undefined;
|
|
407
516
|
if (sandbox.kind === "selfhosted") {
|
|
408
517
|
if (!sandbox.enrollmentId) {
|
|
409
518
|
return {
|
|
@@ -412,7 +521,17 @@ async function resolveTarget(
|
|
|
412
521
|
code: "offline_enrollment",
|
|
413
522
|
};
|
|
414
523
|
}
|
|
415
|
-
const enrollment = await
|
|
524
|
+
const enrollment = await getLiveEnrollmentConnection(
|
|
525
|
+
services.db,
|
|
526
|
+
ctx.subjectId
|
|
527
|
+
? {
|
|
528
|
+
accountId: ctx.accountId,
|
|
529
|
+
workspaceId: ctx.workspaceId,
|
|
530
|
+
subjectId: ctx.subjectId,
|
|
531
|
+
}
|
|
532
|
+
: sandbox.workspaceId,
|
|
533
|
+
sandbox.enrollmentId,
|
|
534
|
+
);
|
|
416
535
|
if (!enrollment) {
|
|
417
536
|
return {
|
|
418
537
|
ok: false,
|
|
@@ -420,7 +539,15 @@ async function resolveTarget(
|
|
|
420
539
|
code: "offline_enrollment",
|
|
421
540
|
};
|
|
422
541
|
}
|
|
423
|
-
|
|
542
|
+
if (!enrollment.workspaceRoot) {
|
|
543
|
+
return {
|
|
544
|
+
ok: false,
|
|
545
|
+
reason: `sandbox ${target} has not reported an absolute workspace root; reconnect it with a current agent`,
|
|
546
|
+
code: "workspace_root_unavailable",
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
targetWorkspaceRoot = enrollment.workspaceRoot;
|
|
550
|
+
const probe = await probeEnrollment(services, sandbox.workspaceId, enrollment, enrollment);
|
|
424
551
|
if (probe.liveness !== "online") {
|
|
425
552
|
return {
|
|
426
553
|
ok: false,
|
|
@@ -429,7 +556,11 @@ async function resolveTarget(
|
|
|
429
556
|
};
|
|
430
557
|
}
|
|
431
558
|
}
|
|
432
|
-
return {
|
|
559
|
+
return {
|
|
560
|
+
ok: true,
|
|
561
|
+
targetSandboxId: sandbox.id,
|
|
562
|
+
...(targetWorkspaceRoot ? { workspaceRoot: targetWorkspaceRoot } : {}),
|
|
563
|
+
};
|
|
433
564
|
}
|
|
434
565
|
|
|
435
566
|
/**
|
|
@@ -482,6 +613,7 @@ export async function swapActiveSandbox(
|
|
|
482
613
|
const pointer = (await readActiveSandbox(services.db, ctx.workspaceId, ctx.sessionId)) ?? {
|
|
483
614
|
activeSandboxId: null,
|
|
484
615
|
activeEpoch: 0,
|
|
616
|
+
workingDir: null,
|
|
485
617
|
};
|
|
486
618
|
return {
|
|
487
619
|
swapped: false,
|
|
@@ -503,7 +635,27 @@ export async function swapActiveSandbox(
|
|
|
503
635
|
const pointer = (await readActiveSandbox(services.db, ctx.workspaceId, ctx.sessionId)) ?? {
|
|
504
636
|
activeSandboxId: null,
|
|
505
637
|
activeEpoch: 0,
|
|
638
|
+
workingDir: null,
|
|
506
639
|
};
|
|
640
|
+
let effectiveWorkingDir = workingDir;
|
|
641
|
+
if (resolved.workspaceRoot) {
|
|
642
|
+
try {
|
|
643
|
+
effectiveWorkingDir = resolveConnectedMachineWorkspaceRoot(
|
|
644
|
+
resolved.workspaceRoot,
|
|
645
|
+
workingDir === undefined && pointer.activeSandboxId === resolved.targetSandboxId
|
|
646
|
+
? pointer.workingDir
|
|
647
|
+
: workingDir,
|
|
648
|
+
);
|
|
649
|
+
} catch (error) {
|
|
650
|
+
return {
|
|
651
|
+
swapped: false,
|
|
652
|
+
activeSandboxId: pointer.activeSandboxId,
|
|
653
|
+
activeEpoch: pointer.activeEpoch,
|
|
654
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
655
|
+
code: "invalid_working_directory",
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
}
|
|
507
659
|
// Even a same-target attach advances active_epoch. It is a repair/fence
|
|
508
660
|
// request, not a no-op acknowledgment: any cached stale route is invalidated
|
|
509
661
|
// only after target readiness has been proved above.
|
|
@@ -513,7 +665,13 @@ export async function swapActiveSandbox(
|
|
|
513
665
|
sessionId: ctx.sessionId,
|
|
514
666
|
targetSandboxId: resolved.targetSandboxId,
|
|
515
667
|
expectedEpoch: pointer.activeEpoch,
|
|
516
|
-
...(
|
|
668
|
+
...(ctx.subjectId ? { subjectId: ctx.subjectId } : {}),
|
|
669
|
+
...(ctx.attemptAuthority ? { personalMachineAttempt: ctx.attemptAuthority } : {}),
|
|
670
|
+
...(resolved.workspaceRoot !== undefined
|
|
671
|
+
? { workingDir: effectiveWorkingDir ?? resolved.workspaceRoot }
|
|
672
|
+
: workingDir !== undefined
|
|
673
|
+
? { workingDir }
|
|
674
|
+
: {}),
|
|
517
675
|
});
|
|
518
676
|
if (result.swapped && result.pointer) {
|
|
519
677
|
return {
|
|
@@ -569,6 +727,9 @@ export type RunOnResult = {
|
|
|
569
727
|
export type RunOnSelfhostedMachine = {
|
|
570
728
|
workspaceId: string;
|
|
571
729
|
agentId: string;
|
|
730
|
+
/** Exact live daemon pinned for this operation. */
|
|
731
|
+
connectionInstanceId: string;
|
|
732
|
+
workspaceRoot: string;
|
|
572
733
|
controlRpc: ControlRpc;
|
|
573
734
|
relay: SelfhostedRelayConfig;
|
|
574
735
|
/** Short request/reply deadline for read/write and other control operations. */
|
|
@@ -577,8 +738,50 @@ export type RunOnSelfhostedMachine = {
|
|
|
577
738
|
execTimeoutMs: number;
|
|
578
739
|
/** Streaming transport required when execTimeoutMs is 0 (unbounded). */
|
|
579
740
|
opStream?: SelfhostedOpStreamDeps;
|
|
741
|
+
operationResourcePolicy?: SelfhostedOperationResourcePolicy;
|
|
742
|
+
operationResourcePolicySupported?: boolean;
|
|
743
|
+
operationCpuQuotaSupported?: boolean;
|
|
744
|
+
/** Last-boundary authority/connection resolver invoked before every physical
|
|
745
|
+
* provider operation. */
|
|
746
|
+
resolveOperationAdmission?: () => Promise<SelfhostedOperationAdmission | null>;
|
|
747
|
+
/**
|
|
748
|
+
* Exact-attempt values for this one child process. Never persisted on the
|
|
749
|
+
* enrollment or machine. The caller may supply these only after authenticating
|
|
750
|
+
* the active attempt that owns the one-off operation.
|
|
751
|
+
*/
|
|
752
|
+
transientExecEnvironment?: Readonly<Record<string, string>>;
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
export type RunOnOptions = {
|
|
756
|
+
transientExecEnvironment?: Readonly<Record<string, string>>;
|
|
580
757
|
};
|
|
581
758
|
|
|
759
|
+
function runOnOperationAdmission(
|
|
760
|
+
services: FleetServices,
|
|
761
|
+
enrollment: EnrollmentRecord | null,
|
|
762
|
+
): SelfhostedOperationAdmission | null {
|
|
763
|
+
if (!enrollment?.connectionInstanceId || !enrollment.workspaceRoot) return null;
|
|
764
|
+
const opStream =
|
|
765
|
+
services.settings.agentOpStreamEnabled === true &&
|
|
766
|
+
enrollment.opStream === true &&
|
|
767
|
+
services.bus?.getOpStreamConnection
|
|
768
|
+
? {
|
|
769
|
+
transport: new NatsOpStreamTransport(
|
|
770
|
+
async () => services.bus?.getOpStreamConnection?.() ?? null,
|
|
771
|
+
),
|
|
772
|
+
}
|
|
773
|
+
: undefined;
|
|
774
|
+
return {
|
|
775
|
+
workspaceId: enrollment.workspaceId,
|
|
776
|
+
connectionInstanceId: enrollment.connectionInstanceId,
|
|
777
|
+
workspaceRoot: enrollment.workspaceRoot,
|
|
778
|
+
...(opStream ? { opStream } : {}),
|
|
779
|
+
operationResourcePolicy: enrollment.operationPolicy,
|
|
780
|
+
operationResourcePolicySupported: enrollment.agentCapabilities.operationResourcePolicy === true,
|
|
781
|
+
operationCpuQuotaSupported: enrollment.agentCapabilities.operationCpuQuota === true,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
|
|
582
785
|
/**
|
|
583
786
|
* Execute the one-off machine operation once the workspace/enrollment lookup has
|
|
584
787
|
* succeeded. Kept separate from {@link runOnSandbox} so the command/deadline
|
|
@@ -593,10 +796,29 @@ export async function executeRunOnSelfhostedMachine(
|
|
|
593
796
|
const session = new SelfhostedSession({
|
|
594
797
|
workspaceId: machine.workspaceId,
|
|
595
798
|
agentId: machine.agentId,
|
|
799
|
+
connectionInstanceId: machine.connectionInstanceId,
|
|
800
|
+
workspaceRoot: machine.workspaceRoot,
|
|
596
801
|
controlRpc: machine.controlRpc,
|
|
597
802
|
relay: machine.relay,
|
|
598
803
|
timeoutMs: machine.controlTimeoutMs,
|
|
599
804
|
execTimeoutMs: machine.execTimeoutMs,
|
|
805
|
+
...(machine.operationResourcePolicy !== undefined
|
|
806
|
+
? { operationResourcePolicy: machine.operationResourcePolicy }
|
|
807
|
+
: {}),
|
|
808
|
+
...(machine.operationResourcePolicySupported !== undefined
|
|
809
|
+
? {
|
|
810
|
+
operationResourcePolicySupported: machine.operationResourcePolicySupported,
|
|
811
|
+
}
|
|
812
|
+
: {}),
|
|
813
|
+
...(machine.operationCpuQuotaSupported !== undefined
|
|
814
|
+
? { operationCpuQuotaSupported: machine.operationCpuQuotaSupported }
|
|
815
|
+
: {}),
|
|
816
|
+
...(machine.resolveOperationAdmission !== undefined
|
|
817
|
+
? { resolveOperationAdmission: machine.resolveOperationAdmission }
|
|
818
|
+
: {}),
|
|
819
|
+
...(machine.transientExecEnvironment !== undefined
|
|
820
|
+
? { transientExecEnvironment: () => machine.transientExecEnvironment! }
|
|
821
|
+
: {}),
|
|
600
822
|
...(machine.opStream !== undefined ? { opStream: machine.opStream } : {}),
|
|
601
823
|
});
|
|
602
824
|
|
|
@@ -622,7 +844,9 @@ export async function executeRunOnSelfhostedMachine(
|
|
|
622
844
|
timedOut,
|
|
623
845
|
deadlineMs,
|
|
624
846
|
...(timedOut
|
|
625
|
-
? {
|
|
847
|
+
? {
|
|
848
|
+
reason: `command exceeded the ${deadlineMs} ms execution deadline`,
|
|
849
|
+
}
|
|
626
850
|
: !hasTerminalExit
|
|
627
851
|
? { reason: "machine returned no terminal exit code" }
|
|
628
852
|
: {}),
|
|
@@ -630,9 +854,17 @@ export async function executeRunOnSelfhostedMachine(
|
|
|
630
854
|
}
|
|
631
855
|
if (op.kind === "read") {
|
|
632
856
|
const bytes = await session.readFile({ path: op.path });
|
|
633
|
-
return {
|
|
857
|
+
return {
|
|
858
|
+
target,
|
|
859
|
+
kind: "read",
|
|
860
|
+
ok: true,
|
|
861
|
+
content: new TextDecoder().decode(bytes),
|
|
862
|
+
};
|
|
634
863
|
}
|
|
635
|
-
const bytesWritten = await session.writeFile({
|
|
864
|
+
const bytesWritten = await session.writeFile({
|
|
865
|
+
path: op.path,
|
|
866
|
+
content: op.content,
|
|
867
|
+
});
|
|
636
868
|
return { target, kind: "write", ok: true, bytesWritten };
|
|
637
869
|
} catch (error) {
|
|
638
870
|
const reason = error instanceof Error ? error.message : String(error);
|
|
@@ -667,8 +899,19 @@ export async function runOnSandbox(
|
|
|
667
899
|
ctx: FleetContext,
|
|
668
900
|
target: string,
|
|
669
901
|
op: RunOnOp,
|
|
902
|
+
options: RunOnOptions = {},
|
|
670
903
|
): Promise<RunOnResult> {
|
|
671
|
-
const sandbox = await getSandbox(
|
|
904
|
+
const sandbox = await getSandbox(
|
|
905
|
+
services.db,
|
|
906
|
+
ctx.subjectId
|
|
907
|
+
? {
|
|
908
|
+
accountId: ctx.accountId,
|
|
909
|
+
workspaceId: ctx.workspaceId,
|
|
910
|
+
subjectId: ctx.subjectId,
|
|
911
|
+
}
|
|
912
|
+
: ctx.workspaceId,
|
|
913
|
+
target,
|
|
914
|
+
);
|
|
672
915
|
if (!sandbox) {
|
|
673
916
|
return {
|
|
674
917
|
target,
|
|
@@ -686,8 +929,59 @@ export async function runOnSandbox(
|
|
|
686
929
|
reason: `run_on routes one-off ops to enrolled selfhosted machines; ${sandbox.kind} targets are reached via the active sandbox (swap to it first)`,
|
|
687
930
|
};
|
|
688
931
|
}
|
|
689
|
-
|
|
690
|
-
|
|
932
|
+
if (sandbox.scope === "user") {
|
|
933
|
+
if (!ctx.subjectId || !ctx.attemptAuthority) {
|
|
934
|
+
return {
|
|
935
|
+
target,
|
|
936
|
+
targetName: sandbox.name,
|
|
937
|
+
kind: op.kind,
|
|
938
|
+
ok: false,
|
|
939
|
+
reason: "personal Connected Machine use requires an exact admitted agent attempt",
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
try {
|
|
943
|
+
const authorized = await authorizePersonalMachineForAttempt(services.db, {
|
|
944
|
+
accountId: ctx.accountId,
|
|
945
|
+
workspaceId: ctx.workspaceId,
|
|
946
|
+
subjectId: ctx.subjectId,
|
|
947
|
+
sessionId: ctx.sessionId,
|
|
948
|
+
turnId: ctx.attemptAuthority.turnId,
|
|
949
|
+
attemptId: ctx.attemptAuthority.attemptId,
|
|
950
|
+
executionGeneration: ctx.attemptAuthority.executionGeneration,
|
|
951
|
+
enrollmentId: sandbox.enrollmentId,
|
|
952
|
+
requireActiveSandbox: false,
|
|
953
|
+
});
|
|
954
|
+
if (!authorized) {
|
|
955
|
+
return {
|
|
956
|
+
target,
|
|
957
|
+
targetName: sandbox.name,
|
|
958
|
+
kind: op.kind,
|
|
959
|
+
ok: false,
|
|
960
|
+
reason: "personal Connected Machine authority was not admitted for this attempt",
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
} catch {
|
|
964
|
+
return {
|
|
965
|
+
target,
|
|
966
|
+
targetName: sandbox.name,
|
|
967
|
+
kind: op.kind,
|
|
968
|
+
ok: false,
|
|
969
|
+
reason: "personal Connected Machine authority is not live for this attempt",
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
const enrollment = await getLiveEnrollmentConnection(
|
|
974
|
+
services.db,
|
|
975
|
+
ctx.subjectId
|
|
976
|
+
? {
|
|
977
|
+
accountId: ctx.accountId,
|
|
978
|
+
workspaceId: ctx.workspaceId,
|
|
979
|
+
subjectId: ctx.subjectId,
|
|
980
|
+
}
|
|
981
|
+
: sandbox.workspaceId,
|
|
982
|
+
sandbox.enrollmentId,
|
|
983
|
+
);
|
|
984
|
+
if (!enrollment || enrollment.status !== "active" || !enrollment.connectionInstanceId) {
|
|
691
985
|
return {
|
|
692
986
|
target,
|
|
693
987
|
targetName: sandbox.name,
|
|
@@ -696,15 +990,65 @@ export async function runOnSandbox(
|
|
|
696
990
|
reason: `sandbox ${target} is not enrolled/active`,
|
|
697
991
|
};
|
|
698
992
|
}
|
|
993
|
+
if (!enrollment.workspaceRoot) {
|
|
994
|
+
return {
|
|
995
|
+
target,
|
|
996
|
+
targetName: sandbox.name,
|
|
997
|
+
kind: op.kind,
|
|
998
|
+
ok: false,
|
|
999
|
+
reason: `sandbox ${target} has not reported an absolute workspace root; reconnect it with a current agent`,
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
const resolveOperationAdmission = async (): Promise<SelfhostedOperationAdmission | null> => {
|
|
1003
|
+
try {
|
|
1004
|
+
const current =
|
|
1005
|
+
sandbox.scope === "user" && ctx.subjectId && ctx.attemptAuthority
|
|
1006
|
+
? await resolvePersonalMachineConnectionForAttempt(services.db, {
|
|
1007
|
+
accountId: ctx.accountId,
|
|
1008
|
+
workspaceId: ctx.workspaceId,
|
|
1009
|
+
subjectId: ctx.subjectId,
|
|
1010
|
+
sessionId: ctx.sessionId,
|
|
1011
|
+
turnId: ctx.attemptAuthority.turnId,
|
|
1012
|
+
attemptId: ctx.attemptAuthority.attemptId,
|
|
1013
|
+
executionGeneration: ctx.attemptAuthority.executionGeneration,
|
|
1014
|
+
enrollmentId: sandbox.enrollmentId!,
|
|
1015
|
+
requireActiveSandbox: false,
|
|
1016
|
+
})
|
|
1017
|
+
: await getLiveEnrollmentConnection(
|
|
1018
|
+
services.db,
|
|
1019
|
+
ctx.subjectId
|
|
1020
|
+
? {
|
|
1021
|
+
accountId: ctx.accountId,
|
|
1022
|
+
workspaceId: ctx.workspaceId,
|
|
1023
|
+
subjectId: ctx.subjectId,
|
|
1024
|
+
}
|
|
1025
|
+
: sandbox.workspaceId,
|
|
1026
|
+
sandbox.enrollmentId!,
|
|
1027
|
+
);
|
|
1028
|
+
return runOnOperationAdmission(services, current);
|
|
1029
|
+
} catch {
|
|
1030
|
+
return null;
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
699
1033
|
|
|
700
1034
|
const result = await executeRunOnSelfhostedMachine(
|
|
701
1035
|
{
|
|
702
|
-
workspaceId:
|
|
1036
|
+
workspaceId: sandbox.workspaceId,
|
|
703
1037
|
agentId: sandbox.enrollmentId,
|
|
1038
|
+
connectionInstanceId: enrollment.connectionInstanceId,
|
|
1039
|
+
workspaceRoot: enrollment.workspaceRoot,
|
|
704
1040
|
controlRpc: controlRpc(services.bus),
|
|
705
1041
|
relay: relayConfigFromSettings(services.settings),
|
|
706
1042
|
controlTimeoutMs: services.settings.sandboxSelfhostedControlTimeoutMs,
|
|
707
1043
|
execTimeoutMs: services.settings.sandboxSelfhostedExecTimeoutMs,
|
|
1044
|
+
operationResourcePolicy: enrollment.operationPolicy,
|
|
1045
|
+
operationResourcePolicySupported:
|
|
1046
|
+
enrollment.agentCapabilities.operationResourcePolicy === true,
|
|
1047
|
+
operationCpuQuotaSupported: enrollment.agentCapabilities.operationCpuQuota === true,
|
|
1048
|
+
resolveOperationAdmission,
|
|
1049
|
+
...(options.transientExecEnvironment !== undefined
|
|
1050
|
+
? { transientExecEnvironment: options.transientExecEnvironment }
|
|
1051
|
+
: {}),
|
|
708
1052
|
...(services.settings.agentOpStreamEnabled === true &&
|
|
709
1053
|
enrollment.opStream === true &&
|
|
710
1054
|
services.bus?.getOpStreamConnection
|
|
@@ -749,17 +1093,19 @@ export async function provisionSandbox(
|
|
|
749
1093
|
input: { kind: "selfhosted" | "modal"; name?: string },
|
|
750
1094
|
): Promise<ProvisionResult> {
|
|
751
1095
|
if (input.kind === "selfhosted") {
|
|
752
|
-
const base = (services.settings.publicBaseUrl ?? "https://
|
|
1096
|
+
const base = (services.settings.publicBaseUrl ?? "https://app.opengeni.ai").replace(/\/+$/, "");
|
|
1097
|
+
const unixEnvironment = `OPENGENI_API_URL=${base} OPENGENI_WORKSPACE_ID=${ctx.workspaceId}`;
|
|
1098
|
+
const windowsEnvironment = `$env:OPENGENI_API_URL='${base}'; $env:OPENGENI_WORKSPACE_ID='${ctx.workspaceId}';`;
|
|
753
1099
|
return {
|
|
754
1100
|
kind: "selfhosted",
|
|
755
1101
|
instructions:
|
|
756
|
-
"Share these
|
|
1102
|
+
"Share one of these deployment-specific commands with a human operator. It installs the OpenGeni agent and starts `opengeni-agent connect` for this exact deployment and workspace; do not run a second bare `connect`. Complete the device-flow at the verification URL (the loud whole-machine + screen-control consent), and the machine then appears here as an attachable selfhosted sandbox. Existing connections to other OpenGeni workspaces or deployments are preserved.",
|
|
757
1103
|
// Install from THIS control plane's origin (not a hardcoded public CDN): the
|
|
758
1104
|
// served install script is rewritten to pull the per-SHA agent baked into
|
|
759
1105
|
// this exact deployment (see apps/api/src/routes/install.ts), so a deployed
|
|
760
1106
|
// env is self-contained and a private/air-gapped one works with no public DNS.
|
|
761
|
-
installCommandUnix: `curl -fsSL ${base}/install.sh | sh`,
|
|
762
|
-
installCommandWindows:
|
|
1107
|
+
installCommandUnix: `curl -fsSL ${base}/install.sh | ${unixEnvironment} sh`,
|
|
1108
|
+
installCommandWindows: `${windowsEnvironment} irm ${base}/install.ps1 | iex`,
|
|
763
1109
|
verificationUri: `${base}/device`,
|
|
764
1110
|
note: "Whole-machine access requires explicit human consent in the device-flow web page; the agent cannot self-consent.",
|
|
765
1111
|
};
|