@opengeni/core 0.21.2 → 0.24.1
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 +9 -1
- package/dist/application/new-session-drafts.d.ts +1 -1
- package/dist/application/session-commands.d.ts +9 -4
- package/dist/billing/limits.d.ts +1 -1
- package/dist/chunk-6WKPWE5S.js +2380 -0
- package/dist/chunk-6WKPWE5S.js.map +1 -0
- package/dist/chunk-JJU6I5XD.js +2713 -0
- package/dist/chunk-JJU6I5XD.js.map +1 -0
- package/dist/chunk-NYPMQ7NO.js +2270 -0
- package/dist/chunk-NYPMQ7NO.js.map +1 -0
- package/dist/dependencies.d.ts +26 -6
- package/dist/domain/capabilities.d.ts +35 -3
- package/dist/domain/company-profile-durable-learning-adapter.d.ts +39 -0
- package/dist/domain/durable-learning-slack-publication.d.ts +72 -0
- package/dist/domain/editable-artifacts/agent-application.d.ts +192 -0
- package/dist/domain/editable-artifacts/durable-export-postgres.d.ts +4 -0
- package/dist/domain/editable-artifacts/durable-export.d.ts +212 -0
- package/dist/domain/editable-artifacts/errors.d.ts +47 -0
- package/dist/domain/editable-artifacts/hash.d.ts +11 -0
- package/dist/domain/editable-artifacts/in-memory.d.ts +99 -0
- package/dist/domain/editable-artifacts/index.d.ts +14 -0
- package/dist/domain/editable-artifacts/ports.d.ts +341 -0
- package/dist/domain/editable-artifacts/postgres-adapters.d.ts +13 -0
- package/dist/domain/editable-artifacts/service.d.ts +105 -0
- package/dist/domain/editable-artifacts/snapshot-compaction.d.ts +44 -0
- package/dist/domain/editable-artifacts/snapshot-verifier-genesis.d.ts +50 -0
- package/dist/domain/editable-artifacts/snapshot-verifier-materialization.d.ts +21 -0
- package/dist/domain/editable-artifacts/snapshot-verifier.d.ts +118 -0
- package/dist/domain/editable-artifacts/types.d.ts +410 -0
- package/dist/domain/memory-slack-delivery.d.ts +19 -0
- package/dist/domain/memory-slack-publication.d.ts +0 -2
- package/dist/domain/packs.d.ts +29 -1
- package/dist/domain/personal-connection-delegations.d.ts +9 -1
- package/dist/domain/scheduled-tasks.d.ts +27 -3
- package/dist/domain/session-tool-policy.d.ts +2 -2
- package/dist/domain/sessions.d.ts +44 -10
- package/dist/domain/skill-imports.d.ts +32 -0
- package/dist/domain/slack-publication-secret-safety.d.ts +8 -0
- package/dist/domain/timeline-annotations.d.ts +5 -0
- package/dist/domain/video-generation-capabilities.d.ts +9 -0
- package/dist/domain/video-generation.d.ts +47 -0
- package/dist/editable-artifact-live/application.d.ts +69 -0
- package/dist/editable-artifact-live/errors.d.ts +1 -0
- package/dist/editable-artifact-live/index.d.ts +8 -0
- package/dist/editable-artifact-live/outbox-dispatcher.d.ts +162 -0
- package/dist/editable-artifact-live/ports.d.ts +100 -0
- package/dist/editable-artifact-live/server.d.ts +71 -0
- package/dist/editable-artifact-live/ticket.d.ts +40 -0
- package/dist/editable-artifact-live/types.d.ts +213 -0
- package/dist/editable-artifact-live/wire.d.ts +34 -0
- package/dist/editable-artifact-live.d.ts +2 -0
- package/dist/editable-artifact-live.js +58 -0
- package/dist/editable-artifact-live.js.map +1 -0
- package/dist/editable-artifacts.d.ts +2 -0
- package/dist/editable-artifacts.js +179 -0
- package/dist/editable-artifacts.js.map +1 -0
- package/dist/index.d.ts +35 -25
- package/dist/index.js +3184 -652
- package/dist/index.js.map +1 -1
- package/dist/managed-session.d.ts +53 -0
- package/dist/rigs/index.d.ts +1 -1
- package/dist/rigs/provider-images.d.ts +31 -0
- package/dist/sandbox/fleet.d.ts +9 -1
- package/dist/sandbox/routing.d.ts +1 -0
- package/dist/session-authorization.d.ts +10 -1
- package/dist/workflow-wake-contract.d.ts +7 -0
- package/package.json +18 -10
- package/src/access/index.ts +43 -3
- package/src/application/new-session-drafts.ts +2 -0
- package/src/application/session-commands.ts +217 -116
- package/src/dependencies.ts +30 -2
- package/src/domain/capabilities.ts +672 -218
- package/src/domain/company-profile-durable-learning-adapter.ts +221 -0
- package/src/domain/durable-learning-slack-publication.ts +202 -0
- package/src/domain/editable-artifacts/agent-application.ts +687 -0
- package/src/domain/editable-artifacts/durable-export-postgres.ts +139 -0
- package/src/domain/editable-artifacts/durable-export.ts +734 -0
- package/src/domain/editable-artifacts/errors.ts +115 -0
- package/src/domain/editable-artifacts/hash.ts +131 -0
- package/src/domain/editable-artifacts/in-memory.ts +1662 -0
- package/src/domain/editable-artifacts/index.ts +14 -0
- package/src/domain/editable-artifacts/ports.ts +462 -0
- package/src/domain/editable-artifacts/postgres-adapters.ts +49 -0
- package/src/domain/editable-artifacts/service.ts +2402 -0
- package/src/domain/editable-artifacts/snapshot-compaction.ts +246 -0
- package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +235 -0
- package/src/domain/editable-artifacts/snapshot-verifier-materialization.ts +74 -0
- package/src/domain/editable-artifacts/snapshot-verifier.ts +687 -0
- package/src/domain/editable-artifacts/types.ts +835 -0
- package/src/domain/insights.ts +136 -33
- package/src/domain/memory-slack-delivery.ts +204 -0
- package/src/domain/memory-slack-publication.ts +10 -19
- package/src/domain/packs.ts +266 -5
- package/src/domain/personal-connection-delegations.ts +97 -6
- package/src/domain/resources.ts +10 -20
- package/src/domain/scheduled-tasks.ts +365 -25
- package/src/domain/session-tool-policy.ts +8 -2
- package/src/domain/sessions.ts +388 -155
- package/src/domain/skill-imports.ts +350 -0
- package/src/domain/slack-publication-secret-safety.ts +42 -0
- package/src/domain/timeline-annotations.ts +204 -0
- package/src/domain/video-generation-capabilities.ts +69 -0
- package/src/domain/video-generation.ts +249 -0
- package/src/editable-artifact-live/application.ts +182 -0
- package/src/editable-artifact-live/errors.ts +4 -0
- package/src/editable-artifact-live/index.ts +8 -0
- package/src/editable-artifact-live/outbox-dispatcher.ts +916 -0
- package/src/editable-artifact-live/ports.ts +139 -0
- package/src/editable-artifact-live/server.ts +1707 -0
- package/src/editable-artifact-live/ticket.ts +217 -0
- package/src/editable-artifact-live/types.ts +286 -0
- package/src/editable-artifact-live/wire.ts +95 -0
- package/src/editable-artifact-live.ts +2 -0
- package/src/editable-artifacts.ts +2 -0
- package/src/index.ts +14 -0
- package/src/managed-auth-type.ts +4 -3
- package/src/managed-session.ts +36 -0
- package/src/rigs/index.ts +26 -19
- package/src/rigs/provider-images.ts +110 -0
- package/src/sandbox/fleet.ts +56 -1
- package/src/sandbox/routing.ts +32 -3
- package/src/session-authorization.ts +20 -0
- package/src/workflow-wake-contract.ts +8 -0
package/src/rigs/index.ts
CHANGED
|
@@ -36,6 +36,9 @@ import {
|
|
|
36
36
|
type Database,
|
|
37
37
|
} from "@opengeni/db";
|
|
38
38
|
import { HTTPException } from "hono/http-exception";
|
|
39
|
+
import { rigProviderImagesFromVerification } from "./provider-images";
|
|
40
|
+
|
|
41
|
+
export * from "./provider-images";
|
|
39
42
|
|
|
40
43
|
export const MAX_RIGS_PER_WORKSPACE = 50;
|
|
41
44
|
export const MAX_CHECKS_PER_RIG = 100;
|
|
@@ -352,6 +355,13 @@ export async function promoteSetupAppendChange(
|
|
|
352
355
|
if (typeof payload.command !== "string" || !payload.command.trim()) {
|
|
353
356
|
throw new HTTPException(422, { message: "setup_append change is missing command" });
|
|
354
357
|
}
|
|
358
|
+
const nextDefinition = {
|
|
359
|
+
image: base.image,
|
|
360
|
+
setupScript: appendRigSetupCommand(base.setupScript, payload.command),
|
|
361
|
+
checks: base.checks,
|
|
362
|
+
credentialHooks: base.credentialHooks,
|
|
363
|
+
defaultVariableSetIds: base.defaultVariableSetIds,
|
|
364
|
+
};
|
|
355
365
|
const { version, change: updated } = await promoteChangeWithActiveCas(
|
|
356
366
|
deps,
|
|
357
367
|
grant.workspaceId,
|
|
@@ -359,16 +369,13 @@ export async function promoteSetupAppendChange(
|
|
|
359
369
|
change.id,
|
|
360
370
|
{
|
|
361
371
|
expectedActiveVersionId: change.baseVersionId,
|
|
362
|
-
|
|
363
|
-
setupScript: appendRigSetupCommand(base.setupScript, payload.command),
|
|
364
|
-
checks: base.checks,
|
|
365
|
-
credentialHooks: base.credentialHooks,
|
|
366
|
-
defaultVariableSetIds: base.defaultVariableSetIds,
|
|
372
|
+
...nextDefinition,
|
|
367
373
|
changelog:
|
|
368
374
|
typeof payload.note === "string" && payload.note.trim()
|
|
369
375
|
? payload.note
|
|
370
376
|
: "Verified setup append",
|
|
371
377
|
createdBy: change.proposedBy ?? rigActorForGrant(grant),
|
|
378
|
+
providerImages: rigProviderImagesFromVerification(change.verification, nextDefinition),
|
|
372
379
|
},
|
|
373
380
|
);
|
|
374
381
|
await recordRigAuditEvent(deps.db, {
|
|
@@ -418,6 +425,18 @@ export async function promoteVerifiedDefinitionEditChangeForApi(
|
|
|
418
425
|
defaultVariableSetIds?: unknown;
|
|
419
426
|
changelog?: unknown;
|
|
420
427
|
};
|
|
428
|
+
const nextDefinition = {
|
|
429
|
+
image: payload.image === undefined ? base.image : (payload.image as string | null),
|
|
430
|
+
setupScript:
|
|
431
|
+
payload.setupScript === undefined ? base.setupScript : (payload.setupScript as string | null),
|
|
432
|
+
checks: Array.isArray(payload.checks) ? (payload.checks as RigVersion["checks"]) : base.checks,
|
|
433
|
+
credentialHooks: Array.isArray(payload.credentialHooks)
|
|
434
|
+
? (payload.credentialHooks as string[])
|
|
435
|
+
: base.credentialHooks,
|
|
436
|
+
defaultVariableSetIds: Array.isArray(payload.defaultVariableSetIds)
|
|
437
|
+
? (payload.defaultVariableSetIds as string[])
|
|
438
|
+
: base.defaultVariableSetIds,
|
|
439
|
+
};
|
|
421
440
|
const { version, change: updated } = await promoteChangeWithActiveCas(
|
|
422
441
|
deps,
|
|
423
442
|
grant.workspaceId,
|
|
@@ -425,25 +444,13 @@ export async function promoteVerifiedDefinitionEditChangeForApi(
|
|
|
425
444
|
change.id,
|
|
426
445
|
{
|
|
427
446
|
expectedActiveVersionId: change.baseVersionId,
|
|
428
|
-
|
|
429
|
-
setupScript:
|
|
430
|
-
payload.setupScript === undefined
|
|
431
|
-
? base.setupScript
|
|
432
|
-
: (payload.setupScript as string | null),
|
|
433
|
-
checks: Array.isArray(payload.checks)
|
|
434
|
-
? (payload.checks as RigVersion["checks"])
|
|
435
|
-
: base.checks,
|
|
436
|
-
credentialHooks: Array.isArray(payload.credentialHooks)
|
|
437
|
-
? (payload.credentialHooks as string[])
|
|
438
|
-
: base.credentialHooks,
|
|
439
|
-
defaultVariableSetIds: Array.isArray(payload.defaultVariableSetIds)
|
|
440
|
-
? (payload.defaultVariableSetIds as string[])
|
|
441
|
-
: base.defaultVariableSetIds,
|
|
447
|
+
...nextDefinition,
|
|
442
448
|
changelog:
|
|
443
449
|
typeof payload.changelog === "string" && payload.changelog.trim()
|
|
444
450
|
? payload.changelog
|
|
445
451
|
: "Verified definition edit",
|
|
446
452
|
createdBy: rigActorForGrant(grant),
|
|
453
|
+
providerImages: rigProviderImagesFromVerification(change.verification, nextDefinition),
|
|
447
454
|
},
|
|
448
455
|
);
|
|
449
456
|
await recordRigAuditEvent(deps.db, {
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
RigProviderImage as RigProviderImageContract,
|
|
4
|
+
stableJson,
|
|
5
|
+
type RigChangeVerification,
|
|
6
|
+
type RigProviderImage,
|
|
7
|
+
type RigProviderImages,
|
|
8
|
+
type RigVersion,
|
|
9
|
+
type SandboxBackend,
|
|
10
|
+
} from "@opengeni/contracts";
|
|
11
|
+
|
|
12
|
+
export type RigProviderImageDefinition = Pick<
|
|
13
|
+
RigVersion,
|
|
14
|
+
"image" | "setupScript" | "checks" | "credentialHooks" | "defaultVariableSetIds"
|
|
15
|
+
>;
|
|
16
|
+
|
|
17
|
+
function sha256(value: string): string {
|
|
18
|
+
return `sha256:${createHash("sha256").update(value, "utf8").digest("hex")}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function rigProviderImageProviderBindingKeyHash(bindingKey: string): string {
|
|
22
|
+
return sha256(bindingKey);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function rigProviderImageSetupHash(
|
|
26
|
+
definition: Pick<RigProviderImageDefinition, "setupScript">,
|
|
27
|
+
): string {
|
|
28
|
+
return sha256(definition.setupScript ?? "");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Hash every exact rig-definition input that can affect whether a provider
|
|
33
|
+
* image may be reused. Credential values and variable-set contents are
|
|
34
|
+
* intentionally absent: verification never injects them into the build box.
|
|
35
|
+
* Their declared hook/set identities still participate so a new rig version
|
|
36
|
+
* cannot silently inherit an older build record.
|
|
37
|
+
*/
|
|
38
|
+
export function rigProviderImageContentHash(input: {
|
|
39
|
+
backend: SandboxBackend;
|
|
40
|
+
sourceImage: string | null;
|
|
41
|
+
definition: RigProviderImageDefinition;
|
|
42
|
+
}): string {
|
|
43
|
+
return sha256(
|
|
44
|
+
stableJson({
|
|
45
|
+
version: 1,
|
|
46
|
+
backend: input.backend,
|
|
47
|
+
sourceImage: input.sourceImage,
|
|
48
|
+
image: input.definition.image,
|
|
49
|
+
setupScript: input.definition.setupScript,
|
|
50
|
+
checks: input.definition.checks,
|
|
51
|
+
credentialHooks: input.definition.credentialHooks,
|
|
52
|
+
defaultVariableSetIds: input.definition.defaultVariableSetIds,
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Modal's patched snapshot API accepts a caller-owned UUID idempotency key.
|
|
58
|
+
* v2 denotes images proven by a separate cold boot before publication. */
|
|
59
|
+
export function rigProviderImageBuildRequestId(input: {
|
|
60
|
+
targetId: string;
|
|
61
|
+
backend: SandboxBackend;
|
|
62
|
+
contentHash: string;
|
|
63
|
+
}): string {
|
|
64
|
+
const bytes = createHash("sha256")
|
|
65
|
+
.update("opengeni-rig-provider-image-build-v2\0", "utf8")
|
|
66
|
+
.update(input.targetId, "utf8")
|
|
67
|
+
.update("\0", "utf8")
|
|
68
|
+
.update(input.backend, "utf8")
|
|
69
|
+
.update("\0", "utf8")
|
|
70
|
+
.update(input.contentHash, "utf8")
|
|
71
|
+
.digest()
|
|
72
|
+
.subarray(0, 16);
|
|
73
|
+
// RFC 4122 variant + deterministic version-5-shaped UUID. The digest, not a
|
|
74
|
+
// provider response, is the idempotency identity.
|
|
75
|
+
bytes[6] = (bytes[6]! & 0x0f) | 0x50;
|
|
76
|
+
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
|
|
77
|
+
const hex = bytes.toString("hex");
|
|
78
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function rigProviderImageMatchesDefinition(
|
|
82
|
+
image: RigProviderImage,
|
|
83
|
+
definition: RigProviderImageDefinition,
|
|
84
|
+
): boolean {
|
|
85
|
+
return (
|
|
86
|
+
image.setupHash === rigProviderImageSetupHash(definition) &&
|
|
87
|
+
image.contentHash ===
|
|
88
|
+
rigProviderImageContentHash({
|
|
89
|
+
backend: image.backend,
|
|
90
|
+
sourceImage: image.sourceImage,
|
|
91
|
+
definition,
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Promotion copies only a finalized, structurally valid build record whose
|
|
98
|
+
* hashes match the exact version definition being inserted. A malformed or
|
|
99
|
+
* stale optimization record is discarded; the version remains correct and
|
|
100
|
+
* falls back to runtime setup.
|
|
101
|
+
*/
|
|
102
|
+
export function rigProviderImagesFromVerification(
|
|
103
|
+
verification: RigChangeVerification | null,
|
|
104
|
+
definition: RigProviderImageDefinition,
|
|
105
|
+
): RigProviderImages {
|
|
106
|
+
const parsed = RigProviderImageContract.safeParse(verification?.providerImage);
|
|
107
|
+
if (!parsed.success || parsed.data.status === "building") return {};
|
|
108
|
+
if (!rigProviderImageMatchesDefinition(parsed.data, definition)) return {};
|
|
109
|
+
return { [parsed.data.backend]: parsed.data };
|
|
110
|
+
}
|
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
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
// The DB-coupled glue (readActiveSandbox / getSandbox / the selfhosted ControlRpc
|
|
12
12
|
// over the events bus) lives here, not in the leaf (which stays db-free).
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { sandboxLifecycleTransitionWaitMs, 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 = {
|
|
@@ -58,6 +61,7 @@ export type ChannelARoutingServices = {
|
|
|
58
61
|
settings: Settings;
|
|
59
62
|
bus?: EventBus;
|
|
60
63
|
onSandboxOperation?: RoutingSandboxOperationObserver;
|
|
64
|
+
waitSignal?: AbortSignal;
|
|
61
65
|
};
|
|
62
66
|
|
|
63
67
|
/** Map the deployment relay URL to the leaf's `SelfhostedRelayConfig` shape. The
|
|
@@ -114,6 +118,27 @@ function controlRpcFactory(bus: EventBus | undefined): () => ControlRpc {
|
|
|
114
118
|
});
|
|
115
119
|
}
|
|
116
120
|
|
|
121
|
+
async function resolveSelfhostedOpStream(
|
|
122
|
+
services: ChannelARoutingServices,
|
|
123
|
+
workspaceId: string,
|
|
124
|
+
sandbox: RoutableSandbox,
|
|
125
|
+
): Promise<SelfhostedOpStreamDeps | undefined> {
|
|
126
|
+
if (
|
|
127
|
+
services.settings.agentOpStreamEnabled !== true ||
|
|
128
|
+
!services.bus?.getOpStreamConnection ||
|
|
129
|
+
!sandbox.enrollmentId
|
|
130
|
+
) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
const enrollment = await getEnrollment(services.db, workspaceId, sandbox.enrollmentId);
|
|
134
|
+
if (enrollment?.opStream !== true) return undefined;
|
|
135
|
+
return {
|
|
136
|
+
transport: new NatsOpStreamTransport(
|
|
137
|
+
async () => services.bus?.getOpStreamConnection?.() ?? null,
|
|
138
|
+
),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
117
142
|
/** Whether the routing proxy should wrap the Channel-A box: gated by the
|
|
118
143
|
* selfhosted flag (the active pointer + swap are only meaningful then). */
|
|
119
144
|
export function routingEnabled(settings: Settings): boolean {
|
|
@@ -190,7 +215,8 @@ export function wrapChannelABoxWithRouting(
|
|
|
190
215
|
routeTargetId: backend.sandboxId,
|
|
191
216
|
routeEpoch: backend.activeEpoch,
|
|
192
217
|
operation: op,
|
|
193
|
-
captureWaitMs:
|
|
218
|
+
captureWaitMs: sandboxLifecycleTransitionWaitMs(settings),
|
|
219
|
+
...(services.waitSignal ? { waitSignal: services.waitSignal } : {}),
|
|
194
220
|
});
|
|
195
221
|
return { admission, providerBinding };
|
|
196
222
|
}
|
|
@@ -286,7 +312,8 @@ export function wrapChannelABoxWithRouting(
|
|
|
286
312
|
sessionId: ids.sessionId,
|
|
287
313
|
processId: process.id,
|
|
288
314
|
operation: op,
|
|
289
|
-
captureWaitMs:
|
|
315
|
+
captureWaitMs: sandboxLifecycleTransitionWaitMs(settings),
|
|
316
|
+
...(services.waitSignal ? { waitSignal: services.waitSignal } : {}),
|
|
290
317
|
})
|
|
291
318
|
: undefined;
|
|
292
319
|
const afterProcessMutation = homeLease
|
|
@@ -389,6 +416,8 @@ export function wrapChannelABoxWithRouting(
|
|
|
389
416
|
: null;
|
|
390
417
|
},
|
|
391
418
|
controlRpcFactory: controlRpcFactory(bus),
|
|
419
|
+
resolveSelfhostedOpStream: (sandbox) =>
|
|
420
|
+
resolveSelfhostedOpStream(services, ids.workspaceId, sandbox),
|
|
392
421
|
relay: relayConfigFromSettings(settings),
|
|
393
422
|
selfhostedTimeoutMs: settings.sandboxSelfhostedControlTimeoutMs,
|
|
394
423
|
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
|
|
@@ -221,5 +238,8 @@ async function resolveSessionAuthorizationActor(
|
|
|
221
238
|
executionGeneration,
|
|
222
239
|
initiator: turn.initiator,
|
|
223
240
|
initiatorContext: turn.initiatorContext,
|
|
241
|
+
initiatingHumanSubjectId:
|
|
242
|
+
turn.initiatingHumanSubjectId ??
|
|
243
|
+
(turn.initiator.kind === "subject" ? turn.initiator.subjectId : null),
|
|
224
244
|
});
|
|
225
245
|
}
|
|
@@ -4,3 +4,11 @@ export const SESSION_WORKFLOW_WAKE_DISPATCHER_SCHEDULE_ID =
|
|
|
4
4
|
export const SESSION_WORKFLOW_WAKE_DISPATCHER_WORKFLOW_TYPE =
|
|
5
5
|
"sessionWorkflowWakeDispatcherWorkflow";
|
|
6
6
|
export const SESSION_WORKFLOW_WAKE_DISPATCHER_PERIOD_MS = 10_000;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Temporal delivers activity cancellation through heartbeats. Keep both the
|
|
10
|
+
* activity's local heartbeat timer and the worker's SDK throttle at this bound
|
|
11
|
+
* so the end-to-end Pause/Steer physical-cancellation contract is not already
|
|
12
|
+
* exhausted before sandbox/tool quiescence and replacement admission begin.
|
|
13
|
+
*/
|
|
14
|
+
export const TURN_ACTIVITY_CANCELLATION_HEARTBEAT_INTERVAL_MS = 500;
|