sdxloop 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.
- sdxloop/__init__.py +26 -0
- sdxloop/_vendor/sdxloop_worker-0.1.0-py3-none-any.whl +0 -0
- sdxloop/cli/__init__.py +1 -0
- sdxloop/cli/app.py +359 -0
- sdxloop/cli/doctor.py +145 -0
- sdxloop/cli/tui.py +129 -0
- sdxloop/config.py +180 -0
- sdxloop/engine/__init__.py +26 -0
- sdxloop/engine/engine.py +380 -0
- sdxloop/engine/model.py +151 -0
- sdxloop/engine/phases.py +223 -0
- sdxloop/engine/prompts/__init__.py +31 -0
- sdxloop/engine/prompts/decompose.md +41 -0
- sdxloop/engine/prompts/execute.md +31 -0
- sdxloop/engine/prompts/plan.md +35 -0
- sdxloop/engine/prompts/scrutinize.md +43 -0
- sdxloop/engine/prompts/validate.md +38 -0
- sdxloop/engine/store.py +257 -0
- sdxloop/errors.py +73 -0
- sdxloop/events.py +87 -0
- sdxloop/gh/__init__.py +6 -0
- sdxloop/gh/ops.py +140 -0
- sdxloop/gh/reporter.py +75 -0
- sdxloop/ids.py +35 -0
- sdxloop/py.typed +0 -0
- sdxloop/sbx/__init__.py +6 -0
- sdxloop/sbx/cli.py +210 -0
- sdxloop/sbx/models.py +83 -0
- sdxloop/sbx/pair.py +125 -0
- sdxloop/sbx/parse.py +67 -0
- sdxloop/sbx/provision.py +209 -0
- sdxloop/sbx/sandbox.py +70 -0
- sdxloop/worker/__init__.py +6 -0
- sdxloop/worker/client.py +260 -0
- sdxloop/worker/wheel.py +73 -0
- sdxloop-0.1.0.dist-info/METADATA +32 -0
- sdxloop-0.1.0.dist-info/RECORD +39 -0
- sdxloop-0.1.0.dist-info/WHEEL +4 -0
- sdxloop-0.1.0.dist-info/entry_points.txt +2 -0
sdxloop/sbx/sandbox.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""A handle to one live sandbox: exec, file transfer, lifecycle."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from sdxloop.sbx.cli import SbxCLI
|
|
11
|
+
from sdxloop.sbx.models import ExecResult
|
|
12
|
+
|
|
13
|
+
# Canonical in-sandbox layout. The sandbox user is `agent` in the official
|
|
14
|
+
# Docker sandbox templates.
|
|
15
|
+
SANDBOX_HOME = "/home/agent"
|
|
16
|
+
SDXLOOP_DIR = f"{SANDBOX_HOME}/.sdxloop"
|
|
17
|
+
JOBS_DIR = f"{SDXLOOP_DIR}/jobs"
|
|
18
|
+
RESULTS_DIR = f"{SDXLOOP_DIR}/results"
|
|
19
|
+
EVENTS_DIR = f"{SDXLOOP_DIR}/events"
|
|
20
|
+
ENV_FILE = f"{SDXLOOP_DIR}/env.sh"
|
|
21
|
+
VENV_DIR = f"{SDXLOOP_DIR}/venv"
|
|
22
|
+
VENV_PYTHON = f"{VENV_DIR}/bin/python"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Sandbox:
|
|
26
|
+
"""Operations on one existing sandbox. Does not own creation."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, cli: SbxCLI, name: str) -> None:
|
|
29
|
+
self.cli = cli
|
|
30
|
+
self.name = name
|
|
31
|
+
|
|
32
|
+
def __repr__(self) -> str:
|
|
33
|
+
return f"Sandbox({self.name!r})"
|
|
34
|
+
|
|
35
|
+
def exec(self, cmd: Sequence[str], *, timeout: float | None = None) -> ExecResult:
|
|
36
|
+
return self.cli.exec(self.name, cmd, timeout=timeout)
|
|
37
|
+
|
|
38
|
+
def exec_stream(self, cmd: Sequence[str]) -> subprocess.Popen[str]:
|
|
39
|
+
"""Start a streaming exec; caller owns the returned process."""
|
|
40
|
+
return self.cli.popen("exec", self.name, *cmd)
|
|
41
|
+
|
|
42
|
+
def cp_in(self, host_path: Path, sb_path: str) -> None:
|
|
43
|
+
self.cli.cp(str(host_path), f"{self.name}:{sb_path}")
|
|
44
|
+
|
|
45
|
+
def cp_out(self, sb_path: str, host_path: Path) -> None:
|
|
46
|
+
self.cli.cp(f"{self.name}:{sb_path}", str(host_path))
|
|
47
|
+
|
|
48
|
+
def write_text(self, sb_path: str, text: str) -> None:
|
|
49
|
+
with tempfile.NamedTemporaryFile("w", suffix=".sdxloop", delete=False) as f:
|
|
50
|
+
f.write(text)
|
|
51
|
+
tmp = Path(f.name)
|
|
52
|
+
try:
|
|
53
|
+
self.cp_in(tmp, sb_path)
|
|
54
|
+
finally:
|
|
55
|
+
tmp.unlink(missing_ok=True)
|
|
56
|
+
|
|
57
|
+
def read_text(self, sb_path: str) -> str:
|
|
58
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
59
|
+
target = Path(tmpdir) / "out"
|
|
60
|
+
self.cp_out(sb_path, target)
|
|
61
|
+
return target.read_text()
|
|
62
|
+
|
|
63
|
+
def mkdirs(self, *sb_paths: str) -> None:
|
|
64
|
+
self.exec(["mkdir", "-p", *sb_paths])
|
|
65
|
+
|
|
66
|
+
def stop(self) -> None:
|
|
67
|
+
self.cli.stop(self.name)
|
|
68
|
+
|
|
69
|
+
def rm(self) -> None:
|
|
70
|
+
self.cli.rm(self.name, force=True)
|
sdxloop/worker/client.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
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 queue
|
|
22
|
+
import threading
|
|
23
|
+
import time
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
import sdxloop
|
|
27
|
+
from sdxloop.config import WorkerTransport
|
|
28
|
+
from sdxloop.errors import SbxError, WorkerError, WorkerTimeoutError
|
|
29
|
+
from sdxloop.events import EventBus
|
|
30
|
+
from sdxloop.sbx.models import ExecResult
|
|
31
|
+
from sdxloop.sbx.sandbox import ENV_FILE, EVENTS_DIR, JOBS_DIR, RESULTS_DIR, VENV_DIR, Sandbox
|
|
32
|
+
from sdxloop.sbx.sandbox import VENV_PYTHON as DEFAULT_PYTHON
|
|
33
|
+
from sdxloop.worker.wheel import resolve_worker_wheel
|
|
34
|
+
from sdxloop_worker.protocol import Event, EventTypes, JobRequest, JobResult
|
|
35
|
+
|
|
36
|
+
STAGED_WHEEL = "/tmp/sdxloop_worker.whl"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class WorkerClient:
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
sandbox: Sandbox,
|
|
43
|
+
bus: EventBus | None = None,
|
|
44
|
+
*,
|
|
45
|
+
transport: WorkerTransport = "stream",
|
|
46
|
+
python: str = DEFAULT_PYTHON,
|
|
47
|
+
poll_interval: float = 2.0,
|
|
48
|
+
grace_s: float = 60.0,
|
|
49
|
+
) -> None:
|
|
50
|
+
self.sandbox = sandbox
|
|
51
|
+
self.bus = bus or EventBus()
|
|
52
|
+
self.transport = transport
|
|
53
|
+
self.python = python
|
|
54
|
+
self.poll_interval = poll_interval
|
|
55
|
+
self.grace_s = grace_s
|
|
56
|
+
|
|
57
|
+
# -- install -----------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
def install(
|
|
60
|
+
self,
|
|
61
|
+
*,
|
|
62
|
+
extras: str = "copilot",
|
|
63
|
+
wheel: Path | None = None,
|
|
64
|
+
timeout: float = 600.0,
|
|
65
|
+
no_deps: bool = False,
|
|
66
|
+
system_site_packages: bool = False,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Create the worker venv in the sandbox and install sdxloop-worker.
|
|
69
|
+
|
|
70
|
+
``no_deps``/``system_site_packages`` are test seams for hermetic
|
|
71
|
+
installs; production uses full dependency resolution (PyPI is
|
|
72
|
+
reachable under the balanced network policy).
|
|
73
|
+
"""
|
|
74
|
+
wheel = wheel if wheel is not None else resolve_worker_wheel()
|
|
75
|
+
venv_cmd = ["python3", "-m", "venv"]
|
|
76
|
+
if system_site_packages:
|
|
77
|
+
venv_cmd.append("--system-site-packages")
|
|
78
|
+
self._check(self.sandbox.exec([*venv_cmd, VENV_DIR], timeout=timeout), "venv creation")
|
|
79
|
+
|
|
80
|
+
pip = [f"{VENV_DIR}/bin/pip", "install", "--quiet"]
|
|
81
|
+
if no_deps:
|
|
82
|
+
pip.append("--no-deps")
|
|
83
|
+
if wheel is not None:
|
|
84
|
+
self.sandbox.cp_in(wheel, STAGED_WHEEL)
|
|
85
|
+
target = f"{STAGED_WHEEL}[{extras}]" if extras else STAGED_WHEEL
|
|
86
|
+
else:
|
|
87
|
+
spec = f"sdxloop-worker=={sdxloop.__version__}"
|
|
88
|
+
target = f"sdxloop-worker[{extras}]=={sdxloop.__version__}" if extras else spec
|
|
89
|
+
self._check(self.sandbox.exec([*pip, target], timeout=timeout), "worker install")
|
|
90
|
+
|
|
91
|
+
verify = self.sandbox.exec(
|
|
92
|
+
[DEFAULT_PYTHON, "-c", "import sdxloop_worker; print(sdxloop_worker.__version__)"]
|
|
93
|
+
)
|
|
94
|
+
self._check(verify, "worker import check")
|
|
95
|
+
installed = verify.stdout.strip()
|
|
96
|
+
if installed != sdxloop.__version__:
|
|
97
|
+
raise WorkerError(
|
|
98
|
+
f"worker version {installed!r} does not match host {sdxloop.__version__!r}"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def _check(result: ExecResult, step: str) -> None:
|
|
103
|
+
if not result.ok:
|
|
104
|
+
raise WorkerError(
|
|
105
|
+
f"{step} failed (rc={result.returncode}): {result.stderr.strip()[:2000]}"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# -- submit ------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
def submit(self, job: JobRequest) -> JobResult:
|
|
111
|
+
job_path = f"{JOBS_DIR}/{job.job_id}.json"
|
|
112
|
+
events_path = f"{EVENTS_DIR}/{job.job_id}.jsonl"
|
|
113
|
+
result_path = f"{RESULTS_DIR}/{job.job_id}.json"
|
|
114
|
+
self.sandbox.write_text(job_path, job.model_dump_json())
|
|
115
|
+
|
|
116
|
+
argv = [
|
|
117
|
+
self.python,
|
|
118
|
+
"-m",
|
|
119
|
+
"sdxloop_worker",
|
|
120
|
+
"run",
|
|
121
|
+
"--job",
|
|
122
|
+
job_path,
|
|
123
|
+
"--events",
|
|
124
|
+
events_path,
|
|
125
|
+
"--result",
|
|
126
|
+
result_path,
|
|
127
|
+
"--env-file",
|
|
128
|
+
ENV_FILE,
|
|
129
|
+
]
|
|
130
|
+
deadline = time.monotonic() + job.timeout_s + self.grace_s
|
|
131
|
+
if self.transport == "poll":
|
|
132
|
+
self._run_poll(job, argv, events_path, result_path, deadline)
|
|
133
|
+
else:
|
|
134
|
+
self._run_stream(job, argv, deadline)
|
|
135
|
+
return self._fetch_result(job, result_path)
|
|
136
|
+
|
|
137
|
+
# -- stream transport --------------------------------------------------
|
|
138
|
+
|
|
139
|
+
def _run_stream(self, job: JobRequest, argv: list[str], deadline: float) -> None:
|
|
140
|
+
proc = self.sandbox.exec_stream(argv)
|
|
141
|
+
lines: queue.Queue[str | None] = queue.Queue()
|
|
142
|
+
|
|
143
|
+
def reader() -> None:
|
|
144
|
+
assert proc.stdout is not None
|
|
145
|
+
for line in proc.stdout:
|
|
146
|
+
lines.put(line)
|
|
147
|
+
lines.put(None)
|
|
148
|
+
|
|
149
|
+
thread = threading.Thread(target=reader, name="sdxloop-stream-reader", daemon=True)
|
|
150
|
+
thread.start()
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
while True:
|
|
154
|
+
remaining = deadline - time.monotonic()
|
|
155
|
+
if remaining <= 0:
|
|
156
|
+
self._kill(job, proc)
|
|
157
|
+
raise WorkerTimeoutError(
|
|
158
|
+
f"job {job.job_id} exceeded {job.timeout_s}s (+{self.grace_s}s grace)"
|
|
159
|
+
)
|
|
160
|
+
try:
|
|
161
|
+
line = lines.get(timeout=min(remaining, 0.5))
|
|
162
|
+
except queue.Empty:
|
|
163
|
+
continue
|
|
164
|
+
if line is None:
|
|
165
|
+
break
|
|
166
|
+
self._handle_line(job, line)
|
|
167
|
+
finally:
|
|
168
|
+
with contextlib.suppress(Exception):
|
|
169
|
+
proc.wait(timeout=self.grace_s)
|
|
170
|
+
|
|
171
|
+
def _handle_line(self, job: JobRequest, line: str) -> None:
|
|
172
|
+
line = line.strip()
|
|
173
|
+
if not line:
|
|
174
|
+
return
|
|
175
|
+
try:
|
|
176
|
+
event = Event.from_json_line(line)
|
|
177
|
+
except ValueError:
|
|
178
|
+
self.bus.publish(
|
|
179
|
+
Event.now(EventTypes.WORKER_STDOUT, job.run_id, job_id=job.job_id, line=line)
|
|
180
|
+
)
|
|
181
|
+
return
|
|
182
|
+
self.bus.publish(event)
|
|
183
|
+
|
|
184
|
+
# -- poll transport ----------------------------------------------------
|
|
185
|
+
|
|
186
|
+
def _run_poll(
|
|
187
|
+
self,
|
|
188
|
+
job: JobRequest,
|
|
189
|
+
argv: list[str],
|
|
190
|
+
events_path: str,
|
|
191
|
+
result_path: str,
|
|
192
|
+
deadline: float,
|
|
193
|
+
) -> None:
|
|
194
|
+
quoted = " ".join(argv)
|
|
195
|
+
launch = self.sandbox.exec(["sh", "-c", f"nohup {quoted} >/dev/null 2>&1 & echo $!"])
|
|
196
|
+
if not launch.ok:
|
|
197
|
+
raise WorkerError(f"failed to launch worker: {launch.stderr.strip()[:2000]}")
|
|
198
|
+
pid = launch.stdout.strip().splitlines()[-1] if launch.stdout.strip() else ""
|
|
199
|
+
|
|
200
|
+
offset = 0
|
|
201
|
+
buffer = ""
|
|
202
|
+
|
|
203
|
+
def drain() -> bool:
|
|
204
|
+
"""Read new event bytes; return True once worker.end is seen."""
|
|
205
|
+
nonlocal offset, buffer
|
|
206
|
+
chunk = self.sandbox.exec(
|
|
207
|
+
["sh", "-c", f"tail -c +{offset + 1} {events_path} 2>/dev/null || true"]
|
|
208
|
+
)
|
|
209
|
+
finished = False
|
|
210
|
+
if chunk.stdout:
|
|
211
|
+
offset += len(chunk.stdout.encode())
|
|
212
|
+
*complete, buffer = (buffer + chunk.stdout).split("\n")
|
|
213
|
+
for line in complete:
|
|
214
|
+
self._handle_line(job, line)
|
|
215
|
+
if '"worker.end"' in line:
|
|
216
|
+
finished = True
|
|
217
|
+
return finished
|
|
218
|
+
|
|
219
|
+
while True:
|
|
220
|
+
if time.monotonic() > deadline:
|
|
221
|
+
self._kill(job, None)
|
|
222
|
+
raise WorkerTimeoutError(
|
|
223
|
+
f"job {job.job_id} exceeded {job.timeout_s}s (+{self.grace_s}s grace)"
|
|
224
|
+
)
|
|
225
|
+
time.sleep(self.poll_interval)
|
|
226
|
+
if drain():
|
|
227
|
+
break
|
|
228
|
+
if pid:
|
|
229
|
+
alive = self.sandbox.exec(
|
|
230
|
+
["sh", "-c", f"kill -0 {pid} 2>/dev/null && echo alive || echo dead"]
|
|
231
|
+
)
|
|
232
|
+
if "dead" in alive.stdout:
|
|
233
|
+
# Worker exited between polls: drain whatever remains.
|
|
234
|
+
drain()
|
|
235
|
+
break
|
|
236
|
+
|
|
237
|
+
# -- helpers -----------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
def _kill(self, job: JobRequest, proc: object) -> None:
|
|
240
|
+
# Pattern is job-id scoped so concurrent workers are never collateral.
|
|
241
|
+
with contextlib.suppress(Exception):
|
|
242
|
+
self.sandbox.exec(["pkill", "-f", f"sdxloop_worker.*{job.job_id}"])
|
|
243
|
+
if proc is not None:
|
|
244
|
+
with contextlib.suppress(Exception):
|
|
245
|
+
proc.kill() # type: ignore[attr-defined]
|
|
246
|
+
|
|
247
|
+
def _fetch_result(self, job: JobRequest, result_path: str) -> JobResult:
|
|
248
|
+
try:
|
|
249
|
+
raw = self.sandbox.read_text(result_path)
|
|
250
|
+
except SbxError as exc:
|
|
251
|
+
raise WorkerError(
|
|
252
|
+
f"worker for job {job.job_id} produced no result file ({result_path})"
|
|
253
|
+
) from exc
|
|
254
|
+
try:
|
|
255
|
+
result = JobResult.model_validate_json(raw)
|
|
256
|
+
except ValueError as exc:
|
|
257
|
+
raise WorkerError(f"invalid result file for job {job.job_id}: {exc}") from exc
|
|
258
|
+
if result.job_id != job.job_id:
|
|
259
|
+
raise WorkerError(f"result job_id mismatch: expected {job.job_id}, got {result.job_id}")
|
|
260
|
+
return result
|
sdxloop/worker/wheel.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Locate (or build) the sdxloop-worker wheel to install into sandboxes.
|
|
2
|
+
|
|
3
|
+
Resolution order:
|
|
4
|
+
|
|
5
|
+
1. **Vendored** — the wheel embedded in the installed sdxloop package at
|
|
6
|
+
``sdxloop/_vendor/`` (placed there by the hatch build hook). This is the
|
|
7
|
+
production path and works with zero network access to PyPI for sdxloop
|
|
8
|
+
itself.
|
|
9
|
+
2. **Workspace build** — when running from a source checkout (no vendor dir),
|
|
10
|
+
build the wheel from the sibling ``packages/sdxloop-worker`` tree with
|
|
11
|
+
``uv build``. Cached per process.
|
|
12
|
+
3. **None** — the caller falls back to installing ``sdxloop-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 sdxloop
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _vendored_wheel() -> Path | None:
|
|
32
|
+
expected = f"sdxloop_worker-{sdxloop.__version__}-py3-none-any.whl"
|
|
33
|
+
try:
|
|
34
|
+
vendor = resources.files("sdxloop") / "_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/sdxloop/src/sdxloop/__init__.py -> <repo>/packages/sdxloop-worker
|
|
43
|
+
package_dir = Path(sdxloop.__file__).resolve().parent
|
|
44
|
+
candidate = package_dir.parent.parent.parent / "sdxloop-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="sdxloop-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("sdxloop_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,32 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sdxloop
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agentic loop orchestration on Docker Sandboxes (sbx) with isolated credential domains
|
|
5
|
+
Project-URL: Homepage, https://github.com/brettbergin/sdxloop
|
|
6
|
+
Project-URL: Repository, https://github.com/brettbergin/sdxloop
|
|
7
|
+
Project-URL: Issues, https://github.com/brettbergin/sdxloop/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: rich>=13.7
|
|
21
|
+
Requires-Dist: sdxloop-worker==0.1.0
|
|
22
|
+
Requires-Dist: typer>=0.12
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# sdxloop
|
|
26
|
+
|
|
27
|
+
Agentic loop orchestration on Docker Sandboxes (`sbx`) with isolated credential
|
|
28
|
+
domains. This is the host-side orchestrator package: sandbox provisioning, the
|
|
29
|
+
loop engine, and the `sdxloop` CLI.
|
|
30
|
+
|
|
31
|
+
See the [project README](https://github.com/brettbergin/sdxloop) for full
|
|
32
|
+
documentation.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
sdxloop/__init__.py,sha256=KhX7QUkDPgqudf_H_yLGJAzw-stge28AtS6yNjCT0MA,741
|
|
2
|
+
sdxloop/config.py,sha256=RDy4ImUNroUPgFDVCHKzoj6ndATSy51trYSM7VO8RtY,5692
|
|
3
|
+
sdxloop/errors.py,sha256=uD5THzpjLS7CMkBzVAyE2HySK7F3VIRcYlD3dgIK5IQ,1847
|
|
4
|
+
sdxloop/events.py,sha256=pJjPI6jaJOb7rbyVFGfl9uwt39g94Ah9PBGYe2W7rIA,2499
|
|
5
|
+
sdxloop/ids.py,sha256=fbROK_3iWl8PahvlX33ucPO3d9p_5Kih63h5EexhA6c,847
|
|
6
|
+
sdxloop/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
sdxloop/_vendor/sdxloop_worker-0.1.0-py3-none-any.whl,sha256=1UhPfHt_RbmZeEx-bTm9N9xbu22dflqbGDEpxrRPDEE,16397
|
|
8
|
+
sdxloop/cli/__init__.py,sha256=FxivpRL36ALgWz0Mzo5Led1UqQP1ZpDHZ1WpiOAtqyE,42
|
|
9
|
+
sdxloop/cli/app.py,sha256=yRNsqDj0XgUYgRGUkAFYbGJXzTvf1zpVwO5PQl9u1pQ,12213
|
|
10
|
+
sdxloop/cli/doctor.py,sha256=1E4MXbGukQRcrzpdUmR3hRZiSeKXZC5W8Qf6weV56I0,4462
|
|
11
|
+
sdxloop/cli/tui.py,sha256=Lxiv0VdwkU_CtZowGeYuLGUTidxAhi1zwx1iUVnO9NU,4724
|
|
12
|
+
sdxloop/engine/__init__.py,sha256=8UJ3y0FEYyOcIkFboQBtXXkmsB3D6Npo2dJwkjXS6Io,461
|
|
13
|
+
sdxloop/engine/engine.py,sha256=69cyoUpwCYSf84w9b9ztCnqK5TwECvtFg6W0ZJ_UZ68,14759
|
|
14
|
+
sdxloop/engine/model.py,sha256=ZwrwgOxvi6IfXS61eM_tGMYl0DijkogGfgJraA2ivzk,4312
|
|
15
|
+
sdxloop/engine/phases.py,sha256=RrdtTh1pzJmtlgLDJO8EMO4nAFxMdyn7zY9feeT5VRI,8903
|
|
16
|
+
sdxloop/engine/store.py,sha256=jHudRJs-HXXS_957Hw1-AZk-CZ_LgtFZ5Ze1fwLSKhw,8772
|
|
17
|
+
sdxloop/engine/prompts/__init__.py,sha256=GyVOTZ_QO0V27xdmV8sPbI3WK5911_ysiZrX_FMGw7E,937
|
|
18
|
+
sdxloop/engine/prompts/decompose.md,sha256=_oQ931PknBXW7mtNIQWrjWl1JNPr4vznicYaIj6v14I,1160
|
|
19
|
+
sdxloop/engine/prompts/execute.md,sha256=sFQDHFBwXd5snAi-i3rFdXe9PJPyR7Ry6SIrMOsSwVs,695
|
|
20
|
+
sdxloop/engine/prompts/plan.md,sha256=rgU5OSovIuW6BTBv90Y3QAKeC9o6fgX8rcytp81v7eM,770
|
|
21
|
+
sdxloop/engine/prompts/scrutinize.md,sha256=HMHlXiKXwIM4zf7vmI46GxMk8E0uk_BPBK7F-UVW3f8,1106
|
|
22
|
+
sdxloop/engine/prompts/validate.md,sha256=a8lAKaSoSz16qWJ2oNL5WXR7L4AxSeEbIRPoZlKyNYE,940
|
|
23
|
+
sdxloop/gh/__init__.py,sha256=fqroiIpvQp2EqSqlzAa0c_zmMN3-eUR9qo-av8Z1yEA,246
|
|
24
|
+
sdxloop/gh/ops.py,sha256=-1RD8DP30Nj3ySrEDnTU4O2lk_rp_gdatUYOtV_s3cI,4205
|
|
25
|
+
sdxloop/gh/reporter.py,sha256=KzuTpfpjOIH7dtZkeopGBUDMAnwpYtf7HrgrRS18jRk,2732
|
|
26
|
+
sdxloop/sbx/__init__.py,sha256=EA6RrZUrOsfP7oWByQ_l9MJaAvbQhwvIa3aq1rlA6gY,264
|
|
27
|
+
sdxloop/sbx/cli.py,sha256=kjZKiVPmT70XtXJdKMXOXlGxRNqaSw0CqjDlboI3ht4,6858
|
|
28
|
+
sdxloop/sbx/models.py,sha256=czUotIcyPxz9RYYP19cuv8CYwePvO62Kw8oG55iyjr4,2410
|
|
29
|
+
sdxloop/sbx/pair.py,sha256=ARkOGu2UpHms59oL2O0-9SsJwAbPBP5gcZH2f_tOmfs,3961
|
|
30
|
+
sdxloop/sbx/parse.py,sha256=qCMMXjjMCe-VEmAg2OVfp_1MUVA91eaI0nUIgroz0LQ,2144
|
|
31
|
+
sdxloop/sbx/provision.py,sha256=Z1yTK1lgZe999dkZMHp7cCPkeDxnUdfG9XVjxGFAos0,8051
|
|
32
|
+
sdxloop/sbx/sandbox.py,sha256=P7A4x_NF1Nr7clTx4g9VlYKiEDTJoYJ3PCg1klLF92s,2286
|
|
33
|
+
sdxloop/worker/__init__.py,sha256=CfOP1joUZRLWZKiqQ0hpEHX8-NY8LSW3W5ZCEWbxTdE,227
|
|
34
|
+
sdxloop/worker/client.py,sha256=5Y8DTesKK7p2iUTtD_VgntQ98GyCkPz0bujsGzO-ZeU,9796
|
|
35
|
+
sdxloop/worker/wheel.py,sha256=0wiqOWfy3W0k-GnBW7B1n5ZlykZF82qA61yvG4fH_0I,2564
|
|
36
|
+
sdxloop-0.1.0.dist-info/METADATA,sha256=lCmXPTJYE_L-RnOH7IjOpgBeDCzdex898r3BnIf1ebk,1235
|
|
37
|
+
sdxloop-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
38
|
+
sdxloop-0.1.0.dist-info/entry_points.txt,sha256=s2Me94C3iGEfT_9rs_O1ojQIy2UfNlUUBYH4yjWj9Fg,49
|
|
39
|
+
sdxloop-0.1.0.dist-info/RECORD,,
|