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.
Files changed (60) hide show
  1. {sbxloop-0.6.2 → sbxloop-0.6.3}/PKG-INFO +1 -1
  2. 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
  3. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/_version.py +2 -2
  4. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/cli/app.py +13 -2
  5. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/daemon/loop.py +11 -4
  6. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/sbx/cli.py +18 -3
  7. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/sbx/pair.py +18 -2
  8. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/sbx/prune.py +29 -1
  9. {sbxloop-0.6.2 → sbxloop-0.6.3}/.gitignore +0 -0
  10. {sbxloop-0.6.2 → sbxloop-0.6.3}/README.md +0 -0
  11. {sbxloop-0.6.2 → sbxloop-0.6.3}/hatch_build.py +0 -0
  12. {sbxloop-0.6.2 → sbxloop-0.6.3}/pyproject.toml +0 -0
  13. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/__init__.py +0 -0
  14. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/cli/__init__.py +0 -0
  15. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/cli/doctor.py +0 -0
  16. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/cli/models.py +0 -0
  17. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/cli/tui.py +0 -0
  18. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/config.py +0 -0
  19. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/daemon/__init__.py +0 -0
  20. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/daemon/backlog.py +0 -0
  21. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/daemon/discord.py +0 -0
  22. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/daemon/github.py +0 -0
  23. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/daemon/model.py +0 -0
  24. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/daemon/sources.py +0 -0
  25. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/daemon/store.py +0 -0
  26. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/deliver.py +0 -0
  27. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/engine/__init__.py +0 -0
  28. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/engine/engine.py +0 -0
  29. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/engine/model.py +0 -0
  30. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/engine/phases.py +0 -0
  31. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/engine/prompts/__init__.py +0 -0
  32. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/engine/prompts/decompose.md +0 -0
  33. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/engine/prompts/execute.md +0 -0
  34. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/engine/prompts/plan.md +0 -0
  35. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/engine/prompts/scrutinize.md +0 -0
  36. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/engine/prompts/steer.md +0 -0
  37. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/engine/prompts/validate.md +0 -0
  38. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/engine/store.py +0 -0
  39. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/errors.py +0 -0
  40. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/events.py +0 -0
  41. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/gh/__init__.py +0 -0
  42. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/gh/ops.py +0 -0
  43. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/gh/reporter.py +0 -0
  44. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/hostgit.py +0 -0
  45. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/ids.py +0 -0
  46. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/policy.py +0 -0
  47. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/py.typed +0 -0
  48. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/sbx/__init__.py +0 -0
  49. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/sbx/bake.py +0 -0
  50. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/sbx/conformance.py +0 -0
  51. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/sbx/models.py +0 -0
  52. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/sbx/parse.py +0 -0
  53. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/sbx/provision.py +0 -0
  54. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/sbx/sandbox.py +0 -0
  55. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/sbx/secretstate.py +0 -0
  56. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/toolchains.py +0 -0
  57. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/verifylint.py +0 -0
  58. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/worker/__init__.py +0 -0
  59. {sbxloop-0.6.2 → sbxloop-0.6.3}/src/sbxloop/worker/client.py +0 -0
  60. {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.2
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
@@ -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.2'
22
- __version_tuple__ = version_tuple = (0, 6, 2)
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 classify_sandboxes, format_age, remove_sandbox
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
- remove_sandbox(cli, v.name)
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.sandbox import Sandbox
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
- Sandbox(self.sbx, name).rm()
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
- pass # not there — the common case
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
- scope = [sandbox] if sandbox else ["-g"]
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
- return self.run(*args, check=False).ok
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. Idempotent, best-effort each."""
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
- for sandbox in (self.agent, self.github):
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