@alchemy.run/daytona 0.0.0 → 2.0.0-beta.86

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 (274) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +76 -4
  3. package/lib/ApiKey.d.ts +99 -0
  4. package/lib/ApiKey.d.ts.map +1 -0
  5. package/lib/ApiKey.js +152 -0
  6. package/lib/ApiKey.js.map +1 -0
  7. package/lib/AuthProvider.d.ts +55 -0
  8. package/lib/AuthProvider.d.ts.map +1 -0
  9. package/lib/AuthProvider.js +280 -0
  10. package/lib/AuthProvider.js.map +1 -0
  11. package/lib/Bootstrap.d.ts +8 -0
  12. package/lib/Bootstrap.d.ts.map +1 -0
  13. package/lib/Bootstrap.js +28 -0
  14. package/lib/Bootstrap.js.map +1 -0
  15. package/lib/ConnectSandbox.d.ts +72 -0
  16. package/lib/ConnectSandbox.d.ts.map +1 -0
  17. package/lib/ConnectSandbox.js +3 -0
  18. package/lib/ConnectSandbox.js.map +1 -0
  19. package/lib/ConnectSandboxHttp.d.ts +5 -0
  20. package/lib/ConnectSandboxHttp.d.ts.map +1 -0
  21. package/lib/ConnectSandboxHttp.js +22 -0
  22. package/lib/ConnectSandboxHttp.js.map +1 -0
  23. package/lib/Contributions.d.ts +7 -0
  24. package/lib/Contributions.d.ts.map +1 -0
  25. package/lib/Contributions.js +6 -0
  26. package/lib/Contributions.js.map +1 -0
  27. package/lib/CreateSandbox.d.ts +86 -0
  28. package/lib/CreateSandbox.d.ts.map +1 -0
  29. package/lib/CreateSandbox.js +3 -0
  30. package/lib/CreateSandbox.js.map +1 -0
  31. package/lib/CreateSandboxHttp.d.ts +5 -0
  32. package/lib/CreateSandboxHttp.d.ts.map +1 -0
  33. package/lib/CreateSandboxHttp.js +65 -0
  34. package/lib/CreateSandboxHttp.js.map +1 -0
  35. package/lib/CreateSecret.d.ts +56 -0
  36. package/lib/CreateSecret.d.ts.map +1 -0
  37. package/lib/CreateSecret.js +3 -0
  38. package/lib/CreateSecret.js.map +1 -0
  39. package/lib/CreateSecretHttp.d.ts +5 -0
  40. package/lib/CreateSecretHttp.d.ts.map +1 -0
  41. package/lib/CreateSecretHttp.js +39 -0
  42. package/lib/CreateSecretHttp.js.map +1 -0
  43. package/lib/CreateSnapshot.d.ts +80 -0
  44. package/lib/CreateSnapshot.d.ts.map +1 -0
  45. package/lib/CreateSnapshot.js +3 -0
  46. package/lib/CreateSnapshot.js.map +1 -0
  47. package/lib/CreateSnapshotHttp.d.ts +5 -0
  48. package/lib/CreateSnapshotHttp.d.ts.map +1 -0
  49. package/lib/CreateSnapshotHttp.js +47 -0
  50. package/lib/CreateSnapshotHttp.js.map +1 -0
  51. package/lib/CreateVolume.d.ts +81 -0
  52. package/lib/CreateVolume.d.ts.map +1 -0
  53. package/lib/CreateVolume.js +10 -0
  54. package/lib/CreateVolume.js.map +1 -0
  55. package/lib/CreateVolumeHttp.d.ts +5 -0
  56. package/lib/CreateVolumeHttp.d.ts.map +1 -0
  57. package/lib/CreateVolumeHttp.js +31 -0
  58. package/lib/CreateVolumeHttp.js.map +1 -0
  59. package/lib/Credentials.d.ts +9 -0
  60. package/lib/Credentials.d.ts.map +1 -0
  61. package/lib/Credentials.js +18 -0
  62. package/lib/Credentials.js.map +1 -0
  63. package/lib/DaytonaBindings.d.ts +51 -0
  64. package/lib/DaytonaBindings.d.ts.map +1 -0
  65. package/lib/DaytonaBindings.js +61 -0
  66. package/lib/DaytonaBindings.js.map +1 -0
  67. package/lib/IdentityProvider.d.ts +80 -0
  68. package/lib/IdentityProvider.d.ts.map +1 -0
  69. package/lib/IdentityProvider.js +111 -0
  70. package/lib/IdentityProvider.js.map +1 -0
  71. package/lib/MountVolume.d.ts +86 -0
  72. package/lib/MountVolume.d.ts.map +1 -0
  73. package/lib/MountVolume.js +27 -0
  74. package/lib/MountVolume.js.map +1 -0
  75. package/lib/OAuthClient.d.ts +14 -0
  76. package/lib/OAuthClient.d.ts.map +1 -0
  77. package/lib/OAuthClient.js +23 -0
  78. package/lib/OAuthClient.js.map +1 -0
  79. package/lib/Organization.d.ts +40 -0
  80. package/lib/Organization.d.ts.map +1 -0
  81. package/lib/Organization.js +80 -0
  82. package/lib/Organization.js.map +1 -0
  83. package/lib/OrganizationInvitation.d.ts +70 -0
  84. package/lib/OrganizationInvitation.d.ts.map +1 -0
  85. package/lib/OrganizationInvitation.js +109 -0
  86. package/lib/OrganizationInvitation.js.map +1 -0
  87. package/lib/OrganizationMember.d.ts +86 -0
  88. package/lib/OrganizationMember.d.ts.map +1 -0
  89. package/lib/OrganizationMember.js +109 -0
  90. package/lib/OrganizationMember.js.map +1 -0
  91. package/lib/OrganizationOtelConfig.d.ts +59 -0
  92. package/lib/OrganizationOtelConfig.d.ts.map +1 -0
  93. package/lib/OrganizationOtelConfig.js +87 -0
  94. package/lib/OrganizationOtelConfig.js.map +1 -0
  95. package/lib/OrganizationRole.d.ts +66 -0
  96. package/lib/OrganizationRole.d.ts.map +1 -0
  97. package/lib/OrganizationRole.js +106 -0
  98. package/lib/OrganizationRole.js.map +1 -0
  99. package/lib/OrganizationSettings.d.ts +76 -0
  100. package/lib/OrganizationSettings.d.ts.map +1 -0
  101. package/lib/OrganizationSettings.js +107 -0
  102. package/lib/OrganizationSettings.js.map +1 -0
  103. package/lib/Providers.d.ts +14 -0
  104. package/lib/Providers.d.ts.map +1 -0
  105. package/lib/Providers.js +53 -0
  106. package/lib/Providers.js.map +1 -0
  107. package/lib/ReadAuditLogs.d.ts +30 -0
  108. package/lib/ReadAuditLogs.d.ts.map +1 -0
  109. package/lib/ReadAuditLogs.js +17 -0
  110. package/lib/ReadAuditLogs.js.map +1 -0
  111. package/lib/ReadOrganization.d.ts +39 -0
  112. package/lib/ReadOrganization.d.ts.map +1 -0
  113. package/lib/ReadOrganization.js +39 -0
  114. package/lib/ReadOrganization.js.map +1 -0
  115. package/lib/ReadUsage.d.ts +31 -0
  116. package/lib/ReadUsage.d.ts.map +1 -0
  117. package/lib/ReadUsage.js +17 -0
  118. package/lib/ReadUsage.js.map +1 -0
  119. package/lib/Region.d.ts +78 -0
  120. package/lib/Region.d.ts.map +1 -0
  121. package/lib/Region.js +169 -0
  122. package/lib/Region.js.map +1 -0
  123. package/lib/Registry.d.ts +84 -0
  124. package/lib/Registry.d.ts.map +1 -0
  125. package/lib/Registry.js +97 -0
  126. package/lib/Registry.js.map +1 -0
  127. package/lib/Runner.d.ts +66 -0
  128. package/lib/Runner.d.ts.map +1 -0
  129. package/lib/Runner.js +116 -0
  130. package/lib/Runner.js.map +1 -0
  131. package/lib/Sandbox.d.ts +376 -0
  132. package/lib/Sandbox.d.ts.map +1 -0
  133. package/lib/Sandbox.js +565 -0
  134. package/lib/Sandbox.js.map +1 -0
  135. package/lib/SandboxClient.d.ts +258 -0
  136. package/lib/SandboxClient.d.ts.map +1 -0
  137. package/lib/SandboxClient.js +233 -0
  138. package/lib/SandboxClient.js.map +1 -0
  139. package/lib/Secret.d.ts +93 -0
  140. package/lib/Secret.d.ts.map +1 -0
  141. package/lib/Secret.js +130 -0
  142. package/lib/Secret.js.map +1 -0
  143. package/lib/Snapshot.d.ts +327 -0
  144. package/lib/Snapshot.d.ts.map +1 -0
  145. package/lib/Snapshot.js +447 -0
  146. package/lib/Snapshot.js.map +1 -0
  147. package/lib/Volume.d.ts +99 -0
  148. package/lib/Volume.d.ts.map +1 -0
  149. package/lib/Volume.js +152 -0
  150. package/lib/Volume.js.map +1 -0
  151. package/lib/WarmPool.d.ts +61 -0
  152. package/lib/WarmPool.d.ts.map +1 -0
  153. package/lib/WarmPool.js +81 -0
  154. package/lib/WarmPool.js.map +1 -0
  155. package/lib/Webhooks.d.ts +58 -0
  156. package/lib/Webhooks.d.ts.map +1 -0
  157. package/lib/Webhooks.js +75 -0
  158. package/lib/Webhooks.js.map +1 -0
  159. package/lib/index.d.ts +37 -0
  160. package/lib/index.d.ts.map +1 -0
  161. package/lib/index.js +36 -0
  162. package/lib/index.js.map +1 -0
  163. package/lib/internal/BuildContext.d.ts +16 -0
  164. package/lib/internal/BuildContext.d.ts.map +1 -0
  165. package/lib/internal/BuildContext.js +50 -0
  166. package/lib/internal/BuildContext.js.map +1 -0
  167. package/lib/internal/Declared.d.ts +29 -0
  168. package/lib/internal/Declared.d.ts.map +1 -0
  169. package/lib/internal/Declared.js +31 -0
  170. package/lib/internal/Declared.js.map +1 -0
  171. package/lib/internal/Derived.d.ts +40 -0
  172. package/lib/internal/Derived.d.ts.map +1 -0
  173. package/lib/internal/Derived.js +64 -0
  174. package/lib/internal/Derived.js.map +1 -0
  175. package/lib/internal/Errors.d.ts +4 -0
  176. package/lib/internal/Errors.d.ts.map +1 -0
  177. package/lib/internal/Errors.js +1 -0
  178. package/lib/internal/Errors.js.map +1 -0
  179. package/lib/internal/Labels.d.ts +20 -0
  180. package/lib/internal/Labels.d.ts.map +1 -0
  181. package/lib/internal/Labels.js +22 -0
  182. package/lib/internal/Labels.js.map +1 -0
  183. package/lib/internal/Names.d.ts +18 -0
  184. package/lib/internal/Names.d.ts.map +1 -0
  185. package/lib/internal/Names.js +25 -0
  186. package/lib/internal/Names.js.map +1 -0
  187. package/lib/internal/Organization.d.ts +27 -0
  188. package/lib/internal/Organization.d.ts.map +1 -0
  189. package/lib/internal/Organization.js +32 -0
  190. package/lib/internal/Organization.js.map +1 -0
  191. package/lib/internal/Process.d.ts +14 -0
  192. package/lib/internal/Process.d.ts.map +1 -0
  193. package/lib/internal/Process.js +15 -0
  194. package/lib/internal/Process.js.map +1 -0
  195. package/lib/internal/Program.d.ts +83 -0
  196. package/lib/internal/Program.d.ts.map +1 -0
  197. package/lib/internal/Program.js +169 -0
  198. package/lib/internal/Program.js.map +1 -0
  199. package/lib/internal/Ref.d.ts +14 -0
  200. package/lib/internal/Ref.d.ts.map +1 -0
  201. package/lib/internal/Ref.js +7 -0
  202. package/lib/internal/Ref.js.map +1 -0
  203. package/lib/internal/Scoped.d.ts +31 -0
  204. package/lib/internal/Scoped.d.ts.map +1 -0
  205. package/lib/internal/Scoped.js +48 -0
  206. package/lib/internal/Scoped.js.map +1 -0
  207. package/lib/internal/Tar.d.ts +13 -0
  208. package/lib/internal/Tar.d.ts.map +1 -0
  209. package/lib/internal/Tar.js +62 -0
  210. package/lib/internal/Tar.js.map +1 -0
  211. package/lib/internal/Toolbox.d.ts +11 -0
  212. package/lib/internal/Toolbox.d.ts.map +1 -0
  213. package/lib/internal/Toolbox.js +20 -0
  214. package/lib/internal/Toolbox.js.map +1 -0
  215. package/lib/internal/Wait.d.ts +49 -0
  216. package/lib/internal/Wait.d.ts.map +1 -0
  217. package/lib/internal/Wait.js +57 -0
  218. package/lib/internal/Wait.js.map +1 -0
  219. package/package.json +64 -3
  220. package/src/ApiKey.ts +221 -0
  221. package/src/AuthProvider.ts +406 -0
  222. package/src/Bootstrap.ts +55 -0
  223. package/src/ConnectSandbox.ts +76 -0
  224. package/src/ConnectSandboxHttp.ts +26 -0
  225. package/src/Contributions.ts +9 -0
  226. package/src/CreateSandbox.ts +107 -0
  227. package/src/CreateSandboxHttp.ts +97 -0
  228. package/src/CreateSecret.ts +73 -0
  229. package/src/CreateSecretHttp.ts +58 -0
  230. package/src/CreateSnapshot.ts +133 -0
  231. package/src/CreateSnapshotHttp.ts +77 -0
  232. package/src/CreateVolume.ts +104 -0
  233. package/src/CreateVolumeHttp.ts +50 -0
  234. package/src/Credentials.ts +37 -0
  235. package/src/DaytonaBindings.ts +111 -0
  236. package/src/IdentityProvider.ts +181 -0
  237. package/src/MountVolume.ts +122 -0
  238. package/src/OAuthClient.ts +27 -0
  239. package/src/Organization.ts +121 -0
  240. package/src/OrganizationInvitation.ts +174 -0
  241. package/src/OrganizationMember.ts +174 -0
  242. package/src/OrganizationOtelConfig.ts +138 -0
  243. package/src/OrganizationRole.ts +167 -0
  244. package/src/OrganizationSettings.ts +187 -0
  245. package/src/Providers.ts +93 -0
  246. package/src/ReadAuditLogs.ts +50 -0
  247. package/src/ReadOrganization.ts +83 -0
  248. package/src/ReadUsage.ts +51 -0
  249. package/src/Region.ts +226 -0
  250. package/src/Registry.ts +164 -0
  251. package/src/Runner.ts +166 -0
  252. package/src/Sandbox.ts +927 -0
  253. package/src/SandboxClient.ts +625 -0
  254. package/src/Secret.ts +203 -0
  255. package/src/Snapshot.ts +685 -0
  256. package/src/Volume.ts +229 -0
  257. package/src/WarmPool.ts +128 -0
  258. package/src/Webhooks.ts +121 -0
  259. package/src/globals.d.ts +2 -0
  260. package/src/index.ts +46 -0
  261. package/src/internal/BuildContext.ts +62 -0
  262. package/src/internal/Declared.ts +80 -0
  263. package/src/internal/Derived.ts +97 -0
  264. package/src/internal/Errors.ts +4 -0
  265. package/src/internal/Labels.ts +39 -0
  266. package/src/internal/Names.ts +37 -0
  267. package/src/internal/Organization.ts +40 -0
  268. package/src/internal/Process.ts +25 -0
  269. package/src/internal/Program.ts +239 -0
  270. package/src/internal/Ref.ts +15 -0
  271. package/src/internal/Scoped.ts +102 -0
  272. package/src/internal/Tar.ts +72 -0
  273. package/src/internal/Toolbox.ts +27 -0
  274. package/src/internal/Wait.ts +105 -0
