Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
aa4ed81
fix(copilot): inject actual working directory into SDK system prompt
majdyz Feb 26, 2026
a092c24
Merge branch 'master' of github.com:Significant-Gravitas/AutoGPT into…
majdyz Feb 26, 2026
224a8ca
fix(copilot): guard cwd precompute and remove unrelated frontend changes
majdyz Feb 26, 2026
a5d6e7d
fix(copilot): move makedirs into early cwd try/except
majdyz Feb 26, 2026
15bdb09
fix(copilot): replace _precomputed_cwd with sdk_cwd directly
majdyz Feb 26, 2026
cfb0afb
feat(copilot): add E2B cloud sandbox integration for persistent bash …
majdyz Feb 26, 2026
5167948
fix(copilot): guard e2b integration test against pytest collection crash
majdyz Feb 26, 2026
7d98f1b
fix(copilot): address CodeRabbit review comments
majdyz Feb 26, 2026
cb35d46
fix(copilot/e2b): path traversal guard + race-free sandbox creation
majdyz Feb 26, 2026
bb59655
merge: resolve conflict with master (cwd inside try/finally)
majdyz Feb 27, 2026
408155e
feat(copilot): route file tools through E2B sandbox when active
majdyz Feb 27, 2026
600851b
fix(copilot): make workspace_files tests async to match _resolve_writ…
majdyz Feb 27, 2026
e1b2a31
fix(copilot): use integer timeouts for E2B sandbox commands
majdyz Feb 27, 2026
70da86b
fix(copilot): harden E2B file tools and sandbox path handling
majdyz Feb 27, 2026
03fdc28
fix(copilot): validate sandbox path boundaries and cast timeout to int
majdyz Feb 27, 2026
d3d5b88
fix(copilot): remove dead sync code, fix config description, align lo…
majdyz Feb 28, 2026
5fb5f4e
Merge remote-tracking branch 'origin/dev' into feat/improve-copilot-file
majdyz Feb 28, 2026
ae8e669
fix(copilot): simplify E2B file tools, harden DB sanitization, add ou…
majdyz Feb 28, 2026
47c21b2
fix(copilot): disable SDK built-in Read in E2B mode
majdyz Feb 28, 2026
b5957ca
fix(copilot): harden file access — session-scoped tool-results, share…
majdyz Feb 28, 2026
08973c5
fix(copilot): prevent duplicate sandbox creation on lock contention
majdyz Feb 28, 2026
e594aff
fix(copilot): clean up e2b_file_tools imports, remove manual test
majdyz Feb 28, 2026
08287f0
fix(copilot): set _current_project_dir in security_hooks_test
majdyz Feb 28, 2026
008146b
fix(copilot): add defense-in-depth to _read_local and path validation…
majdyz Feb 28, 2026
3ff7327
fix(copilot): cap MCP tool response size to prevent SDK buffer overflow
majdyz Feb 28, 2026
76b1232
fix(copilot): centralize MCP tool truncation and stash, improve front…
majdyz Feb 28, 2026
0e2e0b5
fix(copilot): cleanup — E2B default on, revert unrelated frontend cha…
majdyz Feb 28, 2026
a14e88b
fix(copilot): simplify e2b_sandbox.py, move imports to top-level
majdyz Feb 28, 2026
3959aff
fix(copilot): clean up e2b_file_tools — merge MCP helpers, reduce boi…
majdyz Feb 28, 2026
a3dc0e1
fix(copilot): deduplicate E2B_WORKDIR, move imports to top-level, add…
majdyz Feb 28, 2026
69073d1
fix(copilot): add error handling for sandbox write in _save_to_path
majdyz Feb 28, 2026
060661b
fix(copilot): add error handling for local write in _save_to_path
majdyz Mar 1, 2026
7ee1e8b
fix(copilot): read sandbox files as bytes with utf-8 decode fallback
majdyz Mar 1, 2026
85dfaf1
fix(copilot): try reconnecting before raising on lock contention
majdyz Mar 1, 2026
97097fe
Merge remote-tracking branch 'origin/dev' into feat/improve-copilot-file
majdyz Mar 1, 2026
a878261
fix(copilot): address reviewer should-fix items
majdyz Mar 1, 2026
0b3faa7
feat(copilot): add kill_sandbox() for explicit E2B cleanup on session…
majdyz Mar 1, 2026
bc57672
fix(copilot): address reviewer v7 should-fix items
majdyz Mar 1, 2026
41da564
test(copilot): add kill_sandbox timeout test
majdyz Mar 1, 2026
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
2 changes: 1 addition & 1 deletion autogpt_platform/backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ ENV DEBIAN_FRONTEND=noninteractive

