Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion backend/web/routers/debug.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
"""Debug logging endpoints."""

import tempfile
from pathlib import Path

from fastapi import APIRouter
from pydantic import BaseModel

Expand All @@ -11,9 +14,15 @@ class LogMessage(BaseModel):
timestamp: str


def _debug_log_path() -> Path:
return Path(tempfile.gettempdir()) / "leon-frontend-console.log"


@router.post("/log")
async def log_frontend_message(payload: LogMessage) -> dict:
"""Receive frontend console logs and write to file."""
with open("/tmp/leon-frontend-console.log", "a") as f:
log_path = _debug_log_path()
log_path.parent.mkdir(parents=True, exist_ok=True)
with log_path.open("a", encoding="utf-8") as f:
f.write(f"[{payload.timestamp}] {payload.message}\n")
return {"status": "ok"}
15 changes: 15 additions & 0 deletions core/tools/command/bash/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ def __init__(self, default_cwd: str | None = None):
self._session_lock = asyncio.Lock()
self._current_cwd = default_cwd or os.getcwd()

@staticmethod
def _unsupported_on_windows_message() -> str:
return "BashExecutor is not supported on Windows. Use PowerShellExecutor instead."

def _ensure_supported(self) -> None:
if os.name == "nt":
raise RuntimeError(self._unsupported_on_windows_message())

async def _ensure_session(self, env: dict[str, str]) -> asyncio.subprocess.Process:
"""Ensure persistent shell session exists."""
if self._session is None or self._session.returncode is not None:
Expand Down Expand Up @@ -74,6 +82,12 @@ async def execute(
timeout: float | None = None,
env: dict[str, str] | None = None,
) -> ExecuteResult:
if os.name == "nt":
return ExecuteResult(
exit_code=127,
stdout="",
stderr=self._unsupported_on_windows_message(),
)
work_dir = cwd or self.default_cwd or os.getcwd()

merged_env = os.environ.copy()
Expand Down Expand Up @@ -119,6 +133,7 @@ async def execute_async(
cwd: str | None = None,
env: dict[str, str] | None = None,
) -> AsyncCommand:
self._ensure_supported()
work_dir = cwd or self.default_cwd or os.getcwd()
command_id = f"cmd_{uuid.uuid4().hex[:12]}"

Expand Down
15 changes: 15 additions & 0 deletions core/tools/command/zsh/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ def __init__(self, default_cwd: str | None = None):
self._session_lock = asyncio.Lock()
self._current_cwd = default_cwd or os.getcwd()

@staticmethod
def _unsupported_on_windows_message() -> str:
return "ZshExecutor is not supported on Windows. Use PowerShellExecutor instead."

def _ensure_supported(self) -> None:
if os.name == "nt":
raise RuntimeError(self._unsupported_on_windows_message())

async def _ensure_session(self, env: dict[str, str]) -> asyncio.subprocess.Process:
"""Ensure persistent shell session exists."""
if self._session is None or self._session.returncode is not None:
Expand Down Expand Up @@ -74,6 +82,12 @@ async def execute(
timeout: float | None = None,
env: dict[str, str] | None = None,
) -> ExecuteResult:
if os.name == "nt":
return ExecuteResult(
exit_code=127,
stdout="",
stderr=self._unsupported_on_windows_message(),
)
work_dir = cwd or self.default_cwd or os.getcwd()

merged_env = os.environ.copy()
Expand Down Expand Up @@ -119,6 +133,7 @@ async def execute_async(
cwd: str | None = None,
env: dict[str, str] | None = None,
) -> AsyncCommand:
self._ensure_supported()
work_dir = cwd or self.default_cwd or os.getcwd()
command_id = f"cmd_{uuid.uuid4().hex[:12]}"

Expand Down
18 changes: 16 additions & 2 deletions sandbox/capability.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
from sandbox.manager import SandboxManager


def _ps_quote(value: str) -> str:
return "'" + value.replace("'", "''") + "'"


