fix(agent): default bash/python cwd to data/ to prevent ephemeral file loss (#2586)

Agent subprocesses (bash, python) previously inherited the container's default
working directory (/app), so files created with relative paths landed in the
ephemeral container layer and were silently destroyed on any docker compose up
--build or container recreation.

Set cwd=_AGENT_WORKDIR (resolved to <repo_root>/data at import time) and
HOME=_AGENT_WORKDIR on both subprocess launchers so that:
- pwd inside a bash tool returns the persistent data directory
- relative paths and ~ resolve to a location that survives rebuilds
- the agent can still cd to any absolute path it needs

The resolution uses pathlib.Path(__file__).parent.parent / "data", which
works for both Docker (/app/src → /app/data) and manual installs
(<repo>/src → <repo>/data) without requiring a new env var or compose change.

Fixes #2512

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Giuseppe
2026-06-04 20:16:04 +02:00
committed by GitHub
parent 7188737294
commit dd707ddb1e
+11
View File
@@ -12,12 +12,20 @@ import collections
import json import json
import logging import logging
import os import os
import pathlib
import sys import sys
import time import time
from typing import Any, Awaitable, Callable, Dict, Optional, Tuple from typing import Any, Awaitable, Callable, Dict, Optional, Tuple
from src.tool_security import is_public_blocked_tool, owner_is_admin_or_single_user from src.tool_security import is_public_blocked_tool, owner_is_admin_or_single_user
# Persistent working directory for agent subprocesses.
# Resolves to <repo_root>/data, which is the bind-mounted volume in Docker
# (/app/data) and the local data directory for manual installs.
# Using this as cwd and HOME prevents the agent from silently creating files
# in ephemeral container layers that are lost on the next rebuild.
_AGENT_WORKDIR = str(pathlib.Path(__file__).parent.parent / "data")
MAX_OUTPUT_CHARS = 10_000 MAX_OUTPUT_CHARS = 10_000
MAX_READ_CHARS = 20_000 MAX_READ_CHARS = 20_000
MAX_DIFF_LINES = 400 # cap unified-diff size returned to the UI MAX_DIFF_LINES = 400 # cap unified-diff size returned to the UI
@@ -591,6 +599,7 @@ async def _direct_fallback(
"TERM": "xterm-256color", "TERM": "xterm-256color",
"COLUMNS": "120", "COLUMNS": "120",
"LINES": "40", "LINES": "40",
"HOME": _AGENT_WORKDIR,
} }
try: try:
@@ -600,6 +609,7 @@ async def _direct_fallback(
stdout=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
env=_subproc_env, env=_subproc_env,
cwd=_AGENT_WORKDIR,
) )
stdout, stderr, rc, timed_out = await _run_subprocess_streaming( stdout, stderr, rc, timed_out = await _run_subprocess_streaming(
proc, proc,
@@ -626,6 +636,7 @@ async def _direct_fallback(
stdout=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
env=_subproc_env, env=_subproc_env,
cwd=_AGENT_WORKDIR,
) )
stdout, stderr, rc, timed_out = await _run_subprocess_streaming( stdout, stderr, rc, timed_out = await _run_subprocess_streaming(
proc, proc,