@alchemy.run/modal 0.0.0 → 2.0.0-beta.87
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/LICENSE +201 -0
- package/README.md +96 -4
- package/lib/App.d.ts +143 -0
- package/lib/App.d.ts.map +1 -0
- package/lib/App.js +198 -0
- package/lib/App.js.map +1 -0
- package/lib/AuthProvider.d.ts +53 -0
- package/lib/AuthProvider.d.ts.map +1 -0
- package/lib/AuthProvider.js +209 -0
- package/lib/AuthProvider.js.map +1 -0
- package/lib/BindingHttp.d.ts +56 -0
- package/lib/BindingHttp.d.ts.map +1 -0
- package/lib/BindingHttp.js +50 -0
- package/lib/BindingHttp.js.map +1 -0
- package/lib/Blob.d.ts +34 -0
- package/lib/Blob.d.ts.map +1 -0
- package/lib/Blob.js +59 -0
- package/lib/Blob.js.map +1 -0
- package/lib/Bootstrap.d.ts +8 -0
- package/lib/Bootstrap.d.ts.map +1 -0
- package/lib/Bootstrap.js +26 -0
- package/lib/Bootstrap.js.map +1 -0
- package/lib/Cbor.d.ts +10 -0
- package/lib/Cbor.d.ts.map +1 -0
- package/lib/Cbor.js +177 -0
- package/lib/Cbor.js.map +1 -0
- package/lib/ConnectSandbox.d.ts +64 -0
- package/lib/ConnectSandbox.d.ts.map +1 -0
- package/lib/ConnectSandbox.js +33 -0
- package/lib/ConnectSandbox.js.map +1 -0
- package/lib/Container.d.ts +124 -0
- package/lib/Container.d.ts.map +1 -0
- package/lib/Container.js +1 -0
- package/lib/Container.js.map +1 -0
- package/lib/Contributions.d.ts +7 -0
- package/lib/Contributions.d.ts.map +1 -0
- package/lib/Contributions.js +6 -0
- package/lib/Contributions.js.map +1 -0
- package/lib/CreateDict.d.ts +83 -0
- package/lib/CreateDict.d.ts.map +1 -0
- package/lib/CreateDict.js +3 -0
- package/lib/CreateDict.js.map +1 -0
- package/lib/CreateDictHttp.d.ts +33 -0
- package/lib/CreateDictHttp.d.ts.map +1 -0
- package/lib/CreateDictHttp.js +67 -0
- package/lib/CreateDictHttp.js.map +1 -0
- package/lib/CreateImage.d.ts +98 -0
- package/lib/CreateImage.d.ts.map +1 -0
- package/lib/CreateImage.js +3 -0
- package/lib/CreateImage.js.map +1 -0
- package/lib/CreateImageHttp.d.ts +51 -0
- package/lib/CreateImageHttp.d.ts.map +1 -0
- package/lib/CreateImageHttp.js +87 -0
- package/lib/CreateImageHttp.js.map +1 -0
- package/lib/CreateQueue.d.ts +83 -0
- package/lib/CreateQueue.d.ts.map +1 -0
- package/lib/CreateQueue.js +3 -0
- package/lib/CreateQueue.js.map +1 -0
- package/lib/CreateQueueHttp.d.ts +31 -0
- package/lib/CreateQueueHttp.d.ts.map +1 -0
- package/lib/CreateQueueHttp.js +67 -0
- package/lib/CreateQueueHttp.js.map +1 -0
- package/lib/CreateSandbox.d.ts +186 -0
- package/lib/CreateSandbox.d.ts.map +1 -0
- package/lib/CreateSandbox.js +3 -0
- package/lib/CreateSandbox.js.map +1 -0
- package/lib/CreateSandboxHttp.d.ts +17 -0
- package/lib/CreateSandboxHttp.d.ts.map +1 -0
- package/lib/CreateSandboxHttp.js +117 -0
- package/lib/CreateSandboxHttp.js.map +1 -0
- package/lib/CreateSecret.d.ts +95 -0
- package/lib/CreateSecret.d.ts.map +1 -0
- package/lib/CreateSecret.js +3 -0
- package/lib/CreateSecret.js.map +1 -0
- package/lib/CreateSecretHttp.d.ts +24 -0
- package/lib/CreateSecretHttp.d.ts.map +1 -0
- package/lib/CreateSecretHttp.js +60 -0
- package/lib/CreateSecretHttp.js.map +1 -0
- package/lib/CreateVolume.d.ts +84 -0
- package/lib/CreateVolume.d.ts.map +1 -0
- package/lib/CreateVolume.js +3 -0
- package/lib/CreateVolume.js.map +1 -0
- package/lib/CreateVolumeHttp.d.ts +32 -0
- package/lib/CreateVolumeHttp.d.ts.map +1 -0
- package/lib/CreateVolumeHttp.js +69 -0
- package/lib/CreateVolumeHttp.js.map +1 -0
- package/lib/Credentials.d.ts +32 -0
- package/lib/Credentials.d.ts.map +1 -0
- package/lib/Credentials.js +44 -0
- package/lib/Credentials.js.map +1 -0
- package/lib/Dict.d.ts +147 -0
- package/lib/Dict.d.ts.map +1 -0
- package/lib/Dict.js +175 -0
- package/lib/Dict.js.map +1 -0
- package/lib/DictClient.d.ts +55 -0
- package/lib/DictClient.d.ts.map +1 -0
- package/lib/DictClient.js +89 -0
- package/lib/DictClient.js.map +1 -0
- package/lib/Domain.d.ts +86 -0
- package/lib/Domain.d.ts.map +1 -0
- package/lib/Domain.js +113 -0
- package/lib/Domain.js.map +1 -0
- package/lib/Endpoint.d.ts +181 -0
- package/lib/Endpoint.d.ts.map +1 -0
- package/lib/Endpoint.js +283 -0
- package/lib/Endpoint.js.map +1 -0
- package/lib/Environment.d.ts +174 -0
- package/lib/Environment.d.ts.map +1 -0
- package/lib/Environment.js +244 -0
- package/lib/Environment.js.map +1 -0
- package/lib/FetchServer.d.ts +105 -0
- package/lib/FetchServer.d.ts.map +1 -0
- package/lib/FetchServer.js +3 -0
- package/lib/FetchServer.js.map +1 -0
- package/lib/FetchServerHttp.d.ts +21 -0
- package/lib/FetchServerHttp.d.ts.map +1 -0
- package/lib/FetchServerHttp.js +84 -0
- package/lib/FetchServerHttp.js.map +1 -0
- package/lib/Function.d.ts +236 -0
- package/lib/Function.d.ts.map +1 -0
- package/lib/Function.js +273 -0
- package/lib/Function.js.map +1 -0
- package/lib/FunctionAutoscaler.d.ts +99 -0
- package/lib/FunctionAutoscaler.d.ts.map +1 -0
- package/lib/FunctionAutoscaler.js +136 -0
- package/lib/FunctionAutoscaler.js.map +1 -0
- package/lib/FunctionTarget.d.ts +61 -0
- package/lib/FunctionTarget.d.ts.map +1 -0
- package/lib/FunctionTarget.js +136 -0
- package/lib/FunctionTarget.js.map +1 -0
- package/lib/Image.d.ts +159 -0
- package/lib/Image.d.ts.map +1 -0
- package/lib/Image.js +203 -0
- package/lib/Image.js.map +1 -0
- package/lib/ImageBuild.d.ts +108 -0
- package/lib/ImageBuild.d.ts.map +1 -0
- package/lib/ImageBuild.js +157 -0
- package/lib/ImageBuild.js.map +1 -0
- package/lib/ImagePlan.d.ts +31 -0
- package/lib/ImagePlan.d.ts.map +1 -0
- package/lib/ImagePlan.js +70 -0
- package/lib/ImagePlan.js.map +1 -0
- package/lib/InferEnv.d.ts +26 -0
- package/lib/InferEnv.d.ts.map +1 -0
- package/lib/InferEnv.js +1 -0
- package/lib/InferEnv.js.map +1 -0
- package/lib/InvokeFunction.d.ts +186 -0
- package/lib/InvokeFunction.d.ts.map +1 -0
- package/lib/InvokeFunction.js +10 -0
- package/lib/InvokeFunction.js.map +1 -0
- package/lib/InvokeFunctionHttp.d.ts +39 -0
- package/lib/InvokeFunctionHttp.d.ts.map +1 -0
- package/lib/InvokeFunctionHttp.js +210 -0
- package/lib/InvokeFunctionHttp.js.map +1 -0
- package/lib/ModalBindings.d.ts +138 -0
- package/lib/ModalBindings.d.ts.map +1 -0
- package/lib/ModalBindings.js +223 -0
- package/lib/ModalBindings.js.map +1 -0
- package/lib/MountBucket.d.ts +80 -0
- package/lib/MountBucket.d.ts.map +1 -0
- package/lib/MountBucket.js +44 -0
- package/lib/MountBucket.js.map +1 -0
- package/lib/MountVolume.d.ts +77 -0
- package/lib/MountVolume.d.ts.map +1 -0
- package/lib/MountVolume.js +41 -0
- package/lib/MountVolume.js.map +1 -0
- package/lib/Mounts.d.ts +134 -0
- package/lib/Mounts.d.ts.map +1 -0
- package/lib/Mounts.js +87 -0
- package/lib/Mounts.js.map +1 -0
- package/lib/PersistentSandbox.d.ts +75 -0
- package/lib/PersistentSandbox.d.ts.map +1 -0
- package/lib/PersistentSandbox.js +93 -0
- package/lib/PersistentSandbox.js.map +1 -0
- package/lib/Providers.d.ts +23 -0
- package/lib/Providers.d.ts.map +1 -0
- package/lib/Providers.js +64 -0
- package/lib/Providers.js.map +1 -0
- package/lib/Proxy.d.ts +94 -0
- package/lib/Proxy.d.ts.map +1 -0
- package/lib/Proxy.js +132 -0
- package/lib/Proxy.js.map +1 -0
- package/lib/Queue.d.ts +147 -0
- package/lib/Queue.d.ts.map +1 -0
- package/lib/Queue.js +177 -0
- package/lib/Queue.js.map +1 -0
- package/lib/QueueClient.d.ts +64 -0
- package/lib/QueueClient.d.ts.map +1 -0
- package/lib/QueueClient.js +73 -0
- package/lib/QueueClient.js.map +1 -0
- package/lib/ReadApp.d.ts +85 -0
- package/lib/ReadApp.d.ts.map +1 -0
- package/lib/ReadApp.js +3 -0
- package/lib/ReadApp.js.map +1 -0
- package/lib/ReadAppHttp.d.ts +26 -0
- package/lib/ReadAppHttp.d.ts.map +1 -0
- package/lib/ReadAppHttp.js +106 -0
- package/lib/ReadAppHttp.js.map +1 -0
- package/lib/ReadDict.d.ts +39 -0
- package/lib/ReadDict.d.ts.map +1 -0
- package/lib/ReadDict.js +3 -0
- package/lib/ReadDict.js.map +1 -0
- package/lib/ReadDictHttp.d.ts +12 -0
- package/lib/ReadDictHttp.d.ts.map +1 -0
- package/lib/ReadDictHttp.js +23 -0
- package/lib/ReadDictHttp.js.map +1 -0
- package/lib/ReadFunction.d.ts +73 -0
- package/lib/ReadFunction.d.ts.map +1 -0
- package/lib/ReadFunction.js +3 -0
- package/lib/ReadFunction.js.map +1 -0
- package/lib/ReadFunctionHttp.d.ts +25 -0
- package/lib/ReadFunctionHttp.d.ts.map +1 -0
- package/lib/ReadFunctionHttp.js +63 -0
- package/lib/ReadFunctionHttp.js.map +1 -0
- package/lib/ReadQueue.d.ts +39 -0
- package/lib/ReadQueue.d.ts.map +1 -0
- package/lib/ReadQueue.js +3 -0
- package/lib/ReadQueue.js.map +1 -0
- package/lib/ReadQueueHttp.d.ts +12 -0
- package/lib/ReadQueueHttp.d.ts.map +1 -0
- package/lib/ReadQueueHttp.js +23 -0
- package/lib/ReadQueueHttp.js.map +1 -0
- package/lib/ReadServer.d.ts +72 -0
- package/lib/ReadServer.d.ts.map +1 -0
- package/lib/ReadServer.js +3 -0
- package/lib/ReadServer.js.map +1 -0
- package/lib/ReadServerHttp.d.ts +25 -0
- package/lib/ReadServerHttp.d.ts.map +1 -0
- package/lib/ReadServerHttp.js +70 -0
- package/lib/ReadServerHttp.js.map +1 -0
- package/lib/ReadVolume.d.ts +40 -0
- package/lib/ReadVolume.d.ts.map +1 -0
- package/lib/ReadVolume.js +3 -0
- package/lib/ReadVolume.js.map +1 -0
- package/lib/ReadVolumeHttp.d.ts +12 -0
- package/lib/ReadVolumeHttp.d.ts.map +1 -0
- package/lib/ReadVolumeHttp.js +24 -0
- package/lib/ReadVolumeHttp.js.map +1 -0
- package/lib/ReadWriteDict.d.ts +39 -0
- package/lib/ReadWriteDict.d.ts.map +1 -0
- package/lib/ReadWriteDict.js +3 -0
- package/lib/ReadWriteDict.js.map +1 -0
- package/lib/ReadWriteDictHttp.d.ts +12 -0
- package/lib/ReadWriteDictHttp.d.ts.map +1 -0
- package/lib/ReadWriteDictHttp.js +23 -0
- package/lib/ReadWriteDictHttp.js.map +1 -0
- package/lib/ReadWriteQueue.d.ts +40 -0
- package/lib/ReadWriteQueue.d.ts.map +1 -0
- package/lib/ReadWriteQueue.js +3 -0
- package/lib/ReadWriteQueue.js.map +1 -0
- package/lib/ReadWriteQueueHttp.d.ts +12 -0
- package/lib/ReadWriteQueueHttp.d.ts.map +1 -0
- package/lib/ReadWriteQueueHttp.js +23 -0
- package/lib/ReadWriteQueueHttp.js.map +1 -0
- package/lib/ReadWriteVolume.d.ts +40 -0
- package/lib/ReadWriteVolume.d.ts.map +1 -0
- package/lib/ReadWriteVolume.js +3 -0
- package/lib/ReadWriteVolume.js.map +1 -0
- package/lib/ReadWriteVolumeHttp.d.ts +12 -0
- package/lib/ReadWriteVolumeHttp.d.ts.map +1 -0
- package/lib/ReadWriteVolumeHttp.js +24 -0
- package/lib/ReadWriteVolumeHttp.js.map +1 -0
- package/lib/RuntimeOwner.d.ts +39 -0
- package/lib/RuntimeOwner.d.ts.map +1 -0
- package/lib/RuntimeOwner.js +85 -0
- package/lib/RuntimeOwner.js.map +1 -0
- package/lib/Sandbox.d.ts +215 -0
- package/lib/Sandbox.d.ts.map +1 -0
- package/lib/Sandbox.js +278 -0
- package/lib/Sandbox.js.map +1 -0
- package/lib/SandboxClient.d.ts +748 -0
- package/lib/SandboxClient.d.ts.map +1 -0
- package/lib/SandboxClient.js +710 -0
- package/lib/SandboxClient.js.map +1 -0
- package/lib/ScaleFunction.d.ts +54 -0
- package/lib/ScaleFunction.d.ts.map +1 -0
- package/lib/ScaleFunction.js +3 -0
- package/lib/ScaleFunction.js.map +1 -0
- package/lib/ScaleFunctionHttp.d.ts +21 -0
- package/lib/ScaleFunctionHttp.d.ts.map +1 -0
- package/lib/ScaleFunctionHttp.js +39 -0
- package/lib/ScaleFunctionHttp.js.map +1 -0
- package/lib/Secret.d.ts +170 -0
- package/lib/Secret.d.ts.map +1 -0
- package/lib/Secret.js +196 -0
- package/lib/Secret.js.map +1 -0
- package/lib/Server.d.ts +155 -0
- package/lib/Server.d.ts.map +1 -0
- package/lib/Server.js +184 -0
- package/lib/Server.js.map +1 -0
- package/lib/ServiceUser.d.ts +139 -0
- package/lib/ServiceUser.d.ts.map +1 -0
- package/lib/ServiceUser.js +210 -0
- package/lib/ServiceUser.js.map +1 -0
- package/lib/Volume.d.ts +170 -0
- package/lib/Volume.d.ts.map +1 -0
- package/lib/Volume.js +220 -0
- package/lib/Volume.js.map +1 -0
- package/lib/VolumeClient.d.ts +53 -0
- package/lib/VolumeClient.d.ts.map +1 -0
- package/lib/VolumeClient.js +22 -0
- package/lib/VolumeClient.js.map +1 -0
- package/lib/VolumeFiles.d.ts +94 -0
- package/lib/VolumeFiles.d.ts.map +1 -0
- package/lib/VolumeFiles.js +139 -0
- package/lib/VolumeFiles.js.map +1 -0
- package/lib/WebhookToken.d.ts +102 -0
- package/lib/WebhookToken.d.ts.map +1 -0
- package/lib/WebhookToken.js +176 -0
- package/lib/WebhookToken.js.map +1 -0
- package/lib/WorkspaceSettings.d.ts +70 -0
- package/lib/WorkspaceSettings.d.ts.map +1 -0
- package/lib/WorkspaceSettings.js +105 -0
- package/lib/WorkspaceSettings.js.map +1 -0
- package/lib/WriteApp.d.ts +38 -0
- package/lib/WriteApp.d.ts.map +1 -0
- package/lib/WriteApp.js +3 -0
- package/lib/WriteApp.js.map +1 -0
- package/lib/WriteAppHttp.d.ts +23 -0
- package/lib/WriteAppHttp.d.ts.map +1 -0
- package/lib/WriteAppHttp.js +36 -0
- package/lib/WriteAppHttp.js.map +1 -0
- package/lib/WriteDict.d.ts +39 -0
- package/lib/WriteDict.d.ts.map +1 -0
- package/lib/WriteDict.js +3 -0
- package/lib/WriteDict.js.map +1 -0
- package/lib/WriteDictHttp.d.ts +12 -0
- package/lib/WriteDictHttp.d.ts.map +1 -0
- package/lib/WriteDictHttp.js +23 -0
- package/lib/WriteDictHttp.js.map +1 -0
- package/lib/WriteQueue.d.ts +39 -0
- package/lib/WriteQueue.d.ts.map +1 -0
- package/lib/WriteQueue.js +3 -0
- package/lib/WriteQueue.js.map +1 -0
- package/lib/WriteQueueHttp.d.ts +12 -0
- package/lib/WriteQueueHttp.d.ts.map +1 -0
- package/lib/WriteQueueHttp.js +23 -0
- package/lib/WriteQueueHttp.js.map +1 -0
- package/lib/WriteSecret.d.ts +53 -0
- package/lib/WriteSecret.d.ts.map +1 -0
- package/lib/WriteSecret.js +3 -0
- package/lib/WriteSecret.js.map +1 -0
- package/lib/WriteSecretHttp.d.ts +23 -0
- package/lib/WriteSecretHttp.d.ts.map +1 -0
- package/lib/WriteSecretHttp.js +44 -0
- package/lib/WriteSecretHttp.js.map +1 -0
- package/lib/WriteVolume.d.ts +39 -0
- package/lib/WriteVolume.d.ts.map +1 -0
- package/lib/WriteVolume.js +3 -0
- package/lib/WriteVolume.js.map +1 -0
- package/lib/WriteVolumeHttp.d.ts +12 -0
- package/lib/WriteVolumeHttp.d.ts.map +1 -0
- package/lib/WriteVolumeHttp.js +24 -0
- package/lib/WriteVolumeHttp.js.map +1 -0
- package/lib/index.d.ts +80 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +78 -0
- package/lib/index.js.map +1 -0
- package/lib/internal/Declared.d.ts +29 -0
- package/lib/internal/Declared.d.ts.map +1 -0
- package/lib/internal/Declared.js +31 -0
- package/lib/internal/Declared.js.map +1 -0
- package/lib/internal/Deploy.d.ts +110 -0
- package/lib/internal/Deploy.d.ts.map +1 -0
- package/lib/internal/Deploy.js +230 -0
- package/lib/internal/Deploy.js.map +1 -0
- package/lib/internal/Entrypoint.d.ts +52 -0
- package/lib/internal/Entrypoint.d.ts.map +1 -0
- package/lib/internal/Entrypoint.js +193 -0
- package/lib/internal/Entrypoint.js.map +1 -0
- package/lib/internal/Rpc.d.ts +17 -0
- package/lib/internal/Rpc.d.ts.map +1 -0
- package/lib/internal/Rpc.js +49 -0
- package/lib/internal/Rpc.js.map +1 -0
- package/lib/internal/RpcAuth.d.ts +41 -0
- package/lib/internal/RpcAuth.d.ts.map +1 -0
- package/lib/internal/RpcAuth.js +100 -0
- package/lib/internal/RpcAuth.js.map +1 -0
- package/lib/internal/Runtime.d.ts +127 -0
- package/lib/internal/Runtime.d.ts.map +1 -0
- package/lib/internal/Runtime.js +246 -0
- package/lib/internal/Runtime.js.map +1 -0
- package/lib/internal/Tags.d.ts +19 -0
- package/lib/internal/Tags.d.ts.map +1 -0
- package/lib/internal/Tags.js +33 -0
- package/lib/internal/Tags.js.map +1 -0
- package/lib/internal/TcpConnect.d.ts +12 -0
- package/lib/internal/TcpConnect.d.ts.map +1 -0
- package/lib/internal/TcpConnect.js +46 -0
- package/lib/internal/TcpConnect.js.map +1 -0
- package/lib/internal/Transport.d.ts +13 -0
- package/lib/internal/Transport.d.ts.map +1 -0
- package/lib/internal/Transport.js +16 -0
- package/lib/internal/Transport.js.map +1 -0
- package/lib/internal.d.ts +71 -0
- package/lib/internal.d.ts.map +1 -0
- package/lib/internal.js +102 -0
- package/lib/internal.js.map +1 -0
- package/package.json +59 -3
- package/src/App.ts +262 -0
- package/src/AuthProvider.ts +295 -0
- package/src/BindingHttp.ts +112 -0
- package/src/Blob.ts +71 -0
- package/src/Bootstrap.ts +52 -0
- package/src/Cbor.ts +158 -0
- package/src/ConnectSandbox.ts +99 -0
- package/src/Container.ts +121 -0
- package/src/Contributions.ts +9 -0
- package/src/CreateDict.ts +95 -0
- package/src/CreateDictHttp.ts +91 -0
- package/src/CreateImage.ts +105 -0
- package/src/CreateImageHttp.ts +117 -0
- package/src/CreateQueue.ts +95 -0
- package/src/CreateQueueHttp.ts +91 -0
- package/src/CreateSandbox.ts +228 -0
- package/src/CreateSandboxHttp.ts +160 -0
- package/src/CreateSecret.ts +109 -0
- package/src/CreateSecretHttp.ts +83 -0
- package/src/CreateVolume.ts +96 -0
- package/src/CreateVolumeHttp.ts +93 -0
- package/src/Credentials.ts +74 -0
- package/src/Dict.ts +245 -0
- package/src/DictClient.ts +166 -0
- package/src/Domain.ts +147 -0
- package/src/Endpoint.ts +406 -0
- package/src/Environment.ts +373 -0
- package/src/FetchServer.ts +126 -0
- package/src/FetchServerHttp.ts +110 -0
- package/src/Function.ts +397 -0
- package/src/FunctionAutoscaler.ts +210 -0
- package/src/FunctionTarget.ts +181 -0
- package/src/Image.ts +281 -0
- package/src/ImageBuild.ts +228 -0
- package/src/ImagePlan.ts +78 -0
- package/src/InferEnv.ts +27 -0
- package/src/InvokeFunction.ts +211 -0
- package/src/InvokeFunctionHttp.ts +316 -0
- package/src/ModalBindings.ts +310 -0
- package/src/MountBucket.ts +137 -0
- package/src/MountVolume.ts +113 -0
- package/src/Mounts.ts +230 -0
- package/src/PersistentSandbox.ts +142 -0
- package/src/Providers.ts +100 -0
- package/src/Proxy.ts +191 -0
- package/src/Queue.ts +258 -0
- package/src/QueueClient.ts +149 -0
- package/src/ReadApp.ts +101 -0
- package/src/ReadAppHttp.ts +142 -0
- package/src/ReadDict.ts +44 -0
- package/src/ReadDictHttp.ts +27 -0
- package/src/ReadFunction.ts +107 -0
- package/src/ReadFunctionHttp.ts +91 -0
- package/src/ReadQueue.ts +44 -0
- package/src/ReadQueueHttp.ts +27 -0
- package/src/ReadServer.ts +98 -0
- package/src/ReadServerHttp.ts +101 -0
- package/src/ReadVolume.ts +45 -0
- package/src/ReadVolumeHttp.ts +28 -0
- package/src/ReadWriteDict.ts +44 -0
- package/src/ReadWriteDictHttp.ts +27 -0
- package/src/ReadWriteQueue.ts +45 -0
- package/src/ReadWriteQueueHttp.ts +27 -0
- package/src/ReadWriteVolume.ts +45 -0
- package/src/ReadWriteVolumeHttp.ts +28 -0
- package/src/RuntimeOwner.ts +111 -0
- package/src/Sandbox.ts +402 -0
- package/src/SandboxClient.ts +1404 -0
- package/src/ScaleFunction.ts +70 -0
- package/src/ScaleFunctionHttp.ts +54 -0
- package/src/Secret.ts +296 -0
- package/src/Server.ts +285 -0
- package/src/ServiceUser.ts +295 -0
- package/src/Volume.ts +320 -0
- package/src/VolumeClient.ts +108 -0
- package/src/VolumeFiles.ts +194 -0
- package/src/WebhookToken.ts +230 -0
- package/src/WorkspaceSettings.ts +160 -0
- package/src/WriteApp.ts +43 -0
- package/src/WriteAppHttp.ts +51 -0
- package/src/WriteDict.ts +44 -0
- package/src/WriteDictHttp.ts +27 -0
- package/src/WriteQueue.ts +44 -0
- package/src/WriteQueueHttp.ts +27 -0
- package/src/WriteSecret.ts +60 -0
- package/src/WriteSecretHttp.ts +56 -0
- package/src/WriteVolume.ts +44 -0
- package/src/WriteVolumeHttp.ts +28 -0
- package/src/globals.d.ts +2 -0
- package/src/index.ts +85 -0
- package/src/internal/Declared.ts +80 -0
- package/src/internal/Deploy.ts +309 -0
- package/src/internal/Entrypoint.ts +198 -0
- package/src/internal/Rpc.ts +59 -0
- package/src/internal/RpcAuth.ts +132 -0
- package/src/internal/Runtime.ts +386 -0
- package/src/internal/Tags.ts +44 -0
- package/src/internal/TcpConnect.ts +78 -0
- package/src/internal/Transport.ts +25 -0
- package/src/internal.ts +147 -0
|
@@ -0,0 +1,1404 @@
|
|
|
1
|
+
import * as persistent from "@distilled.cloud/modal/persistent";
|
|
2
|
+
import * as sandbox from "@distilled.cloud/modal/sandbox";
|
|
3
|
+
import * as router from "@distilled.cloud/modal/task_command_router";
|
|
4
|
+
import type { RuntimeContext } from "alchemy/RuntimeContext";
|
|
5
|
+
import * as Sandbox from "alchemy/Sandbox";
|
|
6
|
+
import * as SecureRandom from "effect/crypto/SecureRandom";
|
|
7
|
+
import * as Data from "effect/Data";
|
|
8
|
+
import * as DateTime from "effect/DateTime";
|
|
9
|
+
import * as Effect from "effect/Effect";
|
|
10
|
+
import * as HttpClient from "effect/http/HttpClient";
|
|
11
|
+
import type * as HttpClientError from "effect/http/HttpClientError";
|
|
12
|
+
import * as HttpClientRequest from "effect/http/HttpClientRequest";
|
|
13
|
+
import type * as HttpClientResponse from "effect/http/HttpClientResponse";
|
|
14
|
+
import type * as HttpServerRequest from "effect/http/HttpServerRequest";
|
|
15
|
+
import type * as HttpServerResponse from "effect/http/HttpServerResponse";
|
|
16
|
+
import * as Schedule from "effect/Schedule";
|
|
17
|
+
import type * as Scope from "effect/Scope";
|
|
18
|
+
import type * as Socket from "effect/socket/Socket";
|
|
19
|
+
import * as Stream from "effect/Stream";
|
|
20
|
+
import { buildImage, gpuConfig, ImageBuildOps, type ImageBuildSpec } from "./ImageBuild.ts";
|
|
21
|
+
import { environmentOf, type ErrorOf, operations, seconds } from "./internal.ts";
|
|
22
|
+
import { connectTcp } from "./internal/TcpConnect.ts";
|
|
23
|
+
import { streamRouter, withRouter } from "./ModalBindings.ts";
|
|
24
|
+
import { type ModalMounts, modalMountsOf, type SandboxMount } from "./Mounts.ts";
|
|
25
|
+
|
|
26
|
+
/** A sandbox ended (or never became ready) before the call could finish. */
|
|
27
|
+
export class SandboxNotRunning extends Data.TaggedError("Modal.SandboxNotRunning")<{
|
|
28
|
+
readonly sandboxId: string;
|
|
29
|
+
readonly message: string;
|
|
30
|
+
}> {}
|
|
31
|
+
|
|
32
|
+
/** A Modal call returned without the field the client needs. */
|
|
33
|
+
export class SandboxUnexpectedResponse extends Data.TaggedError("Modal.SandboxUnexpectedResponse")<{
|
|
34
|
+
readonly message: string;
|
|
35
|
+
}> {}
|
|
36
|
+
|
|
37
|
+
/** A filesystem operation inside the sandbox failed (missing path, permissions). */
|
|
38
|
+
export class SandboxFilesystemError extends Data.TaggedError("Modal.SandboxFilesystemError")<{
|
|
39
|
+
readonly path: string;
|
|
40
|
+
readonly message: string;
|
|
41
|
+
}> {}
|
|
42
|
+
|
|
43
|
+
/** A Modal duration: seconds, or a string like `"10 minutes"`. */
|
|
44
|
+
export type DurationInput = number | string;
|
|
45
|
+
|
|
46
|
+
/** An image a sandbox boots from: a registry ref, or a `Modal.Image` (resolved). */
|
|
47
|
+
export type SandboxImage = string | { readonly imageId: string };
|
|
48
|
+
|
|
49
|
+
/** Where a route sends matching traffic: blocked, through a `Modal.Proxy`, or to a sidecar. */
|
|
50
|
+
export type OutboundRedirect =
|
|
51
|
+
| "block"
|
|
52
|
+
| { readonly proxy: { readonly proxyId: string } }
|
|
53
|
+
| { readonly sidecar: string };
|
|
54
|
+
|
|
55
|
+
/** An inclusive port range, or one port. */
|
|
56
|
+
export type OutboundPorts = ReadonlyArray<
|
|
57
|
+
number | { readonly start: number; readonly end: number }
|
|
58
|
+
>;
|
|
59
|
+
|
|
60
|
+
/** Set headers on decrypted HTTPS requests, with values from a secret. */
|
|
61
|
+
export interface HeaderReplacement {
|
|
62
|
+
/** The secret whose keys the header values reference. Omit for static values. */
|
|
63
|
+
readonly secret?: { readonly secretId: string };
|
|
64
|
+
/** Header name to value; `$KEY` is replaced with the secret's `KEY`, `$$` is a literal `$`. */
|
|
65
|
+
readonly headers: Record<string, string>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* One route of an {@link OutboundPolicy}. Routes match in order; traffic a
|
|
70
|
+
* route matches without a `redirect` goes to its destination.
|
|
71
|
+
*/
|
|
72
|
+
export type OutboundRoute =
|
|
73
|
+
| {
|
|
74
|
+
/** Traffic to these CIDR ranges. */
|
|
75
|
+
readonly ip: {
|
|
76
|
+
readonly cidrs: ReadonlyArray<string>;
|
|
77
|
+
/** @default every port */
|
|
78
|
+
readonly ports?: OutboundPorts;
|
|
79
|
+
readonly transport?: "tcp" | "udp" | "any";
|
|
80
|
+
readonly redirect?: OutboundRedirect;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
| {
|
|
84
|
+
/** TLS connections to these domains (`*.` wildcards allowed), never decrypted. */
|
|
85
|
+
readonly tls: {
|
|
86
|
+
readonly domains: ReadonlyArray<string>;
|
|
87
|
+
/** @default 443 */
|
|
88
|
+
readonly ports?: OutboundPorts;
|
|
89
|
+
readonly redirect?: OutboundRedirect;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
| {
|
|
93
|
+
/** HTTPS requests to these domains, decrypted so headers can be replaced. */
|
|
94
|
+
readonly https: {
|
|
95
|
+
readonly domains: ReadonlyArray<string>;
|
|
96
|
+
/** @default 443 */
|
|
97
|
+
readonly ports?: OutboundPorts;
|
|
98
|
+
readonly headerReplacements?: ReadonlyArray<HeaderReplacement>;
|
|
99
|
+
readonly redirect?: OutboundRedirect;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
| {
|
|
103
|
+
/** Everything no earlier route matched. Must be the last route. */
|
|
104
|
+
readonly any: { readonly redirect?: OutboundRedirect };
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* A sandbox's outbound traffic rules, enforced by Modal outside the
|
|
109
|
+
* sandbox. HTTPS routes decrypt traffic with a certificate authority the
|
|
110
|
+
* sandbox trusts, so Modal can add credentials the sandbox never sees.
|
|
111
|
+
*/
|
|
112
|
+
export interface OutboundPolicy {
|
|
113
|
+
readonly routes: ReadonlyArray<OutboundRoute>;
|
|
114
|
+
/**
|
|
115
|
+
* Let HTTPS routes decrypt traffic. Fixed at creation.
|
|
116
|
+
* @default true when a route is `https`
|
|
117
|
+
*/
|
|
118
|
+
readonly tlsDecryption?: boolean;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Options for creating a sandbox: Modal's own sandbox parameters. */
|
|
122
|
+
export interface SandboxCreateOptions {
|
|
123
|
+
/** Image to boot. Defaults to the binding's image, else `debian:bookworm-slim`. */
|
|
124
|
+
readonly image?: SandboxImage;
|
|
125
|
+
/**
|
|
126
|
+
* The sandbox's main process. Without one the sandbox idles (`sleep`) and
|
|
127
|
+
* you drive it with {@link SandboxHandle.exec}.
|
|
128
|
+
*/
|
|
129
|
+
readonly command?: ReadonlyArray<string>;
|
|
130
|
+
/** Working directory of the main process and of exec'd commands. */
|
|
131
|
+
readonly workdir?: string;
|
|
132
|
+
/** Environment variables for the main process. */
|
|
133
|
+
readonly env?: Record<string, string>;
|
|
134
|
+
/** Secrets whose variables the sandbox sees as environment variables. */
|
|
135
|
+
readonly secrets?: ReadonlyArray<{ readonly secretId: string }>;
|
|
136
|
+
/**
|
|
137
|
+
* Volumes and cloud buckets to mount: volume handles, bucket names, or
|
|
138
|
+
* the references `Modal.MountVolume` / `Modal.MountBucket` return.
|
|
139
|
+
*/
|
|
140
|
+
readonly mounts?: ReadonlyArray<SandboxMount>;
|
|
141
|
+
/** CPU cores (fractional allowed). */
|
|
142
|
+
readonly cpu?: number;
|
|
143
|
+
/** Memory in MiB. */
|
|
144
|
+
readonly memory?: number;
|
|
145
|
+
/** GPU: `"T4"`, `"A10G"`, `"A100-80GB"`, `"H100"`, with an optional `":<count>"`. */
|
|
146
|
+
readonly gpu?: string;
|
|
147
|
+
/** Ephemeral disk in MiB. */
|
|
148
|
+
readonly ephemeralDisk?: number;
|
|
149
|
+
/** Most time the sandbox lives. @default 5 minutes (Modal's default); at most 24 hours */
|
|
150
|
+
readonly timeout?: DurationInput;
|
|
151
|
+
/** Terminate after this long without exec'd commands or connections. */
|
|
152
|
+
readonly idleTimeout?: DurationInput;
|
|
153
|
+
/** Name, unique among the app's running sandboxes; find it again with `fromName`. */
|
|
154
|
+
readonly name?: string;
|
|
155
|
+
/** Tags, for `list({ tags })`. */
|
|
156
|
+
readonly tags?: Record<string, string>;
|
|
157
|
+
/** Regions to run in (`"us-east"`, `"eu"`, ...). */
|
|
158
|
+
readonly regions?: ReadonlyArray<string>;
|
|
159
|
+
/** Block all network access. */
|
|
160
|
+
readonly blockNetwork?: boolean;
|
|
161
|
+
/** Allow outbound traffic only to these CIDR ranges (and {@link allowedDomains}). */
|
|
162
|
+
readonly cidrAllowlist?: ReadonlyArray<string>;
|
|
163
|
+
/** Allow outbound traffic only to these domains (and {@link cidrAllowlist}). */
|
|
164
|
+
readonly allowedDomains?: ReadonlyArray<string>;
|
|
165
|
+
/** Allow inbound connections (tunnels) only from these CIDR ranges. */
|
|
166
|
+
readonly inboundCidrAllowlist?: ReadonlyArray<string>;
|
|
167
|
+
/** Outbound header injection: secrets the sandbox never sees. */
|
|
168
|
+
readonly outboundPolicy?: OutboundPolicy;
|
|
169
|
+
/** Route outbound traffic through a `Modal.Proxy`'s static IPs. */
|
|
170
|
+
readonly proxy?: { readonly proxyId: string };
|
|
171
|
+
/**
|
|
172
|
+
* Give the sandbox a `MODAL_IDENTITY_TOKEN` environment variable: an OIDC
|
|
173
|
+
* token Modal signs, naming the workspace, environment, app and sandbox.
|
|
174
|
+
* A cloud that trusts Modal's issuer (`https://oidc.modal.com`) exchanges
|
|
175
|
+
* it for short-lived credentials, so no long-lived key enters the sandbox.
|
|
176
|
+
*/
|
|
177
|
+
readonly includeOidcIdentityToken?: boolean;
|
|
178
|
+
/**
|
|
179
|
+
* Serve the sandbox an X.509 certificate that names it (an X.509-SVID),
|
|
180
|
+
* over the SPIFFE Workload API socket at `SPIFFE_ENDPOINT_SOCKET`. A
|
|
181
|
+
* cloud that trusts Modal's certificate authority (AWS IAM Roles
|
|
182
|
+
* Anywhere) exchanges it for short-lived credentials.
|
|
183
|
+
*/
|
|
184
|
+
readonly includeX509Svid?: boolean;
|
|
185
|
+
/** Ports reachable through TLS tunnels (`sandbox.getTcpPort(port)`). */
|
|
186
|
+
readonly encryptedPorts?: ReadonlyArray<number>;
|
|
187
|
+
/** Ports reachable through plain TCP tunnels. */
|
|
188
|
+
readonly unencryptedPorts?: ReadonlyArray<number>;
|
|
189
|
+
/** Ports reachable through HTTP/2 tunnels. */
|
|
190
|
+
readonly h2Ports?: ReadonlyArray<number>;
|
|
191
|
+
/** Allow memory snapshots of this sandbox (`snapshotMemory`). */
|
|
192
|
+
readonly enableSnapshot?: boolean;
|
|
193
|
+
/** Serve tunnels on a custom domain registered with Modal. */
|
|
194
|
+
readonly customDomain?: string;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** What a finished command printed, and its exit code. */
|
|
198
|
+
export interface ExecResult {
|
|
199
|
+
readonly exitCode: number;
|
|
200
|
+
readonly stdout: string;
|
|
201
|
+
readonly stderr: string;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** A command: a shell string (run with `sh -c`), or an argument vector (no shell). */
|
|
205
|
+
export type Command = Sandbox.Command;
|
|
206
|
+
|
|
207
|
+
/** Options for {@link SandboxHandleOf.exec}. */
|
|
208
|
+
export interface ExecOptions {
|
|
209
|
+
/** Working directory. @default the sandbox's `workdir` */
|
|
210
|
+
readonly cwd?: string;
|
|
211
|
+
/** Extra environment variables for this command. */
|
|
212
|
+
readonly env?: Record<string, string>;
|
|
213
|
+
/** Secrets whose variables this command sees. */
|
|
214
|
+
readonly secrets?: ReadonlyArray<{ readonly secretId: string }>;
|
|
215
|
+
/** Kill the command after this long. */
|
|
216
|
+
readonly timeout?: DurationInput;
|
|
217
|
+
/** Bytes (or text) written to the command's stdin, then closed. */
|
|
218
|
+
readonly stdin?: string | Uint8Array;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** A filesystem entry inside a sandbox. */
|
|
222
|
+
export interface SandboxFileEntry {
|
|
223
|
+
readonly path: string;
|
|
224
|
+
readonly type: "file" | "directory" | "symlink" | "other";
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** What `files.stat` reports about a path. */
|
|
228
|
+
export interface SandboxFileStat extends SandboxFileEntry {
|
|
229
|
+
/** Size in bytes. */
|
|
230
|
+
readonly size: number;
|
|
231
|
+
/** Last modification time. */
|
|
232
|
+
readonly modifiedAt: DateTime.Utc;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** A tunnel to a port inside the sandbox. */
|
|
236
|
+
export interface SandboxTunnel {
|
|
237
|
+
/** `https://…` (or `tcp://…` for unencrypted tunnels). */
|
|
238
|
+
readonly url: string;
|
|
239
|
+
readonly host: string;
|
|
240
|
+
readonly port: number;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** The error of one of {@link SandboxOps}. */
|
|
244
|
+
export type SandboxOpError<K extends keyof SandboxOps> = ErrorOf<SandboxOps[K]>;
|
|
245
|
+
type RouterOpError<K extends keyof typeof routerOps> =
|
|
246
|
+
Effect.Success<ReturnType<(typeof routerOps)[K]>> extends (...args: any[]) => infer Out
|
|
247
|
+
? Out extends Effect.Effect<any, infer E, any>
|
|
248
|
+
? E
|
|
249
|
+
: Out extends Stream.Stream<any, infer E, any>
|
|
250
|
+
? E
|
|
251
|
+
: never
|
|
252
|
+
: never;
|
|
253
|
+
|
|
254
|
+
/** Reaching a sandbox's task: it ended, or Modal's task lookup failed. */
|
|
255
|
+
export type SandboxTaskError =
|
|
256
|
+
| SandboxNotRunning
|
|
257
|
+
| SandboxUnexpectedResponse
|
|
258
|
+
| SandboxOpError<"getTaskId">
|
|
259
|
+
| SandboxOpError<"getCommandRouterAccess">;
|
|
260
|
+
|
|
261
|
+
/** Running a command in a sandbox. */
|
|
262
|
+
export type SandboxExecError =
|
|
263
|
+
| SandboxTaskError
|
|
264
|
+
| RouterOpError<"execStart" | "execStdinWrite" | "execStdioRead" | "execWait">;
|
|
265
|
+
|
|
266
|
+
/** A started process: running it, following its output, writing its stdin, signalling it. */
|
|
267
|
+
export type SandboxProcessError = SandboxExecError | RouterOpError<"execStdioFollow">;
|
|
268
|
+
|
|
269
|
+
/** Modal's task command router has no call to resize a running PTY. */
|
|
270
|
+
export class SandboxPtyResizeUnsupported extends Data.TaggedError(
|
|
271
|
+
"Modal.SandboxPtyResizeUnsupported",
|
|
272
|
+
)<{
|
|
273
|
+
readonly message: string;
|
|
274
|
+
}> {}
|
|
275
|
+
|
|
276
|
+
/** A terminal session's errors. */
|
|
277
|
+
export type SandboxPtyError = SandboxProcessError | SandboxPtyResizeUnsupported;
|
|
278
|
+
|
|
279
|
+
/** Options for `pty.open`: the terminal size, and what runs in it. */
|
|
280
|
+
export interface SandboxPtyOptions extends Sandbox.PtyOptions {
|
|
281
|
+
/** Secrets whose variables the session sees. */
|
|
282
|
+
readonly secrets?: ReadonlyArray<{ readonly secretId: string }>;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** A file operation inside a sandbox (it runs as a command). */
|
|
286
|
+
export type SandboxFilesError = SandboxExecError | SandboxFilesystemError;
|
|
287
|
+
|
|
288
|
+
/** Taking a snapshot or changing a running sandbox through its command router. */
|
|
289
|
+
export type SandboxRouterError<K extends keyof typeof routerOps> =
|
|
290
|
+
| SandboxTaskError
|
|
291
|
+
| RouterOpError<K>;
|
|
292
|
+
|
|
293
|
+
/** The files inside a sandbox, read and written with commands its image has (`sh`, `base64`, `stat`). */
|
|
294
|
+
export interface SandboxFilesOf<R> {
|
|
295
|
+
/** Read a file. */
|
|
296
|
+
read(path: string): Effect.Effect<Uint8Array, SandboxFilesError, R>;
|
|
297
|
+
/** Read a UTF-8 file. */
|
|
298
|
+
readText(path: string): Effect.Effect<string, SandboxFilesError, R>;
|
|
299
|
+
/** Write (create or truncate) a file, creating its directory. */
|
|
300
|
+
write(path: string, data: string | Uint8Array): Effect.Effect<void, SandboxFilesError, R>;
|
|
301
|
+
/** List a directory. */
|
|
302
|
+
list(path: string): Effect.Effect<ReadonlyArray<SandboxFileEntry>, SandboxFilesError, R>;
|
|
303
|
+
/** A path's type, size and modification time. */
|
|
304
|
+
stat(path: string): Effect.Effect<SandboxFileStat, SandboxFilesError, R>;
|
|
305
|
+
/** Remove a file, or a directory with `recursive`. */
|
|
306
|
+
remove(
|
|
307
|
+
path: string,
|
|
308
|
+
options?: { readonly recursive?: boolean },
|
|
309
|
+
): Effect.Effect<void, SandboxFilesError, R>;
|
|
310
|
+
/** Create a directory and its parents. */
|
|
311
|
+
makeDir(path: string): Effect.Effect<void, SandboxFilesError, R>;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** A running (or finished) Modal sandbox. Every method calls Modal's API. */
|
|
315
|
+
export interface SandboxHandleOf<R> {
|
|
316
|
+
readonly sandboxId: string;
|
|
317
|
+
/**
|
|
318
|
+
* Run a command and wait for it. A string runs with `sh -c`; an argument
|
|
319
|
+
* vector runs without a shell. A non-zero exit code is returned, not failed.
|
|
320
|
+
*/
|
|
321
|
+
exec(command: Command, options?: ExecOptions): Effect.Effect<ExecResult, SandboxExecError, R>;
|
|
322
|
+
/**
|
|
323
|
+
* Start a command and follow it: its output streams as it runs, `write`
|
|
324
|
+
* feeds its stdin, `kill` signals it. Closing the scope detaches; the
|
|
325
|
+
* command keeps running.
|
|
326
|
+
*/
|
|
327
|
+
start(
|
|
328
|
+
command: Command,
|
|
329
|
+
options?: ExecOptions,
|
|
330
|
+
): Effect.Effect<Sandbox.Process<SandboxProcessError>, SandboxProcessError, R | Scope.Scope>;
|
|
331
|
+
/**
|
|
332
|
+
* Terminals: `open` starts a login shell (or `command`) on a PTY of the
|
|
333
|
+
* given size. Modal can't resize a running PTY, so `resize` fails with
|
|
334
|
+
* {@link SandboxPtyResizeUnsupported}.
|
|
335
|
+
*/
|
|
336
|
+
readonly pty: Sandbox.Pty<SandboxPtyError, SandboxPtyOptions>;
|
|
337
|
+
/** The sandbox's files. */
|
|
338
|
+
readonly files: SandboxFilesOf<R>;
|
|
339
|
+
/** Tunnels to the sandbox's open ports, by container port. */
|
|
340
|
+
tunnels(
|
|
341
|
+
timeout?: DurationInput,
|
|
342
|
+
): Effect.Effect<Record<number, SandboxTunnel>, SandboxOpError<"getTunnels">, R>;
|
|
343
|
+
/**
|
|
344
|
+
* A token-authenticated URL for one port, for a browser or client that
|
|
345
|
+
* shouldn't hold Modal credentials.
|
|
346
|
+
*/
|
|
347
|
+
connectToken(options?: {
|
|
348
|
+
readonly port?: number;
|
|
349
|
+
readonly userMetadata?: string;
|
|
350
|
+
}): Effect.Effect<
|
|
351
|
+
{ readonly url: string; readonly token: string },
|
|
352
|
+
SandboxOpError<"createConnectToken"> | SandboxUnexpectedResponse,
|
|
353
|
+
R
|
|
354
|
+
>;
|
|
355
|
+
/** Capture the filesystem as a new image (usable as `image` later). */
|
|
356
|
+
snapshotFilesystem(
|
|
357
|
+
timeout?: DurationInput,
|
|
358
|
+
): Effect.Effect<{ readonly imageId: string }, SandboxRouterError<"snapshotFilesystem">, R>;
|
|
359
|
+
/** Capture memory and filesystem (needs `enableSnapshot`); restore with `restore`. */
|
|
360
|
+
snapshotMemory(): Effect.Effect<
|
|
361
|
+
{ readonly snapshotId: string },
|
|
362
|
+
SandboxRouterError<"snapshotMemory">,
|
|
363
|
+
R
|
|
364
|
+
>;
|
|
365
|
+
/** Capture one directory as an image (mount it into another sandbox with `mountDirectory`). */
|
|
366
|
+
snapshotDirectory(
|
|
367
|
+
path: string,
|
|
368
|
+
): Effect.Effect<{ readonly imageId: string }, SandboxRouterError<"snapshotDirectory">, R>;
|
|
369
|
+
/** Mount a directory snapshot at `path`. */
|
|
370
|
+
mountDirectory(
|
|
371
|
+
path: string,
|
|
372
|
+
snapshot: { readonly imageId: string },
|
|
373
|
+
): Effect.Effect<void, SandboxRouterError<"mountDirectory">, R>;
|
|
374
|
+
/**
|
|
375
|
+
* Replace the sandbox's outbound network policy while it runs. Connections
|
|
376
|
+
* the new policy no longer allows are closed. `{}` opens the network.
|
|
377
|
+
*/
|
|
378
|
+
setNetworkAccess(access: {
|
|
379
|
+
readonly blockNetwork?: boolean;
|
|
380
|
+
readonly cidrAllowlist?: ReadonlyArray<string>;
|
|
381
|
+
readonly allowedDomains?: ReadonlyArray<string>;
|
|
382
|
+
}): Effect.Effect<void, SandboxRouterError<"setNetworkAccess">, R>;
|
|
383
|
+
/** Replace the outbound header injection rules while it runs. */
|
|
384
|
+
setOutboundPolicy(
|
|
385
|
+
policy: OutboundPolicy,
|
|
386
|
+
): Effect.Effect<void, SandboxOpError<"setOutboundPolicy">, R>;
|
|
387
|
+
/** Replace the sandbox's tags. */
|
|
388
|
+
setTags(tags: Record<string, string>): Effect.Effect<void, SandboxOpError<"setTags">, R>;
|
|
389
|
+
/** The sandbox's tags. */
|
|
390
|
+
getTags(): Effect.Effect<Record<string, string>, SandboxOpError<"getTags">, R>;
|
|
391
|
+
/** Name the sandbox (unique among its app's running sandboxes) so `fromName` finds it. */
|
|
392
|
+
setName(name: string): Effect.Effect<void, SandboxOpError<"setName">, R>;
|
|
393
|
+
/** CPU, memory and GPU used so far (core-seconds, GiB-seconds, GPU-seconds). */
|
|
394
|
+
resourceUsage(): Effect.Effect<
|
|
395
|
+
{ readonly cpuSeconds: number; readonly memoryGibSeconds: number; readonly gpuSeconds: number },
|
|
396
|
+
SandboxOpError<"getResourceUsage">,
|
|
397
|
+
R
|
|
398
|
+
>;
|
|
399
|
+
/**
|
|
400
|
+
* The main process's stdout so far: what the sandbox's command router sends
|
|
401
|
+
* within `timeout` (default 5 seconds), or all of it once the program has
|
|
402
|
+
* exited.
|
|
403
|
+
*/
|
|
404
|
+
logs(options?: {
|
|
405
|
+
readonly timeout?: DurationInput;
|
|
406
|
+
}): Effect.Effect<string, SandboxRouterError<"mainStdio">, R>;
|
|
407
|
+
/** Wait for the sandbox to finish; `undefined` exit code while still running. */
|
|
408
|
+
wait(
|
|
409
|
+
timeout?: DurationInput,
|
|
410
|
+
): Effect.Effect<
|
|
411
|
+
{ readonly exitCode?: number; readonly status: string },
|
|
412
|
+
SandboxOpError<"wait">,
|
|
413
|
+
R
|
|
414
|
+
>;
|
|
415
|
+
/** Terminate the sandbox (no-op when it already finished). */
|
|
416
|
+
terminate(): Effect.Effect<
|
|
417
|
+
void,
|
|
418
|
+
Exclude<SandboxOpError<"terminate">, { readonly _tag: "SandboxNotFound" }>,
|
|
419
|
+
R
|
|
420
|
+
>;
|
|
421
|
+
/**
|
|
422
|
+
* A port inside the sandbox, reached through its tunnel: `fetch` sends a
|
|
423
|
+
* client request there, or proxies a server request a handler received;
|
|
424
|
+
* `connect` opens a TCP socket to it. The port needs a tunnel
|
|
425
|
+
* (`encryptedPorts`, `unencryptedPorts` or `h2Ports`); without a port
|
|
426
|
+
* number, the `Modal.Sandbox` program's.
|
|
427
|
+
*/
|
|
428
|
+
getTcpPort(port?: number): Effect.Effect<SandboxPortOf<R>, SandboxPortError, R>;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** Looking up a sandbox port's tunnel. */
|
|
432
|
+
export type SandboxPortError = SandboxOpError<"getTunnels"> | SandboxUnexpectedResponse;
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* A port inside a sandbox, with the tunnel Modal opened to it. `fetch` keeps
|
|
436
|
+
* the request's method, path, query, headers and body, and sends it to the
|
|
437
|
+
* tunnel; given the server request a handler received, it proxies it and
|
|
438
|
+
* returns the server response.
|
|
439
|
+
*/
|
|
440
|
+
export interface SandboxPortOf<R> extends SandboxTunnel {
|
|
441
|
+
fetch(
|
|
442
|
+
request: HttpClientRequest.HttpClientRequest,
|
|
443
|
+
): Effect.Effect<
|
|
444
|
+
HttpClientResponse.HttpClientResponse,
|
|
445
|
+
SandboxPortError | HttpClientError.HttpClientError,
|
|
446
|
+
R
|
|
447
|
+
>;
|
|
448
|
+
fetch(
|
|
449
|
+
request: HttpServerRequest.HttpServerRequest,
|
|
450
|
+
): Effect.Effect<
|
|
451
|
+
HttpServerResponse.HttpServerResponse,
|
|
452
|
+
SandboxPortError | HttpClientError.HttpClientError,
|
|
453
|
+
R
|
|
454
|
+
>;
|
|
455
|
+
/**
|
|
456
|
+
* A TCP socket to the port through its tunnel: TLS for an encrypted tunnel
|
|
457
|
+
* (`encryptedPorts`), plain TCP for an unencrypted one (`unencryptedPorts`).
|
|
458
|
+
* It connects when run.
|
|
459
|
+
*/
|
|
460
|
+
connect(): Effect.Effect<Socket.Socket>;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** A sandbox port as bindings return it. */
|
|
464
|
+
export type SandboxPort = SandboxPortOf<RuntimeContext>;
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* A sandbox handle as bindings return it: calls run inside a deployed
|
|
468
|
+
* runtime. It has the shared `alchemy/Sandbox` shape plus Modal's own calls.
|
|
469
|
+
*/
|
|
470
|
+
export interface SandboxHandle
|
|
471
|
+
extends
|
|
472
|
+
SandboxHandleOf<RuntimeContext>,
|
|
473
|
+
Sandbox.SandboxExec<SandboxExecError, ExecResult, ExecOptions>,
|
|
474
|
+
Sandbox.SandboxProcesses<SandboxProcessError, ExecOptions>,
|
|
475
|
+
Sandbox.SandboxFiles<SandboxFilesError, SandboxFileEntry>,
|
|
476
|
+
Sandbox.SandboxPty<SandboxPtyError, SandboxPtyOptions>,
|
|
477
|
+
Sandbox.SandboxPorts<SandboxPortError, SandboxPort> {
|
|
478
|
+
readonly files: SandboxFilesOf<RuntimeContext>;
|
|
479
|
+
getTcpPort(port?: number): Effect.Effect<SandboxPort, SandboxPortError, RuntimeContext>;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** A sandbox handle whose calls need Modal credentials and an HTTP client. */
|
|
483
|
+
export type RawSandboxHandle = SandboxHandleOf<never>;
|
|
484
|
+
|
|
485
|
+
/** Where a sandbox client creates sandboxes. */
|
|
486
|
+
export interface SandboxTarget {
|
|
487
|
+
readonly appId: string;
|
|
488
|
+
/** Environment of the app (for name lookups and tag updates). */
|
|
489
|
+
readonly environment?: string;
|
|
490
|
+
/** Name of the app (for name lookups). */
|
|
491
|
+
readonly appName?: string;
|
|
492
|
+
/** Image used when `create` names none. */
|
|
493
|
+
readonly imageId?: string;
|
|
494
|
+
/** A `Modal.Sandbox` program: the secret holding its env, its command, its port and mounts. */
|
|
495
|
+
readonly program?: {
|
|
496
|
+
readonly secretId: string;
|
|
497
|
+
readonly command: ReadonlyArray<string>;
|
|
498
|
+
readonly port: number;
|
|
499
|
+
readonly mounts?: ModalMounts;
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** The text of a command's output stream, from its router read chunks. */
|
|
504
|
+
const streamText = (chunks: ReadonlyArray<{ readonly data?: string }>) =>
|
|
505
|
+
Buffer.concat(chunks.map((chunk) => Buffer.from(chunk.data ?? "", "base64"))).toString("utf8");
|
|
506
|
+
|
|
507
|
+
const decodeBase64 = (data: string) =>
|
|
508
|
+
Effect.sync(() => new Uint8Array(Buffer.from(data, "base64")));
|
|
509
|
+
const encodeBase64 = (data: string | Uint8Array) =>
|
|
510
|
+
Effect.sync(() =>
|
|
511
|
+
typeof data === "string"
|
|
512
|
+
? Buffer.from(data, "utf8").toString("base64")
|
|
513
|
+
: Buffer.from(data).toString("base64"),
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
const tunnelTypeOf = (h2: boolean): sandbox.TunnelType | undefined =>
|
|
517
|
+
h2 ? "TUNNEL_TYPE_H2" : undefined;
|
|
518
|
+
|
|
519
|
+
/** Modal's `Sandbox` definition for {@link SandboxCreateOptions}. */
|
|
520
|
+
const definitionOf = (
|
|
521
|
+
imageId: string,
|
|
522
|
+
options: SandboxCreateOptions,
|
|
523
|
+
mounts: ModalMounts,
|
|
524
|
+
): sandbox.Sandbox => ({
|
|
525
|
+
imageId,
|
|
526
|
+
entrypointArgs: options.command ? [...options.command] : ["sleep", "infinity"],
|
|
527
|
+
workdir: options.workdir,
|
|
528
|
+
environmentVariables: options.env,
|
|
529
|
+
secretIds: options.secrets?.map((s) => s.secretId),
|
|
530
|
+
volumeMounts: mounts.volumeMounts,
|
|
531
|
+
cloudBucketMounts: mounts.cloudBucketMounts,
|
|
532
|
+
resources: {
|
|
533
|
+
milliCpu: options.cpu !== undefined ? Math.round(options.cpu * 1000) : undefined,
|
|
534
|
+
memoryMb: options.memory,
|
|
535
|
+
gpuConfig: gpuConfig(options.gpu),
|
|
536
|
+
ephemeralDiskMb: options.ephemeralDisk,
|
|
537
|
+
},
|
|
538
|
+
timeoutSecs: seconds(options.timeout),
|
|
539
|
+
idleTimeoutSecs: seconds(options.idleTimeout),
|
|
540
|
+
name: options.name,
|
|
541
|
+
schedulerPlacement: options.regions ? { regions: [...options.regions] } : undefined,
|
|
542
|
+
blockNetwork: options.blockNetwork,
|
|
543
|
+
networkAccess: networkAccessOf(options),
|
|
544
|
+
inboundCidrAllowlist: options.inboundCidrAllowlist
|
|
545
|
+
? [...options.inboundCidrAllowlist]
|
|
546
|
+
: undefined,
|
|
547
|
+
outboundPolicy: options.outboundPolicy ? outboundPolicyOf(options.outboundPolicy) : undefined,
|
|
548
|
+
proxyId: options.proxy?.proxyId,
|
|
549
|
+
includeOidcIdentityToken: options.includeOidcIdentityToken,
|
|
550
|
+
includeX509Svid: options.includeX509Svid,
|
|
551
|
+
openPorts: {
|
|
552
|
+
ports: [
|
|
553
|
+
...(options.encryptedPorts ?? []).map((port) => ({ port })),
|
|
554
|
+
...(options.unencryptedPorts ?? []).map((port) => ({ port, unencrypted: true })),
|
|
555
|
+
...(options.h2Ports ?? []).map((port) => ({ port, tunnelType: tunnelTypeOf(true) })),
|
|
556
|
+
],
|
|
557
|
+
},
|
|
558
|
+
enableSnapshot: options.enableSnapshot,
|
|
559
|
+
customDomain: options.customDomain,
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
const networkAccessOf = (options: {
|
|
563
|
+
readonly blockNetwork?: boolean;
|
|
564
|
+
readonly cidrAllowlist?: ReadonlyArray<string>;
|
|
565
|
+
readonly allowedDomains?: ReadonlyArray<string>;
|
|
566
|
+
}): sandbox.NetworkAccess | undefined =>
|
|
567
|
+
options.blockNetwork
|
|
568
|
+
? { networkAccessType: "BLOCKED" }
|
|
569
|
+
: options.cidrAllowlist !== undefined || options.allowedDomains !== undefined
|
|
570
|
+
? {
|
|
571
|
+
networkAccessType: "ALLOWLIST",
|
|
572
|
+
allowedCidrs: options.cidrAllowlist ? [...options.cidrAllowlist] : [],
|
|
573
|
+
allowedDomains: options.allowedDomains ? [...options.allowedDomains] : [],
|
|
574
|
+
}
|
|
575
|
+
: undefined;
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* The policy for a sandbox that is already running. Modal's task router
|
|
579
|
+
* accepts only an allow-list here: an empty one blocks everything, and
|
|
580
|
+
* `0.0.0.0/0` opens the network.
|
|
581
|
+
*/
|
|
582
|
+
const runningNetworkAccessOf = (access: {
|
|
583
|
+
readonly blockNetwork?: boolean;
|
|
584
|
+
readonly cidrAllowlist?: ReadonlyArray<string>;
|
|
585
|
+
readonly allowedDomains?: ReadonlyArray<string>;
|
|
586
|
+
}): sandbox.NetworkAccess => {
|
|
587
|
+
const open = access.cidrAllowlist === undefined && access.allowedDomains === undefined;
|
|
588
|
+
return {
|
|
589
|
+
networkAccessType: "ALLOWLIST",
|
|
590
|
+
allowedCidrs: access.blockNetwork
|
|
591
|
+
? []
|
|
592
|
+
: open
|
|
593
|
+
? ["0.0.0.0/0"]
|
|
594
|
+
: [...(access.cidrAllowlist ?? [])],
|
|
595
|
+
allowedDomains: access.blockNetwork ? [] : [...(access.allowedDomains ?? [])],
|
|
596
|
+
};
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
const portsOf = (ports: OutboundPorts | undefined) =>
|
|
600
|
+
ports?.map((port) => (typeof port === "number" ? { start: port, end: port } : port));
|
|
601
|
+
|
|
602
|
+
const transportOf = (transport: "tcp" | "udp" | "any" | undefined) =>
|
|
603
|
+
transport === undefined ? undefined : (`TRANSPORT_${transport.toUpperCase()}` as const);
|
|
604
|
+
|
|
605
|
+
const redirectOf = (
|
|
606
|
+
redirect: OutboundRedirect | undefined,
|
|
607
|
+
): sandbox.OutboundPolicyRedirect | undefined =>
|
|
608
|
+
redirect === undefined
|
|
609
|
+
? undefined
|
|
610
|
+
: redirect === "block"
|
|
611
|
+
? { block: {} }
|
|
612
|
+
: "proxy" in redirect
|
|
613
|
+
? { viaProxy: { proxyId: redirect.proxy.proxyId } }
|
|
614
|
+
: { viaSidecar: { sidecarName: redirect.sidecar } };
|
|
615
|
+
|
|
616
|
+
const outboundPolicyOf = (policy: OutboundPolicy): sandbox.OutboundPolicy => ({
|
|
617
|
+
tlsDecryption: {
|
|
618
|
+
allowed: policy.tlsDecryption ?? policy.routes.some((route) => "https" in route),
|
|
619
|
+
},
|
|
620
|
+
routes: policy.routes.map((route): sandbox.OutboundPolicyRoute => {
|
|
621
|
+
if ("ip" in route) {
|
|
622
|
+
return {
|
|
623
|
+
ip: {
|
|
624
|
+
cidrs: [...route.ip.cidrs],
|
|
625
|
+
ports: portsOf(route.ip.ports),
|
|
626
|
+
transport: transportOf(route.ip.transport),
|
|
627
|
+
redirect: redirectOf(route.ip.redirect),
|
|
628
|
+
},
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
if ("tls" in route) {
|
|
632
|
+
return {
|
|
633
|
+
tls: {
|
|
634
|
+
domains: [...route.tls.domains],
|
|
635
|
+
ports: portsOf(route.tls.ports),
|
|
636
|
+
redirect: redirectOf(route.tls.redirect),
|
|
637
|
+
},
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
if ("https" in route) {
|
|
641
|
+
return {
|
|
642
|
+
https: {
|
|
643
|
+
domains: [...route.https.domains],
|
|
644
|
+
ports: portsOf(route.https.ports),
|
|
645
|
+
headerReplacements: route.https.headerReplacements?.map((r) => ({
|
|
646
|
+
secretId: r.secret?.secretId,
|
|
647
|
+
headers: r.headers,
|
|
648
|
+
})),
|
|
649
|
+
redirect: redirectOf(route.https.redirect),
|
|
650
|
+
},
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
return { any: { redirect: redirectOf(route.any.redirect) } };
|
|
654
|
+
}),
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* The operations sandbox clients call, resolved once by
|
|
659
|
+
* `ModalBindings.withCredentials` (or at deploy, with the deployer's).
|
|
660
|
+
*/
|
|
661
|
+
export const SandboxOps = operations({
|
|
662
|
+
create: sandbox.sandboxCreateV2,
|
|
663
|
+
getFromName: sandbox.sandboxGetFromNameV2,
|
|
664
|
+
list: sandbox.sandboxListV2,
|
|
665
|
+
restore: sandbox.sandboxRestoreV2,
|
|
666
|
+
getTaskId: sandbox.sandboxGetTaskIdV2,
|
|
667
|
+
getCommandRouterAccess: sandbox.sandboxGetCommandRouterAccess,
|
|
668
|
+
getTunnels: sandbox.sandboxGetTunnelsV2,
|
|
669
|
+
createConnectToken: sandbox.sandboxCreateConnectTokenV2,
|
|
670
|
+
setOutboundPolicy: sandbox.sandboxSetOutboundPolicyV2,
|
|
671
|
+
setTags: sandbox.sandboxTagsSetV2,
|
|
672
|
+
getTags: sandbox.sandboxTagsGetV2,
|
|
673
|
+
setName: sandbox.sandboxSetName,
|
|
674
|
+
getResourceUsage: sandbox.sandboxGetResourceUsage,
|
|
675
|
+
wait: sandbox.sandboxWaitV2,
|
|
676
|
+
terminate: sandbox.sandboxTerminateV2,
|
|
677
|
+
usage: sandbox.sandboxGetTimeRangeStats,
|
|
678
|
+
createPersistent: persistent.createPersistentSandbox,
|
|
679
|
+
getPersistent: persistent.getPersistentSandbox,
|
|
680
|
+
resumePersistent: persistent.resumePersistentSandbox,
|
|
681
|
+
terminatePersistent: persistent.terminatePersistentSandbox,
|
|
682
|
+
images: ImageBuildOps,
|
|
683
|
+
http: HttpClient.HttpClient,
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
export type SandboxOps = Effect.Success<typeof SandboxOps>;
|
|
687
|
+
|
|
688
|
+
/** Task command router operations, resolved per sandbox with its router access. */
|
|
689
|
+
const routerOps = {
|
|
690
|
+
execStart: withRouter(router.taskExecStart),
|
|
691
|
+
execStdinWrite: withRouter(router.taskExecStdinWrite),
|
|
692
|
+
execStdioRead: withRouter(router.taskExecStdioRead),
|
|
693
|
+
execWait: withRouter(router.taskExecWait),
|
|
694
|
+
// A running command's output, each chunk as the router sends it; the
|
|
695
|
+
// stream ends when the command closes the descriptor.
|
|
696
|
+
execStdioFollow: streamRouter(router.taskExecStdioRead),
|
|
697
|
+
snapshotFilesystem: withRouter(router.taskSnapshotFilesystem),
|
|
698
|
+
snapshotMemory: withRouter(router.taskSnapshotMemory),
|
|
699
|
+
snapshotDirectory: withRouter(router.taskSnapshotDirectory),
|
|
700
|
+
mountDirectory: withRouter(router.taskMountDirectory),
|
|
701
|
+
setNetworkAccess: withRouter(router.taskSetNetworkAccess),
|
|
702
|
+
// The sandbox's own program's output (V2 sandboxes serve it here, not
|
|
703
|
+
// through the control plane's sandboxGetLogs).
|
|
704
|
+
mainStdio: streamRouter(router.sandboxStdioReadV2),
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* The image to boot: a `Modal.Image`'s id, or a registry ref built (and
|
|
709
|
+
* cached by Modal) into an image in the target app.
|
|
710
|
+
*/
|
|
711
|
+
export const resolveSandboxImage = Effect.fn(function* (
|
|
712
|
+
ops: SandboxOps,
|
|
713
|
+
target: SandboxTarget,
|
|
714
|
+
image: SandboxImage | undefined,
|
|
715
|
+
) {
|
|
716
|
+
if (image !== undefined && typeof image !== "string") return image.imageId;
|
|
717
|
+
if (image === undefined && target.imageId !== undefined) return target.imageId;
|
|
718
|
+
const spec: ImageBuildSpec = { dockerTag: image ?? "debian:bookworm-slim", commands: [] };
|
|
719
|
+
return yield* buildImage(ops.images, target.appId, spec);
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
/** The handle for an existing sandbox id. */
|
|
723
|
+
export const sandboxHandle = (
|
|
724
|
+
ops: SandboxOps,
|
|
725
|
+
sandboxId: string,
|
|
726
|
+
target: Pick<SandboxTarget, "environment" | "program">,
|
|
727
|
+
): RawSandboxHandle => {
|
|
728
|
+
let cachedTaskId: string | undefined;
|
|
729
|
+
const taskId = Effect.gen(function* () {
|
|
730
|
+
if (cachedTaskId !== undefined) return cachedTaskId;
|
|
731
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
732
|
+
const response = yield* ops.getTaskId({
|
|
733
|
+
sandboxId,
|
|
734
|
+
timeout: 55,
|
|
735
|
+
waitUntilReady: true,
|
|
736
|
+
});
|
|
737
|
+
if (response.taskId) {
|
|
738
|
+
cachedTaskId = response.taskId;
|
|
739
|
+
return response.taskId;
|
|
740
|
+
}
|
|
741
|
+
if (
|
|
742
|
+
response.taskResult?.status &&
|
|
743
|
+
response.taskResult.status !== "GENERIC_STATUS_UNSPECIFIED"
|
|
744
|
+
) {
|
|
745
|
+
return yield* new SandboxNotRunning({
|
|
746
|
+
sandboxId,
|
|
747
|
+
message: `Sandbox ${sandboxId} finished with ${response.taskResult.status}`,
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return yield* new SandboxNotRunning({
|
|
752
|
+
sandboxId,
|
|
753
|
+
message: `Sandbox ${sandboxId} wasn't scheduled within 9 minutes`,
|
|
754
|
+
});
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
const viaRouter = <Op, A, E>(
|
|
758
|
+
resolve: (access: { readonly url: string; readonly jwt: string }) => Effect.Effect<Op>,
|
|
759
|
+
f: (op: Op, taskId: string) => Effect.Effect<A, E>,
|
|
760
|
+
) =>
|
|
761
|
+
Effect.gen(function* () {
|
|
762
|
+
const id = yield* taskId;
|
|
763
|
+
const access = yield* ops.getCommandRouterAccess({ taskId: id });
|
|
764
|
+
if (!access.url || !access.jwt) {
|
|
765
|
+
return yield* new SandboxUnexpectedResponse({
|
|
766
|
+
message: `Modal returned no command router for sandbox ${sandboxId}`,
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
const op = yield* resolve({ url: access.url, jwt: access.jwt });
|
|
770
|
+
return yield* f(op, id);
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Run a filesystem command with `sh` (every image Modal boots has one) and
|
|
775
|
+
* fail with {@link SandboxFilesystemError} on a non-zero exit.
|
|
776
|
+
*/
|
|
777
|
+
const fsExec = (path: string, script: string, stdin?: string | Uint8Array) =>
|
|
778
|
+
Effect.flatMap(
|
|
779
|
+
exec(["sh", "-c", script, "sh", path], stdin === undefined ? {} : { stdin }),
|
|
780
|
+
(result) =>
|
|
781
|
+
result.exitCode === 0
|
|
782
|
+
? Effect.succeed(result.stdout)
|
|
783
|
+
: Effect.fail(
|
|
784
|
+
new SandboxFilesystemError({
|
|
785
|
+
path,
|
|
786
|
+
message: result.stderr.trim() || `exit code ${result.exitCode}`,
|
|
787
|
+
}),
|
|
788
|
+
),
|
|
789
|
+
);
|
|
790
|
+
|
|
791
|
+
const exec = Effect.fn(function* (command: Command, options: ExecOptions = {}) {
|
|
792
|
+
// V2 sandboxes run commands through the task command router: start the
|
|
793
|
+
// command under an id we choose, then read its output streams while
|
|
794
|
+
// waiting for it to exit.
|
|
795
|
+
const id = yield* taskId;
|
|
796
|
+
const access = yield* ops.getCommandRouterAccess({ taskId: id });
|
|
797
|
+
if (!access.url || !access.jwt) {
|
|
798
|
+
return yield* new SandboxUnexpectedResponse({
|
|
799
|
+
message: `Modal returned no command router for sandbox ${sandboxId}`,
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
const at = { url: access.url, jwt: access.jwt };
|
|
803
|
+
const start = yield* routerOps.execStart(at);
|
|
804
|
+
const stdinWrite = yield* routerOps.execStdinWrite(at);
|
|
805
|
+
const stdioRead = yield* routerOps.execStdioRead(at);
|
|
806
|
+
const wait = yield* routerOps.execWait(at);
|
|
807
|
+
const execId = yield* Effect.sync(() => SecureRandom.uuidV4Unsafe());
|
|
808
|
+
yield* start({
|
|
809
|
+
taskId: id,
|
|
810
|
+
execId,
|
|
811
|
+
commandArgs: typeof command === "string" ? ["sh", "-c", command] : [...command],
|
|
812
|
+
workdir: options.cwd,
|
|
813
|
+
timeoutSecs: seconds(options.timeout),
|
|
814
|
+
secretIds: options.secrets?.map((s) => s.secretId),
|
|
815
|
+
env: options.env,
|
|
816
|
+
stdoutConfig: "TASK_EXEC_STDOUT_CONFIG_PIPE",
|
|
817
|
+
stderrConfig: "TASK_EXEC_STDERR_CONFIG_PIPE",
|
|
818
|
+
});
|
|
819
|
+
if (options.stdin !== undefined) {
|
|
820
|
+
yield* stdinWrite({
|
|
821
|
+
taskId: id,
|
|
822
|
+
execId,
|
|
823
|
+
offset: "0",
|
|
824
|
+
data: yield* encodeBase64(options.stdin),
|
|
825
|
+
eof: true,
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
const read = (fileDescriptor: router.TaskExecStdioFileDescriptor) =>
|
|
829
|
+
stdioRead({ taskId: id, execId, offset: "0", fileDescriptor }).pipe(
|
|
830
|
+
Effect.map(({ items }) => streamText(items)),
|
|
831
|
+
);
|
|
832
|
+
const [stdout, stderr, exit] = yield* Effect.all(
|
|
833
|
+
[
|
|
834
|
+
read("TASK_EXEC_STDIO_FILE_DESCRIPTOR_STDOUT"),
|
|
835
|
+
read("TASK_EXEC_STDIO_FILE_DESCRIPTOR_STDERR"),
|
|
836
|
+
wait({ taskId: id, execId }),
|
|
837
|
+
],
|
|
838
|
+
{ concurrency: "unbounded" },
|
|
839
|
+
);
|
|
840
|
+
return {
|
|
841
|
+
exitCode: exit.code ?? (exit.signal !== undefined ? 128 + exit.signal : -1),
|
|
842
|
+
stdout,
|
|
843
|
+
stderr,
|
|
844
|
+
};
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
/** The task and its router access, for the router calls of one command. */
|
|
848
|
+
const routerAccess = Effect.gen(function* () {
|
|
849
|
+
const id = yield* taskId;
|
|
850
|
+
const access = yield* ops.getCommandRouterAccess({ taskId: id });
|
|
851
|
+
if (!access.url || !access.jwt) {
|
|
852
|
+
return yield* new SandboxUnexpectedResponse({
|
|
853
|
+
message: `Modal returned no command router for sandbox ${sandboxId}`,
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
return { taskId: id, at: { url: access.url, jwt: access.jwt } };
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* Start a command (on a PTY with `pty`) and return how to follow it: a
|
|
861
|
+
* stream per output, stdin writes from a running offset, and its exit.
|
|
862
|
+
*/
|
|
863
|
+
const startExec = Effect.fn(function* (
|
|
864
|
+
commandArgs: ReadonlyArray<string>,
|
|
865
|
+
options: ExecOptions,
|
|
866
|
+
pty?: router.PTYInfo,
|
|
867
|
+
) {
|
|
868
|
+
const { taskId: id, at } = yield* routerAccess;
|
|
869
|
+
const begin = yield* routerOps.execStart(at);
|
|
870
|
+
const stdinWrite = yield* routerOps.execStdinWrite(at);
|
|
871
|
+
const follow = yield* routerOps.execStdioFollow(at);
|
|
872
|
+
const wait = yield* routerOps.execWait(at);
|
|
873
|
+
const execId = yield* Effect.sync(() => SecureRandom.uuidV4Unsafe());
|
|
874
|
+
yield* begin({
|
|
875
|
+
taskId: id,
|
|
876
|
+
execId,
|
|
877
|
+
commandArgs: [...commandArgs],
|
|
878
|
+
workdir: options.cwd,
|
|
879
|
+
timeoutSecs: seconds(options.timeout),
|
|
880
|
+
secretIds: options.secrets?.map((s) => s.secretId),
|
|
881
|
+
env: options.env,
|
|
882
|
+
stdoutConfig: "TASK_EXEC_STDOUT_CONFIG_PIPE",
|
|
883
|
+
stderrConfig: pty ? undefined : "TASK_EXEC_STDERR_CONFIG_PIPE",
|
|
884
|
+
ptyInfo: pty,
|
|
885
|
+
});
|
|
886
|
+
let stdinOffset = 0;
|
|
887
|
+
const write = (data: string | Uint8Array, eof = false) =>
|
|
888
|
+
Effect.gen(function* () {
|
|
889
|
+
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
890
|
+
const offset = stdinOffset;
|
|
891
|
+
stdinOffset += bytes.length;
|
|
892
|
+
yield* stdinWrite({
|
|
893
|
+
taskId: id,
|
|
894
|
+
execId,
|
|
895
|
+
offset: String(offset),
|
|
896
|
+
data: yield* encodeBase64(bytes),
|
|
897
|
+
eof,
|
|
898
|
+
});
|
|
899
|
+
});
|
|
900
|
+
/** One output of the command, each chunk as it arrives. */
|
|
901
|
+
const output = (fileDescriptor: router.TaskExecStdioFileDescriptor) =>
|
|
902
|
+
follow({ taskId: id, execId, offset: "0", fileDescriptor }).pipe(
|
|
903
|
+
Stream.map((item) => new Uint8Array(Buffer.from(item.data, "base64"))),
|
|
904
|
+
Stream.filter((bytes) => bytes.length > 0),
|
|
905
|
+
);
|
|
906
|
+
if (options.stdin !== undefined) yield* write(options.stdin, true);
|
|
907
|
+
return {
|
|
908
|
+
execId,
|
|
909
|
+
write,
|
|
910
|
+
output,
|
|
911
|
+
wait: Effect.map(wait({ taskId: id, execId }), (exit) => ({
|
|
912
|
+
exitCode: exit.code ?? (exit.signal !== undefined ? 128 + exit.signal : -1),
|
|
913
|
+
})),
|
|
914
|
+
};
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
const argvOf = (command: Command): ReadonlyArray<string> =>
|
|
918
|
+
typeof command === "string" ? ["sh", "-c", command] : command;
|
|
919
|
+
|
|
920
|
+
const start = (command: Command, options: ExecOptions = {}) =>
|
|
921
|
+
Effect.gen(function* () {
|
|
922
|
+
// The router has no signal call: the command records its pid, and
|
|
923
|
+
// `kill` signals it with a second command.
|
|
924
|
+
const pidFile = `/tmp/.alchemy-exec-${yield* Effect.sync(() => SecureRandom.uuidV4Unsafe())}.pid`;
|
|
925
|
+
const running = yield* startExec(
|
|
926
|
+
["sh", "-c", 'echo $$ > "$0"; exec "$@"', pidFile, ...argvOf(command)],
|
|
927
|
+
options,
|
|
928
|
+
);
|
|
929
|
+
return {
|
|
930
|
+
id: running.execId,
|
|
931
|
+
output: Stream.merge(
|
|
932
|
+
Stream.map(running.output("TASK_EXEC_STDIO_FILE_DESCRIPTOR_STDOUT"), (data) => ({
|
|
933
|
+
fd: "stdout" as const,
|
|
934
|
+
data,
|
|
935
|
+
})),
|
|
936
|
+
Stream.map(running.output("TASK_EXEC_STDIO_FILE_DESCRIPTOR_STDERR"), (data) => ({
|
|
937
|
+
fd: "stderr" as const,
|
|
938
|
+
data,
|
|
939
|
+
})),
|
|
940
|
+
),
|
|
941
|
+
write: (data: string | Uint8Array) => running.write(data),
|
|
942
|
+
kill: (signal: "SIGTERM" | "SIGKILL" = "SIGTERM") =>
|
|
943
|
+
Effect.asVoid(exec(["sh", "-c", `kill -s ${signal.slice(3)} "$(cat "$0")"`, pidFile])),
|
|
944
|
+
wait: running.wait,
|
|
945
|
+
} satisfies Sandbox.Process<SandboxProcessError>;
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
const pty: Sandbox.Pty<SandboxPtyError, SandboxPtyOptions> = {
|
|
949
|
+
open: (options) =>
|
|
950
|
+
Effect.gen(function* () {
|
|
951
|
+
const running = yield* startExec(
|
|
952
|
+
options.command
|
|
953
|
+
? ["sh", "-c", options.command]
|
|
954
|
+
: ["sh", "-c", "exec bash -l 2>/dev/null || exec sh -l"],
|
|
955
|
+
{ cwd: options.cwd, env: options.env, secrets: options.secrets },
|
|
956
|
+
{
|
|
957
|
+
enabled: true,
|
|
958
|
+
winszRows: options.rows,
|
|
959
|
+
winszCols: options.cols,
|
|
960
|
+
envTerm: "xterm-256color",
|
|
961
|
+
ptyType: "PTY_TYPE_SHELL",
|
|
962
|
+
noTerminateOnIdleStdin: true,
|
|
963
|
+
},
|
|
964
|
+
);
|
|
965
|
+
return {
|
|
966
|
+
id: running.execId,
|
|
967
|
+
output: running.output("TASK_EXEC_STDIO_FILE_DESCRIPTOR_STDOUT"),
|
|
968
|
+
write: (data: string | Uint8Array) => running.write(data),
|
|
969
|
+
resize: () =>
|
|
970
|
+
Effect.fail(
|
|
971
|
+
new SandboxPtyResizeUnsupported({
|
|
972
|
+
message: "Modal's task command router can't resize a running PTY",
|
|
973
|
+
}),
|
|
974
|
+
),
|
|
975
|
+
} satisfies Sandbox.PtyConnection<SandboxPtyError>;
|
|
976
|
+
}),
|
|
977
|
+
};
|
|
978
|
+
|
|
979
|
+
const read = Effect.fn(function* (path: string) {
|
|
980
|
+
// base64 keeps binary content intact through the text output stream.
|
|
981
|
+
const encoded = yield* fsExec(path, 'base64 < "$1" | tr -d "\\n"');
|
|
982
|
+
return yield* decodeBase64(encoded);
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
const typeOf = (letter: string): SandboxFileEntry["type"] =>
|
|
986
|
+
letter === "d" ? "directory" : letter === "l" ? "symlink" : letter === "f" ? "file" : "other";
|
|
987
|
+
|
|
988
|
+
const files: SandboxFilesOf<never> = {
|
|
989
|
+
read,
|
|
990
|
+
readText: (path) => read(path).pipe(Effect.map((bytes) => new TextDecoder().decode(bytes))),
|
|
991
|
+
write: Effect.fn(function* (path, data) {
|
|
992
|
+
const encoded = yield* encodeBase64(data);
|
|
993
|
+
yield* fsExec(path, 'mkdir -p "$(dirname "$1")" && base64 -d > "$1"', encoded);
|
|
994
|
+
}),
|
|
995
|
+
list: Effect.fn(function* (path) {
|
|
996
|
+
const output = yield* fsExec(
|
|
997
|
+
path,
|
|
998
|
+
'cd "$1" && for f in * .[!.]* ..?*; do [ -e "$f" ] || [ -L "$f" ] || continue; if [ -L "$f" ]; then echo "l $f"; elif [ -d "$f" ]; then echo "d $f"; elif [ -f "$f" ]; then echo "f $f"; else echo "o $f"; fi; done',
|
|
999
|
+
);
|
|
1000
|
+
return output
|
|
1001
|
+
.split("\n")
|
|
1002
|
+
.filter((line) => line.length > 2)
|
|
1003
|
+
.map((line) => ({
|
|
1004
|
+
path: `${path.replace(/\/$/, "")}/${line.slice(2)}`,
|
|
1005
|
+
type: typeOf(line[0]!),
|
|
1006
|
+
}));
|
|
1007
|
+
}),
|
|
1008
|
+
stat: Effect.fn(function* (path) {
|
|
1009
|
+
// `%F` is GNU stat's type name; busybox prints the same names.
|
|
1010
|
+
const output = yield* fsExec(path, 'stat -c "%F|%s|%Y" "$1"');
|
|
1011
|
+
const [kind = "", size = "0", mtime = "0"] = output.trim().split("|");
|
|
1012
|
+
return {
|
|
1013
|
+
path,
|
|
1014
|
+
type: kind.includes("directory")
|
|
1015
|
+
? "directory"
|
|
1016
|
+
: kind.includes("symbolic link")
|
|
1017
|
+
? "symlink"
|
|
1018
|
+
: kind.includes("regular")
|
|
1019
|
+
? "file"
|
|
1020
|
+
: "other",
|
|
1021
|
+
size: Number(size),
|
|
1022
|
+
modifiedAt: DateTime.makeUnsafe(Number(mtime) * 1000),
|
|
1023
|
+
} satisfies SandboxFileStat;
|
|
1024
|
+
}),
|
|
1025
|
+
remove: Effect.fn(function* (path, options = {}) {
|
|
1026
|
+
yield* fsExec(path, options.recursive ? 'rm -r "$1"' : 'rm "$1"');
|
|
1027
|
+
}),
|
|
1028
|
+
makeDir: Effect.fn(function* (path) {
|
|
1029
|
+
yield* fsExec(path, 'mkdir -p "$1"');
|
|
1030
|
+
}),
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
const tunnelsOf = Effect.fn(function* (timeout?: DurationInput) {
|
|
1034
|
+
const { tunnels } = yield* ops.getTunnels({
|
|
1035
|
+
sandboxId,
|
|
1036
|
+
timeout: seconds(timeout) ?? 50,
|
|
1037
|
+
});
|
|
1038
|
+
const out: Record<number, SandboxTunnel> = {};
|
|
1039
|
+
for (const tunnel of tunnels ?? []) {
|
|
1040
|
+
if (tunnel.containerPort === undefined) continue;
|
|
1041
|
+
const encrypted = tunnel.host !== undefined && tunnel.host !== "";
|
|
1042
|
+
const host = (encrypted ? tunnel.host : tunnel.unencryptedHost) ?? "";
|
|
1043
|
+
const port = (encrypted ? tunnel.port : tunnel.unencryptedPort) ?? 443;
|
|
1044
|
+
out[tunnel.containerPort] = {
|
|
1045
|
+
host,
|
|
1046
|
+
port,
|
|
1047
|
+
url: encrypted
|
|
1048
|
+
? `https://${host}${port === 443 ? "" : `:${port}`}`
|
|
1049
|
+
: `tcp://${host}:${port}`,
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
return out;
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
const getTcpPort = Effect.fn(function* (port: number | undefined = target.program?.port) {
|
|
1056
|
+
if (port === undefined) {
|
|
1057
|
+
return yield* new SandboxUnexpectedResponse({
|
|
1058
|
+
message: "getTcpPort needs a port: create the sandbox from a Modal.Sandbox, or pass one",
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
const tunnel = (yield* tunnelsOf(50))[port];
|
|
1062
|
+
if (!tunnel) {
|
|
1063
|
+
return yield* new SandboxUnexpectedResponse({
|
|
1064
|
+
message: `Port ${port} of sandbox ${sandboxId} has no tunnel; open it with encryptedPorts`,
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
const tls = tunnel.url.startsWith("https:");
|
|
1068
|
+
// An unencrypted tunnel carries plain TCP, so HTTP to it is cleartext.
|
|
1069
|
+
const origin = tls ? tunnel.url : `http://${tunnel.host}:${tunnel.port}`;
|
|
1070
|
+
const sandboxPort: SandboxPortOf<never> = {
|
|
1071
|
+
...tunnel,
|
|
1072
|
+
// The user's own program, reached through its tunnel (allowed raw HTTP).
|
|
1073
|
+
fetch: Sandbox.portFetch((request) => {
|
|
1074
|
+
const url = new URL(request.url, "http://sandbox");
|
|
1075
|
+
// The tunnel routes by host, so a proxied request's own `host` goes.
|
|
1076
|
+
return ops.http.execute(
|
|
1077
|
+
HttpClientRequest.setUrl(
|
|
1078
|
+
HttpClientRequest.removeHeader(request, "host"),
|
|
1079
|
+
`${origin}${url.pathname}${url.search}`,
|
|
1080
|
+
),
|
|
1081
|
+
);
|
|
1082
|
+
}) as SandboxPortOf<never>["fetch"],
|
|
1083
|
+
connect: () => connectTcp({ host: tunnel.host, port: tunnel.port, tls }),
|
|
1084
|
+
};
|
|
1085
|
+
return sandboxPort;
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
const handle: RawSandboxHandle = {
|
|
1089
|
+
sandboxId,
|
|
1090
|
+
exec,
|
|
1091
|
+
start,
|
|
1092
|
+
pty,
|
|
1093
|
+
files,
|
|
1094
|
+
tunnels: tunnelsOf,
|
|
1095
|
+
connectToken: Effect.fn(function* (options = {}) {
|
|
1096
|
+
const response = yield* ops.createConnectToken({
|
|
1097
|
+
sandboxId,
|
|
1098
|
+
port: options.port,
|
|
1099
|
+
userMetadata: options.userMetadata,
|
|
1100
|
+
});
|
|
1101
|
+
if (!response.url || !response.token) {
|
|
1102
|
+
return yield* new SandboxUnexpectedResponse({ message: "Modal returned no connect token" });
|
|
1103
|
+
}
|
|
1104
|
+
return { url: response.url, token: response.token };
|
|
1105
|
+
}),
|
|
1106
|
+
snapshotFilesystem: (_timeout) =>
|
|
1107
|
+
viaRouter(routerOps.snapshotFilesystem, (op, task) =>
|
|
1108
|
+
Effect.flatMap(
|
|
1109
|
+
Effect.sync(() => SecureRandom.uuidV4Unsafe()),
|
|
1110
|
+
// -1: the snapshot image never expires.
|
|
1111
|
+
(snapshotId) => op({ taskId: task, snapshotId, ttlSeconds: "-1" }),
|
|
1112
|
+
),
|
|
1113
|
+
).pipe(
|
|
1114
|
+
Effect.flatMap((response) =>
|
|
1115
|
+
response.imageId
|
|
1116
|
+
? Effect.succeed({ imageId: response.imageId })
|
|
1117
|
+
: Effect.fail(
|
|
1118
|
+
new SandboxUnexpectedResponse({
|
|
1119
|
+
message: `Filesystem snapshot of ${sandboxId} returned no image`,
|
|
1120
|
+
}),
|
|
1121
|
+
),
|
|
1122
|
+
),
|
|
1123
|
+
),
|
|
1124
|
+
snapshotMemory: () =>
|
|
1125
|
+
viaRouter(routerOps.snapshotMemory, (op, task) =>
|
|
1126
|
+
Effect.flatMap(
|
|
1127
|
+
Effect.sync(() => SecureRandom.uuidV4Unsafe()),
|
|
1128
|
+
(idempotencyKey) => op({ taskId: task, idempotencyKey }),
|
|
1129
|
+
),
|
|
1130
|
+
).pipe(
|
|
1131
|
+
Effect.flatMap((response) =>
|
|
1132
|
+
response.snapshotId
|
|
1133
|
+
? Effect.succeed({ snapshotId: response.snapshotId })
|
|
1134
|
+
: Effect.fail(new SandboxUnexpectedResponse({ message: "No snapshot id" })),
|
|
1135
|
+
),
|
|
1136
|
+
),
|
|
1137
|
+
snapshotDirectory: (path) =>
|
|
1138
|
+
viaRouter(routerOps.snapshotDirectory, (op, task) => op({ taskId: task, path })).pipe(
|
|
1139
|
+
Effect.flatMap((response) =>
|
|
1140
|
+
response.imageId
|
|
1141
|
+
? Effect.succeed({ imageId: response.imageId })
|
|
1142
|
+
: Effect.fail(new SandboxUnexpectedResponse({ message: `No image for ${path}` })),
|
|
1143
|
+
),
|
|
1144
|
+
),
|
|
1145
|
+
mountDirectory: (path, snapshot) =>
|
|
1146
|
+
viaRouter(routerOps.mountDirectory, (op, task) =>
|
|
1147
|
+
op({ taskId: task, path, imageId: snapshot.imageId }),
|
|
1148
|
+
).pipe(Effect.asVoid),
|
|
1149
|
+
setNetworkAccess: (access) =>
|
|
1150
|
+
viaRouter(routerOps.setNetworkAccess, (op, task) =>
|
|
1151
|
+
op({ taskId: task, networkAccess: runningNetworkAccessOf(access) }),
|
|
1152
|
+
).pipe(Effect.asVoid),
|
|
1153
|
+
setOutboundPolicy: Effect.fn(function* (policy) {
|
|
1154
|
+
yield* ops.setOutboundPolicy({
|
|
1155
|
+
sandboxId,
|
|
1156
|
+
outboundPolicy: outboundPolicyOf(policy),
|
|
1157
|
+
});
|
|
1158
|
+
}),
|
|
1159
|
+
setTags: Effect.fn(function* (tags) {
|
|
1160
|
+
yield* ops.setTags({
|
|
1161
|
+
sandboxId,
|
|
1162
|
+
environmentName: environmentOf(target.environment),
|
|
1163
|
+
tags: Object.entries(tags).map(([tagName, tagValue]) => ({ tagName, tagValue })),
|
|
1164
|
+
});
|
|
1165
|
+
}),
|
|
1166
|
+
getTags: Effect.fn(function* () {
|
|
1167
|
+
const { tags } = yield* ops.getTags({ sandboxId });
|
|
1168
|
+
return Object.fromEntries((tags ?? []).map((t) => [t.tagName ?? "", t.tagValue ?? ""]));
|
|
1169
|
+
}),
|
|
1170
|
+
setName: Effect.fn(function* (name) {
|
|
1171
|
+
yield* ops.setName({ sandboxId, name });
|
|
1172
|
+
}),
|
|
1173
|
+
resourceUsage: Effect.fn(function* () {
|
|
1174
|
+
const usage = yield* ops.getResourceUsage({ sandboxId });
|
|
1175
|
+
return {
|
|
1176
|
+
cpuSeconds: Number(usage.cpuCoreNanosecs ?? 0) / 1e9,
|
|
1177
|
+
memoryGibSeconds: Number(usage.memGibNanosecs ?? 0) / 1e9,
|
|
1178
|
+
gpuSeconds: Number(usage.gpuNanosecs ?? 0) / 1e9,
|
|
1179
|
+
};
|
|
1180
|
+
}),
|
|
1181
|
+
logs: Effect.fn(function* (options = {}) {
|
|
1182
|
+
const { taskId: id, at } = yield* routerAccess;
|
|
1183
|
+
const read = yield* routerOps.mainStdio(at);
|
|
1184
|
+
const chunks = yield* read({
|
|
1185
|
+
taskId: id,
|
|
1186
|
+
offset: "0",
|
|
1187
|
+
fileDescriptor: "SANDBOX_STDIO_FILE_DESCRIPTOR_STDOUT",
|
|
1188
|
+
}).pipe(
|
|
1189
|
+
Stream.map((item) => Buffer.from(item.data, "base64")),
|
|
1190
|
+
Stream.interruptWhen(Effect.sleep(`${seconds(options.timeout) ?? 5} seconds`)),
|
|
1191
|
+
Stream.runCollect,
|
|
1192
|
+
);
|
|
1193
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1194
|
+
}),
|
|
1195
|
+
wait: Effect.fn(function* (timeout) {
|
|
1196
|
+
const { result } = yield* ops.wait({
|
|
1197
|
+
sandboxId,
|
|
1198
|
+
timeout: seconds(timeout) ?? 0,
|
|
1199
|
+
});
|
|
1200
|
+
return {
|
|
1201
|
+
exitCode: result?.exitcode,
|
|
1202
|
+
status: result?.status ?? "GENERIC_STATUS_UNSPECIFIED",
|
|
1203
|
+
};
|
|
1204
|
+
}),
|
|
1205
|
+
getTcpPort,
|
|
1206
|
+
terminate: Effect.fn(function* () {
|
|
1207
|
+
yield* ops
|
|
1208
|
+
.terminate({ sandboxId })
|
|
1209
|
+
.pipe(Effect.catchTag("SandboxNotFound", () => Effect.void));
|
|
1210
|
+
}),
|
|
1211
|
+
};
|
|
1212
|
+
return handle;
|
|
1213
|
+
};
|
|
1214
|
+
|
|
1215
|
+
/**
|
|
1216
|
+
* Create a sandbox in `target`'s app and return its handle. The one
|
|
1217
|
+
* implementation behind `CreateSandbox` and warm-base image builds.
|
|
1218
|
+
*/
|
|
1219
|
+
export const createSandbox = Effect.fn(function* (
|
|
1220
|
+
ops: SandboxOps,
|
|
1221
|
+
target: SandboxTarget,
|
|
1222
|
+
options: SandboxCreateOptions = {},
|
|
1223
|
+
) {
|
|
1224
|
+
const imageId = yield* resolveSandboxImage(ops, target, options.image);
|
|
1225
|
+
const program = target.program;
|
|
1226
|
+
const withProgram: SandboxCreateOptions = program
|
|
1227
|
+
? {
|
|
1228
|
+
...options,
|
|
1229
|
+
command: options.command ?? program.command,
|
|
1230
|
+
secrets: [{ secretId: program.secretId }, ...(options.secrets ?? [])],
|
|
1231
|
+
encryptedPorts: [...new Set([program.port, ...(options.encryptedPorts ?? [])])],
|
|
1232
|
+
}
|
|
1233
|
+
: options;
|
|
1234
|
+
const own = yield* modalMountsOf(options.mounts);
|
|
1235
|
+
const mounts: ModalMounts = {
|
|
1236
|
+
volumeMounts: [...(program?.mounts?.volumeMounts ?? []), ...own.volumeMounts],
|
|
1237
|
+
cloudBucketMounts: [...(program?.mounts?.cloudBucketMounts ?? []), ...own.cloudBucketMounts],
|
|
1238
|
+
};
|
|
1239
|
+
const created = yield* ops.create({
|
|
1240
|
+
appId: target.appId,
|
|
1241
|
+
definition: definitionOf(imageId, withProgram, mounts),
|
|
1242
|
+
tags: Object.entries(options.tags ?? {}).map(([tagName, tagValue]) => ({ tagName, tagValue })),
|
|
1243
|
+
});
|
|
1244
|
+
if (!created.sandboxId) {
|
|
1245
|
+
return yield* new SandboxUnexpectedResponse({ message: "Modal returned no sandbox id" });
|
|
1246
|
+
}
|
|
1247
|
+
return sandboxHandle(ops, created.sandboxId, target);
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
/** A running sandbox of `target`'s app by name, or `undefined`. */
|
|
1251
|
+
export const sandboxFromName = Effect.fn(function* (
|
|
1252
|
+
ops: SandboxOps,
|
|
1253
|
+
target: SandboxTarget,
|
|
1254
|
+
name: string,
|
|
1255
|
+
) {
|
|
1256
|
+
const found = yield* ops
|
|
1257
|
+
.getFromName({
|
|
1258
|
+
sandboxName: name,
|
|
1259
|
+
appName: target.appName,
|
|
1260
|
+
environmentName: environmentOf(target.environment),
|
|
1261
|
+
})
|
|
1262
|
+
.pipe(Effect.catchTag("SandboxNotFound", () => Effect.undefined));
|
|
1263
|
+
return found?.sandboxId ? sandboxHandle(ops, found.sandboxId, target) : undefined;
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
/** Running sandboxes of `target`'s app, optionally filtered by tags. */
|
|
1267
|
+
export const listSandboxes = Effect.fn(function* (
|
|
1268
|
+
ops: SandboxOps,
|
|
1269
|
+
target: SandboxTarget,
|
|
1270
|
+
options: { readonly tags?: Record<string, string>; readonly includeFinished?: boolean } = {},
|
|
1271
|
+
) {
|
|
1272
|
+
const { sandboxes } = yield* ops.list({
|
|
1273
|
+
appId: target.appId,
|
|
1274
|
+
environmentName: environmentOf(target.environment),
|
|
1275
|
+
includeFinished: options.includeFinished ?? false,
|
|
1276
|
+
tags: Object.entries(options.tags ?? {}).map(([tagName, tagValue]) => ({ tagName, tagValue })),
|
|
1277
|
+
});
|
|
1278
|
+
return (sandboxes ?? []).flatMap((info) =>
|
|
1279
|
+
info.id
|
|
1280
|
+
? [
|
|
1281
|
+
{
|
|
1282
|
+
sandboxId: info.id,
|
|
1283
|
+
name: info.name || undefined,
|
|
1284
|
+
createdAt: info.createdAt,
|
|
1285
|
+
tags: Object.fromEntries(
|
|
1286
|
+
(info.tags ?? []).map((t) => [t.tagName ?? "", t.tagValue ?? ""]),
|
|
1287
|
+
),
|
|
1288
|
+
},
|
|
1289
|
+
]
|
|
1290
|
+
: [],
|
|
1291
|
+
);
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
/** Restore a memory snapshot into a new sandbox. */
|
|
1295
|
+
export const restoreSandbox = Effect.fn(function* (
|
|
1296
|
+
ops: SandboxOps,
|
|
1297
|
+
target: SandboxTarget,
|
|
1298
|
+
snapshot: { readonly snapshotId: string },
|
|
1299
|
+
options: { readonly name?: string } = {},
|
|
1300
|
+
) {
|
|
1301
|
+
const restored = yield* ops.restore({
|
|
1302
|
+
snapshotId: snapshot.snapshotId,
|
|
1303
|
+
sandboxNameOverride: options.name,
|
|
1304
|
+
sandboxNameOverrideType: options.name ? "SANDBOX_NAME_OVERRIDE_TYPE_STRING" : undefined,
|
|
1305
|
+
});
|
|
1306
|
+
if (!restored.sandboxId) {
|
|
1307
|
+
return yield* new SandboxUnexpectedResponse({ message: "Modal returned no restored sandbox" });
|
|
1308
|
+
}
|
|
1309
|
+
return sandboxHandle(ops, restored.sandboxId, target);
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* Options for a persistent sandbox. Modal fixes each session's lifetime (an
|
|
1314
|
+
* hour, paused after five idle minutes) and doesn't support names, GPUs,
|
|
1315
|
+
* `env`, tags or memory snapshots on persistent sandboxes yet.
|
|
1316
|
+
*/
|
|
1317
|
+
export type PersistentSandboxCreateOptions = Omit<
|
|
1318
|
+
SandboxCreateOptions,
|
|
1319
|
+
"timeout" | "idleTimeout" | "name" | "gpu" | "env" | "tags" | "enableSnapshot"
|
|
1320
|
+
>;
|
|
1321
|
+
|
|
1322
|
+
/** Modal's session limits for persistent sandboxes. */
|
|
1323
|
+
const PERSISTENT_SESSION = { timeout: 60 * 60, idleTimeout: 5 * 60 } as const;
|
|
1324
|
+
|
|
1325
|
+
/** A persistent sandbox: one box whose filesystem outlives its sessions. */
|
|
1326
|
+
export interface PersistentSandboxHandleOf<R> {
|
|
1327
|
+
readonly persistentSandboxId: string;
|
|
1328
|
+
/**
|
|
1329
|
+
* The running session, resuming the box from its last session's
|
|
1330
|
+
* filesystem when it's paused.
|
|
1331
|
+
*/
|
|
1332
|
+
session(): Effect.Effect<
|
|
1333
|
+
SandboxHandleOf<R>,
|
|
1334
|
+
| SandboxOpError<"getPersistent">
|
|
1335
|
+
| SandboxOpError<"resumePersistent">
|
|
1336
|
+
| SandboxUnexpectedResponse,
|
|
1337
|
+
R
|
|
1338
|
+
>;
|
|
1339
|
+
/** Terminate the box for good. */
|
|
1340
|
+
terminate(): Effect.Effect<
|
|
1341
|
+
void,
|
|
1342
|
+
Exclude<SandboxOpError<"terminatePersistent">, { readonly _tag: "PersistentSandboxNotFound" }>,
|
|
1343
|
+
R
|
|
1344
|
+
>;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
export type RawPersistentSandboxHandle = PersistentSandboxHandleOf<never>;
|
|
1348
|
+
export type PersistentSandboxHandle = PersistentSandboxHandleOf<RuntimeContext>;
|
|
1349
|
+
|
|
1350
|
+
/** The handle for an existing persistent sandbox id. */
|
|
1351
|
+
export const persistentSandboxHandle = (
|
|
1352
|
+
ops: SandboxOps,
|
|
1353
|
+
persistentSandboxId: string,
|
|
1354
|
+
target: Pick<SandboxTarget, "environment" | "program">,
|
|
1355
|
+
): RawPersistentSandboxHandle => ({
|
|
1356
|
+
persistentSandboxId,
|
|
1357
|
+
session: Effect.fn(function* () {
|
|
1358
|
+
const running = yield* ops.getPersistent({ persistentSandboxId });
|
|
1359
|
+
if (running.sandboxId) return sandboxHandle(ops, running.sandboxId, target);
|
|
1360
|
+
// A paused box resumes from its last session's filesystem. Modal answers
|
|
1361
|
+
// busy while the previous session is still saving it.
|
|
1362
|
+
const resumed = yield* ops.resumePersistent({ persistentSandboxId }).pipe(
|
|
1363
|
+
Effect.retry({
|
|
1364
|
+
while: (error) => error._tag === "PersistentSandboxBusy",
|
|
1365
|
+
schedule: Schedule.spaced("1 second"),
|
|
1366
|
+
times: 60,
|
|
1367
|
+
}),
|
|
1368
|
+
);
|
|
1369
|
+
if (!resumed.sandboxId) {
|
|
1370
|
+
return yield* new SandboxUnexpectedResponse({
|
|
1371
|
+
message: `Modal returned no session for persistent sandbox ${persistentSandboxId}`,
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
return sandboxHandle(ops, resumed.sandboxId, target);
|
|
1375
|
+
}),
|
|
1376
|
+
terminate: Effect.fn(function* () {
|
|
1377
|
+
yield* ops
|
|
1378
|
+
.terminatePersistent({ persistentSandboxId })
|
|
1379
|
+
.pipe(Effect.catchTag("PersistentSandboxNotFound", () => Effect.void));
|
|
1380
|
+
}),
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
/** Create a persistent sandbox in `target`'s app. */
|
|
1384
|
+
export const createPersistentSandbox = Effect.fn(function* (
|
|
1385
|
+
ops: SandboxOps,
|
|
1386
|
+
target: SandboxTarget,
|
|
1387
|
+
options: PersistentSandboxCreateOptions = {},
|
|
1388
|
+
) {
|
|
1389
|
+
const imageId = yield* resolveSandboxImage(ops, target, options.image);
|
|
1390
|
+
const created = yield* ops.createPersistent({
|
|
1391
|
+
appId: target.appId,
|
|
1392
|
+
definition: definitionOf(
|
|
1393
|
+
imageId,
|
|
1394
|
+
{ ...options, ...PERSISTENT_SESSION },
|
|
1395
|
+
yield* modalMountsOf(options.mounts),
|
|
1396
|
+
),
|
|
1397
|
+
});
|
|
1398
|
+
if (!created.persistentSandboxId) {
|
|
1399
|
+
return yield* new SandboxUnexpectedResponse({
|
|
1400
|
+
message: "Modal returned no persistent sandbox id",
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
return persistentSandboxHandle(ops, created.persistentSandboxId, target);
|
|
1404
|
+
});
|