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.
@@ -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