@opengeni/runtime 2.5.1-canary.0 → 2.5.2-canary.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.
@@ -24,6 +24,15 @@ 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
+
27
36
  import {
28
37
  BROWSER_CONTROL_PORT,
29
38
  DESKTOP_STREAM_PORT,
@@ -31,6 +40,9 @@ import {
31
40
  TERMINAL_STREAM_PORT,
32
41
  omitInlineWorkspaceArchiveWhenObjectRefPresent,
33
42
  parseWorkspaceArchiveObjectRef,
43
+ parseWorkspaceArchiveDescriptor,
44
+ type WorkspaceArchiveObjectRef,
45
+ type TarWorkspaceArchiveDescriptor,
34
46
  type SandboxBackend,
35
47
  type SandboxProviderContinuityRecovery,
36
48
  } from "@opengeni/contracts";
@@ -1676,9 +1688,16 @@ async function terminateCreatedSandbox(
1676
1688
  }
1677
1689
 
1678
1690
  type ModalNativeWorkspacePersistence = "snapshot_filesystem" | "snapshot_directory";
1691
+ type RestoreWorkspaceArchive =
1692
+ | VerifiedWorkspaceArchive
1693
+ | {
1694
+ kind: "host_spool";
1695
+ descriptor: TarWorkspaceArchiveDescriptor;
1696
+ ref: WorkspaceArchiveObjectRef;
1697
+ };
1679
1698
 
1680
1699
  function modalWorkspacePersistenceForRestore(
1681
- archive: VerifiedWorkspaceArchive | null,
1700
+ archive: RestoreWorkspaceArchive | null,
1682
1701
  ): ModalNativeWorkspacePersistence | null {
1683
1702
  if (archive?.kind !== "provider_snapshot") return null;
1684
1703
  const snapshot = archive.nativeSnapshot;
@@ -1707,7 +1726,7 @@ function modalWorkspacePersistenceForRestore(
1707
1726
  function settingsForWorkspaceArchiveRestore(
1708
1727
  backend: SandboxBackend,
1709
1728
  settings: Settings,
1710
- archive: VerifiedWorkspaceArchive | null,
1729
+ archive: RestoreWorkspaceArchive | null,
1711
1730
  ): Settings {
1712
1731
  if (backend !== "modal") return settings;
1713
1732
  const workspacePersistence = modalWorkspacePersistenceForRestore(archive);
@@ -1735,6 +1754,7 @@ export async function establishSandboxSessionFromEnvelope(
1735
1754
  opts: {
1736
1755
  sessionId: string;
1737
1756
  recovery: "create-or-restore" | "resume-only";
1757
+ loadHostWorkspaceArchive?: (ref: WorkspaceArchiveObjectRef) => Promise<WorkspaceArchiveSpool>;
1738
1758
  backendOverride?: SandboxBackend;
1739
1759
  environment?: Record<string, string>;
1740
1760
  onSandboxCreated?: SandboxCreatedCallback;
@@ -1817,6 +1837,7 @@ export async function establishSandboxSessionFromEnvelope(
1817
1837
  envelopeSessionState && typeof envelopeSessionState === "object"
1818
1838
  ? (envelopeSessionState as {
1819
1839
  workspaceArchive?: unknown;
1840
+ workspaceArchiveRef?: unknown;
1820
1841
  workspaceArchiveMeta?: unknown;
1821
1842
  })
1822
1843
  : undefined;
@@ -1838,220 +1859,217 @@ export async function establishSandboxSessionFromEnvelope(
1838
1859
  // Parse/verify lazily: a warm resume-by-id does not consume its retained
1839
1860
  // archive, so a legacy live box remains resumable. Creation/restoration is
1840
1861
  // the boundary that requires complete durable metadata.
1841
- const workspaceArchive: VerifiedWorkspaceArchive | null = readVerifiedWorkspaceArchive(
1862
+ let workspaceArchive: RestoreWorkspaceArchive | null = readVerifiedWorkspaceArchive(
1842
1863
  workspaceArchiveBase64,
1843
1864
  workspaceArchiveMetadata,
1844
1865
  );
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`,
1864
- );
1865
- }
1866
- let createdClient = restoreClient;
1867
- const createStarted = Date.now();
1868
- let restored: Awaited<ReturnType<NonNullable<typeof restoreClient.create>>>;
1869
- try {
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
- );
1866
+ const objectRef = parseWorkspaceArchiveObjectRef(archiveState?.workspaceArchiveRef);
1867
+ const objectDescriptor = parseWorkspaceArchiveDescriptor(workspaceArchiveMetadata);
1868
+ if (
1869
+ process.platform === "linux" &&
1870
+ objectRef &&
1871
+ objectDescriptor?.version === 1 &&
1872
+ objectDescriptor.workspace.projection === "sdk_local_archive_v1" &&
1873
+ opts.loadHostWorkspaceArchive
1874
+ ) {
1892
1875
  if (
1893
- createImageSource !== "provider_immutable" ||
1894
- backend !== "modal" ||
1895
- !isProviderSandboxNotFoundError(restoreClient.backendId, error)
1876
+ (backend !== "local" && backend !== "docker") ||
1877
+ objectRef.sha256 !== objectDescriptor.archiveSha256 ||
1878
+ objectRef.bytes !== objectDescriptor.archiveBytes
1896
1879
  ) {
1897
- throw error;
1880
+ throw new WorkspaceArchiveIntegrityError(
1881
+ "archive_metadata_invalid",
1882
+ "Host archive object reference does not match its provider and descriptor",
1883
+ );
1898
1884
  }
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;
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",
1891
+ );
1892
+ }
1893
+ let hostSpool: WorkspaceArchiveSpool | undefined;
1894
+ try {
1895
+ // Preserve the pre-create integrity boundary used by inline archives.
1896
+ // A missing/corrupt object must not create a provider just to delete it.
1897
+ if (workspaceArchive?.kind === "host_spool") {
1898
+ hostSpool = await opts.loadHostWorkspaceArchive!(workspaceArchive.ref);
1899
+ if (
1900
+ hostSpool.sha256 !== workspaceArchive.descriptor.archiveSha256 ||
1901
+ hostSpool.byteSize !== workspaceArchive.descriptor.archiveBytes
1902
+ ) {
1903
+ throw new WorkspaceArchiveIntegrityError(
1904
+ "archive_hash_mismatch",
1905
+ "Downloaded workspace archive does not match its descriptor",
1906
+ );
1907
+ }
1907
1908
  }
1908
- const fallbackSettings = settingsForWorkspaceArchiveRestore(
1909
+ // The selected native artifact is the durable authority for its restore
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(
1909
1913
  backend,
1910
- fallbackBaseSettings,
1914
+ settings,
1911
1915
  workspaceArchive,
1912
1916
  );
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) {
1917
+ const restoreClient =
1918
+ restoreSettings === settings
1919
+ ? client
1920
+ : ((opts.clientFactory
1921
+ ? opts.clientFactory(backend, restoreSettings, environment)
1922
+ : createSandboxClientForBackend(
1923
+ backend,
1924
+ restoreSettings,
1925
+ environment,
1926
+ opts.metrics,
1927
+ )) as ResumeCapableClient | undefined);
1928
+ if (!restoreClient?.create) {
1920
1929
  throw new SandboxConfigError(
1921
1930
  backend,
1922
- "Modal logical-image fallback does not support fresh creation",
1931
+ `Sandbox backend "${backend}" does not support fresh archive restoration`,
1923
1932
  );
1924
1933
  }
1925
- const fallbackStarted = Date.now();
1934
+ let createdClient = restoreClient;
1935
+ const createStarted = Date.now();
1936
+ let restored: Awaited<ReturnType<NonNullable<typeof restoreClient.create>>>;
1926
1937
  try {
1927
- restored = await fallbackClient.create({ manifest: createManifest });
1938
+ restored = await restoreClient.create({ manifest: createManifest });
1928
1939
  recordSandboxCreateMetric(
1929
1940
  opts.metrics,
1930
- fallbackClient.backendId,
1931
- "logical",
1941
+ restoreClient.backendId,
1942
+ createImageSource,
1932
1943
  "completed",
1933
- fallbackStarted,
1944
+ createStarted,
1934
1945
  );
1935
- createdClient = fallbackClient;
1936
- } catch (fallbackError) {
1946
+ } catch (error) {
1937
1947
  recordSandboxCreateMetric(
1938
1948
  opts.metrics,
1939
- fallbackClient.backendId,
1940
- "logical",
1949
+ restoreClient.backendId,
1950
+ createImageSource,
1941
1951
  "failed",
1942
- fallbackStarted,
1952
+ createStarted,
1943
1953
  );
1944
1954
  recordSandboxProviderApiThrottleMetric(
1945
1955
  opts.metrics,
1946
- fallbackClient.backendId,
1956
+ restoreClient.backendId,
1947
1957
  "create",
1948
- fallbackError,
1958
+ error,
1949
1959
  );
1950
- throw fallbackError;
1951
- }
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") {
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 },
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,
2002
1980
  );
1981
+ await ensureModalRegistryImage(fallbackSettings);
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
+ }
2003
2020
  }
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);
2021
+ let restoredState = (restored as { state?: unknown }).state;
2022
+ const restoredInstanceId = readInstanceId(backend, restored);
2023
+ if (!restoredInstanceId) {
2024
+ await terminateCreatedSandbox(createdClient, restored, restoredState);
2014
2025
  throw new SandboxConfigError(
2015
2026
  backend,
2016
- `Sandbox backend "${backend}" hydrated a handle without its declared provider identity`,
2027
+ `Sandbox backend "${backend}" created a handle without its declared provider identity`,
2017
2028
  );
2018
2029
  }
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
- }
2030
+ let established: EstablishedSandboxSession = {
2031
+ client: createdClient,
2032
+ session: restored,
2033
+ sessionState: restoredState ?? resumeFallbackState,
2034
+ instanceId: restoredInstanceId,
2035
+ backendId: createdClient.backendId,
2036
+ };
2037
+ if (opts.onSandboxCreated) {
2038
+ try {
2039
+ await opts.onSandboxCreated(established);
2040
+ } catch (createCallbackError) {
2041
+ await terminateCreatedSandbox(createdClient, restored, restoredState);
2042
+ throw createCallbackError;
2034
2043
  }
2035
2044
  }
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,
2045
+ let hydrationApplied = false;
2046
+ if (workspaceArchive) {
2047
+ const hydrate = (restored as { hydrateWorkspace?: (data: Uint8Array) => Promise<void> })
2048
+ .hydrateWorkspace;
2049
+ if (workspaceArchive.kind !== "host_spool" && typeof hydrate !== "function") {
2050
+ await terminateCreatedSandbox(createdClient, restored, restoredState);
2051
+ throw new WorkspaceArchiveIntegrityError(
2052
+ "archive_hydration_failed",
2053
+ `sandbox backend ${createdClient.backendId} cannot hydrate selected archive revision ${workspaceArchive.descriptor.revision}`,
2044
2054
  );
2045
- throw error;
2046
2055
  }
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") {
2053
2056
  try {
2054
- await verifyRestoredWorkspace(restored, workspaceArchive.descriptor);
2057
+ // hydrateWorkspace may internally replace the underlying box.
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);
2055
2073
  } catch (error) {
2056
2074
  await terminateCreatedSandbox(
2057
2075
  createdClient,
@@ -2060,35 +2078,106 @@ export async function establishSandboxSessionFromEnvelope(
2060
2078
  );
2061
2079
  if (error instanceof WorkspaceArchiveIntegrityError) throw error;
2062
2080
  throw new WorkspaceArchiveIntegrityError(
2063
- "workspace_fingerprint_unavailable",
2064
- `failed to verify selected workspace archive revision ${workspaceArchive.descriptor.revision}`,
2065
- { retryable: true },
2081
+ "archive_hydration_failed",
2082
+ `failed to hydrate selected workspace archive revision ${workspaceArchive.descriptor.revision}`,
2083
+ { retryable: true, cause: error },
2066
2084
  );
2067
2085
  }
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
+ );
2068
2155
  }
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
- );
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
- );
2156
+ restoredState = (restored as { state?: unknown }).state;
2157
+ const finalInstanceId = readInstanceId(backend, restored);
2158
+ if (!finalInstanceId) {
2159
+ await terminateCreatedSandbox(createdClient, restored, restoredState);
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
+ });
2082
2180
  }
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
- };
2092
2181
  };
2093
2182
 
2094
2183
  // Does the envelope carry a RESUMABLE box id (warm reattach), or only a
@@ -14,6 +14,8 @@ 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";
17
19
 
18
20
  export {
19
21
  WORKSPACE_ARCHIVE_DESCRIPTOR_VERSION,
@@ -36,6 +38,55 @@ export type VerifiedWorkspaceArchive = {
36
38
  nativeSnapshot?: NativeSnapshotRef;
37
39
  };
38
40
 
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
+
39
90
  export type WorkspaceArchiveIntegrityCode =
40
91
  | "archive_metadata_missing"
41
92
  | "archive_metadata_invalid"
@@ -789,7 +840,9 @@ export async function verifyRestoredWorkspace(
789
840
  "selected host-backed workspace archive was restored by a non-host-backed provider",
790
841
  );
791
842
  }
792
- return fingerprintHostBackedWorkspace(root, workspaceFingerprintExcludes(target));
843
+ return process.platform === "linux"
844
+ ? fingerprintHostWorkspace(root, workspaceFingerprintExcludes(target))
845
+ : fingerprintHostBackedWorkspace(root, workspaceFingerprintExcludes(target));
793
846
  })()
794
847
  : await fingerprintRemoteWorkspace(target);
795
848
  if (!fingerprintsEqual(actual, descriptor.workspace)) {