veris-e2b 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.
- veris_e2b-0.1.0/PKG-INFO +331 -0
- veris_e2b-0.1.0/README.md +309 -0
- veris_e2b-0.1.0/pyproject.toml +72 -0
- veris_e2b-0.1.0/pyproject.toml.orig +59 -0
- veris_e2b-0.1.0/src/veris_e2b/__init__.py +71 -0
- veris_e2b-0.1.0/src/veris_e2b/_options.py +278 -0
- veris_e2b-0.1.0/src/veris_e2b/async_sandbox.py +360 -0
- veris_e2b-0.1.0/src/veris_e2b/control_plane.py +508 -0
- veris_e2b-0.1.0/src/veris_e2b/errors.py +83 -0
- veris_e2b-0.1.0/src/veris_e2b/network.py +155 -0
- veris_e2b-0.1.0/src/veris_e2b/py.typed +0 -0
- veris_e2b-0.1.0/src/veris_e2b/receipt.py +393 -0
- veris_e2b-0.1.0/src/veris_e2b/run_receipt.py +254 -0
- veris_e2b-0.1.0/src/veris_e2b/sandbox.py +397 -0
- veris_e2b-0.1.0/src/veris_e2b/service_control.py +113 -0
- veris_e2b-0.1.0/src/veris_e2b/trust.py +92 -0
- veris_e2b-0.1.0/src/veris_e2b/veris_api.py +542 -0
- veris_e2b-0.1.0/src/veris_e2b/version.py +15 -0
veris_e2b-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: veris-e2b
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Veris dependency-sandbox interception for E2B: a drop-in Sandbox subclass whose vendor API calls are answered by stateful Veris twins. Unmodified code, real hostnames, receipts.
|
|
5
|
+
Keywords: veris,e2b,sandbox,integration-testing,agents,mock
|
|
6
|
+
Author: Veris AI
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Topic :: Software Development :: Testing
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Dist: e2b>=2.37,<3
|
|
16
|
+
Requires-Dist: httpx>=0.28.1,<1
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Project-URL: Homepage, https://github.com/veris-ai/veris-e2b
|
|
19
|
+
Project-URL: Repository, https://github.com/veris-ai/veris-e2b
|
|
20
|
+
Project-URL: Issues, https://github.com/veris-ai/veris-e2b/issues
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# Veris SDK for E2B — Python
|
|
24
|
+
|
|
25
|
+
Run your code in an [E2B](https://e2b.dev) sandbox where calls to
|
|
26
|
+
`api.stripe.com`, `www.googleapis.com`, and the rest of your vendor stack are
|
|
27
|
+
answered by **Veris dependency sandboxes** — stateful, contract-accurate mocks —
|
|
28
|
+
with the code under test completely unmodified.
|
|
29
|
+
|
|
30
|
+
No base-URL overrides, no injected config. Your code keeps its production
|
|
31
|
+
hostnames, credentials, and SDKs; the network layer does the rest.
|
|
32
|
+
|
|
33
|
+
This is the Python sibling of [`@veris-ai/e2b`](../e2b). Same control plane, same
|
|
34
|
+
contract, same option names in snake_case — see [differences](#differences-from-the-typescript-sdk).
|
|
35
|
+
|
|
36
|
+
## 1. Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv add veris-e2b # or: pip install veris-e2b
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
It depends on `e2b`, so that comes with it.
|
|
43
|
+
|
|
44
|
+
## 2. Get your keys
|
|
45
|
+
|
|
46
|
+
| Variable | Where from |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `E2B_API_KEY` | [e2b.dev/dashboard](https://e2b.dev/dashboard) |
|
|
49
|
+
| `VERIS_API_KEY` | your Veris dashboard |
|
|
50
|
+
| `VERIS_ENVIRONMENT_ID` | a Veris environment — it decides which vendor services your sandbox gets |
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
export E2B_API_KEY=e2b_…
|
|
54
|
+
export VERIS_API_KEY=…
|
|
55
|
+
export VERIS_ENVIRONMENT_ID=…
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 3. Run code against mocked vendors
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from veris_e2b import Sandbox
|
|
62
|
+
|
|
63
|
+
sbx = Sandbox.create()
|
|
64
|
+
|
|
65
|
+
# api.stripe.com is answered by your Veris mock — the code never knows.
|
|
66
|
+
result = sbx.commands.run("curl -sS https://api.stripe.com/v1/customers -u sk_test_veris:")
|
|
67
|
+
print(result.stdout)
|
|
68
|
+
|
|
69
|
+
sbx.kill()
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Inside an event loop, use `AsyncSandbox` — same contract, awaited:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from veris_e2b import AsyncSandbox
|
|
76
|
+
|
|
77
|
+
sbx = await AsyncSandbox.create()
|
|
78
|
+
await sbx.commands.run("curl -sS https://api.stripe.com/v1/customers -u sk_test_veris:")
|
|
79
|
+
await sbx.kill()
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 4. Check the receipt
|
|
83
|
+
|
|
84
|
+
A test suite that quietly stopped calling its dependency prints the same output
|
|
85
|
+
as one that works. The receipt is how you tell them apart:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from veris_e2b import TouchMatcher
|
|
89
|
+
|
|
90
|
+
# raises VerisUntouchedError unless the service actually saw a matching request
|
|
91
|
+
sbx.veris.assert_touched("stripe", TouchMatcher(method="POST", path="/v1/charges"))
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Creating a sandbox
|
|
95
|
+
|
|
96
|
+
Every E2B option still works; the Veris options live under one `veris` keyword so
|
|
97
|
+
a future e2b release cannot collide with them. Pass a `VerisOpts` or a plain dict.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from veris_e2b import Sandbox, VerisOpts
|
|
101
|
+
|
|
102
|
+
sbx = Sandbox.create(
|
|
103
|
+
"my-template", # any e2b template
|
|
104
|
+
timeout=15 * 60, # any e2b option, seconds
|
|
105
|
+
veris=VerisOpts(
|
|
106
|
+
environment_id="env_…", # default: VERIS_ENVIRONMENT_ID
|
|
107
|
+
api_key="…", # default: VERIS_API_KEY
|
|
108
|
+
api_base="https://svc.api.veris.ai", # default: VERIS_API_BASE
|
|
109
|
+
snapshot_id="snap_…", # boot the twin from a snapshot
|
|
110
|
+
attach_sandbox_id="sb_…", # or reuse an existing twin
|
|
111
|
+
egress="strict", # "strict" | "open"
|
|
112
|
+
allow_out=["pypi.org"], # extra hosts your code may reach
|
|
113
|
+
ttl_minutes=25, # twin lifetime; default: timeout + 10
|
|
114
|
+
install_ca=True, # trust the interception CA
|
|
115
|
+
data_plane_env=True, # inject DATABASE_URL etc.
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
| Option | Default | What it does |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `environment_id` | `VERIS_ENVIRONMENT_ID` | Which Veris environment the mocks come from — it decides which vendor services you get. |
|
|
123
|
+
| `api_key` | `VERIS_API_KEY` | Veris credential. |
|
|
124
|
+
| `api_base` | `VERIS_API_BASE` or `https://svc.api.veris.ai` | Control plane to talk to. |
|
|
125
|
+
| `snapshot_id` | — | Boot the twin from one of the environment's snapshots instead of its baseline, so every run starts from the same known state. Mutually exclusive with `attach_sandbox_id`. |
|
|
126
|
+
| `attach_sandbox_id` | — | Attach to an existing Veris sandbox instead of creating one. `kill()` will not delete it. |
|
|
127
|
+
| `ttl_minutes` | timeout + 10 | Backstop lifetime for the Veris sandbox, in case teardown never runs. |
|
|
128
|
+
| `egress` | `"strict"` | What may leave the sandbox — see [egress policy](#egress-policy). |
|
|
129
|
+
| `allow_out` | `[]` | Extra hosts or CIDRs your code may reach. A hostname is interceptable; a CIDR is passed through. |
|
|
130
|
+
| `install_ca` | `True` | Install the interception CA into the sandbox's trust stores. |
|
|
131
|
+
| `data_plane_env` | `True` | Inject non-HTTP connection strings (e.g. `DATABASE_URL`) as env. |
|
|
132
|
+
| `mode` | `"auto"` | `"auto"` and `"gateway"` both mean gateway mode here — see [differences](#differences-from-the-typescript-sdk). |
|
|
133
|
+
|
|
134
|
+
### Starting from a known state
|
|
135
|
+
|
|
136
|
+
A twin booted from the environment's baseline starts wherever that environment
|
|
137
|
+
starts. `snapshot_id` pins it to a snapshot you captured earlier, so a suite, a
|
|
138
|
+
benchmark, or a person exploring by hand all begin from the same rows:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
sbx = Sandbox.create(veris=VerisOpts(environment_id="env_…", snapshot_id="snap_…"))
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The snapshot must belong to that environment — the control plane refuses one that
|
|
145
|
+
does not, and the error names the snapshot rather than the environment. It is
|
|
146
|
+
recorded in the sandbox's E2B metadata as `veris_snapshot_id`, so a running
|
|
147
|
+
sandbox can always say what state it started from.
|
|
148
|
+
|
|
149
|
+
## The `sbx.veris` API
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
sbx.veris.receipt() # all services: counts + typed requests
|
|
153
|
+
sbx.veris.receipt("stripe") # one service
|
|
154
|
+
sbx.veris.receipt_baseline() # mark the log before a run
|
|
155
|
+
sbx.veris.receipt_since(baseline) # only what this run did
|
|
156
|
+
sbx.veris.assert_touched("stripe") # raises if it was never called
|
|
157
|
+
sbx.veris.control("stripe", "manual") # read a service's control resources
|
|
158
|
+
sbx.veris.services() # what's running in this twin
|
|
159
|
+
sbx.veris.get_data_plane_env() # {"DATABASE_URL": "postgresql://…"}
|
|
160
|
+
sbx.veris.get_trust_env() # CA paths, for processes that scrub env
|
|
161
|
+
sbx.veris.deliver_to(3000) # send webhooks to this sandbox
|
|
162
|
+
sbx.veris.update_network({...}) # change egress without losing interception
|
|
163
|
+
|
|
164
|
+
sbx.veris_sandbox_id # the Veris twin backing this sandbox
|
|
165
|
+
sbx.veris_mode # "gateway"
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`AsyncSandbox` exposes the same names, awaited.
|
|
169
|
+
|
|
170
|
+
### Receipts
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
receipt = sbx.veris.receipt()
|
|
174
|
+
receipt.services["stripe"].requests # 3
|
|
175
|
+
receipt.integrity # "verified" — the tunnel was re-proven just now
|
|
176
|
+
receipt.leaks # [] in strict mode
|
|
177
|
+
receipt.services["stripe"].capped # False — the whole log was read
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`integrity` is `"verified"` only when the canary probe confirmed egress is still
|
|
181
|
+
tunneled at read time. `leaks` names blind spots the current egress mode genuinely
|
|
182
|
+
has (`udp-quic-possible`, `ech-possible`) rather than implying a receipt sees
|
|
183
|
+
everything.
|
|
184
|
+
|
|
185
|
+
`capped` is the third thing to read. The log is paged, and a read that stops before
|
|
186
|
+
the log does reports a **floor**, not a count — `capped` is then `True` and
|
|
187
|
+
`incomplete_reason` says which limit it hit. `assert_touched` treats that as
|
|
188
|
+
insufficient evidence rather than as an untouched dependency, because "we could not
|
|
189
|
+
see them" and "it was never called" are different failures.
|
|
190
|
+
|
|
191
|
+
### Run-scoped receipts
|
|
192
|
+
|
|
193
|
+
A twin you **attached** to already has a log. Counting all of it credits your run
|
|
194
|
+
with traffic from before it began — so mark the log first, and read only past the
|
|
195
|
+
mark:
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
baseline = sbx.veris.receipt_baseline() # before the run
|
|
199
|
+
... # the run
|
|
200
|
+
receipt = sbx.veris.receipt_since(baseline)
|
|
201
|
+
receipt.services["stripe"].requests # this run's calls, and only these
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The baseline is anchored by a unique control request, so it survives a reset that
|
|
205
|
+
preserves numeric ids, and `receipt_since` revalidates it *after* reading — a reset
|
|
206
|
+
part-way through invalidates the whole measurement rather than half of it. It is
|
|
207
|
+
plain data (`baseline.to_dict()` / `ReceiptBaseline.from_dict()`), so a run can
|
|
208
|
+
outlive the process that started it.
|
|
209
|
+
|
|
210
|
+
### Reading and seeding a service by hand
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
sbx.veris.control("stripe", "manual") # how this twin behaves
|
|
214
|
+
sbx.veris.control("stripe", "schema") # its shape
|
|
215
|
+
sbx.veris.control("stripe", "data") # its seed state
|
|
216
|
+
sbx.veris.control("stripe", "data", method="PATCH", body={...}) # change it
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`manual`, `schema`, `operations`, `data` and `requests` are the whole surface, and
|
|
220
|
+
only `data` accepts a write — everything else describes the twin rather than its
|
|
221
|
+
contents. Lifecycle verbs are deliberately absent: you own the sandbox, not the
|
|
222
|
+
twin's existence.
|
|
223
|
+
|
|
224
|
+
### Webhooks
|
|
225
|
+
|
|
226
|
+
If your app *receives* callbacks, tell the mocks where to deliver them:
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
sbx = Sandbox.create(network={"allow_public_traffic": True})
|
|
230
|
+
sbx.commands.run("python app.py", background=True) # listening on :3000
|
|
231
|
+
|
|
232
|
+
sbx.veris.deliver_to(3000) # → https://3000-<id>.e2b.app
|
|
233
|
+
sbx.veris.deliver_to("https://my.tunnel.dev") # or your own URL
|
|
234
|
+
sbx.veris.deliver_to(None) # unregister
|
|
235
|
+
sbx.veris.deliver_to(3000, probe=False) # skip the reachability check
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`deliver_to` resolves the sandbox's own public URL — the address a vendor would
|
|
239
|
+
POST to in production — registers it with **every** mocked service in one call,
|
|
240
|
+
and verifies they can actually reach it before returning.
|
|
241
|
+
|
|
242
|
+
### Reattaching
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
sbx = Sandbox.reconnect("i7x2qk9d0v3mnbhs", api_key="…")
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
`reconnect` restores the whole Veris surface from the sandbox's metadata, re-asserts
|
|
249
|
+
egress in case a raw update dropped it, and re-proves the tunnel. It is named
|
|
250
|
+
`reconnect` rather than `connect` because e2b's `connect` is also an instance method
|
|
251
|
+
(resume *this* sandbox), and one name cannot mean two things.
|
|
252
|
+
|
|
253
|
+
## Egress policy
|
|
254
|
+
|
|
255
|
+
- **`egress="strict"`** (default) — only your vendor hosts, `allow_out` additions,
|
|
256
|
+
and data planes may leave the sandbox. QUIC/HTTP3 and ECH fail closed, so the
|
|
257
|
+
receipt has no known blind spots.
|
|
258
|
+
- **`egress="open"`** — everything may leave (pip, npm, GitHub work with no
|
|
259
|
+
configuration), at the cost of two blind spots the receipt annotates in `leaks`:
|
|
260
|
+
a QUIC or ECH client could reach a real vendor unseen.
|
|
261
|
+
|
|
262
|
+
To change egress later without losing interception, use `sbx.veris.update_network()`
|
|
263
|
+
rather than the raw e2b call — the raw one clears omitted fields and would drop
|
|
264
|
+
the interception config.
|
|
265
|
+
|
|
266
|
+
## Templates
|
|
267
|
+
|
|
268
|
+
Any E2B template works — pass it as the first argument. The image needs
|
|
269
|
+
`ca-certificates` (to trust the interception CA); a template without it raises
|
|
270
|
+
`TemplateUnsupportedError` rather than running half-configured.
|
|
271
|
+
|
|
272
|
+
## Errors
|
|
273
|
+
|
|
274
|
+
Every error subclasses `VerisError`, so one `except` separates Veris failures from
|
|
275
|
+
e2b's, and each carries a `phase` naming where it died.
|
|
276
|
+
|
|
277
|
+
| Error | When |
|
|
278
|
+
|---|---|
|
|
279
|
+
| `MissingCredentialsError` | A required key or environment id is absent — raised before any network call, naming the variable. |
|
|
280
|
+
| `VerisGatewayNotOfferedError` | The control plane does not offer gateway mode. |
|
|
281
|
+
| `VerisGatewayUnreachableError` | The gateway is down. |
|
|
282
|
+
| `ReceiptIntegrityError` | Interception could not be proven — a receipt read now would lie. |
|
|
283
|
+
| `VerisUntouchedError` | `assert_touched` found no matching requests. |
|
|
284
|
+
| `TwinExpiredError` | The Veris sandbox is gone (expired or deleted). |
|
|
285
|
+
| `TemplateUnsupportedError` | The template can't host the interception CA. |
|
|
286
|
+
| `UnsupportedOperationError` | An operation that would break the one-sandbox-one-twin invariant, e.g. `fork()`. |
|
|
287
|
+
|
|
288
|
+
## Differences from the TypeScript SDK
|
|
289
|
+
|
|
290
|
+
- **Gateway mode only.** The in-sandbox `proxy` fallback needs the veris-proxy
|
|
291
|
+
machinery that `@veris-ai/e2b` carries; `mode="proxy"` raises rather than
|
|
292
|
+
pretending. A control plane that does not offer the gateway is refused loudly,
|
|
293
|
+
not silently un-intercepted — use the TypeScript package there.
|
|
294
|
+
- **`reconnect`, not `connect`** — see [Reattaching](#reattaching).
|
|
295
|
+
- **snake_case options**, and `veris=` is a keyword argument rather than a key in
|
|
296
|
+
the options object.
|
|
297
|
+
- **Both sync and async**: `Sandbox` and `AsyncSandbox`, mirroring e2b's own pair.
|
|
298
|
+
|
|
299
|
+
## Limitations
|
|
300
|
+
|
|
301
|
+
- **`fork()` is not supported.** Forked sandboxes would share one twin and corrupt
|
|
302
|
+
each other's receipts, so it raises.
|
|
303
|
+
- **Clients that pin their own CA bundle** (some vendor SDKs ship one and ignore
|
|
304
|
+
the system trust store) must be pointed at `/etc/ssl/certs/ca-certificates.crt`.
|
|
305
|
+
- **HTTP/2 and WebSockets on mocked hosts** are not yet handled in gateway mode;
|
|
306
|
+
HTTP/1.1 over TLS is. Non-mocked hosts are unaffected.
|
|
307
|
+
|
|
308
|
+
## Development
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
uv sync
|
|
312
|
+
uv run pytest # unit tests — mocked, no account needed
|
|
313
|
+
uv run ruff check .
|
|
314
|
+
uv run ruff format .
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Releasing
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
uv version 0.2.0 # then uv lock, commit, PR, merge
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Then **Actions → release-python → Run workflow**. It builds, checks the artifacts
|
|
324
|
+
and publishes to PyPI over trusted publishing (OIDC, no token), then tags
|
|
325
|
+
`python-v0.2.0`. `dry_run: true` rehearses everything but the publish. Versions
|
|
326
|
+
are PEP 440 (`0.2.0rc1`, not `0.2.0-rc.1`), and this package versions separately
|
|
327
|
+
from the npm pair. Details in [CONTRIBUTING.md](../CONTRIBUTING.md#releasing-the-python-package).
|
|
328
|
+
|
|
329
|
+
## License
|
|
330
|
+
|
|
331
|
+
Apache-2.0
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# Veris SDK for E2B — Python
|
|
2
|
+
|
|
3
|
+
Run your code in an [E2B](https://e2b.dev) sandbox where calls to
|
|
4
|
+
`api.stripe.com`, `www.googleapis.com`, and the rest of your vendor stack are
|
|
5
|
+
answered by **Veris dependency sandboxes** — stateful, contract-accurate mocks —
|
|
6
|
+
with the code under test completely unmodified.
|
|
7
|
+
|
|
8
|
+
No base-URL overrides, no injected config. Your code keeps its production
|
|
9
|
+
hostnames, credentials, and SDKs; the network layer does the rest.
|
|
10
|
+
|
|
11
|
+
This is the Python sibling of [`@veris-ai/e2b`](../e2b). Same control plane, same
|
|
12
|
+
contract, same option names in snake_case — see [differences](#differences-from-the-typescript-sdk).
|
|
13
|
+
|
|
14
|
+
## 1. Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
uv add veris-e2b # or: pip install veris-e2b
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
It depends on `e2b`, so that comes with it.
|
|
21
|
+
|
|
22
|
+
## 2. Get your keys
|
|
23
|
+
|
|
24
|
+
| Variable | Where from |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `E2B_API_KEY` | [e2b.dev/dashboard](https://e2b.dev/dashboard) |
|
|
27
|
+
| `VERIS_API_KEY` | your Veris dashboard |
|
|
28
|
+
| `VERIS_ENVIRONMENT_ID` | a Veris environment — it decides which vendor services your sandbox gets |
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
export E2B_API_KEY=e2b_…
|
|
32
|
+
export VERIS_API_KEY=…
|
|
33
|
+
export VERIS_ENVIRONMENT_ID=…
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 3. Run code against mocked vendors
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from veris_e2b import Sandbox
|
|
40
|
+
|
|
41
|
+
sbx = Sandbox.create()
|
|
42
|
+
|
|
43
|
+
# api.stripe.com is answered by your Veris mock — the code never knows.
|
|
44
|
+
result = sbx.commands.run("curl -sS https://api.stripe.com/v1/customers -u sk_test_veris:")
|
|
45
|
+
print(result.stdout)
|
|
46
|
+
|
|
47
|
+
sbx.kill()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Inside an event loop, use `AsyncSandbox` — same contract, awaited:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from veris_e2b import AsyncSandbox
|
|
54
|
+
|
|
55
|
+
sbx = await AsyncSandbox.create()
|
|
56
|
+
await sbx.commands.run("curl -sS https://api.stripe.com/v1/customers -u sk_test_veris:")
|
|
57
|
+
await sbx.kill()
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 4. Check the receipt
|
|
61
|
+
|
|
62
|
+
A test suite that quietly stopped calling its dependency prints the same output
|
|
63
|
+
as one that works. The receipt is how you tell them apart:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from veris_e2b import TouchMatcher
|
|
67
|
+
|
|
68
|
+
# raises VerisUntouchedError unless the service actually saw a matching request
|
|
69
|
+
sbx.veris.assert_touched("stripe", TouchMatcher(method="POST", path="/v1/charges"))
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Creating a sandbox
|
|
73
|
+
|
|
74
|
+
Every E2B option still works; the Veris options live under one `veris` keyword so
|
|
75
|
+
a future e2b release cannot collide with them. Pass a `VerisOpts` or a plain dict.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from veris_e2b import Sandbox, VerisOpts
|
|
79
|
+
|
|
80
|
+
sbx = Sandbox.create(
|
|
81
|
+
"my-template", # any e2b template
|
|
82
|
+
timeout=15 * 60, # any e2b option, seconds
|
|
83
|
+
veris=VerisOpts(
|
|
84
|
+
environment_id="env_…", # default: VERIS_ENVIRONMENT_ID
|
|
85
|
+
api_key="…", # default: VERIS_API_KEY
|
|
86
|
+
api_base="https://svc.api.veris.ai", # default: VERIS_API_BASE
|
|
87
|
+
snapshot_id="snap_…", # boot the twin from a snapshot
|
|
88
|
+
attach_sandbox_id="sb_…", # or reuse an existing twin
|
|
89
|
+
egress="strict", # "strict" | "open"
|
|
90
|
+
allow_out=["pypi.org"], # extra hosts your code may reach
|
|
91
|
+
ttl_minutes=25, # twin lifetime; default: timeout + 10
|
|
92
|
+
install_ca=True, # trust the interception CA
|
|
93
|
+
data_plane_env=True, # inject DATABASE_URL etc.
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
| Option | Default | What it does |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `environment_id` | `VERIS_ENVIRONMENT_ID` | Which Veris environment the mocks come from — it decides which vendor services you get. |
|
|
101
|
+
| `api_key` | `VERIS_API_KEY` | Veris credential. |
|
|
102
|
+
| `api_base` | `VERIS_API_BASE` or `https://svc.api.veris.ai` | Control plane to talk to. |
|
|
103
|
+
| `snapshot_id` | — | Boot the twin from one of the environment's snapshots instead of its baseline, so every run starts from the same known state. Mutually exclusive with `attach_sandbox_id`. |
|
|
104
|
+
| `attach_sandbox_id` | — | Attach to an existing Veris sandbox instead of creating one. `kill()` will not delete it. |
|
|
105
|
+
| `ttl_minutes` | timeout + 10 | Backstop lifetime for the Veris sandbox, in case teardown never runs. |
|
|
106
|
+
| `egress` | `"strict"` | What may leave the sandbox — see [egress policy](#egress-policy). |
|
|
107
|
+
| `allow_out` | `[]` | Extra hosts or CIDRs your code may reach. A hostname is interceptable; a CIDR is passed through. |
|
|
108
|
+
| `install_ca` | `True` | Install the interception CA into the sandbox's trust stores. |
|
|
109
|
+
| `data_plane_env` | `True` | Inject non-HTTP connection strings (e.g. `DATABASE_URL`) as env. |
|
|
110
|
+
| `mode` | `"auto"` | `"auto"` and `"gateway"` both mean gateway mode here — see [differences](#differences-from-the-typescript-sdk). |
|
|
111
|
+
|
|
112
|
+
### Starting from a known state
|
|
113
|
+
|
|
114
|
+
A twin booted from the environment's baseline starts wherever that environment
|
|
115
|
+
starts. `snapshot_id` pins it to a snapshot you captured earlier, so a suite, a
|
|
116
|
+
benchmark, or a person exploring by hand all begin from the same rows:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
sbx = Sandbox.create(veris=VerisOpts(environment_id="env_…", snapshot_id="snap_…"))
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The snapshot must belong to that environment — the control plane refuses one that
|
|
123
|
+
does not, and the error names the snapshot rather than the environment. It is
|
|
124
|
+
recorded in the sandbox's E2B metadata as `veris_snapshot_id`, so a running
|
|
125
|
+
sandbox can always say what state it started from.
|
|
126
|
+
|
|
127
|
+
## The `sbx.veris` API
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
sbx.veris.receipt() # all services: counts + typed requests
|
|
131
|
+
sbx.veris.receipt("stripe") # one service
|
|
132
|
+
sbx.veris.receipt_baseline() # mark the log before a run
|
|
133
|
+
sbx.veris.receipt_since(baseline) # only what this run did
|
|
134
|
+
sbx.veris.assert_touched("stripe") # raises if it was never called
|
|
135
|
+
sbx.veris.control("stripe", "manual") # read a service's control resources
|
|
136
|
+
sbx.veris.services() # what's running in this twin
|
|
137
|
+
sbx.veris.get_data_plane_env() # {"DATABASE_URL": "postgresql://…"}
|
|
138
|
+
sbx.veris.get_trust_env() # CA paths, for processes that scrub env
|
|
139
|
+
sbx.veris.deliver_to(3000) # send webhooks to this sandbox
|
|
140
|
+
sbx.veris.update_network({...}) # change egress without losing interception
|
|
141
|
+
|
|
142
|
+
sbx.veris_sandbox_id # the Veris twin backing this sandbox
|
|
143
|
+
sbx.veris_mode # "gateway"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`AsyncSandbox` exposes the same names, awaited.
|
|
147
|
+
|
|
148
|
+
### Receipts
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
receipt = sbx.veris.receipt()
|
|
152
|
+
receipt.services["stripe"].requests # 3
|
|
153
|
+
receipt.integrity # "verified" — the tunnel was re-proven just now
|
|
154
|
+
receipt.leaks # [] in strict mode
|
|
155
|
+
receipt.services["stripe"].capped # False — the whole log was read
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`integrity` is `"verified"` only when the canary probe confirmed egress is still
|
|
159
|
+
tunneled at read time. `leaks` names blind spots the current egress mode genuinely
|
|
160
|
+
has (`udp-quic-possible`, `ech-possible`) rather than implying a receipt sees
|
|
161
|
+
everything.
|
|
162
|
+
|
|
163
|
+
`capped` is the third thing to read. The log is paged, and a read that stops before
|
|
164
|
+
the log does reports a **floor**, not a count — `capped` is then `True` and
|
|
165
|
+
`incomplete_reason` says which limit it hit. `assert_touched` treats that as
|
|
166
|
+
insufficient evidence rather than as an untouched dependency, because "we could not
|
|
167
|
+
see them" and "it was never called" are different failures.
|
|
168
|
+
|
|
169
|
+
### Run-scoped receipts
|
|
170
|
+
|
|
171
|
+
A twin you **attached** to already has a log. Counting all of it credits your run
|
|
172
|
+
with traffic from before it began — so mark the log first, and read only past the
|
|
173
|
+
mark:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
baseline = sbx.veris.receipt_baseline() # before the run
|
|
177
|
+
... # the run
|
|
178
|
+
receipt = sbx.veris.receipt_since(baseline)
|
|
179
|
+
receipt.services["stripe"].requests # this run's calls, and only these
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The baseline is anchored by a unique control request, so it survives a reset that
|
|
183
|
+
preserves numeric ids, and `receipt_since` revalidates it *after* reading — a reset
|
|
184
|
+
part-way through invalidates the whole measurement rather than half of it. It is
|
|
185
|
+
plain data (`baseline.to_dict()` / `ReceiptBaseline.from_dict()`), so a run can
|
|
186
|
+
outlive the process that started it.
|
|
187
|
+
|
|
188
|
+
### Reading and seeding a service by hand
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
sbx.veris.control("stripe", "manual") # how this twin behaves
|
|
192
|
+
sbx.veris.control("stripe", "schema") # its shape
|
|
193
|
+
sbx.veris.control("stripe", "data") # its seed state
|
|
194
|
+
sbx.veris.control("stripe", "data", method="PATCH", body={...}) # change it
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`manual`, `schema`, `operations`, `data` and `requests` are the whole surface, and
|
|
198
|
+
only `data` accepts a write — everything else describes the twin rather than its
|
|
199
|
+
contents. Lifecycle verbs are deliberately absent: you own the sandbox, not the
|
|
200
|
+
twin's existence.
|
|
201
|
+
|
|
202
|
+
### Webhooks
|
|
203
|
+
|
|
204
|
+
If your app *receives* callbacks, tell the mocks where to deliver them:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
sbx = Sandbox.create(network={"allow_public_traffic": True})
|
|
208
|
+
sbx.commands.run("python app.py", background=True) # listening on :3000
|
|
209
|
+
|
|
210
|
+
sbx.veris.deliver_to(3000) # → https://3000-<id>.e2b.app
|
|
211
|
+
sbx.veris.deliver_to("https://my.tunnel.dev") # or your own URL
|
|
212
|
+
sbx.veris.deliver_to(None) # unregister
|
|
213
|
+
sbx.veris.deliver_to(3000, probe=False) # skip the reachability check
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`deliver_to` resolves the sandbox's own public URL — the address a vendor would
|
|
217
|
+
POST to in production — registers it with **every** mocked service in one call,
|
|
218
|
+
and verifies they can actually reach it before returning.
|
|
219
|
+
|
|
220
|
+
### Reattaching
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
sbx = Sandbox.reconnect("i7x2qk9d0v3mnbhs", api_key="…")
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`reconnect` restores the whole Veris surface from the sandbox's metadata, re-asserts
|
|
227
|
+
egress in case a raw update dropped it, and re-proves the tunnel. It is named
|
|
228
|
+
`reconnect` rather than `connect` because e2b's `connect` is also an instance method
|
|
229
|
+
(resume *this* sandbox), and one name cannot mean two things.
|
|
230
|
+
|
|
231
|
+
## Egress policy
|
|
232
|
+
|
|
233
|
+
- **`egress="strict"`** (default) — only your vendor hosts, `allow_out` additions,
|
|
234
|
+
and data planes may leave the sandbox. QUIC/HTTP3 and ECH fail closed, so the
|
|
235
|
+
receipt has no known blind spots.
|
|
236
|
+
- **`egress="open"`** — everything may leave (pip, npm, GitHub work with no
|
|
237
|
+
configuration), at the cost of two blind spots the receipt annotates in `leaks`:
|
|
238
|
+
a QUIC or ECH client could reach a real vendor unseen.
|
|
239
|
+
|
|
240
|
+
To change egress later without losing interception, use `sbx.veris.update_network()`
|
|
241
|
+
rather than the raw e2b call — the raw one clears omitted fields and would drop
|
|
242
|
+
the interception config.
|
|
243
|
+
|
|
244
|
+
## Templates
|
|
245
|
+
|
|
246
|
+
Any E2B template works — pass it as the first argument. The image needs
|
|
247
|
+
`ca-certificates` (to trust the interception CA); a template without it raises
|
|
248
|
+
`TemplateUnsupportedError` rather than running half-configured.
|
|
249
|
+
|
|
250
|
+
## Errors
|
|
251
|
+
|
|
252
|
+
Every error subclasses `VerisError`, so one `except` separates Veris failures from
|
|
253
|
+
e2b's, and each carries a `phase` naming where it died.
|
|
254
|
+
|
|
255
|
+
| Error | When |
|
|
256
|
+
|---|---|
|
|
257
|
+
| `MissingCredentialsError` | A required key or environment id is absent — raised before any network call, naming the variable. |
|
|
258
|
+
| `VerisGatewayNotOfferedError` | The control plane does not offer gateway mode. |
|
|
259
|
+
| `VerisGatewayUnreachableError` | The gateway is down. |
|
|
260
|
+
| `ReceiptIntegrityError` | Interception could not be proven — a receipt read now would lie. |
|
|
261
|
+
| `VerisUntouchedError` | `assert_touched` found no matching requests. |
|
|
262
|
+
| `TwinExpiredError` | The Veris sandbox is gone (expired or deleted). |
|
|
263
|
+
| `TemplateUnsupportedError` | The template can't host the interception CA. |
|
|
264
|
+
| `UnsupportedOperationError` | An operation that would break the one-sandbox-one-twin invariant, e.g. `fork()`. |
|
|
265
|
+
|
|
266
|
+
## Differences from the TypeScript SDK
|
|
267
|
+
|
|
268
|
+
- **Gateway mode only.** The in-sandbox `proxy` fallback needs the veris-proxy
|
|
269
|
+
machinery that `@veris-ai/e2b` carries; `mode="proxy"` raises rather than
|
|
270
|
+
pretending. A control plane that does not offer the gateway is refused loudly,
|
|
271
|
+
not silently un-intercepted — use the TypeScript package there.
|
|
272
|
+
- **`reconnect`, not `connect`** — see [Reattaching](#reattaching).
|
|
273
|
+
- **snake_case options**, and `veris=` is a keyword argument rather than a key in
|
|
274
|
+
the options object.
|
|
275
|
+
- **Both sync and async**: `Sandbox` and `AsyncSandbox`, mirroring e2b's own pair.
|
|
276
|
+
|
|
277
|
+
## Limitations
|
|
278
|
+
|
|
279
|
+
- **`fork()` is not supported.** Forked sandboxes would share one twin and corrupt
|
|
280
|
+
each other's receipts, so it raises.
|
|
281
|
+
- **Clients that pin their own CA bundle** (some vendor SDKs ship one and ignore
|
|
282
|
+
the system trust store) must be pointed at `/etc/ssl/certs/ca-certificates.crt`.
|
|
283
|
+
- **HTTP/2 and WebSockets on mocked hosts** are not yet handled in gateway mode;
|
|
284
|
+
HTTP/1.1 over TLS is. Non-mocked hosts are unaffected.
|
|
285
|
+
|
|
286
|
+
## Development
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
uv sync
|
|
290
|
+
uv run pytest # unit tests — mocked, no account needed
|
|
291
|
+
uv run ruff check .
|
|
292
|
+
uv run ruff format .
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Releasing
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
uv version 0.2.0 # then uv lock, commit, PR, merge
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Then **Actions → release-python → Run workflow**. It builds, checks the artifacts
|
|
302
|
+
and publishes to PyPI over trusted publishing (OIDC, no token), then tags
|
|
303
|
+
`python-v0.2.0`. `dry_run: true` rehearses everything but the publish. Versions
|
|
304
|
+
are PEP 440 (`0.2.0rc1`, not `0.2.0-rc.1`), and this package versions separately
|
|
305
|
+
from the npm pair. Details in [CONTRIBUTING.md](../CONTRIBUTING.md#releasing-the-python-package).
|
|
306
|
+
|
|
307
|
+
## License
|
|
308
|
+
|
|
309
|
+
Apache-2.0
|