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