@opengeni/runtime 0.6.0 → 0.7.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/{chunk-BHA2SU3H.js → chunk-RYDCVTMW.js} +1959 -208
- package/dist/chunk-RYDCVTMW.js.map +1 -0
- package/dist/{index-otZkoLYq.d.ts → index-DIiiwQY2.d.ts} +693 -55
- package/dist/index.d.ts +293 -121
- package/dist/index.js +1819 -786
- package/dist/index.js.map +1 -1
- package/dist/sandbox/index.d.ts +1 -1
- package/dist/sandbox/index.js +79 -3
- package/package.json +21 -20
- package/src/codex-tool-search.ts +185 -31
- package/src/context-compaction.ts +78 -243
- package/src/history-sanitizer.ts +172 -20
- package/src/index.ts +2573 -897
- package/src/metrics.ts +28 -2
- package/src/sandbox/capabilities.ts +12 -3
- package/src/sandbox/channel-a.ts +420 -72
- package/src/sandbox/display-stack.ts +18 -4
- package/src/sandbox/errors.ts +13 -0
- package/src/sandbox/index.ts +398 -112
- package/src/sandbox/op-correlation.ts +70 -0
- package/src/sandbox/providers/index.ts +9 -3
- package/src/sandbox/providers/modal.ts +99 -24
- package/src/sandbox/providers/selfhosted.ts +1 -4
- package/src/sandbox/recording.ts +146 -90
- package/src/sandbox/routing/backend-resolver.ts +144 -10
- package/src/sandbox/routing/routing-session.ts +129 -25
- package/src/sandbox/select.ts +60 -8
- package/src/sandbox/selfhosted/capabilities.ts +59 -25
- package/src/sandbox/selfhosted/control-rpc.ts +160 -28
- package/src/sandbox/selfhosted/fault-rendering.ts +226 -0
- package/src/sandbox/selfhosted/op-observer.ts +54 -0
- package/src/sandbox/selfhosted/op-stream.ts +801 -0
- package/src/sandbox/selfhosted/op-testing.ts +468 -0
- package/src/sandbox/selfhosted/op-transport.ts +151 -0
- package/src/sandbox/selfhosted/retry-policy.ts +177 -0
- package/src/sandbox/selfhosted/session.ts +498 -55
- package/src/sandbox/selfhosted/testing.ts +60 -19
- package/src/sandbox/stream-port.ts +17 -5
- package/src/sandbox/stream-token.ts +8 -2
- package/src/sandbox/terminal-server.ts +18 -4
- package/src/sandbox-computer.ts +332 -70
- package/src/usage-telemetry.ts +73 -0
- package/dist/chunk-BHA2SU3H.js.map +0 -1
- package/src/image-history.ts +0 -149
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Durable op-identity correlation (op-stream ruling B1): the DURABLE op id a
|
|
2
|
+
// sandbox transport op carries is `{sdk_tool_call_id}:{ordinal}` — minted ABOVE
|
|
3
|
+
// the transport, at the semantic layer. The tool call id lives in the model's
|
|
4
|
+
// function_call (persisted run history), so a re-dispatched turn re-executes the
|
|
5
|
+
// SAME function_call with the SAME call id, and the transport's idempotent
|
|
6
|
+
// OpStart turns an at-least-once re-execution into an ATTACH instead of a
|
|
7
|
+
// re-run. The ordinal is the deterministic position of the physical sub-op
|
|
8
|
+
// inside one tool invocation (a tool that performs read→write→rm mints :0, :1,
|
|
9
|
+
// :2), so a re-executed invocation re-mints identical ids.
|
|
10
|
+
//
|
|
11
|
+
// The correlation rides an AsyncLocalStorage bound around the SDK tool's
|
|
12
|
+
// `execute` (see `withExecOpCorrelation` in the runtime barrel): the SDK's tool
|
|
13
|
+
// machinery passes `details.toolCall` (with `callId`) into every function-tool
|
|
14
|
+
// invocation, and the shell capability's `configureTools` hook lets us wrap
|
|
15
|
+
// exec_command without touching the SDK. Per-async-chain storage keeps PARALLEL
|
|
16
|
+
// tool calls correctly separated.
|
|
17
|
+
//
|
|
18
|
+
// This module is deliberately dependency-free (node:async_hooks only) so the
|
|
19
|
+
// agent-loop-free sandbox leaf may import it: the leaf READS the context; only
|
|
20
|
+
// the runtime barrel (which owns the SDK imports) BINDS it.
|
|
21
|
+
|
|
22
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
23
|
+
|
|
24
|
+
interface ToolCallCorrelation {
|
|
25
|
+
/** The sanitized sdk tool call id (a legal NATS subject-token fragment). */
|
|
26
|
+
callId: string;
|
|
27
|
+
/** The next sub-op ordinal within this tool invocation (mutable). */
|
|
28
|
+
ordinal: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const storage = new AsyncLocalStorage<ToolCallCorrelation>();
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Sanitize a tool call id into a legal NATS subject TOKEN fragment. The op id
|
|
35
|
+
* is interpolated into the per-op frame subject, whose tokens must not contain
|
|
36
|
+
* whitespace/control characters or the subject-structure characters (`.`,
|
|
37
|
+
* `*`, `>`); the runner refuses illegal ids loudly. The mapping is INJECTIVE
|
|
38
|
+
* (each disallowed char becomes `_<hex>_`), so two distinct call ids can never
|
|
39
|
+
* collide into one op id — a collision would merge two different execs through
|
|
40
|
+
* the idempotent-OpStart dedup and return the wrong result.
|
|
41
|
+
*/
|
|
42
|
+
export function sanitizeOpIdToken(raw: string): string {
|
|
43
|
+
return raw.replace(/[^A-Za-z0-9_-]/g, (c) => `_${c.charCodeAt(0).toString(16)}_`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Bind a tool-call correlation context around `fn` (the tool's execute). Every
|
|
48
|
+
* durable op id minted inside — however deep in the transport — is
|
|
49
|
+
* `{callId}:{ordinal}` with ordinals starting at 0 per invocation.
|
|
50
|
+
*/
|
|
51
|
+
export function runWithToolCallCorrelation<T>(callId: string, fn: () => T): T {
|
|
52
|
+
return storage.run({ callId: sanitizeOpIdToken(callId), ordinal: 0 }, fn);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Mint the next durable op id for the current tool invocation, or null when no
|
|
57
|
+
* correlation context is bound (a non-tool caller, e.g. Channel-A structural
|
|
58
|
+
* exec). Callers fall back to a random unique id — safe (never collides, never
|
|
59
|
+
* wrongly dedups), merely not stable across a turn re-dispatch, which degrades
|
|
60
|
+
* that one op to today's at-least-once semantics.
|
|
61
|
+
*/
|
|
62
|
+
export function nextDurableOpId(): string | null {
|
|
63
|
+
const context = storage.getStore();
|
|
64
|
+
if (!context) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const ordinal = context.ordinal;
|
|
68
|
+
context.ordinal += 1;
|
|
69
|
+
return `${context.callId}:${ordinal}`;
|
|
70
|
+
}
|
|
@@ -66,16 +66,22 @@ export function assertProviderRegistryInvariants(): void {
|
|
|
66
66
|
for (const backend of SandboxBackend.options) {
|
|
67
67
|
const registration = PROVIDER_REGISTRY[backend];
|
|
68
68
|
if (registration.backend !== backend) {
|
|
69
|
-
throw new Error(
|
|
69
|
+
throw new Error(
|
|
70
|
+
`PROVIDER_REGISTRY["${backend}"].backend mismatch (got "${registration.backend}")`,
|
|
71
|
+
);
|
|
70
72
|
}
|
|
71
73
|
if (registration.descriptor.backend !== backend) {
|
|
72
|
-
throw new Error(
|
|
74
|
+
throw new Error(
|
|
75
|
+
`PROVIDER_REGISTRY["${backend}"].descriptor.backend mismatch (got "${registration.descriptor.backend}")`,
|
|
76
|
+
);
|
|
73
77
|
}
|
|
74
78
|
if (backend === "none") {
|
|
75
79
|
// "none" has no SDK client (build returns undefined); the descriptor
|
|
76
80
|
// backendId "none" is self-consistent.
|
|
77
81
|
if (registration.descriptor.backendId !== "none") {
|
|
78
|
-
throw new Error(
|
|
82
|
+
throw new Error(
|
|
83
|
+
`"none" descriptor.backendId must be "none" (got "${registration.descriptor.backendId}")`,
|
|
84
|
+
);
|
|
79
85
|
}
|
|
80
86
|
continue;
|
|
81
87
|
}
|
|
@@ -6,7 +6,14 @@ import { SandboxConfigError } from "../errors";
|
|
|
6
6
|
import type { ProviderRegistration } from "./types";
|
|
7
7
|
|
|
8
8
|
const MODAL_ORPHAN_SWEEP_LIMIT = 50;
|
|
9
|
-
|
|
9
|
+
// A provider box is invisible to the lease until Modal create + manifest
|
|
10
|
+
// materialization returns and the creation callback records its instance id.
|
|
11
|
+
// Production baseline (2026-07-15, all 8 turn workers): 155/155 completed creates
|
|
12
|
+
// finished under 10s (154 under 2.5s). Two minutes is a 12x observed-max buffer,
|
|
13
|
+
// while avoiding the former 30-minute retention of boxes abandoned by a rolling
|
|
14
|
+
// worker restart. The live-instance guard below remains authoritative: once a box
|
|
15
|
+
// is recorded by any lease, age and missing/stale tags can never terminate it.
|
|
16
|
+
const MODAL_UNATTRIBUTED_ORPHAN_GRACE_MS = 2 * 60_000;
|
|
10
17
|
|
|
11
18
|
export type ModalSandboxAttribution = {
|
|
12
19
|
leaseId: string;
|
|
@@ -31,7 +38,9 @@ export type ModalOrphanSweepResult = {
|
|
|
31
38
|
skipped: number;
|
|
32
39
|
};
|
|
33
40
|
|
|
34
|
-
export function modalSandboxAttributionEnvironment(
|
|
41
|
+
export function modalSandboxAttributionEnvironment(
|
|
42
|
+
input: ModalSandboxAttribution,
|
|
43
|
+
): Record<string, string> {
|
|
35
44
|
return {
|
|
36
45
|
OPENGENI_SANDBOX_LEASE_ID: input.leaseId,
|
|
37
46
|
OPENGENI_SANDBOX_GROUP_ID: input.sandboxGroupId,
|
|
@@ -39,7 +48,9 @@ export function modalSandboxAttributionEnvironment(input: ModalSandboxAttributio
|
|
|
39
48
|
};
|
|
40
49
|
}
|
|
41
50
|
|
|
42
|
-
export function modalSandboxAttributionTags(
|
|
51
|
+
export function modalSandboxAttributionTags(
|
|
52
|
+
input: ModalSandboxAttribution,
|
|
53
|
+
): Record<string, string> {
|
|
43
54
|
return {
|
|
44
55
|
opengeni: "true",
|
|
45
56
|
opengeni_lease_id: input.leaseId,
|
|
@@ -70,10 +81,11 @@ export const modalProvider: ProviderRegistration = {
|
|
|
70
81
|
sandboxCreateTimeoutS: Math.ceil(settings.sandboxWarmingTimeoutMs / 1000),
|
|
71
82
|
exposedPorts,
|
|
72
83
|
env: environment,
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
|
|
84
|
+
// A registry image's own CMD is not a sandbox keepalive contract (for
|
|
85
|
+
// example, python:3.12-slim can exit immediately). Keep the provider's
|
|
86
|
+
// control process alive so exec/resume remains available; Modal's hard
|
|
87
|
+
// timeout and explicit OpenGeni teardown still own the box lifetime.
|
|
88
|
+
useSleepCmd: true,
|
|
77
89
|
};
|
|
78
90
|
// gap-fill (module 03 §4.1): these SDK options were previously unmapped.
|
|
79
91
|
// ALWAYS pin idleTimeoutMs (sandbox-file-persistence): an UNSET idle timeout
|
|
@@ -115,14 +127,13 @@ type ModalClientLike = InstanceType<ModalModule["ModalClient"]>;
|
|
|
115
127
|
// `fromRegistry(tag, secret)` image ONCE per process, then hand the provider `build`
|
|
116
128
|
// a `ModalImageSelector.fromImage(...)`. `build` is synchronous and modal is imported
|
|
117
129
|
// lazily (never loaded for non-modal backends), so resolution can't happen inside
|
|
118
|
-
// `build`; the worker
|
|
119
|
-
//
|
|
120
|
-
//
|
|
130
|
+
// `build`; the worker awaits `ensureModalRegistryImage` at boot for global refs and
|
|
131
|
+
// at turn time for pack-scoped refs, then `build` reads the settled result. Modal
|
|
132
|
+
// images are lazy, workspace-scoped definitions, so an image built by this module's
|
|
133
|
+
// client is usable by the ModalSandboxClient's own client.
|
|
121
134
|
|
|
122
135
|
/** Loader seam so unit tests can inject a fake modal module. */
|
|
123
|
-
export type ModalModuleLoader = () => Promise<
|
|
124
|
-
Pick<ModalModule, "ModalClient" | "Secret">
|
|
125
|
-
>;
|
|
136
|
+
export type ModalModuleLoader = () => Promise<Pick<ModalModule, "ModalClient">>;
|
|
126
137
|
|
|
127
138
|
const defaultModalLoader: ModalModuleLoader = () => import("modal");
|
|
128
139
|
|
|
@@ -143,8 +154,9 @@ function registryImageCacheKey(settings: Settings): string {
|
|
|
143
154
|
* Resolve + cache the private-registry Modal image. No-op unless BOTH
|
|
144
155
|
* `modalImageRef` and `modalImageRegistrySecret` are set. Memoized per
|
|
145
156
|
* (imageRef, secret, environment) so it runs once per worker process. Awaited at
|
|
146
|
-
* worker boot
|
|
147
|
-
*
|
|
157
|
+
* worker boot for the deployment-global image and at turn time for pack-scoped
|
|
158
|
+
* images BEFORE the first sandbox using that ref is created; `build` then reads the
|
|
159
|
+
* resolved image and otherwise falls back to the public `fromTag` path.
|
|
148
160
|
*/
|
|
149
161
|
export async function ensureModalRegistryImage(
|
|
150
162
|
settings: Settings,
|
|
@@ -162,7 +174,12 @@ export async function ensureModalRegistryImage(
|
|
|
162
174
|
pending = (async () => {
|
|
163
175
|
const modal = await loadModal();
|
|
164
176
|
const client = new modal.ModalClient(modalClientOptions(settings));
|
|
165
|
-
|
|
177
|
+
// Resolve the Secret via the AUTHENTICATED client (client.secrets.fromName),
|
|
178
|
+
// NOT the static `modal.Secret.fromName`, which resolves against
|
|
179
|
+
// `getDefaultClient()` — i.e. the standard MODAL_TOKEN_ID/MODAL_TOKEN_SECRET env
|
|
180
|
+
// or ~/.modal.toml — and so would throw "Profile is missing token_id" in any host
|
|
181
|
+
// that supplies the token only through OpenGeni settings (OPENGENI_MODAL_TOKEN_ID).
|
|
182
|
+
const secret = await client.secrets.fromName(
|
|
166
183
|
settings.modalImageRegistrySecret!,
|
|
167
184
|
settings.modalEnvironment ? { environment: settings.modalEnvironment } : undefined,
|
|
168
185
|
);
|
|
@@ -189,7 +206,7 @@ function cachedModalRegistryImage(settings: Settings): unknown | undefined {
|
|
|
189
206
|
/**
|
|
190
207
|
* Choose the image selector for a Modal sandbox client from settings. Returns:
|
|
191
208
|
* - `fromImage(resolved)` when a private-registry secret is configured AND the
|
|
192
|
-
* image has been resolved (ensureModalRegistryImage ran
|
|
209
|
+
* image has been resolved (ensureModalRegistryImage ran before create);
|
|
193
210
|
* - `fromTag(modalImageRef)` for the public path (no secret, or cold cache — the
|
|
194
211
|
* resume/attach paths never pull an image so the tag branch is harmless there);
|
|
195
212
|
* - `undefined` when no image ref is set (Modal uses its default image).
|
|
@@ -201,7 +218,9 @@ export function resolveModalImageSelector(settings: Settings): ModalImageSelecto
|
|
|
201
218
|
}
|
|
202
219
|
const registryImage = cachedModalRegistryImage(settings);
|
|
203
220
|
return registryImage
|
|
204
|
-
? ModalImageSelector.fromImage(
|
|
221
|
+
? ModalImageSelector.fromImage(
|
|
222
|
+
registryImage as Parameters<typeof ModalImageSelector.fromImage>[0],
|
|
223
|
+
)
|
|
205
224
|
: ModalImageSelector.fromTag(settings.modalImageRef);
|
|
206
225
|
}
|
|
207
226
|
|
|
@@ -211,7 +230,9 @@ export function __resetModalRegistryImageCacheForTest(): void {
|
|
|
211
230
|
inFlightRegistryImages.clear();
|
|
212
231
|
}
|
|
213
232
|
|
|
214
|
-
function modalClientOptions(
|
|
233
|
+
function modalClientOptions(
|
|
234
|
+
settings: Settings,
|
|
235
|
+
): ConstructorParameters<ModalModule["ModalClient"]>[0] {
|
|
215
236
|
return {
|
|
216
237
|
...(settings.modalTokenId ? { tokenId: settings.modalTokenId } : {}),
|
|
217
238
|
...(settings.modalTokenSecret ? { tokenSecret: settings.modalTokenSecret } : {}),
|
|
@@ -243,7 +264,10 @@ export async function tagModalSandbox(
|
|
|
243
264
|
}
|
|
244
265
|
}
|
|
245
266
|
|
|
246
|
-
export async function terminateModalSandboxById(
|
|
267
|
+
export async function terminateModalSandboxById(
|
|
268
|
+
settings: Settings,
|
|
269
|
+
sandboxId: string,
|
|
270
|
+
): Promise<boolean> {
|
|
247
271
|
if (!sandboxId) {
|
|
248
272
|
return true;
|
|
249
273
|
}
|
|
@@ -286,14 +310,22 @@ function tagsFromInfo(info: ModalSandboxInfo): Record<string, string> {
|
|
|
286
310
|
}
|
|
287
311
|
|
|
288
312
|
function sandboxCreatedAtMs(info: ModalSandboxInfo): number | null {
|
|
289
|
-
if (
|
|
313
|
+
if (
|
|
314
|
+
typeof info.createdAt !== "number" ||
|
|
315
|
+
!Number.isFinite(info.createdAt) ||
|
|
316
|
+
info.createdAt <= 0
|
|
317
|
+
) {
|
|
290
318
|
return null;
|
|
291
319
|
}
|
|
292
320
|
// Modal protobuf timestamps in this SDK are seconds as doubles.
|
|
293
|
-
return info.createdAt < 10_000_000_000
|
|
321
|
+
return info.createdAt < 10_000_000_000
|
|
322
|
+
? Math.floor(info.createdAt * 1000)
|
|
323
|
+
: Math.floor(info.createdAt);
|
|
294
324
|
}
|
|
295
325
|
|
|
296
|
-
function attributionKey(
|
|
326
|
+
function attributionKey(
|
|
327
|
+
input: Pick<ModalSandboxAttribution, "leaseId" | "workspaceId" | "sandboxGroupId">,
|
|
328
|
+
): string {
|
|
297
329
|
return `${input.workspaceId}:${input.sandboxGroupId}:${input.leaseId}`;
|
|
298
330
|
}
|
|
299
331
|
|
|
@@ -311,6 +343,18 @@ export async function sweepModalOrphanSandboxes(
|
|
|
311
343
|
const maxTerminations = options.maxTerminations ?? MODAL_ORPHAN_SWEEP_LIMIT;
|
|
312
344
|
const unattributedGraceMs = options.unattributedGraceMs ?? MODAL_UNATTRIBUTED_ORPHAN_GRACE_MS;
|
|
313
345
|
const liveByAttribution = new Map(liveLeases.map((lease) => [attributionKey(lease), lease]));
|
|
346
|
+
// LIVE-INSTANCE GUARD: a box that any live lease's envelope points at is NEVER
|
|
347
|
+
// an orphan, whatever its tags say. Tags are best-effort attribution (setTags
|
|
348
|
+
// is a separate call after create and can fail or lag); the lease envelope is
|
|
349
|
+
// the source of truth the turn path actually resumes by. Judging by tags alone
|
|
350
|
+
// terminated a LIVE box mid-turn at exactly creation+30min (staging session
|
|
351
|
+
// e644e8a8, 2026-07-06) — the box's unpushed work was unrecoverable because
|
|
352
|
+
// nothing outside the reaper drain persists /workspace.
|
|
353
|
+
const liveByInstanceId = new Map(
|
|
354
|
+
liveLeases
|
|
355
|
+
.filter((lease) => lease.instanceId)
|
|
356
|
+
.map((lease) => [lease.instanceId as string, lease]),
|
|
357
|
+
);
|
|
314
358
|
const ownedClient = options.client ? null : await createModalClient(settings);
|
|
315
359
|
const modal = (options.client ?? ownedClient)! as ModalCpListClient;
|
|
316
360
|
try {
|
|
@@ -345,9 +389,40 @@ export async function sweepModalOrphanSandboxes(
|
|
|
345
389
|
const leaseId = tags.opengeni_lease_id;
|
|
346
390
|
const workspaceId = tags.opengeni_workspace_id;
|
|
347
391
|
const sandboxGroupId = tags.opengeni_sandbox_group_id;
|
|
392
|
+
const liveByInstance = info.id ? liveByInstanceId.get(info.id) : undefined;
|
|
393
|
+
if (liveByInstance) {
|
|
394
|
+
// Live-instance guard (see above): a live lease resumes this exact box
|
|
395
|
+
// by id — hard-skip it, and HEAL its attribution tags when they are
|
|
396
|
+
// missing/stale so it stops looking sweep-eligible. Best-effort: a
|
|
397
|
+
// failed re-tag must never fail the sweep (the guard, not the tags,
|
|
398
|
+
// is what protects the box now).
|
|
399
|
+
if (
|
|
400
|
+
leaseId !== liveByInstance.leaseId ||
|
|
401
|
+
workspaceId !== liveByInstance.workspaceId ||
|
|
402
|
+
sandboxGroupId !== liveByInstance.sandboxGroupId
|
|
403
|
+
) {
|
|
404
|
+
try {
|
|
405
|
+
const sandbox = await modal.sandboxes.fromId(info.id);
|
|
406
|
+
await sandbox.setTags(
|
|
407
|
+
modalSandboxAttributionTags({
|
|
408
|
+
leaseId: liveByInstance.leaseId,
|
|
409
|
+
workspaceId: liveByInstance.workspaceId,
|
|
410
|
+
sandboxGroupId: liveByInstance.sandboxGroupId,
|
|
411
|
+
}),
|
|
412
|
+
);
|
|
413
|
+
} catch {
|
|
414
|
+
// Tag healing is opportunistic; the instance guard already
|
|
415
|
+
// protects this box on every future sweep pass.
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
skipped += 1;
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
348
421
|
let reason: ModalOrphanSweepTermination["reason"] | null = null;
|
|
349
422
|
if (leaseId && workspaceId && sandboxGroupId) {
|
|
350
|
-
const live = liveByAttribution.get(
|
|
423
|
+
const live = liveByAttribution.get(
|
|
424
|
+
attributionKey({ leaseId, workspaceId, sandboxGroupId }),
|
|
425
|
+
);
|
|
351
426
|
if (!live || (live.instanceId && live.instanceId !== info.id)) {
|
|
352
427
|
reason = "stale_attribution";
|
|
353
428
|
}
|
|
@@ -20,10 +20,7 @@ import {
|
|
|
20
20
|
type ControlRpc,
|
|
21
21
|
type NatsRequestConnection,
|
|
22
22
|
} from "../selfhosted/control-rpc";
|
|
23
|
-
import {
|
|
24
|
-
SelfhostedSandboxClient,
|
|
25
|
-
type SelfhostedRelayConfig,
|
|
26
|
-
} from "../selfhosted/session";
|
|
23
|
+
import { SelfhostedSandboxClient, type SelfhostedRelayConfig } from "../selfhosted/session";
|
|
27
24
|
import type { ProviderRegistration } from "./types";
|
|
28
25
|
|
|
29
26
|
/**
|