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