@evolvingmachines/modal 0.0.52-project-sable.20260815.143c2cd → 0.0.52-project-sable.20260824.3dfecab
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +4 -4
- package/dist/index.d.cts +225 -8
- package/dist/index.d.ts +225 -8
- package/dist/index.js +4 -4
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
'use strict';var modal=require('modal'),tarStream=require('tar-stream');var l="c-
|
|
2
|
-
`).slice(1).map(s=>{let
|
|
3
|
-
`)){if(!
|
|
4
|
-
exports.EVOLVE_IMAGE_VERSION=l;exports.MODAL_MAX_LIFETIME_MS=
|
|
1
|
+
'use strict';var modal=require('modal'),tarStream=require('tar-stream');var l="c-fae503a880ec";var R={[`evolve-all-${l}`]:`evolvingmachines/evolve-all:${l}`,"evolve-all":"evolvingmachines/evolve-all"},N=1440*60*1e3,u=8*1024*1024;async function*z(n){let e=n.getReader();try{for(;;){let{done:t,value:r}=await e.read();if(t)break;r&&(yield r);}}finally{e.releaseLock();}}var C="user",D="evolve.image",O="evolve.startedAt",S=class extends Error{requestedTimeoutMs;constructor(e){let t=(e/36e5).toFixed(1);super(`Modal sandboxes have a hard 24h lifetime cap; requested timeout was ${t}h. For sessions longer than 24h, persist progress with Evolve checkpoints and resume in a fresh sandbox instead of extending the timeout.`),this.name="ModalSandboxLifetimeError",this.requestedTimeoutMs=e;}};function T(n){if(n>N)throw new S(n)}var p=class extends Error{requestedIdleTimeoutMs;constructor(e,t){super(`Modal idleTimeoutMs of ${e}ms is invalid: ${t}`),this.name="ModalIdleTimeoutError",this.requestedIdleTimeoutMs=e;}};function $(n){if(n===void 0)return {};if(!Number.isFinite(n)||n<=0)throw new p(n,"it must be a positive number of milliseconds");if(n>N)throw new p(n,"it exceeds Modal's 24h lifetime cap, so the sandbox would always die of the lifetime first");return {idleTimeoutMs:n}}var v=class extends Error{constructor(e){super(`Modal bootCommand is invalid: ${e}`),this.name="ModalBootCommandError";}};function B(n){if(n===void 0)return {};if(n.length===0)throw new v(`an empty argv is Modal's own "no args" wire shape and would boot the image's ENTRYPOINT/CMD \u2014 omit bootCommand to boot the image's command`);return {command:[...n]}}function P(n,e,t,r){let s="";r&&Object.keys(r).length>0&&(s=Object.entries(r).filter(([,a])=>a!=null).map(([a,d])=>`export ${a}='${String(d).replace(/'/g,"'\\''")}'`).join("; ")+"; ");let o=t?`cd '${t.replace(/'/g,"'\\''")}' && ${s}${n}`:`${s}${n}`;if(e==="root")return ["bash","-c",o];let i=Buffer.from(o).toString("base64");return ["su",e,"-c",`echo ${i} | base64 -d | bash`]}var g=class extends Error{constructor(e){super(e),this.name="ModalResourcesError";}},Y=4,X=4096;function L(n){if(n?.disk!==void 0)throw new g(`Modal's JS SDK has no create-time disk-size parameter, so a ${n.disk} GiB disk request cannot be enforced. Drop \`resources.disk\` (containers get Modal's default disk quota) or run on a provider that sizes disk.`);if((n?.gpu===void 0||n.gpu<=0)&&n?.gpuTypes?.length)throw new g("resources.gpuTypes was set without a positive resources.gpu count \u2014 declare how many GPUs to reserve, or drop gpuTypes.");let e=n?.gpu!==void 0&&n.gpu>0?`${n.gpuTypes?.length?n.gpuTypes[0]:"any"}:${n.gpu}`:void 0;return {cpu:n?.cpu??Y,memoryMiB:n?.memory!==void 0?Math.ceil(n.memory*1024):X,...e!==void 0?{gpu:e}:{}}}var f=class extends Error{reason;destination;constructor(e,t,r){super(t),this.name="ModalNetworkPolicyError",this.reason=e,this.destination=r;}};function U(n){let e=/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(?:\/(\d{1,3}))?$/.exec(n);return !(!e||[e[1],e[2],e[3],e[4]].some(t=>Number(t)>255)||e[5]!==void 0&&Number(e[5])>32)}function J(n){return /^\d{1,3}(\.\d{1,3}){3}(\/\d+)?$/.test(n)&&!U(n)}function Q(n){return n.startsWith("[")?true:(n.match(/:/g)?.length??0)>=2}function Z(n){return /^[^:]+:\d+$/.test(n)}function M(n){if(!n||n.outbound==="open"){if(n?.allowedDestinations?.length)throw new Error("network.allowedDestinations is only valid when outbound is blocked");return {}}let e=n.allowedDestinations??[];if(e.length===0)return {blockNetwork:true};let t=[],r=[];for(let s of e)if(U(s))t.push(s.includes("/")?s:`${s}/32`);else {if(J(s))throw new f("invalid-ipv4",`"${s}" is not a valid IPv4 address or CIDR (octets must be 0-255, prefix 0-32). Fix the address or list a hostname instead.`,s);if(Q(s))t.push(s.includes("/")?s:`${s}/128`);else {if(Z(s))throw new f("port-unsupported",`Modal's network allowlist filters hosts and IPs only and cannot match a port; drop the ":<port>" from "${s}" and list just the host or IP.`,s);r.push(s);}}return {outboundCidrAllowlist:t,outboundDomainAllowlist:r}}function w(n){if(!n||n.outbound==="open"){if(n?.allowedDestinations?.length)throw new Error("network.allowedDestinations is only valid when outbound is blocked");return {outboundCidrAllowlist:["0.0.0.0/0"],outboundDomainAllowlist:["*"]}}let e=M(n);return {outboundCidrAllowlist:e.outboundCidrAllowlist??[],outboundDomainAllowlist:te(e.outboundDomainAllowlist??[])}}var ee="sealed.invalid";function te(n){return n.length>0?n:[ee]}function F(n,e){if(!e||e.length===0)return false;let t=s=>s===void 0?"open|":`${s.outbound}|${[...new Set(s.allowedDestinations??[])].sort().join(",")}`,r=t(n);return e.some(s=>t(s)!==r)}function W(n){let e=n.split("/")[0];return /^\d{12}\.dkr\.ecr\.[a-z0-9-]+\.amazonaws\.com$/.test(e)?"aws-ecr":e==="gcr.io"||e.endsWith(".gcr.io")||e.endsWith("-docker.pkg.dev")?"gcp-artifact-registry":"registry"}function E(n,e,t){let{[D]:r,[O]:s,...o}=e;return {sandboxId:n,image:r??t??"",metadata:o,startedAt:s??""}}var _=1e4,A=class{constructor(e,t){this.sandbox=e;this.user=t;}async run(e,t){let r=P(e,this.user,t?.cwd,t?.envs),s=await this.sandbox.exec(r,{timeoutMs:t?.timeoutMs}),{stdout:o,stderr:i}=await this.accumulateStreams(s,t?.onStdout,t?.onStderr);return {exitCode:await s.wait(),stdout:o,stderr:i}}async spawn(e,t){let r=P(e,this.user,t?.cwd,t?.envs),s=await this.sandbox.exec(r,{timeoutMs:t?.timeoutMs}),o="",i="",a=this.accumulateStreams(s,t?.onStdout?c=>{t.onStdout(c);}:void 0,t?.onStderr?c=>{t.onStderr(c);}:void 0).then(({stdout:c,stderr:b})=>{o=c,i=b;}).catch(()=>{});return {processId:`modal-${Date.now()}-${Math.random().toString(36).slice(2)}`,wait:async()=>(await a,{exitCode:await s.wait(),stdout:o,stderr:i}),kill:async()=>false}}async list(){let e=await this.sandbox.exec(["ps","-eo","pid,comm,args"],{timeoutMs:1e4});return await e.wait(),(await e.stdout.readText()).trim().split(`
|
|
2
|
+
`).slice(1).map(s=>{let o=s.trim().split(/\s+/);return {processId:o[0],cmd:o[1]||"",args:o.slice(2),envs:{}}})}async connect(e,t){throw new Error("Modal does not support connecting to existing processes")}async sendStdin(e,t){throw new Error("Modal does not support sendStdin by process ID")}async kill(e){return await(await this.sandbox.exec(["kill","-9",e],{timeoutMs:1e4})).wait()===0}async accumulateStreams(e,t,r){let s="",o="",i=[];return i.push((async()=>{try{for await(let a of e.stdout){let d=typeof a=="string"?a:new TextDecoder().decode(a);s+=d,t?.(d);}}catch{}})()),i.push((async()=>{try{for await(let a of e.stderr){let d=typeof a=="string"?a:new TextDecoder().decode(a);o+=d,r?.(d);}}catch{}})()),await Promise.all(i),{stdout:s,stderr:o}}},I=class{constructor(e,t){this.sandbox=e;this.user=t;}async chownToUser(e,t=false){if(this.user==="root")return;let r=t?["chown","-R",`${this.user}:${this.user}`,e]:["chown",`${this.user}:${this.user}`,e];await(await this.sandbox.exec(r,{timeoutMs:t?3e4:1e4})).wait();}async writeStdinChunked(e,t){for(let r=0;r<t.length;r+=u)await e.writeBytes(t.subarray(r,r+u));}async writeStdinCoalesced(e,t){let r=[],s=0,o=async()=>{if(s===0)return;let i=r.length===1?r[0]:Buffer.concat(r,s);r=[],s=0,await this.writeStdinChunked(e,i);};for await(let i of t)r.push(i),s+=i.length,s>=u&&await o();await o();}async read(e){let t=await this.sandbox.exec(["cat",e],{timeoutMs:3e5,mode:"binary"}),r=await t.wait();if(r!==0){let o=await t.stderr.readText();throw new Error(`Failed to read file ${e}: ${o||`exit code ${r}`}`)}let s=await t.stdout.readBytes();if(!s.includes(0))try{return new TextDecoder("utf-8",{fatal:!0,ignoreBOM:!0}).decode(s)}catch{}return s}async write(e,t){let r=this.toBuffer(t),s=e.substring(0,e.lastIndexOf("/"));s&&await this.makeDir(s);let o=e.replace(/'/g,"'\\''"),i=await this.sandbox.exec(["bash","-c",`cat > '${o}'`],{mode:"binary"});await this.writeStdinChunked(i.stdin,new Uint8Array(r)),await i.stdin.getWriter().close(),await i.wait(),await this.chownToUser(e);}async writeBatch(e){let t=tarStream.pack(),r=new Set;for(let i of e){let a=this.toBuffer(i.data),d=i.path.startsWith("/")?i.path.slice(1):i.path;t.entry({name:d},a);let c=i.path.substring(0,i.path.lastIndexOf("/"));c&&r.add(c);}t.finalize();let s=await this.sandbox.exec(["tar","-xf","-","-C","/"],{mode:"binary"});if(await this.writeStdinCoalesced(s.stdin,t),await s.stdin.getWriter().close(),await s.wait(),r.size>0){let i=Array.from(r),a=new Set(i.map(d=>d.split("/").slice(0,4).join("/")));for(let d of a)await this.chownToUser(d,true);}}async makeDir(e){await(await this.sandbox.exec(["mkdir","-p",e],{timeoutMs:1e4})).wait(),await this.chownToUser(e,true);}async exists(e){return await(await this.sandbox.exec(["test","-e",e],{timeoutMs:1e4})).wait()===0}async list(e){let t=e.replace(/'/g,"'\\''"),r=await this.sandbox.exec(["bash","-c",`ls -la '${t}' | tail -n +2`],{timeoutMs:3e4});await r.wait();let s=await r.stdout.readText(),o=[];for(let i of s.trim().split(`
|
|
3
|
+
`)){if(!i)continue;let a=i.split(/\s+/);if(a.length<9)continue;let d=a[0],c=a.slice(8).join(" ");c==="."||c===".."||o.push({name:c,path:e.endsWith("/")?`${e}${c}`:`${e}/${c}`,type:d.startsWith("d")?"dir":"file"});}return o}async remove(e){await(await this.sandbox.exec(["rm","-rf",e],{timeoutMs:3e4})).wait();}async rename(e,t){await(await this.sandbox.exec(["mv",e,t],{timeoutMs:3e4})).wait();}async readStream(e){return (await this.sandbox.exec(["cat",e],{timeoutMs:3e5,mode:"binary"})).stdout}async writeStream(e,t){let r=e.substring(0,e.lastIndexOf("/"));r&&await this.makeDir(r);let s=e.replace(/'/g,"'\\''"),o=await this.sandbox.exec(["bash","-c",`cat > '${s}'`],{mode:"binary"});await this.writeStdinCoalesced(o.stdin,z(t)),await o.stdin.getWriter().close(),await o.wait(),await this.chownToUser(e);}async writeFromPath(e,t){let{createReadStream:r}=await import('fs'),{Readable:s}=await import('stream'),o=s.toWeb(r(t,{highWaterMark:u}));await this.writeStream(e,o);}async uploadUrl(e,t){throw new Error("Modal does not support pre-signed upload URLs")}async downloadUrl(e,t){throw new Error("Modal does not support pre-signed download URLs")}async watchDir(e,t,r){throw new Error("Modal does not support watchDir")}toBuffer(e){if(typeof e=="string")return Buffer.from(e,"utf-8");if(e instanceof Buffer)return e;if(e instanceof ArrayBuffer||e instanceof Uint8Array)return Buffer.from(e);throw new Error(`Unsupported data type: ${typeof e}`)}},h=class{constructor(e,t,r){this.sandbox=e;this.commands=new A(e,r),this.files=new I(e,r),this.image=t;}commands;files;image;get sandboxId(){return this.sandbox.sandboxId}async getHost(e){let r=(await this.sandbox.tunnels())[e];if(!r)throw new Error(`No tunnel found for port ${e}`);return r.url}async isRunning(){try{return await this.sandbox.poll()===null}catch{return false}}async getInfo(){let e=await this.sandbox.getTags();return E(this.sandbox.sandboxId,e,this.image)}async updateNetwork(e){await this.sandbox.updateNetworkPolicy(w(e));}async kill(){try{await this.sandbox.terminate();}catch{await new Promise(e=>setTimeout(e,500)),await this.sandbox.terminate();}}async pause(){throw new Error("Modal does not support pause/resume. Persist progress with Evolve checkpoints and resume in a fresh sandbox, or use kill() to terminate.")}},k=class{providerType="modal";name="Modal";supportsBootCommand=true;client;appName;defaultTimeoutMs;imageName;imageSecretName;_app;sandboxUsers=new Map;constructor(e){!e.endpoint&&process.env.MODAL_SERVER_URL?.startsWith("unix:")?process.env.MODAL_SERVER_URL="https://api.modal.com:443":e.endpoint&&(process.env.MODAL_SERVER_URL=e.endpoint),this.client=new modal.ModalClient({tokenId:e.tokenId,tokenSecret:e.tokenSecret}),this.appName=e.appName??"evolve-sandbox",this.defaultTimeoutMs=e.defaultTimeoutMs??36e5,this.imageName=e.imageName??`evolve-all-${l}`,this.imageSecretName=e.imageSecretName;}async getApp(){return this._app||(this._app=await this.client.apps.fromName(this.appName,{createIfMissing:true})),this._app}async resolveImage(e){let t=W(e);if(t==="registry"){let s=this.imageSecretName?await this.client.secrets.fromName(this.imageSecretName):void 0;return this.client.images.fromRegistry(e,s)}if(!this.imageSecretName)throw new Error(`Private registry image "${e}" requires config.imageSecretName \u2014 the name of a Modal Secret holding registry credentials (AWS ECR: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION with read-only ECR IAM). Create one at https://modal.com/secrets`);let r=t==="aws-ecr"?await this.client.secrets.fromName(this.imageSecretName,{requiredKeys:["AWS_ACCESS_KEY_ID","AWS_SECRET_ACCESS_KEY","AWS_REGION"]}):await this.client.secrets.fromName(this.imageSecretName);return t==="aws-ecr"?this.client.images.fromAwsEcr(e,r):this.client.images.fromGcpArtifactRegistry(e,r)}async resolveAndBuildImage(e){let t=R[e]??e;if(!/[/:@]/.test(t))try{return {tag:t,image:await this.client.images.fromName(t)}}catch(o){if(!(o instanceof modal.NotFoundError))throw o}let r=await this.getApp(),s=await this.resolveImage(t);return {tag:t,image:await s.build(r)}}async publishImageAs(e,t){if(!e.trim())throw new Error("publishImageAs requires a non-empty alias to publish under");let{image:r}=await this.resolveAndBuildImage(t||this.imageName);return await r.publish(e),e}async prepareImage(e){await this.resolveAndBuildImage(e||this.imageName);}async create(e){let t=e.timeoutMs??this.defaultTimeoutMs;T(t);let r=$(e.idleTimeoutMs),s=B(e.bootCommand);for(let m of e.phaseNetworkPolicies??[])w(m);let o=F(e.network,e.phaseNetworkPolicies)?w(e.network):M(e.network),i=L(e.resources),a=e.user??C,d=await this.getApp(),{tag:c,image:b}=await this.resolveAndBuildImage(e.image||this.imageName),x=e.envs?Object.fromEntries(Object.entries(e.envs).filter(([,m])=>m!=null)):void 0,G=x&&Object.keys(x).length>0?x:void 0,H={...e.metadata,[D]:c,[O]:new Date().toISOString()},y=await this.client.sandboxes.create(d,b,{cpu:i.cpu,memoryMiB:i.memoryMiB,...i.gpu!==void 0?{gpu:i.gpu}:{},timeoutMs:t,...r,...s,workdir:e.workingDirectory,env:G,tags:H,...o});return e.workingDirectory&&a!=="root"&&await(await y.exec(["chown","-R",`${a}:${a}`,e.workingDirectory],{timeoutMs:3e4})).wait(),this.sandboxUsers.set(y.sandboxId,a),new h(y,c,a)}async connect(e,t){let r=await this.client.sandboxes.fromId(e),s=this.sandboxUsers.get(e)??C;return new h(r,void 0,s)}async list(e){let t=await this.walk(e);if(t.error&&!t.stoppedAtLimit)throw new Error(t.error);return t.sandboxes}async listAll(e){let{stoppedAtLimit:t,...r}=await this.walk(e);return r}async walk(e){if(e?.state&&!e.state.includes("running"))return {sandboxes:[],complete:true,pagesFetched:0,stoppedAtLimit:false};let t;try{t=await this.getApp();}catch(r){return {sandboxes:[],complete:false,pagesFetched:0,stoppedAtLimit:false,error:`sandbox list failed: ${r instanceof Error?r.message:String(r)}`}}return q(()=>this.client.sandboxes.list({appId:t.appId,tags:e?.metadata}),e?.limit)}async listSandboxIds(){let e=new Set;try{let t=await this.getApp();for await(let r of this.client.sandboxes.list({appId:t.appId}))e.add(r.sandboxId);return {ids:e,complete:!0}}catch{return {ids:new Set,complete:false}}}};async function q(n,e){let t=[];try{for await(let r of n()){if(e!==void 0&&t.length>=e)return {sandboxes:t,complete:!1,pagesFetched:t.length,stoppedAtLimit:!0,error:`stopped at the requested limit of ${e} with more sandboxes available`};if(t.length>=_)return {sandboxes:t,complete:!1,pagesFetched:t.length,stoppedAtLimit:!1,error:`sandbox list exceeded ${_} sandboxes`};let s=await r.getTags();t.push(E(r.sandboxId,s));}}catch(r){return {sandboxes:t,complete:false,pagesFetched:t.length,stoppedAtLimit:false,error:`sandbox list failed: ${r instanceof Error?r.message:String(r)}`}}return {sandboxes:t,complete:true,pagesFetched:t.length,stoppedAtLimit:false}}function me(n={}){let e=n.tokenId??process.env.MODAL_TOKEN_ID,t=n.tokenSecret??process.env.MODAL_TOKEN_SECRET;if(!e||!t)throw new Error("Modal credentials required. Set MODAL_TOKEN_ID and MODAL_TOKEN_SECRET environment variables, or pass tokenId/tokenSecret in config. Get your token at https://modal.com/settings/tokens");return new k({...n,tokenId:e,tokenSecret:t})}var le=P,ue=R,pe=M,ge=w,fe=F,we=L,he=W,be=E,xe=q,ye=T,Se=$,ve=B;
|
|
4
|
+
exports.EVOLVE_IMAGE_VERSION=l;exports.MODAL_MAX_LIFETIME_MS=N;exports.MODAL_MAX_LIST_SANDBOXES=_;exports.MODAL_STDIN_CHUNK_BYTES=u;exports.ModalBootCommandError=v;exports.ModalCommands=A;exports.ModalFiles=I;exports.ModalIdleTimeoutError=p;exports.ModalNetworkPolicyError=f;exports.ModalProvider=k;exports.ModalResourcesError=g;exports.ModalSandboxLifetimeError=S;exports._testBuildSandboxInfo=be;exports._testCollectSandboxes=xe;exports._testDynamicNetworkPolicyParams=ge;exports._testImageMap=ue;exports._testMapBootCommand=ve;exports._testMapIdleTimeout=Se;exports._testMapNetworkPolicy=pe;exports._testMapResources=we;exports._testRequiresDynamicNetwork=fe;exports._testResolveImageRegistry=he;exports._testValidateTimeout=ye;exports._testWrapCommand=le;exports.createModalProvider=me;
|
package/dist/index.d.cts
CHANGED
|
@@ -9,7 +9,7 @@ import { Sandbox } from 'modal';
|
|
|
9
9
|
* packages/daytona/tests/unit/daytona-image-version.test.ts fails the
|
|
10
10
|
* suite whenever this checked-in copy is stale.
|
|
11
11
|
*/
|
|
12
|
-
declare const EVOLVE_IMAGE_VERSION = "c-
|
|
12
|
+
declare const EVOLVE_IMAGE_VERSION = "c-fae503a880ec";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Modal Sandbox Provider - Clean Architecture
|
|
@@ -111,6 +111,35 @@ declare class ModalIdleTimeoutError extends Error {
|
|
|
111
111
|
declare function mapIdleTimeout(idleTimeoutMs?: number): {
|
|
112
112
|
idleTimeoutMs?: number;
|
|
113
113
|
};
|
|
114
|
+
/**
|
|
115
|
+
* Typed error for a boot command Modal could not act on as given. A refusal
|
|
116
|
+
* rather than a repair, same law as the idle bound above: the one shape
|
|
117
|
+
* rejected (an empty argv) is indistinguishable on the wire from "no args",
|
|
118
|
+
* which boots the image's own ENTRYPOINT/CMD — exactly what an explicit
|
|
119
|
+
* bootCommand exists to displace.
|
|
120
|
+
*/
|
|
121
|
+
declare class ModalBootCommandError extends Error {
|
|
122
|
+
constructor(reason: string);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Evolve's boot command -> Modal's create params, same shape as
|
|
126
|
+
* mapIdleTimeout and mapResources: provider-neutral option in, Modal
|
|
127
|
+
* fragment out.
|
|
128
|
+
*
|
|
129
|
+
* ABSENT MEANS ABSENT: with no bootCommand the create carries no `command`,
|
|
130
|
+
* and the image's own ENTRYPOINT/CMD boots as the sandbox main process —
|
|
131
|
+
* Modal's documented default and this provider's behavior since its first
|
|
132
|
+
* release; inventing a keep-alive here would silently change what every
|
|
133
|
+
* existing caller's box runs.
|
|
134
|
+
*
|
|
135
|
+
* An EMPTY argv is refused, never forwarded: Modal reads empty args as "no
|
|
136
|
+
* args" (the wire shape is the same `entrypointArgs: []` an omitted command
|
|
137
|
+
* produces), so `bootCommand: []` would boot the entrypoint while looking
|
|
138
|
+
* like an override — omit the option to boot the image's command.
|
|
139
|
+
*/
|
|
140
|
+
declare function mapBootCommand(bootCommand?: readonly string[]): {
|
|
141
|
+
command?: string[];
|
|
142
|
+
};
|
|
114
143
|
/**
|
|
115
144
|
* Wrap a command with cwd + env handling and (when not root) an
|
|
116
145
|
* `su <user> -c` wrapper.
|
|
@@ -196,6 +225,48 @@ declare class ModalNetworkPolicyError extends Error {
|
|
|
196
225
|
* TLS traffic on port 443; plaintext destinations must be listed as CIDRs.
|
|
197
226
|
*/
|
|
198
227
|
declare function mapNetworkPolicy(network?: SandboxCreateOptions["network"]): ModalNetworkCreateParams;
|
|
228
|
+
/**
|
|
229
|
+
* The same policy expressed the way Modal's RUNTIME switch takes it: both
|
|
230
|
+
* allowlists, always.
|
|
231
|
+
*
|
|
232
|
+
* Two independent reasons, and the second outlives the first. Modal requires
|
|
233
|
+
* both today — "Both `outboundCidrAllowlist` and `outboundDomainAllowlist`
|
|
234
|
+
* must be provided" (modal@0.9.0 index.d.ts:8040) — but that is documented as
|
|
235
|
+
* temporary ("This requirement will be relaxed in a future release",
|
|
236
|
+
* index.d.ts:7917-7919). What does not change is the meaning of leaving one
|
|
237
|
+
* out: "`undefined` leaves that dimension unchanged, while a defined value
|
|
238
|
+
* replaces it" (index.d.ts:7913-7915). An omitted list therefore CARRIES OVER
|
|
239
|
+
* whatever the box already had — which for a switch whose whole contract is
|
|
240
|
+
* "replace the policy" is precisely the silent-widening bug: the dimension the
|
|
241
|
+
* new policy never mentions would keep the old policy's allowances. Stating
|
|
242
|
+
* both is what makes the switch a true replace, and it stays correct when
|
|
243
|
+
* partial updates become legal.
|
|
244
|
+
*
|
|
245
|
+
* `blockNetwork` has no runtime form at all, which is why a sealed policy
|
|
246
|
+
* becomes empty lists here: "an empty array blocks all egress for that
|
|
247
|
+
* dimension" (index.d.ts:7913-7915).
|
|
248
|
+
*
|
|
249
|
+
* Built on mapNetworkPolicy so the classification of a destination — what
|
|
250
|
+
* counts as a CIDR, what is rejected for carrying a port — is the SAME code
|
|
251
|
+
* the create path uses. A second copy of that logic is how an update ends up
|
|
252
|
+
* admitting what the create refused.
|
|
253
|
+
*
|
|
254
|
+
* Upstream: harbor modal.py:1236-1249 (`_dynamic_network_kwargs`).
|
|
255
|
+
*/
|
|
256
|
+
declare function dynamicNetworkPolicyParams(network?: SandboxCreateOptions["network"]): {
|
|
257
|
+
outboundCidrAllowlist: string[];
|
|
258
|
+
outboundDomainAllowlist: string[];
|
|
259
|
+
};
|
|
260
|
+
/**
|
|
261
|
+
* Whether this box must be created in the switchable shape: true when any
|
|
262
|
+
* declared phase policy differs from the boot policy. Order and duplicate
|
|
263
|
+
* destinations are not meaning (Modal applies a set), so they are normalized
|
|
264
|
+
* away before comparing — otherwise a caller listing the same hosts in a
|
|
265
|
+
* different order would arm dynamic mode for no reason.
|
|
266
|
+
*
|
|
267
|
+
* Upstream: harbor modal.py:1040-1047 (`_requires_dynamic_network`).
|
|
268
|
+
*/
|
|
269
|
+
declare function requiresDynamicNetwork(network: SandboxCreateOptions["network"], phases: SandboxCreateOptions["phaseNetworkPolicies"]): boolean;
|
|
199
270
|
/** Container registry family for an image tag. */
|
|
200
271
|
type ImageRegistry = "aws-ecr" | "gcp-artifact-registry" | "registry";
|
|
201
272
|
/** Detect which Modal image constructor an image tag needs. */
|
|
@@ -299,6 +370,29 @@ interface SandboxCreateOptions {
|
|
|
299
370
|
* this re-checked.
|
|
300
371
|
*/
|
|
301
372
|
idleTimeoutMs?: number;
|
|
373
|
+
/**
|
|
374
|
+
* The argv the sandbox boots as its MAIN PROCESS, replacing the image's own
|
|
375
|
+
* ENTRYPOINT/CMD. Forwarded to Modal's Sandbox.create args — documented as
|
|
376
|
+
* "Set the CMD of the Sandbox, overriding any CMD of the container image"
|
|
377
|
+
* (modal.com/docs/reference/modal.Sandbox), and the override is TOTAL in
|
|
378
|
+
* operation, entrypoint included: Harbor's modal backend documents its
|
|
379
|
+
* forwarded args as overriding "the image's ENTRYPOINT/CMD" and must pass
|
|
380
|
+
* None "to inherit the image's command (e.g. dockerd-entrypoint.sh for the
|
|
381
|
+
* DinD image)" (harbor modal.py:1151-1155) — with args present that DinD
|
|
382
|
+
* entrypoint would not run at all.
|
|
383
|
+
*
|
|
384
|
+
* OMITTED, the image's own ENTRYPOINT/CMD boots as the main process, and a
|
|
385
|
+
* start program that exits takes the sandbox down with it (harbor
|
|
386
|
+
* modal.py:285-291). A caller that needs an INERT boot — the box held open
|
|
387
|
+
* while every process it runs arrives by exec, so whoever execs the image's
|
|
388
|
+
* start program is its ONLY launcher — passes a keep-alive here (Harbor's
|
|
389
|
+
* own is ["sh", "-c", "sleep infinity"]).
|
|
390
|
+
*
|
|
391
|
+
* An empty argv is REJECTED (ModalBootCommandError): on the wire it is the
|
|
392
|
+
* same "no args" an omitted option sends, so it would boot the entrypoint
|
|
393
|
+
* while reading like an override.
|
|
394
|
+
*/
|
|
395
|
+
bootCommand?: string[];
|
|
302
396
|
workingDirectory?: string;
|
|
303
397
|
/**
|
|
304
398
|
* Per-sandbox compute sizing: cpu in cores, memory in GiB — mapped to
|
|
@@ -323,6 +417,29 @@ interface SandboxCreateOptions {
|
|
|
323
417
|
outbound: "open" | "blocked";
|
|
324
418
|
allowedDestinations?: string[];
|
|
325
419
|
};
|
|
420
|
+
/**
|
|
421
|
+
* Every policy `updateNetwork()` may later be asked for on this box.
|
|
422
|
+
*
|
|
423
|
+
* LOAD-BEARING ON MODAL, unlike on the other providers. Modal's create call
|
|
424
|
+
* takes EITHER `blockNetwork: true` OR the two allowlists — each allowlist
|
|
425
|
+
* field is documented "Cannot be used with blockNetwork" (modal@0.9.0
|
|
426
|
+
* index.d.ts:7682-7686) — so the blunt `blockNetwork: true` box this adapter
|
|
427
|
+
* builds for a sealed policy has no allowlist to widen later. When a phase
|
|
428
|
+
* policy here differs from `network`, the adapter creates the box in the
|
|
429
|
+
* switchable shape instead: an empty `outboundCidrAllowlist` — "an empty
|
|
430
|
+
* array blocks all egress for that dimension" (index.d.ts:7913-7915) — plus
|
|
431
|
+
* a domain allowlist holding only the unresolvable sentinel, never an empty
|
|
432
|
+
* one (see withDomainFilteringEnabled: an empty domain list leaves Modal's
|
|
433
|
+
* domain filter switched OFF and unswitchable). Same zero egress, still
|
|
434
|
+
* switchable.
|
|
435
|
+
*
|
|
436
|
+
* Upstream: harbor modal.py:1040-1047 (`_requires_dynamic_network`) and
|
|
437
|
+
* :1169-1171 (`if self._dynamic_network: block_network = False`).
|
|
438
|
+
*/
|
|
439
|
+
phaseNetworkPolicies?: Array<{
|
|
440
|
+
outbound: "open" | "blocked";
|
|
441
|
+
allowedDestinations?: string[];
|
|
442
|
+
}>;
|
|
326
443
|
/**
|
|
327
444
|
* Run all commands and file operations as this user (default "user"),
|
|
328
445
|
* enforced via an `su <user> -c` wrapper since Modal executes everything as
|
|
@@ -426,6 +543,11 @@ interface SandboxInstance {
|
|
|
426
543
|
kill(): Promise<void>;
|
|
427
544
|
/** Pause sandbox (preserves state) */
|
|
428
545
|
pause(): Promise<void>;
|
|
546
|
+
/** Replace the outbound network policy of the running sandbox. */
|
|
547
|
+
updateNetwork(network: {
|
|
548
|
+
outbound: "open" | "blocked";
|
|
549
|
+
allowedDestinations?: string[];
|
|
550
|
+
}): Promise<void>;
|
|
429
551
|
}
|
|
430
552
|
/** Sandbox lifecycle management */
|
|
431
553
|
interface SandboxProvider {
|
|
@@ -433,6 +555,14 @@ interface SandboxProvider {
|
|
|
433
555
|
readonly providerType: string;
|
|
434
556
|
/** Human-readable provider name for logging */
|
|
435
557
|
readonly name?: string;
|
|
558
|
+
/**
|
|
559
|
+
* TRUE on every build whose create() maps SandboxCreateOptions.bootCommand
|
|
560
|
+
* to Modal's create args. A consumer whose safety depends on an inert boot
|
|
561
|
+
* must CHECK this and refuse a provider without it: an older build would
|
|
562
|
+
* silently drop the unknown option and boot the image's own ENTRYPOINT/CMD
|
|
563
|
+
* as the sandbox main process.
|
|
564
|
+
*/
|
|
565
|
+
readonly supportsBootCommand?: boolean;
|
|
436
566
|
/** Create new sandbox */
|
|
437
567
|
create(options: SandboxCreateOptions): Promise<SandboxInstance>;
|
|
438
568
|
/** Connect to existing sandbox */
|
|
@@ -572,9 +702,45 @@ declare class ModalFiles implements SandboxFiles {
|
|
|
572
702
|
watchDir(_path: string, _onEvent: (event: FilesystemEvent) => void | Promise<void>, _options?: WatchOptions): Promise<WatchHandle>;
|
|
573
703
|
private toBuffer;
|
|
574
704
|
}
|
|
705
|
+
declare class ModalSandboxImpl implements SandboxInstance {
|
|
706
|
+
private sandbox;
|
|
707
|
+
readonly commands: SandboxCommands;
|
|
708
|
+
readonly files: SandboxFiles;
|
|
709
|
+
private readonly image?;
|
|
710
|
+
constructor(sandbox: Sandbox, image: string | undefined, user: string);
|
|
711
|
+
get sandboxId(): string;
|
|
712
|
+
getHost(port: number): Promise<string>;
|
|
713
|
+
isRunning(): Promise<boolean>;
|
|
714
|
+
getInfo(): Promise<SandboxInfo>;
|
|
715
|
+
/**
|
|
716
|
+
* Replace the running sandbox's outbound policy — Modal's
|
|
717
|
+
* `Sandbox.updateNetworkPolicy`, "Updates the outbound network policy of a
|
|
718
|
+
* running Sandbox. Established connections that the new policy no longer
|
|
719
|
+
* permits are terminated." (modal@0.9.0 index.d.ts:8035-8042).
|
|
720
|
+
*
|
|
721
|
+
* The policy is mapped by dynamicNetworkPolicyParams — the create path's own
|
|
722
|
+
* classification — so an update can never admit a destination the create
|
|
723
|
+
* would have refused.
|
|
724
|
+
*
|
|
725
|
+
* WHAT THIS CANNOT FIX: a box created with `blockNetwork: true` has no
|
|
726
|
+
* allowlist for Modal to widen, because create refuses the two together
|
|
727
|
+
* (index.d.ts:7682-7686). Declare `phaseNetworkPolicies` at create and the
|
|
728
|
+
* adapter builds the box switchable instead. Modal's refusal in that case is
|
|
729
|
+
* its own error, surfaced verbatim rather than reinterpreted here — guessing
|
|
730
|
+
* at a remote refusal is how a real quota or auth failure gets mislabelled.
|
|
731
|
+
*/
|
|
732
|
+
updateNetwork(network: {
|
|
733
|
+
outbound: "open" | "blocked";
|
|
734
|
+
allowedDestinations?: string[];
|
|
735
|
+
}): Promise<void>;
|
|
736
|
+
kill(): Promise<void>;
|
|
737
|
+
pause(): Promise<void>;
|
|
738
|
+
}
|
|
575
739
|
declare class ModalProvider implements SandboxProvider {
|
|
576
740
|
readonly providerType: "modal";
|
|
577
741
|
readonly name = "Modal";
|
|
742
|
+
/** create() maps bootCommand (see SandboxCreateOptions.bootCommand). */
|
|
743
|
+
readonly supportsBootCommand = true;
|
|
578
744
|
private readonly client;
|
|
579
745
|
private readonly appName;
|
|
580
746
|
private readonly defaultTimeoutMs;
|
|
@@ -594,15 +760,23 @@ declare class ModalProvider implements SandboxProvider {
|
|
|
594
760
|
/**
|
|
595
761
|
* Build a Modal Image for the resolved tag, routing private registries
|
|
596
762
|
* (AWS ECR, GCP Artifact Registry) through the configured Modal Secret.
|
|
763
|
+
*
|
|
764
|
+
* Digest-pinned ECR refs (`<repo>@sha256:<digest>`) are accepted by
|
|
765
|
+
* fromAwsEcr — verified LIVE against Modal 2026-08-21 (built and booted a
|
|
766
|
+
* sandbox from one). Modal's docs never say so (the modal.Image reference
|
|
767
|
+
* describes the parameter only as "Full ECR image URI", tag-form example),
|
|
768
|
+
* so treat the capability as observed behavior, not contract.
|
|
597
769
|
*/
|
|
598
770
|
private resolveImage;
|
|
599
771
|
/**
|
|
600
|
-
* Resolve a requested image name to the Modal image identity
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
772
|
+
* Resolve a requested image name to the Modal image identity, eagerly
|
|
773
|
+
* building it when it is a registry reference and resolving WITHOUT a build
|
|
774
|
+
* when it is a published Modal image name. The ONE place that resolution
|
|
775
|
+
* happens, because create(), prepareImage() and publishImageAs() drifting
|
|
776
|
+
* apart is a silent failure: the prewarm would populate one image while
|
|
777
|
+
* trials created against another, and nothing would report the mismatch —
|
|
778
|
+
* only the cold-start cost prewarm was meant to remove would quietly come
|
|
779
|
+
* back.
|
|
606
780
|
*
|
|
607
781
|
* "Eagerly builds an Image on Modal" — the SDK's own description of
|
|
608
782
|
* Image.build(app) (modal@0.9.0, dist/index.d.ts). The call is idempotent:
|
|
@@ -648,6 +822,38 @@ declare class ModalProvider implements SandboxProvider {
|
|
|
648
822
|
* exists — prewarm the versioned name unless you specifically want the
|
|
649
823
|
* account's already-pulled copy.
|
|
650
824
|
*/
|
|
825
|
+
/**
|
|
826
|
+
* Give a built image OUR name on Modal, so it can be found — and deleted —
|
|
827
|
+
* later by a name this platform minted rather than an id Modal minted.
|
|
828
|
+
*
|
|
829
|
+
* WHY THIS EXISTS. Every other provider hands back a named artifact: an e2b
|
|
830
|
+
* template alias, a daytona snapshot name. Modal's image identity is the
|
|
831
|
+
* registry reference plus an opaque server-side id, and its delete verb
|
|
832
|
+
* (`client.images.delete`) takes the ID — which only exists after a build and
|
|
833
|
+
* is never returned by any lookup we could do later from a reference alone.
|
|
834
|
+
* So a Modal image built for a dataset could never be reclaimed when that
|
|
835
|
+
* dataset was deleted; the platform recorded the honest refusal
|
|
836
|
+
* `store_unsupported` and the images accumulated.
|
|
837
|
+
*
|
|
838
|
+
* `Image.publish(name)` closes that: it binds a stable name to the built
|
|
839
|
+
* image, and `images.fromName(name)` resolves that name back to the id
|
|
840
|
+
* WITHOUT rebuilding (it is a plain `imageGetByTag` lookup). Named, findable,
|
|
841
|
+
* deletable — the same shape the other two providers already have.
|
|
842
|
+
*
|
|
843
|
+
* IDEMPOTENT BY CONSTRUCTION for our use: the alias is a content address, so
|
|
844
|
+
* re-publishing the same alias re-binds it to the image that same content
|
|
845
|
+
* built. A caller that publishes twice names the same bytes twice.
|
|
846
|
+
*
|
|
847
|
+
* The build goes through resolveAndBuildImage, the ONE pair every other path
|
|
848
|
+
* uses, so a published image and the image a trial creates against cannot be
|
|
849
|
+
* different images — the same law prepareImage keeps.
|
|
850
|
+
*
|
|
851
|
+
* MODAL-ONLY, deliberately not on the shared provider interface: e2b and
|
|
852
|
+
* daytona name their artifacts at creation and have nothing to publish. The
|
|
853
|
+
* platform feature-detects this method rather than every provider carrying a
|
|
854
|
+
* verb only one of them can honor.
|
|
855
|
+
*/
|
|
856
|
+
publishImageAs(alias: string, imageName?: string): Promise<string>;
|
|
651
857
|
prepareImage(imageName?: string): Promise<void>;
|
|
652
858
|
create(options: SandboxCreateOptions): Promise<SandboxInstance>;
|
|
653
859
|
connect(sandboxId: string, _timeoutMs?: number): Promise<SandboxInstance>;
|
|
@@ -741,11 +947,22 @@ declare function createModalProvider(config?: ModalConfig): SandboxProvider;
|
|
|
741
947
|
declare const _testWrapCommand: typeof wrapCommand;
|
|
742
948
|
declare const _testImageMap: Record<string, string>;
|
|
743
949
|
declare const _testMapNetworkPolicy: typeof mapNetworkPolicy;
|
|
950
|
+
declare const _testDynamicNetworkPolicyParams: typeof dynamicNetworkPolicyParams;
|
|
951
|
+
declare const _testRequiresDynamicNetwork: typeof requiresDynamicNetwork;
|
|
744
952
|
declare const _testMapResources: typeof mapResources;
|
|
745
953
|
declare const _testResolveImageRegistry: typeof resolveImageRegistry;
|
|
746
954
|
declare const _testBuildSandboxInfo: typeof buildSandboxInfo;
|
|
747
955
|
declare const _testCollectSandboxes: typeof collectSandboxes;
|
|
748
956
|
declare const _testValidateTimeout: typeof validateTimeout;
|
|
749
957
|
declare const _testMapIdleTimeout: typeof mapIdleTimeout;
|
|
958
|
+
declare const _testMapBootCommand: typeof mapBootCommand;
|
|
959
|
+
/**
|
|
960
|
+
* TYPE-ONLY handle on the concrete sandbox class, for the contract-conformance
|
|
961
|
+
* seam. create() is declared to return the local SandboxInstance INTERFACE, so
|
|
962
|
+
* a seam reading create()'s return type checks the interface and never the
|
|
963
|
+
* class — which let a narrowed method on the class pass unnoticed. Exporting
|
|
964
|
+
* the type (never the constructor) gives the seam the real methods to pin.
|
|
965
|
+
*/
|
|
966
|
+
type _testModalSandboxImpl = ModalSandboxImpl;
|
|
750
967
|
|
|
751
|
-
export { EVOLVE_IMAGE_VERSION, type FileInfo, type FilesystemEvent, MODAL_MAX_LIFETIME_MS, MODAL_MAX_LIST_SANDBOXES, MODAL_STDIN_CHUNK_BYTES, ModalCommands, type ModalConfig, ModalFiles, ModalIdleTimeoutError, ModalNetworkPolicyError, type ModalNetworkPolicyReason, ModalProvider, ModalResourcesError, ModalSandboxLifetimeError, type ModalSandboxStream, type ProcessInfo, type SandboxCommandHandle, type SandboxCommandResult, type SandboxCommands, type SandboxConnectOptions, type SandboxCreateOptions, type SandboxFiles, type SandboxInfo, type SandboxInstance, type SandboxListOptions, type SandboxListPage, type SandboxProvider, type SandboxRunOptions, type SandboxSpawnOptions, type WatchHandle, type WatchOptions, _testBuildSandboxInfo, _testCollectSandboxes, _testImageMap, _testMapIdleTimeout, _testMapNetworkPolicy, _testMapResources, _testResolveImageRegistry, _testValidateTimeout, _testWrapCommand, createModalProvider };
|
|
968
|
+
export { EVOLVE_IMAGE_VERSION, type FileInfo, type FilesystemEvent, MODAL_MAX_LIFETIME_MS, MODAL_MAX_LIST_SANDBOXES, MODAL_STDIN_CHUNK_BYTES, ModalBootCommandError, ModalCommands, type ModalConfig, ModalFiles, ModalIdleTimeoutError, ModalNetworkPolicyError, type ModalNetworkPolicyReason, ModalProvider, ModalResourcesError, ModalSandboxLifetimeError, type ModalSandboxStream, type ProcessInfo, type SandboxCommandHandle, type SandboxCommandResult, type SandboxCommands, type SandboxConnectOptions, type SandboxCreateOptions, type SandboxFiles, type SandboxInfo, type SandboxInstance, type SandboxListOptions, type SandboxListPage, type SandboxProvider, type SandboxRunOptions, type SandboxSpawnOptions, type WatchHandle, type WatchOptions, _testBuildSandboxInfo, _testCollectSandboxes, _testDynamicNetworkPolicyParams, _testImageMap, _testMapBootCommand, _testMapIdleTimeout, _testMapNetworkPolicy, _testMapResources, type _testModalSandboxImpl, _testRequiresDynamicNetwork, _testResolveImageRegistry, _testValidateTimeout, _testWrapCommand, createModalProvider };
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { Sandbox } from 'modal';
|
|
|
9
9
|
* packages/daytona/tests/unit/daytona-image-version.test.ts fails the
|
|
10
10
|
* suite whenever this checked-in copy is stale.
|
|
11
11
|
*/
|
|
12
|
-
declare const EVOLVE_IMAGE_VERSION = "c-
|
|
12
|
+
declare const EVOLVE_IMAGE_VERSION = "c-fae503a880ec";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Modal Sandbox Provider - Clean Architecture
|
|
@@ -111,6 +111,35 @@ declare class ModalIdleTimeoutError extends Error {
|
|
|
111
111
|
declare function mapIdleTimeout(idleTimeoutMs?: number): {
|
|
112
112
|
idleTimeoutMs?: number;
|
|
113
113
|
};
|
|
114
|
+
/**
|
|
115
|
+
* Typed error for a boot command Modal could not act on as given. A refusal
|
|
116
|
+
* rather than a repair, same law as the idle bound above: the one shape
|
|
117
|
+
* rejected (an empty argv) is indistinguishable on the wire from "no args",
|
|
118
|
+
* which boots the image's own ENTRYPOINT/CMD — exactly what an explicit
|
|
119
|
+
* bootCommand exists to displace.
|
|
120
|
+
*/
|
|
121
|
+
declare class ModalBootCommandError extends Error {
|
|
122
|
+
constructor(reason: string);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Evolve's boot command -> Modal's create params, same shape as
|
|
126
|
+
* mapIdleTimeout and mapResources: provider-neutral option in, Modal
|
|
127
|
+
* fragment out.
|
|
128
|
+
*
|
|
129
|
+
* ABSENT MEANS ABSENT: with no bootCommand the create carries no `command`,
|
|
130
|
+
* and the image's own ENTRYPOINT/CMD boots as the sandbox main process —
|
|
131
|
+
* Modal's documented default and this provider's behavior since its first
|
|
132
|
+
* release; inventing a keep-alive here would silently change what every
|
|
133
|
+
* existing caller's box runs.
|
|
134
|
+
*
|
|
135
|
+
* An EMPTY argv is refused, never forwarded: Modal reads empty args as "no
|
|
136
|
+
* args" (the wire shape is the same `entrypointArgs: []` an omitted command
|
|
137
|
+
* produces), so `bootCommand: []` would boot the entrypoint while looking
|
|
138
|
+
* like an override — omit the option to boot the image's command.
|
|
139
|
+
*/
|
|
140
|
+
declare function mapBootCommand(bootCommand?: readonly string[]): {
|
|
141
|
+
command?: string[];
|
|
142
|
+
};
|
|
114
143
|
/**
|
|
115
144
|
* Wrap a command with cwd + env handling and (when not root) an
|
|
116
145
|
* `su <user> -c` wrapper.
|
|
@@ -196,6 +225,48 @@ declare class ModalNetworkPolicyError extends Error {
|
|
|
196
225
|
* TLS traffic on port 443; plaintext destinations must be listed as CIDRs.
|
|
197
226
|
*/
|
|
198
227
|
declare function mapNetworkPolicy(network?: SandboxCreateOptions["network"]): ModalNetworkCreateParams;
|
|
228
|
+
/**
|
|
229
|
+
* The same policy expressed the way Modal's RUNTIME switch takes it: both
|
|
230
|
+
* allowlists, always.
|
|
231
|
+
*
|
|
232
|
+
* Two independent reasons, and the second outlives the first. Modal requires
|
|
233
|
+
* both today — "Both `outboundCidrAllowlist` and `outboundDomainAllowlist`
|
|
234
|
+
* must be provided" (modal@0.9.0 index.d.ts:8040) — but that is documented as
|
|
235
|
+
* temporary ("This requirement will be relaxed in a future release",
|
|
236
|
+
* index.d.ts:7917-7919). What does not change is the meaning of leaving one
|
|
237
|
+
* out: "`undefined` leaves that dimension unchanged, while a defined value
|
|
238
|
+
* replaces it" (index.d.ts:7913-7915). An omitted list therefore CARRIES OVER
|
|
239
|
+
* whatever the box already had — which for a switch whose whole contract is
|
|
240
|
+
* "replace the policy" is precisely the silent-widening bug: the dimension the
|
|
241
|
+
* new policy never mentions would keep the old policy's allowances. Stating
|
|
242
|
+
* both is what makes the switch a true replace, and it stays correct when
|
|
243
|
+
* partial updates become legal.
|
|
244
|
+
*
|
|
245
|
+
* `blockNetwork` has no runtime form at all, which is why a sealed policy
|
|
246
|
+
* becomes empty lists here: "an empty array blocks all egress for that
|
|
247
|
+
* dimension" (index.d.ts:7913-7915).
|
|
248
|
+
*
|
|
249
|
+
* Built on mapNetworkPolicy so the classification of a destination — what
|
|
250
|
+
* counts as a CIDR, what is rejected for carrying a port — is the SAME code
|
|
251
|
+
* the create path uses. A second copy of that logic is how an update ends up
|
|
252
|
+
* admitting what the create refused.
|
|
253
|
+
*
|
|
254
|
+
* Upstream: harbor modal.py:1236-1249 (`_dynamic_network_kwargs`).
|
|
255
|
+
*/
|
|
256
|
+
declare function dynamicNetworkPolicyParams(network?: SandboxCreateOptions["network"]): {
|
|
257
|
+
outboundCidrAllowlist: string[];
|
|
258
|
+
outboundDomainAllowlist: string[];
|
|
259
|
+
};
|
|
260
|
+
/**
|
|
261
|
+
* Whether this box must be created in the switchable shape: true when any
|
|
262
|
+
* declared phase policy differs from the boot policy. Order and duplicate
|
|
263
|
+
* destinations are not meaning (Modal applies a set), so they are normalized
|
|
264
|
+
* away before comparing — otherwise a caller listing the same hosts in a
|
|
265
|
+
* different order would arm dynamic mode for no reason.
|
|
266
|
+
*
|
|
267
|
+
* Upstream: harbor modal.py:1040-1047 (`_requires_dynamic_network`).
|
|
268
|
+
*/
|
|
269
|
+
declare function requiresDynamicNetwork(network: SandboxCreateOptions["network"], phases: SandboxCreateOptions["phaseNetworkPolicies"]): boolean;
|
|
199
270
|
/** Container registry family for an image tag. */
|
|
200
271
|
type ImageRegistry = "aws-ecr" | "gcp-artifact-registry" | "registry";
|
|
201
272
|
/** Detect which Modal image constructor an image tag needs. */
|
|
@@ -299,6 +370,29 @@ interface SandboxCreateOptions {
|
|
|
299
370
|
* this re-checked.
|
|
300
371
|
*/
|
|
301
372
|
idleTimeoutMs?: number;
|
|
373
|
+
/**
|
|
374
|
+
* The argv the sandbox boots as its MAIN PROCESS, replacing the image's own
|
|
375
|
+
* ENTRYPOINT/CMD. Forwarded to Modal's Sandbox.create args — documented as
|
|
376
|
+
* "Set the CMD of the Sandbox, overriding any CMD of the container image"
|
|
377
|
+
* (modal.com/docs/reference/modal.Sandbox), and the override is TOTAL in
|
|
378
|
+
* operation, entrypoint included: Harbor's modal backend documents its
|
|
379
|
+
* forwarded args as overriding "the image's ENTRYPOINT/CMD" and must pass
|
|
380
|
+
* None "to inherit the image's command (e.g. dockerd-entrypoint.sh for the
|
|
381
|
+
* DinD image)" (harbor modal.py:1151-1155) — with args present that DinD
|
|
382
|
+
* entrypoint would not run at all.
|
|
383
|
+
*
|
|
384
|
+
* OMITTED, the image's own ENTRYPOINT/CMD boots as the main process, and a
|
|
385
|
+
* start program that exits takes the sandbox down with it (harbor
|
|
386
|
+
* modal.py:285-291). A caller that needs an INERT boot — the box held open
|
|
387
|
+
* while every process it runs arrives by exec, so whoever execs the image's
|
|
388
|
+
* start program is its ONLY launcher — passes a keep-alive here (Harbor's
|
|
389
|
+
* own is ["sh", "-c", "sleep infinity"]).
|
|
390
|
+
*
|
|
391
|
+
* An empty argv is REJECTED (ModalBootCommandError): on the wire it is the
|
|
392
|
+
* same "no args" an omitted option sends, so it would boot the entrypoint
|
|
393
|
+
* while reading like an override.
|
|
394
|
+
*/
|
|
395
|
+
bootCommand?: string[];
|
|
302
396
|
workingDirectory?: string;
|
|
303
397
|
/**
|
|
304
398
|
* Per-sandbox compute sizing: cpu in cores, memory in GiB — mapped to
|
|
@@ -323,6 +417,29 @@ interface SandboxCreateOptions {
|
|
|
323
417
|
outbound: "open" | "blocked";
|
|
324
418
|
allowedDestinations?: string[];
|
|
325
419
|
};
|
|
420
|
+
/**
|
|
421
|
+
* Every policy `updateNetwork()` may later be asked for on this box.
|
|
422
|
+
*
|
|
423
|
+
* LOAD-BEARING ON MODAL, unlike on the other providers. Modal's create call
|
|
424
|
+
* takes EITHER `blockNetwork: true` OR the two allowlists — each allowlist
|
|
425
|
+
* field is documented "Cannot be used with blockNetwork" (modal@0.9.0
|
|
426
|
+
* index.d.ts:7682-7686) — so the blunt `blockNetwork: true` box this adapter
|
|
427
|
+
* builds for a sealed policy has no allowlist to widen later. When a phase
|
|
428
|
+
* policy here differs from `network`, the adapter creates the box in the
|
|
429
|
+
* switchable shape instead: an empty `outboundCidrAllowlist` — "an empty
|
|
430
|
+
* array blocks all egress for that dimension" (index.d.ts:7913-7915) — plus
|
|
431
|
+
* a domain allowlist holding only the unresolvable sentinel, never an empty
|
|
432
|
+
* one (see withDomainFilteringEnabled: an empty domain list leaves Modal's
|
|
433
|
+
* domain filter switched OFF and unswitchable). Same zero egress, still
|
|
434
|
+
* switchable.
|
|
435
|
+
*
|
|
436
|
+
* Upstream: harbor modal.py:1040-1047 (`_requires_dynamic_network`) and
|
|
437
|
+
* :1169-1171 (`if self._dynamic_network: block_network = False`).
|
|
438
|
+
*/
|
|
439
|
+
phaseNetworkPolicies?: Array<{
|
|
440
|
+
outbound: "open" | "blocked";
|
|
441
|
+
allowedDestinations?: string[];
|
|
442
|
+
}>;
|
|
326
443
|
/**
|
|
327
444
|
* Run all commands and file operations as this user (default "user"),
|
|
328
445
|
* enforced via an `su <user> -c` wrapper since Modal executes everything as
|
|
@@ -426,6 +543,11 @@ interface SandboxInstance {
|
|
|
426
543
|
kill(): Promise<void>;
|
|
427
544
|
/** Pause sandbox (preserves state) */
|
|
428
545
|
pause(): Promise<void>;
|
|
546
|
+
/** Replace the outbound network policy of the running sandbox. */
|
|
547
|
+
updateNetwork(network: {
|
|
548
|
+
outbound: "open" | "blocked";
|
|
549
|
+
allowedDestinations?: string[];
|
|
550
|
+
}): Promise<void>;
|
|
429
551
|
}
|
|
430
552
|
/** Sandbox lifecycle management */
|
|
431
553
|
interface SandboxProvider {
|
|
@@ -433,6 +555,14 @@ interface SandboxProvider {
|
|
|
433
555
|
readonly providerType: string;
|
|
434
556
|
/** Human-readable provider name for logging */
|
|
435
557
|
readonly name?: string;
|
|
558
|
+
/**
|
|
559
|
+
* TRUE on every build whose create() maps SandboxCreateOptions.bootCommand
|
|
560
|
+
* to Modal's create args. A consumer whose safety depends on an inert boot
|
|
561
|
+
* must CHECK this and refuse a provider without it: an older build would
|
|
562
|
+
* silently drop the unknown option and boot the image's own ENTRYPOINT/CMD
|
|
563
|
+
* as the sandbox main process.
|
|
564
|
+
*/
|
|
565
|
+
readonly supportsBootCommand?: boolean;
|
|
436
566
|
/** Create new sandbox */
|
|
437
567
|
create(options: SandboxCreateOptions): Promise<SandboxInstance>;
|
|
438
568
|
/** Connect to existing sandbox */
|
|
@@ -572,9 +702,45 @@ declare class ModalFiles implements SandboxFiles {
|
|
|
572
702
|
watchDir(_path: string, _onEvent: (event: FilesystemEvent) => void | Promise<void>, _options?: WatchOptions): Promise<WatchHandle>;
|
|
573
703
|
private toBuffer;
|
|
574
704
|
}
|
|
705
|
+
declare class ModalSandboxImpl implements SandboxInstance {
|
|
706
|
+
private sandbox;
|
|
707
|
+
readonly commands: SandboxCommands;
|
|
708
|
+
readonly files: SandboxFiles;
|
|
709
|
+
private readonly image?;
|
|
710
|
+
constructor(sandbox: Sandbox, image: string | undefined, user: string);
|
|
711
|
+
get sandboxId(): string;
|
|
712
|
+
getHost(port: number): Promise<string>;
|
|
713
|
+
isRunning(): Promise<boolean>;
|
|
714
|
+
getInfo(): Promise<SandboxInfo>;
|
|
715
|
+
/**
|
|
716
|
+
* Replace the running sandbox's outbound policy — Modal's
|
|
717
|
+
* `Sandbox.updateNetworkPolicy`, "Updates the outbound network policy of a
|
|
718
|
+
* running Sandbox. Established connections that the new policy no longer
|
|
719
|
+
* permits are terminated." (modal@0.9.0 index.d.ts:8035-8042).
|
|
720
|
+
*
|
|
721
|
+
* The policy is mapped by dynamicNetworkPolicyParams — the create path's own
|
|
722
|
+
* classification — so an update can never admit a destination the create
|
|
723
|
+
* would have refused.
|
|
724
|
+
*
|
|
725
|
+
* WHAT THIS CANNOT FIX: a box created with `blockNetwork: true` has no
|
|
726
|
+
* allowlist for Modal to widen, because create refuses the two together
|
|
727
|
+
* (index.d.ts:7682-7686). Declare `phaseNetworkPolicies` at create and the
|
|
728
|
+
* adapter builds the box switchable instead. Modal's refusal in that case is
|
|
729
|
+
* its own error, surfaced verbatim rather than reinterpreted here — guessing
|
|
730
|
+
* at a remote refusal is how a real quota or auth failure gets mislabelled.
|
|
731
|
+
*/
|
|
732
|
+
updateNetwork(network: {
|
|
733
|
+
outbound: "open" | "blocked";
|
|
734
|
+
allowedDestinations?: string[];
|
|
735
|
+
}): Promise<void>;
|
|
736
|
+
kill(): Promise<void>;
|
|
737
|
+
pause(): Promise<void>;
|
|
738
|
+
}
|
|
575
739
|
declare class ModalProvider implements SandboxProvider {
|
|
576
740
|
readonly providerType: "modal";
|
|
577
741
|
readonly name = "Modal";
|
|
742
|
+
/** create() maps bootCommand (see SandboxCreateOptions.bootCommand). */
|
|
743
|
+
readonly supportsBootCommand = true;
|
|
578
744
|
private readonly client;
|
|
579
745
|
private readonly appName;
|
|
580
746
|
private readonly defaultTimeoutMs;
|
|
@@ -594,15 +760,23 @@ declare class ModalProvider implements SandboxProvider {
|
|
|
594
760
|
/**
|
|
595
761
|
* Build a Modal Image for the resolved tag, routing private registries
|
|
596
762
|
* (AWS ECR, GCP Artifact Registry) through the configured Modal Secret.
|
|
763
|
+
*
|
|
764
|
+
* Digest-pinned ECR refs (`<repo>@sha256:<digest>`) are accepted by
|
|
765
|
+
* fromAwsEcr — verified LIVE against Modal 2026-08-21 (built and booted a
|
|
766
|
+
* sandbox from one). Modal's docs never say so (the modal.Image reference
|
|
767
|
+
* describes the parameter only as "Full ECR image URI", tag-form example),
|
|
768
|
+
* so treat the capability as observed behavior, not contract.
|
|
597
769
|
*/
|
|
598
770
|
private resolveImage;
|
|
599
771
|
/**
|
|
600
|
-
* Resolve a requested image name to the Modal image identity
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
772
|
+
* Resolve a requested image name to the Modal image identity, eagerly
|
|
773
|
+
* building it when it is a registry reference and resolving WITHOUT a build
|
|
774
|
+
* when it is a published Modal image name. The ONE place that resolution
|
|
775
|
+
* happens, because create(), prepareImage() and publishImageAs() drifting
|
|
776
|
+
* apart is a silent failure: the prewarm would populate one image while
|
|
777
|
+
* trials created against another, and nothing would report the mismatch —
|
|
778
|
+
* only the cold-start cost prewarm was meant to remove would quietly come
|
|
779
|
+
* back.
|
|
606
780
|
*
|
|
607
781
|
* "Eagerly builds an Image on Modal" — the SDK's own description of
|
|
608
782
|
* Image.build(app) (modal@0.9.0, dist/index.d.ts). The call is idempotent:
|
|
@@ -648,6 +822,38 @@ declare class ModalProvider implements SandboxProvider {
|
|
|
648
822
|
* exists — prewarm the versioned name unless you specifically want the
|
|
649
823
|
* account's already-pulled copy.
|
|
650
824
|
*/
|
|
825
|
+
/**
|
|
826
|
+
* Give a built image OUR name on Modal, so it can be found — and deleted —
|
|
827
|
+
* later by a name this platform minted rather than an id Modal minted.
|
|
828
|
+
*
|
|
829
|
+
* WHY THIS EXISTS. Every other provider hands back a named artifact: an e2b
|
|
830
|
+
* template alias, a daytona snapshot name. Modal's image identity is the
|
|
831
|
+
* registry reference plus an opaque server-side id, and its delete verb
|
|
832
|
+
* (`client.images.delete`) takes the ID — which only exists after a build and
|
|
833
|
+
* is never returned by any lookup we could do later from a reference alone.
|
|
834
|
+
* So a Modal image built for a dataset could never be reclaimed when that
|
|
835
|
+
* dataset was deleted; the platform recorded the honest refusal
|
|
836
|
+
* `store_unsupported` and the images accumulated.
|
|
837
|
+
*
|
|
838
|
+
* `Image.publish(name)` closes that: it binds a stable name to the built
|
|
839
|
+
* image, and `images.fromName(name)` resolves that name back to the id
|
|
840
|
+
* WITHOUT rebuilding (it is a plain `imageGetByTag` lookup). Named, findable,
|
|
841
|
+
* deletable — the same shape the other two providers already have.
|
|
842
|
+
*
|
|
843
|
+
* IDEMPOTENT BY CONSTRUCTION for our use: the alias is a content address, so
|
|
844
|
+
* re-publishing the same alias re-binds it to the image that same content
|
|
845
|
+
* built. A caller that publishes twice names the same bytes twice.
|
|
846
|
+
*
|
|
847
|
+
* The build goes through resolveAndBuildImage, the ONE pair every other path
|
|
848
|
+
* uses, so a published image and the image a trial creates against cannot be
|
|
849
|
+
* different images — the same law prepareImage keeps.
|
|
850
|
+
*
|
|
851
|
+
* MODAL-ONLY, deliberately not on the shared provider interface: e2b and
|
|
852
|
+
* daytona name their artifacts at creation and have nothing to publish. The
|
|
853
|
+
* platform feature-detects this method rather than every provider carrying a
|
|
854
|
+
* verb only one of them can honor.
|
|
855
|
+
*/
|
|
856
|
+
publishImageAs(alias: string, imageName?: string): Promise<string>;
|
|
651
857
|
prepareImage(imageName?: string): Promise<void>;
|
|
652
858
|
create(options: SandboxCreateOptions): Promise<SandboxInstance>;
|
|
653
859
|
connect(sandboxId: string, _timeoutMs?: number): Promise<SandboxInstance>;
|
|
@@ -741,11 +947,22 @@ declare function createModalProvider(config?: ModalConfig): SandboxProvider;
|
|
|
741
947
|
declare const _testWrapCommand: typeof wrapCommand;
|
|
742
948
|
declare const _testImageMap: Record<string, string>;
|
|
743
949
|
declare const _testMapNetworkPolicy: typeof mapNetworkPolicy;
|
|
950
|
+
declare const _testDynamicNetworkPolicyParams: typeof dynamicNetworkPolicyParams;
|
|
951
|
+
declare const _testRequiresDynamicNetwork: typeof requiresDynamicNetwork;
|
|
744
952
|
declare const _testMapResources: typeof mapResources;
|
|
745
953
|
declare const _testResolveImageRegistry: typeof resolveImageRegistry;
|
|
746
954
|
declare const _testBuildSandboxInfo: typeof buildSandboxInfo;
|
|
747
955
|
declare const _testCollectSandboxes: typeof collectSandboxes;
|
|
748
956
|
declare const _testValidateTimeout: typeof validateTimeout;
|
|
749
957
|
declare const _testMapIdleTimeout: typeof mapIdleTimeout;
|
|
958
|
+
declare const _testMapBootCommand: typeof mapBootCommand;
|
|
959
|
+
/**
|
|
960
|
+
* TYPE-ONLY handle on the concrete sandbox class, for the contract-conformance
|
|
961
|
+
* seam. create() is declared to return the local SandboxInstance INTERFACE, so
|
|
962
|
+
* a seam reading create()'s return type checks the interface and never the
|
|
963
|
+
* class — which let a narrowed method on the class pass unnoticed. Exporting
|
|
964
|
+
* the type (never the constructor) gives the seam the real methods to pin.
|
|
965
|
+
*/
|
|
966
|
+
type _testModalSandboxImpl = ModalSandboxImpl;
|
|
750
967
|
|
|
751
|
-
export { EVOLVE_IMAGE_VERSION, type FileInfo, type FilesystemEvent, MODAL_MAX_LIFETIME_MS, MODAL_MAX_LIST_SANDBOXES, MODAL_STDIN_CHUNK_BYTES, ModalCommands, type ModalConfig, ModalFiles, ModalIdleTimeoutError, ModalNetworkPolicyError, type ModalNetworkPolicyReason, ModalProvider, ModalResourcesError, ModalSandboxLifetimeError, type ModalSandboxStream, type ProcessInfo, type SandboxCommandHandle, type SandboxCommandResult, type SandboxCommands, type SandboxConnectOptions, type SandboxCreateOptions, type SandboxFiles, type SandboxInfo, type SandboxInstance, type SandboxListOptions, type SandboxListPage, type SandboxProvider, type SandboxRunOptions, type SandboxSpawnOptions, type WatchHandle, type WatchOptions, _testBuildSandboxInfo, _testCollectSandboxes, _testImageMap, _testMapIdleTimeout, _testMapNetworkPolicy, _testMapResources, _testResolveImageRegistry, _testValidateTimeout, _testWrapCommand, createModalProvider };
|
|
968
|
+
export { EVOLVE_IMAGE_VERSION, type FileInfo, type FilesystemEvent, MODAL_MAX_LIFETIME_MS, MODAL_MAX_LIST_SANDBOXES, MODAL_STDIN_CHUNK_BYTES, ModalBootCommandError, ModalCommands, type ModalConfig, ModalFiles, ModalIdleTimeoutError, ModalNetworkPolicyError, type ModalNetworkPolicyReason, ModalProvider, ModalResourcesError, ModalSandboxLifetimeError, type ModalSandboxStream, type ProcessInfo, type SandboxCommandHandle, type SandboxCommandResult, type SandboxCommands, type SandboxConnectOptions, type SandboxCreateOptions, type SandboxFiles, type SandboxInfo, type SandboxInstance, type SandboxListOptions, type SandboxListPage, type SandboxProvider, type SandboxRunOptions, type SandboxSpawnOptions, type WatchHandle, type WatchOptions, _testBuildSandboxInfo, _testCollectSandboxes, _testDynamicNetworkPolicyParams, _testImageMap, _testMapBootCommand, _testMapIdleTimeout, _testMapNetworkPolicy, _testMapResources, type _testModalSandboxImpl, _testRequiresDynamicNetwork, _testResolveImageRegistry, _testValidateTimeout, _testWrapCommand, createModalProvider };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {ModalClient}from'modal';import {pack}from'tar-stream';var l="c-
|
|
2
|
-
`).slice(1).map(s=>{let
|
|
3
|
-
`)){if(!
|
|
4
|
-
export{l as EVOLVE_IMAGE_VERSION,
|
|
1
|
+
import {ModalClient,NotFoundError}from'modal';import {pack}from'tar-stream';var l="c-fae503a880ec";var R={[`evolve-all-${l}`]:`evolvingmachines/evolve-all:${l}`,"evolve-all":"evolvingmachines/evolve-all"},N=1440*60*1e3,u=8*1024*1024;async function*z(n){let e=n.getReader();try{for(;;){let{done:t,value:r}=await e.read();if(t)break;r&&(yield r);}}finally{e.releaseLock();}}var C="user",D="evolve.image",O="evolve.startedAt",S=class extends Error{requestedTimeoutMs;constructor(e){let t=(e/36e5).toFixed(1);super(`Modal sandboxes have a hard 24h lifetime cap; requested timeout was ${t}h. For sessions longer than 24h, persist progress with Evolve checkpoints and resume in a fresh sandbox instead of extending the timeout.`),this.name="ModalSandboxLifetimeError",this.requestedTimeoutMs=e;}};function T(n){if(n>N)throw new S(n)}var p=class extends Error{requestedIdleTimeoutMs;constructor(e,t){super(`Modal idleTimeoutMs of ${e}ms is invalid: ${t}`),this.name="ModalIdleTimeoutError",this.requestedIdleTimeoutMs=e;}};function $(n){if(n===void 0)return {};if(!Number.isFinite(n)||n<=0)throw new p(n,"it must be a positive number of milliseconds");if(n>N)throw new p(n,"it exceeds Modal's 24h lifetime cap, so the sandbox would always die of the lifetime first");return {idleTimeoutMs:n}}var v=class extends Error{constructor(e){super(`Modal bootCommand is invalid: ${e}`),this.name="ModalBootCommandError";}};function B(n){if(n===void 0)return {};if(n.length===0)throw new v(`an empty argv is Modal's own "no args" wire shape and would boot the image's ENTRYPOINT/CMD \u2014 omit bootCommand to boot the image's command`);return {command:[...n]}}function P(n,e,t,r){let s="";r&&Object.keys(r).length>0&&(s=Object.entries(r).filter(([,a])=>a!=null).map(([a,d])=>`export ${a}='${String(d).replace(/'/g,"'\\''")}'`).join("; ")+"; ");let o=t?`cd '${t.replace(/'/g,"'\\''")}' && ${s}${n}`:`${s}${n}`;if(e==="root")return ["bash","-c",o];let i=Buffer.from(o).toString("base64");return ["su",e,"-c",`echo ${i} | base64 -d | bash`]}var g=class extends Error{constructor(e){super(e),this.name="ModalResourcesError";}},Y=4,X=4096;function L(n){if(n?.disk!==void 0)throw new g(`Modal's JS SDK has no create-time disk-size parameter, so a ${n.disk} GiB disk request cannot be enforced. Drop \`resources.disk\` (containers get Modal's default disk quota) or run on a provider that sizes disk.`);if((n?.gpu===void 0||n.gpu<=0)&&n?.gpuTypes?.length)throw new g("resources.gpuTypes was set without a positive resources.gpu count \u2014 declare how many GPUs to reserve, or drop gpuTypes.");let e=n?.gpu!==void 0&&n.gpu>0?`${n.gpuTypes?.length?n.gpuTypes[0]:"any"}:${n.gpu}`:void 0;return {cpu:n?.cpu??Y,memoryMiB:n?.memory!==void 0?Math.ceil(n.memory*1024):X,...e!==void 0?{gpu:e}:{}}}var f=class extends Error{reason;destination;constructor(e,t,r){super(t),this.name="ModalNetworkPolicyError",this.reason=e,this.destination=r;}};function U(n){let e=/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(?:\/(\d{1,3}))?$/.exec(n);return !(!e||[e[1],e[2],e[3],e[4]].some(t=>Number(t)>255)||e[5]!==void 0&&Number(e[5])>32)}function J(n){return /^\d{1,3}(\.\d{1,3}){3}(\/\d+)?$/.test(n)&&!U(n)}function Q(n){return n.startsWith("[")?true:(n.match(/:/g)?.length??0)>=2}function Z(n){return /^[^:]+:\d+$/.test(n)}function M(n){if(!n||n.outbound==="open"){if(n?.allowedDestinations?.length)throw new Error("network.allowedDestinations is only valid when outbound is blocked");return {}}let e=n.allowedDestinations??[];if(e.length===0)return {blockNetwork:true};let t=[],r=[];for(let s of e)if(U(s))t.push(s.includes("/")?s:`${s}/32`);else {if(J(s))throw new f("invalid-ipv4",`"${s}" is not a valid IPv4 address or CIDR (octets must be 0-255, prefix 0-32). Fix the address or list a hostname instead.`,s);if(Q(s))t.push(s.includes("/")?s:`${s}/128`);else {if(Z(s))throw new f("port-unsupported",`Modal's network allowlist filters hosts and IPs only and cannot match a port; drop the ":<port>" from "${s}" and list just the host or IP.`,s);r.push(s);}}return {outboundCidrAllowlist:t,outboundDomainAllowlist:r}}function w(n){if(!n||n.outbound==="open"){if(n?.allowedDestinations?.length)throw new Error("network.allowedDestinations is only valid when outbound is blocked");return {outboundCidrAllowlist:["0.0.0.0/0"],outboundDomainAllowlist:["*"]}}let e=M(n);return {outboundCidrAllowlist:e.outboundCidrAllowlist??[],outboundDomainAllowlist:te(e.outboundDomainAllowlist??[])}}var ee="sealed.invalid";function te(n){return n.length>0?n:[ee]}function F(n,e){if(!e||e.length===0)return false;let t=s=>s===void 0?"open|":`${s.outbound}|${[...new Set(s.allowedDestinations??[])].sort().join(",")}`,r=t(n);return e.some(s=>t(s)!==r)}function W(n){let e=n.split("/")[0];return /^\d{12}\.dkr\.ecr\.[a-z0-9-]+\.amazonaws\.com$/.test(e)?"aws-ecr":e==="gcr.io"||e.endsWith(".gcr.io")||e.endsWith("-docker.pkg.dev")?"gcp-artifact-registry":"registry"}function E(n,e,t){let{[D]:r,[O]:s,...o}=e;return {sandboxId:n,image:r??t??"",metadata:o,startedAt:s??""}}var _=1e4,A=class{constructor(e,t){this.sandbox=e;this.user=t;}async run(e,t){let r=P(e,this.user,t?.cwd,t?.envs),s=await this.sandbox.exec(r,{timeoutMs:t?.timeoutMs}),{stdout:o,stderr:i}=await this.accumulateStreams(s,t?.onStdout,t?.onStderr);return {exitCode:await s.wait(),stdout:o,stderr:i}}async spawn(e,t){let r=P(e,this.user,t?.cwd,t?.envs),s=await this.sandbox.exec(r,{timeoutMs:t?.timeoutMs}),o="",i="",a=this.accumulateStreams(s,t?.onStdout?c=>{t.onStdout(c);}:void 0,t?.onStderr?c=>{t.onStderr(c);}:void 0).then(({stdout:c,stderr:b})=>{o=c,i=b;}).catch(()=>{});return {processId:`modal-${Date.now()}-${Math.random().toString(36).slice(2)}`,wait:async()=>(await a,{exitCode:await s.wait(),stdout:o,stderr:i}),kill:async()=>false}}async list(){let e=await this.sandbox.exec(["ps","-eo","pid,comm,args"],{timeoutMs:1e4});return await e.wait(),(await e.stdout.readText()).trim().split(`
|
|
2
|
+
`).slice(1).map(s=>{let o=s.trim().split(/\s+/);return {processId:o[0],cmd:o[1]||"",args:o.slice(2),envs:{}}})}async connect(e,t){throw new Error("Modal does not support connecting to existing processes")}async sendStdin(e,t){throw new Error("Modal does not support sendStdin by process ID")}async kill(e){return await(await this.sandbox.exec(["kill","-9",e],{timeoutMs:1e4})).wait()===0}async accumulateStreams(e,t,r){let s="",o="",i=[];return i.push((async()=>{try{for await(let a of e.stdout){let d=typeof a=="string"?a:new TextDecoder().decode(a);s+=d,t?.(d);}}catch{}})()),i.push((async()=>{try{for await(let a of e.stderr){let d=typeof a=="string"?a:new TextDecoder().decode(a);o+=d,r?.(d);}}catch{}})()),await Promise.all(i),{stdout:s,stderr:o}}},I=class{constructor(e,t){this.sandbox=e;this.user=t;}async chownToUser(e,t=false){if(this.user==="root")return;let r=t?["chown","-R",`${this.user}:${this.user}`,e]:["chown",`${this.user}:${this.user}`,e];await(await this.sandbox.exec(r,{timeoutMs:t?3e4:1e4})).wait();}async writeStdinChunked(e,t){for(let r=0;r<t.length;r+=u)await e.writeBytes(t.subarray(r,r+u));}async writeStdinCoalesced(e,t){let r=[],s=0,o=async()=>{if(s===0)return;let i=r.length===1?r[0]:Buffer.concat(r,s);r=[],s=0,await this.writeStdinChunked(e,i);};for await(let i of t)r.push(i),s+=i.length,s>=u&&await o();await o();}async read(e){let t=await this.sandbox.exec(["cat",e],{timeoutMs:3e5,mode:"binary"}),r=await t.wait();if(r!==0){let o=await t.stderr.readText();throw new Error(`Failed to read file ${e}: ${o||`exit code ${r}`}`)}let s=await t.stdout.readBytes();if(!s.includes(0))try{return new TextDecoder("utf-8",{fatal:!0,ignoreBOM:!0}).decode(s)}catch{}return s}async write(e,t){let r=this.toBuffer(t),s=e.substring(0,e.lastIndexOf("/"));s&&await this.makeDir(s);let o=e.replace(/'/g,"'\\''"),i=await this.sandbox.exec(["bash","-c",`cat > '${o}'`],{mode:"binary"});await this.writeStdinChunked(i.stdin,new Uint8Array(r)),await i.stdin.getWriter().close(),await i.wait(),await this.chownToUser(e);}async writeBatch(e){let t=pack(),r=new Set;for(let i of e){let a=this.toBuffer(i.data),d=i.path.startsWith("/")?i.path.slice(1):i.path;t.entry({name:d},a);let c=i.path.substring(0,i.path.lastIndexOf("/"));c&&r.add(c);}t.finalize();let s=await this.sandbox.exec(["tar","-xf","-","-C","/"],{mode:"binary"});if(await this.writeStdinCoalesced(s.stdin,t),await s.stdin.getWriter().close(),await s.wait(),r.size>0){let i=Array.from(r),a=new Set(i.map(d=>d.split("/").slice(0,4).join("/")));for(let d of a)await this.chownToUser(d,true);}}async makeDir(e){await(await this.sandbox.exec(["mkdir","-p",e],{timeoutMs:1e4})).wait(),await this.chownToUser(e,true);}async exists(e){return await(await this.sandbox.exec(["test","-e",e],{timeoutMs:1e4})).wait()===0}async list(e){let t=e.replace(/'/g,"'\\''"),r=await this.sandbox.exec(["bash","-c",`ls -la '${t}' | tail -n +2`],{timeoutMs:3e4});await r.wait();let s=await r.stdout.readText(),o=[];for(let i of s.trim().split(`
|
|
3
|
+
`)){if(!i)continue;let a=i.split(/\s+/);if(a.length<9)continue;let d=a[0],c=a.slice(8).join(" ");c==="."||c===".."||o.push({name:c,path:e.endsWith("/")?`${e}${c}`:`${e}/${c}`,type:d.startsWith("d")?"dir":"file"});}return o}async remove(e){await(await this.sandbox.exec(["rm","-rf",e],{timeoutMs:3e4})).wait();}async rename(e,t){await(await this.sandbox.exec(["mv",e,t],{timeoutMs:3e4})).wait();}async readStream(e){return (await this.sandbox.exec(["cat",e],{timeoutMs:3e5,mode:"binary"})).stdout}async writeStream(e,t){let r=e.substring(0,e.lastIndexOf("/"));r&&await this.makeDir(r);let s=e.replace(/'/g,"'\\''"),o=await this.sandbox.exec(["bash","-c",`cat > '${s}'`],{mode:"binary"});await this.writeStdinCoalesced(o.stdin,z(t)),await o.stdin.getWriter().close(),await o.wait(),await this.chownToUser(e);}async writeFromPath(e,t){let{createReadStream:r}=await import('fs'),{Readable:s}=await import('stream'),o=s.toWeb(r(t,{highWaterMark:u}));await this.writeStream(e,o);}async uploadUrl(e,t){throw new Error("Modal does not support pre-signed upload URLs")}async downloadUrl(e,t){throw new Error("Modal does not support pre-signed download URLs")}async watchDir(e,t,r){throw new Error("Modal does not support watchDir")}toBuffer(e){if(typeof e=="string")return Buffer.from(e,"utf-8");if(e instanceof Buffer)return e;if(e instanceof ArrayBuffer||e instanceof Uint8Array)return Buffer.from(e);throw new Error(`Unsupported data type: ${typeof e}`)}},h=class{constructor(e,t,r){this.sandbox=e;this.commands=new A(e,r),this.files=new I(e,r),this.image=t;}commands;files;image;get sandboxId(){return this.sandbox.sandboxId}async getHost(e){let r=(await this.sandbox.tunnels())[e];if(!r)throw new Error(`No tunnel found for port ${e}`);return r.url}async isRunning(){try{return await this.sandbox.poll()===null}catch{return false}}async getInfo(){let e=await this.sandbox.getTags();return E(this.sandbox.sandboxId,e,this.image)}async updateNetwork(e){await this.sandbox.updateNetworkPolicy(w(e));}async kill(){try{await this.sandbox.terminate();}catch{await new Promise(e=>setTimeout(e,500)),await this.sandbox.terminate();}}async pause(){throw new Error("Modal does not support pause/resume. Persist progress with Evolve checkpoints and resume in a fresh sandbox, or use kill() to terminate.")}},k=class{providerType="modal";name="Modal";supportsBootCommand=true;client;appName;defaultTimeoutMs;imageName;imageSecretName;_app;sandboxUsers=new Map;constructor(e){!e.endpoint&&process.env.MODAL_SERVER_URL?.startsWith("unix:")?process.env.MODAL_SERVER_URL="https://api.modal.com:443":e.endpoint&&(process.env.MODAL_SERVER_URL=e.endpoint),this.client=new ModalClient({tokenId:e.tokenId,tokenSecret:e.tokenSecret}),this.appName=e.appName??"evolve-sandbox",this.defaultTimeoutMs=e.defaultTimeoutMs??36e5,this.imageName=e.imageName??`evolve-all-${l}`,this.imageSecretName=e.imageSecretName;}async getApp(){return this._app||(this._app=await this.client.apps.fromName(this.appName,{createIfMissing:true})),this._app}async resolveImage(e){let t=W(e);if(t==="registry"){let s=this.imageSecretName?await this.client.secrets.fromName(this.imageSecretName):void 0;return this.client.images.fromRegistry(e,s)}if(!this.imageSecretName)throw new Error(`Private registry image "${e}" requires config.imageSecretName \u2014 the name of a Modal Secret holding registry credentials (AWS ECR: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION with read-only ECR IAM). Create one at https://modal.com/secrets`);let r=t==="aws-ecr"?await this.client.secrets.fromName(this.imageSecretName,{requiredKeys:["AWS_ACCESS_KEY_ID","AWS_SECRET_ACCESS_KEY","AWS_REGION"]}):await this.client.secrets.fromName(this.imageSecretName);return t==="aws-ecr"?this.client.images.fromAwsEcr(e,r):this.client.images.fromGcpArtifactRegistry(e,r)}async resolveAndBuildImage(e){let t=R[e]??e;if(!/[/:@]/.test(t))try{return {tag:t,image:await this.client.images.fromName(t)}}catch(o){if(!(o instanceof NotFoundError))throw o}let r=await this.getApp(),s=await this.resolveImage(t);return {tag:t,image:await s.build(r)}}async publishImageAs(e,t){if(!e.trim())throw new Error("publishImageAs requires a non-empty alias to publish under");let{image:r}=await this.resolveAndBuildImage(t||this.imageName);return await r.publish(e),e}async prepareImage(e){await this.resolveAndBuildImage(e||this.imageName);}async create(e){let t=e.timeoutMs??this.defaultTimeoutMs;T(t);let r=$(e.idleTimeoutMs),s=B(e.bootCommand);for(let m of e.phaseNetworkPolicies??[])w(m);let o=F(e.network,e.phaseNetworkPolicies)?w(e.network):M(e.network),i=L(e.resources),a=e.user??C,d=await this.getApp(),{tag:c,image:b}=await this.resolveAndBuildImage(e.image||this.imageName),x=e.envs?Object.fromEntries(Object.entries(e.envs).filter(([,m])=>m!=null)):void 0,G=x&&Object.keys(x).length>0?x:void 0,H={...e.metadata,[D]:c,[O]:new Date().toISOString()},y=await this.client.sandboxes.create(d,b,{cpu:i.cpu,memoryMiB:i.memoryMiB,...i.gpu!==void 0?{gpu:i.gpu}:{},timeoutMs:t,...r,...s,workdir:e.workingDirectory,env:G,tags:H,...o});return e.workingDirectory&&a!=="root"&&await(await y.exec(["chown","-R",`${a}:${a}`,e.workingDirectory],{timeoutMs:3e4})).wait(),this.sandboxUsers.set(y.sandboxId,a),new h(y,c,a)}async connect(e,t){let r=await this.client.sandboxes.fromId(e),s=this.sandboxUsers.get(e)??C;return new h(r,void 0,s)}async list(e){let t=await this.walk(e);if(t.error&&!t.stoppedAtLimit)throw new Error(t.error);return t.sandboxes}async listAll(e){let{stoppedAtLimit:t,...r}=await this.walk(e);return r}async walk(e){if(e?.state&&!e.state.includes("running"))return {sandboxes:[],complete:true,pagesFetched:0,stoppedAtLimit:false};let t;try{t=await this.getApp();}catch(r){return {sandboxes:[],complete:false,pagesFetched:0,stoppedAtLimit:false,error:`sandbox list failed: ${r instanceof Error?r.message:String(r)}`}}return q(()=>this.client.sandboxes.list({appId:t.appId,tags:e?.metadata}),e?.limit)}async listSandboxIds(){let e=new Set;try{let t=await this.getApp();for await(let r of this.client.sandboxes.list({appId:t.appId}))e.add(r.sandboxId);return {ids:e,complete:!0}}catch{return {ids:new Set,complete:false}}}};async function q(n,e){let t=[];try{for await(let r of n()){if(e!==void 0&&t.length>=e)return {sandboxes:t,complete:!1,pagesFetched:t.length,stoppedAtLimit:!0,error:`stopped at the requested limit of ${e} with more sandboxes available`};if(t.length>=_)return {sandboxes:t,complete:!1,pagesFetched:t.length,stoppedAtLimit:!1,error:`sandbox list exceeded ${_} sandboxes`};let s=await r.getTags();t.push(E(r.sandboxId,s));}}catch(r){return {sandboxes:t,complete:false,pagesFetched:t.length,stoppedAtLimit:false,error:`sandbox list failed: ${r instanceof Error?r.message:String(r)}`}}return {sandboxes:t,complete:true,pagesFetched:t.length,stoppedAtLimit:false}}function me(n={}){let e=n.tokenId??process.env.MODAL_TOKEN_ID,t=n.tokenSecret??process.env.MODAL_TOKEN_SECRET;if(!e||!t)throw new Error("Modal credentials required. Set MODAL_TOKEN_ID and MODAL_TOKEN_SECRET environment variables, or pass tokenId/tokenSecret in config. Get your token at https://modal.com/settings/tokens");return new k({...n,tokenId:e,tokenSecret:t})}var le=P,ue=R,pe=M,ge=w,fe=F,we=L,he=W,be=E,xe=q,ye=T,Se=$,ve=B;
|
|
4
|
+
export{l as EVOLVE_IMAGE_VERSION,N as MODAL_MAX_LIFETIME_MS,_ as MODAL_MAX_LIST_SANDBOXES,u as MODAL_STDIN_CHUNK_BYTES,v as ModalBootCommandError,A as ModalCommands,I as ModalFiles,p as ModalIdleTimeoutError,f as ModalNetworkPolicyError,k as ModalProvider,g as ModalResourcesError,S as ModalSandboxLifetimeError,be as _testBuildSandboxInfo,xe as _testCollectSandboxes,ge as _testDynamicNetworkPolicyParams,ue as _testImageMap,ve as _testMapBootCommand,Se as _testMapIdleTimeout,pe as _testMapNetworkPolicy,we as _testMapResources,fe as _testRequiresDynamicNetwork,he as _testResolveImageRegistry,ye as _testValidateTimeout,le as _testWrapCommand,me as createModalProvider};
|