class SandboxCapability:
"""Agent-facing capability object.

Expand Down Expand Up @@ -79,15 +83,25 @@ def __init__(self, session: ChatSession, manager: SandboxManager | None = None):
db_path = getattr(session.terminal, "db_path", None)
self._db_path: Path | None = Path(db_path) if db_path else None

def _uses_windows_shell(self) -> bool:
return bool(getattr(self._session.runtime, "_use_windows_shell", False))

def _wrap_command(self, command: str, cwd: str | None, env: dict[str, str] | None) -> tuple[str, str]:
wrapped = command
windows_shell = self._uses_windows_shell()
if env:
exports = "\n".join(f"export {k}={shlex.quote(v)}" for k, v in env.items())
if windows_shell:
exports = "\n".join(f"$env:{k} = {_ps_quote(str(v))}" for k, v in env.items())
else:
exports = "\n".join(f"export {k}={shlex.quote(str(v))}" for k, v in env.items())
wrapped = f"{exports}\n{wrapped}"
# @@@runtime-owned-cwd - Preserve runtime session cwd unless caller explicitly requests cwd override.
work_dir = cwd or self._session.terminal.get_state().cwd
if cwd:
wrapped = f"cd {shlex.quote(cwd)}\n{wrapped}"
if windows_shell:
wrapped = f"Set-Location -LiteralPath {_ps_quote(cwd)}\n{wrapped}"
else:
wrapped = f"cd {shlex.quote(cwd)}\n{wrapped}"
return wrapped, work_dir

async def execute(self, command: str, cwd: str | None = None, timeout: float | None = None, env: dict[str, str] | None = None):
Expand Down
5 changes: 4 additions & 1 deletion sandbox/providers/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ def _build_windows_shell_script(
lines = [
"$ErrorActionPreference = 'Continue'",
f"Set-Location -LiteralPath {_ps_quote(cwd)}",
"$leonErrorCount = $Error.Count",
]
for key, value in env_delta.items():
lines.append(f"$env:{key} = {_ps_quote(str(value))}")
Expand All @@ -268,7 +269,9 @@ def _build_windows_shell_script(
command,
"}",
"$leonExit = $LASTEXITCODE",
"if ($null -eq $leonExit) { $leonExit = 0 }",
"if ($null -eq $leonExit -or $leonExit -eq 0) {",
" if ($Error.Count -gt $leonErrorCount) { $leonExit = 1 } else { $leonExit = 0 }",
"}",
f"Write-Output {_ps_quote(start)}",
"(Get-Location).Path",
'Get-ChildItem Env: | ForEach-Object { "{0}={1}" -f $_.Name, $_.Value }',
Expand Down
42 changes: 41 additions & 1 deletion sandbox/terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,45 @@ def _connect(db_path: Path) -> sqlite3.Connection:
return connect_sqlite(db_path)


_REMOTE_PROVIDER_DEFAULT_CWDS = {
"agentbay": "/home/wuying",
"daytona": "/home/daytona",
"docker": "/workspace",
"e2b": "/home/user",
}


def _host_terminal_cwd() -> str:
try:
return str(Path.cwd())
except OSError:
return str(Path.home())


def lease_provider_name(lease_id: str | None, db_path: Path) -> str | None:
if not lease_id:
return None
try:
with _connect(db_path) as conn:
row = conn.execute(
"SELECT provider_name FROM sandbox_leases WHERE lease_id = ?",
(lease_id,),
).fetchone()
except sqlite3.Error:
return None
if not row or not row[0]:
return None
return str(row[0])


def default_terminal_cwd(provider_name: str | None = None) -> str:
if provider_name == "local":
return _host_terminal_cwd()
if provider_name:
return _REMOTE_PROVIDER_DEFAULT_CWDS.get(provider_name, "/home/user")
return _host_terminal_cwd()


@dataclass
class TerminalState:
"""Terminal state snapshot.
Expand Down Expand Up @@ -162,8 +201,9 @@ def _persist_state(self) -> None:

