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 +71 -0
- veris_e2b/_options.py +278 -0
- veris_e2b/async_sandbox.py +360 -0
- veris_e2b/control_plane.py +508 -0
- veris_e2b/errors.py +83 -0
- veris_e2b/network.py +155 -0
- veris_e2b/py.typed +0 -0
- veris_e2b/receipt.py +393 -0
- veris_e2b/run_receipt.py +254 -0
- veris_e2b/sandbox.py +397 -0
- veris_e2b/service_control.py +113 -0
- veris_e2b/trust.py +92 -0
- veris_e2b/veris_api.py +542 -0
- veris_e2b/version.py +15 -0
- veris_e2b-0.1.0.dist-info/METADATA +331 -0
- veris_e2b-0.1.0.dist-info/RECORD +17 -0
- veris_e2b-0.1.0.dist-info/WHEEL +4 -0
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
|
+
)
|