## Fixes - [#3] Status events now add/update a status ActivityNode in timeline (upsertWsStatusNode) - [#4] Done callback adds done node; error callback adds error node + sets session.status="error" - [#5] handleRegenerate fully synced with workspace: status/tool/card/done/error all handled - [#1][#2] localStorage persistence: completed/error sessions auto-saved, lazy-loaded on demand - [#1] handleSelectConversation preloads workspace sessions from localStorage for history messages - Refactored completeWsSession to include done ActivityNode in timeline - Added errorWsSession, upsertWsStatusNode, saveWsToStorage, loadWsFromStorage helpers ## Known limitation - [#7] workspace_card merge:true not yet used by backend (all cards are append-only for now) - History workspace recovery depends on localStorage (browser-local, not cross-device) Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
75 lines
2.3 KiB
Python
75 lines
2.3 KiB
Python
"""Sandboxed code execution tool using Daytona SDK.
|
|
|
|
Creates an ephemeral Daytona sandbox, runs user code, captures
|
|
stdout/stderr, and destroys the sandbox on completion.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from langchain_core.tools import tool
|
|
|
|
from app.config import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@tool
|
|
async def sandbox_run(code: str, language: str = "python") -> str:
|
|
"""Execute code in a secure sandbox and return the output.
|
|
|
|
Use this tool when the user asks you to run, execute, or test code.
|
|
The sandbox is isolated and ephemeral — it is destroyed after execution.
|
|
|
|
Supported languages: python, javascript, typescript, bash/shell.
|
|
|
|
Args:
|
|
code: The source code to execute.
|
|
language: Programming language (default: python).
|
|
"""
|
|
# Lazy import so the app starts even if daytona is not installed
|
|
try:
|
|
from daytona import Daytona, DaytonaConfig, DaytonaError
|
|
except ImportError:
|
|
return "Sandbox execution is not available: the daytona SDK is not installed."
|
|
|
|
logger.info("Sandbox run: language=%s, code length=%d", language, len(code))
|
|
|
|
config = DaytonaConfig(
|
|
api_key=settings.daytona_api_key,
|
|
api_url=settings.daytona_api_url,
|
|
target="us",
|
|
)
|
|
daytona = Daytona(config)
|
|
sandbox = None
|
|
|
|
try:
|
|
sandbox = daytona.create()
|
|
|
|
if language in ("bash", "shell", "sh"):
|
|
response = sandbox.process.exec(code, timeout=30)
|
|
else:
|
|
response = sandbox.process.code_run(code, timeout=30)
|
|
|
|
exit_code = getattr(response, "exit_code", None)
|
|
result = getattr(response, "result", str(response))
|
|
|
|
# Truncate very long output
|
|
if len(result) > 10000:
|
|
result = result[:10000] + "\n... (output truncated)"
|
|
|
|
if exit_code and exit_code != 0:
|
|
return f"[Exit code: {exit_code}]\n{result}"
|
|
return result or "(no output)"
|
|
|
|
except Exception as exc:
|
|
logger.exception("Sandbox execution failed")
|
|
return f"Sandbox execution failed: {exc}"
|
|
finally:
|
|
if sandbox is not None:
|
|
try:
|
|
daytona.remove(sandbox)
|
|
except Exception:
|
|
logger.warning("Failed to remove sandbox", exc_info=True)
|