@evolvingmachines/modal 0.0.52-project-sable.20260726.6e95f36 → 0.0.52-project-sable.20260729.96caaab
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 +125 -1
- package/dist/index.d.ts +125 -1
- package/dist/index.js +4 -4
- package/package.json +2 -2
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 d=o.split(/\s+/);if(d.length<9)continue;let a=d[0],c=d.slice(8).join(" ");c==="."||c===".."||i.push({name:c,path:t.endsWith("/")?`${t}${c}`:`${t}/${c}`,type:a.startsWith("d")?"dir":"file"});}return i}async remove(t){await(await this.sandbox.exec(["rm","-rf",t],{timeoutMs:3e4})).wait();}async rename(t,e){await(await this.sandbox.exec(["mv",t,e],{timeoutMs:3e4})).wait();}async readStream(t){return (await this.sandbox.exec(["cat",t],{timeoutMs:3e5,mode:"binary"})).stdout}async writeStream(t,e){let
|
|
4
|
-
exports.MODAL_MAX_LIFETIME_MS=
|
|
1
|
+
'use strict';var modal=require('modal'),tarStream=require('tar-stream');var H={"evolve-all":"evolvingmachines/evolve-all"},E=1440*60*1e3,A=8*1024*1024,k="user",R="evolve.image",_="evolve.startedAt",j=new Set([".xlsx",".xls",".docx",".doc",".pptx",".ppt",".pdf",".zip",".tar",".gz",".7z",".rar",".png",".jpg",".jpeg",".gif",".webp",".ico",".bmp",".mp3",".wav",".ogg",".flac",".aac",".mp4",".avi",".mov",".mkv",".webm",".woff",".woff2",".ttf",".otf",".eot",".exe",".dll",".so",".dylib",".sqlite",".db",".pickle",".pkl",".parquet"]);function q(n){let t=n.substring(n.lastIndexOf(".")).toLowerCase();return j.has(t)}var x=class extends Error{requestedTimeoutMs;constructor(t){let e=(t/36e5).toFixed(1);super(`Modal sandboxes have a hard 24h lifetime cap; requested timeout was ${e}h. For sessions longer than 24h, persist progress with Evolve checkpoints and resume in a fresh sandbox instead of extending the timeout.`),this.name="ModalSandboxLifetimeError",this.requestedTimeoutMs=t;}};function C(n){if(n>E)throw new x(n)}var p=class extends Error{requestedIdleTimeoutMs;constructor(t,e){super(`Modal idleTimeoutMs of ${t}ms is invalid: ${e}`),this.name="ModalIdleTimeoutError",this.requestedIdleTimeoutMs=t;}};function O(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>E)throw new p(n,"it exceeds Modal's 24h lifetime cap, so the sandbox would always die of the lifetime first");return {idleTimeoutMs:n}}function b(n,t,e,r){let s="";r&&Object.keys(r).length>0&&(s=Object.entries(r).filter(([,d])=>d!=null).map(([d,a])=>`export ${d}='${String(a).replace(/'/g,"'\\''")}'`).join("; ")+"; ");let i=e?`cd '${e.replace(/'/g,"'\\''")}' && ${s}${n}`:`${s}${n}`;if(t==="root")return ["bash","-c",i];let o=Buffer.from(i).toString("base64");return ["su",t,"-c",`echo ${o} | base64 -d | bash`]}var S=class extends Error{constructor(t){super(t),this.name="ModalResourcesError";}},z=4,G=4096;function N(n){if(n?.disk!==void 0)throw new S(`Modal's JS SDK has no create-time disk-size parameter, so a ${n.disk} GiB disk request cannot be enforced. Drop \`resources.disk\` (containers get Modal's default disk quota) or run on a provider that sizes disk.`);return {cpu:n?.cpu??z,memoryMiB:n?.memory!==void 0?Math.ceil(n.memory*1024):G}}var u=class extends Error{reason;destination;constructor(t,e,r){super(e),this.name="ModalNetworkPolicyError",this.reason=t,this.destination=r;}};function D(n){let t=/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(?:\/(\d{1,3}))?$/.exec(n);return !(!t||[t[1],t[2],t[3],t[4]].some(e=>Number(e)>255)||t[5]!==void 0&&Number(t[5])>32)}function K(n){return /^\d{1,3}(\.\d{1,3}){3}(\/\d+)?$/.test(n)&&!D(n)}function X(n){return n.startsWith("[")?true:(n.match(/:/g)?.length??0)>=2}function Y(n){return /^[^:]+:\d+$/.test(n)}function T(n){if(!n||n.outbound==="open"){if(n?.allowedDestinations?.length)throw new Error("network.allowedDestinations is only valid when outbound is blocked");return {}}let t=n.allowedDestinations??[];if(t.length===0)return {blockNetwork:true};let e=[],r=[];for(let s of t)if(D(s))e.push(s.includes("/")?s:`${s}/32`);else {if(K(s))throw new u("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(X(s))e.push(s.includes("/")?s:`${s}/128`);else {if(Y(s))throw new u("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:e,outboundDomainAllowlist:r}}function $(n){let t=n.split("/")[0];return /^\d{12}\.dkr\.ecr\.[a-z0-9-]+\.amazonaws\.com$/.test(t)?"aws-ecr":t==="gcr.io"||t.endsWith(".gcr.io")||t.endsWith("-docker.pkg.dev")?"gcp-artifact-registry":"registry"}function I(n,t,e){let{[R]:r,[_]:s,...i}=t;return {sandboxId:n,image:r??e??"",metadata:i,startedAt:s??""}}var M=1e4,y=class{constructor(t,e){this.sandbox=t;this.user=e;}async run(t,e){let r=b(t,this.user,e?.cwd,e?.envs),s=await this.sandbox.exec(r,{timeoutMs:e?.timeoutMs}),{stdout:i,stderr:o}=await this.accumulateStreams(s,e?.onStdout,e?.onStderr);return {exitCode:await s.wait(),stdout:i,stderr:o}}async spawn(t,e){let r=b(t,this.user,e?.cwd,e?.envs),s=await this.sandbox.exec(r,{timeoutMs:e?.timeoutMs}),i="",o="",d=this.accumulateStreams(s,e?.onStdout?c=>{e.onStdout(c);}:void 0,e?.onStderr?c=>{e.onStderr(c);}:void 0).then(({stdout:c,stderr:m})=>{i=c,o=m;}).catch(()=>{});return {processId:`modal-${Date.now()}-${Math.random().toString(36).slice(2)}`,wait:async()=>(await d,{exitCode:await s.wait(),stdout:i,stderr:o}),kill:async()=>false}}async list(){let t=await this.sandbox.exec(["ps","-eo","pid,comm,args"],{timeoutMs:1e4});return await t.wait(),(await t.stdout.readText()).trim().split(`
|
|
2
|
+
`).slice(1).map(s=>{let i=s.trim().split(/\s+/);return {processId:i[0],cmd:i[1]||"",args:i.slice(2),envs:{}}})}async connect(t,e){throw new Error("Modal does not support connecting to existing processes")}async sendStdin(t,e){throw new Error("Modal does not support sendStdin by process ID")}async kill(t){return await(await this.sandbox.exec(["kill","-9",t],{timeoutMs:1e4})).wait()===0}async accumulateStreams(t,e,r){let s="",i="",o=[];return o.push((async()=>{try{for await(let d of t.stdout){let a=typeof d=="string"?d:new TextDecoder().decode(d);s+=a,e?.(a);}}catch{}})()),o.push((async()=>{try{for await(let d of t.stderr){let a=typeof d=="string"?d:new TextDecoder().decode(d);i+=a,r?.(a);}}catch{}})()),await Promise.all(o),{stdout:s,stderr:i}}},v=class{constructor(t,e){this.sandbox=t;this.user=e;}async chownToUser(t,e=false){if(this.user==="root")return;let r=e?["chown","-R",`${this.user}:${this.user}`,t]:["chown",`${this.user}:${this.user}`,t];await(await this.sandbox.exec(r,{timeoutMs:e?3e4:1e4})).wait();}async writeStdinChunked(t,e){for(let r=0;r<e.length;r+=A)await t.writeBytes(e.subarray(r,r+A));}async read(t){if(q(t)){let s=await this.sandbox.exec(["cat",t],{timeoutMs:3e5,mode:"binary"}),i=await s.wait();if(i!==0){let o=await s.stderr.readText();throw new Error(`Failed to read file ${t}: ${o||`exit code ${i}`}`)}return await s.stdout.readBytes()}let e=await this.sandbox.exec(["cat",t],{timeoutMs:3e5}),r=await e.wait();if(r!==0){let s=await e.stderr.readText();throw new Error(`Failed to read file ${t}: ${s||`exit code ${r}`}`)}return await e.stdout.readText()}async write(t,e){let r=this.toBuffer(e),s=t.substring(0,t.lastIndexOf("/"));s&&await this.makeDir(s);let i=t.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(t);}async writeBatch(t){let e=tarStream.pack(),r=[],s=new Set;for(let a of t){let c=this.toBuffer(a.data),m=a.path.startsWith("/")?a.path.slice(1):a.path;e.entry({name:m},c);let l=a.path.substring(0,a.path.lastIndexOf("/"));l&&s.add(l);}e.finalize();for await(let a of e)r.push(Buffer.from(a));let i=Buffer.concat(r),o=await this.sandbox.exec(["tar","-xf","-","-C","/"],{mode:"binary"});if(await this.writeStdinChunked(o.stdin,new Uint8Array(i)),await o.stdin.getWriter().close(),await o.wait(),s.size>0){let a=Array.from(s),c=new Set(a.map(m=>m.split("/").slice(0,4).join("/")));for(let m of c)await this.chownToUser(m,true);}}async makeDir(t){await(await this.sandbox.exec(["mkdir","-p",t],{timeoutMs:1e4})).wait(),await this.chownToUser(t,true);}async exists(t){return await(await this.sandbox.exec(["test","-e",t],{timeoutMs:1e4})).wait()===0}async list(t){let e=t.replace(/'/g,"'\\''"),r=await this.sandbox.exec(["bash","-c",`ls -la '${e}' | 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 d=o.split(/\s+/);if(d.length<9)continue;let a=d[0],c=d.slice(8).join(" ");c==="."||c===".."||i.push({name:c,path:t.endsWith("/")?`${t}${c}`:`${t}/${c}`,type:a.startsWith("d")?"dir":"file"});}return i}async remove(t){await(await this.sandbox.exec(["rm","-rf",t],{timeoutMs:3e4})).wait();}async rename(t,e){await(await this.sandbox.exec(["mv",t,e],{timeoutMs:3e4})).wait();}async readStream(t){return (await this.sandbox.exec(["cat",t],{timeoutMs:3e5,mode:"binary"})).stdout}async writeStream(t,e){let r=t.substring(0,t.lastIndexOf("/"));r&&await this.makeDir(r);let s=t.replace(/'/g,"'\\''"),i=await this.sandbox.exec(["bash","-c",`cat > '${s}'`],{mode:"binary"}),o=e.getReader();try{for(;;){let{done:a,value:c}=await o.read();if(a)break;await this.writeStdinChunked(i.stdin,c);}}finally{o.releaseLock();}await i.stdin.getWriter().close(),await i.wait(),await this.chownToUser(t);}async writeFromPath(t,e){let{createReadStream:r}=await import('fs'),{Readable:s}=await import('stream'),i=s.toWeb(r(e));await this.writeStream(t,i);}async uploadUrl(t,e){throw new Error("Modal does not support pre-signed upload URLs")}async downloadUrl(t,e){throw new Error("Modal does not support pre-signed download URLs")}async watchDir(t,e,r){throw new Error("Modal does not support watchDir")}toBuffer(t){if(typeof t=="string")return Buffer.from(t,"utf-8");if(t instanceof Buffer)return t;if(t instanceof ArrayBuffer||t instanceof Uint8Array)return Buffer.from(t);throw new Error(`Unsupported data type: ${typeof t}`)}},g=class{constructor(t,e,r){this.sandbox=t;this.commands=new y(t,r),this.files=new v(t,r),this.image=e;}commands;files;image;get sandboxId(){return this.sandbox.sandboxId}async getHost(t){let r=(await this.sandbox.tunnels())[t];if(!r)throw new Error(`No tunnel found for port ${t}`);return r.url}async isRunning(){try{return await this.sandbox.poll()===null}catch{return false}}async getInfo(){let t=await this.sandbox.getTags();return I(this.sandbox.sandboxId,t,this.image)}async kill(){try{await this.sandbox.terminate();}catch{await new Promise(t=>setTimeout(t,500)),await this.sandbox.terminate();}}async pause(){throw new Error("Modal does not support pause/resume. Persist progress with Evolve checkpoints and resume in a fresh sandbox, or use kill() to terminate.")}},P=class{providerType="modal";name="Modal";client;appName;defaultTimeoutMs;imageName;imageSecretName;_app;sandboxUsers=new Map;constructor(t){!t.endpoint&&process.env.MODAL_SERVER_URL?.startsWith("unix:")?process.env.MODAL_SERVER_URL="https://api.modal.com:443":t.endpoint&&(process.env.MODAL_SERVER_URL=t.endpoint),this.client=new modal.ModalClient({tokenId:t.tokenId,tokenSecret:t.tokenSecret}),this.appName=t.appName??"evolve-sandbox",this.defaultTimeoutMs=t.defaultTimeoutMs??36e5,this.imageName=t.imageName??"evolve-all",this.imageSecretName=t.imageSecretName;}async getApp(){return this._app||(this._app=await this.client.apps.fromName(this.appName,{createIfMissing:true})),this._app}async resolveImage(t){let e=$(t);if(e==="registry"){let s=this.imageSecretName?await this.client.secrets.fromName(this.imageSecretName):void 0;return this.client.images.fromRegistry(t,s)}if(!this.imageSecretName)throw new Error(`Private registry image "${t}" requires config.imageSecretName \u2014 the name of a Modal Secret holding registry credentials (AWS ECR: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION with read-only ECR IAM). Create one at https://modal.com/secrets`);let r=await this.client.secrets.fromName(this.imageSecretName);return e==="aws-ecr"?this.client.images.fromAwsEcr(t,r):this.client.images.fromGcpArtifactRegistry(t,r)}async create(t){let e=t.timeoutMs??this.defaultTimeoutMs;C(e);let r=O(t.idleTimeoutMs),s=T(t.network),i=N(t.resources),o=t.user??k,d=await this.getApp(),a=t.image||this.imageName,c=H[a]??a,l=await(await this.resolveImage(c)).build(d),f=t.envs?Object.fromEntries(Object.entries(t.envs).filter(([,h])=>h!=null)):void 0,B=f&&Object.keys(f).length>0?f:void 0,U={...t.metadata,[R]:c,[_]:new Date().toISOString()},w=await this.client.sandboxes.create(d,l,{cpu:i.cpu,memoryMiB:i.memoryMiB,timeoutMs:e,...r,workdir:t.workingDirectory,env:B,tags:U,...s});return t.workingDirectory&&o!=="root"&&await(await w.exec(["chown","-R",`${o}:${o}`,t.workingDirectory],{timeoutMs:3e4})).wait(),this.sandboxUsers.set(w.sandboxId,o),new g(w,c,o)}async connect(t,e){let r=await this.client.sandboxes.fromId(t),s=this.sandboxUsers.get(t)??k;return new g(r,void 0,s)}async list(t){let e=await this.walk(t);if(e.error&&!e.stoppedAtLimit)throw new Error(e.error);return e.sandboxes}async listAll(t){let{stoppedAtLimit:e,...r}=await this.walk(t);return r}async walk(t){if(t?.state&&!t.state.includes("running"))return {sandboxes:[],complete:true,pagesFetched:0,stoppedAtLimit:false};let e;try{e=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 L(()=>this.client.sandboxes.list({appId:e.appId,tags:t?.metadata}),t?.limit)}async listSandboxIds(){let t=new Set;try{let e=await this.getApp();for await(let r of this.client.sandboxes.list({appId:e.appId}))t.add(r.sandboxId);return {ids:t,complete:!0}}catch{return {ids:new Set,complete:false}}}};async function L(n,t){let e=[];try{for await(let r of n()){if(t!==void 0&&e.length>=t)return {sandboxes:e,complete:!1,pagesFetched:e.length,stoppedAtLimit:!0,error:`stopped at the requested limit of ${t} with more sandboxes available`};if(e.length>=M)return {sandboxes:e,complete:!1,pagesFetched:e.length,stoppedAtLimit:!1,error:`sandbox list exceeded ${M} sandboxes`};let s=await r.getTags();e.push(I(r.sandboxId,s));}}catch(r){return {sandboxes:e,complete:false,pagesFetched:e.length,stoppedAtLimit:false,error:`sandbox list failed: ${r instanceof Error?r.message:String(r)}`}}return {sandboxes:e,complete:true,pagesFetched:e.length,stoppedAtLimit:false}}function rt(n={}){let t=n.tokenId??process.env.MODAL_TOKEN_ID,e=n.tokenSecret??process.env.MODAL_TOKEN_SECRET;if(!t||!e)throw new Error("Modal credentials required. Set MODAL_TOKEN_ID and MODAL_TOKEN_SECRET environment variables, or pass tokenId/tokenSecret in config. Get your token at https://modal.com/settings/tokens");return new P({...n,tokenId:t,tokenSecret:e})}var st=b,nt=T,it=N,ot=$,at=I,dt=L,ct=C,mt=O;
|
|
4
|
+
exports.MODAL_MAX_LIFETIME_MS=E;exports.MODAL_MAX_LIST_SANDBOXES=M;exports.MODAL_STDIN_CHUNK_BYTES=A;exports.ModalCommands=y;exports.ModalFiles=v;exports.ModalIdleTimeoutError=p;exports.ModalNetworkPolicyError=u;exports.ModalProvider=P;exports.ModalResourcesError=S;exports.ModalSandboxLifetimeError=x;exports._testBuildSandboxInfo=at;exports._testCollectSandboxes=dt;exports._testMapIdleTimeout=mt;exports._testMapNetworkPolicy=nt;exports._testMapResources=it;exports._testResolveImageRegistry=ot;exports._testValidateTimeout=ct;exports._testWrapCommand=st;exports.createModalProvider=rt;
|
package/dist/index.d.cts
CHANGED
|
@@ -50,6 +50,32 @@ declare class ModalSandboxLifetimeError extends Error {
|
|
|
50
50
|
}
|
|
51
51
|
/** Throws ModalSandboxLifetimeError when the timeout exceeds Modal's 24h cap. */
|
|
52
52
|
declare function validateTimeout(timeoutMs: number): void;
|
|
53
|
+
/**
|
|
54
|
+
* Typed error for an idle timeout Modal could not act on. Both bounds are
|
|
55
|
+
* refusals rather than clamps: silently raising a zero, or lowering a value past
|
|
56
|
+
* the lifetime cap, would hand back a box that dies on a schedule the caller
|
|
57
|
+
* never asked for.
|
|
58
|
+
*/
|
|
59
|
+
declare class ModalIdleTimeoutError extends Error {
|
|
60
|
+
readonly requestedIdleTimeoutMs: number;
|
|
61
|
+
constructor(requestedIdleTimeoutMs: number, reason: string);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Evolve's idle bound -> Modal's create params, same shape as mapNetworkPolicy
|
|
65
|
+
* and mapResources: provider-neutral option in, Modal fragment out.
|
|
66
|
+
*
|
|
67
|
+
* ABSENT MEANS ABSENT. Modal's own default is no idle timer at all, so an unset
|
|
68
|
+
* option must spread to nothing — inventing a default here would start killing
|
|
69
|
+
* boxes that today live out their lifetime, for every caller who never asked.
|
|
70
|
+
*
|
|
71
|
+
* An idle timeout has to be a positive span, and one above the 24h lifetime cap
|
|
72
|
+
* can never fire because the sandbox is already gone. Both are caller mistakes,
|
|
73
|
+
* and both throw rather than clamp: silently raising a zero or lowering an
|
|
74
|
+
* over-cap value hands back a box that dies on a schedule nobody chose.
|
|
75
|
+
*/
|
|
76
|
+
declare function mapIdleTimeout(idleTimeoutMs?: number): {
|
|
77
|
+
idleTimeoutMs?: number;
|
|
78
|
+
};
|
|
53
79
|
/**
|
|
54
80
|
* Wrap a command with cwd + env handling and (when not root) an
|
|
55
81
|
* `su <user> -c` wrapper.
|
|
@@ -211,6 +237,19 @@ interface SandboxCreateOptions {
|
|
|
211
237
|
metadata?: Record<string, string>;
|
|
212
238
|
/** Sandbox lifetime in ms. Modal hard-caps lifetime at 24h (MODAL_MAX_LIFETIME_MS). */
|
|
213
239
|
timeoutMs?: number;
|
|
240
|
+
/**
|
|
241
|
+
* Terminate the sandbox after this long with nothing running in it — the
|
|
242
|
+
* bound that reclaims a box whose client died, without waiting out the whole
|
|
243
|
+
* lifetime. Modal is the only provider with both clocks.
|
|
244
|
+
*
|
|
245
|
+
* OMITTED BY DEFAULT: Modal runs no idle timer unless asked. Modal counts a
|
|
246
|
+
* sandbox active while an exec is running, while its stdin is being written,
|
|
247
|
+
* or while a tunnel connection is open — file operations are not named in
|
|
248
|
+
* that list, and this adapter is safe only because it routes reads and writes
|
|
249
|
+
* through exec (`cat` / `cat >`). A future native filesystem path would need
|
|
250
|
+
* this re-checked.
|
|
251
|
+
*/
|
|
252
|
+
idleTimeoutMs?: number;
|
|
214
253
|
workingDirectory?: string;
|
|
215
254
|
/**
|
|
216
255
|
* Per-sandbox compute sizing: cpu in cores, memory in GiB — mapped to
|
|
@@ -250,6 +289,29 @@ interface SandboxListOptions {
|
|
|
250
289
|
metadata?: Record<string, string>;
|
|
251
290
|
limit?: number;
|
|
252
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* A COMPLETE (or admittedly incomplete) enumeration of the app's fleet.
|
|
294
|
+
*
|
|
295
|
+
* `complete` is the load-bearing field. Callers that need a whole fleet —
|
|
296
|
+
* orphan sweeps, lifecycle reconciliation — read a sandbox's ABSENCE from the
|
|
297
|
+
* list as evidence it is gone, so a truncated walk and a small fleet must never
|
|
298
|
+
* be the same answer. That includes a walk stopped by the caller's own `limit`:
|
|
299
|
+
* "you asked for ten and there are more" is a truncated fleet.
|
|
300
|
+
*/
|
|
301
|
+
interface SandboxListPage {
|
|
302
|
+
sandboxes: SandboxInfo[];
|
|
303
|
+
complete: boolean;
|
|
304
|
+
pagesFetched: number;
|
|
305
|
+
error?: string;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Sandboxes a single enumeration will walk before it gives up and reports
|
|
309
|
+
* itself incomplete. Modal's list is an async generator with no page size we
|
|
310
|
+
* control, so the ceiling is counted in SANDBOXES rather than pages — same
|
|
311
|
+
* purpose as the other providers' page caps: never return a short list that
|
|
312
|
+
* reads like a whole one.
|
|
313
|
+
*/
|
|
314
|
+
declare const MODAL_MAX_LIST_SANDBOXES = 10000;
|
|
253
315
|
/** Command execution capabilities */
|
|
254
316
|
interface SandboxCommands {
|
|
255
317
|
/** Run command and wait for completion */
|
|
@@ -326,7 +388,10 @@ interface SandboxProvider {
|
|
|
326
388
|
/** Connect to existing sandbox */
|
|
327
389
|
connect(sandboxId: string, timeoutMs?: number): Promise<SandboxInstance>;
|
|
328
390
|
/** List sandboxes (first page only, up to limit) */
|
|
391
|
+
/** List sandboxes, walking the whole app. `limit` bounds items returned. */
|
|
329
392
|
list(options?: SandboxListOptions): Promise<SandboxInfo[]>;
|
|
393
|
+
/** The same enumeration for fleet bookkeeping: never throws, reports completeness. */
|
|
394
|
+
listAll(options?: SandboxListOptions): Promise<SandboxListPage>;
|
|
330
395
|
}
|
|
331
396
|
interface ModalConfig {
|
|
332
397
|
/** Modal app name. Default: "evolve-sandbox" */
|
|
@@ -442,7 +507,36 @@ declare class ModalProvider implements SandboxProvider {
|
|
|
442
507
|
private resolveImage;
|
|
443
508
|
create(options: SandboxCreateOptions): Promise<SandboxInstance>;
|
|
444
509
|
connect(sandboxId: string, _timeoutMs?: number): Promise<SandboxInstance>;
|
|
510
|
+
/**
|
|
511
|
+
* List sandboxes, walking the whole app.
|
|
512
|
+
*
|
|
513
|
+
* This used to stop at a hardcoded default of 100 regardless of fleet size,
|
|
514
|
+
* which silently truncated any app with more — and said nothing about it
|
|
515
|
+
* while the shared SandboxProvider interface promised exhaustive listing.
|
|
516
|
+
* `limit` still bounds the sandboxes RETURNED, so a caller wanting one cheap
|
|
517
|
+
* sample asks for one; without it the answer is the whole app.
|
|
518
|
+
*
|
|
519
|
+
* The O(N)-round-trips warning below is unchanged and is the reason `limit`
|
|
520
|
+
* matters here more than on the other providers.
|
|
521
|
+
*/
|
|
445
522
|
list(options?: SandboxListOptions): Promise<SandboxInfo[]>;
|
|
523
|
+
/**
|
|
524
|
+
* The fleet-bookkeeping enumeration: same walk, never throws.
|
|
525
|
+
*
|
|
526
|
+
* Modal has no lifecycle webhooks, so absence from a list is the ONLY
|
|
527
|
+
* termination signal either lane gets — which makes the difference between
|
|
528
|
+
* "the app is empty" and "the enumeration stopped early" the difference
|
|
529
|
+
* between a quiet fleet and one about to be reclaimed.
|
|
530
|
+
*
|
|
531
|
+
* NOTE the divergence from `listSandboxIds` below, which returns an EMPTY set
|
|
532
|
+
* on failure on the grounds that partial results are worse than none for a
|
|
533
|
+
* terminal-state decision. This one returns what it saw alongside
|
|
534
|
+
* `complete: false`. Both are safe because `complete` is what callers branch
|
|
535
|
+
* on, and the shared type documents the choice; do not align one to the other
|
|
536
|
+
* without deciding which rule you want.
|
|
537
|
+
*/
|
|
538
|
+
listAll(options?: SandboxListOptions): Promise<SandboxListPage>;
|
|
539
|
+
private walk;
|
|
446
540
|
/**
|
|
447
541
|
* Live sandbox ids for the whole app, in ONE streamed call and O(1) round
|
|
448
542
|
* trips — the fleet-bookkeeping counterpart to `list()`.
|
|
@@ -463,6 +557,34 @@ declare class ModalProvider implements SandboxProvider {
|
|
|
463
557
|
complete: boolean;
|
|
464
558
|
}>;
|
|
465
559
|
}
|
|
560
|
+
/** The streamed sandbox surface this walk needs — Modal's list() satisfies it. */
|
|
561
|
+
interface ModalSandboxStream {
|
|
562
|
+
sandboxId: string;
|
|
563
|
+
getTags(): Promise<Record<string, string>>;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Drain Modal's sandbox generator into one answer, with an honest completeness
|
|
567
|
+
* verdict.
|
|
568
|
+
*
|
|
569
|
+
* Separate from the provider because everything worth getting wrong lives here
|
|
570
|
+
* and none of it needs a gRPC connection: the difference between "the caller
|
|
571
|
+
* asked for ten" and "the app ran out", the ceiling that stops an unbounded
|
|
572
|
+
* walk, and the rule that a failure mid-walk yields what it saw marked
|
|
573
|
+
* INCOMPLETE rather than an exception or a short complete list.
|
|
574
|
+
*
|
|
575
|
+
* COST WARNING — this loop is O(N) ROUND TRIPS, not O(1). `list()` itself is one
|
|
576
|
+
* streamed call, but `getTags()` is a separate gRPC request per sandbox
|
|
577
|
+
* (`sandboxTagsGet`), so listing N sandboxes costs N+1 calls. That is fine for a
|
|
578
|
+
* user listing their handful of boxes with metadata, and NOT fine for fleet-wide
|
|
579
|
+
* bookkeeping: anything that only needs to know WHICH ids are alive must use
|
|
580
|
+
* `listSandboxIds`, never this. Please do not "optimize" by reintroducing tag
|
|
581
|
+
* reads into those paths.
|
|
582
|
+
*
|
|
583
|
+
* Exported for its test (`_testCollectSandboxes`).
|
|
584
|
+
*/
|
|
585
|
+
declare function collectSandboxes(iterate: () => AsyncIterable<ModalSandboxStream>, wanted?: number): Promise<SandboxListPage & {
|
|
586
|
+
stoppedAtLimit: boolean;
|
|
587
|
+
}>;
|
|
466
588
|
/**
|
|
467
589
|
* Create Modal sandbox provider.
|
|
468
590
|
*
|
|
@@ -477,6 +599,8 @@ declare const _testMapNetworkPolicy: typeof mapNetworkPolicy;
|
|
|
477
599
|
declare const _testMapResources: typeof mapResources;
|
|
478
600
|
declare const _testResolveImageRegistry: typeof resolveImageRegistry;
|
|
479
601
|
declare const _testBuildSandboxInfo: typeof buildSandboxInfo;
|
|
602
|
+
declare const _testCollectSandboxes: typeof collectSandboxes;
|
|
480
603
|
declare const _testValidateTimeout: typeof validateTimeout;
|
|
604
|
+
declare const _testMapIdleTimeout: typeof mapIdleTimeout;
|
|
481
605
|
|
|
482
|
-
export { type FileInfo, type FilesystemEvent, MODAL_MAX_LIFETIME_MS, MODAL_STDIN_CHUNK_BYTES, ModalCommands, type ModalConfig, ModalFiles, ModalNetworkPolicyError, type ModalNetworkPolicyReason, ModalProvider, ModalResourcesError, ModalSandboxLifetimeError, type ProcessInfo, type SandboxCommandHandle, type SandboxCommandResult, type SandboxCommands, type SandboxConnectOptions, type SandboxCreateOptions, type SandboxFiles, type SandboxInfo, type SandboxInstance, type SandboxListOptions, type SandboxProvider, type SandboxRunOptions, type SandboxSpawnOptions, type WatchHandle, type WatchOptions, _testBuildSandboxInfo, _testMapNetworkPolicy, _testMapResources, _testResolveImageRegistry, _testValidateTimeout, _testWrapCommand, createModalProvider };
|
|
606
|
+
export { 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, _testMapIdleTimeout, _testMapNetworkPolicy, _testMapResources, _testResolveImageRegistry, _testValidateTimeout, _testWrapCommand, createModalProvider };
|
package/dist/index.d.ts
CHANGED
|
@@ -50,6 +50,32 @@ declare class ModalSandboxLifetimeError extends Error {
|
|
|
50
50
|
}
|
|
51
51
|
/** Throws ModalSandboxLifetimeError when the timeout exceeds Modal's 24h cap. */
|
|
52
52
|
declare function validateTimeout(timeoutMs: number): void;
|
|
53
|
+
/**
|
|
54
|
+
* Typed error for an idle timeout Modal could not act on. Both bounds are
|
|
55
|
+
* refusals rather than clamps: silently raising a zero, or lowering a value past
|
|
56
|
+
* the lifetime cap, would hand back a box that dies on a schedule the caller
|
|
57
|
+
* never asked for.
|
|
58
|
+
*/
|
|
59
|
+
declare class ModalIdleTimeoutError extends Error {
|
|
60
|
+
readonly requestedIdleTimeoutMs: number;
|
|
61
|
+
constructor(requestedIdleTimeoutMs: number, reason: string);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Evolve's idle bound -> Modal's create params, same shape as mapNetworkPolicy
|
|
65
|
+
* and mapResources: provider-neutral option in, Modal fragment out.
|
|
66
|
+
*
|
|
67
|
+
* ABSENT MEANS ABSENT. Modal's own default is no idle timer at all, so an unset
|
|
68
|
+
* option must spread to nothing — inventing a default here would start killing
|
|
69
|
+
* boxes that today live out their lifetime, for every caller who never asked.
|
|
70
|
+
*
|
|
71
|
+
* An idle timeout has to be a positive span, and one above the 24h lifetime cap
|
|
72
|
+
* can never fire because the sandbox is already gone. Both are caller mistakes,
|
|
73
|
+
* and both throw rather than clamp: silently raising a zero or lowering an
|
|
74
|
+
* over-cap value hands back a box that dies on a schedule nobody chose.
|
|
75
|
+
*/
|
|
76
|
+
declare function mapIdleTimeout(idleTimeoutMs?: number): {
|
|
77
|
+
idleTimeoutMs?: number;
|
|
78
|
+
};
|
|
53
79
|
/**
|
|
54
80
|
* Wrap a command with cwd + env handling and (when not root) an
|
|
55
81
|
* `su <user> -c` wrapper.
|
|
@@ -211,6 +237,19 @@ interface SandboxCreateOptions {
|
|
|
211
237
|
metadata?: Record<string, string>;
|
|
212
238
|
/** Sandbox lifetime in ms. Modal hard-caps lifetime at 24h (MODAL_MAX_LIFETIME_MS). */
|
|
213
239
|
timeoutMs?: number;
|
|
240
|
+
/**
|
|
241
|
+
* Terminate the sandbox after this long with nothing running in it — the
|
|
242
|
+
* bound that reclaims a box whose client died, without waiting out the whole
|
|
243
|
+
* lifetime. Modal is the only provider with both clocks.
|
|
244
|
+
*
|
|
245
|
+
* OMITTED BY DEFAULT: Modal runs no idle timer unless asked. Modal counts a
|
|
246
|
+
* sandbox active while an exec is running, while its stdin is being written,
|
|
247
|
+
* or while a tunnel connection is open — file operations are not named in
|
|
248
|
+
* that list, and this adapter is safe only because it routes reads and writes
|
|
249
|
+
* through exec (`cat` / `cat >`). A future native filesystem path would need
|
|
250
|
+
* this re-checked.
|
|
251
|
+
*/
|
|
252
|
+
idleTimeoutMs?: number;
|
|
214
253
|
workingDirectory?: string;
|
|
215
254
|
/**
|
|
216
255
|
* Per-sandbox compute sizing: cpu in cores, memory in GiB — mapped to
|
|
@@ -250,6 +289,29 @@ interface SandboxListOptions {
|
|
|
250
289
|
metadata?: Record<string, string>;
|
|
251
290
|
limit?: number;
|
|
252
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* A COMPLETE (or admittedly incomplete) enumeration of the app's fleet.
|
|
294
|
+
*
|
|
295
|
+
* `complete` is the load-bearing field. Callers that need a whole fleet —
|
|
296
|
+
* orphan sweeps, lifecycle reconciliation — read a sandbox's ABSENCE from the
|
|
297
|
+
* list as evidence it is gone, so a truncated walk and a small fleet must never
|
|
298
|
+
* be the same answer. That includes a walk stopped by the caller's own `limit`:
|
|
299
|
+
* "you asked for ten and there are more" is a truncated fleet.
|
|
300
|
+
*/
|
|
301
|
+
interface SandboxListPage {
|
|
302
|
+
sandboxes: SandboxInfo[];
|
|
303
|
+
complete: boolean;
|
|
304
|
+
pagesFetched: number;
|
|
305
|
+
error?: string;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Sandboxes a single enumeration will walk before it gives up and reports
|
|
309
|
+
* itself incomplete. Modal's list is an async generator with no page size we
|
|
310
|
+
* control, so the ceiling is counted in SANDBOXES rather than pages — same
|
|
311
|
+
* purpose as the other providers' page caps: never return a short list that
|
|
312
|
+
* reads like a whole one.
|
|
313
|
+
*/
|
|
314
|
+
declare const MODAL_MAX_LIST_SANDBOXES = 10000;
|
|
253
315
|
/** Command execution capabilities */
|
|
254
316
|
interface SandboxCommands {
|
|
255
317
|
/** Run command and wait for completion */
|
|
@@ -326,7 +388,10 @@ interface SandboxProvider {
|
|
|
326
388
|
/** Connect to existing sandbox */
|
|
327
389
|
connect(sandboxId: string, timeoutMs?: number): Promise<SandboxInstance>;
|
|
328
390
|
/** List sandboxes (first page only, up to limit) */
|
|
391
|
+
/** List sandboxes, walking the whole app. `limit` bounds items returned. */
|
|
329
392
|
list(options?: SandboxListOptions): Promise<SandboxInfo[]>;
|
|
393
|
+
/** The same enumeration for fleet bookkeeping: never throws, reports completeness. */
|
|
394
|
+
listAll(options?: SandboxListOptions): Promise<SandboxListPage>;
|
|
330
395
|
}
|
|
331
396
|
interface ModalConfig {
|
|
332
397
|
/** Modal app name. Default: "evolve-sandbox" */
|
|
@@ -442,7 +507,36 @@ declare class ModalProvider implements SandboxProvider {
|
|
|
442
507
|
private resolveImage;
|
|
443
508
|
create(options: SandboxCreateOptions): Promise<SandboxInstance>;
|
|
444
509
|
connect(sandboxId: string, _timeoutMs?: number): Promise<SandboxInstance>;
|
|
510
|
+
/**
|
|
511
|
+
* List sandboxes, walking the whole app.
|
|
512
|
+
*
|
|
513
|
+
* This used to stop at a hardcoded default of 100 regardless of fleet size,
|
|
514
|
+
* which silently truncated any app with more — and said nothing about it
|
|
515
|
+
* while the shared SandboxProvider interface promised exhaustive listing.
|
|
516
|
+
* `limit` still bounds the sandboxes RETURNED, so a caller wanting one cheap
|
|
517
|
+
* sample asks for one; without it the answer is the whole app.
|
|
518
|
+
*
|
|
519
|
+
* The O(N)-round-trips warning below is unchanged and is the reason `limit`
|
|
520
|
+
* matters here more than on the other providers.
|
|
521
|
+
*/
|
|
445
522
|
list(options?: SandboxListOptions): Promise<SandboxInfo[]>;
|
|
523
|
+
/**
|
|
524
|
+
* The fleet-bookkeeping enumeration: same walk, never throws.
|
|
525
|
+
*
|
|
526
|
+
* Modal has no lifecycle webhooks, so absence from a list is the ONLY
|
|
527
|
+
* termination signal either lane gets — which makes the difference between
|
|
528
|
+
* "the app is empty" and "the enumeration stopped early" the difference
|
|
529
|
+
* between a quiet fleet and one about to be reclaimed.
|
|
530
|
+
*
|
|
531
|
+
* NOTE the divergence from `listSandboxIds` below, which returns an EMPTY set
|
|
532
|
+
* on failure on the grounds that partial results are worse than none for a
|
|
533
|
+
* terminal-state decision. This one returns what it saw alongside
|
|
534
|
+
* `complete: false`. Both are safe because `complete` is what callers branch
|
|
535
|
+
* on, and the shared type documents the choice; do not align one to the other
|
|
536
|
+
* without deciding which rule you want.
|
|
537
|
+
*/
|
|
538
|
+
listAll(options?: SandboxListOptions): Promise<SandboxListPage>;
|
|
539
|
+
private walk;
|
|
446
540
|
/**
|
|
447
541
|
* Live sandbox ids for the whole app, in ONE streamed call and O(1) round
|
|
448
542
|
* trips — the fleet-bookkeeping counterpart to `list()`.
|
|
@@ -463,6 +557,34 @@ declare class ModalProvider implements SandboxProvider {
|
|
|
463
557
|
complete: boolean;
|
|
464
558
|
}>;
|
|
465
559
|
}
|
|
560
|
+
/** The streamed sandbox surface this walk needs — Modal's list() satisfies it. */
|
|
561
|
+
interface ModalSandboxStream {
|
|
562
|
+
sandboxId: string;
|
|
563
|
+
getTags(): Promise<Record<string, string>>;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Drain Modal's sandbox generator into one answer, with an honest completeness
|
|
567
|
+
* verdict.
|
|
568
|
+
*
|
|
569
|
+
* Separate from the provider because everything worth getting wrong lives here
|
|
570
|
+
* and none of it needs a gRPC connection: the difference between "the caller
|
|
571
|
+
* asked for ten" and "the app ran out", the ceiling that stops an unbounded
|
|
572
|
+
* walk, and the rule that a failure mid-walk yields what it saw marked
|
|
573
|
+
* INCOMPLETE rather than an exception or a short complete list.
|
|
574
|
+
*
|
|
575
|
+
* COST WARNING — this loop is O(N) ROUND TRIPS, not O(1). `list()` itself is one
|
|
576
|
+
* streamed call, but `getTags()` is a separate gRPC request per sandbox
|
|
577
|
+
* (`sandboxTagsGet`), so listing N sandboxes costs N+1 calls. That is fine for a
|
|
578
|
+
* user listing their handful of boxes with metadata, and NOT fine for fleet-wide
|
|
579
|
+
* bookkeeping: anything that only needs to know WHICH ids are alive must use
|
|
580
|
+
* `listSandboxIds`, never this. Please do not "optimize" by reintroducing tag
|
|
581
|
+
* reads into those paths.
|
|
582
|
+
*
|
|
583
|
+
* Exported for its test (`_testCollectSandboxes`).
|
|
584
|
+
*/
|
|
585
|
+
declare function collectSandboxes(iterate: () => AsyncIterable<ModalSandboxStream>, wanted?: number): Promise<SandboxListPage & {
|
|
586
|
+
stoppedAtLimit: boolean;
|
|
587
|
+
}>;
|
|
466
588
|
/**
|
|
467
589
|
* Create Modal sandbox provider.
|
|
468
590
|
*
|
|
@@ -477,6 +599,8 @@ declare const _testMapNetworkPolicy: typeof mapNetworkPolicy;
|
|
|
477
599
|
declare const _testMapResources: typeof mapResources;
|
|
478
600
|
declare const _testResolveImageRegistry: typeof resolveImageRegistry;
|
|
479
601
|
declare const _testBuildSandboxInfo: typeof buildSandboxInfo;
|
|
602
|
+
declare const _testCollectSandboxes: typeof collectSandboxes;
|
|
480
603
|
declare const _testValidateTimeout: typeof validateTimeout;
|
|
604
|
+
declare const _testMapIdleTimeout: typeof mapIdleTimeout;
|
|
481
605
|
|
|
482
|
-
export { type FileInfo, type FilesystemEvent, MODAL_MAX_LIFETIME_MS, MODAL_STDIN_CHUNK_BYTES, ModalCommands, type ModalConfig, ModalFiles, ModalNetworkPolicyError, type ModalNetworkPolicyReason, ModalProvider, ModalResourcesError, ModalSandboxLifetimeError, type ProcessInfo, type SandboxCommandHandle, type SandboxCommandResult, type SandboxCommands, type SandboxConnectOptions, type SandboxCreateOptions, type SandboxFiles, type SandboxInfo, type SandboxInstance, type SandboxListOptions, type SandboxProvider, type SandboxRunOptions, type SandboxSpawnOptions, type WatchHandle, type WatchOptions, _testBuildSandboxInfo, _testMapNetworkPolicy, _testMapResources, _testResolveImageRegistry, _testValidateTimeout, _testWrapCommand, createModalProvider };
|
|
606
|
+
export { 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, _testMapIdleTimeout, _testMapNetworkPolicy, _testMapResources, _testResolveImageRegistry, _testValidateTimeout, _testWrapCommand, createModalProvider };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {ModalClient}from'modal';import {pack}from'tar-stream';var
|
|
2
|
-
`).slice(1).map(
|
|
3
|
-
`)){if(!o)continue;let d=o.split(/\s+/);if(d.length<9)continue;let a=d[0],c=d.slice(8).join(" ");c==="."||c===".."||i.push({name:c,path:t.endsWith("/")?`${t}${c}`:`${t}/${c}`,type:a.startsWith("d")?"dir":"file"});}return i}async remove(t){await(await this.sandbox.exec(["rm","-rf",t],{timeoutMs:3e4})).wait();}async rename(t,e){await(await this.sandbox.exec(["mv",t,e],{timeoutMs:3e4})).wait();}async readStream(t){return (await this.sandbox.exec(["cat",t],{timeoutMs:3e5,mode:"binary"})).stdout}async writeStream(t,e){let
|
|
4
|
-
export{
|
|
1
|
+
import {ModalClient}from'modal';import {pack}from'tar-stream';var H={"evolve-all":"evolvingmachines/evolve-all"},E=1440*60*1e3,A=8*1024*1024,k="user",R="evolve.image",_="evolve.startedAt",j=new Set([".xlsx",".xls",".docx",".doc",".pptx",".ppt",".pdf",".zip",".tar",".gz",".7z",".rar",".png",".jpg",".jpeg",".gif",".webp",".ico",".bmp",".mp3",".wav",".ogg",".flac",".aac",".mp4",".avi",".mov",".mkv",".webm",".woff",".woff2",".ttf",".otf",".eot",".exe",".dll",".so",".dylib",".sqlite",".db",".pickle",".pkl",".parquet"]);function q(n){let t=n.substring(n.lastIndexOf(".")).toLowerCase();return j.has(t)}var x=class extends Error{requestedTimeoutMs;constructor(t){let e=(t/36e5).toFixed(1);super(`Modal sandboxes have a hard 24h lifetime cap; requested timeout was ${e}h. For sessions longer than 24h, persist progress with Evolve checkpoints and resume in a fresh sandbox instead of extending the timeout.`),this.name="ModalSandboxLifetimeError",this.requestedTimeoutMs=t;}};function C(n){if(n>E)throw new x(n)}var p=class extends Error{requestedIdleTimeoutMs;constructor(t,e){super(`Modal idleTimeoutMs of ${t}ms is invalid: ${e}`),this.name="ModalIdleTimeoutError",this.requestedIdleTimeoutMs=t;}};function O(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>E)throw new p(n,"it exceeds Modal's 24h lifetime cap, so the sandbox would always die of the lifetime first");return {idleTimeoutMs:n}}function b(n,t,e,r){let s="";r&&Object.keys(r).length>0&&(s=Object.entries(r).filter(([,d])=>d!=null).map(([d,a])=>`export ${d}='${String(a).replace(/'/g,"'\\''")}'`).join("; ")+"; ");let i=e?`cd '${e.replace(/'/g,"'\\''")}' && ${s}${n}`:`${s}${n}`;if(t==="root")return ["bash","-c",i];let o=Buffer.from(i).toString("base64");return ["su",t,"-c",`echo ${o} | base64 -d | bash`]}var S=class extends Error{constructor(t){super(t),this.name="ModalResourcesError";}},z=4,G=4096;function N(n){if(n?.disk!==void 0)throw new S(`Modal's JS SDK has no create-time disk-size parameter, so a ${n.disk} GiB disk request cannot be enforced. Drop \`resources.disk\` (containers get Modal's default disk quota) or run on a provider that sizes disk.`);return {cpu:n?.cpu??z,memoryMiB:n?.memory!==void 0?Math.ceil(n.memory*1024):G}}var u=class extends Error{reason;destination;constructor(t,e,r){super(e),this.name="ModalNetworkPolicyError",this.reason=t,this.destination=r;}};function D(n){let t=/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(?:\/(\d{1,3}))?$/.exec(n);return !(!t||[t[1],t[2],t[3],t[4]].some(e=>Number(e)>255)||t[5]!==void 0&&Number(t[5])>32)}function K(n){return /^\d{1,3}(\.\d{1,3}){3}(\/\d+)?$/.test(n)&&!D(n)}function X(n){return n.startsWith("[")?true:(n.match(/:/g)?.length??0)>=2}function Y(n){return /^[^:]+:\d+$/.test(n)}function T(n){if(!n||n.outbound==="open"){if(n?.allowedDestinations?.length)throw new Error("network.allowedDestinations is only valid when outbound is blocked");return {}}let t=n.allowedDestinations??[];if(t.length===0)return {blockNetwork:true};let e=[],r=[];for(let s of t)if(D(s))e.push(s.includes("/")?s:`${s}/32`);else {if(K(s))throw new u("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(X(s))e.push(s.includes("/")?s:`${s}/128`);else {if(Y(s))throw new u("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:e,outboundDomainAllowlist:r}}function $(n){let t=n.split("/")[0];return /^\d{12}\.dkr\.ecr\.[a-z0-9-]+\.amazonaws\.com$/.test(t)?"aws-ecr":t==="gcr.io"||t.endsWith(".gcr.io")||t.endsWith("-docker.pkg.dev")?"gcp-artifact-registry":"registry"}function I(n,t,e){let{[R]:r,[_]:s,...i}=t;return {sandboxId:n,image:r??e??"",metadata:i,startedAt:s??""}}var M=1e4,y=class{constructor(t,e){this.sandbox=t;this.user=e;}async run(t,e){let r=b(t,this.user,e?.cwd,e?.envs),s=await this.sandbox.exec(r,{timeoutMs:e?.timeoutMs}),{stdout:i,stderr:o}=await this.accumulateStreams(s,e?.onStdout,e?.onStderr);return {exitCode:await s.wait(),stdout:i,stderr:o}}async spawn(t,e){let r=b(t,this.user,e?.cwd,e?.envs),s=await this.sandbox.exec(r,{timeoutMs:e?.timeoutMs}),i="",o="",d=this.accumulateStreams(s,e?.onStdout?c=>{e.onStdout(c);}:void 0,e?.onStderr?c=>{e.onStderr(c);}:void 0).then(({stdout:c,stderr:m})=>{i=c,o=m;}).catch(()=>{});return {processId:`modal-${Date.now()}-${Math.random().toString(36).slice(2)}`,wait:async()=>(await d,{exitCode:await s.wait(),stdout:i,stderr:o}),kill:async()=>false}}async list(){let t=await this.sandbox.exec(["ps","-eo","pid,comm,args"],{timeoutMs:1e4});return await t.wait(),(await t.stdout.readText()).trim().split(`
|
|
2
|
+
`).slice(1).map(s=>{let i=s.trim().split(/\s+/);return {processId:i[0],cmd:i[1]||"",args:i.slice(2),envs:{}}})}async connect(t,e){throw new Error("Modal does not support connecting to existing processes")}async sendStdin(t,e){throw new Error("Modal does not support sendStdin by process ID")}async kill(t){return await(await this.sandbox.exec(["kill","-9",t],{timeoutMs:1e4})).wait()===0}async accumulateStreams(t,e,r){let s="",i="",o=[];return o.push((async()=>{try{for await(let d of t.stdout){let a=typeof d=="string"?d:new TextDecoder().decode(d);s+=a,e?.(a);}}catch{}})()),o.push((async()=>{try{for await(let d of t.stderr){let a=typeof d=="string"?d:new TextDecoder().decode(d);i+=a,r?.(a);}}catch{}})()),await Promise.all(o),{stdout:s,stderr:i}}},v=class{constructor(t,e){this.sandbox=t;this.user=e;}async chownToUser(t,e=false){if(this.user==="root")return;let r=e?["chown","-R",`${this.user}:${this.user}`,t]:["chown",`${this.user}:${this.user}`,t];await(await this.sandbox.exec(r,{timeoutMs:e?3e4:1e4})).wait();}async writeStdinChunked(t,e){for(let r=0;r<e.length;r+=A)await t.writeBytes(e.subarray(r,r+A));}async read(t){if(q(t)){let s=await this.sandbox.exec(["cat",t],{timeoutMs:3e5,mode:"binary"}),i=await s.wait();if(i!==0){let o=await s.stderr.readText();throw new Error(`Failed to read file ${t}: ${o||`exit code ${i}`}`)}return await s.stdout.readBytes()}let e=await this.sandbox.exec(["cat",t],{timeoutMs:3e5}),r=await e.wait();if(r!==0){let s=await e.stderr.readText();throw new Error(`Failed to read file ${t}: ${s||`exit code ${r}`}`)}return await e.stdout.readText()}async write(t,e){let r=this.toBuffer(e),s=t.substring(0,t.lastIndexOf("/"));s&&await this.makeDir(s);let i=t.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(t);}async writeBatch(t){let e=pack(),r=[],s=new Set;for(let a of t){let c=this.toBuffer(a.data),m=a.path.startsWith("/")?a.path.slice(1):a.path;e.entry({name:m},c);let l=a.path.substring(0,a.path.lastIndexOf("/"));l&&s.add(l);}e.finalize();for await(let a of e)r.push(Buffer.from(a));let i=Buffer.concat(r),o=await this.sandbox.exec(["tar","-xf","-","-C","/"],{mode:"binary"});if(await this.writeStdinChunked(o.stdin,new Uint8Array(i)),await o.stdin.getWriter().close(),await o.wait(),s.size>0){let a=Array.from(s),c=new Set(a.map(m=>m.split("/").slice(0,4).join("/")));for(let m of c)await this.chownToUser(m,true);}}async makeDir(t){await(await this.sandbox.exec(["mkdir","-p",t],{timeoutMs:1e4})).wait(),await this.chownToUser(t,true);}async exists(t){return await(await this.sandbox.exec(["test","-e",t],{timeoutMs:1e4})).wait()===0}async list(t){let e=t.replace(/'/g,"'\\''"),r=await this.sandbox.exec(["bash","-c",`ls -la '${e}' | 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 d=o.split(/\s+/);if(d.length<9)continue;let a=d[0],c=d.slice(8).join(" ");c==="."||c===".."||i.push({name:c,path:t.endsWith("/")?`${t}${c}`:`${t}/${c}`,type:a.startsWith("d")?"dir":"file"});}return i}async remove(t){await(await this.sandbox.exec(["rm","-rf",t],{timeoutMs:3e4})).wait();}async rename(t,e){await(await this.sandbox.exec(["mv",t,e],{timeoutMs:3e4})).wait();}async readStream(t){return (await this.sandbox.exec(["cat",t],{timeoutMs:3e5,mode:"binary"})).stdout}async writeStream(t,e){let r=t.substring(0,t.lastIndexOf("/"));r&&await this.makeDir(r);let s=t.replace(/'/g,"'\\''"),i=await this.sandbox.exec(["bash","-c",`cat > '${s}'`],{mode:"binary"}),o=e.getReader();try{for(;;){let{done:a,value:c}=await o.read();if(a)break;await this.writeStdinChunked(i.stdin,c);}}finally{o.releaseLock();}await i.stdin.getWriter().close(),await i.wait(),await this.chownToUser(t);}async writeFromPath(t,e){let{createReadStream:r}=await import('fs'),{Readable:s}=await import('stream'),i=s.toWeb(r(e));await this.writeStream(t,i);}async uploadUrl(t,e){throw new Error("Modal does not support pre-signed upload URLs")}async downloadUrl(t,e){throw new Error("Modal does not support pre-signed download URLs")}async watchDir(t,e,r){throw new Error("Modal does not support watchDir")}toBuffer(t){if(typeof t=="string")return Buffer.from(t,"utf-8");if(t instanceof Buffer)return t;if(t instanceof ArrayBuffer||t instanceof Uint8Array)return Buffer.from(t);throw new Error(`Unsupported data type: ${typeof t}`)}},g=class{constructor(t,e,r){this.sandbox=t;this.commands=new y(t,r),this.files=new v(t,r),this.image=e;}commands;files;image;get sandboxId(){return this.sandbox.sandboxId}async getHost(t){let r=(await this.sandbox.tunnels())[t];if(!r)throw new Error(`No tunnel found for port ${t}`);return r.url}async isRunning(){try{return await this.sandbox.poll()===null}catch{return false}}async getInfo(){let t=await this.sandbox.getTags();return I(this.sandbox.sandboxId,t,this.image)}async kill(){try{await this.sandbox.terminate();}catch{await new Promise(t=>setTimeout(t,500)),await this.sandbox.terminate();}}async pause(){throw new Error("Modal does not support pause/resume. Persist progress with Evolve checkpoints and resume in a fresh sandbox, or use kill() to terminate.")}},P=class{providerType="modal";name="Modal";client;appName;defaultTimeoutMs;imageName;imageSecretName;_app;sandboxUsers=new Map;constructor(t){!t.endpoint&&process.env.MODAL_SERVER_URL?.startsWith("unix:")?process.env.MODAL_SERVER_URL="https://api.modal.com:443":t.endpoint&&(process.env.MODAL_SERVER_URL=t.endpoint),this.client=new ModalClient({tokenId:t.tokenId,tokenSecret:t.tokenSecret}),this.appName=t.appName??"evolve-sandbox",this.defaultTimeoutMs=t.defaultTimeoutMs??36e5,this.imageName=t.imageName??"evolve-all",this.imageSecretName=t.imageSecretName;}async getApp(){return this._app||(this._app=await this.client.apps.fromName(this.appName,{createIfMissing:true})),this._app}async resolveImage(t){let e=$(t);if(e==="registry"){let s=this.imageSecretName?await this.client.secrets.fromName(this.imageSecretName):void 0;return this.client.images.fromRegistry(t,s)}if(!this.imageSecretName)throw new Error(`Private registry image "${t}" requires config.imageSecretName \u2014 the name of a Modal Secret holding registry credentials (AWS ECR: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION with read-only ECR IAM). Create one at https://modal.com/secrets`);let r=await this.client.secrets.fromName(this.imageSecretName);return e==="aws-ecr"?this.client.images.fromAwsEcr(t,r):this.client.images.fromGcpArtifactRegistry(t,r)}async create(t){let e=t.timeoutMs??this.defaultTimeoutMs;C(e);let r=O(t.idleTimeoutMs),s=T(t.network),i=N(t.resources),o=t.user??k,d=await this.getApp(),a=t.image||this.imageName,c=H[a]??a,l=await(await this.resolveImage(c)).build(d),f=t.envs?Object.fromEntries(Object.entries(t.envs).filter(([,h])=>h!=null)):void 0,B=f&&Object.keys(f).length>0?f:void 0,U={...t.metadata,[R]:c,[_]:new Date().toISOString()},w=await this.client.sandboxes.create(d,l,{cpu:i.cpu,memoryMiB:i.memoryMiB,timeoutMs:e,...r,workdir:t.workingDirectory,env:B,tags:U,...s});return t.workingDirectory&&o!=="root"&&await(await w.exec(["chown","-R",`${o}:${o}`,t.workingDirectory],{timeoutMs:3e4})).wait(),this.sandboxUsers.set(w.sandboxId,o),new g(w,c,o)}async connect(t,e){let r=await this.client.sandboxes.fromId(t),s=this.sandboxUsers.get(t)??k;return new g(r,void 0,s)}async list(t){let e=await this.walk(t);if(e.error&&!e.stoppedAtLimit)throw new Error(e.error);return e.sandboxes}async listAll(t){let{stoppedAtLimit:e,...r}=await this.walk(t);return r}async walk(t){if(t?.state&&!t.state.includes("running"))return {sandboxes:[],complete:true,pagesFetched:0,stoppedAtLimit:false};let e;try{e=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 L(()=>this.client.sandboxes.list({appId:e.appId,tags:t?.metadata}),t?.limit)}async listSandboxIds(){let t=new Set;try{let e=await this.getApp();for await(let r of this.client.sandboxes.list({appId:e.appId}))t.add(r.sandboxId);return {ids:t,complete:!0}}catch{return {ids:new Set,complete:false}}}};async function L(n,t){let e=[];try{for await(let r of n()){if(t!==void 0&&e.length>=t)return {sandboxes:e,complete:!1,pagesFetched:e.length,stoppedAtLimit:!0,error:`stopped at the requested limit of ${t} with more sandboxes available`};if(e.length>=M)return {sandboxes:e,complete:!1,pagesFetched:e.length,stoppedAtLimit:!1,error:`sandbox list exceeded ${M} sandboxes`};let s=await r.getTags();e.push(I(r.sandboxId,s));}}catch(r){return {sandboxes:e,complete:false,pagesFetched:e.length,stoppedAtLimit:false,error:`sandbox list failed: ${r instanceof Error?r.message:String(r)}`}}return {sandboxes:e,complete:true,pagesFetched:e.length,stoppedAtLimit:false}}function rt(n={}){let t=n.tokenId??process.env.MODAL_TOKEN_ID,e=n.tokenSecret??process.env.MODAL_TOKEN_SECRET;if(!t||!e)throw new Error("Modal credentials required. Set MODAL_TOKEN_ID and MODAL_TOKEN_SECRET environment variables, or pass tokenId/tokenSecret in config. Get your token at https://modal.com/settings/tokens");return new P({...n,tokenId:t,tokenSecret:e})}var st=b,nt=T,it=N,ot=$,at=I,dt=L,ct=C,mt=O;
|
|
4
|
+
export{E as MODAL_MAX_LIFETIME_MS,M as MODAL_MAX_LIST_SANDBOXES,A as MODAL_STDIN_CHUNK_BYTES,y as ModalCommands,v as ModalFiles,p as ModalIdleTimeoutError,u as ModalNetworkPolicyError,P as ModalProvider,S as ModalResourcesError,x as ModalSandboxLifetimeError,at as _testBuildSandboxInfo,dt as _testCollectSandboxes,mt as _testMapIdleTimeout,nt as _testMapNetworkPolicy,it as _testMapResources,ot as _testResolveImageRegistry,ct as _testValidateTimeout,st as _testWrapCommand,rt as createModalProvider};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evolvingmachines/modal",
|
|
3
|
-
"version": "0.0.52-project-sable.
|
|
3
|
+
"version": "0.0.52-project-sable.20260729.96caaab",
|
|
4
4
|
"keywords": [
|
|
5
5
|
"ai",
|
|
6
6
|
"agents",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"build": "tsup --minify",
|
|
35
35
|
"dev": "tsup --watch",
|
|
36
36
|
"type-check": "tsc --noEmit",
|
|
37
|
-
"test:unit": "tsx tests/unit/modal-provider.test.ts"
|
|
37
|
+
"test:unit": "tsx tests/unit/modal-provider.test.ts && tsx tests/unit/modal-list-pagination.test.ts"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"modal": "^0.9.0",
|