sbxloop 0.5.76__tar.gz → 0.5.78__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.76 → sbxloop-0.5.78}/PKG-INFO +2 -2
- sbxloop-0.5.76/src/sbxloop/_vendor/sbxloop_worker-0.5.76-py3-none-any.whl → sbxloop-0.5.78/src/sbxloop/_vendor/sbxloop_worker-0.5.78-py3-none-any.whl +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/_version.py +2 -2
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/cli/app.py +18 -5
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/cli/doctor.py +2 -2
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/config.py +45 -6
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/engine/model.py +54 -6
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/sbx/conformance.py +14 -10
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/sbx/parse.py +30 -12
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/sbx/provision.py +31 -5
- {sbxloop-0.5.76 → sbxloop-0.5.78}/.gitignore +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/README.md +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/hatch_build.py +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/pyproject.toml +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/__init__.py +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/cli/__init__.py +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/cli/models.py +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/cli/tui.py +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/deliver.py +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/engine/__init__.py +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/engine/engine.py +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/engine/phases.py +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/engine/prompts/__init__.py +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/engine/prompts/decompose.md +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/engine/prompts/execute.md +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/engine/prompts/plan.md +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/engine/prompts/scrutinize.md +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/engine/prompts/steer.md +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/engine/prompts/validate.md +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/engine/store.py +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/errors.py +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/events.py +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/gh/__init__.py +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/gh/ops.py +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/gh/reporter.py +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/ids.py +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/policy.py +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/py.typed +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/sbx/__init__.py +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/sbx/bake.py +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/sbx/cli.py +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/sbx/models.py +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/sbx/pair.py +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/sbx/prune.py +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/sbx/sandbox.py +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/sbx/secretstate.py +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/toolchains.py +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/worker/__init__.py +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/worker/client.py +0 -0
- {sbxloop-0.5.76 → sbxloop-0.5.78}/src/sbxloop/worker/wheel.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: sbxloop
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.78
|
|
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.78'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 5, 78)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -1274,11 +1274,24 @@ deny = []
|
|
|
1274
1274
|
# deliver_draft = false
|
|
1275
1275
|
|
|
1276
1276
|
[artifacts]
|
|
1277
|
-
# Path components excluded from artifact listings and delivery,
|
|
1278
|
-
# any depth (a bare name each, no slashes).
|
|
1279
|
-
#
|
|
1280
|
-
#
|
|
1281
|
-
|
|
1277
|
+
# Path components excluded from artifact listings, harvest and delivery,
|
|
1278
|
+
# matched at any depth (a bare name each, no slashes). Setting this replaces
|
|
1279
|
+
# the default list below rather than adding to it. The default drops run/VCS
|
|
1280
|
+
# state plus regenerable dependency and build trees — dot-path artifacts like
|
|
1281
|
+
# .github/ or .gitignore are kept, and so are the ambiguous generic names
|
|
1282
|
+
# (bin, build, dist, out, lib, vendor), which you can add if you want them
|
|
1283
|
+
# dropped. Exclusions are counted and surfaced, never silent.
|
|
1284
|
+
exclude = [
|
|
1285
|
+
".git", ".sbxloop",
|
|
1286
|
+
".mypy_cache", ".nox", ".pytest_cache", ".ruff_cache", ".tox",
|
|
1287
|
+
".venv", "venv", "__pycache__", # Python
|
|
1288
|
+
"node_modules", # JavaScript / TypeScript
|
|
1289
|
+
"target", # Rust (cargo), Java (Maven)
|
|
1290
|
+
".gradle", # Java (Gradle)
|
|
1291
|
+
"obj", # C# / .NET
|
|
1292
|
+
".bundle", # Ruby (bundler)
|
|
1293
|
+
"CMakeFiles", # C / C++
|
|
1294
|
+
]
|
|
1282
1295
|
|
|
1283
1296
|
[budgets]
|
|
1284
1297
|
max_revisions_per_task = 2
|
|
@@ -31,7 +31,7 @@ from sbxloop.sbx.secretstate import COPILOT_TOKEN_ENV
|
|
|
31
31
|
from sbxloop.worker.wheel import resolve_worker_wheel
|
|
32
32
|
from sbxloop_worker.backends.copilot import SDK_PERMISSION_KINDS, installed_sdk_permission_kinds
|
|
33
33
|
|
|
34
|
-
TESTED_SBX_SERIES = "0.
|
|
34
|
+
TESTED_SBX_SERIES = "0.38"
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
@dataclass
|
|
@@ -163,7 +163,7 @@ def collect_checks(
|
|
|
163
163
|
"consider filing an issue to prefer host-side stats"
|
|
164
164
|
if stats_available
|
|
165
165
|
else "no host-side `sbx stats`; resource telemetry samples in-VM "
|
|
166
|
-
|
|
166
|
+
"on the worker heartbeat",
|
|
167
167
|
hard=False,
|
|
168
168
|
)
|
|
169
169
|
)
|
|
@@ -153,10 +153,28 @@ class ArtifactsConfig(_ConfigModel):
|
|
|
153
153
|
"""What artifact listings and delivery leave out, and how harvesting works.
|
|
154
154
|
|
|
155
155
|
``exclude`` entries are single path components matched at any depth;
|
|
156
|
-
a file is excluded when any component of its path matches.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
156
|
+
a file is excluded when any component of its path matches. Setting it
|
|
157
|
+
replaces the default list wholesale rather than adding to it.
|
|
158
|
+
|
|
159
|
+
The default drops run/VCS state (``.git``, ``.sbxloop``) plus the
|
|
160
|
+
machine-generated dependency and build trees of the supported languages
|
|
161
|
+
— ``node_modules``, ``__pycache__``, ``.venv``/``venv``, ``target``,
|
|
162
|
+
``obj``, ``.gradle``, ``.bundle``, ``CMakeFiles`` and the Python tool
|
|
163
|
+
caches. Those are regenerable, can reach hundreds of MB, and are exactly
|
|
164
|
+
what nobody wants in an ``sbxloop artifacts`` listing or a ``--deliver``
|
|
165
|
+
PR diff. Dot-path artifacts like ``.github/`` or ``.gitignore`` are still
|
|
166
|
+
delivered.
|
|
167
|
+
|
|
168
|
+
Generic names that mean build output in one ecosystem and hand-written
|
|
169
|
+
content in another — ``bin``, ``build``, ``dist``, ``out``, ``lib``,
|
|
170
|
+
``vendor`` — are deliberately *not* excluded; add them here if your
|
|
171
|
+
project wants them dropped. Exclusions are always counted and surfaced,
|
|
172
|
+
never silent.
|
|
173
|
+
|
|
174
|
+
The list covers every supported language regardless of
|
|
175
|
+
``[sandbox] languages``: that key governs which toolchains are
|
|
176
|
+
pre-installed, not which ones the agent ends up using, and it defaults to
|
|
177
|
+
Python alone — see ``engine.model.DEFAULT_ARTIFACT_EXCLUDES``.
|
|
160
178
|
|
|
161
179
|
``harvest_mode`` controls when unmounted-run artifacts are copied to the
|
|
162
180
|
host. ``"per-task"`` (default) copies after every task boundary plus the
|
|
@@ -166,8 +184,29 @@ class ArtifactsConfig(_ConfigModel):
|
|
|
166
184
|
"""
|
|
167
185
|
|
|
168
186
|
# Mirrors engine.model.DEFAULT_ARTIFACT_EXCLUDES (kept literal here —
|
|
169
|
-
# importing engine.model from config would be a circular import).
|
|
170
|
-
|
|
187
|
+
# importing engine.model from config would be a circular import). See
|
|
188
|
+
# that module for why each entry is in and why "bin"/"build"/"vendor"
|
|
189
|
+
# are out; test_config_default_mirrors_model_default pins the two.
|
|
190
|
+
exclude: list[str] = Field(
|
|
191
|
+
default_factory=lambda: [
|
|
192
|
+
".git",
|
|
193
|
+
".sbxloop",
|
|
194
|
+
".mypy_cache",
|
|
195
|
+
".nox",
|
|
196
|
+
".pytest_cache",
|
|
197
|
+
".ruff_cache",
|
|
198
|
+
".tox",
|
|
199
|
+
".venv",
|
|
200
|
+
"__pycache__",
|
|
201
|
+
"venv",
|
|
202
|
+
"node_modules",
|
|
203
|
+
"target",
|
|
204
|
+
".gradle",
|
|
205
|
+
"obj",
|
|
206
|
+
".bundle",
|
|
207
|
+
"CMakeFiles",
|
|
208
|
+
]
|
|
209
|
+
)
|
|
171
210
|
harvest_mode: HarvestMode = "per-task"
|
|
172
211
|
|
|
173
212
|
@field_validator("exclude")
|
|
@@ -226,12 +226,60 @@ class RunResult(_Model):
|
|
|
226
226
|
return self.state == "completed"
|
|
227
227
|
|
|
228
228
|
|
|
229
|
-
# Path components excluded from artifact listings and delivery by
|
|
230
|
-
# A denylist, not "anything dot-prefixed": agents legitimately
|
|
231
|
-
# .github/workflows, .gitignore, .env.example and friends — only
|
|
232
|
-
#
|
|
233
|
-
#
|
|
234
|
-
|
|
229
|
+
# Path components excluded from artifact listings, harvest and delivery by
|
|
230
|
+
# default. A denylist, not "anything dot-prefixed": agents legitimately
|
|
231
|
+
# produce .github/workflows, .gitignore, .env.example and friends — only
|
|
232
|
+
# machine-generated dependency and build trees are dropped. An agent's .git
|
|
233
|
+
# or node_modules would otherwise swamp listings, balloon an unmounted run's
|
|
234
|
+
# harvest, and land in a delivery PR diff. Overridable via [artifacts]
|
|
235
|
+
# exclude (which replaces this list wholesale).
|
|
236
|
+
#
|
|
237
|
+
# The list is static rather than derived from [sandbox] languages, which
|
|
238
|
+
# selects what gets PRE-INSTALLED, not what the workspace ends up holding.
|
|
239
|
+
# That key defaults to toolchains.DEFAULT_LANGUAGES ("python",) and setting
|
|
240
|
+
# it replaces rather than extends, so deriving from it would leave every
|
|
241
|
+
# non-Python default run harvesting node_modules/target/obj — the exact bug
|
|
242
|
+
# this list exists to fix — and would drop __pycache__ back into listings for
|
|
243
|
+
# anyone who set languages = ["rust"]. Provisioning is only a head start
|
|
244
|
+
# besides: agents self-heal missing toolchains mid-run, so a Rust-configured
|
|
245
|
+
# run that npm-installs a docs site still grows a node_modules. A name that
|
|
246
|
+
# never occurs costs nothing to carry, so the narrowing buys nothing either.
|
|
247
|
+
# Entries earn a place only if they are (i) conventionally
|
|
248
|
+
# gitignored in their ecosystem and (ii) implausible as hand-written content
|
|
249
|
+
# in any other. That rule deliberately keeps out the generic names —
|
|
250
|
+
# "bin", "build", "dist", "out", "lib", "vendor" — which are build output in
|
|
251
|
+
# one ecosystem and checked-in scripts or deps in the next; a project that
|
|
252
|
+
# wants those dropped can say so in [artifacts] exclude. Exclusions are
|
|
253
|
+
# always counted and surfaced, never silent (#67), so a wrong call here is
|
|
254
|
+
# visible and recoverable rather than a quiet truncation.
|
|
255
|
+
DEFAULT_ARTIFACT_EXCLUDES = (
|
|
256
|
+
# Run/VCS state, any ecosystem.
|
|
257
|
+
".git",
|
|
258
|
+
".sbxloop",
|
|
259
|
+
# Python: virtualenvs, bytecode, tool caches.
|
|
260
|
+
".mypy_cache",
|
|
261
|
+
".nox",
|
|
262
|
+
".pytest_cache",
|
|
263
|
+
".ruff_cache",
|
|
264
|
+
".tox",
|
|
265
|
+
".venv",
|
|
266
|
+
"__pycache__",
|
|
267
|
+
"venv",
|
|
268
|
+
# JavaScript / TypeScript.
|
|
269
|
+
"node_modules",
|
|
270
|
+
# Rust (cargo) and Java (Maven) both name their build tree "target".
|
|
271
|
+
"target",
|
|
272
|
+
# Java (Gradle's project-local cache; "build" is deliberately kept).
|
|
273
|
+
".gradle",
|
|
274
|
+
# C# / .NET intermediate objects ("bin" is deliberately kept — too many
|
|
275
|
+
# ecosystems check hand-written scripts into it).
|
|
276
|
+
"obj",
|
|
277
|
+
# Ruby (bundler's project-local config/cache).
|
|
278
|
+
".bundle",
|
|
279
|
+
# C / C++ (CMake scratch; Go needs no entry — GOCACHE and the module
|
|
280
|
+
# cache live under $HOME, outside the harvested workspace).
|
|
281
|
+
"CMakeFiles",
|
|
282
|
+
)
|
|
235
283
|
|
|
236
284
|
|
|
237
285
|
@dataclass(frozen=True)
|
|
@@ -135,8 +135,16 @@ def _probe_ls_columns(ctx: ProbeContext) -> tuple[str, str]:
|
|
|
135
135
|
stdout = ctx.cli.run("ls").stdout
|
|
136
136
|
lines = [line for line in stdout.splitlines() if line.strip()]
|
|
137
137
|
header = lines[0].strip() if lines else ""
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
if not header or header.lower().startswith("no sandboxes"):
|
|
139
|
+
# An empty listing has no header row to inspect (0.38 prints "No
|
|
140
|
+
# sandboxes found."); parse_ls returns [] for it either way, so this
|
|
141
|
+
# is not drift evidence — the column check waits for a real listing.
|
|
142
|
+
return "expected-columns", f"empty sandbox list ({header!r}) — no header to check"
|
|
143
|
+
have = {cell.lower() for cell in _CELL_SPLIT.split(header)}
|
|
144
|
+
missing = [col for col in ("agent", "status", "workspace") if col not in have]
|
|
145
|
+
if not {"name", "sandbox"} & have:
|
|
146
|
+
# NAME on 0.35.x, renamed to SANDBOX in 0.38 — parse_ls accepts both.
|
|
147
|
+
missing.insert(0, "name|sandbox")
|
|
140
148
|
if missing:
|
|
141
149
|
return f"drifted({','.join(missing)})", f"header: {header!r}"
|
|
142
150
|
return "expected-columns", f"header: {header!r}"
|
|
@@ -183,22 +191,18 @@ def _probe_cp_dir_semantics(ctx: ProbeContext) -> tuple[str, str]:
|
|
|
183
191
|
|
|
184
192
|
def _probe_workspace_mount(ctx: ProbeContext) -> tuple[str, str]:
|
|
185
193
|
assert ctx.sandbox is not None and ctx.workspace is not None
|
|
186
|
-
from sbxloop.sbx.provision import
|
|
194
|
+
from sbxloop.sbx.provision import mount_probe_command, mount_search_roots
|
|
187
195
|
|
|
188
196
|
marker = f".sbxloop-conformance-{_secrets.token_hex(8)}"
|
|
189
197
|
(ctx.workspace / marker).write_text("")
|
|
190
198
|
try:
|
|
191
|
-
|
|
192
|
-
f"find -L {' '.join(MOUNT_SEARCH_ROOTS)} "
|
|
193
|
-
f"-maxdepth {MOUNT_SEARCH_MAXDEPTH} -name {marker} -print 2>/dev/null | head -1"
|
|
194
|
-
)
|
|
195
|
-
result = ctx.sandbox.exec(["sh", "-c", command])
|
|
199
|
+
result = ctx.sandbox.exec(["sh", "-c", mount_probe_command(ctx.workspace, marker)])
|
|
196
200
|
hit = result.stdout.strip().splitlines()[-1] if result.stdout.strip() else ""
|
|
197
201
|
finally:
|
|
198
202
|
(ctx.workspace / marker).unlink(missing_ok=True)
|
|
199
203
|
if hit.endswith(f"/{marker}"):
|
|
200
204
|
return "discoverable", f"workspace mounted at {hit[: -len(f'/{marker}')] or '/'}"
|
|
201
|
-
return "not-found", f"marker not found under {', '.join(
|
|
205
|
+
return "not-found", f"marker not found under {', '.join(mount_search_roots(ctx.workspace))}"
|
|
202
206
|
|
|
203
207
|
|
|
204
208
|
def _probe_python3_venv(ctx: ProbeContext) -> tuple[str, str]:
|
|
@@ -317,7 +321,7 @@ CATALOG: tuple[Probe, ...] = (
|
|
|
317
321
|
),
|
|
318
322
|
Probe(
|
|
319
323
|
id=PROBE_LS_COLUMNS,
|
|
320
|
-
summary="`sbx ls` header carries the NAME/AGENT/STATUS/WORKSPACE columns",
|
|
324
|
+
summary="`sbx ls` header carries the NAME|SANDBOX/AGENT/STATUS/WORKSPACE columns",
|
|
321
325
|
tier="cheap",
|
|
322
326
|
expected="expected-columns",
|
|
323
327
|
depends="parse_ls (sandbox listing, `sandbox rm --all`, pair cleanup)",
|
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
The sbx binary is proprietary and its output format is not a stable API, so
|
|
4
4
|
parsing is column-header-driven and deliberately forgiving: unknown columns
|
|
5
5
|
are ignored and malformed lines are skipped rather than raising. Formats are
|
|
6
|
-
pinned by fixture tests captured from sbx v0.35.x
|
|
7
|
-
|
|
6
|
+
pinned by fixture tests captured from sbx v0.35.x and v0.38.x (which renamed
|
|
7
|
+
the ``NAME`` column to ``SANDBOX`` and added a frequently-empty ``PORTS``
|
|
8
|
+
column); ``sbxloop doctor`` warns on version mismatch.
|
|
8
9
|
"""
|
|
9
10
|
|
|
10
11
|
from __future__ import annotations
|
|
11
12
|
|
|
12
13
|
import re
|
|
14
|
+
from bisect import bisect_right
|
|
13
15
|
|
|
14
16
|
from sbxloop.sbx.models import SandboxInfo
|
|
15
17
|
|
|
@@ -18,36 +20,52 @@ _VERSION_RE = re.compile(r"(\d+\.\d+\.\d+)")
|
|
|
18
20
|
|
|
19
21
|
_CELL_SPLIT = re.compile(r"\s{2,}")
|
|
20
22
|
|
|
23
|
+
# A cell is a run of words separated by single spaces; runs of two or more
|
|
24
|
+
# spaces separate cells.
|
|
25
|
+
_CELL_ITER = re.compile(r"\S+(?: \S+)*")
|
|
26
|
+
|
|
21
27
|
|
|
22
28
|
def parse_columns(text: str) -> list[dict[str, str]]:
|
|
23
29
|
"""Parse a left-aligned column table using the header row for names.
|
|
24
30
|
|
|
25
|
-
Cells are
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
Cells are assigned to columns by their start offset against the header
|
|
32
|
+
positions rather than by index, so a row with an empty middle cell
|
|
33
|
+
(sbx 0.38's ``PORTS`` column) doesn't shift its later cells into the
|
|
34
|
+
wrong columns. A cell that overflows its column width still lands on
|
|
35
|
+
the nearest header at or left of where it starts; if that column is
|
|
36
|
+
already taken the cell spills into the next free one. Missing cells
|
|
37
|
+
stay empty strings.
|
|
28
38
|
"""
|
|
29
39
|
lines = [line.rstrip() for line in text.splitlines() if line.strip()]
|
|
30
40
|
if not lines:
|
|
31
41
|
return []
|
|
32
|
-
headers = [
|
|
42
|
+
headers = [(match.group().lower(), match.start()) for match in _CELL_ITER.finditer(lines[0])]
|
|
33
43
|
if not headers:
|
|
34
44
|
return []
|
|
45
|
+
starts = [start for _, start in headers]
|
|
35
46
|
|
|
36
47
|
rows: list[dict[str, str]] = []
|
|
37
48
|
for line in lines[1:]:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
row = {name: "" for name, _ in headers}
|
|
50
|
+
for match in _CELL_ITER.finditer(line):
|
|
51
|
+
i = max(bisect_right(starts, match.start()) - 1, 0)
|
|
52
|
+
while i < len(headers) and row[headers[i][0]]:
|
|
53
|
+
i += 1
|
|
54
|
+
if i < len(headers):
|
|
55
|
+
row[headers[i][0]] = match.group()
|
|
42
56
|
rows.append(row)
|
|
43
57
|
return rows
|
|
44
58
|
|
|
45
59
|
|
|
46
60
|
def parse_ls(text: str) -> list[SandboxInfo]:
|
|
47
|
-
"""Parse ``sbx ls`` output into SandboxInfo rows (nameless rows skipped).
|
|
61
|
+
"""Parse ``sbx ls`` output into SandboxInfo rows (nameless rows skipped).
|
|
62
|
+
|
|
63
|
+
The name column is ``NAME`` on sbx 0.35.x and ``SANDBOX`` since 0.38;
|
|
64
|
+
both are accepted.
|
|
65
|
+
"""
|
|
48
66
|
infos: list[SandboxInfo] = []
|
|
49
67
|
for row in parse_columns(text):
|
|
50
|
-
name = row.get("name"
|
|
68
|
+
name = row.get("name") or row.get("sandbox") or ""
|
|
51
69
|
if not name:
|
|
52
70
|
continue
|
|
53
71
|
infos.append(
|
|
@@ -76,6 +76,36 @@ GITHUB_ALLOW_DOMAINS = (
|
|
|
76
76
|
MOUNT_SEARCH_ROOTS = ("/workspace", "/home/agent", "/mnt", "/host", "/root")
|
|
77
77
|
MOUNT_SEARCH_MAXDEPTH = 4
|
|
78
78
|
|
|
79
|
+
|
|
80
|
+
def mount_search_roots(workspace: Path | None) -> tuple[str, ...]:
|
|
81
|
+
"""The in-VM roots to search for the workspace mount, most likely first.
|
|
82
|
+
|
|
83
|
+
sbx 0.38 mounts the host workspace at its own host absolute path inside
|
|
84
|
+
the VM (identity mount, field-verified on 0.38.0), so the workspace path
|
|
85
|
+
itself is probed first; the 0.35-era candidate roots remain as fallback.
|
|
86
|
+
"""
|
|
87
|
+
if workspace is None:
|
|
88
|
+
return MOUNT_SEARCH_ROOTS
|
|
89
|
+
return (str(workspace), *MOUNT_SEARCH_ROOTS)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def mount_probe_command(workspace: Path | None, marker: str) -> str:
|
|
93
|
+
"""One bounded in-VM marker search over the candidate mount roots.
|
|
94
|
+
|
|
95
|
+
Roots are existence-filtered before the ``find`` and the command always
|
|
96
|
+
exits 0 on a clean answer: on sbx 0.38 most candidate roots don't exist
|
|
97
|
+
in the VM, and a bare ``find`` over missing roots exits nonzero — which
|
|
98
|
+
would misclassify every clean "not mounted" answer as a broken probe
|
|
99
|
+
(the probe="answered" vs probe="error" split, #63).
|
|
100
|
+
"""
|
|
101
|
+
roots = " ".join(shlex.quote(root) for root in mount_search_roots(workspace))
|
|
102
|
+
return (
|
|
103
|
+
f'set --; for r in {roots}; do [ -e "$r" ] && set -- "$@" "$r"; done; '
|
|
104
|
+
f'[ $# -eq 0 ] || find -L "$@" -maxdepth {MOUNT_SEARCH_MAXDEPTH} '
|
|
105
|
+
f"-name {marker} -print 2>/dev/null | head -1; :"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
79
109
|
PostCreate = Callable[[Sandbox, SandboxRole], None]
|
|
80
110
|
|
|
81
111
|
|
|
@@ -433,11 +463,7 @@ class Provisioner:
|
|
|
433
463
|
return None
|
|
434
464
|
probe_error = ""
|
|
435
465
|
try:
|
|
436
|
-
command = (
|
|
437
|
-
f"find -L {' '.join(MOUNT_SEARCH_ROOTS)} "
|
|
438
|
-
f"-maxdepth {MOUNT_SEARCH_MAXDEPTH} -name {marker} -print 2>/dev/null"
|
|
439
|
-
" | head -1"
|
|
440
|
-
)
|
|
466
|
+
command = mount_probe_command(workspace, marker)
|
|
441
467
|
result = sandbox.exec(["sh", "-c", command])
|
|
442
468
|
hit = result.stdout.strip().splitlines()[-1] if result.stdout.strip() else ""
|
|
443
469
|
if not result.ok:
|
|
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
|