@opengeni/runtime 2.5.2-canary.0 → 2.5.2
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-GBHOT4KS.js → chunk-GJC7H2FC.js} +6 -6
- package/dist/{chunk-GBHOT4KS.js.map → chunk-GJC7H2FC.js.map} +1 -1
- package/dist/{chunk-LEIGL6X2.js → chunk-LGLJ5PVD.js} +1320 -2284
- package/dist/chunk-LGLJ5PVD.js.map +1 -0
- package/dist/index.js +2 -6
- package/dist/sandbox/index.d.ts +1 -5
- package/dist/sandbox/index.js +1 -5
- package/dist/sandbox/workspace-archive.d.ts +0 -7
- package/dist/workspace-tool-gateway.js +2 -2
- package/package.json +11 -11
- package/src/sandbox/index.ts +203 -292
- package/src/sandbox/workspace-archive.ts +1 -54
- package/dist/chunk-LEIGL6X2.js.map +0 -1
- package/dist/sandbox/archive-spool.d.ts +0 -14
- package/dist/sandbox/host-archive-spool.d.ts +0 -19
- package/src/sandbox/archive-spool.ts +0 -16
- package/src/sandbox/host-archive-spool.ts +0 -1040
package/src/sandbox/index.ts
CHANGED
|
@@ -24,15 +24,6 @@ export type {
|
|
|
24
24
|
|
|
25
25
|
import type { Settings } from "@opengeni/config";
|
|
26
26
|
import { collectSandboxEnvironment, parseExposedPorts } from "@opengeni/config";
|
|
27
|
-
export type { WorkspaceArchiveSpool, VerifiedHostWorkspaceArchive } from "./archive-spool";
|
|
28
|
-
import type { WorkspaceArchiveSpool } from "./archive-spool";
|
|
29
|
-
import { restoreHostWorkspaceArchive } from "./host-archive-spool";
|
|
30
|
-
export {
|
|
31
|
-
captureWorkspaceArchiveForStorage,
|
|
32
|
-
disposeWorkspaceArchive,
|
|
33
|
-
type VerifiedWorkspaceArchivePayload,
|
|
34
|
-
} from "./workspace-archive";
|
|
35
|
-
|
|
36
27
|
import {
|
|
37
28
|
BROWSER_CONTROL_PORT,
|
|
38
29
|
DESKTOP_STREAM_PORT,
|
|
@@ -40,9 +31,6 @@ import {
|
|
|
40
31
|
TERMINAL_STREAM_PORT,
|
|
41
32
|
omitInlineWorkspaceArchiveWhenObjectRefPresent,
|
|
42
33
|
parseWorkspaceArchiveObjectRef,
|
|
43
|
-
parseWorkspaceArchiveDescriptor,
|
|
44
|
-
type WorkspaceArchiveObjectRef,
|
|
45
|
-
type TarWorkspaceArchiveDescriptor,
|
|
46
34
|
type SandboxBackend,
|
|
47
35
|
type SandboxProviderContinuityRecovery,
|
|
48
36
|
} from "@opengeni/contracts";
|
|
@@ -1688,16 +1676,9 @@ async function terminateCreatedSandbox(
|
|
|
1688
1676
|
}
|
|
1689
1677
|
|
|
1690
1678
|
type ModalNativeWorkspacePersistence = "snapshot_filesystem" | "snapshot_directory";
|
|
1691
|
-
type RestoreWorkspaceArchive =
|
|
1692
|
-
| VerifiedWorkspaceArchive
|
|
1693
|
-
| {
|
|
1694
|
-
kind: "host_spool";
|
|
1695
|
-
descriptor: TarWorkspaceArchiveDescriptor;
|
|
1696
|
-
ref: WorkspaceArchiveObjectRef;
|
|
1697
|
-
};
|
|
1698
1679
|
|
|
1699
1680
|
function modalWorkspacePersistenceForRestore(
|
|
1700
|
-
archive:
|
|
1681
|
+
archive: VerifiedWorkspaceArchive | null,
|
|
1701
1682
|
): ModalNativeWorkspacePersistence | null {
|
|
1702
1683
|
if (archive?.kind !== "provider_snapshot") return null;
|
|
1703
1684
|
const snapshot = archive.nativeSnapshot;
|
|
@@ -1726,7 +1707,7 @@ function modalWorkspacePersistenceForRestore(
|
|
|
1726
1707
|
function settingsForWorkspaceArchiveRestore(
|
|
1727
1708
|
backend: SandboxBackend,
|
|
1728
1709
|
settings: Settings,
|
|
1729
|
-
archive:
|
|
1710
|
+
archive: VerifiedWorkspaceArchive | null,
|
|
1730
1711
|
): Settings {
|
|
1731
1712
|
if (backend !== "modal") return settings;
|
|
1732
1713
|
const workspacePersistence = modalWorkspacePersistenceForRestore(archive);
|
|
@@ -1754,7 +1735,6 @@ export async function establishSandboxSessionFromEnvelope(
|
|
|
1754
1735
|
opts: {
|
|
1755
1736
|
sessionId: string;
|
|
1756
1737
|
recovery: "create-or-restore" | "resume-only";
|
|
1757
|
-
loadHostWorkspaceArchive?: (ref: WorkspaceArchiveObjectRef) => Promise<WorkspaceArchiveSpool>;
|
|
1758
1738
|
backendOverride?: SandboxBackend;
|
|
1759
1739
|
environment?: Record<string, string>;
|
|
1760
1740
|
onSandboxCreated?: SandboxCreatedCallback;
|
|
@@ -1837,7 +1817,6 @@ export async function establishSandboxSessionFromEnvelope(
|
|
|
1837
1817
|
envelopeSessionState && typeof envelopeSessionState === "object"
|
|
1838
1818
|
? (envelopeSessionState as {
|
|
1839
1819
|
workspaceArchive?: unknown;
|
|
1840
|
-
workspaceArchiveRef?: unknown;
|
|
1841
1820
|
workspaceArchiveMeta?: unknown;
|
|
1842
1821
|
})
|
|
1843
1822
|
: undefined;
|
|
@@ -1859,217 +1838,220 @@ export async function establishSandboxSessionFromEnvelope(
|
|
|
1859
1838
|
// Parse/verify lazily: a warm resume-by-id does not consume its retained
|
|
1860
1839
|
// archive, so a legacy live box remains resumable. Creation/restoration is
|
|
1861
1840
|
// the boundary that requires complete durable metadata.
|
|
1862
|
-
|
|
1841
|
+
const workspaceArchive: VerifiedWorkspaceArchive | null = readVerifiedWorkspaceArchive(
|
|
1863
1842
|
workspaceArchiveBase64,
|
|
1864
1843
|
workspaceArchiveMetadata,
|
|
1865
1844
|
);
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
workspaceArchive = { kind: "host_spool", descriptor: objectDescriptor, ref: objectRef };
|
|
1886
|
-
}
|
|
1887
|
-
if (objectRef && !workspaceArchive) {
|
|
1888
|
-
throw new WorkspaceArchiveIntegrityError(
|
|
1889
|
-
"archive_hydration_failed",
|
|
1890
|
-
"Selected workspace archive object has no supported restore loader",
|
|
1845
|
+
// The selected native artifact is the durable authority for its restore
|
|
1846
|
+
// protocol. The process setting governs new sandboxes only: a mode rollout
|
|
1847
|
+
// must not make an older, verified Modal snapshot impossible to hydrate.
|
|
1848
|
+
const restoreSettings = settingsForWorkspaceArchiveRestore(backend, settings, workspaceArchive);
|
|
1849
|
+
const restoreClient =
|
|
1850
|
+
restoreSettings === settings
|
|
1851
|
+
? client
|
|
1852
|
+
: ((opts.clientFactory
|
|
1853
|
+
? opts.clientFactory(backend, restoreSettings, environment)
|
|
1854
|
+
: createSandboxClientForBackend(
|
|
1855
|
+
backend,
|
|
1856
|
+
restoreSettings,
|
|
1857
|
+
environment,
|
|
1858
|
+
opts.metrics,
|
|
1859
|
+
)) as ResumeCapableClient | undefined);
|
|
1860
|
+
if (!restoreClient?.create) {
|
|
1861
|
+
throw new SandboxConfigError(
|
|
1862
|
+
backend,
|
|
1863
|
+
`Sandbox backend "${backend}" does not support fresh archive restoration`,
|
|
1891
1864
|
);
|
|
1892
1865
|
}
|
|
1893
|
-
let
|
|
1866
|
+
let createdClient = restoreClient;
|
|
1867
|
+
const createStarted = Date.now();
|
|
1868
|
+
let restored: Awaited<ReturnType<NonNullable<typeof restoreClient.create>>>;
|
|
1894
1869
|
try {
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1870
|
+
restored = await restoreClient.create({ manifest: createManifest });
|
|
1871
|
+
recordSandboxCreateMetric(
|
|
1872
|
+
opts.metrics,
|
|
1873
|
+
restoreClient.backendId,
|
|
1874
|
+
createImageSource,
|
|
1875
|
+
"completed",
|
|
1876
|
+
createStarted,
|
|
1877
|
+
);
|
|
1878
|
+
} catch (error) {
|
|
1879
|
+
recordSandboxCreateMetric(
|
|
1880
|
+
opts.metrics,
|
|
1881
|
+
restoreClient.backendId,
|
|
1882
|
+
createImageSource,
|
|
1883
|
+
"failed",
|
|
1884
|
+
createStarted,
|
|
1885
|
+
);
|
|
1886
|
+
recordSandboxProviderApiThrottleMetric(
|
|
1887
|
+
opts.metrics,
|
|
1888
|
+
restoreClient.backendId,
|
|
1889
|
+
"create",
|
|
1890
|
+
error,
|
|
1891
|
+
);
|
|
1892
|
+
if (
|
|
1893
|
+
createImageSource !== "provider_immutable" ||
|
|
1894
|
+
backend !== "modal" ||
|
|
1895
|
+
!isProviderSandboxNotFoundError(restoreClient.backendId, error)
|
|
1896
|
+
) {
|
|
1897
|
+
throw error;
|
|
1898
|
+
}
|
|
1899
|
+
const fallbackBaseSettings =
|
|
1900
|
+
opts.logicalFallbackSettings ??
|
|
1901
|
+
(settings.modalImageRef ? { ...settings, modalImageId: undefined } : null);
|
|
1902
|
+
if (!fallbackBaseSettings) {
|
|
1903
|
+
// An ID-only logical base is supported. Without the exact pre-selection
|
|
1904
|
+
// settings, clearing the optimized ID would silently boot Modal's
|
|
1905
|
+
// default image, so fail closed instead of changing the rig base.
|
|
1906
|
+
throw error;
|
|
1908
1907
|
}
|
|
1909
|
-
|
|
1910
|
-
// protocol. The process setting governs new sandboxes only: a mode rollout
|
|
1911
|
-
// must not make an older, verified Modal snapshot impossible to hydrate.
|
|
1912
|
-
const restoreSettings = settingsForWorkspaceArchiveRestore(
|
|
1908
|
+
const fallbackSettings = settingsForWorkspaceArchiveRestore(
|
|
1913
1909
|
backend,
|
|
1914
|
-
|
|
1910
|
+
fallbackBaseSettings,
|
|
1915
1911
|
workspaceArchive,
|
|
1916
1912
|
);
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
restoreSettings,
|
|
1925
|
-
environment,
|
|
1926
|
-
opts.metrics,
|
|
1927
|
-
)) as ResumeCapableClient | undefined);
|
|
1928
|
-
if (!restoreClient?.create) {
|
|
1913
|
+
await ensureModalRegistryImage(fallbackSettings);
|
|
1914
|
+
const fallbackClient = (
|
|
1915
|
+
opts.clientFactory
|
|
1916
|
+
? opts.clientFactory(backend, fallbackSettings, environment)
|
|
1917
|
+
: createSandboxClientForBackend(backend, fallbackSettings, environment, opts.metrics)
|
|
1918
|
+
) as ResumeCapableClient | undefined;
|
|
1919
|
+
if (!fallbackClient?.create) {
|
|
1929
1920
|
throw new SandboxConfigError(
|
|
1930
1921
|
backend,
|
|
1931
|
-
|
|
1922
|
+
"Modal logical-image fallback does not support fresh creation",
|
|
1932
1923
|
);
|
|
1933
1924
|
}
|
|
1934
|
-
|
|
1935
|
-
const createStarted = Date.now();
|
|
1936
|
-
let restored: Awaited<ReturnType<NonNullable<typeof restoreClient.create>>>;
|
|
1925
|
+
const fallbackStarted = Date.now();
|
|
1937
1926
|
try {
|
|
1938
|
-
restored = await
|
|
1927
|
+
restored = await fallbackClient.create({ manifest: createManifest });
|
|
1939
1928
|
recordSandboxCreateMetric(
|
|
1940
1929
|
opts.metrics,
|
|
1941
|
-
|
|
1942
|
-
|
|
1930
|
+
fallbackClient.backendId,
|
|
1931
|
+
"logical",
|
|
1943
1932
|
"completed",
|
|
1944
|
-
|
|
1933
|
+
fallbackStarted,
|
|
1945
1934
|
);
|
|
1946
|
-
|
|
1935
|
+
createdClient = fallbackClient;
|
|
1936
|
+
} catch (fallbackError) {
|
|
1947
1937
|
recordSandboxCreateMetric(
|
|
1948
1938
|
opts.metrics,
|
|
1949
|
-
|
|
1950
|
-
|
|
1939
|
+
fallbackClient.backendId,
|
|
1940
|
+
"logical",
|
|
1951
1941
|
"failed",
|
|
1952
|
-
|
|
1942
|
+
fallbackStarted,
|
|
1953
1943
|
);
|
|
1954
1944
|
recordSandboxProviderApiThrottleMetric(
|
|
1955
1945
|
opts.metrics,
|
|
1956
|
-
|
|
1946
|
+
fallbackClient.backendId,
|
|
1957
1947
|
"create",
|
|
1958
|
-
|
|
1959
|
-
);
|
|
1960
|
-
if (
|
|
1961
|
-
createImageSource !== "provider_immutable" ||
|
|
1962
|
-
backend !== "modal" ||
|
|
1963
|
-
!isProviderSandboxNotFoundError(restoreClient.backendId, error)
|
|
1964
|
-
) {
|
|
1965
|
-
throw error;
|
|
1966
|
-
}
|
|
1967
|
-
const fallbackBaseSettings =
|
|
1968
|
-
opts.logicalFallbackSettings ??
|
|
1969
|
-
(settings.modalImageRef ? { ...settings, modalImageId: undefined } : null);
|
|
1970
|
-
if (!fallbackBaseSettings) {
|
|
1971
|
-
// An ID-only logical base is supported. Without the exact pre-selection
|
|
1972
|
-
// settings, clearing the optimized ID would silently boot Modal's
|
|
1973
|
-
// default image, so fail closed instead of changing the rig base.
|
|
1974
|
-
throw error;
|
|
1975
|
-
}
|
|
1976
|
-
const fallbackSettings = settingsForWorkspaceArchiveRestore(
|
|
1977
|
-
backend,
|
|
1978
|
-
fallbackBaseSettings,
|
|
1979
|
-
workspaceArchive,
|
|
1948
|
+
fallbackError,
|
|
1980
1949
|
);
|
|
1981
|
-
|
|
1982
|
-
const fallbackClient = (
|
|
1983
|
-
opts.clientFactory
|
|
1984
|
-
? opts.clientFactory(backend, fallbackSettings, environment)
|
|
1985
|
-
: createSandboxClientForBackend(backend, fallbackSettings, environment, opts.metrics)
|
|
1986
|
-
) as ResumeCapableClient | undefined;
|
|
1987
|
-
if (!fallbackClient?.create) {
|
|
1988
|
-
throw new SandboxConfigError(
|
|
1989
|
-
backend,
|
|
1990
|
-
"Modal logical-image fallback does not support fresh creation",
|
|
1991
|
-
);
|
|
1992
|
-
}
|
|
1993
|
-
const fallbackStarted = Date.now();
|
|
1994
|
-
try {
|
|
1995
|
-
restored = await fallbackClient.create({ manifest: createManifest });
|
|
1996
|
-
recordSandboxCreateMetric(
|
|
1997
|
-
opts.metrics,
|
|
1998
|
-
fallbackClient.backendId,
|
|
1999
|
-
"logical",
|
|
2000
|
-
"completed",
|
|
2001
|
-
fallbackStarted,
|
|
2002
|
-
);
|
|
2003
|
-
createdClient = fallbackClient;
|
|
2004
|
-
} catch (fallbackError) {
|
|
2005
|
-
recordSandboxCreateMetric(
|
|
2006
|
-
opts.metrics,
|
|
2007
|
-
fallbackClient.backendId,
|
|
2008
|
-
"logical",
|
|
2009
|
-
"failed",
|
|
2010
|
-
fallbackStarted,
|
|
2011
|
-
);
|
|
2012
|
-
recordSandboxProviderApiThrottleMetric(
|
|
2013
|
-
opts.metrics,
|
|
2014
|
-
fallbackClient.backendId,
|
|
2015
|
-
"create",
|
|
2016
|
-
fallbackError,
|
|
2017
|
-
);
|
|
2018
|
-
throw fallbackError;
|
|
2019
|
-
}
|
|
1950
|
+
throw fallbackError;
|
|
2020
1951
|
}
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
1952
|
+
}
|
|
1953
|
+
let restoredState = (restored as { state?: unknown }).state;
|
|
1954
|
+
const restoredInstanceId = readInstanceId(backend, restored);
|
|
1955
|
+
if (!restoredInstanceId) {
|
|
1956
|
+
await terminateCreatedSandbox(createdClient, restored, restoredState);
|
|
1957
|
+
throw new SandboxConfigError(
|
|
1958
|
+
backend,
|
|
1959
|
+
`Sandbox backend "${backend}" created a handle without its declared provider identity`,
|
|
1960
|
+
);
|
|
1961
|
+
}
|
|
1962
|
+
let established: EstablishedSandboxSession = {
|
|
1963
|
+
client: createdClient,
|
|
1964
|
+
session: restored,
|
|
1965
|
+
sessionState: restoredState ?? resumeFallbackState,
|
|
1966
|
+
instanceId: restoredInstanceId,
|
|
1967
|
+
backendId: createdClient.backendId,
|
|
1968
|
+
};
|
|
1969
|
+
if (opts.onSandboxCreated) {
|
|
1970
|
+
try {
|
|
1971
|
+
await opts.onSandboxCreated(established);
|
|
1972
|
+
} catch (createCallbackError) {
|
|
1973
|
+
await terminateCreatedSandbox(createdClient, restored, restoredState);
|
|
1974
|
+
throw createCallbackError;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
let hydrationApplied = false;
|
|
1978
|
+
if (workspaceArchive) {
|
|
1979
|
+
const hydrate = (restored as { hydrateWorkspace?: (data: Uint8Array) => Promise<void> })
|
|
1980
|
+
.hydrateWorkspace;
|
|
1981
|
+
if (typeof hydrate !== "function") {
|
|
2024
1982
|
await terminateCreatedSandbox(createdClient, restored, restoredState);
|
|
1983
|
+
throw new WorkspaceArchiveIntegrityError(
|
|
1984
|
+
"archive_hydration_failed",
|
|
1985
|
+
`sandbox backend ${createdClient.backendId} cannot hydrate selected archive revision ${workspaceArchive.descriptor.revision}`,
|
|
1986
|
+
);
|
|
1987
|
+
}
|
|
1988
|
+
try {
|
|
1989
|
+
// hydrateWorkspace may internally replace the underlying box.
|
|
1990
|
+
await hydrate.call(restored, workspaceArchive.bytes);
|
|
1991
|
+
} catch (error) {
|
|
1992
|
+
await terminateCreatedSandbox(
|
|
1993
|
+
createdClient,
|
|
1994
|
+
restored,
|
|
1995
|
+
(restored as { state?: unknown }).state,
|
|
1996
|
+
);
|
|
1997
|
+
if (error instanceof WorkspaceArchiveIntegrityError) throw error;
|
|
1998
|
+
throw new WorkspaceArchiveIntegrityError(
|
|
1999
|
+
"archive_hydration_failed",
|
|
2000
|
+
`failed to hydrate selected workspace archive revision ${workspaceArchive.descriptor.revision}`,
|
|
2001
|
+
{ retryable: true, cause: error },
|
|
2002
|
+
);
|
|
2003
|
+
}
|
|
2004
|
+
// hydrateWorkspace may replace the provider box (Modal's native snapshot
|
|
2005
|
+
// restore does this). Attribute the newly-active identity immediately,
|
|
2006
|
+
// before restore-state marking, fingerprint verification, or any caller
|
|
2007
|
+
// can publish the box warm. The callback is the durable lease/tagging
|
|
2008
|
+
// boundary; if it cannot persist the replacement, the caller fails closed
|
|
2009
|
+
// and this exact replacement is terminated below.
|
|
2010
|
+
const hydratedState = (restored as { state?: unknown }).state;
|
|
2011
|
+
const hydratedInstanceId = readInstanceId(backend, restored);
|
|
2012
|
+
if (!hydratedInstanceId) {
|
|
2013
|
+
await terminateCreatedSandbox(createdClient, restored, hydratedState);
|
|
2025
2014
|
throw new SandboxConfigError(
|
|
2026
2015
|
backend,
|
|
2027
|
-
`Sandbox backend "${backend}"
|
|
2016
|
+
`Sandbox backend "${backend}" hydrated a handle without its declared provider identity`,
|
|
2028
2017
|
);
|
|
2029
2018
|
}
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2019
|
+
if (hydratedInstanceId !== established.instanceId) {
|
|
2020
|
+
established = {
|
|
2021
|
+
client: createdClient,
|
|
2022
|
+
session: restored,
|
|
2023
|
+
sessionState: hydratedState ?? resumeFallbackState,
|
|
2024
|
+
instanceId: hydratedInstanceId,
|
|
2025
|
+
backendId: createdClient.backendId,
|
|
2026
|
+
};
|
|
2027
|
+
if (opts.onSandboxCreated) {
|
|
2028
|
+
try {
|
|
2029
|
+
await opts.onSandboxCreated(established);
|
|
2030
|
+
} catch (createCallbackError) {
|
|
2031
|
+
await terminateCreatedSandbox(createdClient, restored, hydratedState);
|
|
2032
|
+
throw createCallbackError;
|
|
2033
|
+
}
|
|
2043
2034
|
}
|
|
2044
2035
|
}
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
`sandbox backend ${createdClient.backendId} cannot hydrate selected archive revision ${workspaceArchive.descriptor.revision}`,
|
|
2036
|
+
if (opts.onWorkspaceRestoreVerifying) {
|
|
2037
|
+
try {
|
|
2038
|
+
await opts.onWorkspaceRestoreVerifying(workspaceArchive.descriptor);
|
|
2039
|
+
} catch (error) {
|
|
2040
|
+
await terminateCreatedSandbox(
|
|
2041
|
+
createdClient,
|
|
2042
|
+
restored,
|
|
2043
|
+
(restored as { state?: unknown }).state,
|
|
2054
2044
|
);
|
|
2045
|
+
throw error;
|
|
2055
2046
|
}
|
|
2047
|
+
}
|
|
2048
|
+
// Native provider snapshots are restored by their exact opaque receipt.
|
|
2049
|
+
// OpenGeni verifies receipt identity/hash before hydration, but it must not
|
|
2050
|
+
// impose a tar/inode equivalence contract on the provider's filesystem
|
|
2051
|
+
// image. Tar archives remain content-verified after hydration.
|
|
2052
|
+
if (workspaceArchive.kind === "tar") {
|
|
2056
2053
|
try {
|
|
2057
|
-
|
|
2058
|
-
if (workspaceArchive.kind === "host_spool") {
|
|
2059
|
-
// This is the exact newly-created, unpublished destination. The lease
|
|
2060
|
-
// restore fence must exclude admitted writers until verification ends;
|
|
2061
|
-
// descriptor-relative filesystem access alone is not writer isolation.
|
|
2062
|
-
const root = (restored as { state?: { workspaceRootPath?: unknown } }).state
|
|
2063
|
-
?.workspaceRootPath;
|
|
2064
|
-
if (typeof root !== "string")
|
|
2065
|
-
throw new WorkspaceArchiveIntegrityError(
|
|
2066
|
-
"archive_hydration_failed",
|
|
2067
|
-
"Host archive restore has no exact workspace root",
|
|
2068
|
-
);
|
|
2069
|
-
await restoreHostWorkspaceArchive(root, hostSpool!, {
|
|
2070
|
-
archiveLimits: Reflect.get(restored as object, "archiveLimits"),
|
|
2071
|
-
});
|
|
2072
|
-
} else await hydrate!.call(restored, workspaceArchive.bytes);
|
|
2054
|
+
await verifyRestoredWorkspace(restored, workspaceArchive.descriptor);
|
|
2073
2055
|
} catch (error) {
|
|
2074
2056
|
await terminateCreatedSandbox(
|
|
2075
2057
|
createdClient,
|
|
@@ -2078,106 +2060,35 @@ export async function establishSandboxSessionFromEnvelope(
|
|
|
2078
2060
|
);
|
|
2079
2061
|
if (error instanceof WorkspaceArchiveIntegrityError) throw error;
|
|
2080
2062
|
throw new WorkspaceArchiveIntegrityError(
|
|
2081
|
-
"
|
|
2082
|
-
`failed to
|
|
2083
|
-
{ retryable: true
|
|
2063
|
+
"workspace_fingerprint_unavailable",
|
|
2064
|
+
`failed to verify selected workspace archive revision ${workspaceArchive.descriptor.revision}`,
|
|
2065
|
+
{ retryable: true },
|
|
2084
2066
|
);
|
|
2085
2067
|
}
|
|
2086
|
-
// hydrateWorkspace may replace the provider box (Modal's native snapshot
|
|
2087
|
-
// restore does this). Attribute the newly-active identity immediately,
|
|
2088
|
-
// before restore-state marking, fingerprint verification, or any caller
|
|
2089
|
-
// can publish the box warm. The callback is the durable lease/tagging
|
|
2090
|
-
// boundary; if it cannot persist the replacement, the caller fails closed
|
|
2091
|
-
// and this exact replacement is terminated below.
|
|
2092
|
-
const hydratedState = (restored as { state?: unknown }).state;
|
|
2093
|
-
const hydratedInstanceId = readInstanceId(backend, restored);
|
|
2094
|
-
if (!hydratedInstanceId) {
|
|
2095
|
-
await terminateCreatedSandbox(createdClient, restored, hydratedState);
|
|
2096
|
-
throw new SandboxConfigError(
|
|
2097
|
-
backend,
|
|
2098
|
-
`Sandbox backend "${backend}" hydrated a handle without its declared provider identity`,
|
|
2099
|
-
);
|
|
2100
|
-
}
|
|
2101
|
-
if (hydratedInstanceId !== established.instanceId) {
|
|
2102
|
-
established = {
|
|
2103
|
-
client: createdClient,
|
|
2104
|
-
session: restored,
|
|
2105
|
-
sessionState: hydratedState ?? resumeFallbackState,
|
|
2106
|
-
instanceId: hydratedInstanceId,
|
|
2107
|
-
backendId: createdClient.backendId,
|
|
2108
|
-
};
|
|
2109
|
-
if (opts.onSandboxCreated) {
|
|
2110
|
-
try {
|
|
2111
|
-
await opts.onSandboxCreated(established);
|
|
2112
|
-
} catch (createCallbackError) {
|
|
2113
|
-
await terminateCreatedSandbox(createdClient, restored, hydratedState);
|
|
2114
|
-
throw createCallbackError;
|
|
2115
|
-
}
|
|
2116
|
-
}
|
|
2117
|
-
}
|
|
2118
|
-
if (opts.onWorkspaceRestoreVerifying) {
|
|
2119
|
-
try {
|
|
2120
|
-
await opts.onWorkspaceRestoreVerifying(workspaceArchive.descriptor);
|
|
2121
|
-
} catch (error) {
|
|
2122
|
-
await terminateCreatedSandbox(
|
|
2123
|
-
createdClient,
|
|
2124
|
-
restored,
|
|
2125
|
-
(restored as { state?: unknown }).state,
|
|
2126
|
-
);
|
|
2127
|
-
throw error;
|
|
2128
|
-
}
|
|
2129
|
-
}
|
|
2130
|
-
// Native provider snapshots are restored by their exact opaque receipt.
|
|
2131
|
-
// OpenGeni verifies receipt identity/hash before hydration, but it must not
|
|
2132
|
-
// impose a tar/inode equivalence contract on the provider's filesystem
|
|
2133
|
-
// image. Tar archives remain content-verified after hydration.
|
|
2134
|
-
if (workspaceArchive.kind === "tar" || workspaceArchive.kind === "host_spool") {
|
|
2135
|
-
try {
|
|
2136
|
-
await verifyRestoredWorkspace(restored, workspaceArchive.descriptor);
|
|
2137
|
-
} catch (error) {
|
|
2138
|
-
await terminateCreatedSandbox(
|
|
2139
|
-
createdClient,
|
|
2140
|
-
restored,
|
|
2141
|
-
(restored as { state?: unknown }).state,
|
|
2142
|
-
);
|
|
2143
|
-
if (error instanceof WorkspaceArchiveIntegrityError) throw error;
|
|
2144
|
-
throw new WorkspaceArchiveIntegrityError(
|
|
2145
|
-
"workspace_fingerprint_unavailable",
|
|
2146
|
-
`failed to verify selected workspace archive revision ${workspaceArchive.descriptor.revision}`,
|
|
2147
|
-
{ retryable: true },
|
|
2148
|
-
);
|
|
2149
|
-
}
|
|
2150
|
-
}
|
|
2151
|
-
hydrationApplied = true;
|
|
2152
|
-
console.info(
|
|
2153
|
-
`[sandbox] cold-restore applied ${workspaceArchive.kind === "provider_snapshot" ? "native snapshot receipt" : "verified tar archive"} revision ${workspaceArchive.descriptor.revision}`,
|
|
2154
|
-
);
|
|
2155
2068
|
}
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
throw new SandboxConfigError(
|
|
2161
|
-
backend,
|
|
2162
|
-
`Sandbox backend "${backend}" returned a handle without its declared provider identity`,
|
|
2163
|
-
);
|
|
2164
|
-
}
|
|
2165
|
-
return {
|
|
2166
|
-
client: createdClient,
|
|
2167
|
-
session: restored,
|
|
2168
|
-
sessionState: restoredState ?? resumeFallbackState,
|
|
2169
|
-
instanceId: finalInstanceId,
|
|
2170
|
-
backendId: createdClient.backendId,
|
|
2171
|
-
origin: hydrationApplied ? ("restored" as const) : ("created" as const),
|
|
2172
|
-
...(workspaceArchive ? { restoredArchive: workspaceArchive.descriptor } : {}),
|
|
2173
|
-
};
|
|
2174
|
-
} finally {
|
|
2175
|
-
await hostSpool?.dispose().catch(() => {
|
|
2176
|
-
// Cleanup must not discard a successfully established provider handle
|
|
2177
|
-
// or replace the authoritative restore failure with a filesystem error.
|
|
2178
|
-
console.warn("workspace archive download spool cleanup failed");
|
|
2179
|
-
});
|
|
2069
|
+
hydrationApplied = true;
|
|
2070
|
+
console.info(
|
|
2071
|
+
`[sandbox] cold-restore applied ${workspaceArchive.kind === "provider_snapshot" ? "native snapshot receipt" : "verified tar archive"} revision ${workspaceArchive.descriptor.revision}`,
|
|
2072
|
+
);
|
|
2180
2073
|
}
|
|
2074
|
+
restoredState = (restored as { state?: unknown }).state;
|
|
2075
|
+
const finalInstanceId = readInstanceId(backend, restored);
|
|
2076
|
+
if (!finalInstanceId) {
|
|
2077
|
+
await terminateCreatedSandbox(createdClient, restored, restoredState);
|
|
2078
|
+
throw new SandboxConfigError(
|
|
2079
|
+
backend,
|
|
2080
|
+
`Sandbox backend "${backend}" returned a handle without its declared provider identity`,
|
|
2081
|
+
);
|
|
2082
|
+
}
|
|
2083
|
+
return {
|
|
2084
|
+
client: createdClient,
|
|
2085
|
+
session: restored,
|
|
2086
|
+
sessionState: restoredState ?? resumeFallbackState,
|
|
2087
|
+
instanceId: finalInstanceId,
|
|
2088
|
+
backendId: createdClient.backendId,
|
|
2089
|
+
origin: hydrationApplied ? ("restored" as const) : ("created" as const),
|
|
2090
|
+
...(workspaceArchive ? { restoredArchive: workspaceArchive.descriptor } : {}),
|
|
2091
|
+
};
|
|
2181
2092
|
};
|
|
2182
2093
|
|
|
2183
2094
|
// Does the envelope carry a RESUMABLE box id (warm reattach), or only a
|
|
@@ -14,8 +14,6 @@ import {
|
|
|
14
14
|
type WorkspaceTreeFingerprint,
|
|
15
15
|
} from "@opengeni/contracts";
|
|
16
16
|
import { withSandboxProviderCapture } from "./provider-operation-gate";
|
|
17
|
-
import type { VerifiedHostWorkspaceArchive } from "./archive-spool";
|
|
18
|
-
import { captureHostWorkspaceArchive, fingerprintHostWorkspace } from "./host-archive-spool";
|
|
19
17
|
|
|
20
18
|
export {
|
|
21
19
|
WORKSPACE_ARCHIVE_DESCRIPTOR_VERSION,
|
|
@@ -38,55 +36,6 @@ export type VerifiedWorkspaceArchive = {
|
|
|
38
36
|
nativeSnapshot?: NativeSnapshotRef;
|
|
39
37
|
};
|
|
40
38
|
|
|
41
|
-
export type VerifiedWorkspaceArchivePayload =
|
|
42
|
-
| VerifiedWorkspaceArchive
|
|
43
|
-
| VerifiedHostWorkspaceArchive;
|
|
44
|
-
|
|
45
|
-
/** Host-backed portable archives can go directly to object storage without an
|
|
46
|
-
* intermediate whole-document string or buffer. Existing inline/provider APIs
|
|
47
|
-
* retain their compatibility path when disk-backed publication is unavailable. */
|
|
48
|
-
export async function captureWorkspaceArchiveForStorage(
|
|
49
|
-
session: unknown,
|
|
50
|
-
capturedAtMs: number,
|
|
51
|
-
options: WorkspaceArchiveCaptureOptions,
|
|
52
|
-
objectStorageAvailable: boolean,
|
|
53
|
-
): Promise<VerifiedWorkspaceArchivePayload> {
|
|
54
|
-
const target = session as WorkspaceSession;
|
|
55
|
-
const root = hostBackedWorkspaceRoot(target);
|
|
56
|
-
if (
|
|
57
|
-
process.platform !== "linux" ||
|
|
58
|
-
!root ||
|
|
59
|
-
!objectStorageAvailable ||
|
|
60
|
-
options.strategy === "portable_tar"
|
|
61
|
-
) {
|
|
62
|
-
return await captureVerifiedWorkspaceArchive(session, capturedAtMs, options);
|
|
63
|
-
}
|
|
64
|
-
return await withSandboxProviderCapture(session, async () => {
|
|
65
|
-
const { spool, workspace } = await captureHostWorkspaceArchive(
|
|
66
|
-
root,
|
|
67
|
-
workspaceFingerprintExcludes(target),
|
|
68
|
-
);
|
|
69
|
-
return {
|
|
70
|
-
kind: "host_spool" as const,
|
|
71
|
-
spool,
|
|
72
|
-
descriptor: {
|
|
73
|
-
version: 1 as const,
|
|
74
|
-
revision: `wa1:${String(capturedAtMs).padStart(13, "0")}:${spool.sha256}`,
|
|
75
|
-
archiveSha256: spool.sha256,
|
|
76
|
-
archiveBytes: spool.byteSize,
|
|
77
|
-
capturedAt: new Date(capturedAtMs).toISOString(),
|
|
78
|
-
workspace,
|
|
79
|
-
},
|
|
80
|
-
};
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export async function disposeWorkspaceArchive(
|
|
85
|
-
archive: VerifiedWorkspaceArchivePayload | undefined,
|
|
86
|
-
): Promise<void> {
|
|
87
|
-
if (archive?.kind === "host_spool") await archive.spool.dispose();
|
|
88
|
-
}
|
|
89
|
-
|
|
90
39
|
export type WorkspaceArchiveIntegrityCode =
|
|
91
40
|
| "archive_metadata_missing"
|
|
92
41
|
| "archive_metadata_invalid"
|
|
@@ -840,9 +789,7 @@ export async function verifyRestoredWorkspace(
|
|
|
840
789
|
"selected host-backed workspace archive was restored by a non-host-backed provider",
|
|
841
790
|
);
|
|
842
791
|
}
|
|
843
|
-
return
|
|
844
|
-
? fingerprintHostWorkspace(root, workspaceFingerprintExcludes(target))
|
|
845
|
-
: fingerprintHostBackedWorkspace(root, workspaceFingerprintExcludes(target));
|
|
792
|
+
return fingerprintHostBackedWorkspace(root, workspaceFingerprintExcludes(target));
|
|
846
793
|
})()
|
|
847
794
|
: await fingerprintRemoteWorkspace(target);
|
|
848
795
|
if (!fingerprintsEqual(actual, descriptor.workspace)) {
|