wbxsandbox 0.15.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 KXCOMM s.r.o.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: wbxsandbox
3
+ Version: 0.15.2
4
+ Summary: Python SDK for WBX Sandbox – isolated cloud sandboxes (Firecracker microVMs) for AI agents
5
+ Author: KXCOMM s.r.o.
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://sandbox.wbx.cz
8
+ Project-URL: Documentation, https://sandbox.wbx.cz/docs
9
+ Project-URL: Pricing, https://sandbox.wbx.cz/pricing
10
+ Keywords: sandbox,firecracker,microvm,ai agents,code execution,wbx
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Classifier: Topic :: System :: Distributed Computing
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Dynamic: license-file
20
+
21
+ # WBX Sandbox – Python SDK
22
+
23
+ Python SDK for [WBX Sandbox](https://sandbox.wbx.cz): isolated cloud sandboxes for AI agents
24
+ and code execution. Each sandbox is a Firecracker microVM with its own Linux kernel, disk and
25
+ network rules. No dependencies – only the Python standard library (Python 3.9+).
26
+
27
+ ## Install
28
+
29
+ pip install wbxsandbox
30
+
31
+ ## Configure
32
+
33
+ Create an API key in the portal (API keys) and set it in the environment:
34
+
35
+ export WBXSANDBOX_API_KEY=...
36
+ # optional, defaults to https://sandbox.wbx.cz
37
+ export WBXSANDBOX_URL=https://sandbox.wbx.cz
38
+
39
+ Both can also be passed as `url=` and `api_key=` arguments.
40
+
41
+ ## Quick start
42
+
43
+ ```python
44
+ from wbxsandbox import Sandbox
45
+
46
+ with Sandbox.create(template="base", timeout=300) as sb:
47
+ r = sb.run("python3 -c 'print(1 + 1)'")
48
+ print(r.exit_code, r.stdout) # 0 "2\n"
49
+ sb.files.write("/home/user/data.txt", "hello")
50
+ print(sb.files.read_text("/home/user/data.txt"))
51
+ # the sandbox is destroyed when the block ends
52
+ ```
53
+
54
+ Background jobs and batches:
55
+
56
+ ```python
57
+ from wbxsandbox import run_job
58
+
59
+ job = run_job(command="python3 -c 'print(42)'", template="base", timeout_seconds=600)
60
+ job.wait()
61
+ print(job.summary()["status"])
62
+ ```
63
+
64
+ Interactive terminal in a running sandbox (installed as the `wbx` command):
65
+
66
+ wbx connect sb_...
67
+
68
+ Documentation: https://sandbox.wbx.cz/docs · Pricing: https://sandbox.wbx.cz/pricing
69
+
70
+ ## Versioning
71
+
72
+ The SDK has its own version. Its `major.minor` follows the platform release line
73
+ (SDK 0.15.x works with platform 0.15.x); the patch number is raised only when the SDK
74
+ itself changes. `pyproject.toml` and `wbxsandbox.__version__` always carry the same
75
+ version (checked by `tests/test_sdk.py`).
76
+
77
+ ## Tests
78
+
79
+ cd sdk/python && python3 -m unittest discover -s tests
80
+
81
+ The tests do not need network access.
82
+
83
+ ## License
84
+
85
+ MIT – see `LICENSE`.
@@ -0,0 +1,65 @@
1
+ # WBX Sandbox – Python SDK
2
+
3
+ Python SDK for [WBX Sandbox](https://sandbox.wbx.cz): isolated cloud sandboxes for AI agents
4
+ and code execution. Each sandbox is a Firecracker microVM with its own Linux kernel, disk and
5
+ network rules. No dependencies – only the Python standard library (Python 3.9+).
6
+
7
+ ## Install
8
+
9
+ pip install wbxsandbox
10
+
11
+ ## Configure
12
+
13
+ Create an API key in the portal (API keys) and set it in the environment:
14
+
15
+ export WBXSANDBOX_API_KEY=...
16
+ # optional, defaults to https://sandbox.wbx.cz
17
+ export WBXSANDBOX_URL=https://sandbox.wbx.cz
18
+
19
+ Both can also be passed as `url=` and `api_key=` arguments.
20
+
21
+ ## Quick start
22
+
23
+ ```python
24
+ from wbxsandbox import Sandbox
25
+
26
+ with Sandbox.create(template="base", timeout=300) as sb:
27
+ r = sb.run("python3 -c 'print(1 + 1)'")
28
+ print(r.exit_code, r.stdout) # 0 "2\n"
29
+ sb.files.write("/home/user/data.txt", "hello")
30
+ print(sb.files.read_text("/home/user/data.txt"))
31
+ # the sandbox is destroyed when the block ends
32
+ ```
33
+
34
+ Background jobs and batches:
35
+
36
+ ```python
37
+ from wbxsandbox import run_job
38
+
39
+ job = run_job(command="python3 -c 'print(42)'", template="base", timeout_seconds=600)
40
+ job.wait()
41
+ print(job.summary()["status"])
42
+ ```
43
+
44
+ Interactive terminal in a running sandbox (installed as the `wbx` command):
45
+
46
+ wbx connect sb_...
47
+
48
+ Documentation: https://sandbox.wbx.cz/docs · Pricing: https://sandbox.wbx.cz/pricing
49
+
50
+ ## Versioning
51
+
52
+ The SDK has its own version. Its `major.minor` follows the platform release line
53
+ (SDK 0.15.x works with platform 0.15.x); the patch number is raised only when the SDK
54
+ itself changes. `pyproject.toml` and `wbxsandbox.__version__` always carry the same
55
+ version (checked by `tests/test_sdk.py`).
56
+
57
+ ## Tests
58
+
59
+ cd sdk/python && python3 -m unittest discover -s tests
60
+
61
+ The tests do not need network access.
62
+
63
+ ## License
64
+
65
+ MIT – see `LICENSE`.
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "wbxsandbox"
7
+ version = "0.15.2"
8
+ description = "Python SDK for WBX Sandbox – isolated cloud sandboxes (Firecracker microVMs) for AI agents"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ dependencies = []
12
+ license = "MIT"
13
+ authors = [{ name = "KXCOMM s.r.o." }]
14
+ keywords = ["sandbox", "firecracker", "microvm", "ai agents", "code execution", "wbx"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Software Development :: Libraries :: Python Modules",
20
+ "Topic :: System :: Distributed Computing",
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://sandbox.wbx.cz"
25
+ Documentation = "https://sandbox.wbx.cz/docs"
26
+ Pricing = "https://sandbox.wbx.cz/pricing"
27
+
28
+ [project.scripts]
29
+ wbx = "wbxsandbox.cli:main"
30
+
31
+ [tool.setuptools]
32
+ packages = ["wbxsandbox"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,128 @@
1
+ """Testy Python SDK bez sítě: verze, podpis webhooku, rozpočet (měna účtu, starší názvy parametrů).
2
+
3
+ Spuštění: cd sdk/python && python3 -m unittest discover -s tests
4
+ """
5
+ import hashlib
6
+ import hmac
7
+ import os
8
+ import re
9
+ import sys
10
+ import time
11
+ import unittest
12
+ from unittest import mock
13
+
14
+ HERE = os.path.dirname(os.path.abspath(__file__))
15
+ SDK = os.path.dirname(HERE)
16
+ sys.path.insert(0, SDK)
17
+
18
+ import wbxsandbox # noqa: E402
19
+
20
+
21
+ def _read(*parts):
22
+ with open(os.path.join(*parts), encoding="utf-8") as f:
23
+ return f.read()
24
+
25
+
26
+ class VersionTest(unittest.TestCase):
27
+ def test_pyproject_matches_package(self):
28
+ m = re.search(r'^version\s*=\s*"([^"]+)"', _read(SDK, "pyproject.toml"), re.M)
29
+ self.assertIsNotNone(m)
30
+ self.assertEqual(m.group(1), wbxsandbox.__version__)
31
+
32
+ def test_same_major_minor_as_platform(self):
33
+ # SDK má vlastní patch verzi, ale řada major.minor odpovídá platformě. Verze platformy:
34
+ # RELEASE ve vydaném balíku (src/sdk/python i sdk/python), v repozitáři Makefile.
35
+ # Bez metadat platformy test selže – nesmí se tiše přeskočit.
36
+ version = platform_version()
37
+ self.assertIsNotNone(version, "platform version not found (RELEASE in the bundle or Makefile in the repository)")
38
+ self.assertEqual(version.split(".")[:2], wbxsandbox.__version__.split(".")[:2])
39
+
40
+
41
+ def platform_version():
42
+ """Verze platformy z RELEASE (balík) nebo z Makefile (repozitář); None = nenalezeno."""
43
+ for rel in (os.path.join(SDK, "..", "..", "RELEASE"), os.path.join(SDK, "..", "..", "..", "RELEASE")):
44
+ if os.path.exists(rel):
45
+ m = re.search(r"^version:\s*(\S+)", _read(rel), re.M)
46
+ if m:
47
+ return m.group(1)
48
+ mk = os.path.join(SDK, "..", "..", "Makefile")
49
+ if os.path.exists(mk):
50
+ m = re.search(r"^VERSION\s*:=\s*(\S+)", _read(mk), re.M)
51
+ if m:
52
+ return m.group(1)
53
+ return None
54
+
55
+
56
+ class PlatformVersionLookupTest(unittest.TestCase):
57
+ def test_reads_release_then_makefile_and_reports_missing(self):
58
+ import tempfile
59
+ global SDK
60
+ orig = SDK
61
+ try:
62
+ with tempfile.TemporaryDirectory() as d:
63
+ SDK = os.path.join(d, "src", "sdk", "python")
64
+ os.makedirs(SDK)
65
+ self.assertIsNone(platform_version()) # nic → None (test výše pak selže)
66
+ with open(os.path.join(d, "RELEASE"), "w") as f:
67
+ f.write("version: 9.8.7\ncommit: x\n")
68
+ self.assertEqual(platform_version(), "9.8.7") # src/sdk/python → ../../../RELEASE
69
+ with tempfile.TemporaryDirectory() as d:
70
+ SDK = os.path.join(d, "sdk", "python")
71
+ os.makedirs(SDK)
72
+ with open(os.path.join(d, "Makefile"), "w") as f:
73
+ f.write("VERSION := 1.2.3\n")
74
+ self.assertEqual(platform_version(), "1.2.3")
75
+ with open(os.path.join(d, "RELEASE"), "w") as f:
76
+ f.write("version: 1.2.4\n")
77
+ self.assertEqual(platform_version(), "1.2.4") # RELEASE má přednost
78
+ finally:
79
+ SDK = orig
80
+
81
+
82
+ class WebhookTest(unittest.TestCase):
83
+ def sign(self, secret, t, body):
84
+ return "t=%d,v1=%s" % (t, hmac.new(secret.encode(), b"%d." % t + body, hashlib.sha256).hexdigest())
85
+
86
+ def test_valid_tampered_and_old(self):
87
+ body = b'{"event":"job.finished","id":"job_1"}'
88
+ now = int(time.time())
89
+ sig = self.sign("whsec_x", now, body)
90
+ self.assertTrue(wbxsandbox.verify_webhook("whsec_x", body, sig))
91
+ self.assertFalse(wbxsandbox.verify_webhook("whsec_x", body + b" ", sig))
92
+ self.assertFalse(wbxsandbox.verify_webhook("whsec_y", body, sig))
93
+ self.assertFalse(wbxsandbox.verify_webhook("whsec_x", body, self.sign("whsec_x", now - 3600, body)))
94
+ self.assertFalse(wbxsandbox.verify_webhook("whsec_x", body, "garbage"))
95
+
96
+
97
+ class BudgetTest(unittest.TestCase):
98
+ def call(self, **kw):
99
+ with mock.patch.object(wbxsandbox.Client, "request", return_value={"currency": "CZK"}) as req:
100
+ wbxsandbox.set_budget(url="https://x", api_key="k", **kw)
101
+ return req.call_args
102
+
103
+ def test_limits_in_billing_currency(self):
104
+ args = self.call(monthly="500", daily=0, alert_pct=90)
105
+ self.assertEqual(args.args[:2], ("PUT", "/v1/budget"))
106
+ self.assertEqual(args.kwargs["body"], {"monthly_limit": "500", "daily_limit": "0", "alert_pct": 90})
107
+
108
+ def test_old_parameter_names_still_work(self):
109
+ args = self.call(monthly_eur="12.5", daily_eur="")
110
+ self.assertEqual(args.kwargs["body"], {"monthly_limit": "12.5", "daily_limit": ""})
111
+ # kladně: starší pořadí pozičních argumentů (měsíc, den) zůstává
112
+ with mock.patch.object(wbxsandbox.Client, "request", return_value={}) as req:
113
+ wbxsandbox.set_budget("7", "1", url="https://x", api_key="k")
114
+ self.assertEqual(req.call_args.kwargs["body"], {"monthly_limit": "7", "daily_limit": "1"})
115
+
116
+
117
+ class PublicURLTest(unittest.TestCase):
118
+ def test_get_url(self):
119
+ c = wbxsandbox.Client(url="https://x", api_key="k")
120
+ sb = wbxsandbox.Sandbox(id="sb_n1_0123456789abcdef", template="base", state="running", _c=c)
121
+ with mock.patch.object(wbxsandbox.Client, "request", return_value={"url": "https://u/"}) as req:
122
+ self.assertEqual(sb.get_url("8080")["url"], "https://u/")
123
+ self.assertEqual(req.call_args.args, ("GET", "/v1/sandboxes/sb_n1_0123456789abcdef/url"))
124
+ self.assertEqual(req.call_args.kwargs["query"], {"port": 8080})
125
+
126
+
127
+ if __name__ == "__main__":
128
+ unittest.main()
@@ -0,0 +1,107 @@
1
+ """Terminál (wbx connect): adresa WebSocketu a minimální klient WebSocketu proti lokálnímu serveru."""
2
+ import base64
3
+ import hashlib
4
+ import os
5
+ import socket
6
+ import struct
7
+ import sys
8
+ import threading
9
+ import unittest
10
+
11
+ HERE = os.path.dirname(os.path.abspath(__file__))
12
+ sys.path.insert(0, os.path.dirname(HERE))
13
+
14
+ from wbxsandbox import terminal # noqa: E402
15
+ from wbxsandbox.cli import main # noqa: E402
16
+
17
+
18
+ def _server(handler):
19
+ srv = socket.socket()
20
+ srv.bind(("127.0.0.1", 0))
21
+ srv.listen(1)
22
+
23
+ def run():
24
+ c, _ = srv.accept()
25
+ req = b""
26
+ while b"\r\n\r\n" not in req:
27
+ req += c.recv(4096)
28
+ handler(c, req.decode())
29
+ c.close()
30
+ srv.close()
31
+
32
+ threading.Thread(target=run, daemon=True).start()
33
+ return srv.getsockname()[1]
34
+
35
+
36
+ def _frame(opcode, payload):
37
+ return bytes([0x80 | opcode, len(payload)]) + payload
38
+
39
+
40
+ def _read_client_frame(c):
41
+ b0, b1 = c.recv(2)
42
+ n = b1 & 0x7F
43
+ if n == 126:
44
+ n = struct.unpack("!H", c.recv(2))[0]
45
+ mask = c.recv(4)
46
+ data = b""
47
+ while len(data) < n:
48
+ data += c.recv(n - len(data))
49
+ return b0 & 0x0F, bytes(b ^ mask[i % 4] for i, b in enumerate(data))
50
+
51
+
52
+ class TerminalTest(unittest.TestCase):
53
+ def test_url(self):
54
+ self.assertEqual(terminal.terminal_url("https://sandbox.wbx.cz/", "sb_n01_x", 80, 24),
55
+ "wss://sandbox.wbx.cz/v1/sandboxes/sb_n01_x/terminal?cols=80&rows=24")
56
+ self.assertEqual(terminal.terminal_url("http://10.0.0.1:3000", "sb_n01_x", 1, 2),
57
+ "ws://10.0.0.1:3000/v1/sandboxes/sb_n01_x/terminal?cols=1&rows=2")
58
+
59
+ def test_websocket_roundtrip(self):
60
+ got = {}
61
+
62
+ def handler(c, req):
63
+ got["req"] = req
64
+ key = [l.split(": ", 1)[1] for l in req.split("\r\n") if l.lower().startswith("sec-websocket-key")][0]
65
+ acc = base64.b64encode(hashlib.sha1((key + terminal._GUID).encode()).digest()).decode()
66
+ c.sendall(("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n"
67
+ f"Sec-WebSocket-Accept: {acc}\r\n\r\n").encode())
68
+ c.sendall(_frame(0x9, b"hi")) # ping → klient odpoví pong
69
+ op, data = _read_client_frame(c)
70
+ got["pong"] = (op, data)
71
+ c.sendall(bytes([0x01, 3]) + b'{"a' + _frame(0x0, b'":1}')) # fragmentovaná zpráva
72
+ op, data = _read_client_frame(c)
73
+ got["sent"] = (op, data)
74
+ c.sendall(_frame(0x8, struct.pack("!H", 1000)))
75
+ _read_client_frame(c)
76
+
77
+ port = _server(handler)
78
+ ws = terminal.WebSocket(f"ws://127.0.0.1:{port}/v1/sandboxes/sb/terminal?cols=1", {"X-API-Key": "k"})
79
+ self.assertEqual(ws.recv_text(), '{"a":1}')
80
+ ws.send_text("x" * 300) # délka přes 126 bajtů
81
+ self.assertIsNone(ws.recv_text())
82
+ ws.close()
83
+ self.assertIn("GET /v1/sandboxes/sb/terminal?cols=1 HTTP/1.1", got["req"])
84
+ self.assertIn("X-API-Key: k", got["req"])
85
+ self.assertEqual(got["pong"], (0xA, b"hi"))
86
+ self.assertEqual(got["sent"], (0x1, b"x" * 300))
87
+
88
+ def test_http_error_is_reported(self):
89
+ def handler(c, req):
90
+ c.sendall(b'HTTP/1.1 404 Not Found\r\nContent-Type: application/json\r\n\r\n{"error":"sandbox not found"}')
91
+
92
+ port = _server(handler)
93
+ with self.assertRaises(terminal.WebSocketError) as e:
94
+ terminal.WebSocket(f"ws://127.0.0.1:{port}/x", {})
95
+ self.assertIn("sandbox not found", str(e.exception))
96
+
97
+ def test_cli_requires_key(self):
98
+ env = os.environ.pop("WBXSANDBOX_API_KEY", None)
99
+ try:
100
+ self.assertEqual(main(["connect", "sb_x", "--url", "http://127.0.0.1:1"]), 2)
101
+ finally:
102
+ if env is not None:
103
+ os.environ["WBXSANDBOX_API_KEY"] = env
104
+
105
+
106
+ if __name__ == "__main__":
107
+ unittest.main()