@evolvingmachines/modal 0.0.50 → 0.0.52-project-sable.20260726.6e95f36

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 b={"evolve-all":"evolvingmachines/evolve-all"},h=new Set([".xlsx",".xls",".docx",".doc",".pptx",".ppt",".pdf",".zip",".tar",".gz",".7z",".rar",".png",".jpg",".jpeg",".gif",".webp",".ico",".bmp",".mp3",".wav",".ogg",".flac",".aac",".mp4",".avi",".mov",".mkv",".webm",".woff",".woff2",".ttf",".otf",".eot",".exe",".dll",".so",".dylib",".sqlite",".db",".pickle",".pkl",".parquet"]);function y(c){let t=c.substring(c.lastIndexOf(".")).toLowerCase();return h.has(t)}var u=class{constructor(t){this.sandbox=t;}wrapAsUser(t,e,n){let r="";n&&Object.keys(n).length>0&&(r=Object.entries(n).filter(([,o])=>o!=null).map(([o,i])=>`export ${o}='${String(i).replace(/'/g,"'\\''")}'`).join("; ")+"; ");let s=e?`cd '${e.replace(/'/g,"'\\''")}' && ${r}${t}`:`${r}${t}`;return ["su","user","-c",`echo ${Buffer.from(s).toString("base64")} | base64 -d | bash`]}async run(t,e){let n=this.wrapAsUser(t,e?.cwd,e?.envs),r=await this.sandbox.exec(n,{timeoutMs:e?.timeoutMs}),{stdout:s,stderr:a}=await this.accumulateStreams(r,e?.onStdout,e?.onStderr);return {exitCode:await r.wait(),stdout:s,stderr:a}}async spawn(t,e){let n=this.wrapAsUser(t,e?.cwd,e?.envs),r=await this.sandbox.exec(n,{timeoutMs:e?.timeoutMs}),s="",a="",o=this.accumulateStreams(r,e?.onStdout?d=>{e.onStdout(d);}:void 0,e?.onStderr?d=>{e.onStderr(d);}:void 0).then(({stdout:d,stderr:m})=>{s=d,a=m;}).catch(()=>{});return {processId:`modal-${Date.now()}-${Math.random().toString(36).slice(2)}`,wait:async()=>(await o,{exitCode:await r.wait(),stdout:s,stderr:a}),kill:async()=>false}}async list(){let t=await this.sandbox.exec(["ps","-eo","pid,comm,args"],{timeoutMs:1e4});return await t.wait(),(await t.stdout.readText()).trim().split(`
2
- `).slice(1).map(r=>{let s=r.trim().split(/\s+/);return {processId:s[0],cmd:s[1]||"",args:s.slice(2),envs:{}}})}async connect(t,e){throw new Error("Modal does not support connecting to existing processes")}async sendStdin(t,e){throw new Error("Modal does not support sendStdin by process ID")}async kill(t){return await(await this.sandbox.exec(["kill","-9",t],{timeoutMs:1e4})).wait()===0}async accumulateStreams(t,e,n){let r="",s="",a=[];return a.push((async()=>{try{for await(let o of t.stdout){let i=typeof o=="string"?o:new TextDecoder().decode(o);r+=i,e?.(i);}}catch{}})()),a.push((async()=>{try{for await(let o of t.stderr){let i=typeof o=="string"?o:new TextDecoder().decode(o);s+=i,n?.(i);}}catch{}})()),await Promise.all(a),{stdout:r,stderr:s}}},g=class{constructor(t){this.sandbox=t;}async read(t){if(y(t)){let r=await this.sandbox.exec(["cat",t],{timeoutMs:3e5,mode:"binary"}),s=await r.wait();if(s!==0){let a=await r.stderr.readText();throw new Error(`Failed to read file ${t}: ${a||`exit code ${s}`}`)}return await r.stdout.readBytes()}let e=await this.sandbox.exec(["cat",t],{timeoutMs:3e5}),n=await e.wait();if(n!==0){let r=await e.stderr.readText();throw new Error(`Failed to read file ${t}: ${r||`exit code ${n}`}`)}return await e.stdout.readText()}async write(t,e){let n=this.toBuffer(e),r=t.substring(0,t.lastIndexOf("/"));r&&await this.makeDir(r);let s=t.replace(/'/g,"'\\''"),a=await this.sandbox.exec(["bash","-c",`cat > '${s}'`],{mode:"binary"});await a.stdin.writeBytes(new Uint8Array(n)),await a.stdin.getWriter().close(),await a.wait(),await(await this.sandbox.exec(["chown","user:user",t],{timeoutMs:1e4})).wait();}async writeBatch(t){let e=tarStream.pack(),n=[],r=new Set;for(let i of t){let d=this.toBuffer(i.data),m=i.path.startsWith("/")?i.path.slice(1):i.path;e.entry({name:m},d);let p=i.path.substring(0,i.path.lastIndexOf("/"));p&&r.add(p);}e.finalize();for await(let i of e)n.push(Buffer.from(i));let s=Buffer.concat(n),a=await this.sandbox.exec(["tar","-xf","-","-C","/"],{mode:"binary"});if(await a.stdin.writeBytes(new Uint8Array(s)),await a.stdin.getWriter().close(),await a.wait(),r.size>0){let i=Array.from(r),d=new Set(i.map(m=>m.split("/").slice(0,4).join("/")));for(let m of d)await(await this.sandbox.exec(["chown","-R","user:user",m],{timeoutMs:3e4})).wait();}}async makeDir(t){await(await this.sandbox.exec(["mkdir","-p",t],{timeoutMs:1e4})).wait(),await(await this.sandbox.exec(["chown","-R","user:user",t],{timeoutMs:1e4})).wait();}async exists(t){return await(await this.sandbox.exec(["test","-e",t],{timeoutMs:1e4})).wait()===0}async list(t){let e=t.replace(/'/g,"'\\''"),n=await this.sandbox.exec(["bash","-c",`ls -la '${e}' | tail -n +2`],{timeoutMs:3e4});await n.wait();let r=await n.stdout.readText(),s=[];for(let a of r.trim().split(`
3
- `)){if(!a)continue;let o=a.split(/\s+/);if(o.length<9)continue;let i=o[0],d=o.slice(8).join(" ");d==="."||d===".."||s.push({name:d,path:t.endsWith("/")?`${t}${d}`:`${t}/${d}`,type:i.startsWith("d")?"dir":"file"});}return s}async remove(t){await(await this.sandbox.exec(["rm","-rf",t],{timeoutMs:3e4})).wait();}async rename(t,e){await(await this.sandbox.exec(["mv",t,e],{timeoutMs:3e4})).wait();}async readStream(t){return (await this.sandbox.exec(["cat",t],{timeoutMs:3e5,mode:"binary"})).stdout}async writeStream(t,e){let n=t.substring(0,t.lastIndexOf("/"));n&&await this.makeDir(n);let r=t.replace(/'/g,"'\\''"),s=await this.sandbox.exec(["bash","-c",`cat > '${r}'`],{mode:"binary"}),a=e.getReader();try{for(;;){let{done:i,value:d}=await a.read();if(i)break;await s.stdin.writeBytes(d);}}finally{a.releaseLock();}await s.stdin.getWriter().close(),await s.wait();}async uploadUrl(t,e){throw new Error("Modal does not support pre-signed upload URLs")}async downloadUrl(t,e){throw new Error("Modal does not support pre-signed download URLs")}async watchDir(t,e,n){throw new Error("Modal does not support watchDir")}toBuffer(t){if(typeof t=="string")return Buffer.from(t,"utf-8");if(t instanceof Buffer)return t;if(t instanceof ArrayBuffer||t instanceof Uint8Array)return Buffer.from(t);throw new Error(`Unsupported data type: ${typeof t}`)}},l=class{constructor(t,e){this.sandbox=t;this.commands=new u(t),this.files=new g(t),this.image=e,this.startTime=new Date;}commands;files;image;startTime;get sandboxId(){return this.sandbox.sandboxId}async getHost(t){let n=(await this.sandbox.tunnels())[t];if(!n)throw new Error(`No tunnel found for port ${t}`);return n.url}async isRunning(){try{return await(await this.sandbox.exec(["echo","ping"],{timeoutMs:5e3})).wait(),!0}catch{return false}}async getInfo(){return {sandboxId:this.sandbox.sandboxId,image:this.image,metadata:{},startedAt:this.startTime.toISOString()}}async kill(){try{await this.sandbox.terminate();}catch{await new Promise(t=>setTimeout(t,500)),await this.sandbox.terminate();}}async pause(){throw new Error("Modal does not support pause. Use kill() instead.")}},w=class{providerType="modal";name="Modal";client;appName;defaultTimeoutMs;imageName;_app;constructor(t){!t.endpoint&&process.env.MODAL_SERVER_URL?.startsWith("unix:")?process.env.MODAL_SERVER_URL="https://api.modal.com:443":t.endpoint&&(process.env.MODAL_SERVER_URL=t.endpoint),this.client=new modal.ModalClient({tokenId:t.tokenId,tokenSecret:t.tokenSecret}),this.appName=t.appName??"evolve-sandbox",this.defaultTimeoutMs=t.defaultTimeoutMs??36e5,this.imageName=t.imageName??"evolve-all";}async getApp(){return this._app||(this._app=await this.client.apps.fromName(this.appName,{createIfMissing:true})),this._app}async create(t){let e=await this.getApp(),n=t.timeoutMs??this.defaultTimeoutMs,r=t.image||this.imageName,s=b[r]??r,o=await this.client.images.fromRegistry(s).build(e),i=t.envs?Object.fromEntries(Object.entries(t.envs).filter(([,p])=>p!=null)):void 0,d=i&&Object.keys(i).length>0?i:void 0,m=await this.client.sandboxes.create(e,o,{cpu:4,memoryMiB:4096,timeoutMs:n,workdir:t.workingDirectory,env:d});return t.workingDirectory&&await(await m.exec(["chown","-R","user:user",t.workingDirectory],{timeoutMs:3e4})).wait(),new l(m,s)}async connect(t,e){let n=await this.client.sandboxes.fromId(t);return new l(n,"unknown")}async list(t){let e=[],n=t?.limit??100;try{for await(let r of this.client.sandboxes.list())if(e.push({sandboxId:r.sandboxId,image:"unknown",metadata:{},startedAt:new Date().toISOString()}),e.length>=n)break}catch{}return e}};function k(c={}){let t=c.tokenId??process.env.MODAL_TOKEN_ID,e=c.tokenSecret??process.env.MODAL_TOKEN_SECRET;if(!t||!e)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 w({...c,tokenId:t,tokenSecret:e})}
4
- exports.ModalProvider=w;exports.createModalProvider=k;
1
+ 'use strict';var modal=require('modal'),tarStream=require('tar-stream');var B={"evolve-all":"evolvingmachines/evolve-all"},U=1440*60*1e3,P=8*1024*1024,I="user",k="evolve.image",M="evolve.startedAt",$=new Set([".xlsx",".xls",".docx",".doc",".pptx",".ppt",".pdf",".zip",".tar",".gz",".7z",".rar",".png",".jpg",".jpeg",".gif",".webp",".ico",".bmp",".mp3",".wav",".ogg",".flac",".aac",".mp4",".avi",".mov",".mkv",".webm",".woff",".woff2",".ttf",".otf",".eot",".exe",".dll",".so",".dylib",".sqlite",".db",".pickle",".pkl",".parquet"]);function L(n){let t=n.substring(n.lastIndexOf(".")).toLowerCase();return $.has(t)}var w=class extends Error{requestedTimeoutMs;constructor(t){let e=(t/36e5).toFixed(1);super(`Modal sandboxes have a hard 24h lifetime cap; requested timeout was ${e}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=t;}};function A(n){if(n>U)throw new w(n)}function h(n,t,e,s){let r="";s&&Object.keys(s).length>0&&(r=Object.entries(s).filter(([,d])=>d!=null).map(([d,a])=>`export ${d}='${String(a).replace(/'/g,"'\\''")}'`).join("; ")+"; ");let i=e?`cd '${e.replace(/'/g,"'\\''")}' && ${r}${n}`:`${r}${n}`;if(t==="root")return ["bash","-c",i];let o=Buffer.from(i).toString("base64");return ["su",t,"-c",`echo ${o} | base64 -d | bash`]}var x=class extends Error{constructor(t){super(t),this.name="ModalResourcesError";}},F=4,W=4096;function R(n){if(n?.disk!==void 0)throw new x(`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.`);return {cpu:n?.cpu??F,memoryMiB:n?.memory!==void 0?Math.ceil(n.memory*1024):W}}var u=class extends Error{reason;destination;constructor(t,e,s){super(e),this.name="ModalNetworkPolicyError",this.reason=t,this.destination=s;}};function C(n){let t=/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(?:\/(\d{1,3}))?$/.exec(n);return !(!t||[t[1],t[2],t[3],t[4]].some(e=>Number(e)>255)||t[5]!==void 0&&Number(t[5])>32)}function H(n){return /^\d{1,3}(\.\d{1,3}){3}(\/\d+)?$/.test(n)&&!C(n)}function j(n){return n.startsWith("[")?true:(n.match(/:/g)?.length??0)>=2}function z(n){return /^[^:]+:\d+$/.test(n)}function E(n){if(!n||n.outbound==="open"){if(n?.allowedDestinations?.length)throw new Error("network.allowedDestinations is only valid when outbound is blocked");return {}}let t=n.allowedDestinations??[];if(t.length===0)return {blockNetwork:true};let e=[],s=[];for(let r of t)if(C(r))e.push(r.includes("/")?r:`${r}/32`);else {if(H(r))throw new u("invalid-ipv4",`"${r}" is not a valid IPv4 address or CIDR (octets must be 0-255, prefix 0-32). Fix the address or list a hostname instead.`,r);if(j(r))e.push(r.includes("/")?r:`${r}/128`);else {if(z(r))throw new u("port-unsupported",`Modal's network allowlist filters hosts and IPs only and cannot match a port; drop the ":<port>" from "${r}" and list just the host or IP.`,r);s.push(r);}}return {outboundCidrAllowlist:e,outboundDomainAllowlist:s}}function _(n){let t=n.split("/")[0];return /^\d{12}\.dkr\.ecr\.[a-z0-9-]+\.amazonaws\.com$/.test(t)?"aws-ecr":t==="gcr.io"||t.endsWith(".gcr.io")||t.endsWith("-docker.pkg.dev")?"gcp-artifact-registry":"registry"}function v(n,t,e){let{[k]:s,[M]:r,...i}=t;return {sandboxId:n,image:s??e??"",metadata:i,startedAt:r??""}}var b=class{constructor(t,e){this.sandbox=t;this.user=e;}async run(t,e){let s=h(t,this.user,e?.cwd,e?.envs),r=await this.sandbox.exec(s,{timeoutMs:e?.timeoutMs}),{stdout:i,stderr:o}=await this.accumulateStreams(r,e?.onStdout,e?.onStderr);return {exitCode:await r.wait(),stdout:i,stderr:o}}async spawn(t,e){let s=h(t,this.user,e?.cwd,e?.envs),r=await this.sandbox.exec(s,{timeoutMs:e?.timeoutMs}),i="",o="",d=this.accumulateStreams(r,e?.onStdout?c=>{e.onStdout(c);}:void 0,e?.onStderr?c=>{e.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 r.wait(),stdout:i,stderr:o}),kill:async()=>false}}async list(){let t=await this.sandbox.exec(["ps","-eo","pid,comm,args"],{timeoutMs:1e4});return await t.wait(),(await t.stdout.readText()).trim().split(`
2
+ `).slice(1).map(r=>{let i=r.trim().split(/\s+/);return {processId:i[0],cmd:i[1]||"",args:i.slice(2),envs:{}}})}async connect(t,e){throw new Error("Modal does not support connecting to existing processes")}async sendStdin(t,e){throw new Error("Modal does not support sendStdin by process ID")}async kill(t){return await(await this.sandbox.exec(["kill","-9",t],{timeoutMs:1e4})).wait()===0}async accumulateStreams(t,e,s){let r="",i="",o=[];return o.push((async()=>{try{for await(let d of t.stdout){let a=typeof d=="string"?d:new TextDecoder().decode(d);r+=a,e?.(a);}}catch{}})()),o.push((async()=>{try{for await(let d of t.stderr){let a=typeof d=="string"?d:new TextDecoder().decode(d);i+=a,s?.(a);}}catch{}})()),await Promise.all(o),{stdout:r,stderr:i}}},y=class{constructor(t,e){this.sandbox=t;this.user=e;}async chownToUser(t,e=false){if(this.user==="root")return;let s=e?["chown","-R",`${this.user}:${this.user}`,t]:["chown",`${this.user}:${this.user}`,t];await(await this.sandbox.exec(s,{timeoutMs:e?3e4:1e4})).wait();}async writeStdinChunked(t,e){for(let s=0;s<e.length;s+=P)await t.writeBytes(e.subarray(s,s+P));}async read(t){if(L(t)){let r=await this.sandbox.exec(["cat",t],{timeoutMs:3e5,mode:"binary"}),i=await r.wait();if(i!==0){let o=await r.stderr.readText();throw new Error(`Failed to read file ${t}: ${o||`exit code ${i}`}`)}return await r.stdout.readBytes()}let e=await this.sandbox.exec(["cat",t],{timeoutMs:3e5}),s=await e.wait();if(s!==0){let r=await e.stderr.readText();throw new Error(`Failed to read file ${t}: ${r||`exit code ${s}`}`)}return await e.stdout.readText()}async write(t,e){let s=this.toBuffer(e),r=t.substring(0,t.lastIndexOf("/"));r&&await this.makeDir(r);let i=t.replace(/'/g,"'\\''"),o=await this.sandbox.exec(["bash","-c",`cat > '${i}'`],{mode:"binary"});await this.writeStdinChunked(o.stdin,new Uint8Array(s)),await o.stdin.getWriter().close(),await o.wait(),await this.chownToUser(t);}async writeBatch(t){let e=tarStream.pack(),s=[],r=new Set;for(let a of t){let c=this.toBuffer(a.data),m=a.path.startsWith("/")?a.path.slice(1):a.path;e.entry({name:m},c);let l=a.path.substring(0,a.path.lastIndexOf("/"));l&&r.add(l);}e.finalize();for await(let a of e)s.push(Buffer.from(a));let i=Buffer.concat(s),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(),r.size>0){let a=Array.from(r),c=new Set(a.map(m=>m.split("/").slice(0,4).join("/")));for(let m of c)await this.chownToUser(m,true);}}async makeDir(t){await(await this.sandbox.exec(["mkdir","-p",t],{timeoutMs:1e4})).wait(),await this.chownToUser(t,true);}async exists(t){return await(await this.sandbox.exec(["test","-e",t],{timeoutMs:1e4})).wait()===0}async list(t){let e=t.replace(/'/g,"'\\''"),s=await this.sandbox.exec(["bash","-c",`ls -la '${e}' | tail -n +2`],{timeoutMs:3e4});await s.wait();let r=await s.stdout.readText(),i=[];for(let o of r.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:t.endsWith("/")?`${t}${c}`:`${t}/${c}`,type:a.startsWith("d")?"dir":"file"});}return i}async remove(t){await(await this.sandbox.exec(["rm","-rf",t],{timeoutMs:3e4})).wait();}async rename(t,e){await(await this.sandbox.exec(["mv",t,e],{timeoutMs:3e4})).wait();}async readStream(t){return (await this.sandbox.exec(["cat",t],{timeoutMs:3e5,mode:"binary"})).stdout}async writeStream(t,e){let s=t.substring(0,t.lastIndexOf("/"));s&&await this.makeDir(s);let r=t.replace(/'/g,"'\\''"),i=await this.sandbox.exec(["bash","-c",`cat > '${r}'`],{mode:"binary"}),o=e.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(t);}async writeFromPath(t,e){let{createReadStream:s}=await import('fs'),{Readable:r}=await import('stream'),i=r.toWeb(s(e));await this.writeStream(t,i);}async uploadUrl(t,e){throw new Error("Modal does not support pre-signed upload URLs")}async downloadUrl(t,e){throw new Error("Modal does not support pre-signed download URLs")}async watchDir(t,e,s){throw new Error("Modal does not support watchDir")}toBuffer(t){if(typeof t=="string")return Buffer.from(t,"utf-8");if(t instanceof Buffer)return t;if(t instanceof ArrayBuffer||t instanceof Uint8Array)return Buffer.from(t);throw new Error(`Unsupported data type: ${typeof t}`)}},p=class{constructor(t,e,s){this.sandbox=t;this.commands=new b(t,s),this.files=new y(t,s),this.image=e;}commands;files;image;get sandboxId(){return this.sandbox.sandboxId}async getHost(t){let s=(await this.sandbox.tunnels())[t];if(!s)throw new Error(`No tunnel found for port ${t}`);return s.url}async isRunning(){try{return await this.sandbox.poll()===null}catch{return false}}async getInfo(){let t=await this.sandbox.getTags();return v(this.sandbox.sandboxId,t,this.image)}async kill(){try{await this.sandbox.terminate();}catch{await new Promise(t=>setTimeout(t,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.")}},S=class{providerType="modal";name="Modal";client;appName;defaultTimeoutMs;imageName;imageSecretName;_app;sandboxUsers=new Map;constructor(t){!t.endpoint&&process.env.MODAL_SERVER_URL?.startsWith("unix:")?process.env.MODAL_SERVER_URL="https://api.modal.com:443":t.endpoint&&(process.env.MODAL_SERVER_URL=t.endpoint),this.client=new modal.ModalClient({tokenId:t.tokenId,tokenSecret:t.tokenSecret}),this.appName=t.appName??"evolve-sandbox",this.defaultTimeoutMs=t.defaultTimeoutMs??36e5,this.imageName=t.imageName??"evolve-all",this.imageSecretName=t.imageSecretName;}async getApp(){return this._app||(this._app=await this.client.apps.fromName(this.appName,{createIfMissing:true})),this._app}async resolveImage(t){let e=_(t);if(e==="registry"){let r=this.imageSecretName?await this.client.secrets.fromName(this.imageSecretName):void 0;return this.client.images.fromRegistry(t,r)}if(!this.imageSecretName)throw new Error(`Private registry image "${t}" 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 s=await this.client.secrets.fromName(this.imageSecretName);return e==="aws-ecr"?this.client.images.fromAwsEcr(t,s):this.client.images.fromGcpArtifactRegistry(t,s)}async create(t){let e=t.timeoutMs??this.defaultTimeoutMs;A(e);let s=E(t.network),r=R(t.resources),i=t.user??I,o=await this.getApp(),d=t.image||this.imageName,a=B[d]??d,m=await(await this.resolveImage(a)).build(o),l=t.envs?Object.fromEntries(Object.entries(t.envs).filter(([,f])=>f!=null)):void 0,N=l&&Object.keys(l).length>0?l:void 0,O={...t.metadata,[k]:a,[M]:new Date().toISOString()},g=await this.client.sandboxes.create(o,m,{cpu:r.cpu,memoryMiB:r.memoryMiB,timeoutMs:e,workdir:t.workingDirectory,env:N,tags:O,...s});return t.workingDirectory&&i!=="root"&&await(await g.exec(["chown","-R",`${i}:${i}`,t.workingDirectory],{timeoutMs:3e4})).wait(),this.sandboxUsers.set(g.sandboxId,i),new p(g,a,i)}async connect(t,e){let s=await this.client.sandboxes.fromId(t),r=this.sandboxUsers.get(t)??I;return new p(s,void 0,r)}async list(t){if(t?.state&&!t.state.includes("running"))return [];let e=await this.getApp(),s=t?.limit??100,r=[];for await(let i of this.client.sandboxes.list({appId:e.appId,tags:t?.metadata})){let o=await i.getTags();if(r.push(v(i.sandboxId,o)),r.length>=s)break}return r}async listSandboxIds(){let t=new Set;try{let e=await this.getApp();for await(let s of this.client.sandboxes.list({appId:e.appId}))t.add(s.sandboxId);return {ids:t,complete:!0}}catch{return {ids:new Set,complete:false}}}};function J(n={}){let t=n.tokenId??process.env.MODAL_TOKEN_ID,e=n.tokenSecret??process.env.MODAL_TOKEN_SECRET;if(!t||!e)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 S({...n,tokenId:t,tokenSecret:e})}var Q=h,Z=E,tt=R,et=_,rt=v,st=A;
4
+ exports.MODAL_MAX_LIFETIME_MS=U;exports.MODAL_STDIN_CHUNK_BYTES=P;exports.ModalCommands=b;exports.ModalFiles=y;exports.ModalNetworkPolicyError=u;exports.ModalProvider=S;exports.ModalResourcesError=x;exports.ModalSandboxLifetimeError=w;exports._testBuildSandboxInfo=rt;exports._testMapNetworkPolicy=Z;exports._testMapResources=tt;exports._testResolveImageRegistry=et;exports._testValidateTimeout=st;exports._testWrapCommand=Q;exports.createModalProvider=J;
package/dist/index.d.cts CHANGED
@@ -1,7 +1,9 @@
1
+ import { Sandbox } from 'modal';
2
+
1
3
  /**
2
4
  * Modal Sandbox Provider - Clean Architecture
3
5
  *
4
- * @requires modal >= 0.3.0
6
+ * @requires modal >= 0.9.0
5
7
  * @requires Node.js >= 18 (for ReadableStream support)
6
8
  *
7
9
  * Design principles:
@@ -13,9 +15,123 @@
13
15
  *
14
16
  * Modal-specific notes:
15
17
  * - No native file APIs - uses exec() with stdin/stdout
16
- * - pause() not supported - throws error
18
+ * - pause() not supported - throws error (use Evolve checkpoints for persistence)
17
19
  * - Requires app context for sandbox creation
20
+ * - Hard 24h sandbox lifetime cap (ModalSandboxLifetimeError when exceeded)
21
+ * - Everything executes as root inside the sandbox; the `user` option is
22
+ * enforced through an `su <user> -c` wrapper (default user: "user")
23
+ * - Network policy maps to Modal's blockNetwork / outboundDomainAllowlist /
24
+ * outboundCidrAllowlist (domain allowlist admits TLS on port 443 only —
25
+ * plaintext destinations must be listed as IPs/CIDRs)
26
+ * - Modal exposes no metadata or public timestamps on sandboxes; both are
27
+ * stamped into sandbox tags at create time and read back via getTags()
28
+ */
29
+
30
+ /**
31
+ * Modal's hard cap on sandbox lifetime (24 hours).
32
+ * Requests beyond this throw ModalSandboxLifetimeError.
18
33
  */
34
+ declare const MODAL_MAX_LIFETIME_MS: number;
35
+ /**
36
+ * Chunk size for stdin uploads. Modal's gRPC transport rejects any single
37
+ * TaskExecStdinWrite message larger than 100MiB (RESOURCE_EXHAUSTED at
38
+ * 104,857,600 bytes), so file payloads are split into 8MiB writeBytes()
39
+ * calls — the same per-chunk pattern writeStream() uses.
40
+ */
41
+ declare const MODAL_STDIN_CHUNK_BYTES: number;
42
+ /**
43
+ * Typed error for Modal's hard 24h sandbox lifetime cap.
44
+ * Long-running sessions must persist progress with Evolve checkpoints and
45
+ * resume in a fresh sandbox instead of extending the timeout.
46
+ */
47
+ declare class ModalSandboxLifetimeError extends Error {
48
+ readonly requestedTimeoutMs: number;
49
+ constructor(requestedTimeoutMs: number);
50
+ }
51
+ /** Throws ModalSandboxLifetimeError when the timeout exceeds Modal's 24h cap. */
52
+ declare function validateTimeout(timeoutMs: number): void;
53
+ /**
54
+ * Wrap a command with cwd + env handling and (when not root) an
55
+ * `su <user> -c` wrapper.
56
+ *
57
+ * Modal sandboxes run as root by default (ignoring the Dockerfile USER
58
+ * directive), but Claude CLI and other tools refuse certain operations when
59
+ * running as root.
60
+ *
61
+ * Uses `su <user> -c` instead of `sudo -u <user>` because Claude CLI's
62
+ * --dangerously-skip-permissions flag refuses to run when it detects sudo.
63
+ *
64
+ * Uses base64 encoding to avoid shell escaping issues with complex commands
65
+ * that contain quotes, special characters, etc. Env vars are inlined because
66
+ * su does not preserve the environment the way `sudo -E` does.
67
+ */
68
+ declare function wrapCommand(command: string, user: string, cwd?: string, envs?: Record<string, string>): string[];
69
+ /**
70
+ * Typed error for sizing requests Modal's create() cannot enforce.
71
+ * The installed Modal JS SDK sizes cpu (cores) and memoryMiB at create time
72
+ * only — there is no disk-size parameter, so a requested disk size would be
73
+ * silently ignored. Per the provider law (reject what you cannot enforce,
74
+ * never silently ignore) it is refused loudly here.
75
+ */
76
+ declare class ModalResourcesError extends Error {
77
+ constructor(message: string);
78
+ }
79
+ /**
80
+ * Map Evolve's provider-neutral resources (cpu cores, memory GiB, disk GiB)
81
+ * onto Modal's create() params (cpu cores, memoryMiB). Fractional GiB rounds
82
+ * UP so the sandbox never gets less memory than requested. `disk` throws
83
+ * ModalResourcesError — the SDK cannot express it.
84
+ */
85
+ declare function mapResources(resources?: SandboxCreateOptions["resources"]): {
86
+ cpu: number;
87
+ memoryMiB: number;
88
+ };
89
+ /** Modal create() params derived from Evolve's provider-neutral network policy. */
90
+ interface ModalNetworkCreateParams {
91
+ blockNetwork?: boolean;
92
+ outboundCidrAllowlist?: string[];
93
+ outboundDomainAllowlist?: string[];
94
+ }
95
+ /** Why a network destination cannot be mapped onto Modal's allowlist. */
96
+ type ModalNetworkPolicyReason = "port-unsupported" | "invalid-ipv4";
97
+ /**
98
+ * Typed error for destinations Modal's allowlist cannot express.
99
+ *
100
+ * Modal's allowlist filters hosts (domain allowlist) and IPs/CIDRs (CIDR
101
+ * allowlist) only — it has no notion of a port, and an invalid IPv4/CIDR
102
+ * would be silently forwarded to the API. Both are rejected loudly here
103
+ * instead of weakening or mangling the sandbox's egress policy.
104
+ */
105
+ declare class ModalNetworkPolicyError extends Error {
106
+ readonly reason: ModalNetworkPolicyReason;
107
+ /** The offending destination. */
108
+ readonly destination?: string;
109
+ constructor(reason: ModalNetworkPolicyReason, message: string, destination?: string);
110
+ }
111
+ /**
112
+ * Map Evolve's provider-neutral network policy onto Modal create() params.
113
+ *
114
+ * - outbound "open" (or no policy) → no restrictions
115
+ * - outbound "blocked", no allowlist → blockNetwork: true (drops all egress)
116
+ * - outbound "blocked" with allowlist → outboundDomainAllowlist (hostnames,
117
+ * wildcards like "*.example.com") + outboundCidrAllowlist (IPs/CIDRs; bare
118
+ * IPs get /32 or /128 appended). Both lists are always set because Modal
119
+ * treats an unset list as "allow all" — an empty array means "allow none"
120
+ * for that class of destination. Note: Modal's domain allowlist only admits
121
+ * TLS traffic on port 443; plaintext destinations must be listed as CIDRs.
122
+ */
123
+ declare function mapNetworkPolicy(network?: SandboxCreateOptions["network"]): ModalNetworkCreateParams;
124
+ /** Container registry family for an image tag. */
125
+ type ImageRegistry = "aws-ecr" | "gcp-artifact-registry" | "registry";
126
+ /** Detect which Modal image constructor an image tag needs. */
127
+ declare function resolveImageRegistry(tag: string): ImageRegistry;
128
+ /**
129
+ * Build a SandboxInfo from a sandbox's tags. Modal exposes no metadata or
130
+ * public timestamps, so image and startedAt come from the tags stamped at
131
+ * create time; for sandboxes not created by this SDK they are empty strings
132
+ * (never fabricated). endAt is always undefined — Modal does not expose it.
133
+ */
134
+ declare function buildSandboxInfo(sandboxId: string, tags: Record<string, string>, fallbackImage?: string): SandboxInfo;
19
135
  /** Result of a completed sandbox command */
20
136
  interface SandboxCommandResult {
21
137
  exitCode: number;
@@ -93,11 +209,43 @@ interface SandboxCreateOptions {
93
209
  image?: string;
94
210
  envs?: Record<string, string>;
95
211
  metadata?: Record<string, string>;
212
+ /** Sandbox lifetime in ms. Modal hard-caps lifetime at 24h (MODAL_MAX_LIFETIME_MS). */
96
213
  timeoutMs?: number;
97
214
  workingDirectory?: string;
215
+ /**
216
+ * Per-sandbox compute sizing: cpu in cores, memory in GiB — mapped to
217
+ * Modal's create-time cpu / memoryMiB requests (defaults when omitted:
218
+ * 4 cores / 4 GiB). `disk` is REJECTED with ModalResourcesError: the Modal
219
+ * JS SDK exposes no disk-size parameter, so a specific disk size cannot be
220
+ * enforced (containers get Modal's default disk quota).
221
+ */
222
+ resources?: {
223
+ cpu?: number;
224
+ memory?: number;
225
+ disk?: number;
226
+ };
227
+ /**
228
+ * Provider-neutral outbound network policy, enforced by Modal's network
229
+ * stack. "blocked" with no allowedDestinations drops all egress; with
230
+ * allowedDestinations, hostnames go to Modal's domain allowlist (TLS/443
231
+ * only) and IPs/CIDRs to the CIDR allowlist.
232
+ */
233
+ network?: {
234
+ outbound: "open" | "blocked";
235
+ allowedDestinations?: string[];
236
+ };
237
+ /**
238
+ * Run all commands and file operations as this user (default "user"),
239
+ * enforced via an `su <user> -c` wrapper since Modal executes everything as
240
+ * root. Pass "root" to run directly as root with no wrapper.
241
+ */
242
+ user?: string;
243
+ /** Home directory used by the SDK for agent config paths; not consumed by the provider. */
244
+ homeDir?: string;
98
245
  }
99
246
  /** Options for listing sandboxes */
100
247
  interface SandboxListOptions {
248
+ /** Modal has no paused state; filters that exclude "running" match nothing. */
101
249
  state?: ("running" | "paused")[];
102
250
  metadata?: Record<string, string>;
103
251
  limit?: number;
@@ -132,6 +280,8 @@ interface SandboxFiles {
132
280
  readStream(path: string): Promise<ReadableStream<Uint8Array>>;
133
281
  /** Write from stream */
134
282
  writeStream(path: string, stream: ReadableStream<Uint8Array>): Promise<void>;
283
+ /** Upload a local file by path, streamed off disk (never buffered whole) */
284
+ writeFromPath(sandboxPath: string, localPath: string): Promise<void>;
135
285
  /** Get pre-signed upload URL for large files (expiration in seconds) */
136
286
  uploadUrl(path: string, expiresInSeconds?: number): Promise<string>;
137
287
  /** Get pre-signed download URL for large files (expiration in seconds) */
@@ -169,6 +319,8 @@ interface SandboxInstance {
169
319
  interface SandboxProvider {
170
320
  /** Provider type identifier */
171
321
  readonly providerType: string;
322
+ /** Human-readable provider name for logging */
323
+ readonly name?: string;
172
324
  /** Create new sandbox */
173
325
  create(options: SandboxCreateOptions): Promise<SandboxInstance>;
174
326
  /** Connect to existing sandbox */
@@ -189,6 +341,13 @@ interface ModalConfig {
189
341
  endpoint?: string;
190
342
  /** Docker image name (default: 'evolve-all'). Resolved through IMAGE_MAP or used as-is for custom images. */
191
343
  imageName?: string;
344
+ /**
345
+ * Name of a Modal Secret holding registry credentials for private images.
346
+ * Required for AWS ECR (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY,
347
+ * AWS_REGION with read-only ECR IAM) and GCP Artifact Registry; optional
348
+ * for private Docker Hub images. Create one at https://modal.com/secrets
349
+ */
350
+ imageSecretName?: string;
192
351
  }
193
352
  /** Internal resolved config with required credentials */
194
353
  interface ResolvedModalConfig {
@@ -198,6 +357,64 @@ interface ResolvedModalConfig {
198
357
  defaultTimeoutMs?: number;
199
358
  endpoint?: string;
200
359
  imageName?: string;
360
+ imageSecretName?: string;
361
+ }
362
+ declare class ModalCommands implements SandboxCommands {
363
+ private sandbox;
364
+ private user;
365
+ constructor(sandbox: Sandbox, user: string);
366
+ run(command: string, options?: SandboxRunOptions): Promise<SandboxCommandResult>;
367
+ spawn(command: string, options?: SandboxSpawnOptions): Promise<SandboxCommandHandle>;
368
+ list(): Promise<ProcessInfo[]>;
369
+ connect(_processId: string, _options?: SandboxConnectOptions): Promise<SandboxCommandHandle>;
370
+ sendStdin(_processId: string, _data: string): Promise<void>;
371
+ kill(processId: string): Promise<boolean>;
372
+ /**
373
+ * Accumulate stdout/stderr using for-await pattern (more reliable with Modal streams).
374
+ * Based on vibekit's approach which works correctly with Modal SDK.
375
+ */
376
+ private accumulateStreams;
377
+ }
378
+ declare class ModalFiles implements SandboxFiles {
379
+ private sandbox;
380
+ private user;
381
+ constructor(sandbox: Sandbox, user: string);
382
+ /**
383
+ * Chown a path to the sandbox user so agent CLIs (running via the su
384
+ * wrapper) can access files created by root-level exec. No-op when the
385
+ * sandbox user is root.
386
+ */
387
+ private chownToUser;
388
+ /**
389
+ * Write a payload to a process's stdin in MODAL_STDIN_CHUNK_BYTES slices.
390
+ * Each writeBytes() call becomes one gRPC TaskExecStdinWrite message and
391
+ * Modal rejects messages over 100MiB, so large files must be chunked
392
+ * (multi-hundred-MB payloads are common).
393
+ */
394
+ private writeStdinChunked;
395
+ read(path: string): Promise<string | Uint8Array>;
396
+ write(path: string, content: string | Buffer | ArrayBuffer | Uint8Array): Promise<void>;
397
+ writeBatch(files: Array<{
398
+ path: string;
399
+ data: string | Buffer | ArrayBuffer | Uint8Array;
400
+ }>): Promise<void>;
401
+ makeDir(path: string): Promise<void>;
402
+ exists(path: string): Promise<boolean>;
403
+ list(path: string): Promise<FileInfo[]>;
404
+ remove(path: string): Promise<void>;
405
+ rename(oldPath: string, newPath: string): Promise<void>;
406
+ readStream(path: string): Promise<ReadableStream<Uint8Array>>;
407
+ writeStream(path: string, stream: ReadableStream<Uint8Array>): Promise<void>;
408
+ /**
409
+ * Upload a local file by PATH, chunk by chunk into the same `cat >` sink
410
+ * writeStream() uses — so peak memory is one chunk rather than the whole
411
+ * file, which is what makes a large artifact safe under concurrency.
412
+ */
413
+ writeFromPath(sandboxPath: string, localPath: string): Promise<void>;
414
+ uploadUrl(_path: string, _expiresInSeconds?: number): Promise<string>;
415
+ downloadUrl(_path: string, _expiresInSeconds?: number): Promise<string>;
416
+ watchDir(_path: string, _onEvent: (event: FilesystemEvent) => void | Promise<void>, _options?: WatchOptions): Promise<WatchHandle>;
417
+ private toBuffer;
201
418
  }
202
419
  declare class ModalProvider implements SandboxProvider {
203
420
  readonly providerType: "modal";
@@ -206,12 +423,45 @@ declare class ModalProvider implements SandboxProvider {
206
423
  private readonly appName;
207
424
  private readonly defaultTimeoutMs;
208
425
  private readonly imageName;
426
+ private readonly imageSecretName?;
209
427
  private _app;
428
+ /**
429
+ * Sandbox user configured at create time, reapplied on connect() so the su
430
+ * wrapper keeps targeting the same account. In-memory only: a connect()
431
+ * from a fresh process falls back to the default "user" account — callers
432
+ * reconnecting across processes must recreate the provider and sandbox with
433
+ * the same user, or operations on user-owned files fail loudly.
434
+ */
435
+ private readonly sandboxUsers;
210
436
  constructor(config: ResolvedModalConfig);
211
437
  private getApp;
438
+ /**
439
+ * Build a Modal Image for the resolved tag, routing private registries
440
+ * (AWS ECR, GCP Artifact Registry) through the configured Modal Secret.
441
+ */
442
+ private resolveImage;
212
443
  create(options: SandboxCreateOptions): Promise<SandboxInstance>;
213
444
  connect(sandboxId: string, _timeoutMs?: number): Promise<SandboxInstance>;
214
- list(_options?: SandboxListOptions): Promise<SandboxInfo[]>;
445
+ list(options?: SandboxListOptions): Promise<SandboxInfo[]>;
446
+ /**
447
+ * Live sandbox ids for the whole app, in ONE streamed call and O(1) round
448
+ * trips — the fleet-bookkeeping counterpart to `list()`.
449
+ *
450
+ * Exists because `list()` cannot be made cheap without dropping metadata from
451
+ * its contract: it owes callers a populated `SandboxInfo.metadata`, and Modal
452
+ * only serves tags per sandbox. Anything that just needs "which ids are
453
+ * alive" — lifecycle polling, orphan sweeps, reconciliation — uses this and
454
+ * pays one request no matter how large the fleet is.
455
+ *
456
+ * `complete` is the load-bearing field, not a nicety: absence from this list
457
+ * is what callers read as "terminated", so a truncated or errored enumeration
458
+ * MUST NOT be mistaken for an empty fleet. A caller that sees complete=false
459
+ * has to leave rows alone rather than mass-marking live sandboxes dead.
460
+ */
461
+ listSandboxIds(): Promise<{
462
+ ids: Set<string>;
463
+ complete: boolean;
464
+ }>;
215
465
  }
216
466
  /**
217
467
  * Create Modal sandbox provider.
@@ -222,5 +472,11 @@ declare class ModalProvider implements SandboxProvider {
222
472
  * @see https://github.com/evolving-machines-lab/evolve/issues/8
223
473
  */
224
474
  declare function createModalProvider(config?: ModalConfig): SandboxProvider;
475
+ declare const _testWrapCommand: typeof wrapCommand;
476
+ declare const _testMapNetworkPolicy: typeof mapNetworkPolicy;
477
+ declare const _testMapResources: typeof mapResources;
478
+ declare const _testResolveImageRegistry: typeof resolveImageRegistry;
479
+ declare const _testBuildSandboxInfo: typeof buildSandboxInfo;
480
+ declare const _testValidateTimeout: typeof validateTimeout;
225
481
 
226
- export { type FileInfo, type FilesystemEvent, type ModalConfig, ModalProvider, type ProcessInfo, type SandboxCommandHandle, type SandboxCommandResult, type SandboxCommands, type SandboxConnectOptions, type SandboxCreateOptions, type SandboxFiles, type SandboxInfo, type SandboxInstance, type SandboxListOptions, type SandboxProvider, type SandboxRunOptions, type SandboxSpawnOptions, type WatchHandle, type WatchOptions, createModalProvider };
482
+ export { type FileInfo, type FilesystemEvent, MODAL_MAX_LIFETIME_MS, MODAL_STDIN_CHUNK_BYTES, ModalCommands, type ModalConfig, ModalFiles, ModalNetworkPolicyError, type ModalNetworkPolicyReason, ModalProvider, ModalResourcesError, ModalSandboxLifetimeError, type ProcessInfo, type SandboxCommandHandle, type SandboxCommandResult, type SandboxCommands, type SandboxConnectOptions, type SandboxCreateOptions, type SandboxFiles, type SandboxInfo, type SandboxInstance, type SandboxListOptions, type SandboxProvider, type SandboxRunOptions, type SandboxSpawnOptions, type WatchHandle, type WatchOptions, _testBuildSandboxInfo, _testMapNetworkPolicy, _testMapResources, _testResolveImageRegistry, _testValidateTimeout, _testWrapCommand, createModalProvider };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,9 @@
1
+ import { Sandbox } from 'modal';
2
+
1
3
  /**
2
4
  * Modal Sandbox Provider - Clean Architecture
3
5
  *
4
- * @requires modal >= 0.3.0
6
+ * @requires modal >= 0.9.0
5
7
  * @requires Node.js >= 18 (for ReadableStream support)
6
8
  *
7
9
  * Design principles:
@@ -13,9 +15,123 @@
13
15
  *
14
16
  * Modal-specific notes:
15
17
  * - No native file APIs - uses exec() with stdin/stdout
16
- * - pause() not supported - throws error
18
+ * - pause() not supported - throws error (use Evolve checkpoints for persistence)
17
19
  * - Requires app context for sandbox creation
20
+ * - Hard 24h sandbox lifetime cap (ModalSandboxLifetimeError when exceeded)
21
+ * - Everything executes as root inside the sandbox; the `user` option is
22
+ * enforced through an `su <user> -c` wrapper (default user: "user")
23
+ * - Network policy maps to Modal's blockNetwork / outboundDomainAllowlist /
24
+ * outboundCidrAllowlist (domain allowlist admits TLS on port 443 only —
25
+ * plaintext destinations must be listed as IPs/CIDRs)
26
+ * - Modal exposes no metadata or public timestamps on sandboxes; both are
27
+ * stamped into sandbox tags at create time and read back via getTags()
28
+ */
29
+
30
+ /**
31
+ * Modal's hard cap on sandbox lifetime (24 hours).
32
+ * Requests beyond this throw ModalSandboxLifetimeError.
18
33
  */
34
+ declare const MODAL_MAX_LIFETIME_MS: number;
35
+ /**
36
+ * Chunk size for stdin uploads. Modal's gRPC transport rejects any single
37
+ * TaskExecStdinWrite message larger than 100MiB (RESOURCE_EXHAUSTED at
38
+ * 104,857,600 bytes), so file payloads are split into 8MiB writeBytes()
39
+ * calls — the same per-chunk pattern writeStream() uses.
40
+ */
41
+ declare const MODAL_STDIN_CHUNK_BYTES: number;
42
+ /**
43
+ * Typed error for Modal's hard 24h sandbox lifetime cap.
44
+ * Long-running sessions must persist progress with Evolve checkpoints and
45
+ * resume in a fresh sandbox instead of extending the timeout.
46
+ */
47
+ declare class ModalSandboxLifetimeError extends Error {
48
+ readonly requestedTimeoutMs: number;
49
+ constructor(requestedTimeoutMs: number);
50
+ }
51
+ /** Throws ModalSandboxLifetimeError when the timeout exceeds Modal's 24h cap. */
52
+ declare function validateTimeout(timeoutMs: number): void;
53
+ /**
54
+ * Wrap a command with cwd + env handling and (when not root) an
55
+ * `su <user> -c` wrapper.
56
+ *
57
+ * Modal sandboxes run as root by default (ignoring the Dockerfile USER
58
+ * directive), but Claude CLI and other tools refuse certain operations when
59
+ * running as root.
60
+ *
61
+ * Uses `su <user> -c` instead of `sudo -u <user>` because Claude CLI's
62
+ * --dangerously-skip-permissions flag refuses to run when it detects sudo.
63
+ *
64
+ * Uses base64 encoding to avoid shell escaping issues with complex commands
65
+ * that contain quotes, special characters, etc. Env vars are inlined because
66
+ * su does not preserve the environment the way `sudo -E` does.
67
+ */
68
+ declare function wrapCommand(command: string, user: string, cwd?: string, envs?: Record<string, string>): string[];
69
+ /**
70
+ * Typed error for sizing requests Modal's create() cannot enforce.
71
+ * The installed Modal JS SDK sizes cpu (cores) and memoryMiB at create time
72
+ * only — there is no disk-size parameter, so a requested disk size would be
73
+ * silently ignored. Per the provider law (reject what you cannot enforce,
74
+ * never silently ignore) it is refused loudly here.
75
+ */
76
+ declare class ModalResourcesError extends Error {
77
+ constructor(message: string);
78
+ }
79
+ /**
80
+ * Map Evolve's provider-neutral resources (cpu cores, memory GiB, disk GiB)
81
+ * onto Modal's create() params (cpu cores, memoryMiB). Fractional GiB rounds
82
+ * UP so the sandbox never gets less memory than requested. `disk` throws
83
+ * ModalResourcesError — the SDK cannot express it.
84
+ */
85
+ declare function mapResources(resources?: SandboxCreateOptions["resources"]): {
86
+ cpu: number;
87
+ memoryMiB: number;
88
+ };
89
+ /** Modal create() params derived from Evolve's provider-neutral network policy. */
90
+ interface ModalNetworkCreateParams {
91
+ blockNetwork?: boolean;
92
+ outboundCidrAllowlist?: string[];
93
+ outboundDomainAllowlist?: string[];
94
+ }
95
+ /** Why a network destination cannot be mapped onto Modal's allowlist. */
96
+ type ModalNetworkPolicyReason = "port-unsupported" | "invalid-ipv4";
97
+ /**
98
+ * Typed error for destinations Modal's allowlist cannot express.
99
+ *
100
+ * Modal's allowlist filters hosts (domain allowlist) and IPs/CIDRs (CIDR
101
+ * allowlist) only — it has no notion of a port, and an invalid IPv4/CIDR
102
+ * would be silently forwarded to the API. Both are rejected loudly here
103
+ * instead of weakening or mangling the sandbox's egress policy.
104
+ */
105
+ declare class ModalNetworkPolicyError extends Error {
106
+ readonly reason: ModalNetworkPolicyReason;
107
+ /** The offending destination. */
108
+ readonly destination?: string;
109
+ constructor(reason: ModalNetworkPolicyReason, message: string, destination?: string);
110
+ }
111
+ /**
112
+ * Map Evolve's provider-neutral network policy onto Modal create() params.
113
+ *
114
+ * - outbound "open" (or no policy) → no restrictions
115
+ * - outbound "blocked", no allowlist → blockNetwork: true (drops all egress)
116
+ * - outbound "blocked" with allowlist → outboundDomainAllowlist (hostnames,
117
+ * wildcards like "*.example.com") + outboundCidrAllowlist (IPs/CIDRs; bare
118
+ * IPs get /32 or /128 appended). Both lists are always set because Modal
119
+ * treats an unset list as "allow all" — an empty array means "allow none"
120
+ * for that class of destination. Note: Modal's domain allowlist only admits
121
+ * TLS traffic on port 443; plaintext destinations must be listed as CIDRs.
122
+ */
123
+ declare function mapNetworkPolicy(network?: SandboxCreateOptions["network"]): ModalNetworkCreateParams;
124
+ /** Container registry family for an image tag. */
125
+ type ImageRegistry = "aws-ecr" | "gcp-artifact-registry" | "registry";
126
+ /** Detect which Modal image constructor an image tag needs. */
127
+ declare function resolveImageRegistry(tag: string): ImageRegistry;
128
+ /**
129
+ * Build a SandboxInfo from a sandbox's tags. Modal exposes no metadata or
130
+ * public timestamps, so image and startedAt come from the tags stamped at
131
+ * create time; for sandboxes not created by this SDK they are empty strings
132
+ * (never fabricated). endAt is always undefined — Modal does not expose it.
133
+ */
134
+ declare function buildSandboxInfo(sandboxId: string, tags: Record<string, string>, fallbackImage?: string): SandboxInfo;
19
135
  /** Result of a completed sandbox command */
20
136
  interface SandboxCommandResult {
21
137
  exitCode: number;
@@ -93,11 +209,43 @@ interface SandboxCreateOptions {
93
209
  image?: string;
94
210
  envs?: Record<string, string>;
95
211
  metadata?: Record<string, string>;
212
+ /** Sandbox lifetime in ms. Modal hard-caps lifetime at 24h (MODAL_MAX_LIFETIME_MS). */
96
213
  timeoutMs?: number;
97
214
  workingDirectory?: string;
215
+ /**
216
+ * Per-sandbox compute sizing: cpu in cores, memory in GiB — mapped to
217
+ * Modal's create-time cpu / memoryMiB requests (defaults when omitted:
218
+ * 4 cores / 4 GiB). `disk` is REJECTED with ModalResourcesError: the Modal
219
+ * JS SDK exposes no disk-size parameter, so a specific disk size cannot be
220
+ * enforced (containers get Modal's default disk quota).
221
+ */
222
+ resources?: {
223
+ cpu?: number;
224
+ memory?: number;
225
+ disk?: number;
226
+ };
227
+ /**
228
+ * Provider-neutral outbound network policy, enforced by Modal's network
229
+ * stack. "blocked" with no allowedDestinations drops all egress; with
230
+ * allowedDestinations, hostnames go to Modal's domain allowlist (TLS/443
231
+ * only) and IPs/CIDRs to the CIDR allowlist.
232
+ */
233
+ network?: {
234
+ outbound: "open" | "blocked";
235
+ allowedDestinations?: string[];
236
+ };
237
+ /**
238
+ * Run all commands and file operations as this user (default "user"),
239
+ * enforced via an `su <user> -c` wrapper since Modal executes everything as
240
+ * root. Pass "root" to run directly as root with no wrapper.
241
+ */
242
+ user?: string;
243
+ /** Home directory used by the SDK for agent config paths; not consumed by the provider. */
244
+ homeDir?: string;
98
245
  }
99
246
  /** Options for listing sandboxes */
100
247
  interface SandboxListOptions {
248
+ /** Modal has no paused state; filters that exclude "running" match nothing. */
101
249
  state?: ("running" | "paused")[];
102
250
  metadata?: Record<string, string>;
103
251
  limit?: number;
@@ -132,6 +280,8 @@ interface SandboxFiles {
132
280
  readStream(path: string): Promise<ReadableStream<Uint8Array>>;
133
281
  /** Write from stream */
134
282
  writeStream(path: string, stream: ReadableStream<Uint8Array>): Promise<void>;
283
+ /** Upload a local file by path, streamed off disk (never buffered whole) */
284
+ writeFromPath(sandboxPath: string, localPath: string): Promise<void>;
135
285
  /** Get pre-signed upload URL for large files (expiration in seconds) */
136
286
  uploadUrl(path: string, expiresInSeconds?: number): Promise<string>;
137
287
  /** Get pre-signed download URL for large files (expiration in seconds) */
@@ -169,6 +319,8 @@ interface SandboxInstance {
169
319
  interface SandboxProvider {
170
320
  /** Provider type identifier */
171
321
  readonly providerType: string;
322
+ /** Human-readable provider name for logging */
323
+ readonly name?: string;
172
324
  /** Create new sandbox */
173
325
  create(options: SandboxCreateOptions): Promise<SandboxInstance>;
174
326
  /** Connect to existing sandbox */
@@ -189,6 +341,13 @@ interface ModalConfig {
189
341
  endpoint?: string;
190
342
  /** Docker image name (default: 'evolve-all'). Resolved through IMAGE_MAP or used as-is for custom images. */
191
343
  imageName?: string;
344
+ /**
345
+ * Name of a Modal Secret holding registry credentials for private images.
346
+ * Required for AWS ECR (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY,
347
+ * AWS_REGION with read-only ECR IAM) and GCP Artifact Registry; optional
348
+ * for private Docker Hub images. Create one at https://modal.com/secrets
349
+ */
350
+ imageSecretName?: string;
192
351
  }
193
352
  /** Internal resolved config with required credentials */
194
353
  interface ResolvedModalConfig {
@@ -198,6 +357,64 @@ interface ResolvedModalConfig {
198
357
  defaultTimeoutMs?: number;
199
358
  endpoint?: string;
200
359
  imageName?: string;
360
+ imageSecretName?: string;
361
+ }
362
+ declare class ModalCommands implements SandboxCommands {
363
+ private sandbox;
364
+ private user;
365
+ constructor(sandbox: Sandbox, user: string);
366
+ run(command: string, options?: SandboxRunOptions): Promise<SandboxCommandResult>;
367
+ spawn(command: string, options?: SandboxSpawnOptions): Promise<SandboxCommandHandle>;
368
+ list(): Promise<ProcessInfo[]>;
369
+ connect(_processId: string, _options?: SandboxConnectOptions): Promise<SandboxCommandHandle>;
370
+ sendStdin(_processId: string, _data: string): Promise<void>;
371
+ kill(processId: string): Promise<boolean>;
372
+ /**
373
+ * Accumulate stdout/stderr using for-await pattern (more reliable with Modal streams).
374
+ * Based on vibekit's approach which works correctly with Modal SDK.
375
+ */
376
+ private accumulateStreams;
377
+ }
378
+ declare class ModalFiles implements SandboxFiles {
379
+ private sandbox;
380
+ private user;
381
+ constructor(sandbox: Sandbox, user: string);
382
+ /**
383
+ * Chown a path to the sandbox user so agent CLIs (running via the su
384
+ * wrapper) can access files created by root-level exec. No-op when the
385
+ * sandbox user is root.
386
+ */
387
+ private chownToUser;
388
+ /**
389
+ * Write a payload to a process's stdin in MODAL_STDIN_CHUNK_BYTES slices.
390
+ * Each writeBytes() call becomes one gRPC TaskExecStdinWrite message and
391
+ * Modal rejects messages over 100MiB, so large files must be chunked
392
+ * (multi-hundred-MB payloads are common).
393
+ */
394
+ private writeStdinChunked;
395
+ read(path: string): Promise<string | Uint8Array>;
396
+ write(path: string, content: string | Buffer | ArrayBuffer | Uint8Array): Promise<void>;
397
+ writeBatch(files: Array<{
398
+ path: string;
399
+ data: string | Buffer | ArrayBuffer | Uint8Array;
400
+ }>): Promise<void>;
401
+ makeDir(path: string): Promise<void>;
402
+ exists(path: string): Promise<boolean>;
403
+ list(path: string): Promise<FileInfo[]>;
404
+ remove(path: string): Promise<void>;
405
+ rename(oldPath: string, newPath: string): Promise<void>;
406
+ readStream(path: string): Promise<ReadableStream<Uint8Array>>;
407
+ writeStream(path: string, stream: ReadableStream<Uint8Array>): Promise<void>;
408
+ /**
409
+ * Upload a local file by PATH, chunk by chunk into the same `cat >` sink
410
+ * writeStream() uses — so peak memory is one chunk rather than the whole
411
+ * file, which is what makes a large artifact safe under concurrency.
412
+ */
413
+ writeFromPath(sandboxPath: string, localPath: string): Promise<void>;
414
+ uploadUrl(_path: string, _expiresInSeconds?: number): Promise<string>;
415
+ downloadUrl(_path: string, _expiresInSeconds?: number): Promise<string>;
416
+ watchDir(_path: string, _onEvent: (event: FilesystemEvent) => void | Promise<void>, _options?: WatchOptions): Promise<WatchHandle>;
417
+ private toBuffer;
201
418
  }
202
419
  declare class ModalProvider implements SandboxProvider {
203
420
  readonly providerType: "modal";
@@ -206,12 +423,45 @@ declare class ModalProvider implements SandboxProvider {
206
423
  private readonly appName;
207
424
  private readonly defaultTimeoutMs;
208
425
  private readonly imageName;
426
+ private readonly imageSecretName?;
209
427
  private _app;
428
+ /**
429
+ * Sandbox user configured at create time, reapplied on connect() so the su
430
+ * wrapper keeps targeting the same account. In-memory only: a connect()
431
+ * from a fresh process falls back to the default "user" account — callers
432
+ * reconnecting across processes must recreate the provider and sandbox with
433
+ * the same user, or operations on user-owned files fail loudly.
434
+ */
435
+ private readonly sandboxUsers;
210
436
  constructor(config: ResolvedModalConfig);
211
437
  private getApp;
438
+ /**
439
+ * Build a Modal Image for the resolved tag, routing private registries
440
+ * (AWS ECR, GCP Artifact Registry) through the configured Modal Secret.
441
+ */
442
+ private resolveImage;
212
443
  create(options: SandboxCreateOptions): Promise<SandboxInstance>;
213
444
  connect(sandboxId: string, _timeoutMs?: number): Promise<SandboxInstance>;
214
- list(_options?: SandboxListOptions): Promise<SandboxInfo[]>;
445
+ list(options?: SandboxListOptions): Promise<SandboxInfo[]>;
446
+ /**
447
+ * Live sandbox ids for the whole app, in ONE streamed call and O(1) round
448
+ * trips — the fleet-bookkeeping counterpart to `list()`.
449
+ *
450
+ * Exists because `list()` cannot be made cheap without dropping metadata from
451
+ * its contract: it owes callers a populated `SandboxInfo.metadata`, and Modal
452
+ * only serves tags per sandbox. Anything that just needs "which ids are
453
+ * alive" — lifecycle polling, orphan sweeps, reconciliation — uses this and
454
+ * pays one request no matter how large the fleet is.
455
+ *
456
+ * `complete` is the load-bearing field, not a nicety: absence from this list
457
+ * is what callers read as "terminated", so a truncated or errored enumeration
458
+ * MUST NOT be mistaken for an empty fleet. A caller that sees complete=false
459
+ * has to leave rows alone rather than mass-marking live sandboxes dead.
460
+ */
461
+ listSandboxIds(): Promise<{
462
+ ids: Set<string>;
463
+ complete: boolean;
464
+ }>;
215
465
  }
216
466
  /**
217
467
  * Create Modal sandbox provider.
@@ -222,5 +472,11 @@ declare class ModalProvider implements SandboxProvider {
222
472
  * @see https://github.com/evolving-machines-lab/evolve/issues/8
223
473
  */
224
474
  declare function createModalProvider(config?: ModalConfig): SandboxProvider;
475
+ declare const _testWrapCommand: typeof wrapCommand;
476
+ declare const _testMapNetworkPolicy: typeof mapNetworkPolicy;
477
+ declare const _testMapResources: typeof mapResources;
478
+ declare const _testResolveImageRegistry: typeof resolveImageRegistry;
479
+ declare const _testBuildSandboxInfo: typeof buildSandboxInfo;
480
+ declare const _testValidateTimeout: typeof validateTimeout;
225
481
 
226
- export { type FileInfo, type FilesystemEvent, type ModalConfig, ModalProvider, type ProcessInfo, type SandboxCommandHandle, type SandboxCommandResult, type SandboxCommands, type SandboxConnectOptions, type SandboxCreateOptions, type SandboxFiles, type SandboxInfo, type SandboxInstance, type SandboxListOptions, type SandboxProvider, type SandboxRunOptions, type SandboxSpawnOptions, type WatchHandle, type WatchOptions, createModalProvider };
482
+ export { type FileInfo, type FilesystemEvent, MODAL_MAX_LIFETIME_MS, MODAL_STDIN_CHUNK_BYTES, ModalCommands, type ModalConfig, ModalFiles, ModalNetworkPolicyError, type ModalNetworkPolicyReason, ModalProvider, ModalResourcesError, ModalSandboxLifetimeError, type ProcessInfo, type SandboxCommandHandle, type SandboxCommandResult, type SandboxCommands, type SandboxConnectOptions, type SandboxCreateOptions, type SandboxFiles, type SandboxInfo, type SandboxInstance, type SandboxListOptions, type SandboxProvider, type SandboxRunOptions, type SandboxSpawnOptions, type WatchHandle, type WatchOptions, _testBuildSandboxInfo, _testMapNetworkPolicy, _testMapResources, _testResolveImageRegistry, _testValidateTimeout, _testWrapCommand, createModalProvider };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import {ModalClient}from'modal';import {pack}from'tar-stream';var b={"evolve-all":"evolvingmachines/evolve-all"},h=new Set([".xlsx",".xls",".docx",".doc",".pptx",".ppt",".pdf",".zip",".tar",".gz",".7z",".rar",".png",".jpg",".jpeg",".gif",".webp",".ico",".bmp",".mp3",".wav",".ogg",".flac",".aac",".mp4",".avi",".mov",".mkv",".webm",".woff",".woff2",".ttf",".otf",".eot",".exe",".dll",".so",".dylib",".sqlite",".db",".pickle",".pkl",".parquet"]);function y(c){let t=c.substring(c.lastIndexOf(".")).toLowerCase();return h.has(t)}var u=class{constructor(t){this.sandbox=t;}wrapAsUser(t,e,n){let r="";n&&Object.keys(n).length>0&&(r=Object.entries(n).filter(([,o])=>o!=null).map(([o,i])=>`export ${o}='${String(i).replace(/'/g,"'\\''")}'`).join("; ")+"; ");let s=e?`cd '${e.replace(/'/g,"'\\''")}' && ${r}${t}`:`${r}${t}`;return ["su","user","-c",`echo ${Buffer.from(s).toString("base64")} | base64 -d | bash`]}async run(t,e){let n=this.wrapAsUser(t,e?.cwd,e?.envs),r=await this.sandbox.exec(n,{timeoutMs:e?.timeoutMs}),{stdout:s,stderr:a}=await this.accumulateStreams(r,e?.onStdout,e?.onStderr);return {exitCode:await r.wait(),stdout:s,stderr:a}}async spawn(t,e){let n=this.wrapAsUser(t,e?.cwd,e?.envs),r=await this.sandbox.exec(n,{timeoutMs:e?.timeoutMs}),s="",a="",o=this.accumulateStreams(r,e?.onStdout?d=>{e.onStdout(d);}:void 0,e?.onStderr?d=>{e.onStderr(d);}:void 0).then(({stdout:d,stderr:m})=>{s=d,a=m;}).catch(()=>{});return {processId:`modal-${Date.now()}-${Math.random().toString(36).slice(2)}`,wait:async()=>(await o,{exitCode:await r.wait(),stdout:s,stderr:a}),kill:async()=>false}}async list(){let t=await this.sandbox.exec(["ps","-eo","pid,comm,args"],{timeoutMs:1e4});return await t.wait(),(await t.stdout.readText()).trim().split(`
2
- `).slice(1).map(r=>{let s=r.trim().split(/\s+/);return {processId:s[0],cmd:s[1]||"",args:s.slice(2),envs:{}}})}async connect(t,e){throw new Error("Modal does not support connecting to existing processes")}async sendStdin(t,e){throw new Error("Modal does not support sendStdin by process ID")}async kill(t){return await(await this.sandbox.exec(["kill","-9",t],{timeoutMs:1e4})).wait()===0}async accumulateStreams(t,e,n){let r="",s="",a=[];return a.push((async()=>{try{for await(let o of t.stdout){let i=typeof o=="string"?o:new TextDecoder().decode(o);r+=i,e?.(i);}}catch{}})()),a.push((async()=>{try{for await(let o of t.stderr){let i=typeof o=="string"?o:new TextDecoder().decode(o);s+=i,n?.(i);}}catch{}})()),await Promise.all(a),{stdout:r,stderr:s}}},g=class{constructor(t){this.sandbox=t;}async read(t){if(y(t)){let r=await this.sandbox.exec(["cat",t],{timeoutMs:3e5,mode:"binary"}),s=await r.wait();if(s!==0){let a=await r.stderr.readText();throw new Error(`Failed to read file ${t}: ${a||`exit code ${s}`}`)}return await r.stdout.readBytes()}let e=await this.sandbox.exec(["cat",t],{timeoutMs:3e5}),n=await e.wait();if(n!==0){let r=await e.stderr.readText();throw new Error(`Failed to read file ${t}: ${r||`exit code ${n}`}`)}return await e.stdout.readText()}async write(t,e){let n=this.toBuffer(e),r=t.substring(0,t.lastIndexOf("/"));r&&await this.makeDir(r);let s=t.replace(/'/g,"'\\''"),a=await this.sandbox.exec(["bash","-c",`cat > '${s}'`],{mode:"binary"});await a.stdin.writeBytes(new Uint8Array(n)),await a.stdin.getWriter().close(),await a.wait(),await(await this.sandbox.exec(["chown","user:user",t],{timeoutMs:1e4})).wait();}async writeBatch(t){let e=pack(),n=[],r=new Set;for(let i of t){let d=this.toBuffer(i.data),m=i.path.startsWith("/")?i.path.slice(1):i.path;e.entry({name:m},d);let p=i.path.substring(0,i.path.lastIndexOf("/"));p&&r.add(p);}e.finalize();for await(let i of e)n.push(Buffer.from(i));let s=Buffer.concat(n),a=await this.sandbox.exec(["tar","-xf","-","-C","/"],{mode:"binary"});if(await a.stdin.writeBytes(new Uint8Array(s)),await a.stdin.getWriter().close(),await a.wait(),r.size>0){let i=Array.from(r),d=new Set(i.map(m=>m.split("/").slice(0,4).join("/")));for(let m of d)await(await this.sandbox.exec(["chown","-R","user:user",m],{timeoutMs:3e4})).wait();}}async makeDir(t){await(await this.sandbox.exec(["mkdir","-p",t],{timeoutMs:1e4})).wait(),await(await this.sandbox.exec(["chown","-R","user:user",t],{timeoutMs:1e4})).wait();}async exists(t){return await(await this.sandbox.exec(["test","-e",t],{timeoutMs:1e4})).wait()===0}async list(t){let e=t.replace(/'/g,"'\\''"),n=await this.sandbox.exec(["bash","-c",`ls -la '${e}' | tail -n +2`],{timeoutMs:3e4});await n.wait();let r=await n.stdout.readText(),s=[];for(let a of r.trim().split(`
3
- `)){if(!a)continue;let o=a.split(/\s+/);if(o.length<9)continue;let i=o[0],d=o.slice(8).join(" ");d==="."||d===".."||s.push({name:d,path:t.endsWith("/")?`${t}${d}`:`${t}/${d}`,type:i.startsWith("d")?"dir":"file"});}return s}async remove(t){await(await this.sandbox.exec(["rm","-rf",t],{timeoutMs:3e4})).wait();}async rename(t,e){await(await this.sandbox.exec(["mv",t,e],{timeoutMs:3e4})).wait();}async readStream(t){return (await this.sandbox.exec(["cat",t],{timeoutMs:3e5,mode:"binary"})).stdout}async writeStream(t,e){let n=t.substring(0,t.lastIndexOf("/"));n&&await this.makeDir(n);let r=t.replace(/'/g,"'\\''"),s=await this.sandbox.exec(["bash","-c",`cat > '${r}'`],{mode:"binary"}),a=e.getReader();try{for(;;){let{done:i,value:d}=await a.read();if(i)break;await s.stdin.writeBytes(d);}}finally{a.releaseLock();}await s.stdin.getWriter().close(),await s.wait();}async uploadUrl(t,e){throw new Error("Modal does not support pre-signed upload URLs")}async downloadUrl(t,e){throw new Error("Modal does not support pre-signed download URLs")}async watchDir(t,e,n){throw new Error("Modal does not support watchDir")}toBuffer(t){if(typeof t=="string")return Buffer.from(t,"utf-8");if(t instanceof Buffer)return t;if(t instanceof ArrayBuffer||t instanceof Uint8Array)return Buffer.from(t);throw new Error(`Unsupported data type: ${typeof t}`)}},l=class{constructor(t,e){this.sandbox=t;this.commands=new u(t),this.files=new g(t),this.image=e,this.startTime=new Date;}commands;files;image;startTime;get sandboxId(){return this.sandbox.sandboxId}async getHost(t){let n=(await this.sandbox.tunnels())[t];if(!n)throw new Error(`No tunnel found for port ${t}`);return n.url}async isRunning(){try{return await(await this.sandbox.exec(["echo","ping"],{timeoutMs:5e3})).wait(),!0}catch{return false}}async getInfo(){return {sandboxId:this.sandbox.sandboxId,image:this.image,metadata:{},startedAt:this.startTime.toISOString()}}async kill(){try{await this.sandbox.terminate();}catch{await new Promise(t=>setTimeout(t,500)),await this.sandbox.terminate();}}async pause(){throw new Error("Modal does not support pause. Use kill() instead.")}},w=class{providerType="modal";name="Modal";client;appName;defaultTimeoutMs;imageName;_app;constructor(t){!t.endpoint&&process.env.MODAL_SERVER_URL?.startsWith("unix:")?process.env.MODAL_SERVER_URL="https://api.modal.com:443":t.endpoint&&(process.env.MODAL_SERVER_URL=t.endpoint),this.client=new ModalClient({tokenId:t.tokenId,tokenSecret:t.tokenSecret}),this.appName=t.appName??"evolve-sandbox",this.defaultTimeoutMs=t.defaultTimeoutMs??36e5,this.imageName=t.imageName??"evolve-all";}async getApp(){return this._app||(this._app=await this.client.apps.fromName(this.appName,{createIfMissing:true})),this._app}async create(t){let e=await this.getApp(),n=t.timeoutMs??this.defaultTimeoutMs,r=t.image||this.imageName,s=b[r]??r,o=await this.client.images.fromRegistry(s).build(e),i=t.envs?Object.fromEntries(Object.entries(t.envs).filter(([,p])=>p!=null)):void 0,d=i&&Object.keys(i).length>0?i:void 0,m=await this.client.sandboxes.create(e,o,{cpu:4,memoryMiB:4096,timeoutMs:n,workdir:t.workingDirectory,env:d});return t.workingDirectory&&await(await m.exec(["chown","-R","user:user",t.workingDirectory],{timeoutMs:3e4})).wait(),new l(m,s)}async connect(t,e){let n=await this.client.sandboxes.fromId(t);return new l(n,"unknown")}async list(t){let e=[],n=t?.limit??100;try{for await(let r of this.client.sandboxes.list())if(e.push({sandboxId:r.sandboxId,image:"unknown",metadata:{},startedAt:new Date().toISOString()}),e.length>=n)break}catch{}return e}};function k(c={}){let t=c.tokenId??process.env.MODAL_TOKEN_ID,e=c.tokenSecret??process.env.MODAL_TOKEN_SECRET;if(!t||!e)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 w({...c,tokenId:t,tokenSecret:e})}
4
- export{w as ModalProvider,k as createModalProvider};
1
+ import {ModalClient}from'modal';import {pack}from'tar-stream';var B={"evolve-all":"evolvingmachines/evolve-all"},U=1440*60*1e3,P=8*1024*1024,I="user",k="evolve.image",M="evolve.startedAt",$=new Set([".xlsx",".xls",".docx",".doc",".pptx",".ppt",".pdf",".zip",".tar",".gz",".7z",".rar",".png",".jpg",".jpeg",".gif",".webp",".ico",".bmp",".mp3",".wav",".ogg",".flac",".aac",".mp4",".avi",".mov",".mkv",".webm",".woff",".woff2",".ttf",".otf",".eot",".exe",".dll",".so",".dylib",".sqlite",".db",".pickle",".pkl",".parquet"]);function L(n){let t=n.substring(n.lastIndexOf(".")).toLowerCase();return $.has(t)}var w=class extends Error{requestedTimeoutMs;constructor(t){let e=(t/36e5).toFixed(1);super(`Modal sandboxes have a hard 24h lifetime cap; requested timeout was ${e}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=t;}};function A(n){if(n>U)throw new w(n)}function h(n,t,e,s){let r="";s&&Object.keys(s).length>0&&(r=Object.entries(s).filter(([,d])=>d!=null).map(([d,a])=>`export ${d}='${String(a).replace(/'/g,"'\\''")}'`).join("; ")+"; ");let i=e?`cd '${e.replace(/'/g,"'\\''")}' && ${r}${n}`:`${r}${n}`;if(t==="root")return ["bash","-c",i];let o=Buffer.from(i).toString("base64");return ["su",t,"-c",`echo ${o} | base64 -d | bash`]}var x=class extends Error{constructor(t){super(t),this.name="ModalResourcesError";}},F=4,W=4096;function R(n){if(n?.disk!==void 0)throw new x(`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.`);return {cpu:n?.cpu??F,memoryMiB:n?.memory!==void 0?Math.ceil(n.memory*1024):W}}var u=class extends Error{reason;destination;constructor(t,e,s){super(e),this.name="ModalNetworkPolicyError",this.reason=t,this.destination=s;}};function C(n){let t=/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(?:\/(\d{1,3}))?$/.exec(n);return !(!t||[t[1],t[2],t[3],t[4]].some(e=>Number(e)>255)||t[5]!==void 0&&Number(t[5])>32)}function H(n){return /^\d{1,3}(\.\d{1,3}){3}(\/\d+)?$/.test(n)&&!C(n)}function j(n){return n.startsWith("[")?true:(n.match(/:/g)?.length??0)>=2}function z(n){return /^[^:]+:\d+$/.test(n)}function E(n){if(!n||n.outbound==="open"){if(n?.allowedDestinations?.length)throw new Error("network.allowedDestinations is only valid when outbound is blocked");return {}}let t=n.allowedDestinations??[];if(t.length===0)return {blockNetwork:true};let e=[],s=[];for(let r of t)if(C(r))e.push(r.includes("/")?r:`${r}/32`);else {if(H(r))throw new u("invalid-ipv4",`"${r}" is not a valid IPv4 address or CIDR (octets must be 0-255, prefix 0-32). Fix the address or list a hostname instead.`,r);if(j(r))e.push(r.includes("/")?r:`${r}/128`);else {if(z(r))throw new u("port-unsupported",`Modal's network allowlist filters hosts and IPs only and cannot match a port; drop the ":<port>" from "${r}" and list just the host or IP.`,r);s.push(r);}}return {outboundCidrAllowlist:e,outboundDomainAllowlist:s}}function _(n){let t=n.split("/")[0];return /^\d{12}\.dkr\.ecr\.[a-z0-9-]+\.amazonaws\.com$/.test(t)?"aws-ecr":t==="gcr.io"||t.endsWith(".gcr.io")||t.endsWith("-docker.pkg.dev")?"gcp-artifact-registry":"registry"}function v(n,t,e){let{[k]:s,[M]:r,...i}=t;return {sandboxId:n,image:s??e??"",metadata:i,startedAt:r??""}}var b=class{constructor(t,e){this.sandbox=t;this.user=e;}async run(t,e){let s=h(t,this.user,e?.cwd,e?.envs),r=await this.sandbox.exec(s,{timeoutMs:e?.timeoutMs}),{stdout:i,stderr:o}=await this.accumulateStreams(r,e?.onStdout,e?.onStderr);return {exitCode:await r.wait(),stdout:i,stderr:o}}async spawn(t,e){let s=h(t,this.user,e?.cwd,e?.envs),r=await this.sandbox.exec(s,{timeoutMs:e?.timeoutMs}),i="",o="",d=this.accumulateStreams(r,e?.onStdout?c=>{e.onStdout(c);}:void 0,e?.onStderr?c=>{e.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 r.wait(),stdout:i,stderr:o}),kill:async()=>false}}async list(){let t=await this.sandbox.exec(["ps","-eo","pid,comm,args"],{timeoutMs:1e4});return await t.wait(),(await t.stdout.readText()).trim().split(`
2
+ `).slice(1).map(r=>{let i=r.trim().split(/\s+/);return {processId:i[0],cmd:i[1]||"",args:i.slice(2),envs:{}}})}async connect(t,e){throw new Error("Modal does not support connecting to existing processes")}async sendStdin(t,e){throw new Error("Modal does not support sendStdin by process ID")}async kill(t){return await(await this.sandbox.exec(["kill","-9",t],{timeoutMs:1e4})).wait()===0}async accumulateStreams(t,e,s){let r="",i="",o=[];return o.push((async()=>{try{for await(let d of t.stdout){let a=typeof d=="string"?d:new TextDecoder().decode(d);r+=a,e?.(a);}}catch{}})()),o.push((async()=>{try{for await(let d of t.stderr){let a=typeof d=="string"?d:new TextDecoder().decode(d);i+=a,s?.(a);}}catch{}})()),await Promise.all(o),{stdout:r,stderr:i}}},y=class{constructor(t,e){this.sandbox=t;this.user=e;}async chownToUser(t,e=false){if(this.user==="root")return;let s=e?["chown","-R",`${this.user}:${this.user}`,t]:["chown",`${this.user}:${this.user}`,t];await(await this.sandbox.exec(s,{timeoutMs:e?3e4:1e4})).wait();}async writeStdinChunked(t,e){for(let s=0;s<e.length;s+=P)await t.writeBytes(e.subarray(s,s+P));}async read(t){if(L(t)){let r=await this.sandbox.exec(["cat",t],{timeoutMs:3e5,mode:"binary"}),i=await r.wait();if(i!==0){let o=await r.stderr.readText();throw new Error(`Failed to read file ${t}: ${o||`exit code ${i}`}`)}return await r.stdout.readBytes()}let e=await this.sandbox.exec(["cat",t],{timeoutMs:3e5}),s=await e.wait();if(s!==0){let r=await e.stderr.readText();throw new Error(`Failed to read file ${t}: ${r||`exit code ${s}`}`)}return await e.stdout.readText()}async write(t,e){let s=this.toBuffer(e),r=t.substring(0,t.lastIndexOf("/"));r&&await this.makeDir(r);let i=t.replace(/'/g,"'\\''"),o=await this.sandbox.exec(["bash","-c",`cat > '${i}'`],{mode:"binary"});await this.writeStdinChunked(o.stdin,new Uint8Array(s)),await o.stdin.getWriter().close(),await o.wait(),await this.chownToUser(t);}async writeBatch(t){let e=pack(),s=[],r=new Set;for(let a of t){let c=this.toBuffer(a.data),m=a.path.startsWith("/")?a.path.slice(1):a.path;e.entry({name:m},c);let l=a.path.substring(0,a.path.lastIndexOf("/"));l&&r.add(l);}e.finalize();for await(let a of e)s.push(Buffer.from(a));let i=Buffer.concat(s),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(),r.size>0){let a=Array.from(r),c=new Set(a.map(m=>m.split("/").slice(0,4).join("/")));for(let m of c)await this.chownToUser(m,true);}}async makeDir(t){await(await this.sandbox.exec(["mkdir","-p",t],{timeoutMs:1e4})).wait(),await this.chownToUser(t,true);}async exists(t){return await(await this.sandbox.exec(["test","-e",t],{timeoutMs:1e4})).wait()===0}async list(t){let e=t.replace(/'/g,"'\\''"),s=await this.sandbox.exec(["bash","-c",`ls -la '${e}' | tail -n +2`],{timeoutMs:3e4});await s.wait();let r=await s.stdout.readText(),i=[];for(let o of r.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:t.endsWith("/")?`${t}${c}`:`${t}/${c}`,type:a.startsWith("d")?"dir":"file"});}return i}async remove(t){await(await this.sandbox.exec(["rm","-rf",t],{timeoutMs:3e4})).wait();}async rename(t,e){await(await this.sandbox.exec(["mv",t,e],{timeoutMs:3e4})).wait();}async readStream(t){return (await this.sandbox.exec(["cat",t],{timeoutMs:3e5,mode:"binary"})).stdout}async writeStream(t,e){let s=t.substring(0,t.lastIndexOf("/"));s&&await this.makeDir(s);let r=t.replace(/'/g,"'\\''"),i=await this.sandbox.exec(["bash","-c",`cat > '${r}'`],{mode:"binary"}),o=e.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(t);}async writeFromPath(t,e){let{createReadStream:s}=await import('fs'),{Readable:r}=await import('stream'),i=r.toWeb(s(e));await this.writeStream(t,i);}async uploadUrl(t,e){throw new Error("Modal does not support pre-signed upload URLs")}async downloadUrl(t,e){throw new Error("Modal does not support pre-signed download URLs")}async watchDir(t,e,s){throw new Error("Modal does not support watchDir")}toBuffer(t){if(typeof t=="string")return Buffer.from(t,"utf-8");if(t instanceof Buffer)return t;if(t instanceof ArrayBuffer||t instanceof Uint8Array)return Buffer.from(t);throw new Error(`Unsupported data type: ${typeof t}`)}},p=class{constructor(t,e,s){this.sandbox=t;this.commands=new b(t,s),this.files=new y(t,s),this.image=e;}commands;files;image;get sandboxId(){return this.sandbox.sandboxId}async getHost(t){let s=(await this.sandbox.tunnels())[t];if(!s)throw new Error(`No tunnel found for port ${t}`);return s.url}async isRunning(){try{return await this.sandbox.poll()===null}catch{return false}}async getInfo(){let t=await this.sandbox.getTags();return v(this.sandbox.sandboxId,t,this.image)}async kill(){try{await this.sandbox.terminate();}catch{await new Promise(t=>setTimeout(t,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.")}},S=class{providerType="modal";name="Modal";client;appName;defaultTimeoutMs;imageName;imageSecretName;_app;sandboxUsers=new Map;constructor(t){!t.endpoint&&process.env.MODAL_SERVER_URL?.startsWith("unix:")?process.env.MODAL_SERVER_URL="https://api.modal.com:443":t.endpoint&&(process.env.MODAL_SERVER_URL=t.endpoint),this.client=new ModalClient({tokenId:t.tokenId,tokenSecret:t.tokenSecret}),this.appName=t.appName??"evolve-sandbox",this.defaultTimeoutMs=t.defaultTimeoutMs??36e5,this.imageName=t.imageName??"evolve-all",this.imageSecretName=t.imageSecretName;}async getApp(){return this._app||(this._app=await this.client.apps.fromName(this.appName,{createIfMissing:true})),this._app}async resolveImage(t){let e=_(t);if(e==="registry"){let r=this.imageSecretName?await this.client.secrets.fromName(this.imageSecretName):void 0;return this.client.images.fromRegistry(t,r)}if(!this.imageSecretName)throw new Error(`Private registry image "${t}" 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 s=await this.client.secrets.fromName(this.imageSecretName);return e==="aws-ecr"?this.client.images.fromAwsEcr(t,s):this.client.images.fromGcpArtifactRegistry(t,s)}async create(t){let e=t.timeoutMs??this.defaultTimeoutMs;A(e);let s=E(t.network),r=R(t.resources),i=t.user??I,o=await this.getApp(),d=t.image||this.imageName,a=B[d]??d,m=await(await this.resolveImage(a)).build(o),l=t.envs?Object.fromEntries(Object.entries(t.envs).filter(([,f])=>f!=null)):void 0,N=l&&Object.keys(l).length>0?l:void 0,O={...t.metadata,[k]:a,[M]:new Date().toISOString()},g=await this.client.sandboxes.create(o,m,{cpu:r.cpu,memoryMiB:r.memoryMiB,timeoutMs:e,workdir:t.workingDirectory,env:N,tags:O,...s});return t.workingDirectory&&i!=="root"&&await(await g.exec(["chown","-R",`${i}:${i}`,t.workingDirectory],{timeoutMs:3e4})).wait(),this.sandboxUsers.set(g.sandboxId,i),new p(g,a,i)}async connect(t,e){let s=await this.client.sandboxes.fromId(t),r=this.sandboxUsers.get(t)??I;return new p(s,void 0,r)}async list(t){if(t?.state&&!t.state.includes("running"))return [];let e=await this.getApp(),s=t?.limit??100,r=[];for await(let i of this.client.sandboxes.list({appId:e.appId,tags:t?.metadata})){let o=await i.getTags();if(r.push(v(i.sandboxId,o)),r.length>=s)break}return r}async listSandboxIds(){let t=new Set;try{let e=await this.getApp();for await(let s of this.client.sandboxes.list({appId:e.appId}))t.add(s.sandboxId);return {ids:t,complete:!0}}catch{return {ids:new Set,complete:false}}}};function J(n={}){let t=n.tokenId??process.env.MODAL_TOKEN_ID,e=n.tokenSecret??process.env.MODAL_TOKEN_SECRET;if(!t||!e)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 S({...n,tokenId:t,tokenSecret:e})}var Q=h,Z=E,tt=R,et=_,rt=v,st=A;
4
+ export{U as MODAL_MAX_LIFETIME_MS,P as MODAL_STDIN_CHUNK_BYTES,b as ModalCommands,y as ModalFiles,u as ModalNetworkPolicyError,S as ModalProvider,x as ModalResourcesError,w as ModalSandboxLifetimeError,rt as _testBuildSandboxInfo,Z as _testMapNetworkPolicy,tt as _testMapResources,et as _testResolveImageRegistry,st as _testValidateTimeout,Q as _testWrapCommand,J as createModalProvider};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evolvingmachines/modal",
3
- "version": "0.0.50",
3
+ "version": "0.0.52-project-sable.20260726.6e95f36",
4
4
  "keywords": [
5
5
  "ai",
6
6
  "agents",
@@ -33,10 +33,11 @@
33
33
  "scripts": {
34
34
  "build": "tsup --minify",
35
35
  "dev": "tsup --watch",
36
- "type-check": "tsc --noEmit"
36
+ "type-check": "tsc --noEmit",
37
+ "test:unit": "tsx tests/unit/modal-provider.test.ts"
37
38
  },
38
39
  "dependencies": {
39
- "modal": "^0.6.0",
40
+ "modal": "^0.9.0",
40
41
  "tar-stream": "^3.1.7"
41
42
  },
42
43
  "devDependencies": {