# Install Python, FFmpeg, ImageMagick, and CLI tools for agent use.
# bubblewrap provides OS-level sandbox (whitelist-only FS + no network)
# for the bash_exec MCP tool.
# for the bash_exec MCP tool (fallback when E2B is not configured).
# Using --no-install-recommends saves ~650MB by skipping unnecessary deps like llvm, mesa, etc.
RUN apt-get update && apt-get install -y --no-install-recommends \
python3.13 \
Expand Down
37 changes: 37 additions & 0 deletions autogpt_platform/backend/backend/copilot/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,49 @@ class ChatConfig(BaseSettings):
"history compression. Falls back to compression when unavailable.",
)

# E2B Sandbox Configuration
use_e2b_sandbox: bool = Field(
default=False,
description="Use E2B cloud sandboxes for persistent bash/python execution. "
"When enabled, bash_exec routes commands to E2B and the session workspace "
"is mounted via sshfs so SDK file tools share the same filesystem.",
)
e2b_api_key: str | None = Field(
default=None,
description="E2B API key. Falls back to E2B_API_KEY environment variable.",
)
e2b_sandbox_template: str = Field(
default="base",
description="E2B sandbox template to use for copilot sessions.",
)
e2b_sandbox_timeout: int = Field(
default=43200, # 12 hours — same as session_ttl
description="E2B sandbox keepalive timeout in seconds.",
)

# Extended thinking configuration for Claude models
thinking_enabled: bool = Field(
default=True,
description="Enable adaptive thinking for Claude models via OpenRouter",
)

@field_validator("use_e2b_sandbox", mode="before")
@classmethod
def get_use_e2b_sandbox(cls, v):
"""Get use_e2b_sandbox from environment if not provided."""
env_val = os.getenv("CHAT_USE_E2B_SANDBOX", "").lower()
if env_val:
return env_val in ("true", "1", "yes", "on")
return False if v is None else v

@field_validator("e2b_api_key", mode="before")
@classmethod
def get_e2b_api_key(cls, v):
"""Get E2B API key from environment if not provided."""
if v is None:
v = os.getenv("E2B_API_KEY")
return v
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@field_validator("api_key", mode="before")
@classmethod
def get_api_key(cls, v):
Expand Down
85 changes: 74 additions & 11 deletions autogpt_platform/backend/backend/copilot/sdk/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,20 +79,30 @@ def available(self) -> bool:
# IMPORTANT: Must be less than frontend timeout (12s in useCopilotPage.ts)
_HEARTBEAT_INTERVAL = 10.0 # seconds


# Appended to the system prompt to inform the agent about available tools.
# The SDK built-in Bash is NOT available — use mcp__copilot__bash_exec instead,
# which has kernel-level network isolation (unshare --net).
_SDK_TOOL_SUPPLEMENT = """
def _build_sdk_tool_supplement(cwd: str) -> str:
"""Build the SDK tool supplement with the actual working directory injected."""
return f"""

## Tool notes

### Shell commands
- The SDK built-in Bash tool is NOT available. Use the `bash_exec` MCP tool
for shell commands — it runs in a network-isolated sandbox.

### Working directory
- Your working directory is: `{cwd}`
- All SDK Read/Write/Edit/Glob/Grep tools AND `bash_exec` operate inside this
directory. This is the ONLY writable path — do not attempt to read or write
anywhere else on the filesystem.
- Use relative paths or absolute paths under `{cwd}` for all file operations.

### Two storage systems — CRITICAL to understand

