@evolvingmachines/modal 0.0.52-project-sable.20260815.143c2cd → 0.0.52-project-sable.20260824.3dfecab

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