@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.
Files changed (123) hide show
  1. package/dist/access/index.d.ts +9 -1
  2. package/dist/application/new-session-drafts.d.ts +1 -1
  3. package/dist/application/session-commands.d.ts +9 -4
  4. package/dist/billing/limits.d.ts +1 -1
  5. package/dist/chunk-6WKPWE5S.js +2380 -0
  6. package/dist/chunk-6WKPWE5S.js.map +1 -0
  7. package/dist/chunk-JJU6I5XD.js +2713 -0
  8. package/dist/chunk-JJU6I5XD.js.map +1 -0
  9. package/dist/chunk-NYPMQ7NO.js +2270 -0
  10. package/dist/chunk-NYPMQ7NO.js.map +1 -0
  11. package/dist/dependencies.d.ts +26 -6
  12. package/dist/domain/capabilities.d.ts +35 -3
  13. package/dist/domain/company-profile-durable-learning-adapter.d.ts +39 -0
  14. package/dist/domain/durable-learning-slack-publication.d.ts +72 -0
  15. package/dist/domain/editable-artifacts/agent-application.d.ts +192 -0
  16. package/dist/domain/editable-artifacts/durable-export-postgres.d.ts +4 -0
  17. package/dist/domain/editable-artifacts/durable-export.d.ts +212 -0
  18. package/dist/domain/editable-artifacts/errors.d.ts +47 -0
  19. package/dist/domain/editable-artifacts/hash.d.ts +11 -0
  20. package/dist/domain/editable-artifacts/in-memory.d.ts +99 -0
  21. package/dist/domain/editable-artifacts/index.d.ts +14 -0
  22. package/dist/domain/editable-artifacts/ports.d.ts +341 -0
  23. package/dist/domain/editable-artifacts/postgres-adapters.d.ts +13 -0
  24. package/dist/domain/editable-artifacts/service.d.ts +105 -0
  25. package/dist/domain/editable-artifacts/snapshot-compaction.d.ts +44 -0
  26. package/dist/domain/editable-artifacts/snapshot-verifier-genesis.d.ts +50 -0
  27. package/dist/domain/editable-artifacts/snapshot-verifier-materialization.d.ts +21 -0
  28. package/dist/domain/editable-artifacts/snapshot-verifier.d.ts +118 -0
  29. package/dist/domain/editable-artifacts/types.d.ts +410 -0
  30. package/dist/domain/memory-slack-delivery.d.ts +19 -0
  31. package/dist/domain/memory-slack-publication.d.ts +0 -2
  32. package/dist/domain/packs.d.ts +29 -1
  33. package/dist/domain/personal-connection-delegations.d.ts +9 -1
  34. package/dist/domain/scheduled-tasks.d.ts +27 -3
  35. package/dist/domain/session-tool-policy.d.ts +2 -2
  36. package/dist/domain/sessions.d.ts +44 -10
  37. package/dist/domain/skill-imports.d.ts +32 -0
  38. package/dist/domain/slack-publication-secret-safety.d.ts +8 -0
  39. package/dist/domain/timeline-annotations.d.ts +5 -0
  40. package/dist/domain/video-generation-capabilities.d.ts +9 -0
  41. package/dist/domain/video-generation.d.ts +47 -0
  42. package/dist/editable-artifact-live/application.d.ts +69 -0
  43. package/dist/editable-artifact-live/errors.d.ts +1 -0
  44. package/dist/editable-artifact-live/index.d.ts +8 -0
  45. package/dist/editable-artifact-live/outbox-dispatcher.d.ts +162 -0
  46. package/dist/editable-artifact-live/ports.d.ts +100 -0
  47. package/dist/editable-artifact-live/server.d.ts +71 -0
  48. package/dist/editable-artifact-live/ticket.d.ts +40 -0
  49. package/dist/editable-artifact-live/types.d.ts +213 -0
  50. package/dist/editable-artifact-live/wire.d.ts +34 -0
  51. package/dist/editable-artifact-live.d.ts +2 -0
  52. package/dist/editable-artifact-live.js +58 -0
  53. package/dist/editable-artifact-live.js.map +1 -0
  54. package/dist/editable-artifacts.d.ts +2 -0
  55. package/dist/editable-artifacts.js +179 -0
  56. package/dist/editable-artifacts.js.map +1 -0
  57. package/dist/index.d.ts +35 -25
  58. package/dist/index.js +3184 -652
  59. package/dist/index.js.map +1 -1
  60. package/dist/managed-session.d.ts +53 -0
  61. package/dist/rigs/index.d.ts +1 -1
  62. package/dist/rigs/provider-images.d.ts +31 -0
  63. package/dist/sandbox/fleet.d.ts +9 -1
  64. package/dist/sandbox/routing.d.ts +1 -0
  65. package/dist/session-authorization.d.ts +10 -1
  66. package/dist/workflow-wake-contract.d.ts +7 -0
  67. package/package.json +18 -10
  68. package/src/access/index.ts +43 -3
  69. package/src/application/new-session-drafts.ts +2 -0
  70. package/src/application/session-commands.ts +217 -116
  71. package/src/dependencies.ts +30 -2
  72. package/src/domain/capabilities.ts +672 -218
  73. package/src/domain/company-profile-durable-learning-adapter.ts +221 -0
  74. package/src/domain/durable-learning-slack-publication.ts +202 -0
  75. package/src/domain/editable-artifacts/agent-application.ts +687 -0
  76. package/src/domain/editable-artifacts/durable-export-postgres.ts +139 -0
  77. package/src/domain/editable-artifacts/durable-export.ts +734 -0
  78. package/src/domain/editable-artifacts/errors.ts +115 -0
  79. package/src/domain/editable-artifacts/hash.ts +131 -0
  80. package/src/domain/editable-artifacts/in-memory.ts +1662 -0
  81. package/src/domain/editable-artifacts/index.ts +14 -0
  82. package/src/domain/editable-artifacts/ports.ts +462 -0
  83. package/src/domain/editable-artifacts/postgres-adapters.ts +49 -0
  84. package/src/domain/editable-artifacts/service.ts +2402 -0
  85. package/src/domain/editable-artifacts/snapshot-compaction.ts +246 -0
  86. package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +235 -0
  87. package/src/domain/editable-artifacts/snapshot-verifier-materialization.ts +74 -0
  88. package/src/domain/editable-artifacts/snapshot-verifier.ts +687 -0
  89. package/src/domain/editable-artifacts/types.ts +835 -0
  90. package/src/domain/insights.ts +136 -33
  91. package/src/domain/memory-slack-delivery.ts +204 -0
  92. package/src/domain/memory-slack-publication.ts +10 -19
  93. package/src/domain/packs.ts +266 -5
  94. package/src/domain/personal-connection-delegations.ts +97 -6
  95. package/src/domain/resources.ts +10 -20
  96. package/src/domain/scheduled-tasks.ts +365 -25
  97. package/src/domain/session-tool-policy.ts +8 -2
  98. package/src/domain/sessions.ts +388 -155
  99. package/src/domain/skill-imports.ts +350 -0
  100. package/src/domain/slack-publication-secret-safety.ts +42 -0
  101. package/src/domain/timeline-annotations.ts +204 -0
  102. package/src/domain/video-generation-capabilities.ts +69 -0
  103. package/src/domain/video-generation.ts +249 -0
  104. package/src/editable-artifact-live/application.ts +182 -0
  105. package/src/editable-artifact-live/errors.ts +4 -0
  106. package/src/editable-artifact-live/index.ts +8 -0
  107. package/src/editable-artifact-live/outbox-dispatcher.ts +916 -0
  108. package/src/editable-artifact-live/ports.ts +139 -0
  109. package/src/editable-artifact-live/server.ts +1707 -0
  110. package/src/editable-artifact-live/ticket.ts +217 -0
  111. package/src/editable-artifact-live/types.ts +286 -0
  112. package/src/editable-artifact-live/wire.ts +95 -0
  113. package/src/editable-artifact-live.ts +2 -0
  114. package/src/editable-artifacts.ts +2 -0
  115. package/src/index.ts +14 -0
  116. package/src/managed-auth-type.ts +4 -3
  117. package/src/managed-session.ts +36 -0
  118. package/src/rigs/index.ts +26 -19
  119. package/src/rigs/provider-images.ts +110 -0
  120. package/src/sandbox/fleet.ts +56 -1
  121. package/src/sandbox/routing.ts +32 -3
  122. package/src/session-authorization.ts +20 -0
  123. 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
- image: base.image,
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
- image: payload.image === undefined ? base.image : (payload.image as string | null),
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
+ }
@@ -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 enroll`, 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.",
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
@@ -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 { sandboxArchiveCaptureTimeoutMs, type Settings } from "@opengeni/config";
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: sandboxArchiveCaptureTimeoutMs(settings),
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: sandboxArchiveCaptureTimeoutMs(settings),
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;