@opengeni/core 0.21.2 → 0.21.10
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 +8 -0
- package/dist/application/session-commands.d.ts +7 -2
- package/dist/domain/capabilities.d.ts +7 -0
- package/dist/domain/memory-slack-publication.d.ts +0 -2
- package/dist/domain/scheduled-tasks.d.ts +25 -1
- package/dist/domain/sessions.d.ts +30 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +451 -102
- package/dist/index.js.map +1 -1
- package/dist/managed-session.d.ts +53 -0
- package/dist/sandbox/fleet.d.ts +9 -1
- package/dist/session-authorization.d.ts +9 -0
- package/package.json +10 -10
- package/src/access/index.ts +43 -3
- package/src/application/session-commands.ts +29 -9
- package/src/domain/capabilities.ts +99 -29
- package/src/domain/memory-slack-publication.ts +5 -17
- package/src/domain/resources.ts +10 -20
- package/src/domain/scheduled-tasks.ts +221 -7
- package/src/domain/sessions.ts +127 -37
- package/src/index.ts +1 -0
- package/src/managed-auth-type.ts +4 -3
- package/src/managed-session.ts +36 -0
- package/src/sandbox/fleet.ts +56 -1
- package/src/sandbox/routing.ts +26 -0
- package/src/session-authorization.ts +17 -0
package/src/sandbox/fleet.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
import type { EventBus } from "@opengeni/events";
|
|
28
28
|
import {
|
|
29
29
|
NatsControlRpc,
|
|
30
|
+
NatsOpStreamTransport,
|
|
30
31
|
selfhostedLiveness,
|
|
31
32
|
SelfhostedSession,
|
|
32
33
|
swapTargetEstablishability,
|
|
@@ -34,6 +35,7 @@ import {
|
|
|
34
35
|
type ControlRpc,
|
|
35
36
|
type NatsRequestConnection,
|
|
36
37
|
type SelfhostedRelayConfig,
|
|
38
|
+
type SelfhostedOpStreamDeps,
|
|
37
39
|
} from "@opengeni/runtime/sandbox";
|
|
38
40
|
import { HTTPException } from "hono/http-exception";
|
|
39
41
|
import { relayConfigFromSettings } from "./routing";
|
|
@@ -95,6 +97,7 @@ export async function buildFleetContextForSession(
|
|
|
95
97
|
|
|
96
98
|
/** The dominant liveness of a fleet member, surfaced to the dock + the agent. */
|
|
97
99
|
export type FleetLiveness = "online" | "reconnecting" | "offline";
|
|
100
|
+
export type FleetOperationAvailability = "ready" | "wakeable" | "recovering" | "unavailable";
|
|
98
101
|
|
|
99
102
|
/**
|
|
100
103
|
* A fleet member as the agent + the dock see it (the M8b/M9 UI seam — the
|
|
@@ -117,6 +120,11 @@ export type FleetSandboxEntry = {
|
|
|
117
120
|
enrollmentId: string | null;
|
|
118
121
|
/** Whether this target can be attached/swapped to right now (live + addressable). */
|
|
119
122
|
attachable: boolean;
|
|
123
|
+
/** Whether an ordinary shell/files operation can use this target. This is
|
|
124
|
+
* deliberately separate from `attachable`: an idle managed home sandbox can
|
|
125
|
+
* be wakeable even while its holderless lease is cold/draining and therefore
|
|
126
|
+
* not an already-live swap target. */
|
|
127
|
+
operationAvailability: FleetOperationAvailability;
|
|
120
128
|
/** Selfhosted only: whether whole-machine + screen-control consent is acked. */
|
|
121
129
|
consented?: boolean;
|
|
122
130
|
/** Selfhosted only: whether a display (real/Xvfb) is present. */
|
|
@@ -254,6 +262,22 @@ export async function listFleet(
|
|
|
254
262
|
groupLease.recovery.restore.status === "restoring" ||
|
|
255
263
|
groupLease.recovery.restore.status === "verifying"),
|
|
256
264
|
);
|
|
265
|
+
const groupRecoveryUnavailable = Boolean(
|
|
266
|
+
groupLease &&
|
|
267
|
+
(groupLease.recovery.restore.status === "degraded" ||
|
|
268
|
+
groupLease.recovery.restore.status === "unrecoverable" ||
|
|
269
|
+
groupLease.recovery.workspace.status === "degraded" ||
|
|
270
|
+
groupLease.recovery.workspace.status === "unrecoverable"),
|
|
271
|
+
);
|
|
272
|
+
const groupOperationAvailability: FleetOperationAvailability = groupOnline
|
|
273
|
+
? "ready"
|
|
274
|
+
: groupRecoveryUnavailable
|
|
275
|
+
? "unavailable"
|
|
276
|
+
: groupRecovering
|
|
277
|
+
? "recovering"
|
|
278
|
+
: ctx.sessionBackend === "selfhosted"
|
|
279
|
+
? "unavailable"
|
|
280
|
+
: "wakeable";
|
|
257
281
|
entries.push({
|
|
258
282
|
id: ctx.sessionGroupId,
|
|
259
283
|
kind: ctx.sessionBackend === "selfhosted" ? "selfhosted" : "modal",
|
|
@@ -263,6 +287,7 @@ export async function listFleet(
|
|
|
263
287
|
isSessionGroup: true,
|
|
264
288
|
enrollmentId: null,
|
|
265
289
|
attachable: groupOnline,
|
|
290
|
+
operationAvailability: groupOperationAvailability,
|
|
266
291
|
providerStatus: groupLease?.recovery.provider.status ?? "not_created",
|
|
267
292
|
leaseLiveness: groupLease?.liveness ?? null,
|
|
268
293
|
routeStatus: groupActive ? "attached" : "detached",
|
|
@@ -284,6 +309,11 @@ export async function listFleet(
|
|
|
284
309
|
continue;
|
|
285
310
|
}
|
|
286
311
|
const enrollment = await getEnrollment(db, ctx.workspaceId, sandbox.enrollmentId);
|
|
312
|
+
if (!enrollment || enrollment.status !== "active") {
|
|
313
|
+
// Revoked enrollments remain durable audit/history records, but they are
|
|
314
|
+
// intentionally absent from the normal attach/run picker.
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
287
317
|
const probe = enrollment
|
|
288
318
|
? await probeEnrollment(services, ctx.workspaceId, enrollment)
|
|
289
319
|
: { liveness: "offline" as FleetLiveness, consented: false, hasDisplay: false };
|
|
@@ -296,6 +326,12 @@ export async function listFleet(
|
|
|
296
326
|
isSessionGroup: false,
|
|
297
327
|
enrollmentId: sandbox.enrollmentId,
|
|
298
328
|
attachable: probe.liveness === "online",
|
|
329
|
+
operationAvailability:
|
|
330
|
+
probe.liveness === "online"
|
|
331
|
+
? "ready"
|
|
332
|
+
: probe.liveness === "reconnecting"
|
|
333
|
+
? "recovering"
|
|
334
|
+
: "unavailable",
|
|
299
335
|
consented: probe.consented,
|
|
300
336
|
hasDisplay: probe.hasDisplay,
|
|
301
337
|
lastSeenAt: enrollment?.lastSeenAt ?? null,
|
|
@@ -533,6 +569,8 @@ export type RunOnSelfhostedMachine = {
|
|
|
533
569
|
controlTimeoutMs: number;
|
|
534
570
|
/** Longer agent-side process deadline for exec. */
|
|
535
571
|
execTimeoutMs: number;
|
|
572
|
+
/** Streaming transport required when execTimeoutMs is 0 (unbounded). */
|
|
573
|
+
opStream?: SelfhostedOpStreamDeps;
|
|
536
574
|
};
|
|
537
575
|
|
|
538
576
|
/**
|
|
@@ -553,6 +591,7 @@ export async function executeRunOnSelfhostedMachine(
|
|
|
553
591
|
relay: machine.relay,
|
|
554
592
|
timeoutMs: machine.controlTimeoutMs,
|
|
555
593
|
execTimeoutMs: machine.execTimeoutMs,
|
|
594
|
+
...(machine.opStream !== undefined ? { opStream: machine.opStream } : {}),
|
|
556
595
|
});
|
|
557
596
|
|
|
558
597
|
try {
|
|
@@ -600,6 +639,11 @@ export async function executeRunOnSelfhostedMachine(
|
|
|
600
639
|
// so leave `timedOut` absent while still reporting the enforced deadline.
|
|
601
640
|
...(op.kind === "exec" ? { deadlineMs: session.effectiveExecDeadlineMs } : {}),
|
|
602
641
|
};
|
|
642
|
+
} finally {
|
|
643
|
+
// This one-off has no turn journal. Once its result has been accepted by
|
|
644
|
+
// this call, final-ack any settled stream so the runner can immediately
|
|
645
|
+
// release replay/output retention instead of waiting for TTL cleanup.
|
|
646
|
+
await session.finalizeOpStreamOps().catch(() => undefined);
|
|
603
647
|
}
|
|
604
648
|
}
|
|
605
649
|
|
|
@@ -655,6 +699,17 @@ export async function runOnSandbox(
|
|
|
655
699
|
relay: relayConfigFromSettings(services.settings),
|
|
656
700
|
controlTimeoutMs: services.settings.sandboxSelfhostedControlTimeoutMs,
|
|
657
701
|
execTimeoutMs: services.settings.sandboxSelfhostedExecTimeoutMs,
|
|
702
|
+
...(services.settings.agentOpStreamEnabled === true &&
|
|
703
|
+
enrollment.opStream === true &&
|
|
704
|
+
services.bus?.getOpStreamConnection
|
|
705
|
+
? {
|
|
706
|
+
opStream: {
|
|
707
|
+
transport: new NatsOpStreamTransport(
|
|
708
|
+
async () => services.bus?.getOpStreamConnection?.() ?? null,
|
|
709
|
+
),
|
|
710
|
+
},
|
|
711
|
+
}
|
|
712
|
+
: {}),
|
|
658
713
|
},
|
|
659
714
|
target,
|
|
660
715
|
op,
|
|
@@ -692,7 +747,7 @@ export async function provisionSandbox(
|
|
|
692
747
|
return {
|
|
693
748
|
kind: "selfhosted",
|
|
694
749
|
instructions:
|
|
695
|
-
"Share these instructions with a human operator. They install the OpenGeni agent on the machine, run `opengeni-agent
|
|
750
|
+
"Share these instructions with a human operator. They install the OpenGeni agent on the machine, run `opengeni-agent 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.",
|
|
696
751
|
// Install from THIS control plane's origin (not a hardcoded public CDN): the
|
|
697
752
|
// served install script is rewritten to pull the per-SHA agent baked into
|
|
698
753
|
// this exact deployment (see apps/api/src/routes/install.ts), so a deployed
|
package/src/sandbox/routing.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { sandboxArchiveCaptureTimeoutMs, type Settings } from "@opengeni/config"
|
|
|
15
15
|
import {
|
|
16
16
|
advanceWorkspaceGenerationForDirectRequest,
|
|
17
17
|
advanceWorkspaceGenerationForRetainedProcess,
|
|
18
|
+
getEnrollment,
|
|
18
19
|
getRetainedProcess,
|
|
19
20
|
getSandbox,
|
|
20
21
|
markWarmLeaseInstanceLost,
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
isProviderSandboxGoneDuringRoutedOperation,
|
|
33
34
|
makeActiveBackendResolver,
|
|
34
35
|
NatsControlRpc,
|
|
36
|
+
NatsOpStreamTransport,
|
|
35
37
|
RoutingSandboxSession,
|
|
36
38
|
resolveModalCheckpointProviderBindingForSession,
|
|
37
39
|
type ControlRpc,
|
|
@@ -44,6 +46,7 @@ import {
|
|
|
44
46
|
type RoutingRetainedProcessTerminalProof,
|
|
45
47
|
type RoutingSandboxOperationObserver,
|
|
46
48
|
type SelfhostedRelayConfig,
|
|
49
|
+
type SelfhostedOpStreamDeps,
|
|
47
50
|
} from "@opengeni/runtime/sandbox";
|
|
48
51
|
|
|
49
52
|
type PersistableMutationAdmission = {
|
|
@@ -114,6 +117,27 @@ function controlRpcFactory(bus: EventBus | undefined): () => ControlRpc {
|
|
|
114
117
|
});
|
|
115
118
|
}
|
|
116
119
|
|
|
120
|
+
async function resolveSelfhostedOpStream(
|
|
121
|
+
services: ChannelARoutingServices,
|
|
122
|
+
workspaceId: string,
|
|
123
|
+
sandbox: RoutableSandbox,
|
|
124
|
+
): Promise<SelfhostedOpStreamDeps | undefined> {
|
|
125
|
+
if (
|
|
126
|
+
services.settings.agentOpStreamEnabled !== true ||
|
|
127
|
+
!services.bus?.getOpStreamConnection ||
|
|
128
|
+
!sandbox.enrollmentId
|
|
129
|
+
) {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
const enrollment = await getEnrollment(services.db, workspaceId, sandbox.enrollmentId);
|
|
133
|
+
if (enrollment?.opStream !== true) return undefined;
|
|
134
|
+
return {
|
|
135
|
+
transport: new NatsOpStreamTransport(
|
|
136
|
+
async () => services.bus?.getOpStreamConnection?.() ?? null,
|
|
137
|
+
),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
117
141
|
/** Whether the routing proxy should wrap the Channel-A box: gated by the
|
|
118
142
|
* selfhosted flag (the active pointer + swap are only meaningful then). */
|
|
119
143
|
export function routingEnabled(settings: Settings): boolean {
|
|
@@ -389,6 +413,8 @@ export function wrapChannelABoxWithRouting(
|
|
|
389
413
|
: null;
|
|
390
414
|
},
|
|
391
415
|
controlRpcFactory: controlRpcFactory(bus),
|
|
416
|
+
resolveSelfhostedOpStream: (sandbox) =>
|
|
417
|
+
resolveSelfhostedOpStream(services, ids.workspaceId, sandbox),
|
|
392
418
|
relay: relayConfigFromSettings(settings),
|
|
393
419
|
selfhostedTimeoutMs: settings.sandboxSelfhostedControlTimeoutMs,
|
|
394
420
|
selfhostedExecTimeoutMs: settings.sandboxSelfhostedExecTimeoutMs,
|
|
@@ -46,6 +46,23 @@ export type ResolvedSessionAuthorization = {
|
|
|
46
46
|
reauthorizeAfterMs: number | null;
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Prove that a first-party request belongs to the exact currently active
|
|
51
|
+
* attempt of the named caller session. Unlike the optional embedding-host ACL
|
|
52
|
+
* port, this database fence is mandatory for high-trust operations.
|
|
53
|
+
*/
|
|
54
|
+
export async function requireLiveAgentAttemptAuthorization(
|
|
55
|
+
db: Database,
|
|
56
|
+
grant: AccessGrant,
|
|
57
|
+
callerSessionId: string,
|
|
58
|
+
): Promise<Extract<SessionAuthorizationActor, { kind: "agent_attempt" }>> {
|
|
59
|
+
const actor = await resolveSessionAuthorizationActor(db, grant);
|
|
60
|
+
if (actor.kind !== "agent_attempt" || actor.callerSessionId !== callerSessionId) {
|
|
61
|
+
throw new SessionAuthorizationDeniedError("caller_stale");
|
|
62
|
+
}
|
|
63
|
+
return actor;
|
|
64
|
+
}
|
|
65
|
+
|
|
49
66
|
/**
|
|
50
67
|
* Resolve and enforce the host ACL for one session. The target and agent actor
|
|
51
68
|
* are reconstructed from workspace-scoped durable state. A request can supply
|