quickstarted 0.2.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.
@@ -0,0 +1,16 @@
1
+ """Unenforced local execution: a throwaway directory and a scrubbed environment.
2
+
3
+ Commands run as your user, on your filesystem, with your network. Absolute
4
+ paths reach your home directory and a command that ignores the proxy variables
5
+ reaches any host. Useful for developing journeys against your own code; wrong
6
+ for anything you did not write.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from .base import ProcessExecutor
12
+
13
+
14
+ class LocalExecutor(ProcessExecutor):
15
+ name = "local"
16
+ enforced = False
@@ -0,0 +1,83 @@
1
+ """Enforced local execution on macOS via sandbox-exec (Seatbelt).
2
+
3
+ This is the backend that makes the harness's central claim true rather than
4
+ merely intended. The kernel, not the agent's good manners, is what stops a
5
+ command from reaching a documentation host directly: all outbound network is
6
+ denied except the loopback port the harness proxy listens on, so every page
7
+ the agent reads is either a `read_docs` call or a recorded proxy request.
8
+
9
+ It also confines the blast radius of running commands that came out of a
10
+ stranger's quickstart: reads of the real home directory are denied and writes
11
+ are confined to the workspace.
12
+
13
+ Seatbelt is deprecated by Apple but present and functional; Docker is the
14
+ portable path, and CI should use it.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import os
20
+ import platform
21
+ from pathlib import Path
22
+
23
+ from .base import ExecutorError, ProcessExecutor
24
+
25
+ SANDBOX_EXEC = "/usr/bin/sandbox-exec"
26
+
27
+
28
+ def available() -> bool:
29
+ return platform.system() == "Darwin" and Path(SANDBOX_EXEC).is_file()
30
+
31
+
32
+ def build_profile(workspace: Path, proxy_port: int | None, real_home: Path) -> str:
33
+ """Seatbelt profile source. Later rules win, so order is load-bearing."""
34
+ lines = [
35
+ "(version 1)",
36
+ "(deny default)",
37
+ # Running programs at all.
38
+ "(allow process-exec)",
39
+ "(allow process-fork)",
40
+ "(allow sysctl-read)",
41
+ "(allow mach-lookup)",
42
+ "(allow ipc-posix-shm)",
43
+ "(allow signal (target same-sandbox))",
44
+ # Interpreters and compilers read all over the system prefix.
45
+ "(allow file-read*)",
46
+ # ... but not the user's own files. This is the point.
47
+ f'(deny file-read* (subpath "{real_home}"))',
48
+ f'(allow file-read* (subpath "{workspace}"))',
49
+ # Writes stay in the workspace, plus the device files tools expect.
50
+ f'(allow file-write* (subpath "{workspace}"))',
51
+ '(allow file-write* (regex #"^/dev/"))',
52
+ "(allow file-ioctl)",
53
+ ]
54
+ if proxy_port:
55
+ # The only route off the machine is the harness proxy.
56
+ lines.append(f'(allow network-outbound (remote ip "localhost:{proxy_port}"))')
57
+ # Loopback DNS and similar helpers travel over unix sockets; the proxy
58
+ # resolves real hostnames on the agent's behalf.
59
+ lines.append("(allow network-outbound (remote unix-socket))")
60
+ lines.append("(allow network-bind (local ip \"localhost:*\"))")
61
+ return "\n".join(lines) + "\n"
62
+
63
+
64
+ class SeatbeltExecutor(ProcessExecutor):
65
+ name = "seatbelt"
66
+ enforced = True
67
+
68
+ def __init__(self, keep: bool = False, proxy_url: str | None = None):
69
+ if not available():
70
+ raise ExecutorError(
71
+ "seatbelt backend requires macOS with /usr/bin/sandbox-exec"
72
+ )
73
+ super().__init__(keep=keep, proxy_url=proxy_url)
74
+ port = None
75
+ if proxy_url:
76
+ port = int(proxy_url.rsplit(":", 1)[-1].strip("/"))
77
+ real_home = Path(os.path.expanduser("~")).resolve()
78
+ self.profile = build_profile(self.root.resolve(), port, real_home)
79
+ self._profile_path = self.root / "tmp" / ".quickstarted-sandbox.sb"
80
+ self._profile_path.write_text(self.profile, encoding="utf-8")
81
+
82
+ def argv(self, command: str) -> list[str]:
83
+ return [SANDBOX_EXEC, "-f", str(self._profile_path), "bash", "-c", command]
@@ -0,0 +1,182 @@
1
+ """Journey definitions: the YAML unit of testing.
2
+
3
+ A journey states a goal an agent should reach using only the target project's
4
+ documentation, plus a machine-checkable success assertion. Pass/fail is always
5
+ decided by the assertion script's exit code, never by a model's opinion.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from pathlib import Path
12
+ from urllib.parse import urlparse
13
+
14
+ import yaml
15
+
16
+ from .net.proxy import DEFAULT_NETWORK_ALLOW, host_matches
17
+
18
+
19
+ class JourneyError(ValueError):
20
+ """Raised when a journey file is missing or malformed."""
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class Budgets:
25
+ max_turns: int = 20
26
+ max_seconds: int = 900
27
+ max_command_seconds: int = 300
28
+ max_output_chars: int = 20_000
29
+ #: Hard ceiling on billable tokens for one run, cache traffic included.
30
+ #: 0 means unlimited. Prefer this to a dollar cap: it needs no price list
31
+ #: and cannot drift when vendors change their rates.
32
+ max_tokens: int = 0
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class Journey:
37
+ name: str
38
+ goal: str
39
+ docs_entrypoint: str
40
+ docs_allow: tuple[str, ...]
41
+ success_script: str
42
+ setup: tuple[str, ...] = ()
43
+ replay: tuple[str, ...] = ()
44
+ budgets: Budgets = field(default_factory=Budgets)
45
+ #: Hosts a shell may reach (package registries and the like). Documentation
46
+ #: hosts are deliberately excluded: they are readable only through the
47
+ #: recorded `read_docs` tool, which is what keeps attribution complete.
48
+ network_allow: tuple[str, ...] = DEFAULT_NETWORK_ALLOW
49
+ #: Hosts named by hand under `network.allow`/`network.only`. These beat the
50
+ #: docs-host rule, so a registry that also serves documentation stays
51
+ #: installable when the author says so.
52
+ network_explicit: tuple[str, ...] = ()
53
+ source: str = ""
54
+
55
+ @property
56
+ def network_conflicts(self) -> tuple[str, ...]:
57
+ """Docs hosts that installs usually need, which will now be refused.
58
+
59
+ Declaring a package registry as a documentation host is the easy
60
+ mistake: the proxy then refuses `pip install`, and the run fails for a
61
+ reason that has nothing to do with the documentation.
62
+ """
63
+ return tuple(
64
+ host
65
+ for host in self.docs_allow
66
+ if host_matches(host, DEFAULT_NETWORK_ALLOW)
67
+ and not host_matches(host, self.network_explicit)
68
+ )
69
+
70
+ @property
71
+ def attribution_gaps(self) -> tuple[str, ...]:
72
+ """Docs hosts a shell may also reach, so their reads are not all logged."""
73
+ return tuple(
74
+ host for host in self.docs_allow if host_matches(host, self.network_explicit)
75
+ )
76
+
77
+ def host_allowed(self, url: str) -> bool:
78
+ host = (urlparse(url).hostname or "").lower()
79
+ if not host:
80
+ return False
81
+ return host_matches(host, self.docs_allow)
82
+
83
+
84
+ def _require(data: dict, key: str, source: str):
85
+ if key not in data or data[key] in (None, "", []):
86
+ raise JourneyError(f"{source}: missing required field '{key}'")
87
+ return data[key]
88
+
89
+
90
+ def _str_list(value, key: str, source: str) -> tuple[str, ...]:
91
+ if value is None:
92
+ return ()
93
+ if not isinstance(value, list) or not all(isinstance(x, str) for x in value):
94
+ raise JourneyError(f"{source}: '{key}' must be a list of strings")
95
+ return tuple(x.strip() for x in value if x.strip())
96
+
97
+
98
+ def _normalize_host(entry: str, source: str) -> str:
99
+ entry = entry.strip().lower()
100
+ if "://" in entry:
101
+ entry = urlparse(entry).hostname or ""
102
+ entry = entry.strip("/")
103
+ if not entry or "/" in entry:
104
+ raise JourneyError(
105
+ f"{source}: docs.allow entries must be bare hostnames, got {entry!r}"
106
+ )
107
+ return entry
108
+
109
+
110
+ def load_journey(path: str | Path) -> Journey:
111
+ path = Path(path)
112
+ source = str(path)
113
+ if not path.is_file():
114
+ raise JourneyError(f"{source}: no such file")
115
+ try:
116
+ data = yaml.safe_load(path.read_text(encoding="utf-8"))
117
+ except yaml.YAMLError as exc:
118
+ raise JourneyError(f"{source}: invalid YAML: {exc}") from exc
119
+ if not isinstance(data, dict):
120
+ raise JourneyError(f"{source}: top level must be a mapping")
121
+
122
+ name = str(_require(data, "name", source))
123
+ goal = str(_require(data, "goal", source)).strip()
124
+
125
+ docs = _require(data, "docs", source)
126
+ if not isinstance(docs, dict):
127
+ raise JourneyError(f"{source}: 'docs' must be a mapping")
128
+ entrypoint = str(_require(docs, "entrypoint", source)).strip()
129
+ parsed = urlparse(entrypoint)
130
+ if parsed.scheme not in ("http", "https") or not parsed.hostname:
131
+ raise JourneyError(f"{source}: docs.entrypoint must be an http(s) URL")
132
+ allow = [
133
+ _normalize_host(h, source)
134
+ for h in _str_list(docs.get("allow"), "docs.allow", source)
135
+ ]
136
+ entry_host = parsed.hostname.lower()
137
+ if entry_host not in allow:
138
+ allow.insert(0, entry_host)
139
+
140
+ network = data.get("network") or {}
141
+ if not isinstance(network, dict):
142
+ raise JourneyError(f"{source}: 'network' must be a mapping")
143
+ unknown_net = set(network) - {"allow", "only"}
144
+ if unknown_net:
145
+ raise JourneyError(f"{source}: unknown network keys: {sorted(unknown_net)}")
146
+ extra_net = [
147
+ _normalize_host(h, source)
148
+ for h in _str_list(network.get("allow"), "network.allow", source)
149
+ ]
150
+ only_net = [
151
+ _normalize_host(h, source)
152
+ for h in _str_list(network.get("only"), "network.only", source)
153
+ ]
154
+ network_allow = tuple(only_net) if only_net else DEFAULT_NETWORK_ALLOW + tuple(extra_net)
155
+
156
+ success = _require(data, "success", source)
157
+ if not isinstance(success, dict):
158
+ raise JourneyError(f"{source}: 'success' must be a mapping")
159
+ success_script = str(_require(success, "script", source))
160
+
161
+ budgets_data = data.get("budgets") or {}
162
+ if not isinstance(budgets_data, dict):
163
+ raise JourneyError(f"{source}: 'budgets' must be a mapping")
164
+ known = set(Budgets.__dataclass_fields__)
165
+ unknown = set(budgets_data) - known
166
+ if unknown:
167
+ raise JourneyError(f"{source}: unknown budget keys: {sorted(unknown)}")
168
+ budgets = Budgets(**{k: int(v) for k, v in budgets_data.items()})
169
+
170
+ return Journey(
171
+ name=name,
172
+ goal=goal,
173
+ docs_entrypoint=entrypoint,
174
+ docs_allow=tuple(allow),
175
+ success_script=success_script,
176
+ setup=_str_list(data.get("setup"), "setup", source),
177
+ replay=_str_list(data.get("replay"), "replay", source),
178
+ budgets=budgets,
179
+ network_allow=network_allow,
180
+ network_explicit=tuple(only_net or extra_net),
181
+ source=source,
182
+ )
@@ -0,0 +1,5 @@
1
+ """Network policy: the harness-owned egress proxy."""
2
+
3
+ from .proxy import DEFAULT_NETWORK_ALLOW, EgressProxy, host_matches
4
+
5
+ __all__ = ["DEFAULT_NETWORK_ALLOW", "EgressProxy", "host_matches"]
@@ -0,0 +1,274 @@
1
+ """The harness-owned egress proxy.
2
+
3
+ Every packet an agent's shell sends leaves through here, which buys three
4
+ things the v0 design only hoped for:
5
+
6
+ 1. **Enforcement.** The journey's network allowlist is applied by the proxy,
7
+ not by asking the agent nicely to use the `read_docs` tool.
8
+ 2. **Attribution.** Documentation hosts are deliberately *not* reachable from
9
+ the shell. An agent that tries `curl https://docs.example.com/...` is
10
+ refused and the attempt is recorded, so the set of pages the agent read is
11
+ the set the trace knows about. Failure attribution stops being a guess.
12
+ 3. **Honest infrastructure signals.** A connection refused by us and a
13
+ connection that failed upstream are different events, which is what lets a
14
+ run be classified as a docs failure rather than a flaky network.
15
+
16
+ The split matters: `docs.allow` hosts are readable only through `read_docs`,
17
+ while `network.allow` hosts (package registries and the like) are what a
18
+ shell may talk to in order to install things.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import select
24
+ import socket
25
+ import threading
26
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
27
+ from urllib.parse import urlparse
28
+
29
+ # Package registries and source hosts a quickstart legitimately needs.
30
+ DEFAULT_NETWORK_ALLOW = (
31
+ "pypi.org",
32
+ "files.pythonhosted.org",
33
+ "registry.npmjs.org",
34
+ "github.com",
35
+ "codeload.github.com",
36
+ "objects.githubusercontent.com",
37
+ "crates.io",
38
+ "static.crates.io",
39
+ "proxy.golang.org",
40
+ )
41
+
42
+ _RELAY_CHUNK = 65536
43
+
44
+
45
+ def host_matches(host: str, entries) -> bool:
46
+ host = (host or "").lower().strip(".")
47
+ for entry in entries:
48
+ entry = entry.lower().strip(".")
49
+ if host == entry or host.endswith("." + entry):
50
+ return True
51
+ return False
52
+
53
+
54
+ class EgressProxy:
55
+ """A minimal forward proxy: CONNECT tunnels plus absolute-URI HTTP."""
56
+
57
+ #: Overridden to "0.0.0.0" when running as a sidecar for a container.
58
+ bind_host = "127.0.0.1"
59
+
60
+ def __init__(self, network_allow, docs_hosts=(), trace=None, explicit_allow=()):
61
+ self.network_allow = tuple(network_allow)
62
+ self.docs_hosts = tuple(docs_hosts)
63
+ #: Hosts the journey named under `network.allow` by hand. A host can be
64
+ #: both documentation and a package registry (PyPI is the obvious
65
+ #: case), and when an author says so explicitly, installs win over
66
+ #: attribution for that host. The override is recorded, not silent.
67
+ self.explicit_allow = tuple(explicit_allow)
68
+ self.trace = trace
69
+ self._server: ThreadingHTTPServer | None = None
70
+ self._thread: threading.Thread | None = None
71
+ self.blocked_docs_attempts = 0
72
+
73
+ # -- policy ---------------------------------------------------------
74
+ def decide(self, host: str) -> tuple[bool, str]:
75
+ """(allowed, reason). Docs hosts lose to keep attribution complete."""
76
+ if host_matches(host, self.explicit_allow):
77
+ return True, "explicit_network_override"
78
+ if host_matches(host, self.docs_hosts):
79
+ return False, "docs_host_requires_read_docs"
80
+ if host_matches(host, self.network_allow):
81
+ return True, "allowlisted"
82
+ return False, "not_allowlisted"
83
+
84
+ def record(self, event: str, **data) -> None:
85
+ if self.trace is not None:
86
+ self.trace.add(event, **data)
87
+
88
+ # -- lifecycle ------------------------------------------------------
89
+ @property
90
+ def url(self) -> str:
91
+ if not self._server:
92
+ raise RuntimeError("proxy not started")
93
+ _host, port = self._server.server_address[:2]
94
+ return f"http://127.0.0.1:{port}"
95
+
96
+ @property
97
+ def port(self) -> int:
98
+ if not self._server:
99
+ raise RuntimeError("proxy not started")
100
+ return int(self._server.server_address[1])
101
+
102
+ def start(self, port: int = 0) -> str:
103
+ proxy = self
104
+
105
+ class Handler(_ProxyHandler):
106
+ pass
107
+
108
+ Handler.proxy = proxy
109
+ self._server = ThreadingHTTPServer((self.bind_host, port), Handler)
110
+ self._server.daemon_threads = True
111
+ self._thread = threading.Thread(
112
+ target=self._server.serve_forever, kwargs={"poll_interval": 0.2}, daemon=True
113
+ )
114
+ self._thread.start()
115
+ return self.url
116
+
117
+ def stop(self) -> None:
118
+ if self._server:
119
+ self._server.shutdown()
120
+ self._server.server_close()
121
+ self._server = None
122
+ if self._thread:
123
+ self._thread.join(timeout=5)
124
+ self._thread = None
125
+
126
+ def __enter__(self) -> EgressProxy:
127
+ self.start()
128
+ return self
129
+
130
+ def __exit__(self, *exc) -> None:
131
+ self.stop()
132
+
133
+
134
+ class _ProxyHandler(BaseHTTPRequestHandler):
135
+ proxy: EgressProxy
136
+ protocol_version = "HTTP/1.1"
137
+
138
+ def log_message(self, fmt, *args):
139
+ pass
140
+
141
+ # -- helpers --------------------------------------------------------
142
+ def _refuse(self, host: str, reason: str, method: str) -> None:
143
+ self.proxy.record(
144
+ "egress_blocked", host=host, reason=reason, method=method
145
+ )
146
+ if reason == "docs_host_requires_read_docs":
147
+ self.proxy.blocked_docs_attempts += 1
148
+ body = (
149
+ f"BLOCKED by quickstarted: {host} is a documentation host. "
150
+ "Read documentation with the read_docs tool, not the shell, so "
151
+ "that every page you read is recorded.\n"
152
+ ).encode()
153
+ else:
154
+ allowed = ", ".join(self.proxy.network_allow) or "(none)"
155
+ body = (
156
+ f"BLOCKED by quickstarted: {host} is not on this journey's network "
157
+ f"allowlist ({allowed}).\n"
158
+ ).encode()
159
+ self.send_response(403)
160
+ self.send_header("Content-Type", "text/plain")
161
+ self.send_header("Content-Length", str(len(body)))
162
+ self.send_header("Connection", "close")
163
+ self.end_headers()
164
+ self.wfile.write(body)
165
+
166
+ def _upstream_failed(self, host: str, exc: Exception, method: str) -> None:
167
+ self.proxy.record(
168
+ "egress_error", host=host, error=str(exc), method=method
169
+ )
170
+ body = f"quickstarted proxy: upstream connection to {host} failed: {exc}\n".encode()
171
+ self.send_response(502)
172
+ self.send_header("Content-Type", "text/plain")
173
+ self.send_header("Content-Length", str(len(body)))
174
+ self.send_header("Connection", "close")
175
+ self.end_headers()
176
+ self.wfile.write(body)
177
+
178
+ # -- CONNECT (https) -------------------------------------------------
179
+ def do_CONNECT(self) -> None:
180
+ target = self.path
181
+ host, _, port_s = target.partition(":")
182
+ port = int(port_s or 443)
183
+ allowed, reason = self.proxy.decide(host)
184
+ if not allowed:
185
+ self._refuse(host, reason, "CONNECT")
186
+ return
187
+ try:
188
+ upstream = socket.create_connection((host, port), timeout=30)
189
+ except OSError as exc:
190
+ self._upstream_failed(host, exc, "CONNECT")
191
+ return
192
+ self.proxy.record("egress_allowed", host=host, port=port, method="CONNECT")
193
+ self.send_response(200, "Connection established")
194
+ self.end_headers()
195
+ try:
196
+ self.wfile.flush()
197
+ except OSError:
198
+ upstream.close()
199
+ return
200
+ self._relay(self.connection, upstream)
201
+
202
+ def _relay(self, client: socket.socket, upstream: socket.socket) -> None:
203
+ sockets = [client, upstream]
204
+ try:
205
+ while True:
206
+ readable, _, errored = select.select(sockets, [], sockets, 60)
207
+ if errored or not readable:
208
+ break
209
+ for sock in readable:
210
+ other = upstream if sock is client else client
211
+ try:
212
+ data = sock.recv(_RELAY_CHUNK)
213
+ except OSError:
214
+ return
215
+ if not data:
216
+ return
217
+ try:
218
+ other.sendall(data)
219
+ except OSError:
220
+ return
221
+ finally:
222
+ upstream.close()
223
+
224
+ # -- plain HTTP ------------------------------------------------------
225
+ def _forward(self) -> None:
226
+ parsed = urlparse(self.path)
227
+ host = parsed.hostname or ""
228
+ if not host:
229
+ self._refuse(host or "(relative-url)", "not_allowlisted", self.command)
230
+ return
231
+ allowed, reason = self.proxy.decide(host)
232
+ if not allowed:
233
+ self._refuse(host, reason, self.command)
234
+ return
235
+ port = parsed.port or 80
236
+ path = parsed.path or "/"
237
+ if parsed.query:
238
+ path += "?" + parsed.query
239
+ length = int(self.headers.get("Content-Length") or 0)
240
+ body = self.rfile.read(length) if length else b""
241
+ try:
242
+ upstream = socket.create_connection((host, port), timeout=30)
243
+ except OSError as exc:
244
+ self._upstream_failed(host, exc, self.command)
245
+ return
246
+ self.proxy.record(
247
+ "egress_allowed", host=host, port=port, method=self.command, path=path
248
+ )
249
+ try:
250
+ request = [f"{self.command} {path} HTTP/1.1", f"Host: {parsed.netloc}"]
251
+ for key, value in self.headers.items():
252
+ if key.lower() in ("proxy-connection", "connection", "host"):
253
+ continue
254
+ request.append(f"{key}: {value}")
255
+ request.append("Connection: close")
256
+ blob = ("\r\n".join(request) + "\r\n\r\n").encode() + body
257
+ upstream.sendall(blob)
258
+ while True:
259
+ chunk = upstream.recv(_RELAY_CHUNK)
260
+ if not chunk:
261
+ break
262
+ self.wfile.write(chunk)
263
+ except OSError:
264
+ pass
265
+ finally:
266
+ upstream.close()
267
+ self.close_connection = True
268
+
269
+ do_GET = _forward
270
+ do_POST = _forward
271
+ do_HEAD = _forward
272
+ do_PUT = _forward
273
+ do_DELETE = _forward
274
+ do_PATCH = _forward
@@ -0,0 +1,53 @@
1
+ """Entrypoint for running the egress proxy as a sidecar container.
2
+
3
+ Kept dependency-free and importable as a bare script so it can run in a stock
4
+ `python:*-alpine` image with only this directory mounted: the sidecar must not
5
+ need quickstarted's own dependencies installed.
6
+
7
+ Events are written to stdout as JSONL; the harness collects them with
8
+ `docker logs` and merges them into the run trace.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ import sys
16
+ import threading
17
+ import time
18
+
19
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
20
+
21
+ from proxy import EgressProxy
22
+
23
+
24
+ class StdoutTrace:
25
+ """Duck-types Trace.add, emitting one JSON object per event."""
26
+
27
+ def add(self, type: str, **data) -> None:
28
+ sys.stdout.write(json.dumps({"ts": time.time(), "type": type, **data}) + "\n")
29
+ sys.stdout.flush()
30
+
31
+
32
+ def _hosts(name: str) -> tuple[str, ...]:
33
+ raw = os.environ.get(name, "")
34
+ return tuple(h for h in (x.strip() for x in raw.split(",")) if h)
35
+
36
+
37
+ def main() -> int:
38
+ port = int(os.environ.get("QUICKSTARTED_PROXY_PORT", "8080"))
39
+ proxy = EgressProxy(
40
+ network_allow=_hosts("QUICKSTARTED_NETWORK_ALLOW"),
41
+ docs_hosts=_hosts("QUICKSTARTED_DOCS_HOSTS"),
42
+ trace=StdoutTrace(),
43
+ )
44
+ # Bind on all interfaces inside the sidecar so the sandbox container can
45
+ # reach it; the container is on an internal network with no route out.
46
+ proxy.bind_host = "0.0.0.0"
47
+ proxy.start(port=port)
48
+ threading.Event().wait()
49
+ return 0
50
+
51
+
52
+ if __name__ == "__main__":
53
+ raise SystemExit(main())