@evolvingmachines/modal 0.0.52-project-sable.20260810.ca7ed91 → 0.0.52-project-sable.20260815.143c2cd

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 CHANGED
@@ -1,4 +1,4 @@
1
- 'use strict';var modal=require('modal'),tarStream=require('tar-stream');var p="c-7bcda3467a79";var R={[`evolve-all-${p}`]:`evolvingmachines/evolve-all:${p}`,"evolve-all":"evolvingmachines/evolve-all"},_=1440*60*1e3,M=8*1024*1024,k="user",C="evolve.image",O="evolve.startedAt",y=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(s){if(s>_)throw new y(s)}var u=class extends Error{requestedIdleTimeoutMs;constructor(e,t){super(`Modal idleTimeoutMs of ${e}ms is invalid: ${t}`),this.name="ModalIdleTimeoutError",this.requestedIdleTimeoutMs=e;}};function D(s){if(s===void 0)return {};if(!Number.isFinite(s)||s<=0)throw new u(s,"it must be a positive number of milliseconds");if(s>_)throw new u(s,"it exceeds Modal's 24h lifetime cap, so the sandbox would always die of the lifetime first");return {idleTimeoutMs:s}}function S(s,e,t,r){let n="";r&&Object.keys(r).length>0&&(n=Object.entries(r).filter(([,d])=>d!=null).map(([d,a])=>`export ${d}='${String(a).replace(/'/g,"'\\''")}'`).join("; ")+"; ");let i=t?`cd '${t.replace(/'/g,"'\\''")}' && ${n}${s}`:`${n}${s}`;if(e==="root")return ["bash","-c",i];let o=Buffer.from(i).toString("base64");return ["su",e,"-c",`echo ${o} | base64 -d | bash`]}var g=class extends Error{constructor(e){super(e),this.name="ModalResourcesError";}},V=4,j=4096;function N(s){if(s?.disk!==void 0)throw new g(`Modal's JS SDK has no create-time disk-size parameter, so a ${s.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((s?.gpu===void 0||s.gpu<=0)&&s?.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=s?.gpu!==void 0&&s.gpu>0?`${s.gpuTypes?.length?s.gpuTypes[0]:"any"}:${s.gpu}`:void 0;return {cpu:s?.cpu??V,memoryMiB:s?.memory!==void 0?Math.ceil(s.memory*1024):j,...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 $(s){let e=/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(?:\/(\d{1,3}))?$/.exec(s);return !(!e||[e[1],e[2],e[3],e[4]].some(t=>Number(t)>255)||e[5]!==void 0&&Number(e[5])>32)}function q(s){return /^\d{1,3}(\.\d{1,3}){3}(\/\d+)?$/.test(s)&&!$(s)}function K(s){return s.startsWith("[")?true:(s.match(/:/g)?.length??0)>=2}function z(s){return /^[^:]+:\d+$/.test(s)}function L(s){if(!s||s.outbound==="open"){if(s?.allowedDestinations?.length)throw new Error("network.allowedDestinations is only valid when outbound is blocked");return {}}let e=s.allowedDestinations??[];if(e.length===0)return {blockNetwork:true};let t=[],r=[];for(let n of e)if($(n))t.push(n.includes("/")?n:`${n}/32`);else {if(q(n))throw new f("invalid-ipv4",`"${n}" is not a valid IPv4 address or CIDR (octets must be 0-255, prefix 0-32). Fix the address or list a hostname instead.`,n);if(K(n))t.push(n.includes("/")?n:`${n}/128`);else {if(z(n))throw new f("port-unsupported",`Modal's network allowlist filters hosts and IPs only and cannot match a port; drop the ":<port>" from "${n}" and list just the host or IP.`,n);r.push(n);}}return {outboundCidrAllowlist:t,outboundDomainAllowlist:r}}function U(s){let e=s.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 A(s,e,t){let{[C]:r,[O]:n,...i}=e;return {sandboxId:s,image:r??t??"",metadata:i,startedAt:n??""}}var E=1e4,v=class{constructor(e,t){this.sandbox=e;this.user=t;}async run(e,t){let r=S(e,this.user,t?.cwd,t?.envs),n=await this.sandbox.exec(r,{timeoutMs:t?.timeoutMs}),{stdout:i,stderr:o}=await this.accumulateStreams(n,t?.onStdout,t?.onStderr);return {exitCode:await n.wait(),stdout:i,stderr:o}}async spawn(e,t){let r=S(e,this.user,t?.cwd,t?.envs),n=await this.sandbox.exec(r,{timeoutMs:t?.timeoutMs}),i="",o="",d=this.accumulateStreams(n,t?.onStdout?c=>{t.onStdout(c);}:void 0,t?.onStderr?c=>{t.onStderr(c);}:void 0).then(({stdout:c,stderr:m})=>{i=c,o=m;}).catch(()=>{});return {processId:`modal-${Date.now()}-${Math.random().toString(36).slice(2)}`,wait:async()=>(await d,{exitCode:await n.wait(),stdout:i,stderr:o}),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(n=>{let i=n.trim().split(/\s+/);return {processId:i[0],cmd:i[1]||"",args:i.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 n="",i="",o=[];return o.push((async()=>{try{for await(let d of e.stdout){let a=typeof d=="string"?d:new TextDecoder().decode(d);n+=a,t?.(a);}}catch{}})()),o.push((async()=>{try{for await(let d of e.stderr){let a=typeof d=="string"?d:new TextDecoder().decode(d);i+=a,r?.(a);}}catch{}})()),await Promise.all(o),{stdout:n,stderr:i}}},P=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+=M)await e.writeBytes(t.subarray(r,r+M));}async read(e){let t=await this.sandbox.exec(["cat",e],{timeoutMs:3e5,mode:"binary"}),r=await t.wait();if(r!==0){let i=await t.stderr.readText();throw new Error(`Failed to read file ${e}: ${i||`exit code ${r}`}`)}let n=await t.stdout.readBytes();if(!n.includes(0))try{return new TextDecoder("utf-8",{fatal:!0,ignoreBOM:!0}).decode(n)}catch{}return n}async write(e,t){let r=this.toBuffer(t),n=e.substring(0,e.lastIndexOf("/"));n&&await this.makeDir(n);let i=e.replace(/'/g,"'\\''"),o=await this.sandbox.exec(["bash","-c",`cat > '${i}'`],{mode:"binary"});await this.writeStdinChunked(o.stdin,new Uint8Array(r)),await o.stdin.getWriter().close(),await o.wait(),await this.chownToUser(e);}async writeBatch(e){let t=tarStream.pack(),r=[],n=new Set;for(let a of e){let c=this.toBuffer(a.data),m=a.path.startsWith("/")?a.path.slice(1):a.path;t.entry({name:m},c);let l=a.path.substring(0,a.path.lastIndexOf("/"));l&&n.add(l);}t.finalize();for await(let a of t)r.push(Buffer.from(a));let i=Buffer.concat(r),o=await this.sandbox.exec(["tar","-xf","-","-C","/"],{mode:"binary"});if(await this.writeStdinChunked(o.stdin,new Uint8Array(i)),await o.stdin.getWriter().close(),await o.wait(),n.size>0){let a=Array.from(n),c=new Set(a.map(m=>m.split("/").slice(0,4).join("/")));for(let m of c)await this.chownToUser(m,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 n=await r.stdout.readText(),i=[];for(let o of n.trim().split(`
3
- `)){if(!o)continue;let d=o.split(/\s+/);if(d.length<9)continue;let a=d[0],c=d.slice(8).join(" ");c==="."||c===".."||i.push({name:c,path:e.endsWith("/")?`${e}${c}`:`${e}/${c}`,type:a.startsWith("d")?"dir":"file"});}return i}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 n=e.replace(/'/g,"'\\''"),i=await this.sandbox.exec(["bash","-c",`cat > '${n}'`],{mode:"binary"}),o=t.getReader();try{for(;;){let{done:a,value:c}=await o.read();if(a)break;await this.writeStdinChunked(i.stdin,c);}}finally{o.releaseLock();}await i.stdin.getWriter().close(),await i.wait(),await this.chownToUser(e);}async writeFromPath(e,t){let{createReadStream:r}=await import('fs'),{Readable:n}=await import('stream'),i=n.toWeb(r(t));await this.writeStream(e,i);}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 v(e,r),this.files=new P(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 A(this.sandbox.sandboxId,e,this.image)}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.")}},I=class{providerType="modal";name="Modal";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-${p}`,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=U(e);if(t==="registry"){let n=this.imageSecretName?await this.client.secrets.fromName(this.imageSecretName):void 0;return this.client.images.fromRegistry(e,n)}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=await this.client.secrets.fromName(this.imageSecretName);return t==="aws-ecr"?this.client.images.fromAwsEcr(e,r):this.client.images.fromGcpArtifactRegistry(e,r)}async create(e){let t=e.timeoutMs??this.defaultTimeoutMs;T(t);let r=D(e.idleTimeoutMs),n=L(e.network),i=N(e.resources),o=e.user??k,d=await this.getApp(),a=e.image||this.imageName,c=R[a]??a,l=await(await this.resolveImage(c)).build(d),w=e.envs?Object.fromEntries(Object.entries(e.envs).filter(([,x])=>x!=null)):void 0,F=w&&Object.keys(w).length>0?w:void 0,W={...e.metadata,[C]:c,[O]:new Date().toISOString()},b=await this.client.sandboxes.create(d,l,{cpu:i.cpu,memoryMiB:i.memoryMiB,...i.gpu!==void 0?{gpu:i.gpu}:{},timeoutMs:t,...r,workdir:e.workingDirectory,env:F,tags:W,...n});return e.workingDirectory&&o!=="root"&&await(await b.exec(["chown","-R",`${o}:${o}`,e.workingDirectory],{timeoutMs:3e4})).wait(),this.sandboxUsers.set(b.sandboxId,o),new h(b,c,o)}async connect(e,t){let r=await this.client.sandboxes.fromId(e),n=this.sandboxUsers.get(e)??k;return new h(r,void 0,n)}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 B(()=>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 B(s,e){let t=[];try{for await(let r of s()){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>=E)return {sandboxes:t,complete:!1,pagesFetched:t.length,stoppedAtLimit:!1,error:`sandbox list exceeded ${E} sandboxes`};let n=await r.getTags();t.push(A(r.sandboxId,n));}}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 se(s={}){let e=s.tokenId??process.env.MODAL_TOKEN_ID,t=s.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 I({...s,tokenId:e,tokenSecret:t})}var ne=S,ie=R,oe=L,ae=N,de=U,ce=A,me=B,le=T,pe=D;
4
- exports.EVOLVE_IMAGE_VERSION=p;exports.MODAL_MAX_LIFETIME_MS=_;exports.MODAL_MAX_LIST_SANDBOXES=E;exports.MODAL_STDIN_CHUNK_BYTES=M;exports.ModalCommands=v;exports.ModalFiles=P;exports.ModalIdleTimeoutError=u;exports.ModalNetworkPolicyError=f;exports.ModalProvider=I;exports.ModalResourcesError=g;exports.ModalSandboxLifetimeError=y;exports._testBuildSandboxInfo=ce;exports._testCollectSandboxes=me;exports._testImageMap=ie;exports._testMapIdleTimeout=pe;exports._testMapNetworkPolicy=oe;exports._testMapResources=ae;exports._testResolveImageRegistry=de;exports._testValidateTimeout=le;exports._testWrapCommand=ne;exports.createModalProvider=se;
1
+ 'use strict';var modal=require('modal'),tarStream=require('tar-stream');var l="c-7bcda3467a79";var k={[`evolve-all-${l}`]:`evolvingmachines/evolve-all:${l}`,"evolve-all":"evolvingmachines/evolve-all"},E=1440*60*1e3,p=8*1024*1024;async function*H(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 A="user",R="evolve.image",_="evolve.startedAt",x=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 C(n){if(n>E)throw new x(n)}var u=class extends Error{requestedIdleTimeoutMs;constructor(e,t){super(`Modal idleTimeoutMs of ${e}ms is invalid: ${t}`),this.name="ModalIdleTimeoutError",this.requestedIdleTimeoutMs=e;}};function O(n){if(n===void 0)return {};if(!Number.isFinite(n)||n<=0)throw new u(n,"it must be a positive number of milliseconds");if(n>E)throw new u(n,"it exceeds Modal's 24h lifetime cap, so the sandbox would always die of the lifetime first");return {idleTimeoutMs:n}}function y(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 i=t?`cd '${t.replace(/'/g,"'\\''")}' && ${s}${n}`:`${s}${n}`;if(e==="root")return ["bash","-c",i];let o=Buffer.from(i).toString("base64");return ["su",e,"-c",`echo ${o} | base64 -d | bash`]}var g=class extends Error{constructor(e){super(e),this.name="ModalResourcesError";}},G=4,V=4096;function T(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??G,memoryMiB:n?.memory!==void 0?Math.ceil(n.memory*1024):V,...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 D(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)&&!D(n)}function q(n){return n.startsWith("[")?true:(n.match(/:/g)?.length??0)>=2}function K(n){return /^[^:]+:\d+$/.test(n)}function $(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(D(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(K(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 N(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 I(n,e,t){let{[R]:r,[_]:s,...i}=e;return {sandboxId:n,image:r??t??"",metadata:i,startedAt:s??""}}var M=1e4,S=class{constructor(e,t){this.sandbox=e;this.user=t;}async run(e,t){let r=y(e,this.user,t?.cwd,t?.envs),s=await this.sandbox.exec(r,{timeoutMs:t?.timeoutMs}),{stdout:i,stderr:o}=await this.accumulateStreams(s,t?.onStdout,t?.onStderr);return {exitCode:await s.wait(),stdout:i,stderr:o}}async spawn(e,t){let r=y(e,this.user,t?.cwd,t?.envs),s=await this.sandbox.exec(r,{timeoutMs:t?.timeoutMs}),i="",o="",a=this.accumulateStreams(s,t?.onStdout?c=>{t.onStdout(c);}:void 0,t?.onStderr?c=>{t.onStderr(c);}:void 0).then(({stdout:c,stderr:m})=>{i=c,o=m;}).catch(()=>{});return {processId:`modal-${Date.now()}-${Math.random().toString(36).slice(2)}`,wait:async()=>(await a,{exitCode:await s.wait(),stdout:i,stderr:o}),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 i=s.trim().split(/\s+/);return {processId:i[0],cmd:i[1]||"",args:i.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="",i="",o=[];return o.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{}})()),o.push((async()=>{try{for await(let a of e.stderr){let d=typeof a=="string"?a:new TextDecoder().decode(a);i+=d,r?.(d);}}catch{}})()),await Promise.all(o),{stdout:s,stderr:i}}},v=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+=p)await e.writeBytes(t.subarray(r,r+p));}async writeStdinCoalesced(e,t){let r=[],s=0,i=async()=>{if(s===0)return;let o=r.length===1?r[0]:Buffer.concat(r,s);r=[],s=0,await this.writeStdinChunked(e,o);};for await(let o of t)r.push(o),s+=o.length,s>=p&&await i();await i();}async read(e){let t=await this.sandbox.exec(["cat",e],{timeoutMs:3e5,mode:"binary"}),r=await t.wait();if(r!==0){let i=await t.stderr.readText();throw new Error(`Failed to read file ${e}: ${i||`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 i=e.replace(/'/g,"'\\''"),o=await this.sandbox.exec(["bash","-c",`cat > '${i}'`],{mode:"binary"});await this.writeStdinChunked(o.stdin,new Uint8Array(r)),await o.stdin.getWriter().close(),await o.wait(),await this.chownToUser(e);}async writeBatch(e){let t=tarStream.pack(),r=new Set;for(let o of e){let a=this.toBuffer(o.data),d=o.path.startsWith("/")?o.path.slice(1):o.path;t.entry({name:d},a);let c=o.path.substring(0,o.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 o=Array.from(r),a=new Set(o.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(),i=[];for(let o of s.trim().split(`
3
+ `)){if(!o)continue;let a=o.split(/\s+/);if(a.length<9)continue;let d=a[0],c=a.slice(8).join(" ");c==="."||c===".."||i.push({name:c,path:e.endsWith("/")?`${e}${c}`:`${e}/${c}`,type:d.startsWith("d")?"dir":"file"});}return i}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,"'\\''"),i=await this.sandbox.exec(["bash","-c",`cat > '${s}'`],{mode:"binary"});await this.writeStdinCoalesced(i.stdin,H(t)),await i.stdin.getWriter().close(),await i.wait(),await this.chownToUser(e);}async writeFromPath(e,t){let{createReadStream:r}=await import('fs'),{Readable:s}=await import('stream'),i=s.toWeb(r(t,{highWaterMark:p}));await this.writeStream(e,i);}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 S(e,r),this.files=new v(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 I(this.sandbox.sandboxId,e,this.image)}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.")}},P=class{providerType="modal";name="Modal";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=N(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=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=await this.getApp(),r=k[e]??e,s=await this.resolveImage(r);return {tag:r,image:await s.build(t)}}async prepareImage(e){await this.resolveAndBuildImage(e||this.imageName);}async create(e){let t=e.timeoutMs??this.defaultTimeoutMs;C(t);let r=O(e.idleTimeoutMs),s=$(e.network),i=T(e.resources),o=e.user??A,a=await this.getApp(),{tag:d,image:c}=await this.resolveAndBuildImage(e.image||this.imageName),m=e.envs?Object.fromEntries(Object.entries(e.envs).filter(([,b])=>b!=null)):void 0,L=m&&Object.keys(m).length>0?m:void 0,B={...e.metadata,[R]:d,[_]:new Date().toISOString()},w=await this.client.sandboxes.create(a,c,{cpu:i.cpu,memoryMiB:i.memoryMiB,...i.gpu!==void 0?{gpu:i.gpu}:{},timeoutMs:t,...r,workdir:e.workingDirectory,env:L,tags:B,...s});return e.workingDirectory&&o!=="root"&&await(await w.exec(["chown","-R",`${o}:${o}`,e.workingDirectory],{timeoutMs:3e4})).wait(),this.sandboxUsers.set(w.sandboxId,o),new h(w,d,o)}async connect(e,t){let r=await this.client.sandboxes.fromId(e),s=this.sandboxUsers.get(e)??A;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 U(()=>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 U(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>=M)return {sandboxes:t,complete:!1,pagesFetched:t.length,stoppedAtLimit:!1,error:`sandbox list exceeded ${M} sandboxes`};let s=await r.getTags();t.push(I(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 re(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 P({...n,tokenId:e,tokenSecret:t})}var se=y,ne=k,ie=$,oe=T,ae=N,de=I,ce=U,me=C,le=O;
4
+ exports.EVOLVE_IMAGE_VERSION=l;exports.MODAL_MAX_LIFETIME_MS=E;exports.MODAL_MAX_LIST_SANDBOXES=M;exports.MODAL_STDIN_CHUNK_BYTES=p;exports.ModalCommands=S;exports.ModalFiles=v;exports.ModalIdleTimeoutError=u;exports.ModalNetworkPolicyError=f;exports.ModalProvider=P;exports.ModalResourcesError=g;exports.ModalSandboxLifetimeError=x;exports._testBuildSandboxInfo=de;exports._testCollectSandboxes=ce;exports._testImageMap=ne;exports._testMapIdleTimeout=le;exports._testMapNetworkPolicy=ie;exports._testMapResources=oe;exports._testResolveImageRegistry=ae;exports._testValidateTimeout=me;exports._testWrapCommand=se;exports.createModalProvider=re;
package/dist/index.d.cts CHANGED
@@ -44,10 +44,34 @@ declare const EVOLVE_IMAGE_VERSION = "c-7bcda3467a79";
44
44
  */
45
45
  declare const MODAL_MAX_LIFETIME_MS: number;
46
46
  /**
47
- * Chunk size for stdin uploads. Modal's gRPC transport rejects any single
48
- * TaskExecStdinWrite message larger than 100MiB (RESOURCE_EXHAUSTED at
49
- * 104,857,600 bytes), so file payloads are split into 8MiB writeBytes()
50
- * calls — the same per-chunk pattern writeStream() uses.
47
+ * Chunk size for stdin uploads, and the reason it is THIS big.
48
+ *
49
+ * Every writeBytes() call is one awaited unary TaskExecStdinWrite round trip
50
+ * to Modal, so upload time is set by how many messages a payload becomes, not
51
+ * by how many bytes it is. Measured on live sandboxes with a 180MiB file:
52
+ *
53
+ * 64KiB 311.6s (2880 messages — Node's default read size)
54
+ * 4MiB 36.2s
55
+ * 8MiB 30.1s <- this constant
56
+ * 16MiB 31.4s
57
+ *
58
+ * Below roughly 4MiB the upload is round-trip bound and shrinking the chunk
59
+ * makes it dramatically slower; above it the transport saturates near 6MiB/s
60
+ * and a bigger chunk buys nothing.
61
+ *
62
+ * Modal's 100MiB per-message cap (RESOURCE_EXHAUSTED at 104,857,600 bytes) is
63
+ * the CEILING this must stay under — it is not the reason for the value. Do
64
+ * not "play it safe" by trimming this toward the cap-satisfying end: 64KiB is
65
+ * equally cap-safe and ten times slower, which is exactly the state this
66
+ * constant was raised to fix.
67
+ *
68
+ * STANDING DECISION (chunk size stays 8MiB) and its trigger: a worker running
69
+ * 16 concurrent uploads buffers about 256MiB at this size, which is real
70
+ * pressure in a 1GB worker. The re-profile's saturation row runs exactly that
71
+ * 16-parallel case; the ruling is to move on ITS data, not on precaution. If
72
+ * that row shows memory pressure, dropping to 4MiB is a measured one-line
73
+ * change — 36.2s versus 30.1s, about 83% of the throughput for half the
74
+ * buffer — and everything above still holds.
51
75
  */
52
76
  declare const MODAL_STDIN_CHUNK_BYTES: number;
53
77
  /**
@@ -413,11 +437,18 @@ interface SandboxProvider {
413
437
  create(options: SandboxCreateOptions): Promise<SandboxInstance>;
414
438
  /** Connect to existing sandbox */
415
439
  connect(sandboxId: string, timeoutMs?: number): Promise<SandboxInstance>;
416
- /** List sandboxes (first page only, up to limit) */
417
440
  /** List sandboxes, walking the whole app. `limit` bounds items returned. */
418
441
  list(options?: SandboxListOptions): Promise<SandboxInfo[]>;
419
442
  /** The same enumeration for fleet bookkeeping: never throws, reports completeness. */
420
443
  listAll(options?: SandboxListOptions): Promise<SandboxListPage>;
444
+ /**
445
+ * Build or pull the sandbox image ahead of time so a later create() does not
446
+ * wait for it. Takes what `create({ image })` takes, resolved by the same
447
+ * path, and defaults to the provider's configured image. Optional on the
448
+ * same terms as the SDK contract: declared so every provider offering it
449
+ * offers the same signature.
450
+ */
451
+ prepareImage?(image?: string): Promise<void>;
421
452
  }
