Skip to content

Commit 2e6b08d

Browse files
authored
fix: workspace folder permission & app container cannot access client API (#3300)
* also copy over pyproject and poetry lock * add missing readme * remove extra git config init since it is already done in client.py * only chown if the /workspace dir does not exists * Revert "remove extra git config init since it is already done in client.py" This reverts commit e8556cd. * remove extra git config init since it is already done in client.py * fix test runtime * print container log while reconnecting * print log in more readable format * print log in more readable format * increase lines * clean up sandbox and ssh related stuff * remove ssh hostname * remove ssh hostname * fix docker app cannot access runtime API issue * remove ssh password * API HOSTNAME should be pre-fixed with SANDBOX * update config * fix typo that breaks the test
1 parent ddd2565 commit 2e6b08d

File tree

5 files changed

+37
-25
lines changed

5 files changed

+37
-25
lines changed

containers/app/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ ARG OPEN_DEVIN_BUILD_VERSION #re-declare for this section
3737
ENV RUN_AS_DEVIN=true
3838
# A random number--we need this to be different from the user's UID on the host machine
3939
ENV OPENDEVIN_USER_ID=42420
40+
ENV SANDBOX_API_HOSTNAME=host.docker.internal
4041
ENV USE_HOST_NETWORK=false
4142
ENV WORKSPACE_BASE=/opt/workspace_base
4243
ENV OPEN_DEVIN_BUILD_VERSION=$OPEN_DEVIN_BUILD_VERSION

opendevin/core/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ class SandboxConfig(metaclass=Singleton):
145145
"""Configuration for the sandbox.
146146
147147
Attributes:
148+
api_hostname: The hostname for the EventStream Runtime API.
148149
container_image: The container image to use for the sandbox.
149150
user_id: The user ID for the sandbox.
150151
timeout: The timeout for the sandbox.
@@ -164,6 +165,7 @@ class SandboxConfig(metaclass=Singleton):
164165
Default is None for general purpose browsing. Check evaluation/miniwob and evaluation/webarena for examples.
165166
"""
166167

168+
api_hostname: str = 'localhost'
167169
container_image: str = (
168170
'ubuntu:22.04' # default to ubuntu:22.04 for eventstream runtime
169171
)

opendevin/runtime/client/client.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -128,14 +128,19 @@ def _init_user(self, username: str, user_id: int) -> None:
128128
raise RuntimeError(f'Failed to add sudoer: {output.stderr.decode()}')
129129
logger.debug(f'Added sudoer successfully. Output: [{output.stdout.decode()}]')
130130

131-
# Add user and change ownership of the initial working directory
131+
# Add user and change ownership of the initial working directory if it doesn't exist
132+
command = (
133+
f'useradd -rm -d /home/{username} -s /bin/bash '
134+
f'-g root -G sudo -u {user_id} {username}'
135+
)
136+
137+
if not os.path.exists(self.initial_pwd):
138+
command += f' && mkdir -p {self.initial_pwd}'
139+
command += f' && chown -R {username}:root {self.initial_pwd}'
140+
command += f' && chmod g+s {self.initial_pwd}'
141+
132142
output = subprocess.run(
133-
(
134-
f'useradd -rm -d /home/{username} -s /bin/bash '
135-
f'-g root -G sudo -u {user_id} {username} &&'
136-
f'chown -R {username}:root {self.initial_pwd} && '
137-
f'chmod g+s {self.initial_pwd}'
138-
),
143+
command,
139144
shell=True,
140145
capture_output=True,
141146
)
@@ -381,11 +386,11 @@ async def write(self, action: FileWriteAction) -> Observation:
381386
assert file_stat is not None
382387
# restore the original file permissions if the file already exists
383388
os.chmod(filepath, file_stat.st_mode)
384-
os.chown(filepath, file_stat.st_uid, ROOT_GID)
389+
os.chown(filepath, file_stat.st_uid, file_stat.st_gid)
385390
else:
386391
# set the new file permissions if the file is new
387392
os.chmod(filepath, 0o644)
388-
os.chown(filepath, self.user_id, ROOT_GID)
393+
os.chown(filepath, self.user_id, self.user_id)
389394

390395
except FileNotFoundError:
391396
return ErrorObservation(f'File not found: {filepath}')

opendevin/runtime/client/runtime.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
)
2323
from opendevin.events.action.action import Action
2424
from opendevin.events.observation import (
25-
CmdOutputObservation,
2625
ErrorObservation,
2726
NullObservation,
2827
Observation,
@@ -54,7 +53,7 @@ def __init__(
5453
config, event_stream, sid, plugins
5554
) # will initialize the event stream
5655
self._port = find_available_tcp_port()
57-
self.api_url = f'http://localhost:{self._port}'
56+
self.api_url = f'http://{self.config.sandbox.api_hostname}:{self._port}'
5857
self.session: Optional[aiohttp.ClientSession] = None
5958

6059
self.instance_id = (
@@ -98,8 +97,6 @@ async def ainit(self, env_vars: dict[str, str] | None = None):
9897
)
9998
logger.info(f'Container initialized with env vars: {env_vars}')
10099

101-
await self._init_git_config()
102-
103100
@staticmethod
104101
def _init_docker_client() -> docker.DockerClient:
105102
try:
@@ -183,16 +180,6 @@ async def _init_container(
183180
await self.close(close_client=False)
184181
raise e
185182

186-
async def _init_git_config(self):
187-
action = CmdRunAction(
188-
'git config --global user.name "opendevin" && '
189-
'git config --global user.email "[email protected]"'
190-
)
191-
logger.info(f'Setting git config: {action}')
192-
obs: Observation = await self.run_action(action)
193-
assert isinstance(obs, CmdOutputObservation)
194-
assert obs.exit_code == 0, f'Failed to set git config: {obs}'
195-
196183
async def _ensure_session(self):
197184
await asyncio.sleep(1)
198185
if self.session is None or self.session.closed:
@@ -205,6 +192,20 @@ async def _ensure_session(self):
205192
)
206193
async def _wait_until_alive(self):
207194
logger.info('Reconnecting session')
195+
container = self.docker_client.containers.get(self.container_name)
196+
# print logs
197+
_logs = container.logs(tail=10).decode('utf-8').split('\n')
198+
# add indent
199+
_logs = '\n'.join([f' |{log}' for log in _logs])
200+
logger.info(
201+
'\n'
202+
+ '-' * 30
203+
+ 'Container logs (last 10 lines):'
204+
+ '-' * 30
205+
+ f'\n{_logs}'
206+
+ '\n'
207+
+ '-' * 90
208+
)
208209
async with aiohttp.ClientSession() as session:
209210
async with session.get(f'{self.api_url}/alive') as response:
210211
if response.status == 200:

tests/unit/test_runtime.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1265,9 +1265,12 @@ async def test_keep_prompt(temp_dir):
12651265

12661266

12671267
@pytest.mark.asyncio
1268-
async def test_git_operation(temp_dir, box_class):
1268+
async def test_git_operation(box_class):
1269+
# do not mount workspace, since workspace mount by tests will be owned by root
1270+
# while the user_id we get via os.getuid() is different from root
1271+
# which causes permission issues
12691272
runtime = await _load_runtime(
1270-
temp_dir,
1273+
temp_dir=None,
12711274
box_class=box_class,
12721275
# Need to use non-root user to expose issues
12731276
run_as_devin=True,

0 commit comments

Comments
 (0)