package/src/Sandbox.ts ADDED
@@ -0,0 +1,927 @@
1
+ import * as sandboxes from "@distilled.cloud/daytona/sandbox";
2
+ import { Unowned } from "alchemy/AdoptPolicy";
3
+ import type * as Bundle from "alchemy/Bundle/Bundle";
4
+ import { deepEqual, isResolved } from "alchemy/Diff";
5
+ import type { MountCapability } from "alchemy/Mount";
6
+ import { Platform, type Main, type PlatformProps } from "alchemy/Platform";
7
+ import * as Provider from "alchemy/Provider";
8
+ import type { Resource, ResourceBinding, LookupMethod } from "alchemy/Resource";
9
+ import {
10
+ createHostRuntimeContext,
11
+ type HostRuntimeContext,
12
+ type ServerHost,
13
+ } from "alchemy/Runtime/Process";
14
+ import { Stack } from "alchemy/Stack";
15
+ import type * as Duration from "effect/Duration";
16
+ import * as Effect from "effect/Effect";
17
+ import * as Stream from "effect/Stream";
18
+ import {
19
+ capabilitiesOf,
20
+ checkSupport,
21
+ ensureDerivedSnapshot,
22
+ installLinesOf,
23
+ isMountCapability,
24
+ } from "./internal/Derived.ts";
25
+ import {
26
+ ALCHEMY_LABEL_PREFIX,
27
+ createInternalLabels,
28
+ definedLabels,
29
+ hasAlchemyLabels,
30
+ userLabels,
31
+ } from "./internal/Labels.ts";
32
+ import { resolveName } from "./internal/Names.ts";
33
+ import {
34
+ bundleProgram,
35
+ DEFAULT_PORT,
36
+ deployProgram,
37
+ isProgramRunning,
38
+ type ProgramState,
39
+ } from "./internal/Program.ts";
40
+ import { resolved, type Ref } from "./internal/Ref.ts";
41
+ import { toMinutes, waitForState, waitUntilGone } from "./internal/Wait.ts";
42
+ import type { Providers } from "./Providers.ts";
43
+ import { deleteSnapshotByName } from "./Snapshot.ts";
44
+
45
+ /** A snapshot, by name. Accepts a `Daytona.Snapshot` or `{ name }`. */
46
+ export type SandboxSnapshot = { readonly name: string };
47
+ /** A volume, by id. Accepts a `Daytona.Volume` or `{ volumeId }`. */
48
+ export type SandboxVolumeRef = { readonly volumeId: string };
49
+ /** A secret, by name. Accepts a `Daytona.Secret` or `{ name }`. */
50
+ export type SandboxSecretRef = { readonly name: string };
51
+
52
+ /** A volume mounted into a sandbox. */
53
+ export interface SandboxMount {
54
+ /** The volume to mount: a `Daytona.Volume`. */
55
+ volume: Ref<SandboxVolumeRef>;
56
+ /** Absolute path in the sandbox. */
57
+ path: string;
58
+ /** Mount only this directory of the volume. */
59
+ subpath?: string;
60
+ }
61
+
62
+ /** A volume a binding mounts into a sandbox (`Daytona.MountVolume`). */
63
+ export interface SandboxVolumeBinding {
64
+ volumeId: string;
65
+ path: string;
66
+ subpath?: string;
67
+ }
68
+
69
+ /**
70
+ * What bindings attach to a sandbox that runs a program: environment
71
+ * variables for the process, and `mounts`: volumes to mount
72
+ * (`Daytona.MountVolume`), or `alchemy/Mount` capabilities whose clients
73
+ * the sandbox's image needs (a derived snapshot installs them).
74
+ */
75
+ export interface SandboxBinding {
76
+ env?: Record<string, any>;
77
+ mounts?: ReadonlyArray<SandboxVolumeBinding | MountCapability>;
78
+ }
79
+
80
+ /** Another sandbox, by id. Accepts a `Daytona.Sandbox` or `{ sandboxId }`. */
81
+ export type SandboxLinkRef = { readonly sandboxId: string };
82
+
83
+ /** Whether the sandbox should be running, stopped (disk kept) or archived (moved to object storage). */
84
+ export type SandboxRunState = "started" | "stopped" | "archived";
85
+
86
+ export interface SandboxProps extends PlatformProps {
87
+ /**
88
+ * Sandbox name, unique in the organization. If omitted, a unique name is
89
+ * generated from the stack, stage and logical ID. Changing it replaces
90
+ * the sandbox.
91
+ */
92
+ name?: string;
93
+ /**
94
+ * Snapshot to boot from. Defaults to Daytona's default snapshot. The
95
+ * snapshot sets the sandbox's CPU, memory, disk and GPUs: declare a
96
+ * `Daytona.Snapshot` with the size you want. Changing it replaces the
97
+ * sandbox.
98
+ */
99
+ snapshot?: Ref<SandboxSnapshot>;
100
+ /** OS user commands run as. Changing it replaces the sandbox. */
101
+ user?: string;
102
+ /**
103
+ * Environment variables. On a sandbox without `main` they are the
104
+ * sandbox's, and changing them replaces it. On a sandbox with `main`
105
+ * they are the program's, and changing them restarts it.
106
+ */
107
+ env?: Record<string, any>;
108
+ /**
109
+ * Secrets exposed as environment variables, keyed by variable name. The
110
+ * sandbox sees each secret's placeholder; Daytona swaps in the value only
111
+ * on requests to the secret's `hosts`. Updated in place.
112
+ */
113
+ secrets?: Record<string, Ref<SandboxSecretRef>>;
114
+ /** Labels. Updated in place. */
115
+ labels?: Record<string, string>;
116
+ /** Whether preview URLs work without a token. Updated in place. @default false */
117
+ public?: boolean;
118
+ /** Block all outbound traffic. Updated in place. @default false */
119
+ networkBlockAll?: boolean;
120
+ /** CIDR ranges outbound traffic may reach (at most 10). Updated in place. */
121
+ networkAllowList?: string[];
122
+ /** Domains outbound traffic may reach (at most 100). Updated in place. */
123
+ domainAllowList?: string[];
124
+ /**
125
+ * A forward proxy for the sandbox's HTTP(S) traffic, set as `HTTP_PROXY`
126
+ * and `HTTPS_PROXY` in the sandbox, and enforced at the network only with a
127
+ * `domainAllowList`. HTTPS reaches it as `CONNECT` tunnels, so a Cloudflare
128
+ * Worker can't serve it. The sandbox can read the URL. Changing it
129
+ * replaces the sandbox.
130
+ */
131
+ outboundProxyUrl?: string;
132
+ /** Region. Changing it replaces the sandbox. */
133
+ target?: string;
134
+ /**
135
+ * Run a GPU sandbox on spot capacity: cheaper, but Daytona can end it
136
+ * without notice. Changing it replaces the sandbox.
137
+ */
138
+ spot?: boolean;
139
+ /** Expose `/dev/kvm` (VM sandboxes). Changing it replaces the sandbox. */
140
+ kvm?: boolean;
141
+ /**
142
+ * Volumes to mount. A program's `Daytona.MountVolume` bindings add to
143
+ * them. Changing them replaces the sandbox.
144
+ */
145
+ mounts?: SandboxMount[];
146
+ /** Stop after this long without activity. `0` never stops. Updated in place. @default "15 minutes" */
147
+ autoStopInterval?: Duration.Input;
148
+ /**
149
+ * Pause (memory kept) after this long without activity. Only `linux-vm`
150
+ * and `windows` sandboxes can pause; a container fails with
151
+ * `AutoPauseNotSupported`. Updated in place.
152
+ */
153
+ autoPauseInterval?: Duration.Input;
154
+ /** Archive after being stopped this long. Updated in place. @default "7 days" */
155
+ autoArchiveInterval?: Duration.Input;
156
+ /** Delete after being stopped this long. Updated in place. */
157
+ autoDeleteInterval?: Duration.Input;
158
+ /**
159
+ * Destroy the sandbox this long after it is created (or after the deploy
160
+ * that last changed it), whatever its state. Changing it restarts the
161
+ * clock from now. `0` disables it, unless the region caps sandbox lifespan.
162
+ */
163
+ ttl?: Duration.Input;
164
+ /**
165
+ * Give up if no runner takes the sandbox within this long. The
166
+ * organization needs the queue-timeout feature; without it, creating the
167
+ * sandbox fails with `QueueTimeoutNotEnabled`. Only used when the sandbox
168
+ * is created.
169
+ */
170
+ queueTimeout?: Duration.Input;
171
+ /**
172
+ * Schedule the sandbox on the same runner as this one, so the two share
173
+ * a local network. Both must be ephemeral (`autoDeleteInterval: 0`), and
174
+ * neither can have GPUs. Changing it replaces the sandbox.
175
+ */
176
+ linkedSandbox?: Ref<SandboxLinkRef>;
177
+ /**
178
+ * Send the sandbox's OpenTelemetry data here instead of Daytona's
179
+ * collector. It then doesn't show in Daytona's dashboard or the
180
+ * telemetry API. Changing it replaces the sandbox.
181
+ */
182
+ otelEndpointOverride?: string;
183
+ /**
184
+ * Whether the sandbox should be running, stopped (disk kept) or archived
185
+ * (disk moved to object storage). Archiving can take many minutes, so a
186
+ * deploy returns once Daytona has started it. Updated in place.
187
+ * @default "started"
188
+ */
189
+ state?: SandboxRunState;
190
+ /**
191
+ * Module entrypoint of a program to run in the sandbox, typically
192
+ * `import.meta.url` in the class form. Alchemy bundles it, uploads it
193
+ * into the sandbox and runs it with Node; a code change uploads it again
194
+ * and restarts the process, without replacing the sandbox. The snapshot
195
+ * needs Node.js 20 or later (Daytona's default snapshot has Node 24).
196
+ */
197
+ main?: string;
198
+ /**
199
+ * Port the program listens on. Written to `PORT`; the sandbox's preview
200
+ * URL of this port is `url`.
201
+ * @default 3000
202
+ */
203
+ port?: number;
204
+ /**
205
+ * Named export to load from `main`.
206
+ * @default "default"
207
+ */
208
+ handler?: string;
209
+ /** Bundler configuration for `main`. */
210
+ build?: Bundle.BundleConfig;
211
+ }
212
+
213
+ export interface SandboxAttributes {
214
+ /** Sandbox ID. */
215
+ sandboxId: string;
216
+ /** Sandbox name. */
217
+ name: string;
218
+ /** Organization ID. */
219
+ organizationId: string;
220
+ /** Snapshot it booted from. */
221
+ snapshot: string | undefined;
222
+ /** Current state. */
223
+ state: sandboxes.SandboxState | undefined;
224
+ /** Region. */
225
+ target: string;
226
+ /** vCPUs. */
227
+ cpu: number;
228
+ /** Memory in GiB. */
229
+ memory: number;
230
+ /** Disk in GiB. */
231
+ disk: number;
232
+ /** GPUs. */
233
+ gpu: number;
234
+ /** Sandbox class: `container`, `linux-vm` or `windows`, set by the snapshot. */
235
+ sandboxClass: sandboxes.SandboxSandboxClass | undefined;
236
+ /** When the TTL destroys the sandbox, if one is set. */
237
+ autoDestroyAt: string | undefined;
238
+ /** ID of the sandbox it shares a runner with. */
239
+ linkedSandboxId: string | undefined;
240
+ /** Labels you set (alchemy's ownership labels excluded). */
241
+ labels: Record<string, string>;
242
+ /** Whether previews are public. */
243
+ public: boolean;
244
+ /** Base URL of the sandbox's toolbox proxy. */
245
+ toolboxProxyUrl: string;
246
+ /**
247
+ * Preview URL of the program's port, for a sandbox with `main`. It is
248
+ * open when the sandbox is `public`; otherwise send the preview token
249
+ * (`getPortPreviewUrl`) in `X-Daytona-Preview-Token`.
250
+ */
251
+ url: string | undefined;
252
+ /** The program the sandbox runs, for a sandbox with `main`. */
253
+ program: ProgramState | undefined;
254
+ /**
255
+ * The snapshot built for this sandbox when its bindings need clients
256
+ * installed (`alchemy/Mount` capabilities): `FROM` its snapshot plus their
257
+ * install lines. Deleted with the sandbox.
258
+ */
259
+ derivedSnapshot: string | undefined;
260
+ }
261
+
262
+ export type Sandbox = Resource<
263
+ "Daytona.Sandbox",
264
+ SandboxProps,
265
+ SandboxAttributes,
266
+ SandboxBinding,
267
+ Providers,
268
+ SandboxLookup
269
+ >;
270
+
271
+ /**
272
+ * Props for {@link Sandbox.lookup}: find a sandbox this stack
273
+ * doesn't manage by its name or id.
274
+ */
275
+ export interface SandboxLookup {
276
+ /** Sandbox name or id. */
277
+ name: string;
278
+ }
279
+
280
+ export type SandboxServices = ServerHost;
281
+
282
+ /** A program's shape: a long-running server returning `{ fetch }`, or `host.run` loops. */
283
+ export type SandboxShape = Main<SandboxServices>;
284
+
285
+ export type SandboxRuntimeContext = HostRuntimeContext;
286
+
287
+ /**
288
+ * A long-lived Daytona sandbox: an isolated machine with its own
289
+ * filesystem, processes and network, for a dev box, a preview environment
290
+ * or a persistent agent workspace. Its disk survives stops; Daytona stops
291
+ * it when idle and archives it later, and deploy starts it again.
292
+ *
293
+ * The class form runs an Effect program in the sandbox: give it `main`
294
+ * and an implementation, and Alchemy bundles the program, uploads it and
295
+ * runs it with Node, served on the sandbox's preview URL (`url`). A code
296
+ * change uploads the new bundle and restarts the process; the sandbox and
297
+ * its URL stay the same. The process lives as long as the sandbox runs,
298
+ * so a sandbox with `main` never auto-stops unless `autoStopInterval` says
299
+ * so; if it stopped anyway, the next deploy starts it and the program.
300
+ *
301
+ * For sandboxes created per request or per task, use
302
+ * {@link CreateSandbox} at runtime instead; for one declared here, use
303
+ * {@link ConnectSandbox} to run commands in it from a Worker.
304
+ * @see https://www.daytona.io/docs/en/sandboxes/
305
+ *
306
+ * ### Creating a Sandbox
307
+ * **Example:** A dev box from a snapshot
308
+ * ```typescript
309
+ * export const DevBox = Daytona.Sandbox("DevBox", {
310
+ * snapshot: Base,
311
+ * autoStopInterval: "30 minutes",
312
+ * });
313
+ * ```
314
+ *
315
+ * ### Restricting the Network
316
+ * **Example:** Only GitHub and npm are reachable
317
+ * ```typescript
318
+ * export const Locked = Daytona.Sandbox("Locked", {
319
+ * snapshot: Base,
320
+ * domainAllowList: ["github.com", "registry.npmjs.org"],
321
+ * });
322
+ * ```
323
+ *
324
+ * ### Volumes and Secrets
325
+ * **Example:** A shared cache volume and an npm token only sent to npm
326
+ * ```typescript
327
+ * export const Builder = Daytona.Sandbox("Builder", {
328
+ * snapshot: Base,
329
+ * mounts: [{ volume: Cache, path: "/cache" }],
330
+ * secrets: { NPM_TOKEN: NpmToken },
331
+ * });
332
+ * ```
333
+ *
334
+ * ### Running a Program
335
+ * **Example:** An HTTP server with a mounted volume
336
+ * ```typescript
337
+ * export default class Agent extends Daytona.Sandbox<Agent>()(
338
+ * "Agent",
339
+ * { snapshot: Base, main: import.meta.url, public: true },
340
+ * Effect.gen(function* () {
341
+ * const data = yield* Daytona.MountVolume(Data, { path: "/data" });
342
+ * const fs = yield* FileSystem.FileSystem;
343
+ * return {
344
+ * fetch: Effect.gen(function* () {
345
+ * return yield* HttpServerResponse.json(yield* fs.readDirectory(data.path));
346
+ * }),
347
+ * };
348
+ * }).pipe(Effect.provide(Daytona.MountVolumeLive)),
349
+ * ) {}
350
+ * ```
351
+ *
352
+ * A mount binding that needs a client in the image (an `alchemy/Mount`
353
+ * capability) makes the sandbox boot from a derived snapshot: its snapshot
354
+ * plus the client's install lines, built on Daytona, named from a hash and
355
+ * deleted with the sandbox. Sandboxes provide FUSE; a client that needs
356
+ * NFS fails with `MountUnsupported`.
357
+ *
358
+ * The program reaches the internet as its sandbox does. Below Daytona's
359
+ * Tier 3, sandboxes reach only an allowlist of essential services
360
+ * (package registries, GitHub, AI APIs, Daytona's API), so bindings that
361
+ * manage sandboxes work but toolbox calls are refused.
362
+ *
363
+ * ### GPUs
364
+ * A GPU sandbox boots from a GPU snapshot, such as Daytona's `daytona-gpu`
365
+ * (one H100) in its `earth` region, and must be ephemeral. The
366
+ * organization needs GPU credits in its wallet.
367
+ *
368
+ * **Example:** An ephemeral H100 sandbox on spot capacity
369
+ * ```typescript
370
+ * export const Trainer = Daytona.Sandbox("Trainer", {
371
+ * snapshot: { name: "daytona-gpu" },
372
+ * target: "earth",
373
+ * autoDeleteInterval: 0,
374
+ * spot: true,
375
+ * });
376
+ * ```
377
+ *
378
+ * ### Limiting Its Lifetime
379
+ * **Example:** Destroyed two hours after it is created
380
+ * ```typescript
381
+ * export const Scratch = Daytona.Sandbox("Scratch", { ttl: "2 hours" });
382
+ * ```
383
+ *
384
+ * ### Sharing a Runner
385
+ * **Example:** A database and an app on one local network
386
+ * ```typescript
387
+ * export const Db = Daytona.Sandbox("Db", { autoDeleteInterval: 0 });
388
+ * export const App = Daytona.Sandbox("App", {
389
+ * autoDeleteInterval: 0,
390
+ * linkedSandbox: Db,
391
+ * });
392
+ * ```
393
+ *
394
+ * ### Looking up an existing sandbox
395
+ * `Sandbox.lookup` references a sandbox Alchemy doesn't manage (created in
396
+ * the Daytona dashboard or by another stack). It is read during plan/deploy,
397
+ * never created, updated or deleted.
398
+ * `url` and `program` are `undefined`: they come from the deploy that started
399
+ * the sandbox's program.
400
+ *
401
+ * **Example:** Reference an existing sandbox
402
+ * ```typescript
403
+ * const box = Daytona.Sandbox.lookup({ name: "research" });
404
+ * ```
405
+ *
406
+ * @resource
407
+ * @product Sandbox
408
+ */
409
+ export const Sandbox: Platform<Sandbox, SandboxServices, SandboxShape, SandboxRuntimeContext> &
410
+ LookupMethod<Sandbox> = Platform("Daytona.Sandbox", {
411
+ createRuntimeContext: createHostRuntimeContext("Daytona.Sandbox"),
412
+ // A declaration passed as a prop (`snapshot: Base`) is an Effect until
413
+ // yielded. Yield it here so it is registered and the prop resolves to
414
+ // its attributes, as with `yield* Snapshot(...)` inside Effect.gen.
415
+ transformProps: (_id, props: SandboxProps) =>
416
+ Effect.gen(function* () {
417
+ if (globalThis.__ALCHEMY_RUNTIME__) return props;
418
+ const ref = <T>(value: T) =>
419
+ Effect.isEffect(value)
420
+ ? (value as unknown as Effect.Effect<T, never, Providers>)
421
+ : Effect.succeed(value);
422
+ return {
423
+ ...props,
424
+ snapshot: props.snapshot === undefined ? undefined : yield* ref(props.snapshot),
425
+ linkedSandbox:
426
+ props.linkedSandbox === undefined ? undefined : yield* ref(props.linkedSandbox),
427
+ secrets:
428
+ props.secrets === undefined
429
+ ? undefined
430
+ : Object.fromEntries(
431
+ yield* Effect.forEach(Object.entries(props.secrets), ([key, value]) =>
432
+ Effect.map(ref(value), (resolved) => [key, resolved] as const),
433
+ ),
434
+ ),
435
+ mounts:
436
+ props.mounts === undefined
437
+ ? undefined
438
+ : yield* Effect.forEach(props.mounts, (mount) =>
439
+ Effect.map(ref(mount.volume), (volume) => ({ ...mount, volume })),
440
+ ),
441
+ };
442
+ }),
443
+ });
444
+
445
+ const resolveRef = <T>(ref: Ref<T>) => Effect.succeed(resolved(ref));
446
+
447
+ const csv = (list: string[] | undefined) => (list === undefined ? undefined : list.join(","));
448
+ const fromCsv = (value: string | undefined) =>
449
+ value === undefined || value === ""
450
+ ? []
451
+ : value
452
+ .split(",")
453
+ .map((s) => s.trim())
454
+ .sort();
455
+
456
+ /** What both `getSandbox` and the sandbox list return. */
457
+ export type SandboxInfo = Omit<sandboxes.Sandbox2, "env" | "kvm">;
458
+
459
+ export const toSandboxAttributes = (s: SandboxInfo): SandboxAttributes => ({
460
+ sandboxId: s.id,
461
+ name: s.name,
462
+ organizationId: s.organizationId,
463
+ snapshot: s.snapshot,
464
+ state: s.state,
465
+ target: s.target,
466
+ cpu: s.cpu,
467
+ memory: s.memory,
468
+ disk: s.disk,
469
+ gpu: s.gpu,
470
+ sandboxClass: s.sandboxClass,
471
+ autoDestroyAt: s.autoDestroyAt,
472
+ linkedSandboxId: s.linkedSandboxId,
473
+ labels: userLabels(s.labels),
474
+ public: s.public,
475
+ toolboxProxyUrl: s.toolboxProxyUrl,
476
+ url: undefined,
477
+ program: undefined,
478
+ derivedSnapshot: undefined,
479
+ });
480
+
481
+ type BoundMount = SandboxVolumeBinding;
482
+
483
+ /**
484
+ * The snapshot the sandbox was declared with. A sandbox booted from a
485
+ * derived snapshot reports that one, so the declared base is read from
486
+ * the last deploy's props.
487
+ */
488
+ const bootedFrom = Effect.fn(function* (olds: SandboxProps | undefined, output: SandboxAttributes) {
489
+ if (output.derivedSnapshot === undefined) return output.snapshot;
490
+ return olds?.snapshot ? (yield* resolveRef(olds.snapshot)).name : undefined;
491
+ });
492
+
493
+ /** The bindings still attached (a removed binding arrives as a `delete`). */
494
+ const activeBindings = (bindings: ReadonlyArray<ResourceBinding<SandboxBinding>>) =>
495
+ bindings.filter(
496
+ (binding: ResourceBinding<SandboxBinding> & { action?: string }) => binding.action !== "delete",
497
+ );
498
+
499
+ /**
500
+ * Every volume the sandbox mounts: its `mounts` and what bindings added, in
501
+ * a stable order. At plan, a volume created in the same deploy has no id
502
+ * yet; it compares as unresolved, which differs from any mounted volume.
503
+ */
504
+ const allMounts = Effect.fn(function* (
505
+ props: SandboxProps | undefined,
506
+ bindings: ReadonlyArray<ResourceBinding<SandboxBinding>> | ReadonlyArray<unknown>,
507
+ ) {
508
+ const declared = yield* Effect.forEach(props?.mounts ?? [], (m) =>
509
+ Effect.map(resolveRef(m.volume), (v): BoundMount => ({
510
+ volumeId: typeof v?.volumeId === "string" ? v.volumeId : "(unresolved)",
511
+ path: m.path,
512
+ subpath: m.subpath,
513
+ })),
514
+ );
515
+ const bound = activeBindings(bindings as ReadonlyArray<ResourceBinding<SandboxBinding>>)
516
+ .flatMap((b) => (b.data?.mounts ?? []).filter((m) => !isMountCapability(m)))
517
+ .map((m) => m as SandboxVolumeBinding)
518
+ .map((m) => ({ ...m, volumeId: typeof m.volumeId === "string" ? m.volumeId : "(unresolved)" }));
519
+ const seen = new Set<string>();
520
+ return [...declared, ...bound]
521
+ .filter((m) => {
522
+ const key = `${m.volumeId}:${m.path}:${m.subpath ?? ""}`;
523
+ if (seen.has(key)) return false;
524
+ seen.add(key);
525
+ return true;
526
+ })
527
+ .sort((a, b) => a.path.localeCompare(b.path));
528
+ });
529
+
530
+ /** The program's environment: bindings' env, alchemy's, the port, then the props'. */
531
+ const bindingEnv = (bindings: ReadonlyArray<ResourceBinding<SandboxBinding>>) =>
532
+ activeBindings(bindings).reduce<Record<string, any>>(
533
+ (acc, binding) => ({ ...acc, ...binding.data?.env }),
534
+ {},
535
+ );
536
+
537
+ const isGone = (s: SandboxInfo) => s.state === "destroyed" || s.state === "destroying";
538
+
539
+ export const getSandbox = (idOrName: string) =>
540
+ sandboxes.getSandbox({ sandboxIdOrName: idOrName }).pipe(
541
+ Effect.map((s) => (isGone(s) ? undefined : s)),
542
+ Effect.catchTag("SandboxNotFound", () => Effect.undefined),
543
+ );
544
+
545
+ const TRANSITIONS: Partial<Record<string, readonly string[]>> = {
546
+ started: ["started"],
547
+ stopped: ["stopped"],
548
+ // Archiving moves the disk to object storage in the background, and can
549
+ // take many minutes; the sandbox is on its way once Daytona accepts it.
550
+ archived: ["archived", "archiving"],
551
+ };
552
+
553
+ export const waitForSandbox = (idOrName: string, want: SandboxRunState | "paused" = "started") =>
554
+ waitForState({
555
+ kind: "sandbox",
556
+ id: idOrName,
557
+ observe: sandboxes.getSandbox({ sandboxIdOrName: idOrName }),
558
+ want: TRANSITIONS[want] ?? [want],
559
+ fail: ["error", "build_failed", "destroyed"],
560
+ reason: (s) => s.errorReason,
561
+ budget: "10 minutes",
562
+ });
563
+
564
+ /** Move a sandbox to the run state it should be in. */
565
+ export const syncRunState = Effect.fn(function* (
566
+ current: sandboxes.Sandbox2,
567
+ want: SandboxRunState,
568
+ ) {
569
+ const id = current.id;
570
+ let state = current.state;
571
+ if (want === "archived" && state === "archiving") return current;
572
+ // Let an in-flight transition land first.
573
+ if (state && !["started", "stopped", "archived", "paused", "error"].includes(state)) {
574
+ state = (yield* waitForState({
575
+ kind: "sandbox",
576
+ id,
577
+ observe: sandboxes.getSandbox({ sandboxIdOrName: id }),
578
+ want: ["started", "stopped", "archived", "paused", "error"],
579
+ budget: "10 minutes",
580
+ })).state;
581
+ }
582
+ if (state === "error") {
583
+ if (current.recoverable) {
584
+ yield* sandboxes.recoverSandbox({ sandboxIdOrName: id });
585
+ state = (yield* waitForSandbox(id, "started")).state;
586
+ } else {
587
+ return yield* waitForSandbox(id, "started");
588
+ }
589
+ }
590
+ if (state === want) return yield* sandboxes.getSandbox({ sandboxIdOrName: id });
591
+ if (want === "started") {
592
+ yield* sandboxes.startSandbox({ sandboxIdOrName: id });
593
+ return yield* waitForSandbox(id, "started");
594
+ }
595
+ if (want === "stopped") {
596
+ if (state === "archived") {
597
+ // An archived sandbox only leaves the archive by starting.
598
+ yield* sandboxes.startSandbox({ sandboxIdOrName: id });
599
+ yield* waitForSandbox(id, "started");
600
+ }
601
+ yield* sandboxes.stopSandbox({ sandboxIdOrName: id });
602
+ return yield* waitForSandbox(id, "stopped");
603
+ }
604
+ if (state === "started" || state === "paused") {
605
+ yield* sandboxes.stopSandbox({ sandboxIdOrName: id });
606
+ yield* waitForSandbox(id, "stopped");
607
+ }
608
+ yield* sandboxes.archiveSandbox({ sandboxIdOrName: id });
609
+ return yield* waitForSandbox(id, "archived");
610
+ });
611
+
612
+ export const deleteSandboxById = Effect.fn(function* (idOrName: string) {
613
+ yield* sandboxes
614
+ .deleteSandbox({ sandboxIdOrName: idOrName })
615
+ .pipe(Effect.catchTag("SandboxNotFound", () => Effect.void));
616
+ yield* waitUntilGone(
617
+ "sandbox",
618
+ idOrName,
619
+ getSandbox(idOrName).pipe(Effect.map((s) => s !== undefined)),
620
+ "5 minutes",
621
+ );
622
+ });
623
+
624
+ const resolveCreate = Effect.fn(function* (
625
+ name: string,
626
+ news: SandboxProps,
627
+ labels: Record<string, string>,
628
+ mounts: ReadonlyArray<BoundMount>,
629
+ ) {
630
+ const snapshot = news.snapshot ? (yield* resolveRef(news.snapshot)).name : undefined;
631
+ const volumes =
632
+ mounts.length > 0
633
+ ? mounts.map((m) => ({ volumeId: m.volumeId, mountPath: m.path, subpath: m.subpath }))
634
+ : undefined;
635
+ const secrets = news.secrets
636
+ ? yield* Effect.forEach(Object.entries(news.secrets), ([env, ref]) =>
637
+ Effect.map(resolveRef(ref), (s) => ({ [env]: s.name })),
638
+ )
639
+ : undefined;
640
+ const linkedSandbox = news.linkedSandbox
641
+ ? (yield* resolveRef(news.linkedSandbox)).sandboxId
642
+ : undefined;
643
+ return {
644
+ name,
645
+ snapshot,
646
+ linkedSandbox,
647
+ otelEndpointOverride: news.otelEndpointOverride,
648
+ ttlMinutes: toMinutes(news.ttl),
649
+ queueTimeout: toMinutes(news.queueTimeout),
650
+ user: news.user,
651
+ // A program's env is the process's, written at deploy, not the sandbox's.
652
+ env: news.main === undefined ? news.env : undefined,
653
+ labels,
654
+ public: news.public,
655
+ networkBlockAll: news.networkBlockAll,
656
+ networkAllowList: csv(news.networkAllowList),
657
+ domainAllowList: csv(news.domainAllowList),
658
+ outboundProxyUrl: news.outboundProxyUrl,
659
+ target: news.target,
660
+ spot: news.spot,
661
+ kvm: news.kvm,
662
+ volumes,
663
+ secrets,
664
+ // A program serves for as long as the sandbox runs: don't stop it when idle.
665
+ autoStopInterval: toMinutes(news.autoStopInterval ?? (news.main !== undefined ? 0 : undefined)),
666
+ autoPauseInterval: toMinutes(news.autoPauseInterval),
667
+ autoArchiveInterval: toMinutes(news.autoArchiveInterval),
668
+ autoDeleteInterval: toMinutes(news.autoDeleteInterval),
669
+ } satisfies sandboxes.CreateSandboxRequest;
670
+ });
671
+
672
+ export const SandboxProvider = () =>
673
+ Provider.effect(
674
+ Sandbox,
675
+ Effect.gen(function* () {
676
+ const stack = yield* Stack;
677
+ // The bundled program's bootstrap reads these to rebuild the Stack at runtime.
678
+ const alchemyEnv = {
679
+ ALCHEMY_STACK_NAME: stack.name,
680
+ ALCHEMY_STAGE: stack.stage,
681
+ ALCHEMY_PHASE: "runtime",
682
+ HOST: "0.0.0.0",
683
+ };
684
+ return {
685
+ stables: ["sandboxId", "organizationId", "toolboxProxyUrl"],
686
+ list: Effect.fn(function* () {
687
+ const all = yield* sandboxes.listSandboxes.items({}).pipe(Stream.runCollect);
688
+ return Array.from(all)
689
+ .filter(
690
+ (s) =>
691
+ !isGone(s) &&
692
+ Object.keys(s.labels ?? {}).some((k) => k.startsWith(ALCHEMY_LABEL_PREFIX)),
693
+ )
694
+ .map(toSandboxAttributes);
695
+ }),
696
+ diff: Effect.fn(function* ({ id, olds, news, output, oldBindings, newBindings }) {
697
+ if (output === undefined) return undefined;
698
+ // A program's props are partly unresolved at plan (its bindings' env
699
+ // holds Outputs), so each field is compared once it is known.
700
+ const props = news as SandboxProps;
701
+ const known = isResolved;
702
+ const name = known(props.name)
703
+ ? yield* resolveName(id, props.name, props.name === undefined ? output.name : undefined)
704
+ : output.name;
705
+ const snapshot =
706
+ props.snapshot && known(props.snapshot)
707
+ ? (yield* resolveRef(props.snapshot)).name
708
+ : undefined;
709
+ const linked =
710
+ props.linkedSandbox && known(props.linkedSandbox)
711
+ ? (yield* resolveRef(props.linkedSandbox)).sandboxId
712
+ : undefined;
713
+ const replace =
714
+ name !== output.name ||
715
+ (snapshot !== undefined && snapshot !== (yield* bootedFrom(olds, output))) ||
716
+ (known(props.user) && props.user !== olds?.user) ||
717
+ // A sandbox env is set at create; a program's restarts the program.
718
+ (props.main === undefined) !== (olds?.main === undefined) ||
719
+ (props.main === undefined &&
720
+ known(props.env) &&
721
+ !deepEqual(props.env ?? {}, olds?.env ?? {})) ||
722
+ (known(props.outboundProxyUrl) && props.outboundProxyUrl !== olds?.outboundProxyUrl) ||
723
+ (known(props.target) && props.target !== undefined && props.target !== output.target) ||
724
+ props.spot !== olds?.spot ||
725
+ props.kvm !== olds?.kvm ||
726
+ (known(props.otelEndpointOverride) &&
727
+ props.otelEndpointOverride !== olds?.otelEndpointOverride) ||
728
+ (linked !== undefined && linked !== output.linkedSandboxId) ||
729
+ !deepEqual(
730
+ yield* allMounts(props, Array.isArray(newBindings) ? newBindings : []),
731
+ yield* allMounts(olds, oldBindings ?? []),
732
+ );
733
+ // A change to the clients bindings need rebuilds the derived snapshot.
734
+ // Bindings may hold Outputs at plan; capabilities are plain data.
735
+ const caps = capabilitiesOf(
736
+ (Array.isArray(newBindings) ? newBindings : []) as ReadonlyArray<
737
+ ResourceBinding<SandboxBinding>
738
+ >,
739
+ );
740
+ yield* checkSupport(caps);
741
+ const installChanged = !deepEqual(
742
+ installLinesOf(caps),
743
+ installLinesOf(capabilitiesOf(oldBindings ?? [])),
744
+ );
745
+ if (replace || installChanged) {
746
+ return { action: "replace", deleteFirst: name === output.name } as const;
747
+ }
748
+ // The bundle depends only on `main`, `handler` and `build`, which are plain.
749
+ if (typeof props.main === "string") {
750
+ // A code change, or a process that ended (the sandbox restarted),
751
+ // redeploys the program in place.
752
+ const { hash } = yield* bundleProgram({ ...props, main: props.main });
753
+ if (output.program === undefined || hash !== output.program.codeHash) {
754
+ return { action: "update" } as const;
755
+ }
756
+ if (!(yield* isProgramRunning(output.sandboxId, output.program))) {
757
+ return { action: "update" } as const;
758
+ }
759
+ }
760
+ }),
761
+ lookup: Effect.fn(function* ({ name }) {
762
+ const found = yield* getSandbox(name);
763
+ return found === undefined
764
+ ? undefined
765
+ : {
766
+ ...toSandboxAttributes(found),
767
+ url: undefined,
768
+ program: undefined,
769
+ derivedSnapshot: undefined,
770
+ };
771
+ }),
772
+
773
+ read: Effect.fn(function* ({ id, olds, output }) {
774
+ const found = yield* getSandbox(
775
+ output?.sandboxId ?? (yield* resolveName(id, olds?.name, output?.name)),
776
+ );
777
+ if (found === undefined) return undefined;
778
+ // The program isn't visible on the sandbox; keep what the last deploy recorded.
779
+ const attrs = {
780
+ ...toSandboxAttributes(found),
781
+ url: output?.url,
782
+ program: output?.program,
783
+ derivedSnapshot: output?.derivedSnapshot,
784
+ };
785
+ return (yield* hasAlchemyLabels(id, found.labels)) ? attrs : Unowned(attrs);
786
+ }),
787
+ reconcile: Effect.fn(function* ({ id, news, olds, output, bindings, session }) {
788
+ const name = yield* resolveName(id, news.name, output?.name);
789
+ const labels = { ...news.labels, ...(yield* createInternalLabels(id)) };
790
+ const mounts = yield* allMounts(news, bindings);
791
+ const base = yield* resolveCreate(name, news, labels, mounts);
792
+ // Clients bindings need are installed into a snapshot derived from the base.
793
+ const caps = capabilitiesOf(bindings);
794
+ yield* checkSupport(caps);
795
+ const derivedSnapshot =
796
+ caps.length > 0
797
+ ? yield* ensureDerivedSnapshot({
798
+ sandboxName: name,
799
+ snapshot: base.snapshot,
800
+ caps,
801
+ note: session.note,
802
+ })
803
+ : undefined;
804
+ const request = derivedSnapshot ? { ...base, snapshot: derivedSnapshot } : base;
805
+
806
+ // Observe
807
+ let current = yield* getSandbox(output?.sandboxId ?? name);
808
+ const created = current === undefined;
809
+
810
+ // Ensure
811
+ if (current === undefined) {
812
+ yield* session.note(`Creating sandbox ${name}...`);
813
+ current = yield* sandboxes
814
+ .createSandbox(request)
815
+ .pipe(
816
+ Effect.catchTag("SandboxAlreadyExists", () =>
817
+ sandboxes.getSandbox({ sandboxIdOrName: name }),
818
+ ),
819
+ );
820
+ current = yield* waitForSandbox(current.id, "started");
821
+ }
822
+ const sandboxId = current.id;
823
+
824
+ // Sync labels against the observed set.
825
+ if (!deepEqual(definedLabels(current.labels), labels)) {
826
+ yield* sandboxes.replaceLabels({ sandboxIdOrName: sandboxId, labels });
827
+ }
828
+ // Sync network.
829
+ const network = {
830
+ networkBlockAll: news.networkBlockAll ?? false,
831
+ networkAllowList: fromCsv(request.networkAllowList),
832
+ domainAllowList: fromCsv(request.domainAllowList),
833
+ };
834
+ if (
835
+ network.networkBlockAll !== current.networkBlockAll ||
836
+ !deepEqual(network.networkAllowList, fromCsv(current.networkAllowList)) ||
837
+ !deepEqual(network.domainAllowList, fromCsv(current.domainAllowList))
838
+ ) {
839
+ yield* sandboxes.updateNetworkSettings({
840
+ sandboxIdOrName: sandboxId,
841
+ networkBlockAll: network.networkBlockAll,
842
+ networkAllowList: network.networkAllowList.join(","),
843
+ domainAllowList: network.domainAllowList.join(","),
844
+ });
845
+ }
846
+ // Sync public previews.
847
+ if ((news.public ?? false) !== current.public) {
848
+ yield* sandboxes.updatePublicStatus({
849
+ sandboxIdOrName: sandboxId,
850
+ isPublic: news.public ?? false,
851
+ });
852
+ }
853
+ // Sync lifecycle intervals that were declared.
854
+ const intervals = [
855
+ [request.autoStopInterval, current.autoStopInterval, sandboxes.setAutostopInterval],
856
+ [request.autoPauseInterval, current.autoPauseInterval, sandboxes.setAutoPauseInterval],
857
+ [
858
+ request.autoArchiveInterval,
859
+ current.autoArchiveInterval,
860
+ sandboxes.setAutoArchiveInterval,
861
+ ],
862
+ [
863
+ request.autoDeleteInterval,
864
+ current.autoDeleteInterval,
865
+ sandboxes.setAutoDeleteInterval,
866
+ ],
867
+ ] as const;
868
+ for (const [want, have, set] of intervals) {
869
+ if (want !== undefined && want !== have) {
870
+ yield* set({ sandboxIdOrName: sandboxId, interval: want });
871
+ }
872
+ }
873
+ // Sync the TTL. Daytona reports only when it fires, and setting it
874
+ // restarts the clock, so it's written when the declared TTL changes
875
+ // (or on adoption).
876
+ if (
877
+ !created &&
878
+ request.ttlMinutes !== undefined &&
879
+ (olds === undefined || request.ttlMinutes !== toMinutes(olds.ttl))
880
+ ) {
881
+ yield* sandboxes.setTtl({ sandboxIdOrName: sandboxId, ttlMinutes: request.ttlMinutes });
882
+ } else if (!created && request.ttlMinutes === undefined && olds?.ttl !== undefined) {
883
+ yield* sandboxes.setTtl({ sandboxIdOrName: sandboxId, ttlMinutes: 0 });
884
+ }
885
+ // Sync secrets. Daytona doesn't report a sandbox's mounted secrets,
886
+ // so the declared set is written whenever it differs from the last
887
+ // deploy (or on adoption, when there is no last deploy).
888
+ if (request.secrets !== undefined || olds?.secrets !== undefined) {
889
+ const previous = olds?.secrets
890
+ ? yield* resolveCreate(name, olds, labels, mounts)
891
+ : undefined;
892
+ if (olds === undefined || !deepEqual(previous?.secrets ?? [], request.secrets ?? [])) {
893
+ yield* sandboxes.updateSandboxSecrets({
894
+ sandboxIdOrName: sandboxId,
895
+ secrets: request.secrets ?? [],
896
+ });
897
+ }
898
+ }
899
+ // Sync run state last, from a fresh observation.
900
+ const final = yield* syncRunState(
901
+ yield* sandboxes.getSandbox({ sandboxIdOrName: sandboxId }),
902
+ news.state ?? "started",
903
+ );
904
+ const attrs = { ...toSandboxAttributes(final), derivedSnapshot };
905
+ // Deploy the program into the running sandbox.
906
+ if (news.main === undefined || final.state !== "started") return attrs;
907
+ const port = news.port ?? DEFAULT_PORT;
908
+ const program = yield* deployProgram({
909
+ id,
910
+ sandboxId,
911
+ toolboxProxyUrl: final.toolboxProxyUrl,
912
+ source: { ...news, main: news.main },
913
+ port,
914
+ env: { ...bindingEnv(bindings), ...alchemyEnv, PORT: String(port), ...news.env },
915
+ // A replaced sandbox runs nothing yet.
916
+ previous: output?.sandboxId === sandboxId ? output.program : undefined,
917
+ note: session.note,
918
+ });
919
+ return { ...attrs, url: program.url, program: program.state };
920
+ }),
921
+ delete: Effect.fn(function* ({ output }) {
922
+ yield* deleteSandboxById(output.sandboxId);
923
+ if (output.derivedSnapshot) yield* deleteSnapshotByName(output.derivedSnapshot);
924
+ }),
925
+ };
926
+ }),
927
+ );