sbxloop 0.6.2__tar.gz → 0.6.3__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.6.2 → sbxloop-0.6.3}/PKG-INFO +1 -1
- sbxloop-0.6.2/src/sbxloop/_vendor/sbxloop_worker-0.6.2-py3-none-any.whl → sbxloop-0.6.3/src/sbxloop/_vendor/sbxloop_worker-0.6.3-py3-none-any.whl +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/_version.py +2 -2
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/cli/app.py +13 -2
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/daemon/loop.py +11 -4
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/sbx/cli.py +18 -3
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/sbx/pair.py +18 -2
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/sbx/prune.py +29 -1
- {sbxloop-0.6.2 → sbxloop-0.6.3}/.gitignore +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/README.md +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/hatch_build.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/pyproject.toml +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/__init__.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/cli/__init__.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/cli/doctor.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/cli/models.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/cli/tui.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/config.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/daemon/__init__.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/daemon/backlog.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/daemon/discord.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/daemon/github.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/daemon/model.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/daemon/sources.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/daemon/store.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/deliver.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/engine/__init__.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/engine/engine.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/engine/model.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/engine/phases.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/engine/prompts/__init__.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/engine/prompts/decompose.md +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/engine/prompts/execute.md +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/engine/prompts/plan.md +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/engine/prompts/scrutinize.md +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/engine/prompts/steer.md +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/engine/prompts/validate.md +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/engine/store.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/errors.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/events.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/gh/__init__.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/gh/ops.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/gh/reporter.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/hostgit.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/ids.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/policy.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/py.typed +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/sbx/__init__.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/sbx/bake.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/sbx/conformance.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/sbx/models.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/sbx/parse.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/sbx/provision.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/sbx/sandbox.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/sbx/secretstate.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/toolchains.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/verifylint.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/worker/__init__.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/worker/client.py +0 -0
- {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/worker/wheel.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sbxloop
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.3
|
|
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.6.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 6,
|
|
21
|
+
__version__ = version = '0.6.3'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 6, 3)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -42,7 +42,12 @@ from sbxloop.sbx.cli import SbxCLI
|
|
|
42
42
|
from sbxloop.sbx.models import SandboxRole
|
|
43
43
|
from sbxloop.sbx.pair import cleanup_registry
|
|
44
44
|
from sbxloop.sbx.provision import sandbox_name
|
|
45
|
-
from sbxloop.sbx.prune import
|
|
45
|
+
from sbxloop.sbx.prune import (
|
|
46
|
+
classify_sandboxes,
|
|
47
|
+
format_age,
|
|
48
|
+
remove_run_sandbox,
|
|
49
|
+
remove_sandbox,
|
|
50
|
+
)
|
|
46
51
|
from sbxloop.sbx.secretstate import (
|
|
47
52
|
COPILOT_TOKEN_ENV,
|
|
48
53
|
SANDBOX_SCOPE_PREFIX,
|
|
@@ -1084,7 +1089,13 @@ def sandbox_prune(
|
|
|
1084
1089
|
failures = 0
|
|
1085
1090
|
for v in orphans:
|
|
1086
1091
|
try:
|
|
1087
|
-
|
|
1092
|
+
if v.role in ("agent", "github"):
|
|
1093
|
+
# A pruned run sandbox takes its secret registrations with
|
|
1094
|
+
# it; otherwise a later run under the same name (resume)
|
|
1095
|
+
# cannot replace them and comes up with the proxy sentinel.
|
|
1096
|
+
remove_run_sandbox(cli, v.name, v.role) # type: ignore[arg-type]
|
|
1097
|
+
else:
|
|
1098
|
+
remove_sandbox(cli, v.name)
|
|
1088
1099
|
except SbxloopError as exc:
|
|
1089
1100
|
failures += 1
|
|
1090
1101
|
console.print(f"[yellow]skip {v.name}:[/] {exc}")
|
|
@@ -35,7 +35,7 @@ from sbxloop.events import EventBus, HostEventTypes
|
|
|
35
35
|
from sbxloop.ids import new_run_id
|
|
36
36
|
from sbxloop.sbx.cli import SbxCLI
|
|
37
37
|
from sbxloop.sbx.provision import sandbox_name
|
|
38
|
-
from sbxloop.sbx.
|
|
38
|
+
from sbxloop.sbx.prune import remove_run_sandbox, remove_run_sandbox_secrets
|
|
39
39
|
|
|
40
40
|
logger = logging.getLogger(__name__)
|
|
41
41
|
|
|
@@ -531,15 +531,22 @@ class DaemonLoop:
|
|
|
531
531
|
self.dstore.mark_requeued_unstarted(item.item_id, now)
|
|
532
532
|
|
|
533
533
|
def _remove_stale_run_sandboxes(self, run_id: str) -> None:
|
|
534
|
+
"""A dead process leaves the run's microVMs — and their secret
|
|
535
|
+
registrations — behind. Both must go before resume re-provisions
|
|
536
|
+
under the same names: a lingering secret cannot be replaced, so the
|
|
537
|
+
agent would come up holding the proxy sentinel and the Copilot SDK
|
|
538
|
+
401s (field failure rgn9ccjam)."""
|
|
534
539
|
if self.sbx is None:
|
|
535
540
|
return
|
|
536
541
|
for role in ("agent", "github"):
|
|
537
542
|
name = sandbox_name(run_id, role)
|
|
538
543
|
try:
|
|
539
|
-
|
|
540
|
-
self._notify(f"recovery: removed stale sandbox {name}")
|
|
544
|
+
remove_run_sandbox(self.sbx, name, role)
|
|
545
|
+
self._notify(f"recovery: removed stale sandbox {name} (and its secrets)")
|
|
541
546
|
except SbxError:
|
|
542
|
-
|
|
547
|
+
# No such sandbox — the common case — but a secret may
|
|
548
|
+
# still linger from a rollback race; clearing it is cheap.
|
|
549
|
+
remove_run_sandbox_secrets(self.sbx, name, role)
|
|
543
550
|
|
|
544
551
|
def _result_from_record(self, run_id: str) -> RunResult:
|
|
545
552
|
record = self.store.get_run(run_id)
|
|
@@ -336,9 +336,20 @@ class SbxCLI:
|
|
|
336
336
|
existing secret, so provisioning removes and re-sets. Removal syntax
|
|
337
337
|
for custom secrets is not a stable documented API, hence best-effort
|
|
338
338
|
(callers keep the existing value when removal is rejected).
|
|
339
|
+
|
|
340
|
+
``-f`` is load-bearing: sbx 0.38 asks "Delete selected secret?
|
|
341
|
+
(y/N)" and, with no TTY, prints "Cancelled" and exits 0 — the
|
|
342
|
+
removal silently does nothing while looking accepted (field
|
|
343
|
+
failure rgn9ccjam: every run leaked its registrations, and a
|
|
344
|
+
daemon resume then could not replace the stale one). Scope goes
|
|
345
|
+
via ``--sandbox``; the positional form is deprecated in 0.38.
|
|
346
|
+
"No secret found" also exits 0, so the exit code alone cannot tell
|
|
347
|
+
removed from absent: the return value is "something was actually
|
|
348
|
+
deleted" — sbx 0.38 says ``Deleted ...``; a silent exit 0 (older
|
|
349
|
+
sbx) is trusted. Callers that ladder through candidate scopes
|
|
350
|
+
depend on this to keep trying instead of stopping at a no-op.
|
|
339
351
|
"""
|
|
340
|
-
|
|
341
|
-
args = ["secret", "rm", *scope]
|
|
352
|
+
args = ["secret", "rm"]
|
|
342
353
|
if service:
|
|
343
354
|
args.append(service)
|
|
344
355
|
else:
|
|
@@ -346,4 +357,8 @@ class SbxCLI:
|
|
|
346
357
|
if host:
|
|
347
358
|
args += ["--host", host]
|
|
348
359
|
args += ["--env", env]
|
|
349
|
-
|
|
360
|
+
if sandbox:
|
|
361
|
+
args += ["--sandbox", sandbox]
|
|
362
|
+
args.append("-f")
|
|
363
|
+
result = self.run(*args, check=False)
|
|
364
|
+
return result.ok and (not result.stdout.strip() or "Deleted" in result.stdout)
|
|
@@ -22,6 +22,8 @@ from collections.abc import Callable
|
|
|
22
22
|
from pathlib import Path
|
|
23
23
|
from typing import Any
|
|
24
24
|
|
|
25
|
+
from sbxloop.sbx.models import SandboxRole
|
|
26
|
+
from sbxloop.sbx.prune import remove_run_sandbox_secrets
|
|
25
27
|
from sbxloop.sbx.sandbox import WORK_DIR, Sandbox
|
|
26
28
|
|
|
27
29
|
logger = logging.getLogger(__name__)
|
|
@@ -68,12 +70,22 @@ class SandboxPair:
|
|
|
68
70
|
self.cleanup()
|
|
69
71
|
|
|
70
72
|
def cleanup(self) -> None:
|
|
71
|
-
"""Stop and remove the pair's sandboxes
|
|
73
|
+
"""Stop and remove the pair's sandboxes and their secret registrations.
|
|
74
|
+
Idempotent, best-effort each.
|
|
75
|
+
|
|
76
|
+
``sbx rm`` does not unregister the secrets scoped to the sandbox's
|
|
77
|
+
name; left behind they accrete per run and block a later provision
|
|
78
|
+
under the same name (daemon resume) from replacing them.
|
|
79
|
+
"""
|
|
72
80
|
if self._cleaned:
|
|
73
81
|
return
|
|
74
82
|
self._cleaned = True
|
|
75
83
|
cleanup_registry.unregister(self)
|
|
76
|
-
|
|
84
|
+
roles: tuple[tuple[Sandbox | None, SandboxRole], ...] = (
|
|
85
|
+
(self.agent, "agent"),
|
|
86
|
+
(self.github, "github"),
|
|
87
|
+
)
|
|
88
|
+
for sandbox, role in roles:
|
|
77
89
|
if sandbox is None:
|
|
78
90
|
continue
|
|
79
91
|
try:
|
|
@@ -84,6 +96,10 @@ class SandboxPair:
|
|
|
84
96
|
sandbox.rm()
|
|
85
97
|
except Exception:
|
|
86
98
|
logger.warning("failed to remove sandbox %s", sandbox.name, exc_info=True)
|
|
99
|
+
try:
|
|
100
|
+
remove_run_sandbox_secrets(sandbox.cli, sandbox.name, role)
|
|
101
|
+
except Exception:
|
|
102
|
+
logger.warning("failed to remove secrets of %s", sandbox.name, exc_info=True)
|
|
87
103
|
|
|
88
104
|
|
|
89
105
|
class CleanupRegistry:
|
|
@@ -23,7 +23,8 @@ from sbxloop.engine.store import StateStore
|
|
|
23
23
|
from sbxloop.errors import SbxError, StateError
|
|
24
24
|
from sbxloop.ids import is_run_id
|
|
25
25
|
from sbxloop.sbx.cli import SbxCLI
|
|
26
|
-
from sbxloop.sbx.models import SandboxInfo
|
|
26
|
+
from sbxloop.sbx.models import SandboxInfo, SandboxRole
|
|
27
|
+
from sbxloop.sbx.secretstate import COPILOT_TOKEN_ENV, COPILOT_TOKEN_HOST
|
|
27
28
|
|
|
28
29
|
# A run in one of these states has already had (or never needed) its
|
|
29
30
|
# teardown: any of its sandboxes still present are leaked. "failed" is safe
|
|
@@ -186,6 +187,33 @@ def remove_sandbox(cli: SbxCLI, name: str) -> None:
|
|
|
186
187
|
cli.rm(name, force=True)
|
|
187
188
|
|
|
188
189
|
|
|
190
|
+
def remove_run_sandbox_secrets(cli: SbxCLI, name: str, role: SandboxRole) -> None:
|
|
191
|
+
"""Best-effort: unregister the secrets provisioning bound to ``name``.
|
|
192
|
+
|
|
193
|
+
``sbx rm`` removes the microVM but not the secret registrations keyed
|
|
194
|
+
by its name. Left behind, they poison the next provision under the same
|
|
195
|
+
name: replace-on-exists cannot replace, keeps the stale entry, and the
|
|
196
|
+
agent sandbox ends up with the proxy sentinel instead of a usable token
|
|
197
|
+
(field failure rgn9ccjam — a daemon recovery resumed a killed run and
|
|
198
|
+
the Copilot SDK got 401). Agent sandboxes carry the Copilot custom
|
|
199
|
+
secret; github sandboxes the built-in ``github`` service secret.
|
|
200
|
+
"""
|
|
201
|
+
with contextlib.suppress(SbxError):
|
|
202
|
+
if role == "agent":
|
|
203
|
+
cli.secret_rm(host=COPILOT_TOKEN_HOST, env=COPILOT_TOKEN_ENV, sandbox=name)
|
|
204
|
+
else:
|
|
205
|
+
cli.secret_rm(service="github", sandbox=name)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def remove_run_sandbox(cli: SbxCLI, name: str, role: SandboxRole) -> None:
|
|
209
|
+
"""Remove a run's sandbox AND its registered secrets — the pair that a
|
|
210
|
+
dead process leaves behind and a re-provision under the same name
|
|
211
|
+
trips over. Sandbox removal errors propagate; secret removal is
|
|
212
|
+
best-effort (registration syntax is not a stable sbx API)."""
|
|
213
|
+
remove_sandbox(cli, name)
|
|
214
|
+
remove_run_sandbox_secrets(cli, name, role)
|
|
215
|
+
|
|
216
|
+
|
|
189
217
|
def count_orphans(cli: SbxCLI, store: StateStore) -> int:
|
|
190
218
|
"""Orphan-candidate count with default thresholds (doctor's view)."""
|
|
191
219
|
return sum(1 for v in classify_sandboxes(cli.ls(), store) if v.orphan)
|
|
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
|
|
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
|