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 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,8 @@
1
+ README.md
2
+ pyproject.toml
3
+ forkd/__init__.py
4
+ forkd/sandbox.py
5
+ forkd.egg-info/PKG-INFO
6
+ forkd.egg-info/SOURCES.txt
7
+ forkd.egg-info/dependency_links.txt
8
+ forkd.egg-info/top_level.txt
@@ -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
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+