veris-e2b 0.1.0__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,71 @@
1
+ """veris-e2b — Veris dependency-sandbox interception for E2B, in Python.
2
+
3
+ A drop-in subclass of e2b's ``Sandbox`` whose vendor API calls are answered by a
4
+ per-run Veris twin: the code under test dials production hostnames and never
5
+ learns it was intercepted, and every run ends with a receipt of what the vendor
6
+ actually received.
7
+
8
+ from veris_e2b import Sandbox, VerisOpts
9
+
10
+ sbx = Sandbox.create(veris=VerisOpts(environment_id="env_…"))
11
+ sbx.commands.run("curl -sS https://api.stripe.com/v1/customers -u sk_test_veris:")
12
+ sbx.veris.assert_touched("stripe")
13
+ sbx.kill()
14
+
15
+ ``AsyncSandbox`` is the same thing for callers already inside an event loop.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from ._options import VerisOpts
21
+ from .async_sandbox import AsyncSandbox
22
+ from .control_plane import EgressCredential, RouteEntry, ServiceInfo, TwinSandbox
23
+ from .errors import (
24
+ MissingCredentialsError,
25
+ ReceiptIntegrityError,
26
+ TemplateUnsupportedError,
27
+ TwinExpiredError,
28
+ UnsupportedOperationError,
29
+ VerisError,
30
+ VerisGatewayNotOfferedError,
31
+ VerisGatewayUnreachableError,
32
+ VerisUntouchedError,
33
+ )
34
+ from .network import EgressMode
35
+ from .receipt import Receipt, ReceiptEntry, ReceiptLeak, ReceiptRequest
36
+ from .run_receipt import ReceiptBaseline
37
+ from .sandbox import Sandbox
38
+ from .service_control import ControlMethod, ControlResource
39
+ from .veris_api import AsyncVerisApi, TouchMatcher, VerisApi
40
+ from .version import SDK_VERSION
41
+
42
+ __all__ = [
43
+ "AsyncSandbox",
44
+ "AsyncVerisApi",
45
+ "ControlMethod",
46
+ "ControlResource",
47
+ "EgressCredential",
48
+ "EgressMode",
49
+ "MissingCredentialsError",
50
+ "Receipt",
51
+ "ReceiptBaseline",
52
+ "ReceiptEntry",
53
+ "ReceiptIntegrityError",
54
+ "ReceiptLeak",
55
+ "ReceiptRequest",
56
+ "RouteEntry",
57
+ "SDK_VERSION",
58
+ "Sandbox",
59
+ "ServiceInfo",
60
+ "TemplateUnsupportedError",
61
+ "TouchMatcher",
62
+ "TwinExpiredError",
63
+ "TwinSandbox",
64
+ "UnsupportedOperationError",
65
+ "VerisApi",
66
+ "VerisError",
67
+ "VerisGatewayNotOfferedError",
68
+ "VerisGatewayUnreachableError",
69
+ "VerisOpts",
70
+ "VerisUntouchedError",
71
+ ]
veris_e2b/_options.py ADDED
@@ -0,0 +1,278 @@
1
+ """Options, coordinates and the create-time decisions that are pure.
2
+
3
+ Everything here is IO-free so the blocking and non-blocking sandboxes make the
4
+ same choices from the same code — the two ``create`` bodies differ only in how
5
+ they await.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import math
12
+ import os
13
+ from collections.abc import Mapping
14
+ from dataclasses import dataclass, field
15
+ from typing import Any, Literal
16
+
17
+ from .control_plane import DEFAULT_API_BASE
18
+ from .errors import MissingCredentialsError, VerisError
19
+ from .network import EgressMode
20
+
21
+ VerisMode = Literal["auto", "gateway"]
22
+
23
+ #: E2B's own default sandbox lifetime, in seconds — the base for the twin's TTL
24
+ #: backstop when the caller names no timeout.
25
+ DEFAULT_TIMEOUT_S = 300
26
+
27
+
28
+ @dataclass
29
+ class VerisOpts:
30
+ """The Veris half of ``Sandbox.create``. Every field has an env fallback or a default."""
31
+
32
+ #: Veris API key. Falls back to ``VERIS_API_KEY``. Required.
33
+ api_key: str | None = None
34
+ #: Veris environment the per-run twin is deployed from. Falls back to ``VERIS_ENVIRONMENT_ID``.
35
+ environment_id: str | None = None
36
+ #: Control plane base. Falls back to ``VERIS_API_BASE``, then svc.api.veris.ai.
37
+ api_base: str | None = None
38
+ #: Attach to an EXISTING twin instead of provisioning one. ``kill()`` will NOT delete it.
39
+ attach_sandbox_id: str | None = None
40
+ #: Boot the twin from one of the environment's snapshots instead of its
41
+ #: baseline, so the run starts from a known state. Mutually exclusive with
42
+ #: ``attach_sandbox_id`` (an existing twin already is at some state).
43
+ snapshot_id: str | None = None
44
+ #: Twin TTL backstop, minutes. Default: derived from the sandbox timeout + 10.
45
+ ttl_minutes: int | None = None
46
+ #: ``strict`` (default): only vendor hosts + allow_out + data planes may leave.
47
+ #: ``open``: everything egresses, with documented QUIC/ECH blind spots.
48
+ egress: EgressMode = "strict"
49
+ #: Extra hosts or CIDRs the code may reach, merged into the allowlist.
50
+ allow_out: list[str] = field(default_factory=list)
51
+ #: Install the CA + inject the trust env family at create.
52
+ install_ca: bool = True
53
+ #: Inject ``{env_hint: dsn}`` for non-HTTP twin services (e.g. ``DATABASE_URL``).
54
+ data_plane_env: bool = True
55
+ #: Only ``auto``/``gateway`` here — see :func:`validate`.
56
+ mode: str = "auto"
57
+
58
+
59
+ def coerce_opts(veris: VerisOpts | Mapping[str, Any] | None) -> VerisOpts:
60
+ """Accept a ``VerisOpts``, a plain dict, or nothing."""
61
+ if veris is None:
62
+ return VerisOpts()
63
+ if isinstance(veris, VerisOpts):
64
+ return veris
65
+ unknown = set(veris) - {f for f in VerisOpts.__dataclass_fields__}
66
+ if unknown:
67
+ raise VerisError(
68
+ f"unknown veris option(s): {', '.join(sorted(unknown))}", phase="credentials"
69
+ )
70
+ return VerisOpts(**dict(veris))
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class Coordinates:
75
+ api_key: str
76
+ api_base: str
77
+ environment_id: str | None
78
+
79
+
80
+ def validate(opts: VerisOpts) -> None:
81
+ """Refuse contradictory or unimplemented combinations before any network call."""
82
+ # An attached twin is already at whatever state it is at — a snapshot to boot
83
+ # it from is a contradiction, not a refinement.
84
+ if opts.snapshot_id and opts.attach_sandbox_id:
85
+ raise VerisError(
86
+ "snapshot_id and attach_sandbox_id are mutually exclusive: attaching reuses an "
87
+ "existing twin, which cannot be re-booted from a snapshot",
88
+ phase="credentials",
89
+ )
90
+ if opts.mode == "proxy":
91
+ raise VerisError(
92
+ "proxy mode is not implemented in the Python SDK — it needs the in-sandbox "
93
+ "veris-proxy machinery that @veris-ai/e2b carries. Use mode='gateway' (or the "
94
+ "default 'auto'), or the TypeScript package for a control plane without the gateway.",
95
+ phase="credentials",
96
+ )
97
+ if opts.mode not in ("auto", "gateway"):
98
+ raise VerisError(
99
+ f"unknown mode {opts.mode!r}: expected 'auto' or 'gateway'", phase="credentials"
100
+ )
101
+ if opts.egress not in ("strict", "open"):
102
+ raise VerisError(
103
+ f"unknown egress {opts.egress!r}: expected 'strict' or 'open'", phase="credentials"
104
+ )
105
+
106
+
107
+ def resolve_coordinates(opts: VerisOpts) -> Coordinates:
108
+ """Credentials, from the options or the environment, naming what is missing."""
109
+ api_key = opts.api_key or os.environ.get("VERIS_API_KEY")
110
+ if not api_key:
111
+ raise MissingCredentialsError(
112
+ "no Veris API key: pass veris.api_key or set VERIS_API_KEY", phase="credentials"
113
+ )
114
+ environment_id = opts.environment_id or os.environ.get("VERIS_ENVIRONMENT_ID")
115
+ # Attaching names the twin directly, so it needs no environment; anything
116
+ # that provisions one does.
117
+ if not opts.attach_sandbox_id and not environment_id:
118
+ raise MissingCredentialsError(
119
+ "no Veris environment: pass veris.environment_id or set VERIS_ENVIRONMENT_ID",
120
+ phase="credentials",
121
+ )
122
+ api_base = opts.api_base or os.environ.get("VERIS_API_BASE") or DEFAULT_API_BASE
123
+ return Coordinates(api_key=api_key, api_base=api_base, environment_id=environment_id)
124
+
125
+
126
+ def ttl_minutes_for(timeout_s: int | None) -> int:
127
+ """Twin TTL backstop for an E2B timeout: outlive the sandbox by 10 minutes."""
128
+ return max(10, math.ceil((timeout_s or DEFAULT_TIMEOUT_S) / 60) + 10)
129
+
130
+
131
+ class Meta:
132
+ """E2B metadata keys this package stamps, so ``connect`` can rehydrate
133
+ without re-asking. Every one is reserved: a caller cannot set them."""
134
+
135
+ TWIN_ID = "veris_sandbox_id"
136
+ ENV_ID = "veris_env_id"
137
+ API_BASE = "veris_api_base"
138
+ MODE = "veris_mode"
139
+ EGRESS = "veris_egress"
140
+ OWNS_TWIN = "veris_owns_twin"
141
+ ALLOW_OUT = "veris_allow_out"
142
+ SNAPSHOT_ID = "veris_snapshot_id"
143
+
144
+
145
+ RESERVED_META = frozenset(
146
+ value for key, value in vars(Meta).items() if not key.startswith("_") and isinstance(value, str)
147
+ )
148
+
149
+
150
+ def build_metadata(
151
+ caller: Mapping[str, str] | None,
152
+ *,
153
+ twin_id: str,
154
+ environment_id: str,
155
+ api_base: str,
156
+ egress: EgressMode,
157
+ owns_twin: bool,
158
+ allow_out: list[str],
159
+ snapshot_id: str | None,
160
+ ) -> dict[str, str]:
161
+ """The caller's metadata with Veris-reserved keys stripped, plus ours."""
162
+ out = {k: v for k, v in (caller or {}).items() if k not in RESERVED_META}
163
+ out.update(
164
+ {
165
+ Meta.TWIN_ID: twin_id,
166
+ Meta.ENV_ID: environment_id,
167
+ Meta.API_BASE: api_base,
168
+ Meta.MODE: "gateway",
169
+ Meta.EGRESS: egress,
170
+ Meta.OWNS_TWIN: str(owns_twin).lower(),
171
+ Meta.ALLOW_OUT: json.dumps(allow_out),
172
+ }
173
+ )
174
+ if snapshot_id:
175
+ out[Meta.SNAPSHOT_ID] = snapshot_id
176
+ return out
177
+
178
+
179
+ def read_allow_out(metadata: Mapping[str, str]) -> list[str]:
180
+ """The allow_out list a sandbox was created with, from its metadata."""
181
+ try:
182
+ parsed = json.loads(metadata.get(Meta.ALLOW_OUT, "[]"))
183
+ except ValueError:
184
+ return []
185
+ return [entry for entry in parsed if isinstance(entry, str)] if isinstance(parsed, list) else []
186
+
187
+
188
+ def check_caller_network(network: Mapping[str, Any] | None, twin_id: str) -> None:
189
+ """A caller-supplied egress proxy would fight the one gateway mode installs."""
190
+ if (network or {}).get("egress_proxy"):
191
+ raise VerisError(
192
+ "network.egress_proxy cannot be set on a Veris gateway-mode sandbox — Veris owns "
193
+ "the egress proxy (pass extra allowances via veris.allow_out)",
194
+ phase="e2b-create",
195
+ veris_sandbox_id=twin_id,
196
+ )
197
+
198
+
199
+ def merge_envs(
200
+ caller: Mapping[str, str] | None,
201
+ *,
202
+ trust_env: Mapping[str, str],
203
+ data_plane: Mapping[str, str],
204
+ twin_id: str,
205
+ install_ca: bool,
206
+ inject_data_plane: bool,
207
+ ) -> dict[str, str]:
208
+ """Caller envs, then the Veris-managed ones.
209
+
210
+ Veris-managed WINS: a caller value for a data-plane hint (e.g.
211
+ ``DATABASE_URL``) would silently point the code under test at production.
212
+ """
213
+ out = dict(caller or {})
214
+ if install_ca:
215
+ out.update(trust_env)
216
+ if inject_data_plane:
217
+ out.update(data_plane)
218
+ out["VERIS_SANDBOX_ID"] = twin_id
219
+ return out
220
+
221
+
222
+ @dataclass(frozen=True)
223
+ class Rehydrated:
224
+ """What a running sandbox's metadata says about its Veris wiring."""
225
+
226
+ api_key: str
227
+ api_base: str
228
+ twin_id: str
229
+ environment_id: str
230
+ egress: EgressMode
231
+ owns_twin: bool
232
+ allow_out: list[str]
233
+
234
+
235
+ def rehydrate(
236
+ meta: Mapping[str, str], sandbox_id: str, api_key: str | None, api_base: str | None
237
+ ) -> Rehydrated:
238
+ """Read a sandbox's Veris wiring back out of its E2B metadata.
239
+
240
+ Where the API key is sent is decided by a trusted source — never by the
241
+ metadata, which a compromised sandbox could rewrite to exfiltrate the key.
242
+ """
243
+ if not meta.get(Meta.MODE):
244
+ raise VerisError(
245
+ f"sandbox {sandbox_id} carries no Veris metadata — it was not created by veris-e2b",
246
+ phase="connect",
247
+ )
248
+ twin_id = meta.get(Meta.TWIN_ID)
249
+ if not twin_id:
250
+ raise VerisError(
251
+ f"sandbox {sandbox_id} has Veris metadata but no resolvable twin id",
252
+ phase="connect",
253
+ response_body=dict(meta),
254
+ )
255
+ key = api_key or os.environ.get("VERIS_API_KEY")
256
+ if not key:
257
+ raise MissingCredentialsError(
258
+ "no Veris API key for reconnect: pass api_key or set VERIS_API_KEY",
259
+ phase="credentials",
260
+ )
261
+ trusted_base = api_base or os.environ.get("VERIS_API_BASE")
262
+ meta_base = meta.get(Meta.API_BASE)
263
+ if trusted_base and meta_base and meta_base != trusted_base:
264
+ raise VerisError(
265
+ f"sandbox metadata names a different Veris control plane ({meta_base}) than your "
266
+ f"configuration ({trusted_base}) — refusing to send the API key to an unverified host",
267
+ phase="connect",
268
+ )
269
+ egress = meta.get(Meta.EGRESS, "strict")
270
+ return Rehydrated(
271
+ api_key=key,
272
+ api_base=trusted_base or meta_base or DEFAULT_API_BASE,
273
+ twin_id=twin_id,
274
+ environment_id=meta.get(Meta.ENV_ID, ""),
275
+ egress="open" if egress == "open" else "strict",
276
+ owns_twin=meta.get(Meta.OWNS_TWIN) != "false",
277
+ allow_out=read_allow_out(meta),
278
+ )