def terminal_from_row(row: dict, db_path: Path) -> AbstractTerminal:
"""Construct SQLiteTerminal from a repo dict."""
provider_name = lease_provider_name(row.get("lease_id"), db_path)
state = TerminalState(
cwd=row.get("cwd", "/root"),
cwd=row.get("cwd") or default_terminal_cwd(provider_name=provider_name),
env_delta=json.loads(row.get("env_delta_json", "{}")),
state_version=int(row.get("state_version", 0)),
)
Expand Down
2 changes: 1 addition & 1 deletion storage/contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def get_by_id(self, terminal_id: str) -> dict[str, Any] | None: ...
def get_latest_by_lease(self, lease_id: str) -> dict[str, Any] | None: ...
def get_timestamps(self, terminal_id: str) -> tuple[str | None, str | None]: ...
def list_by_thread(self, thread_id: str) -> list[dict[str, Any]]: ...
def create(self, terminal_id: str, thread_id: str, lease_id: str, initial_cwd: str = "/root") -> dict[str, Any]: ...
def create(self, terminal_id: str, thread_id: str, lease_id: str, initial_cwd: str | None = None) -> dict[str, Any]: ...
def set_active(self, thread_id: str, terminal_id: str) -> None: ...
def delete_by_thread(self, thread_id: str) -> None: ...
def delete(self, terminal_id: str) -> None: ...
Expand Down
10 changes: 7 additions & 3 deletions storage/providers/sqlite/terminal_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from sandbox.terminal import (
REQUIRED_ABSTRACT_TERMINAL_COLUMNS,
REQUIRED_TERMINAL_POINTER_COLUMNS,
default_terminal_cwd,
lease_provider_name,
)
from storage.providers.sqlite.connection import create_connection
from storage.providers.sqlite.kernel import SQLiteDBRole, resolve_role_db_path
Expand Down Expand Up @@ -235,19 +237,21 @@ def create(
terminal_id: str,
thread_id: str,
lease_id: str,
initial_cwd: str = "/root",
initial_cwd: str | None = None,
) -> dict[str, Any]:
now = datetime.now().isoformat()
env_delta_json = "{}"
state_version = 0
provider_name = lease_provider_name(lease_id, self._db_path)
effective_cwd = initial_cwd or default_terminal_cwd(provider_name=provider_name)

with self._lock:
self._conn.execute(
"""
INSERT INTO abstract_terminals (terminal_id, thread_id, lease_id, cwd, env_delta_json, state_version, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(terminal_id, thread_id, lease_id, initial_cwd, env_delta_json, state_version, now, now),
(terminal_id, thread_id, lease_id, effective_cwd, env_delta_json, state_version, now, now),
)
self._conn.commit()

Expand All @@ -257,7 +261,7 @@ def create(
"terminal_id": terminal_id,
"thread_id": thread_id,
"lease_id": lease_id,
"cwd": initial_cwd,
"cwd": effective_cwd,
"env_delta_json": env_delta_json,
"state_version": state_version,
"created_at": now,
Expand Down
19 changes: 16 additions & 3 deletions storage/providers/supabase/terminal_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from datetime import datetime
from typing import Any

from sandbox.terminal import default_terminal_cwd
from storage.providers.supabase import _query as q

_REPO = "terminal repo"
Expand All @@ -30,6 +31,16 @@ def _terminals(self) -> Any:
def _pointers(self) -> Any:
return self._client.table(_POINTERS_TABLE)

def _provider_name_for_lease(self, lease_id: str) -> str | None:
rows = q.rows(
self._client.table("sandbox_leases").select("provider_name").eq("lease_id", lease_id).limit(1).execute(),
_REPO,
"lookup lease provider",
)
if not rows or not rows[0].get("provider_name"):
return None
return str(rows[0]["provider_name"])

# ------------------------------------------------------------------
# Reads
# ------------------------------------------------------------------
Expand Down Expand Up @@ -145,17 +156,19 @@ def create(
terminal_id: str,
thread_id: str,
lease_id: str,
initial_cwd: str = "/root",
initial_cwd: str | None = None,
) -> dict[str, Any]:
now = datetime.now().isoformat()
env_delta_json = "{}"
state_version = 0
provider_name = self._provider_name_for_lease(lease_id)
effective_cwd = initial_cwd or default_terminal_cwd(provider_name=provider_name)
self._terminals().insert(
{
"terminal_id": terminal_id,
"thread_id": thread_id,
"lease_id": lease_id,
"cwd": initial_cwd,
"cwd": effective_cwd,
"env_delta_json": env_delta_json,
"state_version": state_version,
"created_at": now,
Expand All @@ -167,7 +180,7 @@ def create(
"terminal_id": terminal_id,
"thread_id": thread_id,
"lease_id": lease_id,
"cwd": initial_cwd,
"cwd": effective_cwd,
"env_delta_json": env_delta_json,
"state_version": state_version,
"created_at": now,
Expand Down
Loading