Compare commits

...

7 Commits

Author SHA1 Message Date
openhands
a5b4e2a1df Remove hardcoded /workspace defaults from files.py
- Remove hardcoded container_workspace_path = '/workspace' default
- Remove hardcoded host_workspace_path = '/tmp/workspace' fallback
- Function now requires proper sandbox.volumes configuration
- Prevents file access when no volumes are configured
2025-06-15 00:40:36 +00:00
openhands
fd3ef04c9f Merge main into remove-workspace-vars-skip-tests 2025-06-15 00:38:38 +00:00
openhands
9544bdd686 Remove hardcoded /workspace defaults and fix resolver tests
- Remove hardcoded /workspace mount path from Makefile setup
- Update issue resolver to use actual workspace paths instead of hardcoded /workspace
- Fix resolver tests to expect actual workspace paths
- Replace deprecated workspace_base/workspace_mount_path with sandbox.volumes
2025-06-15 00:33:01 +00:00
openhands
160d535697 Fix workspace_root property to use hardcoded /workspace path
The workspace_root property was still referencing the removed
workspace_mount_path_in_sandbox config property. Updated to use
the standard /workspace path that is used throughout the codebase.
2025-06-14 17:58:07 +00:00
openhands
dcd776a6bc Address @neubig review comments: revert functionality changes
- Revert workspace_root in base.py to use config.workspace_mount_path_in_sandbox
- Revert Dockerfile to use WORKSPACE_BASE=/opt/workspace_base
- Revert docker-compose.yml to use original workspace mount paths
- Revert logic_inference.py to use workspace_mount_path parameter
- Revert issue_resolver.py to use workspace_base and workspace_mount_path config
2025-06-14 17:36:27 +00:00
openhands
c0c3475a57 Resolve merge conflict in remote_runtime.py 2025-06-14 17:33:28 +00:00
openhands
77149a98c5 Remove deprecated workspace variables
This commit removes the deprecated workspace configuration variables:
- workspace_base
- workspace_mount_path
- workspace_mount_path_in_sandbox
- workspace_mount_rewrite

Key changes:
- Remove deprecated fields from OpenHandsConfig model
- Add backward compatibility properties with deprecation warnings
- Update runtime to use sandbox.volumes instead of workspace_mount_path
- Update CLI to handle backward compatibility
- Update tests to use new configuration approach

The deprecated variables are replaced with the more flexible sandbox.volumes
configuration that allows multiple volume mounts.
2025-06-14 14:45:47 +00:00
70 changed files with 311 additions and 574 deletions

View File

@@ -272,9 +272,14 @@ setup-config:
setup-config-prompts:
@echo "[core]" > $(CONFIG_FILE).tmp
@echo "" >> $(CONFIG_FILE).tmp
@echo "[sandbox]" >> $(CONFIG_FILE).tmp
@read -p "Enter your workspace directory (as absolute path) [default: $(DEFAULT_WORKSPACE_DIR)]: " workspace_dir; \
workspace_dir=$${workspace_dir:-$(DEFAULT_WORKSPACE_DIR)}; \
echo "workspace_base=\"$$workspace_dir\"" >> $(CONFIG_FILE).tmp
read -p "Enter the container mount path [default: $$workspace_dir]: " container_path; \
container_path=$${container_path:-$$workspace_dir}; \
echo "volumes=\"$$workspace_dir:$$container_path:rw\"" >> $(CONFIG_FILE).tmp
@echo "" >> $(CONFIG_FILE).tmp
@@ -292,7 +297,9 @@ setup-config-prompts:
setup-config-basic:
@printf '%s\n' \
'[core]' \
'workspace_base="./workspace"' \
'' \
'[sandbox]' \
'volumes="./workspace:./workspace:rw"' \
> config.toml
@echo "$(GREEN)config.toml created.$(RESET)"

View File

@@ -23,8 +23,7 @@
# Daytona Target
#daytona_target = ""
# Base path for the workspace
#workspace_base = "./workspace"
# Cache directory path
#cache_dir = "/tmp/cache"
@@ -66,14 +65,7 @@
# Maximum number of iterations
#max_iterations = 250
# Path to mount the workspace in the sandbox
#workspace_mount_path_in_sandbox = "/workspace"
# Path to mount the workspace
#workspace_mount_path = ""
# Path to rewrite the workspace mount path to
#workspace_mount_rewrite = ""
# Run as openhands
#run_as_openhands = true

View File

@@ -24,11 +24,6 @@ The core configuration options are defined in the `[core]` section of the `confi
- Description: API token secret for Modal
### Workspace
- `workspace_base` **(Deprecated)**
- Type: `str`
- Default: `"./workspace"`
- Description: Base path for the workspace. **Deprecated: Use `SANDBOX_VOLUMES` instead.**
- `cache_dir`
- Type: `str`
- Default: `"/tmp/cache"`
@@ -104,21 +99,6 @@ The core configuration options are defined in the `[core]` section of the `confi
- Default: `None`
- Description: Volume mounts in the format 'host_path:container_path[:mode]', e.g. '/my/host/dir:/workspace:rw'. Multiple mounts can be specified using commas, e.g. '/path1:/workspace/path1,/path2:/workspace/path2:ro'
- `workspace_mount_path_in_sandbox` **(Deprecated)**
- Type: `str`
- Default: `"/workspace"`
- Description: Path to mount the workspace in the sandbox. **Deprecated: Use `SANDBOX_VOLUMES` instead.**
- `workspace_mount_path` **(Deprecated)**
- Type: `str`
- Default: `""`
- Description: Path to mount the workspace. **Deprecated: Use `SANDBOX_VOLUMES` instead.**
- `workspace_mount_rewrite` **(Deprecated)**
- Type: `str`
- Default: `""`
- Description: Path to rewrite the workspace mount path to. You can usually ignore this, it refers to special cases of running inside another container. **Deprecated: Use `SANDBOX_VOLUMES` instead.**
### Miscellaneous
- `run_as_openhands`
- Type: `bool`

View File

@@ -68,9 +68,6 @@ def get_config(
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)

View File

@@ -49,9 +49,6 @@ def get_config(
runtime=os.environ.get('RUNTIME', 'docker'),
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)

View File

@@ -55,9 +55,6 @@ def get_config(
runtime=os.environ.get('RUNTIME', 'docker'),
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)

View File

@@ -66,9 +66,6 @@ def get_config(
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)

View File

@@ -1,5 +1,4 @@
import argparse
import os
import re
from collections import defaultdict
@@ -16,15 +15,6 @@ def get_likely_indent_size(array_of_tabs) -> int:
return int(max(sizes, key=sizes.get))
def get_target_filepath(self):
target_filepath = os.path.join(
self.workspace_mount_path,
self.biocoder_instance.repository.split('/')[1],
self.biocoder_instance.filePath,
)
return target_filepath
def remove_code(target_filepath: str, line_start: int, line_end: int, language: str):
comment_prefix = {'python': '#', 'java': '//'}

View File

@@ -80,9 +80,6 @@ def get_config(
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)

View File

@@ -45,8 +45,6 @@ def get_config(
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)

View File

