Skip to content

Commit bccf2a8

Browse files
authored
fix(sandbox): bake project source synchronizer (#194)
## Why Production cold-cache recovery exposed that the source-mirror implementation was transported as one inline Python command argument. The sandbox runtime correctly rejects arguments above 8 KiB, so preview preparation failed before dependency restoration could run. ## What changed - move the project source synchronizer into the immutable sandbox image as a root-owned executable - keep Worker-to-sandbox commands bounded to the helper path and validated runtime arguments - retain the existing conflict-safe package transaction and continuous preview mirror semantics - smoke-test the helper's executable bit and Python syntax during snapshot publication - document the image/Worker ownership boundary ## Architecture effects This makes the immutable sandbox snapshot the source of truth for executable sandbox runtime code. Promotion therefore requires a new verified Daytona snapshot followed by the normal reviewed `DAYTONA_SANDBOX_SNAPSHOT` configuration change. ## Verification - `pnpm lint` - `pnpm typecheck` - `pnpm turbo build --force` - `pnpm deadcode` - `pnpm architecture:check` - `pnpm turbo skills:build` - `actionlint .github/workflows/build-snapshot.yml` - `git diff --check` - local source-sync success and concurrent-write conflict checks - generated preview command argument lengths: `[2, 3, 1592]` (all below the 8 KiB sandbox contract) All repository gates ran with Node 24.18.0 and pnpm 11.15.0.
1 parent 2d7a422 commit bccf2a8

5 files changed

Lines changed: 252 additions & 215 deletions

File tree

.github/workflows/build-snapshot.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ jobs:
144144
npm --version
145145
pnpm --version
146146
python3 --version
147+
test -x /opt/cheatcode/project-source-sync.py
148+
python3 -c "import ast,pathlib;ast.parse(pathlib.Path(\"/opt/cheatcode/project-source-sync.py\").read_text())"
147149
code-server --version
148150
"$CHROME_PATH" --version
149151
libreoffice --version

apps/agent-worker/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@ webpack compilation even after the listening socket opens. A baked Python synchr
152152
full refresh on every process start and mirrors subsequent writes and deletions within one second,
153153
including shell-based edits. The local source, dependency tree, and build cache are disposable;
154154
wake and restart reconstruct them from the durable project without changing the Files surface.
155+
The immutable sandbox exposes that synchronizer as the root-owned
156+
`/opt/cheatcode/project-source-sync.py` helper, keeping Worker-to-sandbox command arguments small
157+
and making the snapshot the source of truth for executable sandbox runtime code.
155158
Direct pnpm commands use that same native-disk source as a transaction: the runtime snapshots the
156159
durable source, executes pnpm locally, and copies only command-produced source changes back after
157160
verifying that the corresponding durable paths did not change concurrently. Long-running pnpm

apps/agent-worker/src/durable-objects/project-sandbox-local-source.ts

Lines changed: 2 additions & 215 deletions
Original file line numberDiff line numberDiff line change
@@ -1,218 +1,7 @@
11
import { shellQuote } from "../sandbox-support";
22
import type { ProjectLocalRuntime } from "./project-sandbox-package-runtime";
33

4-
const LOCAL_SOURCE_SYNC_SCRIPT = `
5-
import hashlib
6-
import json
7-
import os
8-
import shutil
9-
import sys
10-
import time
11-
12-
IGNORED_DIRS = {".expo", ".next", ".turbo", "build", "coverage", "dist", "node_modules", "out"}
13-
LOCAL_ONLY_NAMES = IGNORED_DIRS | {"next-env.d.ts"}
14-
LOCK_STALE_SECONDS = 15 * 60
15-
16-
def remove_path(path):
17-
if os.path.isdir(path) and not os.path.islink(path):
18-
shutil.rmtree(path)
19-
elif os.path.lexists(path):
20-
os.unlink(path)
21-
22-
def sync_link(source, target):
23-
link = os.readlink(source)
24-
if os.path.islink(target) and os.readlink(target) == link:
25-
return
26-
remove_path(target)
27-
os.symlink(link, target)
28-
29-
def sync_file(source, target, force):
30-
source_stat = os.stat(source, follow_symlinks=False)
31-
try:
32-
target_stat = os.stat(target, follow_symlinks=False)
33-
except FileNotFoundError:
34-
target_stat = None
35-
changed = (
36-
force
37-
or target_stat is None
38-
or not os.path.isfile(target)
39-
or source_stat.st_size != target_stat.st_size
40-
or source_stat.st_mtime_ns > target_stat.st_mtime_ns
41-
)
42-
if changed:
43-
remove_path(target)
44-
shutil.copyfile(source, target, follow_symlinks=False)
45-
46-
def sync_directory(source, target, force=False):
47-
if os.path.lexists(target) and not os.path.isdir(target):
48-
remove_path(target)
49-
os.makedirs(target, exist_ok=True)
50-
source_names = set()
51-
for entry in os.scandir(source):
52-
if entry.name in IGNORED_DIRS:
53-
continue
54-
source_names.add(entry.name)
55-
destination = os.path.join(target, entry.name)
56-
try:
57-
if entry.is_symlink():
58-
sync_link(entry.path, destination)
59-
elif entry.is_dir(follow_symlinks=False):
60-
sync_directory(entry.path, destination, force)
61-
elif entry.is_file(follow_symlinks=False):
62-
sync_file(entry.path, destination, force)
63-
except FileNotFoundError:
64-
continue
65-
for entry in os.scandir(target):
66-
if entry.name not in source_names and entry.name not in LOCAL_ONLY_NAMES:
67-
remove_path(entry.path)
68-
69-
def digest(path):
70-
value = hashlib.sha256()
71-
with open(path, "rb") as source:
72-
for chunk in iter(lambda: source.read(1024 * 1024), b""):
73-
value.update(chunk)
74-
return value.hexdigest()
75-
76-
def tree_state(root):
77-
state = {}
78-
if not os.path.isdir(root):
79-
return state
80-
for current, dir_names, file_names in os.walk(root, topdown=True, followlinks=False):
81-
dir_names[:] = sorted(name for name in dir_names if name not in IGNORED_DIRS)
82-
relative_root = os.path.relpath(current, root)
83-
for name in dir_names:
84-
path = os.path.join(current, name)
85-
relative = os.path.normpath(os.path.join(relative_root, name))
86-
if os.path.islink(path):
87-
state[relative] = ["link", os.readlink(path)]
88-
else:
89-
state[relative] = ["dir"]
90-
for name in sorted(file_names):
91-
if name in LOCAL_ONLY_NAMES:
92-
continue
93-
path = os.path.join(current, name)
94-
relative = os.path.normpath(os.path.join(relative_root, name))
95-
if os.path.islink(path):
96-
state[relative] = ["link", os.readlink(path)]
97-
else:
98-
state[relative] = ["file", digest(path)]
99-
return state
100-
101-
def package_lock_active(lock_path):
102-
try:
103-
age = time.time() - os.path.getmtime(lock_path)
104-
except FileNotFoundError:
105-
return False
106-
if age <= LOCK_STALE_SECONDS:
107-
return True
108-
try:
109-
os.unlink(lock_path)
110-
except FileNotFoundError:
111-
pass
112-
return False
113-
114-
def wait_for_preview(lock_path, busy_path):
115-
deadline = time.time() + 30
116-
while os.path.exists(busy_path):
117-
if time.time() >= deadline:
118-
raise TimeoutError("local source mirror did not quiesce")
119-
time.sleep(0.05)
120-
121-
def preview_sync(source, target, mode, lock_path, busy_path):
122-
force = mode == "preview-once"
123-
while True:
124-
if package_lock_active(lock_path):
125-
if force:
126-
time.sleep(0.05)
127-
continue
128-
time.sleep(0.25)
129-
continue
130-
os.makedirs(os.path.dirname(busy_path), exist_ok=True)
131-
with open(busy_path, "w", encoding="utf-8"):
132-
pass
133-
try:
134-
if package_lock_active(lock_path):
135-
continue
136-
sync_directory(source, target, force)
137-
finally:
138-
try:
139-
os.unlink(busy_path)
140-
except FileNotFoundError:
141-
pass
142-
if force:
143-
return
144-
time.sleep(0.75)
145-
146-
def prepare(source, target, lock_path, busy_path, baseline_path):
147-
os.makedirs(os.path.dirname(lock_path), exist_ok=True)
148-
with open(lock_path, "w", encoding="utf-8") as lock:
149-
lock.write(str(time.time()))
150-
try:
151-
wait_for_preview(lock_path, busy_path)
152-
sync_directory(source, target, True)
153-
with open(baseline_path, "w", encoding="utf-8") as baseline:
154-
json.dump(tree_state(target), baseline, sort_keys=True, separators=(",", ":"))
155-
except Exception:
156-
cleanup(lock_path, baseline_path)
157-
raise
158-
159-
def copy_state(source_root, target_root, relative, state):
160-
source = os.path.join(source_root, relative)
161-
target = os.path.join(target_root, relative)
162-
kind = state[0]
163-
if kind == "dir":
164-
if os.path.lexists(target) and not os.path.isdir(target):
165-
remove_path(target)
166-
os.makedirs(target, exist_ok=True)
167-
return
168-
os.makedirs(os.path.dirname(target), exist_ok=True)
169-
if kind == "link":
170-
sync_link(source, target)
171-
return
172-
if os.path.isdir(target) or os.path.islink(target):
173-
remove_path(target)
174-
shutil.copyfile(source, target, follow_symlinks=False)
175-
176-
def cleanup(lock_path, baseline_path):
177-
for path in (baseline_path, lock_path):
178-
try:
179-
os.unlink(path)
180-
except FileNotFoundError:
181-
pass
182-
183-
def commit(source, target, lock_path, baseline_path):
184-
try:
185-
with open(baseline_path, encoding="utf-8") as baseline_file:
186-
baseline = json.load(baseline_file)
187-
local = tree_state(target)
188-
durable = tree_state(source)
189-
changed = sorted(path for path in set(baseline) | set(local) if baseline.get(path) != local.get(path))
190-
conflicts = [
191-
path
192-
for path in changed
193-
if durable.get(path) != baseline.get(path) and durable.get(path) != local.get(path)
194-
]
195-
if conflicts:
196-
raise RuntimeError("durable project changed during package command: " + ", ".join(conflicts[:8]))
197-
for relative in sorted((path for path in changed if path not in local), key=lambda value: value.count(os.sep), reverse=True):
198-
remove_path(os.path.join(source, relative))
199-
for relative in sorted((path for path in changed if path in local), key=lambda value: value.count(os.sep)):
200-
copy_state(target, source, relative, local[relative])
201-
finally:
202-
cleanup(lock_path, baseline_path)
203-
204-
mode, source, target, lock_path, busy_path, baseline_path = sys.argv[1:7]
205-
if mode in {"preview-once", "preview-loop"}:
206-
preview_sync(source, target, mode, lock_path, busy_path)
207-
elif mode == "package-prepare":
208-
prepare(source, target, lock_path, busy_path, baseline_path)
209-
elif mode == "package-commit":
210-
commit(source, target, lock_path, baseline_path)
211-
elif mode == "package-abort":
212-
cleanup(lock_path, baseline_path)
213-
else:
214-
raise ValueError("unknown local source synchronization mode")
215-
`;
4+
const LOCAL_SOURCE_SYNC_BINARY = "/opt/cheatcode/project-source-sync.py";
2165

2176
function runtimeStatePaths(runtime: ProjectLocalRuntime): {
2187
baselinePath: string;
@@ -232,9 +21,7 @@ export function localSourceSyncCommand(
23221
): string {
23322
const { baselinePath, busyPath, lockPath } = runtimeStatePaths(runtime);
23423
return [
235-
"python3",
236-
"-c",
237-
LOCAL_SOURCE_SYNC_SCRIPT,
24+
LOCAL_SOURCE_SYNC_BINARY,
23825
mode,
23926
runtime.workspaceDir,
24027
runtime.localSourceDir,

infra/containers/sandbox/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ restored from the reviewed package store after a sandbox replacement. Expo uses
9595
`default` alias. These locks prevent a snapshot rebuild from resolving a different
9696
dependency tree while the application source stays unchanged.
9797

98+
The root-owned `/opt/cheatcode/project-source-sync.py` helper is the single runtime
99+
boundary between persistent project source and the native-disk project mirror. It is
100+
baked and syntax-checked with the immutable image so Workers invoke a short, bounded
101+
command instead of transporting executable source through sandbox command arguments.
102+
98103
Open VSX currently publishes Parquet Viewer 3.1.0 with vulnerable Thrift and WebSocket
99104
runtimes. The image keeps the extension feature but replaces those two runtime packages
100105
with the exact, lockfile-pinned versions in `extension-overrides/parquet-viewer/` and

0 commit comments

Comments
 (0)