Skip to content

Commit ec1bd40

Browse files
committed
Migrate bridge clients to process resources
Replace session and command-oriented bridge clients with lifecycle and process resource APIs. Keep the Worker middleware, examples, and tests aligned with the argv-only Sandbox bridge.
1 parent 99ac257 commit ec1bd40

15 files changed

Lines changed: 615 additions & 1822 deletions

File tree

bridge/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Cloudflare Sandbox Bridge
22

3-
HTTP bridge between the [OpenAI Agents SDK](https://github.com/openai/openai-agents-python) and the [Cloudflare Sandbox](https://developers.cloudflare.com/sandbox/). Translates the SDK's sandbox session operations into calls against the `@cloudflare/sandbox` Durable Object API.
3+
HTTP bridge between the [OpenAI Agents SDK](https://github.com/openai/openai-agents-python) and the [Cloudflare Sandbox](https://developers.cloudflare.com/sandbox/). Translates Sandbox operations into calls against the `@cloudflare/sandbox` Durable Object API.
44

55
| Directory | Description |
66
| ------------------------ | --------------------------------------------------- |
@@ -17,6 +17,12 @@ Deploy the bridge worker with one click:
1717

1818
Or deploy manually — see [worker/README.md](./worker/README.md) for setup, configuration, and the full API reference.
1919

20+
## Process model
21+
22+
The bridge launches argv processes and returns after launch, not completion. Process status and SSE logs are recoverable by runtime-local ID while the current container remains alive; log cursors support replay after a client reconnects. Process discovery does not wake a sleeping sandbox, and IDs from a stopped or replaced runtime cannot be recovered. Shell syntax requires an explicit shell in `argv`, and process control uses numeric signals.
23+
24+
Terminals are separate PTY resources with input, resize, interrupt, terminate, and reconnect behavior. Closing an SSE connection cancels only that log observation and does not kill its process.
25+
2026
## Examples
2127

2228
- **[basic/](./examples/basic/)** — One-shot coding agent that executes a task and copies output files to the host. Supports `--image` for visual references.

bridge/examples/basic/main.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,13 +135,13 @@ async def _print_stream(result) -> None: # noqa: ANN001
135135
# ---------------------------------------------------------------------------
136136

137137

138-
async def _copy_sandbox_output(session, output_dir: Path) -> list[Path]:
138+
async def _copy_sandbox_output(sandbox_client, output_dir: Path) -> list[Path]:
139139
"""Read files from /workspace/output/ in the sandbox and write them locally."""
140140
output_dir.mkdir(parents=True, exist_ok=True)
141141
copied: list[Path] = []
142142

143143
# List files via exec since the SandboxSession wrapper doesn't expose ls().
144-
ls_result = await session.exec(
144+
ls_result = await sandbox_client.exec(
145145
"find",
146146
"/workspace/output",
147147
"-maxdepth",
@@ -163,7 +163,7 @@ async def _copy_sandbox_output(session, output_dir: Path) -> list[Path]:
163163
]
164164

165165
for filepath in filenames:
166-
handle = await session.read(Path(filepath))
166+
handle = await sandbox_client.read(Path(filepath))
167167
try:
168168
payload = handle.read()
169169
finally:

bridge/examples/workspace-chat/backend/main.py

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -213,11 +213,11 @@ def _sse_event(data: dict | str) -> str:
213213

214214
async def _stream_agent_response(prompt: str, history: list[dict]):
215215
"""Run the agent and yield SSE events in AI SDK UI Message Stream format."""
216-
session = _get_session()
216+
sandbox_client = _get_session()
217217
agent = _build_agent()
218218

219219
run_config = RunConfig(
220-
sandbox=SandboxRunConfig(session=session),
220+
sandbox=SandboxRunConfig(session=sandbox_client),
221221
workflow_name="workspace-chat",
222222
tracing_disabled=True,
223223
)
@@ -444,8 +444,8 @@ async def list_endpoint(request: Request) -> JSONResponse:
444444
except ValueError as e:
445445
return JSONResponse({"error": str(e)}, status_code=403)
446446

447-
session = _get_session()
448-
result = await session.exec(
447+
sandbox_client = _get_session()
448+
result = await sandbox_client.exec(
449449
"find",
450450
safe,
451451
"-maxdepth",
@@ -488,8 +488,8 @@ async def read_file_endpoint(request: Request) -> Response:
488488
except ValueError as e:
489489
return JSONResponse({"error": str(e)}, status_code=403)
490490

491-
session = _get_session()
492-
result = await session.exec("cat", "--", safe, shell=False)
491+
sandbox_client = _get_session()
492+
result = await sandbox_client.exec("cat", "--", safe, shell=False)
493493
if not result.ok():
494494
return JSONResponse({"error": f"File not found: {path}"}, status_code=404)
495495

@@ -510,27 +510,27 @@ async def delete_file_endpoint(request: Request) -> JSONResponse:
510510
except ValueError as e:
511511
return JSONResponse({"error": str(e)}, status_code=403)
512512

513-
session = _get_session()
514-
result = await session.exec("rm", "-f", "--", safe, shell=False)
513+
sandbox_client = _get_session()
514+
result = await sandbox_client.exec("rm", "-f", "--", safe, shell=False)
515515
return JSONResponse({"path": path, "deleted": result.ok()})
516516

517517

518518
async def workspace_info_endpoint(request: Request) -> JSONResponse:
519519
"""GET /api/workspace/info — workspace statistics."""
520-
session = _get_session()
520+
sandbox_client = _get_session()
521521
file_count = 0
522522
dir_count = 0
523523
total_bytes = 0
524524
try:
525-
file_result = await session.exec("find", WORKSPACE_ROOT, "-type", "f", shell=False)
525+
file_result = await sandbox_client.exec("find", WORKSPACE_ROOT, "-type", "f", shell=False)
526526
file_count = len(file_result.stdout.decode().strip().splitlines()) if file_result.ok() else 0
527527

528-
dir_result = await session.exec(
528+
dir_result = await sandbox_client.exec(
529529
"find", WORKSPACE_ROOT, "-mindepth", "1", "-type", "d", shell=False
530530
)
531531
dir_count = len(dir_result.stdout.decode().strip().splitlines()) if dir_result.ok() else 0
532532

533-
du_result = await session.exec("du", "-sb", WORKSPACE_ROOT, shell=False)
533+
du_result = await sandbox_client.exec("du", "-sb", WORKSPACE_ROOT, shell=False)
534534
if du_result.ok():
535535
parts = du_result.stdout.decode().strip().split()
536536
if parts:
@@ -549,10 +549,10 @@ async def workspace_info_endpoint(request: Request) -> JSONResponse:
549549

550550
async def file_tree_endpoint(request: Request) -> JSONResponse:
551551
"""GET /api/files-tree — recursive flat listing of all files in the workspace."""
552-
session = _get_session()
552+
sandbox_client = _get_session()
553553
entries: list[dict[str, object]] = []
554554
try:
555-
result = await session.exec(
555+
result = await sandbox_client.exec(
556556
"find", WORKSPACE_ROOT, "-type", "f", "-printf", "%s\t%p\n",
557557
shell=False,
558558
)
@@ -616,9 +616,9 @@ async def artifact_endpoint(request: Request) -> Response:
616616
except ValueError as e:
617617
return JSONResponse({"error": str(e)}, status_code=403)
618618

619-
session = _get_session()
619+
sandbox_client = _get_session()
620620
try:
621-
file_obj = await session.read(safe)
621+
file_obj = await sandbox_client.read(safe)
622622
except (WorkspaceReadNotFoundError, FileNotFoundError, Exception):
623623
return JSONResponse({"error": f"File not found: {path}"}, status_code=404)
624624

@@ -673,7 +673,7 @@ async def upload_endpoint(request: Request) -> JSONResponse:
673673
"""POST /api/upload — upload files into the sandbox workspace."""
674674
form = await request.form()
675675
uploaded: list[dict[str, object]] = []
676-
session = _get_session()
676+
sandbox_client = _get_session()
677677

678678
for key in form:
679679
upload = form[key]
@@ -693,8 +693,8 @@ async def upload_endpoint(request: Request) -> JSONResponse:
693693
)
694694
safe = f"/workspace/uploads/{filename}"
695695
# Ensure the uploads directory exists
696-
await session.exec("mkdir", "-p", "/workspace/uploads", shell=False)
697-
await session.write(Path(safe), io.BytesIO(content))
696+
await sandbox_client.exec("mkdir", "-p", "/workspace/uploads", shell=False)
697+
await sandbox_client.write(Path(safe), io.BytesIO(content))
698698
uploaded.append({"path": f"/workspace/uploads/{filename}", "size": len(content)})
699699

700700
return JSONResponse({"uploaded": uploaded})

0 commit comments

Comments
 (0)