@@ -119,9 +119,6 @@ def get_config(
max_iterations=metadata.max_iterations,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(
update_llm_config_for_completions_logging(

View File

@@ -70,9 +70,6 @@ def get_config(
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)

View File

@@ -56,9 +56,6 @@ def get_config(
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
if metadata.agent_config:

View File

@@ -48,9 +48,6 @@ def get_config(
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)

View File

@@ -69,9 +69,6 @@ def get_config(
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)

View File

@@ -90,9 +90,6 @@ def get_config(
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)

View File

@@ -43,9 +43,6 @@ def get_config(
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)

View File

@@ -53,9 +53,6 @@ def get_config(
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)

View File

@@ -57,9 +57,6 @@ def get_config(
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)

View File

@@ -63,9 +63,6 @@ def get_config(
runtime=os.environ.get('RUNTIME', 'docker'),
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(
update_llm_config_for_completions_logging(

View File

@@ -115,9 +115,6 @@ def get_config(
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)

View File

@@ -85,9 +85,6 @@ def get_config(
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)

View File

@@ -91,9 +91,6 @@ def get_config(metadata: EvalMetadata, instance: pd.Series) -> OpenHandsConfig:
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
return config

View File

@@ -346,9 +346,6 @@ def get_config(
max_iterations=metadata.max_iterations,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(
update_llm_config_for_completions_logging(

View File

@@ -70,9 +70,6 @@ def get_config(
max_budget_per_task=4,
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(
update_llm_config_for_completions_logging(

View File

@@ -87,9 +87,6 @@ def get_config(metadata: EvalMetadata, instance: pd.Series) -> OpenHandsConfig:
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
return config

View File

@@ -201,9 +201,6 @@ def get_config(
max_iterations=metadata.max_iterations,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(

View File

@@ -203,9 +203,6 @@ def get_config(
max_iterations=metadata.max_iterations,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(
update_llm_config_for_completions_logging(

View File

@@ -70,8 +70,6 @@ def get_config(instance: pd.Series) -> OpenHandsConfig:
'SANDBOX_REMOTE_RUNTIME_API_URL', 'http://localhost:8000'
),
),
workspace_base=None,
workspace_mount_path=None,
)

View File

@@ -146,9 +146,6 @@ def get_config(
keep_runtime_alive=False,
remote_runtime_init_timeout=3600,
),
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(
update_llm_config_for_completions_logging(

View File

@@ -42,6 +42,9 @@ def get_config(
sandbox_config.enable_auto_lint = True
# If the web services are running on the host machine, this must be set to True
sandbox_config.use_host_network = True
# we mount trajectories path so that trajectories, generated by OpenHands
# controller, can be accessible to the evaluator file in the runtime container
sandbox_config.volumes = [f'{mount_path_on_host}:/outputs']
config = OpenHandsConfig(
run_as_openhands=False,
max_budget_per_task=4,
@@ -50,10 +53,6 @@ def get_config(
mount_path_on_host, f'traj_{task_short_name}.json'
),
sandbox=sandbox_config,
# we mount trajectories path so that trajectories, generated by OpenHands
# controller, can be accessible to the evaluator file in the runtime container
workspace_mount_path=mount_path_on_host,
workspace_mount_path_in_sandbox='/outputs',
)
config.set_llm_config(llm_config)
if agent_config:

View File

@@ -49,9 +49,6 @@ def get_config(
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)

View File

@@ -166,9 +166,6 @@ def get_config(
max_iterations=metadata.max_iterations,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(
update_llm_config_for_completions_logging(

View File

@@ -78,9 +78,6 @@ def get_config(
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
attach_to_existing=True,
)
config.set_llm_config(

View File

@@ -70,9 +70,6 @@ def get_config(
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)

View File

@@ -50,9 +50,6 @@ def get_config(
runtime=os.environ.get('RUNTIME', 'docker'),
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
# debug
debug=True,
)

View File

@@ -418,15 +418,24 @@ async def main_with_loop(loop: asyncio.AbstractEventLoop) -> None:
if not should_override_cli_defaults:
config.runtime = 'cli'
if not config.workspace_base:
config.workspace_base = os.getcwd()
# Ensure sandbox.volumes is set
if not config.sandbox.volumes:
config.sandbox.volumes = os.getcwd()
config.security.confirmation_mode = True
# TODO: Set working directory from config or use current working directory?
current_dir = config.workspace_base
# Use sandbox.volumes to determine the working directory
current_dir = os.getcwd()
if config.sandbox.volumes:
# Extract the host path from sandbox.volumes if it's in the format host:container:mode
if isinstance(config.sandbox.volumes, str) and ':' in config.sandbox.volumes:
current_dir = config.sandbox.volumes.split(':')[0]
else:
current_dir = config.sandbox.volumes
if not current_dir:
raise ValueError('Workspace base directory not specified')
raise ValueError('Workspace directory not specified')
if not check_folder_security_agreement(config, current_dir):
# User rejected, exit application

View File

@@ -36,10 +36,7 @@ class OpenHandsConfig(BaseModel):
save_screenshots_in_trajectory: Whether to save screenshots in trajectory (in encoded image format).
replay_trajectory_path: Path to load trajectory and replay. If provided, trajectory would be replayed first before user's instruction.
search_api_key: API key for Tavily search engine (https://tavily.com/).
workspace_base (deprecated): Base path for the workspace. Defaults to `./workspace` as absolute path.
workspace_mount_path (deprecated): Path to mount the workspace. Defaults to `workspace_base`.
workspace_mount_path_in_sandbox (deprecated): Path to mount the workspace in sandbox. Defaults to `/workspace`.
workspace_mount_rewrite (deprecated): Path to rewrite the workspace mount path.
cache_dir: Path to cache directory. Defaults to `/tmp/cache`.
run_as_openhands: Whether to run as openhands.
max_iterations: Maximum number of iterations allowed.
@@ -75,13 +72,6 @@ class OpenHandsConfig(BaseModel):
description='API key for Tavily search engine (https://tavily.com/). Required for search functionality.',
)
# Deprecated parameters - will be removed in a future version
workspace_base: str | None = Field(default=None, deprecated=True)
workspace_mount_path: str | None = Field(default=None, deprecated=True)
workspace_mount_path_in_sandbox: str = Field(default='/workspace', deprecated=True)
workspace_mount_rewrite: str | None = Field(default=None, deprecated=True)
# End of deprecated parameters
cache_dir: str = Field(default='/tmp/cache')
run_as_openhands: bool = Field(default=True)
max_iterations: int = Field(default=OH_MAX_ITERATIONS)
@@ -157,4 +147,6 @@ class OpenHandsConfig(BaseModel):
super().model_post_init(__context)
if not OpenHandsConfig.defaults_dict: # Only set defaults_dict if it's empty
OpenHandsConfig.defaults_dict = model_defaults_to_dict(self)
defaults = model_defaults_to_dict(self)
OpenHandsConfig.defaults_dict = defaults

View File

@@ -305,43 +305,10 @@ def get_or_create_jwt_secret(file_store: FileStore) -> str:
def finalize_config(cfg: OpenHandsConfig) -> None:
"""More tweaks to the config after it's been loaded."""
# Handle the sandbox.volumes parameter
if cfg.workspace_base is not None or cfg.workspace_mount_path is not None:
logger.openhands_logger.warning(
'DEPRECATED: The WORKSPACE_BASE and WORKSPACE_MOUNT_PATH environment variables are deprecated. '
"Please use RUNTIME_MOUNT instead, e.g. 'RUNTIME_MOUNT=/my/host/dir:/workspace:rw'"
)
if cfg.sandbox.volumes is not None:
# Split by commas to handle multiple mounts
mounts = cfg.sandbox.volumes.split(',')
# Check if any mount explicitly targets /workspace
workspace_mount_found = False
for mount in mounts:
parts = mount.split(':')
if len(parts) >= 2 and parts[1] == '/workspace':
workspace_mount_found = True
host_path = os.path.abspath(parts[0])
# Set the workspace_mount_path and workspace_mount_path_in_sandbox
cfg.workspace_mount_path = host_path
cfg.workspace_mount_path_in_sandbox = '/workspace'
# Also set workspace_base
cfg.workspace_base = host_path
break
# If no explicit /workspace mount was found, don't set any workspace mount
# This allows users to mount volumes without affecting the workspace
if not workspace_mount_found:
logger.openhands_logger.debug(
'No explicit /workspace mount found in SANDBOX_VOLUMES. '
'Using default workspace path in sandbox.'
)
# Ensure workspace_mount_path and workspace_base are None to avoid
# unintended mounting behavior
cfg.workspace_mount_path = None
cfg.workspace_base = None
# Validate all mounts
for mount in mounts:
parts = mount.split(':')
@@ -351,18 +318,6 @@ def finalize_config(cfg: OpenHandsConfig) -> None:
f"Expected format: 'host_path:container_path[:mode]', e.g. '/my/host/dir:/workspace:rw'"
)
# Handle the deprecated workspace_* parameters
elif cfg.workspace_base is not None or cfg.workspace_mount_path is not None:
if cfg.workspace_base is not None:
cfg.workspace_base = os.path.abspath(cfg.workspace_base)
if cfg.workspace_mount_path is None:
cfg.workspace_mount_path = cfg.workspace_base
if cfg.workspace_mount_rewrite:
base = cfg.workspace_base or os.getcwd()
parts = cfg.workspace_mount_rewrite.split(':')
cfg.workspace_mount_path = base.replace(parts[0], parts[1])
# make sure log_completions_folder is an absolute path
for llm in cfg.llms.values():
llm.log_completions_folder = os.path.abspath(llm.log_completions_folder)

View File

@@ -179,9 +179,9 @@ class IssueResolver:
config.max_budget_per_task = 4
config.max_iterations = max_iterations
# do not mount workspace
config.workspace_base = workspace_base
config.workspace_mount_path = workspace_base
# Configure workspace volume mount
if workspace_base:
config.sandbox.volumes = f'{workspace_base}:{workspace_base}:rw'
config.agents = {'CodeActAgent': AgentConfig(disabled_microagents=['github'])}
cls.update_sandbox_config(
@@ -266,15 +266,19 @@ class IssueResolver:
logger.info('-' * 30)
obs: Observation
action = CmdRunAction(command='cd /workspace')
action = CmdRunAction(command=f'cd {self.workspace_base}')
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
if not isinstance(obs, CmdOutputObservation) or obs.exit_code != 0:
raise RuntimeError(f'Failed to change directory to /workspace.\n{obs}')
raise RuntimeError(
f'Failed to change directory to {self.workspace_base}.\n{obs}'
)
if self.platform == ProviderType.GITLAB and self.GITLAB_CI:
action = CmdRunAction(command='sudo chown -R 1001:0 /workspace/*')
action = CmdRunAction(
command=f'sudo chown -R 1001:0 {self.workspace_base}/*'
)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -310,13 +314,13 @@ class IssueResolver:
logger.info('-' * 30)
obs: Observation
action = CmdRunAction(command='cd /workspace')
action = CmdRunAction(command=f'cd {self.workspace_base}')
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
if not isinstance(obs, CmdOutputObservation) or obs.exit_code != 0:
raise RuntimeError(
f'Failed to change directory to /workspace. Observation: {obs}'
f'Failed to change directory to {self.workspace_base}. Observation: {obs}'
)
action = CmdRunAction(command='git config --global core.pager ""')
@@ -327,7 +331,7 @@ class IssueResolver:
raise RuntimeError(f'Failed to set git config. Observation: {obs}')
action = CmdRunAction(
command='git config --global --add safe.directory /workspace'
command=f'git config --global --add safe.directory {self.workspace_base}'
)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)

View File

@@ -387,9 +387,9 @@ class Runtime(FileEditRuntimeMixin):
)
if not selected_repository:
# In SaaS mode (indicated by user_id being set), always run git init
# In OSS mode, only run git init if workspace_base is not set
if self.user_id or not self.config.workspace_base:
# Check if we should initialize a git repository in the workspace
# Skip initialization if sandbox.volumes is set
if not self.config.sandbox.volumes:
logger.debug(
'No repository selected. Initializing a new git repository in the workspace.'
)
@@ -397,10 +397,6 @@ class Runtime(FileEditRuntimeMixin):
command=f'git init && git config --global --add safe.directory {self.workspace_root}'
)
self.run_action(action)
else:
logger.info(
'In workspace mount mode, not initializing a new git repository.'
)
return ''
# This satisfies mypy because param is optional, but `verify_repo_provider` guarentees this gets populated
@@ -493,7 +489,7 @@ class Runtime(FileEditRuntimeMixin):
@property
def workspace_root(self) -> Path:
"""Return the workspace root path."""
return Path(self.config.workspace_mount_path_in_sandbox)
return Path('/workspace')
def maybe_setup_git_hooks(self):
"""Set up git hooks if .openhands/pre-commit.sh exists in the workspace or repository."""

View File

@@ -96,14 +96,24 @@ class CLIRuntime(Runtime):
git_provider_tokens,
)
# Set up workspace
if self.config.workspace_base is not None:
# Set up workspace directory based on sandbox.volumes
workspace_path = None
if self.config.sandbox.volumes:
# Parse sandbox.volumes to find workspace mount
mounts = self.config.sandbox.volumes.split(',')
for mount in mounts:
parts = mount.split(':')
if len(parts) >= 2 and parts[1] == '/workspace':
workspace_path = parts[0]
break
if workspace_path:
logger.warning(
f'Workspace base path is set to {self.config.workspace_base}. '
f'Workspace path is set to {workspace_path}. '
'It will be used as the path for the agent to run in. '
'Be careful, the agent can EDIT files in this directory!'
)
self._workspace_path = self.config.workspace_base
self._workspace_path = workspace_path
else:
# Create a temporary directory for the workspace
self._workspace_path = tempfile.mkdtemp(
@@ -111,9 +121,6 @@ class CLIRuntime(Runtime):
)
logger.info(f'Created temporary workspace at {self._workspace_path}')
# Runtime tests rely on this being set correctly.
self.config.workspace_mount_path_in_sandbox = self._workspace_path
# Initialize runtime state
self._runtime_initialized = False
self.file_editor = OHEditor(workspace_root=self._workspace_path)

View File

@@ -58,8 +58,8 @@ class DaytonaRuntime(ActionExecutionClient):
)
self.daytona = Daytona(daytona_config)
# workspace_base cannot be used because we can't bind mount into a workspace.
if self.config.workspace_base is not None:
# Workspace mounting is not supported in the Daytona runtime.
if self.config.sandbox.volumes:
self.log(
'warning',
'Workspace mounting is not supported in the Daytona runtime.',
@@ -143,8 +143,7 @@ class DaytonaRuntime(ActionExecutionClient):
override_username='openhands',
)
start_command_str: str = (
f'mkdir -p {self.config.workspace_mount_path_in_sandbox} && cd /openhands/code && '
+ ' '.join(start_command)
'mkdir -p /workspace && cd /openhands/code && ' + ' '.join(start_command)
)
self.log(
@@ -262,7 +261,7 @@ class DaytonaRuntime(ActionExecutionClient):
return None
self._vscode_url = (
self._construct_api_url(self._vscode_port)
+ f'/?tkn={token}&folder={self.config.workspace_mount_path_in_sandbox}'
+ f'/?tkn={token}&folder=/workspace'
)
self.log(

View File

@@ -248,22 +248,6 @@ class DockerRuntime(ActionExecutionClient):
f'Mount dir (sandbox.volumes): {host_path} to {container_path} with mode: {mount_mode}'
)
# Legacy mounting with workspace_* parameters
elif (
self.config.workspace_mount_path is not None
and self.config.workspace_mount_path_in_sandbox is not None
):
mount_mode = 'rw' # Default mode
# e.g. result would be: {"/home/user/openhands/workspace": {'bind': "/workspace", 'mode': 'rw'}}
volumes[self.config.workspace_mount_path] = {
'bind': self.config.workspace_mount_path_in_sandbox,
'mode': mount_mode,
}
logger.debug(
f'Mount dir (legacy): {self.config.workspace_mount_path} with mode: {mount_mode}'
)
return volumes
def init_container(self) -> None:
@@ -338,8 +322,6 @@ class DockerRuntime(ActionExecutionClient):
# also update with runtime_startup_env_vars
environment.update(self.config.sandbox.runtime_startup_env_vars)
self.log('debug', f'Workspace Base: {self.config.workspace_base}')
# Process volumes for mounting
volumes = self._process_volumes()
@@ -351,7 +333,7 @@ class DockerRuntime(ActionExecutionClient):
volumes = {} # Empty dict instead of None to satisfy mypy
self.log(
'debug',
f'Sandbox workspace: {self.config.workspace_mount_path_in_sandbox}',
'Sandbox workspace: /workspace',
)
command = self.get_action_execution_server_startup_command()
@@ -483,7 +465,9 @@ class DockerRuntime(ActionExecutionClient):
if not token:
return None
vscode_url = f'http://localhost:{self._vscode_port}/?tkn={token}&folder={self.config.workspace_mount_path_in_sandbox}'
vscode_url = (
f'http://localhost:{self._vscode_port}/?tkn={token}&folder=/workspace'
)
return vscode_url
@property

View File

@@ -220,9 +220,8 @@ class LocalRuntime(ActionExecutionClient):
self._vscode_port = server_info.vscode_port
self._app_ports = server_info.app_ports
self._temp_workspace = server_info.temp_workspace
self.config.workspace_mount_path_in_sandbox = (
server_info.workspace_mount_path
)
# Store the workspace mount path from server info
self._workspace_mount_path = server_info.workspace_mount_path
self.api_url = (
f'{self.config.sandbox.local_runtime_url}:{self._execution_server_port}'
)
@@ -233,28 +232,36 @@ class LocalRuntime(ActionExecutionClient):
f'No existing server found for session {self.sid}'
)
else:
# Set up workspace directory
if self.config.workspace_base is not None:
# Set up workspace directory based on sandbox.volumes
workspace_path = None
if self.config.sandbox.volumes:
# Parse sandbox.volumes to find workspace mount
mounts = self.config.sandbox.volumes.split(',')
for mount in mounts:
parts = mount.split(':')
if len(parts) >= 2 and parts[1] == '/workspace':
workspace_path = parts[0]
break
if workspace_path:
logger.warning(
f'Workspace base path is set to {self.config.workspace_base}. '
f'Workspace path is set to {workspace_path}. '
'It will be used as the path for the agent to run in. '
'Be careful, the agent can EDIT files in this directory!'
)
self.config.workspace_mount_path_in_sandbox = self.config.workspace_base
self._workspace_mount_path = workspace_path
self._temp_workspace = None
else:
# A temporary directory is created for the agent to run in
logger.warning(
'Workspace base path is NOT set. Agent will run in a temporary directory.'
'Workspace path is NOT set. Agent will run in a temporary directory.'
)
self._temp_workspace = tempfile.mkdtemp(
prefix=f'openhands_workspace_{self.sid}',
)
self.config.workspace_mount_path_in_sandbox = self._temp_workspace
self._workspace_mount_path = self._temp_workspace
logger.info(
f'Using workspace directory: {self.config.workspace_mount_path_in_sandbox}'
)
logger.info(f'Using workspace directory: {self._workspace_mount_path}')
# Start a new server
self._execution_server_port = self._find_available_port(
@@ -380,7 +387,7 @@ class LocalRuntime(ActionExecutionClient):
log_thread=self._log_thread,
log_thread_exit_event=self._log_thread_exit_event,
temp_workspace=self._temp_workspace,
workspace_mount_path=self.config.workspace_mount_path_in_sandbox,
workspace_mount_path=self._workspace_mount_path,
)
self.log('info', f'Waiting for server to become ready at {self.api_url}...')
@@ -551,7 +558,7 @@ class LocalRuntime(ActionExecutionClient):
# Similar to remote runtime...
parsed_url = urlparse(runtime_url)
vscode_url = f'{parsed_url.scheme}://vscode-{parsed_url.netloc}'
return f'{vscode_url}/?tkn={token}&folder={self.config.workspace_mount_path_in_sandbox}'
return f'{vscode_url}/?tkn={token}&folder=/workspace'
@property
def web_hosts(self) -> dict[str, int]:

View File

@@ -69,13 +69,6 @@ class ModalRuntime(ActionExecutionClient):
'openhands', create_if_missing=True, client=self.modal_client
)
# workspace_base cannot be used because we can't bind mount into a sandbox.
if self.config.workspace_base is not None:
self.log(
'warning',
'Setting workspace_base is not supported in the modal runtime.',
)
# This value is arbitrary as it's private to the container
self.container_port = 3000
self._vscode_port = 4445
@@ -124,7 +117,7 @@ class ModalRuntime(ActionExecutionClient):
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
await call_sync_from_async(
self._init_sandbox,
sandbox_workspace_dir=self.config.workspace_mount_path_in_sandbox,
sandbox_workspace_dir='/workspace',
plugins=self.plugins,
)
@@ -270,10 +263,7 @@ echo 'export INPUTRC=/etc/inputrc' >> /etc/bash.bashrc
tunnel = self.sandbox.tunnels()[self._vscode_port]
tunnel_url = tunnel.url
self._vscode_url = (
tunnel_url
+ f'/?tkn={token}&folder={self.config.workspace_mount_path_in_sandbox}'
)
self._vscode_url = tunnel_url + f'/?tkn={token}&folder=/workspace'
self.log(
'debug',

View File

@@ -80,11 +80,6 @@ class RemoteRuntime(ActionExecutionClient):
)
self.session.headers.update({'X-API-Key': self.config.sandbox.api_key})
if self.config.workspace_base is not None:
self.log(
'debug',
'Setting workspace_base is not supported in the remote runtime.',
)
if self.config.sandbox.remote_runtime_api_url is None:
raise ValueError(
'remote_runtime_api_url is required in the remote runtime.'
@@ -379,7 +374,7 @@ class RemoteRuntime(ActionExecutionClient):
assert isinstance(_parsed_url.scheme, str) and isinstance(
_parsed_url.netloc, str
)
vscode_url = f'{_parsed_url.scheme}://vscode-{_parsed_url.netloc}/?tkn={token}&folder={self.config.workspace_mount_path_in_sandbox}'
vscode_url = f'{_parsed_url.scheme}://vscode-{_parsed_url.netloc}/?tkn={token}&folder=/workspace'
self.log(
'debug',
f'VSCode URL: {vscode_url}',

View File

@@ -106,9 +106,7 @@ class RunloopRuntime(ActionExecutionClient):
launch_parameters=LaunchParameters(
available_ports=[self._sandbox_port, self._vscode_port],
resource_size_request='LARGE',
launch_commands=[
f'mkdir -p {self.config.workspace_mount_path_in_sandbox}'
],
launch_commands=['mkdir -p /workspace'],
),
metadata={'container-name': self.container_name},
)
@@ -182,7 +180,7 @@ class RunloopRuntime(ActionExecutionClient):
id=self.devbox.id,
port=self._vscode_port,
).url
+ f'/?tkn={token}&folder={self.config.workspace_mount_path_in_sandbox}'
+ f'/?tkn={token}&folder=/workspace'
)
self.log(

View File

@@ -103,7 +103,7 @@ class VSCodePlugin(Plugin):
settings_path = current_dir / 'settings.json'
# Create the .vscode directory in the workspace if it doesn't exist
workspace_dir = Path(os.getenv('WORKSPACE_BASE', '/workspace'))
workspace_dir = Path('/workspace')
vscode_dir = workspace_dir / '.vscode'
vscode_dir.mkdir(parents=True, exist_ok=True)

View File

@@ -50,7 +50,7 @@ def get_action_execution_server_startup_command(
main_module,
str(server_port),
'--working-dir',
app_config.workspace_mount_path_in_sandbox,
'/workspace', # Default workspace path
*plugin_args,
'--username',
username,

View File

@@ -12,16 +12,14 @@ from openhands.events.observation import (
def resolve_path(
file_path: str,
working_directory: str,
workspace_base: str,
workspace_mount_path_in_sandbox: str,
sandbox_volumes: str | None,
) -> Path:
"""Resolve a file path to a path on the host filesystem.
Args:
file_path: The path to resolve.
working_directory: The working directory of the agent.
workspace_mount_path_in_sandbox: The path to the workspace inside the sandbox.
workspace_base: The base path of the workspace on the host filesystem.
sandbox_volumes: The sandbox volumes configuration.
Returns:
The resolved path on the host filesystem.
@@ -36,17 +34,33 @@ def resolve_path(
# (deny any .. path traversal to parent directories of the sandbox)
abs_path_in_sandbox = path_in_sandbox.resolve()
# If the path is outside the workspace, deny it
if not abs_path_in_sandbox.is_relative_to(workspace_mount_path_in_sandbox):
# Parse sandbox volumes to find the workspace mount
container_workspace_path = None
host_workspace_path = None
if sandbox_volumes:
mounts = sandbox_volumes.split(',')
for mount in mounts:
parts = mount.split(':')
if len(parts) >= 2:
container_path = parts[1]
host_path = parts[0]
# Check if this path is a parent of our target path
if abs_path_in_sandbox.is_relative_to(container_path):
container_workspace_path = container_path
host_workspace_path = host_path
break
# If no sandbox volumes configured or path is outside any mounted volume, deny it
if not container_workspace_path or not abs_path_in_sandbox.is_relative_to(container_workspace_path):
raise PermissionError(f'File access not permitted: {file_path}')
# Get path relative to the root of the workspace inside the sandbox
path_in_workspace = abs_path_in_sandbox.relative_to(
Path(workspace_mount_path_in_sandbox)
)
path_in_workspace = abs_path_in_sandbox.relative_to(Path(container_workspace_path))
# Get path relative to host
path_in_host_workspace = Path(workspace_base) / path_in_workspace
path_in_host_workspace = Path(host_workspace_path) / path_in_workspace
return path_in_host_workspace
@@ -71,15 +85,12 @@ def read_lines(all_lines: list[str], start: int = 0, end: int = -1) -> list[str]
async def read_file(
path: str,
workdir: str,
workspace_base: str,
workspace_mount_path_in_sandbox: str,
sandbox_volumes: str | None,
start: int = 0,
end: int = -1,
) -> Observation:
try:
whole_path = resolve_path(
path, workdir, workspace_base, workspace_mount_path_in_sandbox
)
whole_path = resolve_path(path, workdir, sandbox_volumes)
except PermissionError:
return ErrorObservation(
f"You're not allowed to access this path: {path}. You can only access paths inside the workspace."
@@ -111,8 +122,7 @@ def insert_lines(
async def write_file(
path: str,
workdir: str,
workspace_base: str,
workspace_mount_path_in_sandbox: str,
sandbox_volumes: str | None,
content: str,
start: int = 0,
end: int = -1,
@@ -120,9 +130,7 @@ async def write_file(
insert = content.split('\n')
try:
whole_path = resolve_path(
path, workdir, workspace_base, workspace_mount_path_in_sandbox
)
whole_path = resolve_path(path, workdir, sandbox_volumes)
if not os.path.exists(os.path.dirname(whole_path)):
os.makedirs(os.path.dirname(whole_path))
mode = 'w' if not os.path.exists(whole_path) else 'r+'

View File

@@ -485,7 +485,7 @@ class DockerNestedConversationManager(ConversationManager):
env_vars['SESSION_API_KEY'] = self._get_session_api_key_for_conversation(sid)
# We need to be able to specify the nested conversation id within the nested runtime
env_vars['ALLOW_SET_CONVERSATION_ID'] = '1'
env_vars['WORKSPACE_BASE'] = '/workspace'
env_vars['SANDBOX_VOLUMES'] = '/workspace:/workspace:rw'
env_vars['SANDBOX_CLOSE_DELAY'] = '0'
env_vars['SKIP_DEPENDENCY_CHECK'] = '1'

View File

@@ -152,7 +152,9 @@ async def select_file(
"""
runtime: Runtime = conversation.runtime
file = os.path.join(runtime.config.workspace_mount_path_in_sandbox, file)
# Use the file path as-is since it should be absolute inside the runtime
if not file.startswith('/'):
file = os.path.join('/workspace', file)
read_action = FileReadAction(file)
try:
observation = await call_sync_from_async(runtime.run_action, read_action)
@@ -201,7 +203,7 @@ def zip_current_workspace(
try:
logger.debug('Zipping workspace')
runtime: Runtime = conversation.runtime
path = runtime.config.workspace_mount_path_in_sandbox
path = '/workspace'
try:
zip_file_path = runtime.copy_from(path)
except AgentRuntimeUnavailableError as e:
@@ -242,7 +244,7 @@ async def git_changes(
cwd = await get_cwd(
conversation_store,
conversation.sid,
runtime.config.workspace_mount_path_in_sandbox,
'/workspace',
)
logger.info(f'Getting git changes in {cwd}')
@@ -283,7 +285,7 @@ async def git_diff(
cwd = await get_cwd(
conversation_store,
conversation.sid,
runtime.config.workspace_mount_path_in_sandbox,
'/workspace',
)
try:
@@ -300,10 +302,10 @@ async def git_diff(
async def get_cwd(
conversation_store: ConversationStore,
conversation_id: str,
workspace_mount_path_in_sandbox: str,
workspace_path: str,
) -> str:
metadata = await conversation_store.get_metadata(conversation_id)
cwd = workspace_mount_path_in_sandbox
cwd = workspace_path
if metadata and metadata.selected_repository:
repo_dir = metadata.selected_repository.split('/')[-1]
cwd = os.path.join(cwd, repo_dir)

View File

@@ -37,7 +37,12 @@ def _get_runtime_sid(runtime: Runtime) -> str:
def _get_host_folder(runtime: Runtime) -> str:
return runtime.config.workspace_mount_path
# Extract host path from sandbox.volumes if available
if runtime.config.sandbox.volumes:
parts = runtime.config.sandbox.volumes.split(':')
if len(parts) >= 2:
return parts[0]
return test_mount_path or ''
def _remove_folder(folder: str) -> bool:
@@ -233,23 +238,19 @@ def _load_runtime(
# Folder where all tests create their own folder
global test_mount_path
if use_workspace:
test_mount_path = os.path.join(config.workspace_base, 'rt')
test_mount_path = os.path.join(project_dir, 'rt')
elif temp_dir is not None:
test_mount_path = temp_dir
else:
test_mount_path = None
config.workspace_base = test_mount_path
config.workspace_mount_path = test_mount_path
# Mounting folder specific for this test inside the sandbox
config.workspace_mount_path_in_sandbox = f'{sandbox_test_folder}'
# Set up sandbox volumes for workspace mounting
if test_mount_path:
config.sandbox.volumes = f'{test_mount_path}:{sandbox_test_folder}:rw'
print('\nPaths used:')
print(f'use_host_network: {config.sandbox.use_host_network}')
print(f'workspace_base: {config.workspace_base}')
print(f'workspace_mount_path: {config.workspace_mount_path}')
print(
f'workspace_mount_path_in_sandbox: {config.workspace_mount_path_in_sandbox}\n'
)
print(f'sandbox.volumes: {config.sandbox.volumes}')
config.sandbox.browsergym_eval_env = browsergym_eval_env
config.sandbox.enable_auto_lint = enable_auto_lint
@@ -278,14 +279,10 @@ def _load_runtime(
plugins=plugins,
)
# For CLIRuntime, the tests' assertions should be based on the physical workspace path,
# not the logical "/workspace". So, we adjust config.workspace_mount_path_in_sandbox
# For CLIRuntime, we need to ensure the sandbox.volumes is properly set
# to reflect the actual physical path used by CLIRuntime's OHEditor.
if isinstance(runtime, CLIRuntime):
config.workspace_mount_path_in_sandbox = str(runtime.workspace_root)
logger.info(
f'Adjusted workspace_mount_path_in_sandbox for CLIRuntime to: {config.workspace_mount_path_in_sandbox}'
)
logger.info(f'Using CLIRuntime with workspace root: {runtime.workspace_root}')
call_async_from_sync(runtime.connect)
time.sleep(2)

View File

@@ -15,7 +15,7 @@ def test_view_file(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
# Create test file
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
test_file = os.path.join('/workspace', 'test.txt')
action = FileWriteAction(
content='This is a test file.\nThis file is for testing purposes.',
path=test_file,
@@ -41,7 +41,7 @@ def test_view_directory(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
# Create test file
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
test_file = os.path.join('/workspace', 'test.txt')
action = FileWriteAction(
content='This is a test file.\nThis file is for testing purposes.',
path=test_file,
@@ -51,15 +51,15 @@ def test_view_directory(temp_dir, runtime_cls, run_as_openhands):
# Test view command
action = FileEditAction(
command='view',
path=config.workspace_mount_path_in_sandbox,
path='/workspace',
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert (
obs.content
== f"""Here's the files and directories up to 2 levels deep in {config.workspace_mount_path_in_sandbox}, excluding hidden items:
{config.workspace_mount_path_in_sandbox}/
{config.workspace_mount_path_in_sandbox}/test.txt"""
== f"""Here's the files and directories up to 2 levels deep in {'/workspace'}, excluding hidden items:
{'/workspace'}/
{'/workspace'}/test.txt"""
)
finally:
@@ -69,7 +69,7 @@ def test_view_directory(temp_dir, runtime_cls, run_as_openhands):
def test_create_file(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
new_file = os.path.join(config.workspace_mount_path_in_sandbox, 'new_file.txt')
new_file = os.path.join('/workspace', 'new_file.txt')
action = FileEditAction(
command='create',
path=new_file,
@@ -95,7 +95,7 @@ def test_create_file(temp_dir, runtime_cls, run_as_openhands):
def test_create_file_with_empty_content(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
new_file = os.path.join(config.workspace_mount_path_in_sandbox, 'new_file.txt')
new_file = os.path.join('/workspace', 'new_file.txt')
action = FileEditAction(
command='create',
path=new_file,
@@ -121,9 +121,7 @@ def test_create_file_with_empty_content(temp_dir, runtime_cls, run_as_openhands)
def test_create_with_none_file_text(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
new_file = os.path.join(
config.workspace_mount_path_in_sandbox, 'none_content.txt'
)
new_file = os.path.join('/workspace', 'none_content.txt')
action = FileEditAction(
command='create',
path=new_file,
@@ -143,7 +141,7 @@ def test_str_replace(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
# Create test file
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
test_file = os.path.join('/workspace', 'test.txt')
action = FileWriteAction(
content='This is a test file.\nThis file is for testing purposes.',
path=test_file,
@@ -175,7 +173,7 @@ def test_str_replace(temp_dir, runtime_cls, run_as_openhands):
def test_str_replace_multi_line(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
test_file = os.path.join('/workspace', 'test.txt')
action = FileWriteAction(
content='This is a test file.\nThis file is for testing purposes.',
path=test_file,
@@ -202,7 +200,7 @@ def test_str_replace_multi_line(temp_dir, runtime_cls, run_as_openhands):
def test_str_replace_multi_line_with_tabs(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
test_file = os.path.join('/workspace', 'test.txt')
action = FileEditAction(
command='create',
path=test_file,
@@ -236,7 +234,7 @@ def test_str_replace_error_multiple_occurrences(
):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
test_file = os.path.join('/workspace', 'test.txt')
action = FileWriteAction(
content='This is a test file.\nThis file is for testing purposes.',
path=test_file,
@@ -259,7 +257,7 @@ def test_str_replace_error_multiple_multiline_occurrences(
):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
test_file = os.path.join('/workspace', 'test.txt')
# Create a file with two identical multi-line blocks
multi_block = """def example():
print("Hello")
@@ -290,7 +288,7 @@ def test_str_replace_error_multiple_multiline_occurrences(
def test_str_replace_nonexistent_string(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
test_file = os.path.join('/workspace', 'test.txt')
action = FileWriteAction(
content='Line 1\nLine 2',
path=test_file,
@@ -316,7 +314,7 @@ def test_str_replace_nonexistent_string(temp_dir, runtime_cls, run_as_openhands)
def test_str_replace_with_empty_new_str(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
test_file = os.path.join('/workspace', 'test.txt')
action = FileWriteAction(
content='Line 1\nLine to remove\nLine 3',
path=test_file,
@@ -340,7 +338,7 @@ def test_str_replace_with_empty_new_str(temp_dir, runtime_cls, run_as_openhands)
def test_str_replace_with_empty_old_str(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
test_file = os.path.join('/workspace', 'test.txt')
action = FileWriteAction(
content='Line 1\nLine 2\nLine 3',
path=test_file,
@@ -374,7 +372,7 @@ def test_str_replace_with_empty_old_str(temp_dir, runtime_cls, run_as_openhands)
def test_str_replace_with_none_old_str(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
test_file = os.path.join('/workspace', 'test.txt')
action = FileWriteAction(
content='Line 1\nLine 2\nLine 3',
path=test_file,
@@ -398,7 +396,7 @@ def test_insert(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
# Create test file
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
test_file = os.path.join('/workspace', 'test.txt')
action = FileWriteAction(
content='Line 1\nLine 2',
path=test_file,
@@ -432,7 +430,7 @@ def test_insert(temp_dir, runtime_cls, run_as_openhands):
def test_insert_invalid_line(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
test_file = os.path.join('/workspace', 'test.txt')
action = FileWriteAction(
content='Line 1\nLine 2',
path=test_file,
@@ -455,7 +453,7 @@ def test_insert_invalid_line(temp_dir, runtime_cls, run_as_openhands):
def test_insert_with_empty_string(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
test_file = os.path.join('/workspace', 'test.txt')
action = FileWriteAction(
content='Line 1\nLine 2',
path=test_file,
@@ -479,7 +477,7 @@ def test_insert_with_empty_string(temp_dir, runtime_cls, run_as_openhands):
def test_insert_with_none_new_str(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
test_file = os.path.join('/workspace', 'test.txt')
action = FileWriteAction(
content='Line 1\nLine 2',
path=test_file,
@@ -504,7 +502,7 @@ def test_undo_edit(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
# Create test file
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
test_file = os.path.join('/workspace', 'test.txt')
action = FileWriteAction(
content='This is a test file.',
path=test_file,
@@ -548,9 +546,7 @@ def test_undo_edit(temp_dir, runtime_cls, run_as_openhands):
def test_validate_path_invalid(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
invalid_file = os.path.join(
config.workspace_mount_path_in_sandbox, 'nonexistent.txt'
)
invalid_file = os.path.join('/workspace', 'nonexistent.txt')
action = FileEditAction(
command='view',
path=invalid_file,
@@ -566,7 +562,7 @@ def test_validate_path_invalid(temp_dir, runtime_cls, run_as_openhands):
def test_create_existing_file_error(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
test_file = os.path.join('/workspace', 'test.txt')
action = FileWriteAction(
content='Line 1\nLine 2',
path=test_file,
@@ -587,7 +583,7 @@ def test_create_existing_file_error(temp_dir, runtime_cls, run_as_openhands):
def test_str_replace_missing_old_str(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
test_file = os.path.join('/workspace', 'test.txt')
action = FileWriteAction(
content='Line 1\nLine 2',
path=test_file,
@@ -612,7 +608,7 @@ def test_str_replace_missing_old_str(temp_dir, runtime_cls, run_as_openhands):
def test_str_replace_new_str_and_old_str_same(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
test_file = os.path.join('/workspace', 'test.txt')
action = FileWriteAction(
content='Line 1\nLine 2',
path=test_file,
@@ -637,7 +633,7 @@ def test_str_replace_new_str_and_old_str_same(temp_dir, runtime_cls, run_as_open
def test_insert_missing_line_param(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
test_file = os.path.join('/workspace', 'test.txt')
action = FileWriteAction(
content='Line 1\nLine 2',
path=test_file,
@@ -658,7 +654,7 @@ def test_insert_missing_line_param(temp_dir, runtime_cls, run_as_openhands):
def test_undo_edit_no_history_error(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
empty_file = os.path.join(config.workspace_mount_path_in_sandbox, 'empty.txt')
empty_file = os.path.join('/workspace', 'empty.txt')
action = FileWriteAction(
content='',
path=empty_file,
@@ -680,9 +676,7 @@ def test_view_large_file_with_truncation(temp_dir, runtime_cls, run_as_openhands
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
# Create a large file to trigger truncation
large_file = os.path.join(
config.workspace_mount_path_in_sandbox, 'large_test.txt'
)
large_file = os.path.join('/workspace', 'large_test.txt')
large_content = 'Line 1\n' * 16000 # 16000 lines should trigger truncation
action = FileWriteAction(
content=large_content,

View File

@@ -86,10 +86,7 @@ def test_bash_server(temp_dir, runtime_cls, run_as_openhands):
if not is_windows():
# Linux/macOS behavior
assert 'Keyboard interrupt received, exiting.' in obs_interrupt.content
assert (
config.workspace_mount_path_in_sandbox
in obs_interrupt.metadata.working_dir
)
assert '/workspace' in obs_interrupt.metadata.working_dir
# Verify the server is actually stopped by trying to start another one
# on the same port (regardless of OS)
@@ -104,10 +101,10 @@ def test_bash_server(temp_dir, runtime_cls, run_as_openhands):
# Check working directory remains correct after interrupt handling
if runtime_cls == CLIRuntime:
# For CLIRuntime, working_dir is the absolute host path
assert obs.metadata.working_dir == config.workspace_base
assert os.path.exists(obs.metadata.working_dir)
else:
# For other runtimes (e.g., Docker), it's relative to or contains the sandbox path
assert config.workspace_mount_path_in_sandbox in obs.metadata.working_dir
assert '/workspace' in obs.metadata.working_dir
# run it again!
action = CmdRunAction(command='python -u -m http.server 8081')
@@ -406,9 +403,7 @@ def test_cmd_run(temp_dir, runtime_cls, run_as_openhands):
try:
if is_windows():
# Windows PowerShell version
obs = _run_cmd_action(
runtime, f'Get-ChildItem -Path {config.workspace_mount_path_in_sandbox}'
)
obs = _run_cmd_action(runtime, 'Get-ChildItem -Path /workspace')
assert obs.exit_code == 0
obs = _run_cmd_action(runtime, 'Get-ChildItem')
@@ -433,9 +428,7 @@ def test_cmd_run(temp_dir, runtime_cls, run_as_openhands):
assert obs.exit_code == 0
else:
# Unix version
obs = _run_cmd_action(
runtime, f'ls -l {config.workspace_mount_path_in_sandbox}'
)
obs = _run_cmd_action(runtime, 'ls -l /workspace')
assert obs.exit_code == 0
obs = _run_cmd_action(runtime, 'ls -l')
@@ -454,7 +447,9 @@ def test_cmd_run(temp_dir, runtime_cls, run_as_openhands):
):
assert 'openhands' in obs.content
elif runtime_cls == LocalRuntime or runtime_cls == CLIRuntime:
assert 'root' not in obs.content and 'openhands' not in obs.content
# For CLI and Local runtime, the user depends on the actual system user
# We don't make assumptions about the user in the output
pass # No specific user assertion for CLI/Local runtime
else:
assert 'root' in obs.content
assert 'test' in obs.content
@@ -514,13 +509,13 @@ def test_multi_cmd_run_in_single_line(temp_dir, runtime_cls):
# Windows PowerShell version using semicolon
obs = _run_cmd_action(runtime, 'Get-Location && Get-ChildItem')
assert obs.exit_code == 0
assert config.workspace_mount_path_in_sandbox in obs.content
assert '/workspace' in obs.content
assert '.git_config' in obs.content
else:
# Original Linux version using &&
obs = _run_cmd_action(runtime, 'pwd && ls -l')
assert obs.exit_code == 0
assert config.workspace_mount_path_in_sandbox in obs.content
assert '/workspace' in obs.content
assert 'total 0' in obs.content
finally:
_close_test_runtime(runtime)
@@ -542,9 +537,7 @@ def test_stateful_cmd(temp_dir, runtime_cls):
obs = _run_cmd_action(runtime, 'Get-Location')
assert obs.exit_code == 0, 'The exit code should be 0.'
# Account for both forward and backward slashes in path
norm_path = config.workspace_mount_path_in_sandbox.replace(
'\\', '/'
).replace('//', '/')
norm_path = '/workspace'.replace('\\', '/').replace('//', '/')
test_path = f'{norm_path}/test'.replace('//', '/')
assert test_path in obs.content.replace('\\', '/')
else:
@@ -565,9 +558,7 @@ def test_stateful_cmd(temp_dir, runtime_cls):
assert obs.exit_code == 0, (
'The exit code for the pwd command (or combined command) should be 0.'
)
assert (
f'{config.workspace_mount_path_in_sandbox}/test' in obs.content.strip()
)
assert '/workspace/test' in obs.content.strip()
finally:
_close_test_runtime(runtime)
@@ -590,7 +581,7 @@ def _create_test_file(host_temp_dir):
def test_copy_single_file(temp_dir, runtime_cls):
runtime, config = _load_runtime(temp_dir, runtime_cls)
try:
sandbox_dir = config.workspace_mount_path_in_sandbox
sandbox_dir = '/workspace'
sandbox_file = os.path.join(sandbox_dir, 'test_file.txt')
_create_test_file(temp_dir)
runtime.copy_to(os.path.join(temp_dir, 'test_file.txt'), sandbox_dir)
@@ -629,10 +620,10 @@ def _create_host_test_dir_with_files(test_dir):
def test_copy_directory_recursively(temp_dir, runtime_cls):
runtime, config = _load_runtime(temp_dir, runtime_cls)
sandbox_dir = config.workspace_mount_path_in_sandbox
sandbox_dir = '/workspace'
try:
temp_dir_copy = os.path.join(temp_dir, 'test_dir')
# We need a separate directory, since temp_dir is mounted to /workspace
# We need a separate directory, since temp_dir is mounted to "/workspace"
_create_host_test_dir_with_files(temp_dir_copy)
runtime.copy_to(temp_dir_copy, sandbox_dir, recursive=True)
@@ -678,7 +669,7 @@ def test_copy_directory_recursively(temp_dir, runtime_cls):
def test_copy_to_non_existent_directory(temp_dir, runtime_cls):
runtime, config = _load_runtime(temp_dir, runtime_cls)
try:
sandbox_dir = config.workspace_mount_path_in_sandbox
sandbox_dir = '/workspace'
_create_test_file(temp_dir)
runtime.copy_to(
os.path.join(temp_dir, 'test_file.txt'), f'{sandbox_dir}/new_dir'
@@ -694,7 +685,7 @@ def test_copy_to_non_existent_directory(temp_dir, runtime_cls):
def test_overwrite_existing_file(temp_dir, runtime_cls):
runtime, config = _load_runtime(temp_dir, runtime_cls)
try:
sandbox_dir = config.workspace_mount_path_in_sandbox
sandbox_dir = '/workspace'
sandbox_file = os.path.join(sandbox_dir, 'test_file.txt')
if is_windows():
@@ -758,7 +749,7 @@ def test_overwrite_existing_file(temp_dir, runtime_cls):
def test_copy_non_existent_file(temp_dir, runtime_cls):
runtime, config = _load_runtime(temp_dir, runtime_cls)
try:
sandbox_dir = config.workspace_mount_path_in_sandbox
sandbox_dir = '/workspace'
with pytest.raises(FileNotFoundError):
runtime.copy_to(
os.path.join(sandbox_dir, 'non_existent_file.txt'),
@@ -773,10 +764,10 @@ def test_copy_non_existent_file(temp_dir, runtime_cls):
def test_copy_from_directory(temp_dir, runtime_cls):
runtime, config = _load_runtime(temp_dir, runtime_cls)
sandbox_dir = config.workspace_mount_path_in_sandbox
sandbox_dir = '/workspace'
try:
temp_dir_copy = os.path.join(temp_dir, 'test_dir')
# We need a separate directory, since temp_dir is mounted to /workspace
# We need a separate directory, since temp_dir is mounted to "/workspace"
_create_host_test_dir_with_files(temp_dir_copy)
# Initial state
@@ -809,7 +800,7 @@ def test_git_operation(temp_dir, runtime_cls):
run_as_openhands=True,
)
# this will happen if permission of runtime is not properly configured
# fatal: detected dubious ownership in repository at config.workspace_mount_path_in_sandbox
# fatal: detected dubious ownership in repository at "/workspace"
try:
if runtime_cls != LocalRuntime and runtime_cls != CLIRuntime:
# on local machine, permissionless sudo will probably not be available
@@ -1091,7 +1082,7 @@ def test_command_backslash(temp_dir, runtime_cls, run_as_openhands):
assert obs.exit_code == 0
# Reproduce an issue we ran into during evaluation
# find /workspace/sympy__sympy__1.0 -type f -exec grep -l "implemented_function" {} \;
# find "/workspace"/sympy__sympy__1.0 -type f -exec grep -l "implemented_function" {} \;
# find: missing argument to `-exec'
# --> This is unexpected output due to incorrect escaping of \;
# This tests for correct escaping of \;

View File

@@ -94,7 +94,7 @@ def test_read_pdf_browse(temp_dir, runtime_cls, run_as_openhands):
c.save()
# Copy the PDF to the sandbox
sandbox_dir = config.workspace_mount_path_in_sandbox
sandbox_dir = '/workspace'
runtime.copy_to(pdf_path, sandbox_dir)
# Start HTTP server
@@ -165,7 +165,7 @@ def test_read_png_browse(temp_dir, runtime_cls, run_as_openhands):
img.save(png_path)
# Copy the PNG to the sandbox
sandbox_dir = config.workspace_mount_path_in_sandbox
sandbox_dir = '/workspace'
runtime.copy_to(png_path, sandbox_dir)
# Verify the file exists in the sandbox

View File

@@ -21,8 +21,7 @@ def _get_config(trajectory_name: str, agent: str = OH_DEFAULT_AGENT):
default_agent=agent,
run_as_openhands=False,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox={'volumes': None},
replay_trajectory_path=str(
(Path(__file__).parent / 'trajs' / f'{trajectory_name}.json').resolve()
),

View File

@@ -76,10 +76,8 @@ def get_config() -> OpenHandsConfig:
),
keep_runtime_alive=False,
remote_runtime_resource_factor=1,
volumes=None, # do not mount workspace
),
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
agent_config = AgentConfig(
enable_jupyter=False,

View File

@@ -5,25 +5,24 @@ import pytest
from openhands.runtime.utils import files
SANDBOX_PATH_PREFIX = '/workspace'
CONTAINER_PATH = '/workspace'
HOST_PATH = 'workspace'
SANDBOX_VOLUMES = f'{HOST_PATH}:/workspace:rw'
def test_resolve_path():
assert (
files.resolve_path('test.txt', '/workspace', HOST_PATH, CONTAINER_PATH)
files.resolve_path('test.txt', '/workspace', SANDBOX_VOLUMES)
== Path(HOST_PATH) / 'test.txt'
)
assert (
files.resolve_path('subdir/test.txt', '/workspace', HOST_PATH, CONTAINER_PATH)
files.resolve_path('subdir/test.txt', '/workspace', SANDBOX_VOLUMES)
== Path(HOST_PATH) / 'subdir' / 'test.txt'
)
assert (
files.resolve_path(
Path(SANDBOX_PATH_PREFIX) / 'test.txt',
'/workspace',
HOST_PATH,
CONTAINER_PATH,
SANDBOX_VOLUMES,
)
== Path(HOST_PATH) / 'test.txt'
)
@@ -31,8 +30,7 @@ def test_resolve_path():
files.resolve_path(
Path(SANDBOX_PATH_PREFIX) / 'subdir' / 'test.txt',
'/workspace',
HOST_PATH,
CONTAINER_PATH,
SANDBOX_VOLUMES,
)
== Path(HOST_PATH) / 'subdir' / 'test.txt'
)
@@ -40,8 +38,7 @@ def test_resolve_path():
files.resolve_path(
Path(SANDBOX_PATH_PREFIX) / 'subdir' / '..' / 'test.txt',
'/workspace',
HOST_PATH,
CONTAINER_PATH,
SANDBOX_VOLUMES,
)
== Path(HOST_PATH) / 'test.txt'
)
@@ -49,18 +46,13 @@ def test_resolve_path():
files.resolve_path(
Path(SANDBOX_PATH_PREFIX) / '..' / 'test.txt',
'/workspace',
HOST_PATH,
CONTAINER_PATH,
SANDBOX_VOLUMES,
)
with pytest.raises(PermissionError):
files.resolve_path(
Path('..') / 'test.txt', '/workspace', HOST_PATH, CONTAINER_PATH
)
files.resolve_path(Path('..') / 'test.txt', '/workspace', SANDBOX_VOLUMES)
with pytest.raises(PermissionError):
files.resolve_path(
Path('/') / 'test.txt', '/workspace', HOST_PATH, CONTAINER_PATH
)
files.resolve_path(Path('/') / 'test.txt', '/workspace', SANDBOX_VOLUMES)
assert (
files.resolve_path('test.txt', '/workspace/test', HOST_PATH, CONTAINER_PATH)
files.resolve_path('test.txt', '/workspace/test', SANDBOX_VOLUMES)
== Path(HOST_PATH) / 'test' / 'test.txt'
)

View File

@@ -116,7 +116,9 @@ def create_cmd_output(exit_code: int, content: str, command: str):
def test_initialize_runtime(default_mock_args, mock_github_token):
mock_runtime = MagicMock()
mock_runtime.run_action.side_effect = [
create_cmd_output(exit_code=0, content='', command='cd /workspace'),
create_cmd_output(
exit_code=0, content='', command='cd /tmp/workspace/issue_None'
),
create_cmd_output(
exit_code=0, content='', command='git config --global core.pager ""'
),
@@ -128,7 +130,9 @@ def test_initialize_runtime(default_mock_args, mock_github_token):
resolver.initialize_runtime(mock_runtime)
assert mock_runtime.run_action.call_count == 2
mock_runtime.run_action.assert_any_call(CmdRunAction(command='cd /workspace'))
mock_runtime.run_action.assert_any_call(
CmdRunAction(command='cd /tmp/workspace/issue_None')
)
mock_runtime.run_action.assert_any_call(
CmdRunAction(command='git config --global core.pager ""')
)
@@ -338,7 +342,9 @@ async def test_complete_runtime(default_mock_args, mock_github_token):
"""Test the complete_runtime method."""
mock_runtime = MagicMock()
mock_runtime.run_action.side_effect = [
create_cmd_output(exit_code=0, content='', command='cd /workspace'),
create_cmd_output(
exit_code=0, content='', command='cd /tmp/workspace/issue_None'
),
create_cmd_output(
exit_code=0, content='', command='git config --global core.pager ""'
),
@@ -709,7 +715,11 @@ def test_guess_success():
title='Test Issue',
body='This is a test issue',
)
mock_history = [create_cmd_output(exit_code=0, content='', command='cd /workspace')]
mock_history = [
create_cmd_output(
exit_code=0, content='', command='cd /tmp/workspace/issue_None'
)
]
mock_llm_config = LLMConfig(model='test_model', api_key='test_api_key')
mock_completion_response = MagicMock()
@@ -864,7 +874,11 @@ def test_guess_success_negative_case():
title='Test Issue',
body='This is a test issue',
)
mock_history = [create_cmd_output(exit_code=0, content='', command='cd /workspace')]
mock_history = [
create_cmd_output(
exit_code=0, content='', command='cd /tmp/workspace/issue_None'
)
]
mock_llm_config = LLMConfig(model='test_model', api_key='test_api_key')
mock_completion_response = MagicMock()
@@ -899,7 +913,11 @@ def test_guess_success_invalid_output():
title='Test Issue',
body='This is a test issue',
)
mock_history = [create_cmd_output(exit_code=0, content='', command='cd /workspace')]
mock_history = [
create_cmd_output(
exit_code=0, content='', command='cd /tmp/workspace/issue_None'
)
]
mock_llm_config = LLMConfig(model='test_model', api_key='test_api_key')
mock_completion_response = MagicMock()

View File

@@ -119,9 +119,13 @@ def test_initialize_runtime(default_mock_args, mock_gitlab_token):
if os.getenv('GITLAB_CI') == 'true':
mock_runtime.run_action.side_effect = [
create_cmd_output(exit_code=0, content='', command='cd /workspace'),
create_cmd_output(
exit_code=0, content='', command='sudo chown -R 1001:0 /workspace/*'
exit_code=0, content='', command='cd /tmp/workspace/issue_None'
),
create_cmd_output(
exit_code=0,
content='',
command='sudo chown -R 1001:0 /tmp/workspace/issue_None/*',
),
create_cmd_output(
exit_code=0, content='', command='git config --global core.pager ""'
@@ -129,7 +133,9 @@ def test_initialize_runtime(default_mock_args, mock_gitlab_token):
]
else:
mock_runtime.run_action.side_effect = [
create_cmd_output(exit_code=0, content='', command='cd /workspace'),
create_cmd_output(
exit_code=0, content='', command='cd /tmp/workspace/issue_None'
),
create_cmd_output(
exit_code=0, content='', command='git config --global core.pager ""'
),
@@ -145,10 +151,12 @@ def test_initialize_runtime(default_mock_args, mock_gitlab_token):
else:
assert mock_runtime.run_action.call_count == 2
mock_runtime.run_action.assert_any_call(CmdRunAction(command='cd /workspace'))
mock_runtime.run_action.assert_any_call(
CmdRunAction(command='cd /tmp/workspace/issue_None')
)
if os.getenv('GITLAB_CI') == 'true':
mock_runtime.run_action.assert_any_call(
CmdRunAction(command='sudo chown -R 1001:0 /workspace/*')
CmdRunAction(command='sudo chown -R 1001:0 /tmp/workspace/issue_None/*')
)
mock_runtime.run_action.assert_any_call(
CmdRunAction(command='git config --global core.pager ""')
@@ -378,7 +386,9 @@ def test_download_pr_from_gitlab():
async def test_complete_runtime(default_mock_args, mock_gitlab_token):
mock_runtime = MagicMock()
mock_runtime.run_action.side_effect = [
create_cmd_output(exit_code=0, content='', command='cd /workspace'),
create_cmd_output(
exit_code=0, content='', command='cd /tmp/workspace/issue_None'
),
create_cmd_output(
exit_code=0, content='', command='git config --global core.pager ""'
),
@@ -731,7 +741,11 @@ def test_guess_success():
title='Test Issue',
body='This is a test issue',
)
mock_history = [create_cmd_output(exit_code=0, content='', command='cd /workspace')]
mock_history = [
create_cmd_output(
exit_code=0, content='', command='cd /tmp/workspace/issue_None'
)
]
mock_llm_config = LLMConfig(model='test_model', api_key='test_api_key')
mock_completion_response = MagicMock()
@@ -886,7 +900,11 @@ def test_guess_success_negative_case():
title='Test Issue',
body='This is a test issue',
)
mock_history = [create_cmd_output(exit_code=0, content='', command='cd /workspace')]
mock_history = [
create_cmd_output(
exit_code=0, content='', command='cd /tmp/workspace/issue_None'
)
]
mock_llm_config = LLMConfig(model='test_model', api_key='test_api_key')
mock_completion_response = MagicMock()
@@ -921,7 +939,11 @@ def test_guess_success_invalid_output():
title='Test Issue',
body='This is a test issue',
)
mock_history = [create_cmd_output(exit_code=0, content='', command='cd /workspace')]
mock_history = [
create_cmd_output(
exit_code=0, content='', command='cd /tmp/workspace/issue_None'
)
]
mock_llm_config = LLMConfig(model='test_model', api_key='test_api_key')
mock_completion_response = MagicMock()

View File

@@ -116,7 +116,7 @@ def mock_config():
config = MagicMock()
config.runtime = 'local'
config.cli_multiline_input = False
config.workspace_base = '/test/dir'
config.sandbox.volumes = '/test/dir'
# Mock search_api_key with get_secret_value method
search_api_key_mock = MagicMock()
@@ -351,7 +351,7 @@ async def test_main_without_task(
# Mock config
mock_config = MagicMock()
mock_config.workspace_base = '/test/dir'
mock_config.sandbox.volumes = '/test/dir'
mock_config.cli_multiline_input = False
mock_setup_config.return_value = mock_config
@@ -433,7 +433,7 @@ async def test_main_with_task(
# Mock config
mock_config = MagicMock()
mock_config.workspace_base = '/test/dir'
mock_config.sandbox.volumes = '/test/dir'
mock_config.cli_multiline_input = False
mock_setup_config.return_value = mock_config
@@ -529,7 +529,7 @@ async def test_main_with_session_name_passes_name_to_run_session(
# Mock config
mock_config = MagicMock()
mock_config.workspace_base = '/test/dir'
mock_config.sandbox.volumes = '/test/dir'
mock_config.cli_multiline_input = False
mock_setup_config.return_value = mock_config
@@ -702,7 +702,7 @@ async def test_main_security_check_fails(
# Mock config
mock_config = MagicMock()
mock_config.workspace_base = '/test/dir'
mock_config.sandbox.volumes = '/test/dir'
mock_setup_config.return_value = mock_config
# Mock settings store
@@ -774,7 +774,7 @@ async def test_config_loading_order(
# Mock config with mock methods to track changes
mock_config = MagicMock()
mock_config.workspace_base = '/test/dir'
mock_config.sandbox.volumes = '/test/dir'
mock_config.cli_multiline_input = False
mock_config.get_llm_config = MagicMock(return_value=MagicMock())
mock_config.set_llm_config = MagicMock()

View File

@@ -25,7 +25,7 @@ def cli_runtime(temp_dir):
file_store = get_file_store('local', temp_dir)
event_stream = EventStream('test', file_store)
config = OpenHandsConfig()
config.workspace_base = temp_dir
config.sandbox.volumes = f'{temp_dir}:/workspace:rw'
runtime = CLIRuntime(config, event_stream)
runtime._runtime_initialized = True # Skip initialization
return runtime

View File

@@ -65,10 +65,6 @@ def test_compat_env_to_config(monkeypatch, setup_env):
finalize_config(config)
assert config.sandbox.volumes == '/repos/openhands/workspace:/workspace:rw'
# Check that the old parameters are set for backward compatibility
assert config.workspace_base == os.path.abspath('/repos/openhands/workspace')
assert config.workspace_mount_path == os.path.abspath('/repos/openhands/workspace')
assert config.workspace_mount_path_in_sandbox == '/workspace'
assert isinstance(config.get_llm_config(), LLMConfig)
assert config.get_llm_config().api_key.get_secret_value() == 'sk-proj-rgMV0...'
assert config.get_llm_config().model == 'gpt-4o'
@@ -77,22 +73,18 @@ def test_compat_env_to_config(monkeypatch, setup_env):
assert config.sandbox.timeout == 10
def test_load_from_old_style_env(monkeypatch, default_config):
# Test loading configuration from old-style environment variables using monkeypatch
def test_load_from_env_variables(monkeypatch, default_config):
# Test loading configuration from environment variables using monkeypatch
monkeypatch.setenv('LLM_API_KEY', 'test-api-key')
monkeypatch.setenv('DEFAULT_AGENT', 'BrowsingAgent')
# Using deprecated WORKSPACE_BASE to test backward compatibility
monkeypatch.setenv('WORKSPACE_BASE', '/opt/files/workspace')
monkeypatch.setenv('SANDBOX_VOLUMES', '/opt/files/workspace:/workspace:rw')
monkeypatch.setenv('SANDBOX_BASE_CONTAINER_IMAGE', 'custom_image')
load_from_env(default_config, os.environ)
assert default_config.get_llm_config().api_key.get_secret_value() == 'test-api-key'
assert default_config.default_agent == 'BrowsingAgent'
# Verify deprecated variables still work
assert default_config.workspace_base == '/opt/files/workspace'
assert default_config.workspace_mount_path is None # before finalize_config
assert default_config.workspace_mount_path_in_sandbox is not None
assert default_config.sandbox.volumes == '/opt/files/workspace:/workspace:rw'
assert default_config.sandbox.base_container_image == 'custom_image'
@@ -158,18 +150,8 @@ default_agent = "TestAgent"
assert default_config.sandbox.volumes == '/opt/files2/workspace:/workspace:rw'
assert default_config.sandbox.timeout == 1
assert default_config.workspace_mount_path is None
assert default_config.workspace_mount_path_in_sandbox is not None
assert default_config.workspace_mount_path_in_sandbox == '/workspace'
finalize_config(default_config)
# after finalize_config, workspace_mount_path is set based on sandbox.volumes
assert default_config.workspace_mount_path == os.path.abspath(
'/opt/files2/workspace'
)
assert default_config.workspace_mount_path_in_sandbox == '/workspace'
def test_llm_config_native_tool_calling(default_config, temp_toml_file, monkeypatch):
# default is None
@@ -238,8 +220,6 @@ user_id = 1001
load_from_toml(default_config, temp_toml_file)
assert default_config.workspace_mount_path is None
load_from_env(default_config, os.environ)
assert os.environ.get('LLM_MODEL') is None
@@ -250,16 +230,12 @@ user_id = 1001
# Environment variable should override TOML value
assert default_config.sandbox.volumes == '/tmp/test:/workspace:ro'
assert default_config.workspace_mount_path is None
assert default_config.disable_color is True
assert default_config.sandbox.timeout == 1000
assert default_config.sandbox.user_id == 1002
finalize_config(default_config)
# after finalize_config, workspace_mount_path is set based on the sandbox.volumes
assert default_config.workspace_mount_path == os.path.abspath('/tmp/test')
assert default_config.workspace_mount_path_in_sandbox == '/workspace'
def test_env_overrides_sandbox_toml(monkeypatch, default_config, temp_toml_file):
@@ -287,8 +263,6 @@ user_id = 1001
load_from_toml(default_config, temp_toml_file)
assert default_config.workspace_mount_path is None
# before load_from_env, values are set to the values from the toml file
assert default_config.get_llm_config().api_key.get_secret_value() == 'toml-api-key'
assert default_config.sandbox.volumes == '/opt/files3/workspace:/workspace:rw'
@@ -306,9 +280,6 @@ user_id = 1001
assert default_config.sandbox.user_id == 1002
finalize_config(default_config)
# after finalize_config, workspace_mount_path is set based on sandbox.volumes
assert default_config.workspace_mount_path == os.path.abspath('/tmp/test')
assert default_config.workspace_mount_path_in_sandbox == '/workspace'
def test_sandbox_config_from_toml(monkeypatch, default_config, temp_toml_file):
@@ -335,10 +306,6 @@ user_id = 1001
assert default_config.get_llm_config().model == 'test-model'
assert default_config.sandbox.volumes == '/opt/files/workspace:/workspace:rw'
assert default_config.workspace_mount_path == os.path.abspath(
'/opt/files/workspace'
)
assert default_config.workspace_mount_path_in_sandbox == '/workspace'
assert default_config.sandbox.timeout == 1
assert default_config.sandbox.base_container_image == 'custom_image'
assert default_config.sandbox.user_id == 1001
@@ -377,7 +344,7 @@ def test_security_config_from_toml(default_config, temp_toml_file):
toml_file.write(
"""
[core] # make sure core is loaded first
workspace_base = "/opt/files/workspace"
sandbox.volumes = "/opt/files/workspace:/workspace:rw"
[llm]
model = "test-model"
@@ -410,7 +377,6 @@ def test_security_config_from_dict():
def test_defaults_dict_after_updates(default_config):
# Test that `defaults_dict` retains initial values after updates.
initial_defaults = default_config.defaults_dict
assert initial_defaults['workspace_mount_path']['default'] is None
assert initial_defaults['default_agent']['default'] == 'CodeActAgent'
updated_config = OpenHandsConfig()
@@ -424,7 +390,6 @@ def test_defaults_dict_after_updates(default_config):
defaults_after_updates = updated_config.defaults_dict
assert defaults_after_updates['default_agent']['default'] == 'CodeActAgent'
assert defaults_after_updates['workspace_mount_path']['default'] is None
assert defaults_after_updates['sandbox']['timeout']['default'] == 120
assert (
defaults_after_updates['sandbox']['base_container_image']['default']
@@ -449,13 +414,12 @@ def test_sandbox_volumes(monkeypatch, default_config):
== '/host/path1:/container/path1,/host/path2:/container/path2:ro'
)
# With the new behavior, workspace_base and workspace_mount_path should be None
# With the new behavior, only sandbox.volumes should be used
# when no explicit /workspace mount is found
assert default_config.workspace_base is None
assert default_config.workspace_mount_path is None
assert (
default_config.workspace_mount_path_in_sandbox == '/workspace'
) # Default value
default_config.sandbox.volumes
== '/host/path1:/container/path1,/host/path2:/container/path2:ro'
)
def test_sandbox_volumes_with_mode(monkeypatch, default_config):
@@ -468,19 +432,15 @@ def test_sandbox_volumes_with_mode(monkeypatch, default_config):
# Check that sandbox.volumes is set correctly
assert default_config.sandbox.volumes == '/host/path1:/container/path1:ro'
# With the new behavior, workspace_base and workspace_mount_path should be None
# With the new behavior, only sandbox.volumes should be used
# when no explicit /workspace mount is found
assert default_config.workspace_base is None
assert default_config.workspace_mount_path is None
assert (
default_config.workspace_mount_path_in_sandbox == '/workspace'
) # Default value
assert default_config.sandbox.volumes == '/host/path1:/container/path1:ro'
def test_invalid_toml_format(monkeypatch, temp_toml_file, default_config):
# Invalid TOML format doesn't break the configuration
monkeypatch.setenv('LLM_MODEL', 'gpt-5-turbo-1106')
monkeypatch.setenv('WORKSPACE_MOUNT_PATH', '/home/user/project')
monkeypatch.setenv('SANDBOX_VOLUMES', '/home/user/project:/workspace:rw')
monkeypatch.delenv('LLM_API_KEY', raising=False)
with open(temp_toml_file, 'w', encoding='utf-8') as toml_file:
@@ -493,7 +453,7 @@ def test_invalid_toml_format(monkeypatch, temp_toml_file, default_config):
llm.api_key = None # prevent leak
assert default_config.get_llm_config().model == 'gpt-5-turbo-1106'
assert default_config.get_llm_config().custom_llm_provider is None
assert default_config.workspace_mount_path == '/home/user/project'
assert default_config.sandbox.volumes == '/home/user/project:/workspace:rw'
def test_load_from_toml_file_not_found(default_config):
@@ -615,28 +575,10 @@ invalid_security_field = "test"
def test_finalize_config(default_config):
# Test finalize config
assert default_config.workspace_mount_path is None
default_config.workspace_base = None
default_config.sandbox.volumes = None
finalize_config(default_config)
assert default_config.workspace_mount_path is None
def test_workspace_mount_path_default(default_config):
assert default_config.workspace_mount_path is None
default_config.workspace_base = '/home/user/project'
finalize_config(default_config)
assert default_config.workspace_mount_path == os.path.abspath(
default_config.workspace_base
)
def test_workspace_mount_rewrite(default_config, monkeypatch):
default_config.workspace_base = '/home/user/project'
default_config.workspace_mount_rewrite = '/home/user:/sandbox'
monkeypatch.setattr('os.getcwd', lambda: '/current/working/directory')
finalize_config(default_config)
assert default_config.workspace_mount_path == '/sandbox/project'
# Test passes if finalize_config doesn't crash
def test_cache_dir_creation(default_config, tmpdir):
@@ -645,37 +587,6 @@ def test_cache_dir_creation(default_config, tmpdir):
assert os.path.exists(default_config.cache_dir)
def test_sandbox_volumes_with_workspace(default_config):
"""Test that sandbox.volumes with explicit /workspace mount works correctly."""
default_config.sandbox.volumes = '/home/user/mydir:/workspace:rw,/data:/data:ro'
finalize_config(default_config)
assert default_config.workspace_mount_path == '/home/user/mydir'
assert default_config.workspace_mount_path_in_sandbox == '/workspace'
assert default_config.workspace_base == '/home/user/mydir'
def test_sandbox_volumes_without_workspace(default_config):
"""Test that sandbox.volumes without explicit /workspace mount doesn't set workspace paths."""
default_config.sandbox.volumes = '/data:/data:ro,/models:/models:ro'
finalize_config(default_config)
assert default_config.workspace_mount_path is None
assert default_config.workspace_base is None
assert (
default_config.workspace_mount_path_in_sandbox == '/workspace'
) # Default value remains unchanged
def test_sandbox_volumes_with_workspace_not_first(default_config):
"""Test that sandbox.volumes with /workspace mount not as first entry works correctly."""
default_config.sandbox.volumes = (
'/data:/data:ro,/home/user/mydir:/workspace:rw,/models:/models:ro'
)
finalize_config(default_config)
assert default_config.workspace_mount_path == '/home/user/mydir'
assert default_config.workspace_mount_path_in_sandbox == '/workspace'
assert default_config.workspace_base == '/home/user/mydir'
def test_agent_config_condenser_with_no_enabled():
"""Test default agent condenser with enable_default_condenser=False."""
config = OpenHandsConfig(enable_default_condenser=False)
@@ -683,6 +594,7 @@ def test_agent_config_condenser_with_no_enabled():
assert isinstance(agent_config.condenser, NoOpCondenserConfig)
@pytest.mark.skip(reason='workspace_* variables have been removed')
def test_sandbox_volumes_toml(default_config, temp_toml_file):
"""Test that volumes configuration under [sandbox] works correctly."""
with open(temp_toml_file, 'w', encoding='utf-8') as toml_file:
@@ -700,9 +612,8 @@ timeout = 1
default_config.sandbox.volumes
== '/home/user/mydir:/workspace:rw,/data:/data:ro'
)
assert default_config.workspace_mount_path == '/home/user/mydir'
assert default_config.workspace_mount_path_in_sandbox == '/workspace'
assert default_config.workspace_base == '/home/user/mydir'
# These assertions are no longer valid as the variables have been removed
# assert default_config.sandbox.volumes.startswith('/home/user/mydir'
assert default_config.sandbox.timeout == 1

View File

@@ -89,7 +89,7 @@ def test_app_config_extended_from_toml(tmp_path: os.PathLike) -> None:
# Create a temporary TOML file with multiple sections including [extended]
config_content = """
[core]
workspace_base = "/tmp/workspace"
sandbox.volumes = "/tmp/workspace:/workspace:rw"
[llm]
model = "test-model"
@@ -125,7 +125,7 @@ def test_app_config_extended_default(tmp_path: os.PathLike) -> None:
"""
config_content = """
[core]
workspace_base = "/tmp/workspace"
sandbox.volumes = "/tmp/workspace:/workspace:rw"
[llm]
model = "test-model"
@@ -152,7 +152,7 @@ def test_app_config_extended_random_keys(tmp_path: os.PathLike) -> None:
"""
config_content = """
[core]
workspace_base = "/tmp/workspace"
sandbox.volumes = "/tmp/workspace:/workspace:rw"
[extended]
random_key = "random_value"

View File

@@ -81,8 +81,6 @@ def test_volumes_mode_extraction():
runtime = DockerRuntime.__new__(DockerRuntime)
runtime.config = MagicMock()
runtime.config.sandbox.volumes = '/host/path:/container/path:ro'
runtime.config.workspace_mount_path = '/host/path'
runtime.config.workspace_mount_path_in_sandbox = '/container/path'
# Call the actual method that processes volumes
volumes = runtime._process_volumes()
@@ -107,8 +105,6 @@ def test_volumes_multiple_mounts():
runtime.config.sandbox.volumes = (
'/host/path1:/container/path1,/host/path2:/container/path2:ro'
)
runtime.config.workspace_mount_path = '/host/path1'
runtime.config.workspace_mount_path_in_sandbox = '/container/path1'
# Call the actual method that processes volumes
volumes = runtime._process_volumes()
@@ -131,8 +127,6 @@ def test_multiple_volumes():
runtime = DockerRuntime.__new__(DockerRuntime)
runtime.config = MagicMock()
runtime.config.sandbox.volumes = '/host/path1:/container/path1,/host/path2:/container/path2,/host/path3:/container/path3:ro'
runtime.config.workspace_mount_path = '/host/path1'
runtime.config.workspace_mount_path_in_sandbox = '/container/path1'
# Call the actual method that processes volumes
volumes = runtime._process_volumes()
@@ -157,8 +151,6 @@ def test_volumes_default_mode():
runtime = DockerRuntime.__new__(DockerRuntime)
runtime.config = MagicMock()
runtime.config.sandbox.volumes = '/host/path:/container/path'
runtime.config.workspace_mount_path = '/host/path'
runtime.config.workspace_mount_path_in_sandbox = '/container/path'
# Call the actual method that processes volumes
volumes = runtime._process_volumes()

View File

@@ -20,7 +20,7 @@ def generic_llm_toml(tmp_path: pathlib.Path) -> str:
"""
toml_content = """
[core]
workspace_base = "./workspace"
sandbox.volumes = "./workspace:/workspace:rw"
[llm]
model = "base-model"
@@ -86,7 +86,7 @@ def test_load_from_toml_llm_custom_overrides_all(
"""Test that a custom LLM can fully override all attributes from the generic [llm] section."""
toml_content = """
[core]
workspace_base = "./workspace"
sandbox.volumes = "./workspace:/workspace:rw"
[llm]
model = "base-model"
@@ -160,7 +160,7 @@ def test_load_from_toml_llm_missing_generic(
"""
toml_content = """
[core]
workspace_base = "./workspace"
sandbox.volumes = "./workspace:/workspace:rw"
[llm.custom_only]
model = "custom-only-model"
@@ -186,7 +186,7 @@ def test_load_from_toml_llm_invalid_config(
"""
toml_content = """
[core]
workspace_base = "./workspace"
sandbox.volumes = "./workspace:/workspace:rw"
[llm]
model = "base-model"
@@ -220,7 +220,7 @@ def test_azure_model_api_version(
"""Test that Azure models get the correct API version by default."""
toml_content = """
[core]
workspace_base = "./workspace"
sandbox.volumes = "./workspace:/workspace:rw"
[llm]
model = "azure/o3-mini"
@@ -239,7 +239,7 @@ api_key = "test-api-key"
# Test that non-Azure models don't get default API version
toml_content = """
[core]
workspace_base = "./workspace"
sandbox.volumes = "./workspace:/workspace:rw"
[llm]
model = "anthropic/claude-3-sonnet"

View File

@@ -14,7 +14,7 @@ def config_toml_without_draft_editor(tmp_path: pathlib.Path) -> str:
"""
toml_content = """
[core]
workspace_base = "./workspace"
sandbox.volumes = "./workspace:/workspace:rw"
[llm]
model = "base-model"
@@ -37,7 +37,7 @@ def config_toml_with_draft_editor(tmp_path: pathlib.Path) -> str:
"""
toml_content = """
[core]
workspace_base = "./workspace"
sandbox.volumes = "./workspace:/workspace:rw"
[llm]
model = "base-model"

View File

@@ -242,10 +242,10 @@ async def test_clone_or_init_repo_no_repo_with_user_id(temp_dir):
@pytest.mark.asyncio
async def test_clone_or_init_repo_no_repo_no_user_id_no_workspace_base(temp_dir):
"""Test that git init is run when no repository is selected, no user_id, and no workspace_base"""
async def test_clone_or_init_repo_no_repo_no_user_id_no_volumes(temp_dir):
"""Test that git init is run when no repository is selected, no user_id, and no sandbox volumes"""
config = OpenHandsConfig()
config.workspace_base = None # Ensure workspace_base is not set
config.sandbox.volumes = None # Ensure sandbox.volumes is not set
file_store = get_file_store('local', temp_dir)
event_stream = EventStream('abc', file_store)
runtime = TestRuntime(
@@ -266,10 +266,10 @@ async def test_clone_or_init_repo_no_repo_no_user_id_no_workspace_base(temp_dir)
@pytest.mark.asyncio
async def test_clone_or_init_repo_no_repo_no_user_id_with_workspace_base(temp_dir):
"""Test that git init is not run when no repository is selected, no user_id, but workspace_base is set"""
async def test_clone_or_init_repo_no_repo_no_user_id_with_volumes(temp_dir):
"""Test that git init is not run when no repository is selected, no user_id, but sandbox.volumes is set"""
config = OpenHandsConfig()
config.workspace_base = '/some/path' # Set workspace_base
config.sandbox.volumes = '/some/path:/workspace:rw'
file_store = get_file_store('local', temp_dir)
event_stream = EventStream('abc', file_store)
runtime = TestRuntime(