422
453
  interface ModalConfig {
423
454
  /** Modal app name. Default: "evolve-sandbox" */
@@ -483,6 +514,33 @@ declare class ModalFiles implements SandboxFiles {
483
514
  * (multi-hundred-MB payloads are common).
484
515
  */
485
516
  private writeStdinChunked;
517
+ /**
518
+ * Stream a byte source into a process's stdin, buffering it into
519
+ * MODAL_STDIN_CHUNK_BYTES writes rather than forwarding the source's own
520
+ * chunk size.
521
+ *
522
+ * WHY this exists rather than the SDK's own file copy. Modal documents
523
+ * "convenience APIs for streaming file copies in both directions"
524
+ * (https://modal.com/docs/guide/sandbox-files), but the JS
525
+ * `filesystem.copyFromLocal()` in modal@0.9.0 streams the local file with
526
+ * a bare `createReadStream(localPath)` — Node's default 64KiB highWaterMark
527
+ * — and awaits one unary TaskExecStdinWrite per chunk. Modal's own Python
528
+ * SDK reads TASK_COMMAND_ROUTER_MAX_BUFFER_SIZE (16MiB) per chunk for the
529
+ * same operation, so the 64KiB default is a JS-side omission, not a
530
+ * transport limit. Since the size is not a parameter of copyFromLocal(),
531
+ * the native call cannot be made to send larger messages.
532
+ *
533
+ * Measured, 180MiB payload: 64KiB 311.6s (2880 messages), native
534
+ * copyFromLocal 308.7s, 4MiB 36.2s, 8MiB 30.1s, 16MiB 31.4s. The upload is
535
+ * round-trip bound until roughly 4MiB and throughput bound after it, so the
536
+ * fix is message SIZE rather than which sink receives the bytes — the
537
+ * exec-stdin sink stays and the chunking changes.
538
+ *
539
+ * Peak memory stays at one chunk plus the source's own, never the whole
540
+ * payload: bundles run to hundreds of MB while workers hold many trials in
541
+ * a small heap.
542
+ */
543
+ private writeStdinCoalesced;
486
544
  read(path: string): Promise<string | Uint8Array>;
487
545
  write(path: string, content: string | Buffer | ArrayBuffer | Uint8Array): Promise<void>;
488
546
  writeBatch(files: Array<{
@@ -500,6 +558,13 @@ declare class ModalFiles implements SandboxFiles {
500
558
  * Upload a local file by PATH, chunk by chunk into the same `cat >` sink
501
559
  * writeStream() uses — so peak memory is one chunk rather than the whole
502
560
  * file, which is what makes a large artifact safe under concurrency.
561
+ *
562
+ * The read is sized to MODAL_STDIN_CHUNK_BYTES instead of Node's 64KiB
563
+ * default because every chunk costs one awaited round trip to Modal: the
564
+ * same 180MiB bundle took 311.6s at 64KiB and 30.1s at 8MiB (measured, one
565
+ * sandbox, same file). writeStdinCoalesced would batch a small-chunk stream
566
+ * anyway; asking the filesystem for whole chunks just avoids assembling
567
+ * them from 128 pieces.
503
568
  */
504
569
  writeFromPath(sandboxPath: string, localPath: string): Promise<void>;
505
570
  uploadUrl(_path: string, _expiresInSeconds?: number): Promise<string>;
@@ -531,6 +596,59 @@ declare class ModalProvider implements SandboxProvider {
531
596
  * (AWS ECR, GCP Artifact Registry) through the configured Modal Secret.
532
597
  */
533
598
  private resolveImage;
599
+ /**
600
+ * Resolve a requested image name to the Modal image identity and eagerly
601
+ * build it. The ONE place that pair happens, because create() and
602
+ * prepareImage() drifting apart is a silent failure: the prewarm would
603
+ * populate one image while trials created against another, and nothing
604
+ * would report the mismatch — only the cold-start cost prewarm was meant
605
+ * to remove would quietly come back.
606
+ *
607
+ * "Eagerly builds an Image on Modal" — the SDK's own description of
608
+ * Image.build(app) (modal@0.9.0, dist/index.d.ts). The call is idempotent:
609
+ * the same reference returns the same cached imageId, so the first caller
610
+ * pays the registry pull and later ones resolve quickly.
611
+ */
612
+ private resolveAndBuildImage;
613
+ /**
614
+ * Build (or pull) an image on Modal ahead of time, so the sandbox that
615
+ * needs it later does not wait for it.
616
+ *
617
+ * Modal's own guidance: "To avoid blocking creation of new Sandboxes on
618
+ * rebuilding an invalidated Image, it's recommended to use Modal's named
619
+ * Images with sandboxes, rather than using inline Image definitions", and
620
+ * "Use `Image.build` to trigger Image builds as part of a deployment flow
621
+ * or at a regular interval (e.g., in a scheduled job or CI pipeline)"
622
+ * (https://modal.com/docs/guide/sandbox). This is that deployment-flow
623
+ * call: run it at publish time, and trial-time create() finds the image
624
+ * already built.
625
+ *
626
+ * `imageName` takes exactly what `create({ image })` takes and is resolved
627
+ * by the identical path, so callers prewarm the image they will actually
628
+ * run on rather than a reconstruction of it. Omitted, it prewarms the
629
+ * provider's configured default — again what create() would have chosen.
630
+ *
631
+ * There is deliberately no sizing parameter: on Modal, image identity is
632
+ * the registry reference alone, and CPU/memory/GPU are create-time sandbox
633
+ * options, so one eager build serves every sizing.
634
+ *
635
+ * OPERATIONAL TRAP, and prewarming at publish time is what makes it likely.
636
+ * From the same guide: "Modal treats external Image tags as immutable once
637
+ * pulled" and "Modal does not detect upstream changes to mutable tags like
638
+ * `:latest`". So prewarming a MUTABLE tag after re-pushing that tag warms
639
+ * the OLD image, and every later create() keeps launching the old image —
640
+ * quietly, because the reference still resolves. Modal's own remedy is to
641
+ * "update the tag in your deploy script (for example, `ubuntu:24.04` →
642
+ * `ubuntu:24.04-20240523`)".
643
+ *
644
+ * The versioned default (evolve-all-<c-hash>) is immune, because a content
645
+ * change moves the tag. The exposed legacy alias "evolve-all" is NOT: it
646
+ * maps to the mutable Docker Hub name, so prewarming it after a re-push
647
+ * warms the stale image. That alias is precisely why EVOLVE_IMAGE_VERSION
648
+ * exists — prewarm the versioned name unless you specifically want the
649
+ * account's already-pulled copy.
650
+ */
651
+ prepareImage(imageName?: string): Promise<void>;
534
652
  create(options: SandboxCreateOptions): Promise<SandboxInstance>;
535
653
  connect(sandboxId: string, _timeoutMs?: number): Promise<SandboxInstance>;
536
654
  /**
package/dist/index.d.ts CHANGED
@@ -44,10 +44,34 @@ declare const EVOLVE_IMAGE_VERSION = "c-7bcda3467a79";
44
44
  */
45
45
  declare const MODAL_MAX_LIFETIME_MS: number;
46
46
  /**
47
- * Chunk size for stdin uploads. Modal's gRPC transport rejects any single
48
- * TaskExecStdinWrite message larger than 100MiB (RESOURCE_EXHAUSTED at
49
- * 104,857,600 bytes), so file payloads are split into 8MiB writeBytes()
50
- * calls — the same per-chunk pattern writeStream() uses.
47
+ * Chunk size for stdin uploads, and the reason it is THIS big.
48
+ *
49
+ * Every writeBytes() call is one awaited unary TaskExecStdinWrite round trip
50
+ * to Modal, so upload time is set by how many messages a payload becomes, not
51
+ * by how many bytes it is. Measured on live sandboxes with a 180MiB file:
52
+ *
53
+ * 64KiB 311.6s (2880 messages — Node's default read size)
54
+ * 4MiB 36.2s
55
+ * 8MiB 30.1s <- this constant
56
+ * 16MiB 31.4s
57
+ *
58
+ * Below roughly 4MiB the upload is round-trip bound and shrinking the chunk
59
+ * makes it dramatically slower; above it the transport saturates near 6MiB/s
60
+ * and a bigger chunk buys nothing.
61
+ *
62
+ * Modal's 100MiB per-message cap (RESOURCE_EXHAUSTED at 104,857,600 bytes) is
63
+ * the CEILING this must stay under — it is not the reason for the value. Do
64
+ * not "play it safe" by trimming this toward the cap-satisfying end: 64KiB is
65
+ * equally cap-safe and ten times slower, which is exactly the state this
66
+ * constant was raised to fix.
67
+ *
68
+ * STANDING DECISION (chunk size stays 8MiB) and its trigger: a worker running
69
+ * 16 concurrent uploads buffers about 256MiB at this size, which is real
70
+ * pressure in a 1GB worker. The re-profile's saturation row runs exactly that
71
+ * 16-parallel case; the ruling is to move on ITS data, not on precaution. If
72
+ * that row shows memory pressure, dropping to 4MiB is a measured one-line
73
+ * change — 36.2s versus 30.1s, about 83% of the throughput for half the
74
+ * buffer — and everything above still holds.
51
75
  */
52
76
  declare const MODAL_STDIN_CHUNK_BYTES: number;
53
77
  /**
@@ -413,11 +437,18 @@ interface SandboxProvider {
413
437
  create(options: SandboxCreateOptions): Promise<SandboxInstance>;
414
438
  /** Connect to existing sandbox */
415
439
  connect(sandboxId: string, timeoutMs?: number): Promise<SandboxInstance>;
416
- /** List sandboxes (first page only, up to limit) */
417
440
  /** List sandboxes, walking the whole app. `limit` bounds items returned. */
418
441
  list(options?: SandboxListOptions): Promise<SandboxInfo[]>;
419
442
  /** The same enumeration for fleet bookkeeping: never throws, reports completeness. */
420
443
  listAll(options?: SandboxListOptions): Promise<SandboxListPage>;
444
+ /**
445
+ * Build or pull the sandbox image ahead of time so a later create() does not
446
+ * wait for it. Takes what `create({ image })` takes, resolved by the same
447
+ * path, and defaults to the provider's configured image. Optional on the
448
+ * same terms as the SDK contract: declared so every provider offering it
449
+ * offers the same signature.
450
+ */
451
+ prepareImage?(image?: string): Promise<void>;
421
452
  }
422
453
  interface ModalConfig {
423
454
  /** Modal app name. Default: "evolve-sandbox" */
@@ -483,6 +514,33 @@ declare class ModalFiles implements SandboxFiles {
483
514
  * (multi-hundred-MB payloads are common).
484
515
  */
485
516
  private writeStdinChunked;
517
+ /**
518
+ * Stream a byte source into a process's stdin, buffering it into
519
+ * MODAL_STDIN_CHUNK_BYTES writes rather than forwarding the source's own
520
+ * chunk size.
521
+ *
522
+ * WHY this exists rather than the SDK's own file copy. Modal documents
523
+ * "convenience APIs for streaming file copies in both directions"
524
+ * (https://modal.com/docs/guide/sandbox-files), but the JS
525
+ * `filesystem.copyFromLocal()` in modal@0.9.0 streams the local file with
526
+ * a bare `createReadStream(localPath)` — Node's default 64KiB highWaterMark
527
+ * — and awaits one unary TaskExecStdinWrite per chunk. Modal's own Python
528
+ * SDK reads TASK_COMMAND_ROUTER_MAX_BUFFER_SIZE (16MiB) per chunk for the
529
+ * same operation, so the 64KiB default is a JS-side omission, not a
530
+ * transport limit. Since the size is not a parameter of copyFromLocal(),
531
+ * the native call cannot be made to send larger messages.
532
+ *
533
+ * Measured, 180MiB payload: 64KiB 311.6s (2880 messages), native
534
+ * copyFromLocal 308.7s, 4MiB 36.2s, 8MiB 30.1s, 16MiB 31.4s. The upload is
535
+ * round-trip bound until roughly 4MiB and throughput bound after it, so the
536
+ * fix is message SIZE rather than which sink receives the bytes — the
537
+ * exec-stdin sink stays and the chunking changes.
538
+ *
539
+ * Peak memory stays at one chunk plus the source's own, never the whole
540
+ * payload: bundles run to hundreds of MB while workers hold many trials in
541
+ * a small heap.
542
+ */
543
+ private writeStdinCoalesced;
486
544
  read(path: string): Promise<string | Uint8Array>;
487
545
  write(path: string, content: string | Buffer | ArrayBuffer | Uint8Array): Promise<void>;
488
546
  writeBatch(files: Array<{
@@ -500,6 +558,13 @@ declare class ModalFiles implements SandboxFiles {
500
558
  * Upload a local file by PATH, chunk by chunk into the same `cat >` sink
501
559
  * writeStream() uses — so peak memory is one chunk rather than the whole
502
560
  * file, which is what makes a large artifact safe under concurrency.
561
+ *
562
+ * The read is sized to MODAL_STDIN_CHUNK_BYTES instead of Node's 64KiB
563
+ * default because every chunk costs one awaited round trip to Modal: the
564
+ * same 180MiB bundle took 311.6s at 64KiB and 30.1s at 8MiB (measured, one
565
+ * sandbox, same file). writeStdinCoalesced would batch a small-chunk stream
566
+ * anyway; asking the filesystem for whole chunks just avoids assembling
567
+ * them from 128 pieces.
503
568
  */
504
569
  writeFromPath(sandboxPath: string, localPath: string): Promise<void>;
505
570
  uploadUrl(_path: string, _expiresInSeconds?: number): Promise<string>;
@@ -531,6 +596,59 @@ declare class ModalProvider implements SandboxProvider {
531
596
  * (AWS ECR, GCP Artifact Registry) through the configured Modal Secret.
532
597
  */
533
598
  private resolveImage;
599
+ /**
600
+ * Resolve a requested image name to the Modal image identity and eagerly
601
+ * build it. The ONE place that pair happens, because create() and
602
+ * prepareImage() drifting apart is a silent failure: the prewarm would
603
+ * populate one image while trials created against another, and nothing
604
+ * would report the mismatch — only the cold-start cost prewarm was meant
605
+ * to remove would quietly come back.
606
+ *
607
+ * "Eagerly builds an Image on Modal" — the SDK's own description of
608
+ * Image.build(app) (modal@0.9.0, dist/index.d.ts). The call is idempotent:
609
+ * the same reference returns the same cached imageId, so the first caller
610
+ * pays the registry pull and later ones resolve quickly.
611
+ */
612
+ private resolveAndBuildImage;
613
+ /**
614
+ * Build (or pull) an image on Modal ahead of time, so the sandbox that
615
+ * needs it later does not wait for it.
616
+ *
617
+ * Modal's own guidance: "To avoid blocking creation of new Sandboxes on
618
+ * rebuilding an invalidated Image, it's recommended to use Modal's named
619
+ * Images with sandboxes, rather than using inline Image definitions", and
620
+ * "Use `Image.build` to trigger Image builds as part of a deployment flow
621
+ * or at a regular interval (e.g., in a scheduled job or CI pipeline)"
622
+ * (https://modal.com/docs/guide/sandbox). This is that deployment-flow
623
+ * call: run it at publish time, and trial-time create() finds the image
624
+ * already built.
625
+ *
626
+ * `imageName` takes exactly what `create({ image })` takes and is resolved
627
+ * by the identical path, so callers prewarm the image they will actually
628
+ * run on rather than a reconstruction of it. Omitted, it prewarms the
629
+ * provider's configured default — again what create() would have chosen.
630
+ *
631
+ * There is deliberately no sizing parameter: on Modal, image identity is
632
+ * the registry reference alone, and CPU/memory/GPU are create-time sandbox
633
+ * options, so one eager build serves every sizing.
634
+ *
635
+ * OPERATIONAL TRAP, and prewarming at publish time is what makes it likely.
636
+ * From the same guide: "Modal treats external Image tags as immutable once
637
+ * pulled" and "Modal does not detect upstream changes to mutable tags like
638
+ * `:latest`". So prewarming a MUTABLE tag after re-pushing that tag warms
639
+ * the OLD image, and every later create() keeps launching the old image —
640
+ * quietly, because the reference still resolves. Modal's own remedy is to
641
+ * "update the tag in your deploy script (for example, `ubuntu:24.04` →
642
+ * `ubuntu:24.04-20240523`)".
643
+ *
644
+ * The versioned default (evolve-all-<c-hash>) is immune, because a content
645
+ * change moves the tag. The exposed legacy alias "evolve-all" is NOT: it
646
+ * maps to the mutable Docker Hub name, so prewarming it after a re-push
647
+ * warms the stale image. That alias is precisely why EVOLVE_IMAGE_VERSION
648
+ * exists — prewarm the versioned name unless you specifically want the
649
+ * account's already-pulled copy.
650
+ */
651
+ prepareImage(imageName?: string): Promise<void>;
534
652
  create(options: SandboxCreateOptions): Promise<SandboxInstance>;
535
653
  connect(sandboxId: string, _timeoutMs?: number): Promise<SandboxInstance>;
536
654
  /**
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import {ModalClient}from'modal';import {pack}from'tar-stream';var p="c-7bcda3467a79";var R={[`evolve-all-${p}`]:`evolvingmachines/evolve-all:${p}`,"evolve-all":"evolvingmachines/evolve-all"},_=1440*60*1e3,M=8*1024*1024,k="user",C="evolve.image",O="evolve.startedAt",y=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(s){if(s>_)throw new y(s)}var u=class extends Error{requestedIdleTimeoutMs;constructor(e,t){super(`Modal idleTimeoutMs of ${e}ms is invalid: ${t}`),this.name="ModalIdleTimeoutError",this.requestedIdleTimeoutMs=e;}};function D(s){if(s===void 0)return {};if(!Number.isFinite(s)||s<=0)throw new u(s,"it must be a positive number of milliseconds");if(s>_)throw new u(s,"it exceeds Modal's 24h lifetime cap, so the sandbox would always die of the lifetime first");return {idleTimeoutMs:s}}function S(s,e,t,r){let n="";r&&Object.keys(r).length>0&&(n=Object.entries(r).filter(([,d])=>d!=null).map(([d,a])=>`export ${d}='${String(a).replace(/'/g,"'\\''")}'`).join("; ")+"; ");let i=t?`cd '${t.replace(/'/g,"'\\''")}' && ${n}${s}`:`${n}${s}`;if(e==="root")return ["bash","-c",i];let o=Buffer.from(i).toString("base64");return ["su",e,"-c",`echo ${o} | base64 -d | bash`]}var g=class extends Error{constructor(e){super(e),this.name="ModalResourcesError";}},V=4,j=4096;function N(s){if(s?.disk!==void 0)throw new g(`Modal's JS SDK has no create-time disk-size parameter, so a ${s.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((s?.gpu===void 0||s.gpu<=0)&&s?.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=s?.gpu!==void 0&&s.gpu>0?`${s.gpuTypes?.length?s.gpuTypes[0]:"any"}:${s.gpu}`:void 0;return {cpu:s?.cpu??V,memoryMiB:s?.memory!==void 0?Math.ceil(s.memory*1024):j,...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 $(s){let e=/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(?:\/(\d{1,3}))?$/.exec(s);return !(!e||[e[1],e[2],e[3],e[4]].some(t=>Number(t)>255)||e[5]!==void 0&&Number(e[5])>32)}function q(s){return /^\d{1,3}(\.\d{1,3}){3}(\/\d+)?$/.test(s)&&!$(s)}function K(s){return s.startsWith("[")?true:(s.match(/:/g)?.length??0)>=2}function z(s){return /^[^:]+:\d+$/.test(s)}function L(s){if(!s||s.outbound==="open"){if(s?.allowedDestinations?.length)throw new Error("network.allowedDestinations is only valid when outbound is blocked");return {}}let e=s.allowedDestinations??[];if(e.length===0)return {blockNetwork:true};let t=[],r=[];for(let n of e)if($(n))t.push(n.includes("/")?n:`${n}/32`);else {if(q(n))throw new f("invalid-ipv4",`"${n}" is not a valid IPv4 address or CIDR (octets must be 0-255, prefix 0-32). Fix the address or list a hostname instead.`,n);if(K(n))t.push(n.includes("/")?n:`${n}/128`);else {if(z(n))throw new f("port-unsupported",`Modal's network allowlist filters hosts and IPs only and cannot match a port; drop the ":<port>" from "${n}" and list just the host or IP.`,n);r.push(n);}}return {outboundCidrAllowlist:t,outboundDomainAllowlist:r}}function U(s){let e=s.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 A(s,e,t){let{[C]:r,[O]:n,...i}=e;return {sandboxId:s,image:r??t??"",metadata:i,startedAt:n??""}}var E=1e4,v=class{constructor(e,t){this.sandbox=e;this.user=t;}async run(e,t){let r=S(e,this.user,t?.cwd,t?.envs),n=await this.sandbox.exec(r,{timeoutMs:t?.timeoutMs}),{stdout:i,stderr:o}=await this.accumulateStreams(n,t?.onStdout,t?.onStderr);return {exitCode:await n.wait(),stdout:i,stderr:o}}async spawn(e,t){let r=S(e,this.user,t?.cwd,t?.envs),n=await this.sandbox.exec(r,{timeoutMs:t?.timeoutMs}),i="",o="",d=this.accumulateStreams(n,t?.onStdout?c=>{t.onStdout(c);}:void 0,t?.onStderr?c=>{t.onStderr(c);}:void 0).then(({stdout:c,stderr:m})=>{i=c,o=m;}).catch(()=>{});return {processId:`modal-${Date.now()}-${Math.random().toString(36).slice(2)}`,wait:async()=>(await d,{exitCode:await n.wait(),stdout:i,stderr:o}),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(n=>{let i=n.trim().split(/\s+/);return {processId:i[0],cmd:i[1]||"",args:i.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 n="",i="",o=[];return o.push((async()=>{try{for await(let d of e.stdout){let a=typeof d=="string"?d:new TextDecoder().decode(d);n+=a,t?.(a);}}catch{}})()),o.push((async()=>{try{for await(let d of e.stderr){let a=typeof d=="string"?d:new TextDecoder().decode(d);i+=a,r?.(a);}}catch{}})()),await Promise.all(o),{stdout:n,stderr:i}}},P=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+=M)await e.writeBytes(t.subarray(r,r+M));}async read(e){let t=await this.sandbox.exec(["cat",e],{timeoutMs:3e5,mode:"binary"}),r=await t.wait();if(r!==0){let i=await t.stderr.readText();throw new Error(`Failed to read file ${e}: ${i||`exit code ${r}`}`)}let n=await t.stdout.readBytes();if(!n.includes(0))try{return new TextDecoder("utf-8",{fatal:!0,ignoreBOM:!0}).decode(n)}catch{}return n}async write(e,t){let r=this.toBuffer(t),n=e.substring(0,e.lastIndexOf("/"));n&&await this.makeDir(n);let i=e.replace(/'/g,"'\\''"),o=await this.sandbox.exec(["bash","-c",`cat > '${i}'`],{mode:"binary"});await this.writeStdinChunked(o.stdin,new Uint8Array(r)),await o.stdin.getWriter().close(),await o.wait(),await this.chownToUser(e);}async writeBatch(e){let t=pack(),r=[],n=new Set;for(let a of e){let c=this.toBuffer(a.data),m=a.path.startsWith("/")?a.path.slice(1):a.path;t.entry({name:m},c);let l=a.path.substring(0,a.path.lastIndexOf("/"));l&&n.add(l);}t.finalize();for await(let a of t)r.push(Buffer.from(a));let i=Buffer.concat(r),o=await this.sandbox.exec(["tar","-xf","-","-C","/"],{mode:"binary"});if(await this.writeStdinChunked(o.stdin,new Uint8Array(i)),await o.stdin.getWriter().close(),await o.wait(),n.size>0){let a=Array.from(n),c=new Set(a.map(m=>m.split("/").slice(0,4).join("/")));for(let m of c)await this.chownToUser(m,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 n=await r.stdout.readText(),i=[];for(let o of n.trim().split(`
3
- `)){if(!o)continue;let d=o.split(/\s+/);if(d.length<9)continue;let a=d[0],c=d.slice(8).join(" ");c==="."||c===".."||i.push({name:c,path:e.endsWith("/")?`${e}${c}`:`${e}/${c}`,type:a.startsWith("d")?"dir":"file"});}return i}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 n=e.replace(/'/g,"'\\''"),i=await this.sandbox.exec(["bash","-c",`cat > '${n}'`],{mode:"binary"}),o=t.getReader();try{for(;;){let{done:a,value:c}=await o.read();if(a)break;await this.writeStdinChunked(i.stdin,c);}}finally{o.releaseLock();}await i.stdin.getWriter().close(),await i.wait(),await this.chownToUser(e);}async writeFromPath(e,t){let{createReadStream:r}=await import('fs'),{Readable:n}=await import('stream'),i=n.toWeb(r(t));await this.writeStream(e,i);}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 v(e,r),this.files=new P(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 A(this.sandbox.sandboxId,e,this.image)}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.")}},I=class{providerType="modal";name="Modal";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-${p}`,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=U(e);if(t==="registry"){let n=this.imageSecretName?await this.client.secrets.fromName(this.imageSecretName):void 0;return this.client.images.fromRegistry(e,n)}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=await this.client.secrets.fromName(this.imageSecretName);return t==="aws-ecr"?this.client.images.fromAwsEcr(e,r):this.client.images.fromGcpArtifactRegistry(e,r)}async create(e){let t=e.timeoutMs??this.defaultTimeoutMs;T(t);let r=D(e.idleTimeoutMs),n=L(e.network),i=N(e.resources),o=e.user??k,d=await this.getApp(),a=e.image||this.imageName,c=R[a]??a,l=await(await this.resolveImage(c)).build(d),w=e.envs?Object.fromEntries(Object.entries(e.envs).filter(([,x])=>x!=null)):void 0,F=w&&Object.keys(w).length>0?w:void 0,W={...e.metadata,[C]:c,[O]:new Date().toISOString()},b=await this.client.sandboxes.create(d,l,{cpu:i.cpu,memoryMiB:i.memoryMiB,...i.gpu!==void 0?{gpu:i.gpu}:{},timeoutMs:t,...r,workdir:e.workingDirectory,env:F,tags:W,...n});return e.workingDirectory&&o!=="root"&&await(await b.exec(["chown","-R",`${o}:${o}`,e.workingDirectory],{timeoutMs:3e4})).wait(),this.sandboxUsers.set(b.sandboxId,o),new h(b,c,o)}async connect(e,t){let r=await this.client.sandboxes.fromId(e),n=this.sandboxUsers.get(e)??k;return new h(r,void 0,n)}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 B(()=>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 B(s,e){let t=[];try{for await(let r of s()){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>=E)return {sandboxes:t,complete:!1,pagesFetched:t.length,stoppedAtLimit:!1,error:`sandbox list exceeded ${E} sandboxes`};let n=await r.getTags();t.push(A(r.sandboxId,n));}}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 se(s={}){let e=s.tokenId??process.env.MODAL_TOKEN_ID,t=s.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 I({...s,tokenId:e,tokenSecret:t})}var ne=S,ie=R,oe=L,ae=N,de=U,ce=A,me=B,le=T,pe=D;
4
- export{p as EVOLVE_IMAGE_VERSION,_ as MODAL_MAX_LIFETIME_MS,E as MODAL_MAX_LIST_SANDBOXES,M as MODAL_STDIN_CHUNK_BYTES,v as ModalCommands,P as ModalFiles,u as ModalIdleTimeoutError,f as ModalNetworkPolicyError,I as ModalProvider,g as ModalResourcesError,y as ModalSandboxLifetimeError,ce as _testBuildSandboxInfo,me as _testCollectSandboxes,ie as _testImageMap,pe as _testMapIdleTimeout,oe as _testMapNetworkPolicy,ae as _testMapResources,de as _testResolveImageRegistry,le as _testValidateTimeout,ne as _testWrapCommand,se as createModalProvider};
1
+ import {ModalClient}from'modal';import {pack}from'tar-stream';var l="c-7bcda3467a79";var k={[`evolve-all-${l}`]:`evolvingmachines/evolve-all:${l}`,"evolve-all":"evolvingmachines/evolve-all"},E=1440*60*1e3,p=8*1024*1024;async function*H(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 A="user",R="evolve.image",_="evolve.startedAt",x=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 C(n){if(n>E)throw new x(n)}var u=class extends Error{requestedIdleTimeoutMs;constructor(e,t){super(`Modal idleTimeoutMs of ${e}ms is invalid: ${t}`),this.name="ModalIdleTimeoutError",this.requestedIdleTimeoutMs=e;}};function O(n){if(n===void 0)return {};if(!Number.isFinite(n)||n<=0)throw new u(n,"it must be a positive number of milliseconds");if(n>E)throw new u(n,"it exceeds Modal's 24h lifetime cap, so the sandbox would always die of the lifetime first");return {idleTimeoutMs:n}}function y(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 i=t?`cd '${t.replace(/'/g,"'\\''")}' && ${s}${n}`:`${s}${n}`;if(e==="root")return ["bash","-c",i];let o=Buffer.from(i).toString("base64");return ["su",e,"-c",`echo ${o} | base64 -d | bash`]}var g=class extends Error{constructor(e){super(e),this.name="ModalResourcesError";}},G=4,V=4096;function T(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??G,memoryMiB:n?.memory!==void 0?Math.ceil(n.memory*1024):V,...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 D(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)&&!D(n)}function q(n){return n.startsWith("[")?true:(n.match(/:/g)?.length??0)>=2}function K(n){return /^[^:]+:\d+$/.test(n)}function $(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(D(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(K(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 N(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 I(n,e,t){let{[R]:r,[_]:s,...i}=e;return {sandboxId:n,image:r??t??"",metadata:i,startedAt:s??""}}var M=1e4,S=class{constructor(e,t){this.sandbox=e;this.user=t;}async run(e,t){let r=y(e,this.user,t?.cwd,t?.envs),s=await this.sandbox.exec(r,{timeoutMs:t?.timeoutMs}),{stdout:i,stderr:o}=await this.accumulateStreams(s,t?.onStdout,t?.onStderr);return {exitCode:await s.wait(),stdout:i,stderr:o}}async spawn(e,t){let r=y(e,this.user,t?.cwd,t?.envs),s=await this.sandbox.exec(r,{timeoutMs:t?.timeoutMs}),i="",o="",a=this.accumulateStreams(s,t?.onStdout?c=>{t.onStdout(c);}:void 0,t?.onStderr?c=>{t.onStderr(c);}:void 0).then(({stdout:c,stderr:m})=>{i=c,o=m;}).catch(()=>{});return {processId:`modal-${Date.now()}-${Math.random().toString(36).slice(2)}`,wait:async()=>(await a,{exitCode:await s.wait(),stdout:i,stderr:o}),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 i=s.trim().split(/\s+/);return {processId:i[0],cmd:i[1]||"",args:i.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="",i="",o=[];return o.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{}})()),o.push((async()=>{try{for await(let a of e.stderr){let d=typeof a=="string"?a:new TextDecoder().decode(a);i+=d,r?.(d);}}catch{}})()),await Promise.all(o),{stdout:s,stderr:i}}},v=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+=p)await e.writeBytes(t.subarray(r,r+p));}async writeStdinCoalesced(e,t){let r=[],s=0,i=async()=>{if(s===0)return;let o=r.length===1?r[0]:Buffer.concat(r,s);r=[],s=0,await this.writeStdinChunked(e,o);};for await(let o of t)r.push(o),s+=o.length,s>=p&&await i();await i();}async read(e){let t=await this.sandbox.exec(["cat",e],{timeoutMs:3e5,mode:"binary"}),r=await t.wait();if(r!==0){let i=await t.stderr.readText();throw new Error(`Failed to read file ${e}: ${i||`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 i=e.replace(/'/g,"'\\''"),o=await this.sandbox.exec(["bash","-c",`cat > '${i}'`],{mode:"binary"});await this.writeStdinChunked(o.stdin,new Uint8Array(r)),await o.stdin.getWriter().close(),await o.wait(),await this.chownToUser(e);}async writeBatch(e){let t=pack(),r=new Set;for(let o of e){let a=this.toBuffer(o.data),d=o.path.startsWith("/")?o.path.slice(1):o.path;t.entry({name:d},a);let c=o.path.substring(0,o.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 o=Array.from(r),a=new Set(o.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(),i=[];for(let o of s.trim().split(`
3
+ `)){if(!o)continue;let a=o.split(/\s+/);if(a.length<9)continue;let d=a[0],c=a.slice(8).join(" ");c==="."||c===".."||i.push({name:c,path:e.endsWith("/")?`${e}${c}`:`${e}/${c}`,type:d.startsWith("d")?"dir":"file"});}return i}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,"'\\''"),i=await this.sandbox.exec(["bash","-c",`cat > '${s}'`],{mode:"binary"});await this.writeStdinCoalesced(i.stdin,H(t)),await i.stdin.getWriter().close(),await i.wait(),await this.chownToUser(e);}async writeFromPath(e,t){let{createReadStream:r}=await import('fs'),{Readable:s}=await import('stream'),i=s.toWeb(r(t,{highWaterMark:p}));await this.writeStream(e,i);}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 S(e,r),this.files=new v(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 I(this.sandbox.sandboxId,e,this.image)}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.")}},P=class{providerType="modal";name="Modal";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=N(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=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=await this.getApp(),r=k[e]??e,s=await this.resolveImage(r);return {tag:r,image:await s.build(t)}}async prepareImage(e){await this.resolveAndBuildImage(e||this.imageName);}async create(e){let t=e.timeoutMs??this.defaultTimeoutMs;C(t);let r=O(e.idleTimeoutMs),s=$(e.network),i=T(e.resources),o=e.user??A,a=await this.getApp(),{tag:d,image:c}=await this.resolveAndBuildImage(e.image||this.imageName),m=e.envs?Object.fromEntries(Object.entries(e.envs).filter(([,b])=>b!=null)):void 0,L=m&&Object.keys(m).length>0?m:void 0,B={...e.metadata,[R]:d,[_]:new Date().toISOString()},w=await this.client.sandboxes.create(a,c,{cpu:i.cpu,memoryMiB:i.memoryMiB,...i.gpu!==void 0?{gpu:i.gpu}:{},timeoutMs:t,...r,workdir:e.workingDirectory,env:L,tags:B,...s});return e.workingDirectory&&o!=="root"&&await(await w.exec(["chown","-R",`${o}:${o}`,e.workingDirectory],{timeoutMs:3e4})).wait(),this.sandboxUsers.set(w.sandboxId,o),new h(w,d,o)}async connect(e,t){let r=await this.client.sandboxes.fromId(e),s=this.sandboxUsers.get(e)??A;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 U(()=>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 U(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>=M)return {sandboxes:t,complete:!1,pagesFetched:t.length,stoppedAtLimit:!1,error:`sandbox list exceeded ${M} sandboxes`};let s=await r.getTags();t.push(I(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 re(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 P({...n,tokenId:e,tokenSecret:t})}var se=y,ne=k,ie=$,oe=T,ae=N,de=I,ce=U,me=C,le=O;
4
+ export{l as EVOLVE_IMAGE_VERSION,E as MODAL_MAX_LIFETIME_MS,M as MODAL_MAX_LIST_SANDBOXES,p as MODAL_STDIN_CHUNK_BYTES,S as ModalCommands,v as ModalFiles,u as ModalIdleTimeoutError,f as ModalNetworkPolicyError,P as ModalProvider,g as ModalResourcesError,x as ModalSandboxLifetimeError,de as _testBuildSandboxInfo,ce as _testCollectSandboxes,ne as _testImageMap,le as _testMapIdleTimeout,ie as _testMapNetworkPolicy,oe as _testMapResources,ae as _testResolveImageRegistry,me as _testValidateTimeout,se as _testWrapCommand,re as createModalProvider};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evolvingmachines/modal",
3
- "version": "0.0.52-project-sable.20260810.ca7ed91",
3
+ "version": "0.0.52-project-sable.20260815.143c2cd",
4
4
  "keywords": [
5
5
  "ai",
6
6
  "agents",