1. **Ephemeral working directory** (`/tmp/copilot-<session>/`):
1. **Ephemeral working directory** (`{cwd}`):
- Shared by SDK Read/Write/Edit/Glob/Grep tools AND `bash_exec`
- Files here are **lost between turns** — do NOT rely on them persisting
- Use for temporary work: running scripts, processing data, etc.
Expand Down Expand Up @@ -128,6 +138,7 @@ def available(self) -> bool:
All tasks must run in the foreground.
"""


STREAM_LOCK_PREFIX = "copilot:stream:lock:"


Expand Down Expand Up @@ -442,12 +453,26 @@ async def stream_chat_completion_sdk(
_background_tasks.add(task)
task.add_done_callback(_background_tasks.discard)

# Build system prompt (reuses non-SDK path with Langfuse support)
# Build system prompt (reuses non-SDK path with Langfuse support).
# Pre-compute the cwd here so the exact working directory path can be
# injected into the supplement instead of the generic placeholder.
# Catch ValueError early so the failure yields a clean StreamError rather
# than propagating outside the stream error-handling path.
has_history = len(session.messages) > 1
try:
sdk_cwd = _make_sdk_cwd(session_id)
os.makedirs(sdk_cwd, exist_ok=True)
except (ValueError, OSError) as e:
logger.error("[SDK] [%s] Invalid SDK cwd: %s", session_id[:12], e)
yield StreamError(
errorText="Unable to initialize working directory.",
code="sdk_cwd_error",
)
return
system_prompt, _ = await _build_system_prompt(
user_id, has_conversation_history=has_history
)
system_prompt += _SDK_TOOL_SUPPLEMENT
system_prompt += _build_sdk_tool_supplement(sdk_cwd)
message_id = str(uuid.uuid4())
stream_id = str(uuid.uuid4())

Expand Down Expand Up @@ -476,19 +501,42 @@ async def stream_chat_completion_sdk(

stream_completed = False
# Initialise variables before the try so the finally block can
# always attempt transcript upload regardless of errors.
sdk_cwd = ""
# always attempt transcript upload and E2B cleanup regardless of errors.
e2b_sandbox = None
use_resume = False
resume_file: str | None = None
captured_transcript = CapturedTranscript()

try:
# Use a session-specific temp dir to avoid cleanup race conditions
# between concurrent sessions.
sdk_cwd = _make_sdk_cwd(session_id)
os.makedirs(sdk_cwd, exist_ok=True)
# Set up E2B sandbox for persistent cloud execution when configured.
# bash_exec routes commands to sandbox.commands.run() on E2B.
# SDK file tools (Read/Write/Edit) operate on local sdk_cwd, which
# is synced with the sandbox's /home/user via E2B's HTTP files API:
# turn start → sync_from_sandbox (download sandbox → local)
# turn end → sync_to_sandbox (upload local → sandbox)
if config.use_e2b_sandbox and config.e2b_api_key:
from ..tools.e2b_sandbox import get_or_create_sandbox, sync_from_sandbox

try:
e2b_sandbox = await get_or_create_sandbox(
session_id,
api_key=config.e2b_api_key,
template=config.e2b_sandbox_template,
timeout=config.e2b_sandbox_timeout,
)
# Populate local workspace with files from the sandbox so the
# SDK file tools see the latest state from previous turns.
await sync_from_sandbox(e2b_sandbox, sdk_cwd)
except Exception as e2b_err:
logger.error(
"[E2B] [%s] Setup failed: %s",
session_id[:12],
e2b_err,
exc_info=True,
)
e2b_sandbox = None

set_execution_context(user_id, session)
set_execution_context(user_id, session, sandbox=e2b_sandbox)
try:
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient

Expand Down Expand Up @@ -991,6 +1039,21 @@ async def _next_msg() -> Any:
exc_info=True,
)

# Upload any files written by SDK tools to the sandbox so they
# persist across turns and are visible to bash_exec next turn.
if e2b_sandbox is not None and sdk_cwd:
from ..tools.e2b_sandbox import sync_to_sandbox

try:
await sync_to_sandbox(e2b_sandbox, sdk_cwd)
except Exception as sync_err:
logger.error(
"[E2B] [%s] sync_to_sandbox failed: %s",
session_id[:12],
sync_err,
exc_info=True,
)

if sdk_cwd:
_cleanup_sdk_tool_results(sdk_cwd)

Expand Down
21 changes: 19 additions & 2 deletions autogpt_platform/backend/backend/copilot/sdk/tool_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@
import os
import uuid
from contextvars import ContextVar
from typing import Any
from typing import TYPE_CHECKING, Any

from backend.copilot.model import ChatSession
from backend.copilot.tools import TOOL_REGISTRY
from backend.copilot.tools.base import BaseTool

if TYPE_CHECKING:
from e2b import AsyncSandbox

logger = logging.getLogger(__name__)

# Allowed base directory for the Read tool (SDK saves oversized tool results here).
Expand All @@ -33,6 +36,12 @@
_current_session: ContextVar[ChatSession | None] = ContextVar(
"current_session", default=None
)
# E2B cloud sandbox for the current turn (None when E2B is not configured).
# Passed to bash_exec so commands run on E2B instead of the local bwrap sandbox.
_current_sandbox: ContextVar["AsyncSandbox | None"] = ContextVar(
"_current_sandbox", default=None
)

# Stash for MCP tool outputs before the SDK potentially truncates them.
# Keyed by tool_name → full output string. Consumed (popped) by the
# response adapter when it builds StreamToolOutputAvailable.
Expand All @@ -53,22 +62,30 @@
def set_execution_context(
user_id: str | None,
session: ChatSession,
sandbox: "AsyncSandbox | None" = None,
) -> None:
"""Set the execution context for tool calls.

This must be called before streaming begins to ensure tools have access
to user_id and session information.
to user_id, session, and (optionally) an E2B sandbox for bash execution.

Args:
user_id: Current user's ID.
session: Current chat session.
sandbox: Optional E2B sandbox; when set, bash_exec routes commands there.
"""
_current_user_id.set(user_id)
_current_session.set(session)
_current_sandbox.set(sandbox)
_pending_tool_outputs.set({})
_stash_event.set(asyncio.Event())


def get_current_sandbox() -> "AsyncSandbox | None":
"""Return the E2B sandbox for the current turn, or None."""
return _current_sandbox.get()


def get_execution_context() -> tuple[str | None, ChatSession | None]:
"""Get the current execution context."""
return (
Expand Down
Loading
Loading