4141logfire_secret = modal .Secret .from_name ("logfire-token" )
4242
4343
44- def run_claude_code_in_sandbox (
44+ async def run_claude_code_in_sandbox_async (
4545 question : str ,
4646 api_base_url : str = "https://v2.api.policyengine.org" ,
4747) -> tuple [modal .Sandbox , any ]:
4848 """Create a sandbox running Claude Code with MCP server configured.
4949
5050 Returns the sandbox and process handle for streaming output.
51+ Uses Modal's async API for proper streaming support.
5152 """
5253 import logfire
5354
54- print ("[SANDBOX] run_claude_code_in_sandbox starting" , flush = True )
5555 logfire .info (
5656 "run_claude_code_in_sandbox: starting" ,
5757 question = question [:100 ],
@@ -65,35 +65,20 @@ def run_claude_code_in_sandbox(
6565 mcp_config_json = json .dumps (mcp_config )
6666
6767 # Get reference to deployed app (required when calling from outside Modal)
68- print ("[SANDBOX] looking up Modal app" , flush = True )
6968 logfire .info ("run_claude_code_in_sandbox: looking up Modal app" )
7069 sandbox_app = modal .App .lookup ("policyengine-sandbox" , create_if_missing = True )
71- print ("[SANDBOX] Modal app found" , flush = True )
7270 logfire .info ("run_claude_code_in_sandbox: Modal app found" )
7371
74- print ("[SANDBOX] creating sandbox" , flush = True )
7572 logfire .info ("run_claude_code_in_sandbox: creating sandbox" )
76- sb = modal .Sandbox .create (
73+ sb = await modal .Sandbox .create . aio (
7774 app = sandbox_app ,
7875 image = sandbox_image ,
7976 secrets = [anthropic_secret , logfire_secret ],
8077 timeout = 600 ,
8178 workdir = "/tmp" ,
8279 )
83- print ("[SANDBOX] sandbox created" , flush = True )
8480 logfire .info ("run_claude_code_in_sandbox: sandbox created" )
8581
86- # Run Claude Code with the question
87- # Note: Can't use --dangerously-skip-permissions as root (Modal runs as root)
88- # Use shell wrapper with </dev/null to properly close stdin (prevents hanging)
89- # --max-turns: limit execution to prevent runaway
90- # Use --mcp-config to pass MCP config directly (more reliable than config file)
91- print ("[SANDBOX] Starting claude CLI with question" , flush = True )
92- logfire .info (
93- "run_claude_code_in_sandbox: starting claude CLI" ,
94- mcp_url = f"{ api_base_url } /mcp" ,
95- )
96-
9782 # Escape the question and config for shell
9883 escaped_question = question .replace ("'" , "'\" '\" '" )
9984 escaped_mcp_config = mcp_config_json .replace ("'" , "'\" '\" '" )
@@ -112,9 +97,8 @@ def run_claude_code_in_sandbox(
11297 question_len = len (question ),
11398 escaped_question_len = len (escaped_question ),
11499 )
115- # text=True, bufsize=1 enables line-buffered streaming
116- process = sb .exec ("sh" , "-c" , cmd , text = True , bufsize = 1 )
117- print ("[SANDBOX] claude CLI process started" , flush = True )
100+ # Use async exec for proper streaming
101+ process = await sb .exec .aio ("sh" , "-c" , cmd , text = True , bufsize = 1 )
118102 logfire .info ("run_claude_code_in_sandbox: claude CLI process started, returning" )
119103
120104 return sb , process
0 commit comments