forkd 0.1.2__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.
- forkd-0.1.2/PKG-INFO +63 -0
- forkd-0.1.2/README.md +50 -0
- forkd-0.1.2/forkd/__init__.py +10 -0
- forkd-0.1.2/forkd/sandbox.py +190 -0
- forkd-0.1.2/forkd.egg-info/PKG-INFO +63 -0
- forkd-0.1.2/forkd.egg-info/SOURCES.txt +8 -0
- forkd-0.1.2/forkd.egg-info/dependency_links.txt +1 -0
- forkd-0.1.2/forkd.egg-info/top_level.txt +1 -0
- forkd-0.1.2/pyproject.toml +21 -0
- forkd-0.1.2/setup.cfg +4 -0
forkd-0.1.2/PKG-INFO
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: forkd
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Open-source fork-on-write microVM sandbox primitive (E2B-compatible surface)
|
|
5
|
+
Author-email: Deeplethe <info@deeplethe.com>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/deeplethe/forkd
|
|
8
|
+
Project-URL: Source, https://github.com/deeplethe/forkd
|
|
9
|
+
Project-URL: Issues, https://github.com/deeplethe/forkd/issues
|
|
10
|
+
Keywords: sandbox,firecracker,microvm,fork,ai-agents
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# forkd — Python SDK
|
|
15
|
+
|
|
16
|
+
E2B-compatible sandbox API backed by [forkd](https://github.com/deeplethe/forkd).
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from forkd import Sandbox
|
|
20
|
+
|
|
21
|
+
with Sandbox() as sandbox:
|
|
22
|
+
result = sandbox.commands.run("python3 -c 'import numpy; print(numpy.zeros(3))'")
|
|
23
|
+
print(result.stdout) # [0. 0. 0.]
|
|
24
|
+
print(result.exit_code) # 0
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or with explicit lifecycle:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
sandbox = Sandbox()
|
|
31
|
+
result = sandbox.commands.run("echo hello")
|
|
32
|
+
sandbox.kill()
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Bonus: warmed-state eval
|
|
36
|
+
|
|
37
|
+
If your snapshot parent imported numpy, you can skip subprocess overhead
|
|
38
|
+
and use the warmed PID-1 interpreter directly:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
with Sandbox() as sandbox:
|
|
42
|
+
out = sandbox.eval("numpy.zeros(5).tolist()") # ~8 ms
|
|
43
|
+
# vs commands.run("python3 -c '...'") which is ~108 ms (fresh subprocess)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Requirements
|
|
47
|
+
|
|
48
|
+
The `forkd` Rust CLI must be installed and on `PATH`, plus a parent snapshot
|
|
49
|
+
must already exist (`forkd snapshot --tag pyagent ...`). See the main
|
|
50
|
+
[README](https://github.com/deeplethe/forkd) for the full setup.
|
|
51
|
+
|
|
52
|
+
## Status
|
|
53
|
+
|
|
54
|
+
Pre-alpha. Currently supports:
|
|
55
|
+
- `Sandbox()` / `Sandbox.create()` — spawn one sandbox
|
|
56
|
+
- `sandbox.commands.run(cmd)` — run command, get stdout/stderr/exit_code
|
|
57
|
+
- `sandbox.eval(expr)` — eval Python in warmed PID 1
|
|
58
|
+
- `sandbox.kill()` — terminate
|
|
59
|
+
|
|
60
|
+
Not yet (blocked on issues #1 + #4):
|
|
61
|
+
- Multiple concurrent `Sandbox()` instances (all share parent's MAC/IP)
|
|
62
|
+
- `sandbox.files.read/write` — filesystem operations
|
|
63
|
+
- Streaming output
|
forkd-0.1.2/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# forkd — Python SDK
|
|
2
|
+
|
|
3
|
+
E2B-compatible sandbox API backed by [forkd](https://github.com/deeplethe/forkd).
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from forkd import Sandbox
|
|
7
|
+
|
|
8
|
+
with Sandbox() as sandbox:
|
|
9
|
+
result = sandbox.commands.run("python3 -c 'import numpy; print(numpy.zeros(3))'")
|
|
10
|
+
print(result.stdout) # [0. 0. 0.]
|
|
11
|
+
print(result.exit_code) # 0
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Or with explicit lifecycle:
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
sandbox = Sandbox()
|
|
18
|
+
result = sandbox.commands.run("echo hello")
|
|
19
|
+
sandbox.kill()
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Bonus: warmed-state eval
|
|
23
|
+
|
|
24
|
+
If your snapshot parent imported numpy, you can skip subprocess overhead
|
|
25
|
+
and use the warmed PID-1 interpreter directly:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
with Sandbox() as sandbox:
|
|
29
|
+
out = sandbox.eval("numpy.zeros(5).tolist()") # ~8 ms
|
|
30
|
+
# vs commands.run("python3 -c '...'") which is ~108 ms (fresh subprocess)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Requirements
|
|
34
|
+
|
|
35
|
+
The `forkd` Rust CLI must be installed and on `PATH`, plus a parent snapshot
|
|
36
|
+
must already exist (`forkd snapshot --tag pyagent ...`). See the main
|
|
37
|
+
[README](https://github.com/deeplethe/forkd) for the full setup.
|
|
38
|
+
|
|
39
|
+
## Status
|
|
40
|
+
|
|
41
|
+
Pre-alpha. Currently supports:
|
|
42
|
+
- `Sandbox()` / `Sandbox.create()` — spawn one sandbox
|
|
43
|
+
- `sandbox.commands.run(cmd)` — run command, get stdout/stderr/exit_code
|
|
44
|
+
- `sandbox.eval(expr)` — eval Python in warmed PID 1
|
|
45
|
+
- `sandbox.kill()` — terminate
|
|
46
|
+
|
|
47
|
+
Not yet (blocked on issues #1 + #4):
|
|
48
|
+
- Multiple concurrent `Sandbox()` instances (all share parent's MAC/IP)
|
|
49
|
+
- `sandbox.files.read/write` — filesystem operations
|
|
50
|
+
- Streaming output
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""forkd — open-source fork-on-write microVM sandbox primitive.
|
|
2
|
+
|
|
3
|
+
This Python SDK provides an E2B-compatible Sandbox API. Under the hood,
|
|
4
|
+
each Sandbox is a forked microVM child managed by the `forkd` Rust CLI.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .sandbox import CommandResult, Sandbox
|
|
8
|
+
|
|
9
|
+
__version__ = "0.0.1"
|
|
10
|
+
__all__ = ["Sandbox", "CommandResult"]
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""E2B-compatible Sandbox wrapper around forkd's guest agent."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import socket
|
|
9
|
+
import subprocess
|
|
10
|
+
import time
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Optional, Sequence, Union
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class CommandResult:
|
|
17
|
+
"""Result of `sandbox.commands.run(...)`. Mirrors E2B's API."""
|
|
18
|
+
|
|
19
|
+
stdout: str
|
|
20
|
+
stderr: str
|
|
21
|
+
exit_code: int
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class _CommandsProxy:
|
|
25
|
+
"""Implements the `sandbox.commands` namespace from E2B's API."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, sandbox: "Sandbox") -> None:
|
|
28
|
+
self._sandbox = sandbox
|
|
29
|
+
|
|
30
|
+
def run(
|
|
31
|
+
self,
|
|
32
|
+
cmd: Union[str, Sequence[str]],
|
|
33
|
+
timeout: int = 30,
|
|
34
|
+
) -> CommandResult:
|
|
35
|
+
"""Run a command inside the sandbox and return its output.
|
|
36
|
+
|
|
37
|
+
`cmd` can be a string (executed via `sh -c`) or a list/tuple of
|
|
38
|
+
argv tokens (executed directly).
|
|
39
|
+
"""
|
|
40
|
+
if isinstance(cmd, str):
|
|
41
|
+
args = ["/bin/sh", "-c", cmd]
|
|
42
|
+
else:
|
|
43
|
+
args = list(cmd)
|
|
44
|
+
resp = self._sandbox._send({"action": "exec", "args": args, "timeout": timeout})
|
|
45
|
+
if "error" in resp and "stdout" not in resp:
|
|
46
|
+
return CommandResult(stdout="", stderr=resp["error"], exit_code=1)
|
|
47
|
+
return CommandResult(
|
|
48
|
+
stdout=resp.get("stdout", ""),
|
|
49
|
+
stderr=resp.get("stderr", ""),
|
|
50
|
+
exit_code=int(resp.get("exit_code", -1)),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Sandbox:
|
|
55
|
+
"""Open one forked microVM sandbox, E2B-compatible surface.
|
|
56
|
+
|
|
57
|
+
Example
|
|
58
|
+
-------
|
|
59
|
+
>>> with Sandbox() as sb:
|
|
60
|
+
... print(sb.commands.run("echo hi").stdout)
|
|
61
|
+
hi
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
DEFAULT_TAG = os.environ.get("FORKD_TAG", "pyagent")
|
|
65
|
+
DEFAULT_TARGET = os.environ.get("FORKD_TARGET", "10.42.0.2:8888")
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
tag: Optional[str] = None,
|
|
70
|
+
target: Optional[str] = None,
|
|
71
|
+
timeout: int = 30,
|
|
72
|
+
*,
|
|
73
|
+
spawn: bool = True,
|
|
74
|
+
) -> None:
|
|
75
|
+
self.tag = tag or self.DEFAULT_TAG
|
|
76
|
+
self.target = target or self.DEFAULT_TARGET
|
|
77
|
+
self.timeout = timeout
|
|
78
|
+
self.commands = _CommandsProxy(self)
|
|
79
|
+
self._fork_proc: Optional[subprocess.Popen] = None
|
|
80
|
+
if spawn:
|
|
81
|
+
self._spawn()
|
|
82
|
+
|
|
83
|
+
# ----- public API -----------------------------------------------------
|
|
84
|
+
|
|
85
|
+
def eval(self, code: str) -> object:
|
|
86
|
+
"""Evaluate a Python expression against the warmed PID-1 interpreter.
|
|
87
|
+
|
|
88
|
+
This is *not* part of E2B's API but is the killer move of forkd:
|
|
89
|
+
the parent VM's Python interpreter already imported numpy etc.,
|
|
90
|
+
so simple `eval` calls return in single-digit milliseconds vs
|
|
91
|
+
~100 ms for a fresh `python3 -c "..."` subprocess.
|
|
92
|
+
"""
|
|
93
|
+
resp = self._send({"action": "eval", "code": code})
|
|
94
|
+
if "error" in resp:
|
|
95
|
+
raise RuntimeError(f"forkd eval: {resp['error']}")
|
|
96
|
+
return resp.get("result")
|
|
97
|
+
|
|
98
|
+
def ping(self) -> dict:
|
|
99
|
+
"""Probe the guest agent. Returns dict with 'pong' and 'numpy_version'."""
|
|
100
|
+
return self._send({"action": "ping"})
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def create(cls, *args, **kwargs) -> "Sandbox":
|
|
104
|
+
"""Alias for `Sandbox(...)` matching E2B's `Sandbox.create()` style."""
|
|
105
|
+
return cls(*args, **kwargs)
|
|
106
|
+
|
|
107
|
+
def kill(self) -> None:
|
|
108
|
+
"""Terminate the underlying forked microVM."""
|
|
109
|
+
if self._fork_proc is None:
|
|
110
|
+
return
|
|
111
|
+
self._fork_proc.terminate()
|
|
112
|
+
try:
|
|
113
|
+
self._fork_proc.wait(timeout=5)
|
|
114
|
+
except subprocess.TimeoutExpired:
|
|
115
|
+
self._fork_proc.kill()
|
|
116
|
+
self._fork_proc = None
|
|
117
|
+
|
|
118
|
+
# ----- context manager ------------------------------------------------
|
|
119
|
+
|
|
120
|
+
def __enter__(self) -> "Sandbox":
|
|
121
|
+
return self
|
|
122
|
+
|
|
123
|
+
def __exit__(self, *exc) -> None:
|
|
124
|
+
self.kill()
|
|
125
|
+
|
|
126
|
+
# ----- internals ------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
def _spawn(self) -> None:
|
|
129
|
+
if shutil.which("forkd") is None:
|
|
130
|
+
raise RuntimeError(
|
|
131
|
+
"the `forkd` Rust CLI must be on PATH. "
|
|
132
|
+
"Build it with `cargo build --release -p forkd-cli` and add "
|
|
133
|
+
"target/release to PATH."
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Capture stderr so we can show why a fork failed.
|
|
137
|
+
self._fork_proc = subprocess.Popen(
|
|
138
|
+
[
|
|
139
|
+
"forkd",
|
|
140
|
+
"fork",
|
|
141
|
+
"--tag",
|
|
142
|
+
self.tag,
|
|
143
|
+
"-n",
|
|
144
|
+
"1",
|
|
145
|
+
"--settle-secs",
|
|
146
|
+
"3600",
|
|
147
|
+
],
|
|
148
|
+
stdout=subprocess.DEVNULL,
|
|
149
|
+
stderr=subprocess.PIPE,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Poll the agent until it responds (~150 ms typical).
|
|
153
|
+
deadline = time.time() + 30
|
|
154
|
+
last_err: Optional[Exception] = None
|
|
155
|
+
while time.time() < deadline:
|
|
156
|
+
try:
|
|
157
|
+
self.ping()
|
|
158
|
+
return
|
|
159
|
+
except (OSError, socket.error) as e:
|
|
160
|
+
last_err = e
|
|
161
|
+
# If forkd itself died, bubble its stderr up — that's the
|
|
162
|
+
# actually useful information.
|
|
163
|
+
rc = self._fork_proc.poll() if self._fork_proc else None
|
|
164
|
+
if rc is not None and rc != 0:
|
|
165
|
+
err_bytes = self._fork_proc.stderr.read() if self._fork_proc.stderr else b""
|
|
166
|
+
raise RuntimeError(
|
|
167
|
+
f"forkd fork --tag {self.tag} exited with code {rc}:\n"
|
|
168
|
+
f"{err_bytes.decode(errors='replace')}"
|
|
169
|
+
)
|
|
170
|
+
time.sleep(0.1)
|
|
171
|
+
self.kill()
|
|
172
|
+
raise RuntimeError(
|
|
173
|
+
f"sandbox didn't come up at {self.target} within 30s "
|
|
174
|
+
f"(last error: {last_err})"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def _send(self, msg: dict) -> dict:
|
|
178
|
+
host, _, port_s = self.target.rpartition(":")
|
|
179
|
+
port = int(port_s)
|
|
180
|
+
with socket.create_connection((host, port), timeout=5) as s:
|
|
181
|
+
s.settimeout(self.timeout + 5)
|
|
182
|
+
s.sendall((json.dumps(msg) + "\n").encode())
|
|
183
|
+
s.shutdown(socket.SHUT_WR)
|
|
184
|
+
buf = bytearray()
|
|
185
|
+
while True:
|
|
186
|
+
chunk = s.recv(65536)
|
|
187
|
+
if not chunk:
|
|
188
|
+
break
|
|
189
|
+
buf.extend(chunk)
|
|
190
|
+
return json.loads(buf.decode())
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: forkd
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Open-source fork-on-write microVM sandbox primitive (E2B-compatible surface)
|
|
5
|
+
Author-email: Deeplethe <info@deeplethe.com>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/deeplethe/forkd
|
|
8
|
+
Project-URL: Source, https://github.com/deeplethe/forkd
|
|
9
|
+
Project-URL: Issues, https://github.com/deeplethe/forkd/issues
|
|
10
|
+
Keywords: sandbox,firecracker,microvm,fork,ai-agents
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# forkd — Python SDK
|
|
15
|
+
|
|
16
|
+
E2B-compatible sandbox API backed by [forkd](https://github.com/deeplethe/forkd).
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from forkd import Sandbox
|
|
20
|
+
|
|
21
|
+
with Sandbox() as sandbox:
|
|
22
|
+
result = sandbox.commands.run("python3 -c 'import numpy; print(numpy.zeros(3))'")
|
|
23
|
+
print(result.stdout) # [0. 0. 0.]
|
|
24
|
+
print(result.exit_code) # 0
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or with explicit lifecycle:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
sandbox = Sandbox()
|
|
31
|
+
result = sandbox.commands.run("echo hello")
|
|
32
|
+
sandbox.kill()
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Bonus: warmed-state eval
|
|
36
|
+
|
|
37
|
+
If your snapshot parent imported numpy, you can skip subprocess overhead
|
|
38
|
+
and use the warmed PID-1 interpreter directly:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
with Sandbox() as sandbox:
|
|
42
|
+
out = sandbox.eval("numpy.zeros(5).tolist()") # ~8 ms
|
|
43
|
+
# vs commands.run("python3 -c '...'") which is ~108 ms (fresh subprocess)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Requirements
|
|
47
|
+
|
|
48
|
+
The `forkd` Rust CLI must be installed and on `PATH`, plus a parent snapshot
|
|
49
|
+
must already exist (`forkd snapshot --tag pyagent ...`). See the main
|
|
50
|
+
[README](https://github.com/deeplethe/forkd) for the full setup.
|
|
51
|
+
|
|
52
|
+
## Status
|
|
53
|
+
|
|
54
|
+
Pre-alpha. Currently supports:
|
|
55
|
+
- `Sandbox()` / `Sandbox.create()` — spawn one sandbox
|
|
56
|
+
- `sandbox.commands.run(cmd)` — run command, get stdout/stderr/exit_code
|
|
57
|
+
- `sandbox.eval(expr)` — eval Python in warmed PID 1
|
|
58
|
+
- `sandbox.kill()` — terminate
|
|
59
|
+
|
|
60
|
+
Not yet (blocked on issues #1 + #4):
|
|
61
|
+
- Multiple concurrent `Sandbox()` instances (all share parent's MAC/IP)
|
|
62
|
+
- `sandbox.files.read/write` — filesystem operations
|
|
63
|
+
- Streaming output
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
forkd
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "forkd"
|
|
7
|
+
version = "0.1.2"
|
|
8
|
+
description = "Open-source fork-on-write microVM sandbox primitive (E2B-compatible surface)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{name = "Deeplethe", email = "info@deeplethe.com"}]
|
|
11
|
+
license = {text = "Apache-2.0"}
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
keywords = ["sandbox", "firecracker", "microvm", "fork", "ai-agents"]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Homepage = "https://github.com/deeplethe/forkd"
|
|
17
|
+
Source = "https://github.com/deeplethe/forkd"
|
|
18
|
+
Issues = "https://github.com/deeplethe/forkd/issues"
|
|
19
|
+
|
|
20
|
+
[tool.setuptools]
|
|
21
|
+
packages = ["forkd"]
|
forkd-0.1.2/setup.cfg
ADDED