forgevm 0.1.0__tar.gz

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.
forgevm-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,86 @@
1
+ Metadata-Version: 2.4
2
+ Name: forgevm
3
+ Version: 0.1.0
4
+ Summary: Python SDK for ForgeVM — self-hosted microVM sandboxes for LLMs
5
+ Author: DohaerisAI
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/DohaerisAI/forgevm
8
+ Project-URL: Repository, https://github.com/DohaerisAI/forgevm
9
+ Project-URL: Documentation, https://github.com/DohaerisAI/forgevm#use-with-python
10
+ Project-URL: Issues, https://github.com/DohaerisAI/forgevm/issues
11
+ Keywords: forgevm,sandbox,microvm,llm,code-execution,firecracker,ai-agent
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Classifier: Topic :: System :: Emulators
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ Requires-Dist: httpx>=0.27.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8.0; extra == "dev"
28
+ Requires-Dist: pytest-httpx>=0.30.0; extra == "dev"
29
+
30
+ # ForgeVM Python SDK
31
+
32
+ Python client for [ForgeVM](https://github.com/DohaerisAI/forgevm) -- self-hosted compute sandboxes for LLMs.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install forgevm
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ```python
43
+ from forgevm import Client
44
+
45
+ client = Client("http://localhost:7423")
46
+
47
+ # Spawn a sandbox
48
+ sandbox = client.spawn(image="alpine:latest")
49
+
50
+ # Execute commands
51
+ result = sandbox.exec("echo hello")
52
+ print(result.stdout) # "hello\n"
53
+
54
+ # File operations
55
+ sandbox.write_file("/tmp/hello.txt", "Hello, world!")
56
+ content = sandbox.read_file("/tmp/hello.txt")
57
+
58
+ # Extend TTL
59
+ sandbox.extend_ttl("30m")
60
+
61
+ # Auto-cleanup with context manager
62
+ with client.spawn(image="python:3.12") as sb:
63
+ sb.exec("python3 -c 'print(1+1)'")
64
+ # sandbox destroyed automatically
65
+
66
+ # Streaming output
67
+ for chunk in sandbox.exec_stream("ping -c 3 localhost"):
68
+ print(chunk.data, end="")
69
+
70
+ sandbox.destroy()
71
+ ```
72
+
73
+ ## Async Support
74
+
75
+ ```python
76
+ from forgevm import AsyncClient
77
+
78
+ async with AsyncClient("http://localhost:7423") as client:
79
+ sandbox = await client.spawn(image="alpine:latest")
80
+ result = await sandbox.exec("echo hello")
81
+ await sandbox.destroy()
82
+ ```
83
+
84
+ ## License
85
+
86
+ MIT
@@ -0,0 +1,57 @@
1
+ # ForgeVM Python SDK
2
+
3
+ Python client for [ForgeVM](https://github.com/DohaerisAI/forgevm) -- self-hosted compute sandboxes for LLMs.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install forgevm
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ from forgevm import Client
15
+
16
+ client = Client("http://localhost:7423")
17
+
18
+ # Spawn a sandbox
19
+ sandbox = client.spawn(image="alpine:latest")
20
+
21
+ # Execute commands
22
+ result = sandbox.exec("echo hello")
23
+ print(result.stdout) # "hello\n"
24
+
25
+ # File operations
26
+ sandbox.write_file("/tmp/hello.txt", "Hello, world!")
27
+ content = sandbox.read_file("/tmp/hello.txt")
28
+
29
+ # Extend TTL
30
+ sandbox.extend_ttl("30m")
31
+
32
+ # Auto-cleanup with context manager
33
+ with client.spawn(image="python:3.12") as sb:
34
+ sb.exec("python3 -c 'print(1+1)'")
35
+ # sandbox destroyed automatically
36
+
37
+ # Streaming output
38
+ for chunk in sandbox.exec_stream("ping -c 3 localhost"):
39
+ print(chunk.data, end="")
40
+
41
+ sandbox.destroy()
42
+ ```
43
+
44
+ ## Async Support
45
+
46
+ ```python
47
+ from forgevm import AsyncClient
48
+
49
+ async with AsyncClient("http://localhost:7423") as client:
50
+ sandbox = await client.spawn(image="alpine:latest")
51
+ result = await sandbox.exec("echo hello")
52
+ await sandbox.destroy()
53
+ ```
54
+
55
+ ## License
56
+
57
+ MIT
@@ -0,0 +1,28 @@
1
+ """ForgeVM Python SDK — client for the ForgeVM sandbox orchestrator."""
2
+
3
+ from forgevm.client import Client
4
+ from forgevm.sandbox import Sandbox
5
+ from forgevm.async_client import AsyncClient
6
+ from forgevm.async_sandbox import AsyncSandbox
7
+ from forgevm.models import ExecResult, SandboxInfo, Template
8
+ from forgevm.exceptions import (
9
+ ForgevmError,
10
+ SandboxNotFound,
11
+ ProviderError,
12
+ ConnectionError,
13
+ )
14
+
15
+ __version__ = "0.1.0"
16
+ __all__ = [
17
+ "Client",
18
+ "Sandbox",
19
+ "AsyncClient",
20
+ "AsyncSandbox",
21
+ "ExecResult",
22
+ "SandboxInfo",
23
+ "Template",
24
+ "ForgevmError",
25
+ "SandboxNotFound",
26
+ "ProviderError",
27
+ "ConnectionError",
28
+ ]
@@ -0,0 +1,139 @@
1
+ """ForgeVM Async Client — async variant using httpx.AsyncClient."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import httpx
6
+
7
+ from forgevm.async_sandbox import AsyncSandbox
8
+ from forgevm.exceptions import ConnectionError, handle_response
9
+ from forgevm.models import SandboxInfo
10
+
11
+
12
+ class AsyncClient:
13
+ """Async client for the ForgeVM API server.
14
+
15
+ Usage:
16
+ async with AsyncClient("http://localhost:7423") as client:
17
+ sandbox = await client.spawn(image="alpine:latest")
18
+ result = await sandbox.exec("echo hello")
19
+ print(result.stdout)
20
+ await sandbox.destroy()
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ base_url: str = "http://localhost:7423",
26
+ api_key: str | None = None,
27
+ user_id: str | None = None,
28
+ timeout: float = 30.0,
29
+ ):
30
+ headers = {}
31
+ if api_key:
32
+ headers["X-API-Key"] = api_key
33
+ if user_id:
34
+ headers["X-User-ID"] = user_id
35
+
36
+ self._http = httpx.AsyncClient(
37
+ base_url=base_url,
38
+ headers=headers,
39
+ timeout=timeout,
40
+ )
41
+
42
+ async def spawn(
43
+ self,
44
+ image: str = "alpine:latest",
45
+ provider: str | None = None,
46
+ memory_mb: int | None = None,
47
+ vcpus: int | None = None,
48
+ ttl: str | None = None,
49
+ template: str | None = None,
50
+ metadata: dict[str, str] | None = None,
51
+ ) -> AsyncSandbox:
52
+ """Spawn a new sandbox."""
53
+ body: dict = {"image": image}
54
+ if provider:
55
+ body["provider"] = provider
56
+ if memory_mb:
57
+ body["memory_mb"] = memory_mb
58
+ if vcpus:
59
+ body["vcpus"] = vcpus
60
+ if ttl:
61
+ body["ttl"] = ttl
62
+ if template:
63
+ body["template"] = template
64
+ if metadata:
65
+ body["metadata"] = metadata
66
+
67
+ try:
68
+ resp = await self._http.post("/api/v1/sandboxes", json=body)
69
+ except httpx.ConnectError as e:
70
+ raise ConnectionError(f"Cannot connect to ForgeVM server: {e}")
71
+
72
+ handle_response(resp)
73
+ data = resp.json()
74
+ return AsyncSandbox(self._http, data["id"], info=data)
75
+
76
+ async def spawn_template(self, template_name: str) -> AsyncSandbox:
77
+ """Spawn a sandbox from a saved template."""
78
+ try:
79
+ resp = await self._http.post(f"/api/v1/templates/{template_name}/spawn")
80
+ except httpx.ConnectError as e:
81
+ raise ConnectionError(f"Cannot connect to ForgeVM server: {e}")
82
+
83
+ handle_response(resp)
84
+ data = resp.json()
85
+ return AsyncSandbox(self._http, data["id"], info=data)
86
+
87
+ async def get(self, sandbox_id: str) -> AsyncSandbox:
88
+ """Get an existing sandbox by ID."""
89
+ resp = await self._http.get(f"/api/v1/sandboxes/{sandbox_id}")
90
+ handle_response(resp)
91
+ data = resp.json()
92
+ return AsyncSandbox(self._http, data["id"], info=data)
93
+
94
+ async def list(self) -> list[SandboxInfo]:
95
+ """List all active sandboxes."""
96
+ resp = await self._http.get("/api/v1/sandboxes")
97
+ handle_response(resp)
98
+ return [
99
+ SandboxInfo(
100
+ id=s["id"],
101
+ state=s["state"],
102
+ provider=s["provider"],
103
+ image=s["image"],
104
+ memory_mb=s.get("memory_mb", 512),
105
+ vcpus=s.get("vcpus", 1),
106
+ created_at=s.get("created_at", ""),
107
+ expires_at=s.get("expires_at", ""),
108
+ metadata=s.get("metadata", {}),
109
+ )
110
+ for s in resp.json()
111
+ ]
112
+
113
+ async def prune(self) -> int:
114
+ """Prune expired sandboxes. Returns count pruned."""
115
+ resp = await self._http.delete("/api/v1/sandboxes")
116
+ handle_response(resp)
117
+ return resp.json().get("pruned", 0)
118
+
119
+ async def pool_status(self) -> dict:
120
+ """Get VM pool status."""
121
+ resp = await self._http.get("/api/v1/pool/status")
122
+ handle_response(resp)
123
+ return resp.json()
124
+
125
+ async def health(self) -> dict:
126
+ """Check server health."""
127
+ resp = await self._http.get("/api/v1/health")
128
+ handle_response(resp)
129
+ return resp.json()
130
+
131
+ async def close(self) -> None:
132
+ """Close the HTTP client."""
133
+ await self._http.aclose()
134
+
135
+ async def __aenter__(self):
136
+ return self
137
+
138
+ async def __aexit__(self, *args):
139
+ await self.close()
@@ -0,0 +1,171 @@
1
+ """ForgeVM AsyncSandbox — async variant of Sandbox."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import AsyncIterator
6
+
7
+ import httpx
8
+
9
+ from forgevm.exceptions import handle_response
10
+ from forgevm.models import ExecResult
11
+ from forgevm.streaming import StreamChunk
12
+
13
+
14
+ class AsyncSandbox:
15
+ """An async ForgeVM sandbox instance. Supports async context manager for auto-cleanup."""
16
+
17
+ def __init__(self, http: httpx.AsyncClient, sandbox_id: str, info: dict | None = None):
18
+ self._http = http
19
+ self.id = sandbox_id
20
+ self._info = info or {}
21
+
22
+ @property
23
+ def state(self) -> str:
24
+ return self._info.get("state", "unknown")
25
+
26
+ @property
27
+ def provider(self) -> str:
28
+ return self._info.get("provider", "")
29
+
30
+ @property
31
+ def image(self) -> str:
32
+ return self._info.get("image", "")
33
+
34
+ async def exec(
35
+ self,
36
+ command: str,
37
+ args: list[str] | None = None,
38
+ env: dict[str, str] | None = None,
39
+ workdir: str | None = None,
40
+ ) -> ExecResult:
41
+ """Execute a command in the sandbox."""
42
+ body: dict = {"command": command}
43
+ if args:
44
+ body["args"] = args
45
+ if env:
46
+ body["env"] = env
47
+ if workdir:
48
+ body["workdir"] = workdir
49
+
50
+ resp = await self._http.post(f"/api/v1/sandboxes/{self.id}/exec", json=body)
51
+ handle_response(resp)
52
+ data = resp.json()
53
+ return ExecResult(
54
+ exit_code=data["exit_code"],
55
+ stdout=data["stdout"],
56
+ stderr=data["stderr"],
57
+ duration=data.get("duration", ""),
58
+ )
59
+
60
+ async def exec_stream(
61
+ self,
62
+ command: str,
63
+ args: list[str] | None = None,
64
+ env: dict[str, str] | None = None,
65
+ workdir: str | None = None,
66
+ ) -> AsyncIterator[StreamChunk]:
67
+ """Execute a command and stream output chunks asynchronously."""
68
+ import json as jsonlib
69
+
70
+ body: dict = {"command": command, "stream": True}
71
+ if args:
72
+ body["args"] = args
73
+ if env:
74
+ body["env"] = env
75
+ if workdir:
76
+ body["workdir"] = workdir
77
+
78
+ async with self._http.stream("POST", f"/api/v1/sandboxes/{self.id}/exec", json=body) as resp:
79
+ handle_response(resp)
80
+ async for line in resp.aiter_lines():
81
+ line = line.strip()
82
+ if not line:
83
+ continue
84
+ data = jsonlib.loads(line)
85
+ yield StreamChunk(
86
+ stream=data.get("stream", "stdout"),
87
+ data=data.get("data", ""),
88
+ )
89
+
90
+ async def write_file(self, path: str, content: str, mode: str | None = None) -> None:
91
+ """Write a file to the sandbox."""
92
+ body: dict = {"path": path, "content": content}
93
+ if mode:
94
+ body["mode"] = mode
95
+ resp = await self._http.post(f"/api/v1/sandboxes/{self.id}/files", json=body)
96
+ handle_response(resp)
97
+
98
+ async def read_file(self, path: str) -> str:
99
+ """Read a file from the sandbox."""
100
+ resp = await self._http.get(f"/api/v1/sandboxes/{self.id}/files", params={"path": path})
101
+ handle_response(resp)
102
+ return resp.text
103
+
104
+ async def list_files(self, path: str = "/") -> list[dict]:
105
+ """List files in a sandbox directory."""
106
+ resp = await self._http.get(f"/api/v1/sandboxes/{self.id}/files/list", params={"path": path})
107
+ handle_response(resp)
108
+ return resp.json()
109
+
110
+ async def delete_file(self, path: str, recursive: bool = False) -> None:
111
+ """Delete a file or directory from the sandbox."""
112
+ params = {"path": path}
113
+ if recursive:
114
+ params["recursive"] = "true"
115
+ resp = await self._http.delete(f"/api/v1/sandboxes/{self.id}/files", params=params)
116
+ handle_response(resp)
117
+
118
+ async def move_file(self, old_path: str, new_path: str) -> None:
119
+ """Move/rename a file in the sandbox."""
120
+ resp = await self._http.post(
121
+ f"/api/v1/sandboxes/{self.id}/files/move",
122
+ json={"old_path": old_path, "new_path": new_path},
123
+ )
124
+ handle_response(resp)
125
+
126
+ async def chmod_file(self, path: str, mode: str) -> None:
127
+ """Change file permissions in the sandbox."""
128
+ resp = await self._http.post(
129
+ f"/api/v1/sandboxes/{self.id}/files/chmod",
130
+ json={"path": path, "mode": mode},
131
+ )
132
+ handle_response(resp)
133
+
134
+ async def stat_file(self, path: str) -> dict:
135
+ """Get file info for a single file in the sandbox."""
136
+ resp = await self._http.get(
137
+ f"/api/v1/sandboxes/{self.id}/files/stat", params={"path": path}
138
+ )
139
+ handle_response(resp)
140
+ return resp.json()
141
+
142
+ async def glob_files(self, pattern: str) -> list[str]:
143
+ """Return paths matching a glob pattern in the sandbox."""
144
+ resp = await self._http.get(
145
+ f"/api/v1/sandboxes/{self.id}/files/glob", params={"pattern": pattern}
146
+ )
147
+ handle_response(resp)
148
+ return resp.json()
149
+
150
+ async def destroy(self) -> None:
151
+ """Destroy this sandbox."""
152
+ resp = await self._http.delete(f"/api/v1/sandboxes/{self.id}")
153
+ handle_response(resp)
154
+
155
+ async def refresh(self) -> None:
156
+ """Refresh sandbox info from the server."""
157
+ resp = await self._http.get(f"/api/v1/sandboxes/{self.id}")
158
+ handle_response(resp)
159
+ self._info = resp.json()
160
+
161
+ async def __aenter__(self):
162
+ return self
163
+
164
+ async def __aexit__(self, *args):
165
+ try:
166
+ await self.destroy()
167
+ except Exception:
168
+ pass
169
+
170
+ def __repr__(self) -> str:
171
+ return f"AsyncSandbox(id={self.id!r}, state={self.state!r})"
@@ -0,0 +1,125 @@
1
+ """ForgeVM Client — main entry point for the SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import httpx
6
+
7
+ from forgevm.exceptions import ConnectionError, handle_response
8
+ from forgevm.models import SandboxInfo
9
+ from forgevm.sandbox import Sandbox
10
+
11
+
12
+ class Client:
13
+ """Client for the ForgeVM API server.
14
+
15
+ Usage:
16
+ with Client("http://localhost:7423") as client:
17
+ sandbox = client.spawn(image="alpine:latest")
18
+ result = sandbox.exec("echo hello")
19
+ print(result.stdout)
20
+ sandbox.destroy()
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ base_url: str = "http://localhost:7423",
26
+ api_key: str | None = None,
27
+ user_id: str | None = None,
28
+ timeout: float = 30.0,
29
+ ):
30
+ headers = {}
31
+ if api_key:
32
+ headers["X-API-Key"] = api_key
33
+ if user_id:
34
+ headers["X-User-ID"] = user_id
35
+
36
+ self._http = httpx.Client(
37
+ base_url=base_url,
38
+ headers=headers,
39
+ timeout=timeout,
40
+ )
41
+
42
+ def spawn(
43
+ self,
44
+ image: str = "alpine:latest",
45
+ provider: str | None = None,
46
+ memory_mb: int | None = None,
47
+ vcpus: int | None = None,
48
+ ttl: str | None = None,
49
+ metadata: dict[str, str] | None = None,
50
+ ) -> Sandbox:
51
+ """Spawn a new sandbox."""
52
+ body: dict = {"image": image}
53
+ if provider:
54
+ body["provider"] = provider
55
+ if memory_mb:
56
+ body["memory_mb"] = memory_mb
57
+ if vcpus:
58
+ body["vcpus"] = vcpus
59
+ if ttl:
60
+ body["ttl"] = ttl
61
+ if metadata:
62
+ body["metadata"] = metadata
63
+
64
+ try:
65
+ resp = self._http.post("/api/v1/sandboxes", json=body)
66
+ except httpx.ConnectError as e:
67
+ raise ConnectionError(f"Cannot connect to ForgeVM server: {e}")
68
+
69
+ handle_response(resp)
70
+ data = resp.json()
71
+ return Sandbox(self._http, data["id"], info=data)
72
+
73
+ def get(self, sandbox_id: str) -> Sandbox:
74
+ """Get an existing sandbox by ID."""
75
+ resp = self._http.get(f"/api/v1/sandboxes/{sandbox_id}")
76
+ handle_response(resp)
77
+ data = resp.json()
78
+ return Sandbox(self._http, data["id"], info=data)
79
+
80
+ def list(self) -> list[SandboxInfo]:
81
+ """List all active sandboxes."""
82
+ resp = self._http.get("/api/v1/sandboxes")
83
+ handle_response(resp)
84
+ return [
85
+ SandboxInfo(
86
+ id=s["id"],
87
+ state=s["state"],
88
+ provider=s["provider"],
89
+ image=s["image"],
90
+ memory_mb=s.get("memory_mb", 512),
91
+ vcpus=s.get("vcpus", 1),
92
+ created_at=s.get("created_at", ""),
93
+ expires_at=s.get("expires_at", ""),
94
+ metadata=s.get("metadata", {}),
95
+ )
96
+ for s in resp.json()
97
+ ]
98
+
99
+ def prune(self) -> int:
100
+ """Prune expired sandboxes. Returns count pruned."""
101
+ resp = self._http.delete("/api/v1/sandboxes")
102
+ handle_response(resp)
103
+ return resp.json().get("pruned", 0)
104
+
105
+ def pool_status(self) -> dict:
106
+ """Get VM pool status."""
107
+ resp = self._http.get("/api/v1/pool/status")
108
+ handle_response(resp)
109
+ return resp.json()
110
+
111
+ def health(self) -> dict:
112
+ """Check server health."""
113
+ resp = self._http.get("/api/v1/health")
114
+ handle_response(resp)
115
+ return resp.json()
116
+
117
+ def close(self) -> None:
118
+ """Close the HTTP client."""
119
+ self._http.close()
120
+
121
+ def __enter__(self):
122
+ return self
123
+
124
+ def __exit__(self, *args):
125
+ self.close()
@@ -0,0 +1,56 @@
1
+ """ForgeVM SDK exceptions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import httpx
6
+
7
+
8
+ class ForgevmError(Exception):
9
+ """Base exception for ForgeVM SDK."""
10
+
11
+ def __init__(self, message: str, code: str | None = None):
12
+ super().__init__(message)
13
+ self.code = code
14
+
15
+
16
+ class SandboxNotFound(ForgevmError):
17
+ """Raised when a sandbox is not found."""
18
+
19
+ def __init__(self, sandbox_id: str):
20
+ super().__init__(f"Sandbox {sandbox_id!r} not found", code="NOT_FOUND")
21
+ self.sandbox_id = sandbox_id
22
+
23
+
24
+ class ProviderError(ForgevmError):
25
+ """Raised when a provider operation fails."""
26
+
27
+ pass
28
+
29
+
30
+ class ConnectionError(ForgevmError):
31
+ """Raised when the connection to the ForgeVM server fails."""
32
+
33
+ pass
34
+
35
+
36
+ def handle_response(response: httpx.Response) -> None:
37
+ """Check response status and raise appropriate exceptions."""
38
+ if response.is_success:
39
+ return
40
+
41
+ try:
42
+ data = response.json()
43
+ code = data.get("code", "")
44
+ message = data.get("message", response.text)
45
+ except Exception:
46
+ code = ""
47
+ message = response.text
48
+
49
+ if response.status_code == 404:
50
+ raise SandboxNotFound(message)
51
+ if response.status_code == 401:
52
+ raise ForgevmError(message, code="UNAUTHORIZED")
53
+ if response.status_code >= 500:
54
+ raise ProviderError(message, code=code)
55
+
56
+ raise ForgevmError(message, code=code)