sbxloop 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,397 @@
1
+ """WorkerClient: install the worker into a sandbox and run jobs through it.
2
+
3
+ Two transports:
4
+
5
+ - **stream** (default): one blocking ``sbx exec`` per job; the worker mirrors
6
+ its JSONL events to stdout, which the host parses line-by-line and
7
+ republishes on the EventBus. The result file is fetched afterwards with
8
+ ``cp`` — stdout is telemetry, the result file is the outcome.
9
+ - **poll**: the worker is launched detached (``nohup ... &``); the host tails
10
+ the in-sandbox events file by byte offset every ``poll_interval`` seconds.
11
+ Fallback for environments where long-running exec streams are unreliable.
12
+
13
+ Host-side timeouts are ``job.timeout_s`` plus a grace period; on expiry the
14
+ worker process is killed inside the sandbox (pattern-scoped pkill) and
15
+ WorkerTimeoutError is raised.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import contextlib
21
+ import logging
22
+ import queue
23
+ import shlex
24
+ import threading
25
+ import time
26
+ from collections import deque
27
+ from pathlib import Path
28
+
29
+ import sbxloop
30
+ from sbxloop.config import WorkerTransport
31
+ from sbxloop.errors import SbxError, WorkerError, WorkerTimeoutError
32
+ from sbxloop.events import EventBus
33
+ from sbxloop.sbx.models import ExecResult
34
+ from sbxloop.sbx.sandbox import ENV_FILE, EVENTS_DIR, JOBS_DIR, RESULTS_DIR, VENV_DIR, Sandbox
35
+ from sbxloop.sbx.sandbox import VENV_PYTHON as DEFAULT_PYTHON
36
+ from sbxloop.worker.wheel import resolve_worker_wheel
37
+ from sbxloop_worker.protocol import Event, EventTypes, JobRequest, JobResult
38
+
39
+ # Wheels must keep their canonical filename when staged: pip validates the
40
+ # name-version-python-abi-platform structure of the FILENAME itself and
41
+ # refuses to install a renamed wheel ("Invalid wheel filename").
42
+ STAGED_WHEEL_DIR = "/tmp"
43
+
44
+ logger = logging.getLogger(__name__)
45
+
46
+
47
+ def _output_tail(result: ExecResult, limit: int = 2000) -> str:
48
+ """Combined stderr+stdout tail: sbx exec surfaces some in-sandbox errors
49
+ on stdout, so stderr alone can be empty exactly when it matters."""
50
+ combined = "\n".join(part.strip() for part in (result.stderr, result.stdout) if part.strip())
51
+ return combined[-limit:] if combined else "(no output)"
52
+
53
+
54
+ class WorkerClient:
55
+ def __init__(
56
+ self,
57
+ sandbox: Sandbox,
58
+ bus: EventBus | None = None,
59
+ *,
60
+ transport: WorkerTransport = "stream",
61
+ python: str = DEFAULT_PYTHON,
62
+ poll_interval: float = 2.0,
63
+ grace_s: float = 60.0,
64
+ ) -> None:
65
+ self.sandbox = sandbox
66
+ self.bus = bus or EventBus()
67
+ self.transport = transport
68
+ self.python = python
69
+ self.poll_interval = poll_interval
70
+ self.grace_s = grace_s
71
+
72
+ # -- install -----------------------------------------------------------
73
+
74
+ def install(
75
+ self,
76
+ *,
77
+ extras: str = "copilot",
78
+ wheel: Path | None = None,
79
+ timeout: float = 600.0,
80
+ no_deps: bool = False,
81
+ system_site_packages: bool = False,
82
+ ) -> None:
83
+ """Install sbxloop-worker into the sandbox, venv-first with fallbacks.
84
+
85
+ Sandbox templates ship python3 but often lack python3-venv
86
+ (Debian/Ubuntu split ensurepip out). The ladder:
87
+
88
+ 1. ``python3 -m venv`` — the clean path.
89
+ 2. On a venv/ensurepip failure: ``sudo -n apt-get install
90
+ python3-venv python3-pip`` (the template's agent user has sudo;
91
+ apt hosts are on the balanced allowlist), then retry the venv.
92
+ 3. Still no venv: **user-site fallback** — ``python3 -m pip install
93
+ --user`` (adding ``--break-system-packages`` when pip reports an
94
+ externally-managed environment), and the worker runs under the
95
+ system ``python3``. ``self.python`` is updated so submit() uses
96
+ the right interpreter either way.
97
+
98
+ ``no_deps``/``system_site_packages`` are test seams for hermetic
99
+ installs; production uses full dependency resolution (PyPI is
100
+ reachable under the balanced network policy).
101
+ """
102
+ wheel = wheel if wheel is not None else resolve_worker_wheel()
103
+ if wheel is not None:
104
+ staged = f"{STAGED_WHEEL_DIR}/{wheel.name}"
105
+ self.sandbox.cp_in(wheel, staged)
106
+ base_target = staged
107
+ else:
108
+ base_target = f"sbxloop-worker=={sbxloop.__version__}"
109
+ target = f"{base_target}[{extras}]" if extras else base_target
110
+
111
+ if self._create_venv(timeout, system_site_packages):
112
+ self.python = DEFAULT_PYTHON
113
+ pip = [f"{VENV_DIR}/bin/pip", "install", "--quiet"]
114
+ if no_deps:
115
+ pip.append("--no-deps")
116
+ self._check(self.sandbox.exec([*pip, target], timeout=timeout), "worker install")
117
+ else:
118
+ self.python = "python3"
119
+ self._pip_user_install(target, timeout=timeout, no_deps=no_deps)
120
+
121
+ verify = self.sandbox.exec(
122
+ [self.python, "-c", "import sbxloop_worker; print(sbxloop_worker.__version__)"]
123
+ )
124
+ self._check(verify, "worker import check")
125
+ installed = verify.stdout.strip()
126
+ if installed != sbxloop.__version__:
127
+ raise WorkerError(
128
+ f"worker version {installed!r} does not match host {sbxloop.__version__!r}"
129
+ )
130
+
131
+ # Entrypoint smoke check: importing the package proves nothing about
132
+ # `python -m sbxloop_worker` actually executing under sbx exec. A run
133
+ # against a missing job file must exit 64 (the worker's usage-error
134
+ # code) — anything else means jobs would die with no result file,
135
+ # so fail HERE with full output instead of at the first real job.
136
+ smoke = self.sandbox.exec(
137
+ [
138
+ self.python,
139
+ "-m",
140
+ "sbxloop_worker",
141
+ "run",
142
+ "--job",
143
+ "/tmp/sbxloop-smoke-missing.json",
144
+ "--events",
145
+ "/tmp/sbxloop-smoke.events.jsonl",
146
+ "--result",
147
+ "/tmp/sbxloop-smoke.result.json",
148
+ ]
149
+ )
150
+ if smoke.returncode != 64:
151
+ raise WorkerError(
152
+ "worker entrypoint check failed "
153
+ f"(rc={smoke.returncode}, expected 64): {_output_tail(smoke)}"
154
+ )
155
+
156
+ def _create_venv(self, timeout: float, system_site_packages: bool) -> bool:
157
+ venv_cmd = ["python3", "-m", "venv"]
158
+ if system_site_packages:
159
+ venv_cmd.append("--system-site-packages")
160
+ venv_cmd.append(VENV_DIR)
161
+
162
+ result = self.sandbox.exec(venv_cmd, timeout=timeout)
163
+ if result.ok:
164
+ return True
165
+ output = f"{result.stdout} {result.stderr}".lower()
166
+ if "ensurepip" in output or "venv" in output:
167
+ # Self-heal: the official templates run Ubuntu with a sudo-capable
168
+ # agent user, and apt hosts are on the balanced allowlist.
169
+ self.sandbox.exec(
170
+ [
171
+ "sh",
172
+ "-c",
173
+ "sudo -n apt-get update -q && "
174
+ "sudo -n apt-get install -y -q python3-venv python3-pip",
175
+ ],
176
+ timeout=timeout,
177
+ )
178
+ result = self.sandbox.exec(venv_cmd, timeout=timeout)
179
+ if result.ok:
180
+ return True
181
+ logger.warning(
182
+ "venv creation failed (rc=%s): %s — falling back to a user-site install "
183
+ "with the system python3",
184
+ result.returncode,
185
+ _output_tail(result),
186
+ )
187
+ return False
188
+
189
+ def _pip_user_install(self, target: str, *, timeout: float, no_deps: bool) -> None:
190
+ pip = ["python3", "-m", "pip", "install", "--quiet", "--user"]
191
+ if no_deps:
192
+ pip.append("--no-deps")
193
+ result = self.sandbox.exec([*pip, target], timeout=timeout)
194
+ if not result.ok and "externally-managed" in f"{result.stdout} {result.stderr}".lower():
195
+ # PEP 668 (Ubuntu 24.04+): system pip refuses --user without an
196
+ # explicit opt-out.
197
+ result = self.sandbox.exec([*pip, "--break-system-packages", target], timeout=timeout)
198
+ self._check(result, "worker install (user-site fallback)")
199
+
200
+ @staticmethod
201
+ def _check(result: ExecResult, step: str) -> None:
202
+ if not result.ok:
203
+ raise WorkerError(f"{step} failed (rc={result.returncode}): {_output_tail(result)}")
204
+
205
+ # -- submit ------------------------------------------------------------
206
+
207
+ def submit(self, job: JobRequest) -> JobResult:
208
+ job_path = f"{JOBS_DIR}/{job.job_id}.json"
209
+ events_path = f"{EVENTS_DIR}/{job.job_id}.jsonl"
210
+ result_path = f"{RESULTS_DIR}/{job.job_id}.json"
211
+ self.sandbox.write_text(job_path, job.model_dump_json())
212
+
213
+ argv = [
214
+ self.python,
215
+ "-m",
216
+ "sbxloop_worker",
217
+ "run",
218
+ "--job",
219
+ job_path,
220
+ "--events",
221
+ events_path,
222
+ "--result",
223
+ result_path,
224
+ "--env-file",
225
+ ENV_FILE,
226
+ ]
227
+ # sbx injects secrets through the sandbox session/profile machinery;
228
+ # a bare exec'd process may not see them. Run the worker under a
229
+ # login shell so the sandbox environment is fully loaded.
230
+ wrapped = ["sh", "-lc", shlex.join(argv)]
231
+ deadline = time.monotonic() + job.timeout_s + self.grace_s
232
+ if self.transport == "poll":
233
+ self._run_poll(job, wrapped, events_path, result_path, deadline)
234
+ diagnostics = ""
235
+ else:
236
+ diagnostics = self._run_stream(job, wrapped, deadline)
237
+ return self._fetch_result(job, result_path, events_path, diagnostics)
238
+
239
+ # -- stream transport --------------------------------------------------
240
+
241
+ def _run_stream(self, job: JobRequest, argv: list[str], deadline: float) -> str:
242
+ """Run the worker via a blocking exec; returns diagnostics (exit code
243
+ + stderr tail) for the no-result failure path."""
244
+ proc = self.sandbox.exec_stream(argv)
245
+ lines: queue.Queue[str | None] = queue.Queue()
246
+ stderr_tail: deque[str] = deque(maxlen=50)
247
+
248
+ def reader() -> None:
249
+ assert proc.stdout is not None
250
+ for line in proc.stdout:
251
+ lines.put(line)
252
+ lines.put(None)
253
+
254
+ def err_reader() -> None:
255
+ # stderr must be drained: an unread PIPE deadlocks a chatty
256
+ # worker once the 64KB buffer fills — and its content is the
257
+ # only clue when the process dies before writing a result.
258
+ assert proc.stderr is not None
259
+ for line in proc.stderr:
260
+ stderr_tail.append(line.rstrip())
261
+
262
+ threading.Thread(target=reader, name="sbxloop-stream-reader", daemon=True).start()
263
+ threading.Thread(target=err_reader, name="sbxloop-stderr-reader", daemon=True).start()
264
+
265
+ try:
266
+ while True:
267
+ remaining = deadline - time.monotonic()
268
+ if remaining <= 0:
269
+ self._kill(job, proc)
270
+ raise WorkerTimeoutError(
271
+ f"job {job.job_id} exceeded {job.timeout_s}s (+{self.grace_s}s grace)"
272
+ )
273
+ try:
274
+ line = lines.get(timeout=min(remaining, 0.5))
275
+ except queue.Empty:
276
+ continue
277
+ if line is None:
278
+ break
279
+ self._handle_line(job, line)
280
+ finally:
281
+ with contextlib.suppress(Exception):
282
+ proc.wait(timeout=self.grace_s)
283
+ parts = [f"exec rc={proc.returncode}"]
284
+ if stderr_tail:
285
+ parts.append("stderr: " + " | ".join(stderr_tail)[-1500:])
286
+ return "; ".join(parts)
287
+
288
+ def _handle_line(self, job: JobRequest, line: str) -> None:
289
+ line = line.strip()
290
+ if not line:
291
+ return
292
+ try:
293
+ event = Event.from_json_line(line)
294
+ except ValueError:
295
+ self.bus.publish(
296
+ Event.now(EventTypes.WORKER_STDOUT, job.run_id, job_id=job.job_id, line=line)
297
+ )
298
+ return
299
+ self.bus.publish(event)
300
+
301
+ # -- poll transport ----------------------------------------------------
302
+
303
+ def _run_poll(
304
+ self,
305
+ job: JobRequest,
306
+ argv: list[str],
307
+ events_path: str,
308
+ result_path: str,
309
+ deadline: float,
310
+ ) -> None:
311
+ quoted = shlex.join(argv)
312
+ launch = self.sandbox.exec(["sh", "-c", f"nohup {quoted} >/dev/null 2>&1 & echo $!"])
313
+ if not launch.ok:
314
+ raise WorkerError(f"failed to launch worker: {launch.stderr.strip()[:2000]}")
315
+ pid = launch.stdout.strip().splitlines()[-1] if launch.stdout.strip() else ""
316
+
317
+ offset = 0
318
+ buffer = ""
319
+
320
+ def drain() -> bool:
321
+ """Read new event bytes; return True once worker.end is seen."""
322
+ nonlocal offset, buffer
323
+ chunk = self.sandbox.exec(
324
+ ["sh", "-c", f"tail -c +{offset + 1} {events_path} 2>/dev/null || true"]
325
+ )
326
+ finished = False
327
+ if chunk.stdout:
328
+ offset += len(chunk.stdout.encode())
329
+ *complete, buffer = (buffer + chunk.stdout).split("\n")
330
+ for line in complete:
331
+ self._handle_line(job, line)
332
+ if '"worker.end"' in line:
333
+ finished = True
334
+ return finished
335
+
336
+ while True:
337
+ if time.monotonic() > deadline:
338
+ self._kill(job, None)
339
+ raise WorkerTimeoutError(
340
+ f"job {job.job_id} exceeded {job.timeout_s}s (+{self.grace_s}s grace)"
341
+ )
342
+ time.sleep(self.poll_interval)
343
+ if drain():
344
+ break
345
+ if pid:
346
+ alive = self.sandbox.exec(
347
+ ["sh", "-c", f"kill -0 {pid} 2>/dev/null && echo alive || echo dead"]
348
+ )
349
+ if "dead" in alive.stdout:
350
+ # Worker exited between polls: drain whatever remains.
351
+ drain()
352
+ break
353
+
354
+ # -- helpers -----------------------------------------------------------
355
+
356
+ def _kill(self, job: JobRequest, proc: object) -> None:
357
+ # Pattern is job-id scoped so concurrent workers are never collateral.
358
+ with contextlib.suppress(Exception):
359
+ self.sandbox.exec(["pkill", "-f", f"sbxloop_worker.*{job.job_id}"])
360
+ if proc is not None:
361
+ with contextlib.suppress(Exception):
362
+ proc.kill() # type: ignore[attr-defined]
363
+
364
+ def _events_tail(self, events_path: str, lines: int = 5) -> str:
365
+ if not events_path:
366
+ return ""
367
+ with contextlib.suppress(Exception):
368
+ result = self.sandbox.exec(
369
+ ["sh", "-c", f"tail -n {lines} {events_path} 2>/dev/null || true"]
370
+ )
371
+ return result.stdout.strip().replace("\n", " | ")[-1500:]
372
+ return ""
373
+
374
+ def _fetch_result(
375
+ self,
376
+ job: JobRequest,
377
+ result_path: str,
378
+ events_path: str = "",
379
+ diagnostics: str = "",
380
+ ) -> JobResult:
381
+ try:
382
+ raw = self.sandbox.read_text(result_path)
383
+ except SbxError as exc:
384
+ detail = [f"worker for job {job.job_id} produced no result file ({result_path})"]
385
+ if diagnostics:
386
+ detail.append(diagnostics)
387
+ tail = self._events_tail(events_path)
388
+ if tail:
389
+ detail.append(f"last events: {tail}")
390
+ raise WorkerError("; ".join(detail)) from exc
391
+ try:
392
+ result = JobResult.model_validate_json(raw)
393
+ except ValueError as exc:
394
+ raise WorkerError(f"invalid result file for job {job.job_id}: {exc}") from exc
395
+ if result.job_id != job.job_id:
396
+ raise WorkerError(f"result job_id mismatch: expected {job.job_id}, got {result.job_id}")
397
+ return result
@@ -0,0 +1,73 @@
1
+ """Locate (or build) the sbxloop-worker wheel to install into sandboxes.
2
+
3
+ Resolution order:
4
+
5
+ 1. **Vendored** — the wheel embedded in the installed sbxloop package at
6
+ ``sbxloop/_vendor/`` (placed there by the hatch build hook). This is the
7
+ production path and works with zero network access to PyPI for sbxloop
8
+ itself.
9
+ 2. **Workspace build** — when running from a source checkout (no vendor dir),
10
+ build the wheel from the sibling ``packages/sbxloop-worker`` tree with
11
+ ``uv build``. Cached per process.
12
+ 3. **None** — the caller falls back to installing ``sbxloop-worker`` from
13
+ PyPI inside the sandbox at the exact lockstep version.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ import shutil
20
+ import subprocess
21
+ import tempfile
22
+ from functools import lru_cache
23
+ from importlib import resources
24
+ from pathlib import Path
25
+
26
+ import sbxloop
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ def _vendored_wheel() -> Path | None:
32
+ expected = f"sbxloop_worker-{sbxloop.__version__}-py3-none-any.whl"
33
+ try:
34
+ vendor = resources.files("sbxloop") / "_vendor"
35
+ candidate = Path(str(vendor)) / expected
36
+ except (ModuleNotFoundError, TypeError): # pragma: no cover - defensive
37
+ return None
38
+ return candidate if candidate.is_file() else None
39
+
40
+
41
+ def _workspace_worker_root() -> Path | None:
42
+ # <repo>/packages/sbxloop/src/sbxloop/__init__.py -> <repo>/packages/sbxloop-worker
43
+ package_dir = Path(sbxloop.__file__).resolve().parent
44
+ candidate = package_dir.parent.parent.parent / "sbxloop-worker"
45
+ return candidate if (candidate / "pyproject.toml").is_file() else None
46
+
47
+
48
+ @lru_cache(maxsize=1)
49
+ def _workspace_build() -> Path | None:
50
+ worker_root = _workspace_worker_root()
51
+ if worker_root is None or shutil.which("uv") is None:
52
+ return None
53
+ out_dir = Path(tempfile.mkdtemp(prefix="sbxloop-worker-wheel-"))
54
+ try:
55
+ subprocess.run(
56
+ ["uv", "build", "--wheel", "-o", str(out_dir), str(worker_root)],
57
+ check=True,
58
+ capture_output=True,
59
+ timeout=300,
60
+ )
61
+ except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as exc:
62
+ logger.warning("workspace worker wheel build failed: %s", exc)
63
+ return None
64
+ wheels = sorted(out_dir.glob("sbxloop_worker-*.whl"))
65
+ return wheels[-1] if wheels else None
66
+
67
+
68
+ def resolve_worker_wheel() -> Path | None:
69
+ """Best available worker wheel on this host, or None to use PyPI."""
70
+ wheel = _vendored_wheel()
71
+ if wheel is not None:
72
+ return wheel
73
+ return _workspace_build()
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: sbxloop
3
+ Version: 0.2.0
4
+ Summary: Agentic loop orchestration on Docker Sandboxes (sbx) with isolated credential domains
5
+ Project-URL: Homepage, https://github.com/brettbergin/sbxloop
6
+ Project-URL: Repository, https://github.com/brettbergin/sbxloop
7
+ Project-URL: Issues, https://github.com/brettbergin/sbxloop/issues
8
+ Author: Brett Bergin
9
+ License-Expression: MIT
10
+ Keywords: agents,copilot,docker,orchestration,sandbox
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: pydantic>=2.7
20
+ Requires-Dist: python-dotenv>=1.0
21
+ Requires-Dist: rich>=13.7
22
+ Requires-Dist: sbxloop-worker==0.2.0
23
+ Requires-Dist: typer>=0.12
24
+ Description-Content-Type: text/markdown
25
+
26
+ # sbxloop
27
+
28
+ Agentic loop orchestration on Docker Sandboxes (`sbx`) with isolated credential
29
+ domains. This is the host-side orchestrator package: sandbox provisioning, the
30
+ loop engine, and the `sbxloop` CLI.
31
+
32
+ See the [project README](https://github.com/brettbergin/sbxloop) for full
33
+ documentation.
@@ -0,0 +1,39 @@
1
+ sbxloop/__init__.py,sha256=RQcCoppqAISnZXZNTm9VXGKa5xtz38vXNHRW4rSUs9Y,741
2
+ sbxloop/config.py,sha256=VjjmQ-mjRBYw8AHHmo6D2-InqS5A5OO4tCoCy_ACim0,6757
3
+ sbxloop/errors.py,sha256=ncVqK66R2ir1k5q9K_HcOYU7XAnWy6CbaJxxOjjX2UE,1847
4
+ sbxloop/events.py,sha256=DVVTk_v5GrDdHa8XbcKh20VLIivVoW3JIIMnuKjccSM,2499
5
+ sbxloop/ids.py,sha256=fbROK_3iWl8PahvlX33ucPO3d9p_5Kih63h5EexhA6c,847
6
+ sbxloop/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ sbxloop/_vendor/sbxloop_worker-0.2.0-py3-none-any.whl,sha256=_0AhTlG7dc3IyGUI8lnzPgZcohyQv49uwvmdPXLiR7s,16994
8
+ sbxloop/cli/__init__.py,sha256=pjFZzGSZoDSr17oD0ay8d7jElyNl3YliaFDEgsnmKvE,42
9
+ sbxloop/cli/app.py,sha256=HupCo1DWL6fXpIP395ykBBEYXP7rr4ISdWPMqTDSyLs,12577
10
+ sbxloop/cli/doctor.py,sha256=4PfvOH6rg7kSigIwMlR0ACiM0c5BOqAPs0wOBuYnvoQ,5582
11
+ sbxloop/cli/tui.py,sha256=DE2AXG5ifL3VRelvU8S3a2RAbrAm15rbvZ4W4KnAzVY,7608
12
+ sbxloop/engine/__init__.py,sha256=87MAO9S35sn8k30mTPvOUJnDTHRjFNZOfAAu3wqQ2_M,461
13
+ sbxloop/engine/engine.py,sha256=cQO1lx58sZb5n2MpfbrOO1BD4o88-7Ryk6_Ft-HZwjE,14960
14
+ sbxloop/engine/model.py,sha256=ZwrwgOxvi6IfXS61eM_tGMYl0DijkogGfgJraA2ivzk,4312
15
+ sbxloop/engine/phases.py,sha256=jrlAlblGxKsaAApk4Kki16_H9IL8EqHrHA-vb0SchM0,8903
16
+ sbxloop/engine/store.py,sha256=YfsxQ5bp71rw6ukGHb4wH1NXkzdmIREduPeW78Xg8uk,8772
17
+ sbxloop/engine/prompts/__init__.py,sha256=GyVOTZ_QO0V27xdmV8sPbI3WK5911_ysiZrX_FMGw7E,937
18
+ sbxloop/engine/prompts/decompose.md,sha256=_oQ931PknBXW7mtNIQWrjWl1JNPr4vznicYaIj6v14I,1160
19
+ sbxloop/engine/prompts/execute.md,sha256=sFQDHFBwXd5snAi-i3rFdXe9PJPyR7Ry6SIrMOsSwVs,695
20
+ sbxloop/engine/prompts/plan.md,sha256=rgU5OSovIuW6BTBv90Y3QAKeC9o6fgX8rcytp81v7eM,770
21
+ sbxloop/engine/prompts/scrutinize.md,sha256=HMHlXiKXwIM4zf7vmI46GxMk8E0uk_BPBK7F-UVW3f8,1106
22
+ sbxloop/engine/prompts/validate.md,sha256=a8lAKaSoSz16qWJ2oNL5WXR7L4AxSeEbIRPoZlKyNYE,940
23
+ sbxloop/gh/__init__.py,sha256=fepcbuGH_PghbrMopE0hIkQ2M7AvQ79v8mmCEgieuOc,246
24
+ sbxloop/gh/ops.py,sha256=loeqsv98t3725DxrBvcPeoXbsRvC74-x8bJEoPioFsk,4205
25
+ sbxloop/gh/reporter.py,sha256=gNp5jxG9MofOo6A_xXkGL95mpso16ODpiLNCWUw9SpA,2732
26
+ sbxloop/sbx/__init__.py,sha256=BbxXDte1P8QUf8EEq1QvPdqdpLbUmh6tVOIh3ZD8dVw,264
27
+ sbxloop/sbx/cli.py,sha256=YaZwCY5iDRV5eKtcbyRxIsMEX0rqc5x3iFL8GU3MAxM,7764
28
+ sbxloop/sbx/models.py,sha256=czUotIcyPxz9RYYP19cuv8CYwePvO62Kw8oG55iyjr4,2410
29
+ sbxloop/sbx/pair.py,sha256=I7o9eOG7BexPxQFpcIW_OwM1YdAEmryYraiT2hYW7js,3961
30
+ sbxloop/sbx/parse.py,sha256=D3evxMonvYsw9Mx0ozyfKRC_IDt7vXMg-zs7Expozi8,2144
31
+ sbxloop/sbx/provision.py,sha256=0wPWtI9V2Od13tcDQDyzK6UR6-DFvOsPyZTfFwgZRK4,14866
32
+ sbxloop/sbx/sandbox.py,sha256=EYspkBKj5JEt-Y4Ma-D-608BEkEeckIRWDLzj-Gv0Oo,2613
33
+ sbxloop/worker/__init__.py,sha256=1YZ4g5YF6MBaH8Bt_ZZtEIM5ltdhRCu-iFReZ5LGcbE,227
34
+ sbxloop/worker/client.py,sha256=_3btiIM9OmtsCp9UBqBDQ1VfP7Wts-ZNj_OvsffBOto,15963
35
+ sbxloop/worker/wheel.py,sha256=vSFwQHRWVLo124pEHmKKji1_q1Tr4hRofEfKOb0aqV8,2564
36
+ sbxloop-0.2.0.dist-info/METADATA,sha256=-7sPQrRJjOtDldZEMYSWdadhr-wY9oZkeviVqGRxOw8,1269
37
+ sbxloop-0.2.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
38
+ sbxloop-0.2.0.dist-info/entry_points.txt,sha256=47smbBU306_uO80byyBBoEyx-8mo6vLf-FPL5vKX23o,49
39
+ sbxloop-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ sbxloop = sbxloop.cli.app:main