cloudflare / cloudflare/sandbox-sdk
Local R2 mount at `/data` repeatedly fails reverse-sync watcher because `watch()` requires `/workspace`
- Dominant language
- TypeScript
- Stars
- 1.1k
- Forks
- 114
- Avg merge
- 22h 42m
- Merged PRs (30d)
- 14
Description
## Description
When using `mountBucket()` with `localBucket: true` during `wrangler dev`, mounting an R2 binding at `/data` successfully synchronizes existing R2 objects into the container. However, the SDK's container-to-R2 watch loop repeatedly fails with:
```text
PermissionDeniedError: path must be inside /workspace
```
The SDK retries the failed watcher indefinitely with exponential backoff, producing recurring `Container watch loop failed` errors.
Cloudflare's bucket-mount documentation uses paths such as `/data` and describes local mounts as bidirectionally synchronized. However, the local mount synchronizer passes the mount path directly to `WatchClient.watch()`, while the watch API rejects paths outside `/workspace`.
## Environment
- `@cloudflare/sandbox`: `0.12.3`
- Container image: `cloudflare/sandbox:0.12.3-python`
- Wrangler: `4.63.0`
- Local development with `wrangler dev`
- Host: macOS
- R2 binding mounted using `localBucket: true`
## Minimal configuration
```jsonc
{
"r2_buckets": [
{
"binding": "MY_BUCKET",
"bucket_name": "my-bucket"
}
]
}
```
Mount an R2 binding outside `/workspace`:
```ts
await sandbox.mountBucket("MY_BUCKET", "/data", {
localBucket: true,
prefix: "/sandbox/repro",
});
```
Then perform an operation that starts or uses the sandbox:
```ts
await sandbox.exec("ls -la /data");
```
## Actual behavior
The mount initially reports success:
```text
Local mount sync started
mountPath: /data
bucket.mount success /data
provider: local-sync
```
Objects already in R2 are successfully copied into `/data` and can be read by commands running in the container.
Immediately afterward, the reverse-sync watcher fails:
```text
Container watch loop failed
PermissionDeniedError: path must be inside /workspace
at WatchClient.handleErrorResponse
at WatchClient.watch
at LocalMountSyncManager.runContainerWatchLoop
```
The SDK continues retrying this operation indefinitely:
```text
Container watch loop failed
Container watch loop failed
Container watch loop failed
```
The retries appear to use exponential backoff capped at approximately 30 seconds.
## Expected behavior
One of the following:
1. Local bucket mounts at documented paths such as `/data` support bidirectional synchronization.
2. `mountBucket({ localBucket: true })` validates that its mount path is compatible with the watcher and returns a clear error.
3. The documentation states that local bidirectional mounts must be under `/workspace`.
A successfully established mount should not start an indefinitely failing background retry loop.
## Observed functional impact
The two synchronization directions behave differently:
- R2 to container: working
- Container to R2: not working for mounts outside `/workspace`
For example, an R2 object synchronized into `/data` can be successfully processed by a command in the container despite the recurring watcher errors.
This makes the mount appear successful while only providing one-way synchronization.
## Suspected cause
`LocalMountSyncManager.runContainerWatchLoop()` passes the configured mount point directly to the watcher:
```ts
const stream = await this.client.watch.watch({
path: this.mountPath,
recursive: true,
sessionId: this.sessionId,
});
```
For a mount at `/data`, this invokes `watch()` with `/data`.
The watch endpoint requires all watched paths to resolve within `/workspace`, so it rejects the mount path. The reconnect logic then retries the same incompatible request indefinitely.
Relevant references:
- [Local mount synchronization implementation](https://github.com/cloudflare/sandbox-sdk/blob/main/packages/sandbox/src/local-mount-sync.ts)
- [Mount-bucket documentation](https://developers.cloudflare.com/sandbox/guides/mount-buckets/)
- [File-watching path requirements](https://developers.cloudflare.com/sandbox/api/file-watching/)
## Questions
- Should `localBucket: true` support bidirectional synchronization at `/data`, as the mount documentation suggests?
- Should the internal local synchronization watcher be allowed to watch mount paths outside `/workspace`?
- If `/workspace/*` is the intended requirement, should `mountBucket()` validate this before starting the synchronization manager?
- Is there a recommended workaround that preserves `/data` as the production mount path?
Contributor guide
Assessment
This issue has not been assessed yet.