sbxloop 0.5.5__tar.gz → 0.5.6__tar.gz
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.
- {sbxloop-0.5.5 → sbxloop-0.5.6}/PKG-INFO +1 -1
- sbxloop-0.5.5/src/sbxloop/_vendor/sbxloop_worker-0.5.5-py3-none-any.whl → sbxloop-0.5.6/src/sbxloop/_vendor/sbxloop_worker-0.5.6-py3-none-any.whl +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/_version.py +2 -2
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/cli/app.py +69 -1
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/config.py +5 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/engine/engine.py +69 -34
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/engine/model.py +3 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/events.py +1 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/sbx/cli.py +16 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/.gitignore +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/README.md +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/hatch_build.py +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/pyproject.toml +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/__init__.py +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/cli/__init__.py +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/cli/doctor.py +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/cli/tui.py +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/deliver.py +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/engine/__init__.py +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/engine/phases.py +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/engine/prompts/__init__.py +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/engine/prompts/decompose.md +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/engine/prompts/execute.md +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/engine/prompts/plan.md +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/engine/prompts/scrutinize.md +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/engine/prompts/validate.md +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/engine/store.py +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/errors.py +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/gh/__init__.py +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/gh/ops.py +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/gh/reporter.py +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/ids.py +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/py.typed +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/sbx/__init__.py +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/sbx/models.py +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/sbx/pair.py +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/sbx/parse.py +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/sbx/provision.py +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/sbx/prune.py +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/sbx/sandbox.py +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/worker/__init__.py +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/worker/client.py +0 -0
- {sbxloop-0.5.5 → sbxloop-0.5.6}/src/sbxloop/worker/wheel.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sbxloop
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.6
|
|
4
4
|
Summary: Agentic loop orchestration on Docker Sandboxes (sbx) with isolated credential domains
|
|
5
5
|
Project-URL: Homepage, https://github.com/brettbergin/sbxloop
|
|
6
6
|
Project-URL: Repository, https://github.com/brettbergin/sbxloop
|
|
Binary file
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.5.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 5,
|
|
21
|
+
__version__ = version = '0.5.6'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 5, 6)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -169,6 +169,10 @@ def _finish(result: RunResult, config: Config) -> None:
|
|
|
169
169
|
for task in result.tasks:
|
|
170
170
|
console.print(f" {task.spec.id}: {task.state} ({task.spec.title})")
|
|
171
171
|
_print_artifacts_summary(result, config)
|
|
172
|
+
if result.kept_sandboxes:
|
|
173
|
+
console.print(f"\n[bold yellow]sandboxes kept:[/] {', '.join(result.kept_sandboxes)}")
|
|
174
|
+
console.print(f" inspect: [cyan]sbxloop shell {result.run_id}[/] (--role github)")
|
|
175
|
+
console.print(f" remove: [cyan]sbxloop sandbox rm --run {result.run_id}[/]")
|
|
172
176
|
raise typer.Exit(0 if result.succeeded else 1)
|
|
173
177
|
|
|
174
178
|
|
|
@@ -193,10 +197,21 @@ def run(
|
|
|
193
197
|
keep_sandboxes: Annotated[
|
|
194
198
|
bool, typer.Option("--keep-sandboxes", help="Do not remove sandboxes at the end.")
|
|
195
199
|
] = False,
|
|
200
|
+
keep_on_failure: Annotated[
|
|
201
|
+
bool | None,
|
|
202
|
+
typer.Option(
|
|
203
|
+
"--keep-on-failure/--no-keep-on-failure",
|
|
204
|
+
help="Keep the sandbox pair alive when the run fails (inspect with `sbxloop shell`).",
|
|
205
|
+
),
|
|
206
|
+
] = None,
|
|
196
207
|
tui: Annotated[bool, typer.Option("--tui/--no-tui", help="Live dashboard.")] = True,
|
|
197
208
|
) -> None:
|
|
198
209
|
"""Run an agentic loop for OUTCOME in a fresh sandbox pair."""
|
|
199
|
-
config = _config_with_overrides(
|
|
210
|
+
config = _config_with_overrides(
|
|
211
|
+
model=model,
|
|
212
|
+
keep_sandboxes=keep_sandboxes or None,
|
|
213
|
+
keep_on_failure=keep_on_failure,
|
|
214
|
+
)
|
|
200
215
|
if report is not None:
|
|
201
216
|
config = config.model_copy(
|
|
202
217
|
update={"github": config.github.model_copy(update={"report": report})}
|
|
@@ -331,6 +346,57 @@ def logs(
|
|
|
331
346
|
time.sleep(0.5)
|
|
332
347
|
|
|
333
348
|
|
|
349
|
+
# Prefer bash when the template has it, fall back to POSIX sh. `sbx exec`
|
|
350
|
+
# has no documented -it flags; terminal attachment is inherited stdio.
|
|
351
|
+
_INTERACTIVE_SHELL = ("sh", "-c", "command -v bash >/dev/null && exec bash -l; exec sh -l")
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@app.command()
|
|
355
|
+
def shell(
|
|
356
|
+
run_id: Annotated[str, typer.Argument(help="Run id.")],
|
|
357
|
+
role: Annotated[
|
|
358
|
+
str, typer.Option("--role", help="Which sandbox of the pair: agent or github.")
|
|
359
|
+
] = "agent",
|
|
360
|
+
command: Annotated[
|
|
361
|
+
str | None,
|
|
362
|
+
typer.Option(
|
|
363
|
+
"--command", "-c", help="Run one shell command instead of an interactive shell."
|
|
364
|
+
),
|
|
365
|
+
] = None,
|
|
366
|
+
) -> None:
|
|
367
|
+
"""Open a shell inside a run's sandbox (kept, in-flight, or leaked).
|
|
368
|
+
|
|
369
|
+
Attaching to an in-flight run is meant as observation: the worker owns
|
|
370
|
+
its env files and workspace, so avoid mutating them mid-phase.
|
|
371
|
+
"""
|
|
372
|
+
if role not in ("agent", "github"):
|
|
373
|
+
console.print(f"[bold red]invalid --role {role!r}:[/] must be agent or github")
|
|
374
|
+
raise typer.Exit(2)
|
|
375
|
+
config = load_config()
|
|
376
|
+
store = _store(config)
|
|
377
|
+
try:
|
|
378
|
+
store.get_run(run_id)
|
|
379
|
+
except SdxloopError as exc:
|
|
380
|
+
console.print(f"[bold red]{exc}[/]")
|
|
381
|
+
raise typer.Exit(2) from exc
|
|
382
|
+
cli = SbxCLI(app_name=config.app_name or None)
|
|
383
|
+
name = sandbox_name(run_id, "agent" if role == "agent" else "github")
|
|
384
|
+
try:
|
|
385
|
+
live = any(info.name == name for info in cli.ls())
|
|
386
|
+
except SdxloopError as exc:
|
|
387
|
+
console.print(f"[bold red]{exc}[/]")
|
|
388
|
+
raise typer.Exit(2) from exc
|
|
389
|
+
if not live:
|
|
390
|
+
console.print(
|
|
391
|
+
f"[bold red]sandbox {name} is not running.[/] Sandboxes are removed at run end "
|
|
392
|
+
"unless kept (keep_on_failure, --keep-sandboxes), and kept ones may have been "
|
|
393
|
+
"pruned since."
|
|
394
|
+
)
|
|
395
|
+
raise typer.Exit(2)
|
|
396
|
+
argv = ("sh", "-lc", command) if command else _INTERACTIVE_SHELL
|
|
397
|
+
raise typer.Exit(cli.exec_interactive(name, argv))
|
|
398
|
+
|
|
399
|
+
|
|
334
400
|
@app.command()
|
|
335
401
|
def artifacts(
|
|
336
402
|
run_id: Annotated[str, typer.Argument(help="Run id.")],
|
|
@@ -556,6 +622,8 @@ app_name = ""
|
|
|
556
622
|
state_dir = ".sbxloop"
|
|
557
623
|
# Keep sandboxes around after a run (for debugging).
|
|
558
624
|
keep_sandboxes = false
|
|
625
|
+
# Keep the pair alive only when a run fails; inspect with `sbxloop shell <run>`.
|
|
626
|
+
keep_on_failure = false
|
|
559
627
|
# Worker transport: "stream" (default) or "poll".
|
|
560
628
|
worker_transport = "stream"
|
|
561
629
|
# Secret injection: "proxy" (sbx keychain proxy; recommended) or "plain-env".
|
|
@@ -85,6 +85,11 @@ class Config(_ConfigModel):
|
|
|
85
85
|
app_name: str = ""
|
|
86
86
|
state_dir: Path = Path(".sbxloop")
|
|
87
87
|
keep_sandboxes: bool = False
|
|
88
|
+
# Keep the pair alive only when a run fails, so the evidence (worker
|
|
89
|
+
# stderr, install leftovers, workspace state) survives for
|
|
90
|
+
# `sbxloop shell <run>`. Kept runs are marked in the state DB and
|
|
91
|
+
# eventually collectable via `sbxloop sandbox prune --include-kept`.
|
|
92
|
+
keep_on_failure: bool = False
|
|
88
93
|
worker_transport: WorkerTransport = "stream"
|
|
89
94
|
secret_strategy: SecretStrategy = "proxy"
|
|
90
95
|
# Advanced: in-sandbox interpreter for the worker, and whether to run the
|
|
@@ -113,49 +113,60 @@ class LoopEngine:
|
|
|
113
113
|
pair = provisioner.ensure_pair(run_id)
|
|
114
114
|
assert pair.workspace is not None
|
|
115
115
|
self.store.set_run_workspace(run_id, pair.workspace, pair.mounted)
|
|
116
|
+
if pair.keep:
|
|
117
|
+
# keep_sandboxes: mark up front so `sandbox prune` respects it.
|
|
118
|
+
self.store.set_run_kept(run_id, "manual")
|
|
116
119
|
try:
|
|
117
120
|
with pair:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
transport=self.config.worker_transport,
|
|
122
|
-
python=self.worker_python,
|
|
123
|
-
)
|
|
124
|
-
github = (
|
|
125
|
-
WorkerClient(
|
|
126
|
-
pair.github,
|
|
121
|
+
try:
|
|
122
|
+
agent = WorkerClient(
|
|
123
|
+
pair.agent,
|
|
127
124
|
self.bus,
|
|
128
125
|
transport=self.config.worker_transport,
|
|
129
126
|
python=self.worker_python,
|
|
130
127
|
)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
reporter, detach = self._attach_reporter(github, run_id, outcome)
|
|
141
|
-
try:
|
|
142
|
-
phases = PhaseRunner(
|
|
143
|
-
agent, self.config, run_id, outcome, workdir=pair.agent_workdir
|
|
128
|
+
github = (
|
|
129
|
+
WorkerClient(
|
|
130
|
+
pair.github,
|
|
131
|
+
self.bus,
|
|
132
|
+
transport=self.config.worker_transport,
|
|
133
|
+
python=self.worker_python,
|
|
134
|
+
)
|
|
135
|
+
if pair.github is not None
|
|
136
|
+
else None
|
|
144
137
|
)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
138
|
+
if self.install_workers:
|
|
139
|
+
# ensure_dev_tools: the agent builds projects in this VM
|
|
140
|
+
# (venvs, pip) — the github sandbox only runs API ops.
|
|
141
|
+
agent.install(extras="copilot", ensure_dev_tools=True)
|
|
142
|
+
if github is not None:
|
|
143
|
+
github.install(extras="")
|
|
144
|
+
reporter, detach = self._attach_reporter(github, run_id, outcome)
|
|
145
|
+
try:
|
|
146
|
+
phases = PhaseRunner(
|
|
147
|
+
agent, self.config, run_id, outcome, workdir=pair.agent_workdir
|
|
148
|
+
)
|
|
149
|
+
state = self._run_phases(run_id, phases, deadline, pair)
|
|
150
|
+
# Summary must post while the github sandbox is alive;
|
|
151
|
+
# on an infra exception the run is resumable and the
|
|
152
|
+
# resumed run reopens the same tracking issue.
|
|
153
|
+
if reporter is not None:
|
|
154
|
+
reporter.close_run(run_id, state)
|
|
155
|
+
finally:
|
|
156
|
+
detach()
|
|
157
|
+
# Harvest even when a phase raised: the sandbox is still
|
|
158
|
+
# alive here, and partial artifacts beat none.
|
|
159
|
+
self._harvest(run_id, pair)
|
|
160
|
+
self._report_artifacts(run_id, pair)
|
|
161
|
+
except SdxloopError:
|
|
162
|
+
# Infra failures (install, worker, sbx) are exactly what
|
|
163
|
+
# gets diagnosed in-sandbox; decide keep before pair exit.
|
|
164
|
+
self._keep_on_failure(run_id, pair)
|
|
165
|
+
raise
|
|
157
166
|
if state == "completed":
|
|
158
167
|
self._deliver(run_id, outcome, pair, github)
|
|
168
|
+
else:
|
|
169
|
+
self._keep_on_failure(run_id, pair)
|
|
159
170
|
except SdxloopError:
|
|
160
171
|
# State is already persisted; the exception is the kill signal.
|
|
161
172
|
raise
|
|
@@ -168,6 +179,30 @@ class LoopEngine:
|
|
|
168
179
|
tasks=tasks,
|
|
169
180
|
workspace=pair.workspace,
|
|
170
181
|
mounted=pair.mounted,
|
|
182
|
+
kept_sandboxes=self._pair_names(pair) if pair.keep else [],
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
@staticmethod
|
|
186
|
+
def _pair_names(pair: SandboxPair) -> list[str]:
|
|
187
|
+
return [s.name for s in (pair.agent, pair.github) if s is not None]
|
|
188
|
+
|
|
189
|
+
def _keep_on_failure(self, run_id: str, pair: SandboxPair) -> None:
|
|
190
|
+
"""Flip the pair to kept when configured, so a failed run's evidence
|
|
191
|
+
survives for `sbxloop shell`. Marked in the DB for `sandbox prune`."""
|
|
192
|
+
if not self.config.keep_on_failure or pair.keep:
|
|
193
|
+
return
|
|
194
|
+
pair.keep = True
|
|
195
|
+
self.store.set_run_kept(run_id, "debug")
|
|
196
|
+
names = self._pair_names(pair)
|
|
197
|
+
self.bus.emit(
|
|
198
|
+
HostEventTypes.RUN_KEEP,
|
|
199
|
+
run_id,
|
|
200
|
+
sandboxes=names,
|
|
201
|
+
reason="debug",
|
|
202
|
+
message=(
|
|
203
|
+
f"sandboxes kept for debugging: {', '.join(names)} — "
|
|
204
|
+
f"inspect with `sbxloop shell {run_id}`"
|
|
205
|
+
),
|
|
171
206
|
)
|
|
172
207
|
|
|
173
208
|
def _attach_reporter(
|
|
@@ -156,6 +156,9 @@ class RunResult(_Model):
|
|
|
156
156
|
tasks: list[TaskRecord] = Field(default_factory=list)
|
|
157
157
|
workspace: Path | None = None
|
|
158
158
|
mounted: bool = False
|
|
159
|
+
# Sandbox names deliberately left alive (keep_sandboxes/keep_on_failure),
|
|
160
|
+
# so callers can point the user at `sbxloop shell`.
|
|
161
|
+
kept_sandboxes: list[str] = Field(default_factory=list)
|
|
159
162
|
|
|
160
163
|
@property
|
|
161
164
|
def succeeded(self) -> bool:
|
|
@@ -155,6 +155,22 @@ class SbxCLI:
|
|
|
155
155
|
raise self._error_for(result)
|
|
156
156
|
return result
|
|
157
157
|
|
|
158
|
+
def exec_interactive(self, name: str, cmd: Sequence[str]) -> int:
|
|
159
|
+
"""Run a command in a sandbox with the caller's terminal attached
|
|
160
|
+
(stdin/stdout/stderr inherited); returns the command's exit code.
|
|
161
|
+
|
|
162
|
+
`sbx exec` documents no -it-style flags; terminal attachment is
|
|
163
|
+
simply inheriting the caller's stdio.
|
|
164
|
+
"""
|
|
165
|
+
argv = self.argv("exec", name, *cmd)
|
|
166
|
+
try:
|
|
167
|
+
return subprocess.run(argv, check=False).returncode
|
|
168
|
+
except FileNotFoundError as exc:
|
|
169
|
+
raise SbxNotFoundError(
|
|
170
|
+
f"sbx binary {self.binary!r} not found on PATH",
|
|
171
|
+
argv=argv,
|
|
172
|
+
) from exc
|
|
173
|
+
|
|
158
174
|
def cp(self, src: str, dst: str, *, timeout: float | None = None) -> None:
|
|
159
175
|
self.run("cp", src, dst, timeout=timeout)
|
|
160
176
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|