mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
3 Commits
fix_webare
...
fix-cli-co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5d86e8132 | ||
|
|
d615fe26c0 | ||
|
|
01f28f6269 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -257,5 +257,3 @@ containers/runtime/code
|
||||
|
||||
# test results
|
||||
test-results
|
||||
.sessions
|
||||
.eval_sessions
|
||||
|
||||
@@ -363,11 +363,10 @@ classpath = "my_package.my_module.MyCustomAgent"
|
||||
#confirmation_mode = false
|
||||
|
||||
# The security analyzer to use (For Headless / CLI only - In Web this is overridden by Session Init)
|
||||
# Available options: 'llm' (default), 'invariant'
|
||||
#security_analyzer = "llm"
|
||||
#security_analyzer = ""
|
||||
|
||||
# Whether to enable security analyzer
|
||||
#enable_security_analyzer = true
|
||||
#enable_security_analyzer = false
|
||||
|
||||
#################################### Condenser #################################
|
||||
# Condensers control how conversation history is managed and compressed when
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, '/workspace/project/OpenHands')
|
||||
|
||||
from evaluation.benchmarks.webarena.run_infer import initialize_runtime, get_config
|
||||
from evaluation.utils.shared import EvalMetadata, make_metadata
|
||||
from openhands.core.config import load_from_toml
|
||||
from openhands.runtime.impl.docker.docker_runtime import DockerRuntime
|
||||
from openhands.utils.async_utils import call_async_from_sync
|
||||
import pandas as pd
|
||||
|
||||
def debug_webarena_goal():
|
||||
"""Debug what the WebArena goal looks like"""
|
||||
|
||||
# Create a minimal instance for testing
|
||||
instance = pd.Series({
|
||||
'instance_id': 'browsergym/webarena.247',
|
||||
'instruction': 'Test instruction'
|
||||
})
|
||||
|
||||
# Load LLM config
|
||||
config_dict = load_from_toml('config.toml')
|
||||
llm_config = config_dict['llm']['claude-sonnet-4']
|
||||
|
||||
# Create metadata
|
||||
metadata = make_metadata(
|
||||
llm_config=llm_config,
|
||||
dataset_name='webarena',
|
||||
agent_class='CodeActAgent',
|
||||
max_iterations=15,
|
||||
eval_note=None,
|
||||
eval_output_dir='evaluation/evaluation_outputs/outputs/webarena/CodeActAgent/debug',
|
||||
details=None,
|
||||
)
|
||||
|
||||
config = get_config(metadata, instance.instance_id)
|
||||
|
||||
# Create runtime
|
||||
runtime = DockerRuntime(config.sandbox_config)
|
||||
call_async_from_sync(runtime.connect)
|
||||
|
||||
print("=== DEBUGGING WEBARENA GOAL ===")
|
||||
|
||||
# Get the goal
|
||||
try:
|
||||
task_str = initialize_runtime(runtime)
|
||||
print(f"Goal text type: {type(task_str)}")
|
||||
print(f"Goal text length: {len(str(task_str))}")
|
||||
print(f"Goal text content: {repr(task_str)}")
|
||||
|
||||
if task_str:
|
||||
print("✅ Goal text is not empty")
|
||||
else:
|
||||
print("❌ Goal text is empty!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error getting goal: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
finally:
|
||||
# Cleanup
|
||||
try:
|
||||
call_async_from_sync(runtime.close)
|
||||
except:
|
||||
pass
|
||||
|
||||
if __name__ == "__main__":
|
||||
debug_webarena_goal()
|
||||
@@ -1,52 +0,0 @@
|
||||
# Confirmation Mode and Security Analyzers
|
||||
|
||||
OpenHands provides a security framework to help protect users from potentially risky actions through **Confirmation Mode** and **Security Analyzers**. This system analyzes agent actions and prompts users for confirmation when high-risk operations are detected.
|
||||
|
||||
## Overview
|
||||
|
||||
The security system consists of two main components:
|
||||
|
||||
1. **Confirmation Mode**: When enabled, the agent will pause and ask for user confirmation before executing actions that are flagged as high-risk by the security analyzer.
|
||||
|
||||
2. **Security Analyzers**: These are modules that evaluate the risk level of agent actions and determine whether user confirmation is required.
|
||||
|
||||
## Configuration
|
||||
|
||||
### CLI
|
||||
In CLI mode, confirmation is enabled by default. You will have an option to uses the LLM Analyzer and will automatically confirm LOW and MEDIUM risk actions, only prompting for HIGH risk actions.
|
||||
|
||||
## Security Analyzers
|
||||
|
||||
OpenHands includes multiple analyzers:
|
||||
|
||||
- **No Analyzer**: Do not use any security analyzer. The agent will prompt you to confirm *EVERY* action.
|
||||
- **LLM Risk Analyzer** (default): Uses the same LLM as the agent to assess action risk levels
|
||||
- **Invariant Analyzer**: Uses Invariant Labs' policy engine to evaluate action traces against security policies
|
||||
|
||||
### LLM Risk Analyzer
|
||||
The default analyzer that leverages the agent's LLM to evaluate the security risk of each action. It considers the action type, parameters, and context to assign risk levels.
|
||||
|
||||
### Invariant Analyzer
|
||||
An advanced analyzer that:
|
||||
- Collects conversation events and parses them into a trace
|
||||
- Checks the trace against an Invariant policy to classify risk (low, medium, high)
|
||||
- Manages an Invariant server container automatically if needed
|
||||
- Supports optional browsing-alignment and harmful-content checks
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Action Analysis**: When the agent wants to perform an action, the selected security analyzer evaluates its risk level.
|
||||
|
||||
2. **Risk Assessment**: The analyzer returns one of three risk levels:
|
||||
- **LOW**: Action proceeds without confirmation
|
||||
- **MEDIUM**: Action proceeds without confirmation (may be configurable in future)
|
||||
- **HIGH**: Action is paused, and user confirmation is requested
|
||||
|
||||
3. **User Confirmation**: For high-risk actions, a confirmation dialog appears with:
|
||||
- Description of the action
|
||||
- Risk assessment explanation
|
||||
- Options to approve or deny action
|
||||
|
||||
4. **Action Execution**: Based on user response:
|
||||
- **Approve**: Action proceeds as planned
|
||||
- **Deny**: Action is cancelled
|
||||
@@ -9,8 +9,8 @@ from evaluation.utils.shared import (
|
||||
EvalMetadata,
|
||||
EvalOutput,
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
get_openhands_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -61,15 +61,18 @@ AGENT_CLS_TO_INST_SUFFIX = {
|
||||
def get_config(
|
||||
metadata: EvalMetadata,
|
||||
) -> OpenHandsConfig:
|
||||
# Create config with EDA-specific container image
|
||||
config = get_openhands_config_for_eval(
|
||||
metadata=metadata,
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'python:3.12-bookworm'
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
|
||||
# Override the container image for EDA
|
||||
config.sandbox.base_container_image = 'python:3.12-bookworm'
|
||||
|
||||
config.set_llm_config(metadata.llm_config)
|
||||
agent_config = config.get_agent_config(metadata.agent_class)
|
||||
agent_config.enable_prompt_extensions = False
|
||||
|
||||
@@ -17,8 +17,8 @@ from evaluation.utils.shared import (
|
||||
EvalMetadata,
|
||||
EvalOutput,
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
get_openhands_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -41,12 +41,19 @@ from openhands.utils.async_utils import call_async_from_sync
|
||||
def get_config(
|
||||
metadata: EvalMetadata,
|
||||
) -> OpenHandsConfig:
|
||||
# Create config with agent_bench-specific container image
|
||||
config = get_openhands_config_for_eval(metadata=metadata)
|
||||
|
||||
# Override the container image for agent_bench
|
||||
config.sandbox.base_container_image = 'python:3.12-slim'
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'python:3.12-slim'
|
||||
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
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)
|
||||
agent_config.enable_prompt_extensions = False
|
||||
|
||||
@@ -18,7 +18,6 @@ from evaluation.utils.shared import (
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
get_openhands_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -51,10 +50,15 @@ def get_config(
|
||||
) -> OpenHandsConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'python:3.11-bookworm'
|
||||
config = get_openhands_config_for_eval(
|
||||
metadata=metadata,
|
||||
sandbox_config=sandbox_config,
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
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)
|
||||
|
||||
@@ -16,7 +16,6 @@ from evaluation.utils.shared import (
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
get_openhands_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -62,10 +61,15 @@ def get_config(
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = BIOCODER_BENCH_CONTAINER_IMAGE
|
||||
|
||||
config = get_openhands_config_for_eval(
|
||||
metadata=metadata,
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
sandbox_config=sandbox_config,
|
||||
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)
|
||||
|
||||
@@ -19,7 +19,6 @@ from evaluation.utils.shared import (
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
get_openhands_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -76,10 +75,15 @@ def get_config(
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'python:3.12-bookworm'
|
||||
|
||||
config = get_openhands_config_for_eval(
|
||||
metadata=metadata,
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
sandbox_config=sandbox_config,
|
||||
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)
|
||||
|
||||
@@ -12,7 +12,6 @@ from evaluation.utils.shared import (
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
get_openhands_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -41,8 +40,14 @@ def get_config(
|
||||
)
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'python:3.12-bookworm'
|
||||
config = get_openhands_config_for_eval(
|
||||
metadata=metadata, runtime='docker', sandbox_config=sandbox_config
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
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)
|
||||
|
||||
@@ -17,7 +17,6 @@ from evaluation.utils.shared import (
|
||||
codeact_user_response,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
get_openhands_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -115,11 +114,16 @@ def get_config(
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = base_container_image
|
||||
|
||||
config = get_openhands_config_for_eval(
|
||||
metadata=metadata,
|
||||
sandbox_config=sandbox_config,
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
max_iterations=metadata.max_iterations,
|
||||
enable_browser=RUN_WITH_BROWSING,
|
||||
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(
|
||||
|
||||
@@ -18,7 +18,6 @@ from evaluation.utils.shared import (
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
get_openhands_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -66,10 +65,15 @@ def get_config(
|
||||
) -> OpenHandsConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'python:3.12-bookworm'
|
||||
config = get_openhands_config_for_eval(
|
||||
metadata=metadata,
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
sandbox_config=sandbox_config,
|
||||
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)
|
||||
|
||||
@@ -23,7 +23,6 @@ from evaluation.utils.shared import (
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
get_openhands_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -61,10 +60,15 @@ def get_config(
|
||||
) -> OpenHandsConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'nikolaik/python-nodejs:python3.12-nodejs22'
|
||||
config = get_openhands_config_for_eval(
|
||||
metadata=metadata,
|
||||
sandbox_config=sandbox_config,
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
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:
|
||||
|
||||
@@ -13,7 +13,6 @@ from evaluation.utils.shared import (
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
get_openhands_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -44,10 +43,15 @@ def get_config(
|
||||
) -> OpenHandsConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'python:3.12-bookworm'
|
||||
config = get_openhands_config_for_eval(
|
||||
metadata=metadata,
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
sandbox_config=sandbox_config,
|
||||
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)
|
||||
|
||||
@@ -31,7 +31,6 @@ from evaluation.utils.shared import (
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
get_openhands_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -65,10 +64,15 @@ def get_config(
|
||||
) -> OpenHandsConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'python:3.12-bookworm'
|
||||
config = get_openhands_config_for_eval(
|
||||
metadata=metadata,
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
sandbox_config=sandbox_config,
|
||||
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)
|
||||
|
||||
@@ -24,7 +24,6 @@ from evaluation.utils.shared import (
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
get_openhands_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -86,10 +85,15 @@ def get_config(
|
||||
) -> OpenHandsConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'python:3.12-bookworm'
|
||||
config = get_openhands_config_for_eval(
|
||||
metadata=metadata,
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
sandbox_config=sandbox_config,
|
||||
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)
|
||||
|
||||
@@ -16,7 +16,6 @@ import ruamel.yaml
|
||||
from evaluation.utils.shared import (
|
||||
EvalMetadata,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_openhands_config_for_eval,
|
||||
make_metadata,
|
||||
)
|
||||
from openhands.core.config import (
|
||||
@@ -38,10 +37,15 @@ def get_config(
|
||||
) -> OpenHandsConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'python:3.12-bookworm'
|
||||
config = get_openhands_config_for_eval(
|
||||
metadata=metadata,
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
sandbox_config=sandbox_config,
|
||||
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)
|
||||
|
||||
@@ -23,7 +23,6 @@ from evaluation.utils.shared import (
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
get_openhands_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -49,10 +48,15 @@ def get_config(
|
||||
) -> OpenHandsConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'python:3.12-bookworm'
|
||||
config = get_openhands_config_for_eval(
|
||||
metadata=metadata,
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
sandbox_config=sandbox_config,
|
||||
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)
|
||||
|
||||
@@ -11,7 +11,6 @@ from evaluation.utils.shared import (
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
get_openhands_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -53,10 +52,15 @@ def get_config(
|
||||
'$OH_INTERPRETER_PATH -m pip install scitools-pyke'
|
||||
)
|
||||
|
||||
config = get_openhands_config_for_eval(
|
||||
metadata=metadata,
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
sandbox_config=sandbox_config,
|
||||
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)
|
||||
|
||||
@@ -14,7 +14,6 @@ from evaluation.utils.shared import (
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
get_openhands_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -59,10 +58,15 @@ def get_config(
|
||||
) -> OpenHandsConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'xingyaoww/od-eval-miniwob:v1.0'
|
||||
config = get_openhands_config_for_eval(
|
||||
metadata=metadata,
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
sandbox_config=sandbox_config,
|
||||
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(
|
||||
|
||||
@@ -16,7 +16,6 @@ from evaluation.utils.shared import (
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
get_openhands_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -111,10 +110,15 @@ def get_config(
|
||||
f'$OH_INTERPRETER_PATH -m pip install {" ".join(MINT_DEPENDENCIES)}'
|
||||
)
|
||||
|
||||
config = get_openhands_config_for_eval(
|
||||
metadata=metadata,
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
sandbox_config=sandbox_config,
|
||||
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)
|
||||
|
||||
@@ -27,7 +27,6 @@ from evaluation.utils.shared import (
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
get_openhands_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -81,10 +80,15 @@ def get_config(
|
||||
) -> OpenHandsConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'public.ecr.aws/i5g0m1f6/ml-bench'
|
||||
config = get_openhands_config_for_eval(
|
||||
metadata=metadata,
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
sandbox_config=sandbox_config,
|
||||
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)
|
||||
|
||||
@@ -23,7 +23,6 @@ from evaluation.utils.shared import (
|
||||
EvalMetadata,
|
||||
EvalOutput,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_openhands_config_for_eval,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
run_evaluation,
|
||||
@@ -88,9 +87,13 @@ def get_config(metadata: EvalMetadata, instance: pd.Series) -> OpenHandsConfig:
|
||||
dataset_name=metadata.dataset,
|
||||
instance_id=instance['instance_id'],
|
||||
)
|
||||
config = get_openhands_config_for_eval(
|
||||
config = OpenHandsConfig(
|
||||
run_as_openhands=False,
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
sandbox_config=sandbox_config,
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ from evaluation.utils.shared import (
|
||||
codeact_user_response,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
get_openhands_config_for_eval,
|
||||
is_fatal_evaluation_error,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
@@ -342,11 +341,16 @@ def get_config(
|
||||
instance_id=instance['instance_id'],
|
||||
)
|
||||
|
||||
config = get_openhands_config_for_eval(
|
||||
metadata=metadata,
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
max_iterations=metadata.max_iterations,
|
||||
enable_browser=RUN_WITH_BROWSING,
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
sandbox_config=sandbox_config,
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
config.set_llm_config(
|
||||
update_llm_config_for_completions_logging(
|
||||
|
||||
@@ -31,7 +31,6 @@ from evaluation.utils.shared import (
|
||||
codeact_user_response,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
get_openhands_config_for_eval,
|
||||
is_fatal_evaluation_error,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
@@ -175,10 +174,15 @@ def get_config(
|
||||
instance_id=instance['instance_id'],
|
||||
)
|
||||
|
||||
config = get_openhands_config_for_eval(
|
||||
metadata=metadata,
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
max_iterations=metadata.max_iterations,
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
sandbox_config=sandbox_config,
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
|
||||
config.set_llm_config(
|
||||
|
||||
@@ -13,7 +13,6 @@ from evaluation.utils.shared import (
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
get_openhands_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -65,10 +64,16 @@ def get_config(
|
||||
sandbox_config.base_container_image = (
|
||||
'docker.io/xingyaoww/openhands-eval-scienceagentbench'
|
||||
)
|
||||
config = get_openhands_config_for_eval(
|
||||
metadata=metadata,
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
sandbox_config=sandbox_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(
|
||||
|
||||
@@ -19,7 +19,6 @@ from evaluation.utils.shared import (
|
||||
EvalMetadata,
|
||||
EvalOutput,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_openhands_config_for_eval,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
run_evaluation,
|
||||
@@ -84,9 +83,13 @@ def get_config(metadata: EvalMetadata, instance: pd.Series) -> OpenHandsConfig:
|
||||
dataset_name=metadata.dataset,
|
||||
instance_id=instance['instance_id'],
|
||||
)
|
||||
config = get_openhands_config_for_eval(
|
||||
config = OpenHandsConfig(
|
||||
run_as_openhands=False,
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
sandbox_config=sandbox_config,
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ from evaluation.utils.shared import (
|
||||
codeact_user_response,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
get_openhands_config_for_eval,
|
||||
is_fatal_evaluation_error,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
@@ -228,11 +227,16 @@ def get_config(
|
||||
instance_id=instance['instance_id'],
|
||||
)
|
||||
|
||||
config = get_openhands_config_for_eval(
|
||||
metadata=metadata,
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
max_iterations=metadata.max_iterations,
|
||||
enable_browser=RUN_WITH_BROWSING,
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
sandbox_config=sandbox_config,
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
|
||||
config.set_llm_config(
|
||||
|
||||
@@ -20,7 +20,6 @@ from evaluation.utils.shared import (
|
||||
codeact_user_response,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
get_openhands_config_for_eval,
|
||||
is_fatal_evaluation_error,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
@@ -200,11 +199,16 @@ def get_config(
|
||||
'REPO_PATH': f'/workspace/{workspace_dir_name}/',
|
||||
}
|
||||
|
||||
config = get_openhands_config_for_eval(
|
||||
metadata=metadata,
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
max_iterations=metadata.max_iterations,
|
||||
enable_browser=RUN_WITH_BROWSING,
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
sandbox_config=sandbox_config,
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
config.set_llm_config(
|
||||
update_llm_config_for_completions_logging(
|
||||
|
||||
@@ -37,7 +37,6 @@ from evaluation.benchmarks.testgeneval.utils import load_testgeneval_dataset
|
||||
from evaluation.utils.shared import (
|
||||
EvalMetadata,
|
||||
EvalOutput,
|
||||
get_openhands_config_for_eval,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
run_evaluation,
|
||||
@@ -59,21 +58,20 @@ def get_config(instance: pd.Series) -> OpenHandsConfig:
|
||||
f'Invalid container image for instance {instance["instance_id_swebench"]}.'
|
||||
)
|
||||
logger.info(f'Using instance container image: {base_container_image}.')
|
||||
|
||||
# Create custom sandbox config for testgeneval with specific requirements
|
||||
sandbox_config = SandboxConfig(
|
||||
base_container_image=base_container_image,
|
||||
use_host_network=False,
|
||||
timeout=1800, # Longer timeout than default (300)
|
||||
api_key=os.environ.get('ALLHANDS_API_KEY'),
|
||||
remote_runtime_api_url=os.environ.get(
|
||||
'SANDBOX_REMOTE_RUNTIME_API_URL', 'http://localhost:8000'
|
||||
return OpenHandsConfig(
|
||||
run_as_openhands=False,
|
||||
runtime=os.environ.get('RUNTIME', 'eventstream'),
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image=base_container_image,
|
||||
use_host_network=False,
|
||||
timeout=1800,
|
||||
api_key=os.environ.get('ALLHANDS_API_KEY'),
|
||||
remote_runtime_api_url=os.environ.get(
|
||||
'SANDBOX_REMOTE_RUNTIME_API_URL', 'http://localhost:8000'
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
return get_openhands_config_for_eval(
|
||||
sandbox_config=sandbox_config,
|
||||
runtime=os.environ.get('RUNTIME', 'docker'), # Different default runtime
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ from evaluation.utils.shared import (
|
||||
assert_and_raise,
|
||||
codeact_user_response,
|
||||
get_metrics,
|
||||
get_openhands_config_for_eval,
|
||||
is_fatal_evaluation_error,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
@@ -127,26 +126,29 @@ def get_config(
|
||||
f'Submit an issue on https://github.com/All-Hands-AI/OpenHands if you run into any issues.'
|
||||
)
|
||||
|
||||
sandbox_config = SandboxConfig(
|
||||
base_container_image=base_container_image,
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
# large enough timeout, since some testcases take very long to run
|
||||
timeout=300,
|
||||
# Add platform to the sandbox config to solve issue 4401
|
||||
platform='linux/amd64',
|
||||
api_key=os.environ.get('ALLHANDS_API_KEY', None),
|
||||
remote_runtime_api_url=os.environ.get(
|
||||
'SANDBOX_REMOTE_RUNTIME_API_URL', 'http://localhost:8000'
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
max_iterations=metadata.max_iterations,
|
||||
runtime=os.environ.get('RUNTIME', 'eventstream'),
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image=base_container_image,
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
# large enough timeout, since some testcases take very long to run
|
||||
timeout=300,
|
||||
# Add platform to the sandbox config to solve issue 4401
|
||||
platform='linux/amd64',
|
||||
api_key=os.environ.get('ALLHANDS_API_KEY', None),
|
||||
remote_runtime_api_url=os.environ.get(
|
||||
'SANDBOX_REMOTE_RUNTIME_API_URL', 'http://localhost:8000'
|
||||
),
|
||||
keep_runtime_alive=False,
|
||||
remote_runtime_init_timeout=3600,
|
||||
),
|
||||
keep_runtime_alive=False,
|
||||
remote_runtime_init_timeout=3600,
|
||||
)
|
||||
|
||||
config = get_openhands_config_for_eval(
|
||||
metadata=metadata,
|
||||
sandbox_config=sandbox_config,
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
config.set_llm_config(
|
||||
update_llm_config_for_completions_logging(
|
||||
|
||||
@@ -12,10 +12,7 @@ import tempfile
|
||||
import yaml
|
||||
from browsing import pre_login
|
||||
|
||||
from evaluation.utils.shared import (
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_openhands_config_for_eval,
|
||||
)
|
||||
from evaluation.utils.shared import get_default_sandbox_config_for_eval
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
LLMConfig,
|
||||
@@ -45,17 +42,19 @@ 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
|
||||
config = get_openhands_config_for_eval(
|
||||
config = OpenHandsConfig(
|
||||
run_as_openhands=False,
|
||||
max_budget_per_task=4,
|
||||
max_iterations=100,
|
||||
save_trajectory_path=os.path.join(
|
||||
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
|
||||
sandbox_config=sandbox_config,
|
||||
workspace_mount_path=mount_path_on_host,
|
||||
workspace_mount_path_in_sandbox='/outputs',
|
||||
)
|
||||
config.save_trajectory_path = os.path.join(
|
||||
mount_path_on_host, f'traj_{task_short_name}.json'
|
||||
)
|
||||
config.max_budget_per_task = 4
|
||||
config.set_llm_config(llm_config)
|
||||
if agent_config:
|
||||
config.set_agent_config(agent_config)
|
||||
|
||||
@@ -12,7 +12,6 @@ from evaluation.utils.shared import (
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
get_openhands_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -45,10 +44,15 @@ def get_config(
|
||||
) -> OpenHandsConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'python:3.12-bookworm'
|
||||
config = get_openhands_config_for_eval(
|
||||
metadata=metadata,
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
sandbox_config=sandbox_config,
|
||||
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)
|
||||
|
||||
@@ -20,7 +20,6 @@ from evaluation.utils.shared import (
|
||||
codeact_user_response,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
get_openhands_config_for_eval,
|
||||
is_fatal_evaluation_error,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
@@ -161,11 +160,16 @@ def get_config(
|
||||
instance_id=instance['instance_id'],
|
||||
)
|
||||
|
||||
config = get_openhands_config_for_eval(
|
||||
metadata=metadata,
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
max_iterations=metadata.max_iterations,
|
||||
enable_browser=RUN_WITH_BROWSING,
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
sandbox_config=sandbox_config,
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
config.set_llm_config(
|
||||
update_llm_config_for_completions_logging(
|
||||
|
||||
@@ -13,7 +13,6 @@ from evaluation.utils.shared import (
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
get_openhands_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -74,10 +73,16 @@ def get_config(
|
||||
'VWA_WIKIPEDIA': f'{base_url}:8888',
|
||||
'VWA_HOMEPAGE': f'{base_url}:4399',
|
||||
}
|
||||
config = get_openhands_config_for_eval(
|
||||
metadata=metadata,
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
sandbox_config=sandbox_config,
|
||||
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(
|
||||
update_llm_config_for_completions_logging(
|
||||
|
||||
@@ -13,7 +13,6 @@ from evaluation.utils.shared import (
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
get_openhands_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -22,8 +21,8 @@ from evaluation.utils.shared import (
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
OpenHandsConfig,
|
||||
get_evaluation_parser,
|
||||
get_llm_config_arg,
|
||||
parse_arguments,
|
||||
)
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.main import create_runtime, run_controller
|
||||
@@ -32,7 +31,6 @@ from openhands.events.action import (
|
||||
CmdRunAction,
|
||||
MessageAction,
|
||||
)
|
||||
from openhands.events.event import EventSource
|
||||
from openhands.events.observation import CmdOutputObservation
|
||||
from openhands.runtime.base import Runtime
|
||||
from openhands.runtime.browser.browser_env import (
|
||||
@@ -56,24 +54,26 @@ def get_config(
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'python:3.12-bookworm'
|
||||
sandbox_config.browsergym_eval_env = env_id
|
||||
# Install evaluation dependencies in the runtime container (into Poetry environment)
|
||||
sandbox_config.runtime_extra_deps = '/openhands/micromamba/bin/micromamba run -n openhands poetry run pip install browsergym-webarena==0.13.3'
|
||||
sandbox_config.runtime_startup_env_vars = {
|
||||
'WEBARENA_BASE_URL': base_url,
|
||||
'BASE_URL': base_url,
|
||||
'OPENAI_API_KEY': openai_api_key,
|
||||
'WA_SHOPPING': f'{base_url}:7770/',
|
||||
'WA_SHOPPING_ADMIN': f'{base_url}:7780/admin',
|
||||
'WA_REDDIT': f'{base_url}:9999',
|
||||
'WA_GITLAB': f'{base_url}:8023',
|
||||
'WA_WIKIPEDIA': f'{base_url}:8888/wikipedia_en_all_maxi_2022-05/A/User:The_other_Kiwix_guy/Landing',
|
||||
'WA_MAP': f'{base_url}:3000',
|
||||
'WA_HOMEPAGE': f'{base_url}:4399',
|
||||
'SHOPPING': f'{base_url}:7770/',
|
||||
'SHOPPING_ADMIN': f'{base_url}:7780/admin',
|
||||
'REDDIT': f'{base_url}:9999',
|
||||
'GITLAB': f'{base_url}:8023',
|
||||
'WIKIPEDIA': f'{base_url}:8888/wikipedia_en_all_maxi_2022-05/A/User:The_other_Kiwix_guy/Landing',
|
||||
'MAP': f'{base_url}:3000',
|
||||
'HOMEPAGE': f'{base_url}:4399',
|
||||
}
|
||||
config = get_openhands_config_for_eval(
|
||||
metadata=metadata,
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
enable_browser=True,
|
||||
sandbox_config=sandbox_config,
|
||||
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)
|
||||
@@ -83,7 +83,7 @@ def get_config(
|
||||
|
||||
def initialize_runtime(
|
||||
runtime: Runtime,
|
||||
) -> str:
|
||||
) -> dict:
|
||||
"""Initialize the runtime for the agent.
|
||||
|
||||
This function is called before the runtime is used to run the agent.
|
||||
@@ -149,48 +149,13 @@ def process_instance(
|
||||
call_async_from_sync(runtime.connect)
|
||||
task_str = initialize_runtime(runtime)
|
||||
|
||||
logger.info(f"DEBUG: task_str = {repr(task_str)}")
|
||||
logger.info(f"DEBUG: task_str type = {type(task_str)}")
|
||||
|
||||
# Use EventSource.ENVIRONMENT to bypass recall processing in evaluation
|
||||
initial_action = MessageAction(content=task_str)
|
||||
initial_action._source = EventSource.ENVIRONMENT # Bypass recall for evaluation
|
||||
logger.info(f"DEBUG: Created MessageAction: {initial_action}")
|
||||
logger.info(f"DEBUG: MessageAction content: {repr(initial_action.content)}")
|
||||
logger.info(f"DEBUG: MessageAction source: {initial_action.source}")
|
||||
|
||||
# Enable detailed logging for debugging
|
||||
import os
|
||||
os.environ['LOG_ALL_EVENTS'] = '1'
|
||||
|
||||
state: State | None = asyncio.run(
|
||||
run_controller(
|
||||
config=config,
|
||||
initial_user_action=initial_action,
|
||||
initial_user_action=MessageAction(content=task_str),
|
||||
runtime=runtime,
|
||||
)
|
||||
)
|
||||
|
||||
logger.info(f"DEBUG: run_controller returned state: {state}")
|
||||
if state:
|
||||
logger.info(f"DEBUG: state.agent_state: {state.agent_state}")
|
||||
logger.info(f"DEBUG: state.history length: {len(state.history)}")
|
||||
logger.info(f"DEBUG: Last 10 events in history:")
|
||||
for i, event in enumerate(state.history[-10:]):
|
||||
logger.info(f"DEBUG: {i}: {type(event).__name__} - {event}")
|
||||
|
||||
# Look for RecallActions specifically
|
||||
recall_actions = [e for e in state.history if e.__class__.__name__ == 'RecallAction']
|
||||
logger.info(f"DEBUG: Found {len(recall_actions)} RecallAction(s)")
|
||||
for i, recall in enumerate(recall_actions):
|
||||
logger.info(f"DEBUG: RecallAction {i}: {recall}")
|
||||
|
||||
# Look for any observations related to RecallActions
|
||||
recall_observations = [e for e in state.history if hasattr(e, 'cause') and any(str(e.cause) == str(r.id) for r in recall_actions)]
|
||||
logger.info(f"DEBUG: Found {len(recall_observations)} RecallAction observation(s)")
|
||||
for i, obs in enumerate(recall_observations):
|
||||
logger.info(f"DEBUG: RecallAction observation {i}: {obs}")
|
||||
|
||||
# ======= Attempt to evaluate the agent's environment impact =======
|
||||
|
||||
# If you are working on some simpler benchmark that only evaluates the final model output (e.g., in a MessageAction)
|
||||
@@ -233,8 +198,7 @@ def process_instance(
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = get_evaluation_parser()
|
||||
args = parser.parse_args()
|
||||
args = parse_arguments()
|
||||
|
||||
dataset = pd.DataFrame(
|
||||
{
|
||||
@@ -256,7 +220,7 @@ if __name__ == '__main__':
|
||||
|
||||
metadata = make_metadata(
|
||||
llm_config,
|
||||
'webarena',
|
||||
args.dataset_name,
|
||||
args.agent_cls,
|
||||
args.max_iterations,
|
||||
args.eval_note,
|
||||
|
||||
@@ -3,6 +3,9 @@ set -eo pipefail
|
||||
|
||||
source "evaluation/utils/version_control.sh"
|
||||
|
||||
# configure webarena websites and environment
|
||||
source evaluation/benchmarks/webarena/scripts/webarena_env.sh
|
||||
|
||||
# configure browsing agent
|
||||
export USE_NAV="false"
|
||||
export USE_CONCISE_ANSWER="true"
|
||||
|
||||
@@ -10,7 +10,6 @@ from evaluation.utils.shared import (
|
||||
EvalOutput,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
get_openhands_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -46,12 +45,18 @@ def get_config(
|
||||
) -> OpenHandsConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.platform = 'linux/amd64'
|
||||
config = get_openhands_config_for_eval(
|
||||
metadata=metadata,
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
sandbox_config=sandbox_config,
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
# debug
|
||||
debug=True,
|
||||
)
|
||||
config.debug = True
|
||||
config.set_llm_config(
|
||||
update_llm_config_for_completions_logging(
|
||||
metadata.llm_config, metadata.eval_output_dir, instance_id
|
||||
|
||||
@@ -703,79 +703,3 @@ def get_default_sandbox_config_for_eval() -> SandboxConfig:
|
||||
remote_runtime_enable_retries=True,
|
||||
remote_runtime_class='sysbox',
|
||||
)
|
||||
|
||||
|
||||
def get_openhands_config_for_eval(
|
||||
metadata: EvalMetadata | None = None,
|
||||
sandbox_config: SandboxConfig | None = None,
|
||||
runtime: str | None = None,
|
||||
max_iterations: int | None = None,
|
||||
default_agent: str | None = None,
|
||||
enable_browser: bool = False,
|
||||
workspace_base: str | None = None,
|
||||
workspace_mount_path: str | None = None,
|
||||
):
|
||||
"""Create an OpenHandsConfig with common patterns used across evaluation scripts.
|
||||
|
||||
This function provides a standardized way to create OpenHands configurations
|
||||
for evaluation runs, with sensible defaults that match the patterns used in
|
||||
most run_infer.py scripts. Individual evaluation scripts can override specific
|
||||
attributes as needed.
|
||||
|
||||
Args:
|
||||
metadata: EvalMetadata containing agent class, max iterations, etc.
|
||||
sandbox_config: Custom sandbox config. If None, uses get_default_sandbox_config_for_eval()
|
||||
runtime: Runtime type. If None, uses environment RUNTIME or 'docker'
|
||||
max_iterations: Max iterations for the agent. If None, uses metadata.max_iterations
|
||||
default_agent: Agent class name. If None, uses metadata.agent_class
|
||||
enable_browser: Whether to enable browser functionality
|
||||
workspace_base: Workspace base path. Defaults to None
|
||||
workspace_mount_path: Workspace mount path. Defaults to None
|
||||
|
||||
Returns:
|
||||
OpenHandsConfig: Configured for evaluation with eval-specific overrides applied
|
||||
"""
|
||||
# Defer import to avoid circular imports at module load time
|
||||
from openhands.core.config.openhands_config import (
|
||||
OpenHandsConfig as _OHConfig, # type: ignore
|
||||
)
|
||||
|
||||
# Use provided sandbox config or get default
|
||||
if sandbox_config is None:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
|
||||
# Extract values from metadata if provided
|
||||
if metadata is not None:
|
||||
if max_iterations is None:
|
||||
max_iterations = metadata.max_iterations
|
||||
if default_agent is None:
|
||||
default_agent = metadata.agent_class
|
||||
|
||||
# Use environment runtime or default
|
||||
if runtime is None:
|
||||
runtime = os.environ.get('RUNTIME', 'docker')
|
||||
|
||||
# Provide sensible defaults if still None
|
||||
if default_agent is None:
|
||||
default_agent = 'CodeActAgent'
|
||||
if max_iterations is None:
|
||||
max_iterations = 50
|
||||
|
||||
# Always use repo-local .eval_sessions directory (absolute path)
|
||||
eval_store = os.path.abspath(os.path.join(os.getcwd(), '.eval_sessions'))
|
||||
|
||||
# Create the base config with evaluation-specific overrides
|
||||
config = _OHConfig(
|
||||
default_agent=default_agent,
|
||||
run_as_openhands=False,
|
||||
runtime=runtime,
|
||||
max_iterations=max_iterations,
|
||||
enable_browser=enable_browser,
|
||||
sandbox=sandbox_config,
|
||||
workspace_base=workspace_base,
|
||||
workspace_mount_path=workspace_mount_path,
|
||||
file_store='local',
|
||||
file_store_path=eval_store,
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
@@ -79,35 +79,6 @@ describe("Content", () => {
|
||||
expect(screen.getByTestId("set-indicator")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should conditionally show security analyzer based on confirmation mode", async () => {
|
||||
renderLlmSettingsScreen();
|
||||
await screen.findByTestId("llm-settings-screen");
|
||||
|
||||
const confirmation = screen.getByTestId("enable-confirmation-mode-switch");
|
||||
|
||||
// Initially confirmation mode is false, so security analyzer should not be visible
|
||||
expect(confirmation).not.toBeChecked();
|
||||
expect(
|
||||
screen.queryByTestId("security-analyzer-input"),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// Enable confirmation mode
|
||||
await userEvent.click(confirmation);
|
||||
expect(confirmation).toBeChecked();
|
||||
|
||||
// Security analyzer should now be visible
|
||||
screen.getByTestId("security-analyzer-input");
|
||||
|
||||
// Disable confirmation mode again
|
||||
await userEvent.click(confirmation);
|
||||
expect(confirmation).not.toBeChecked();
|
||||
|
||||
// Security analyzer should be hidden again
|
||||
expect(
|
||||
screen.queryByTestId("security-analyzer-input"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Advanced form", () => {
|
||||
@@ -136,6 +107,7 @@ describe("Content", () => {
|
||||
within(advancedForm).getByTestId("llm-api-key-input");
|
||||
within(advancedForm).getByTestId("llm-api-key-help-anchor-advanced");
|
||||
within(advancedForm).getByTestId("agent-input");
|
||||
within(advancedForm).getByTestId("enable-confirmation-mode-switch");
|
||||
within(advancedForm).getByTestId("enable-memory-condenser-switch");
|
||||
|
||||
await userEvent.click(advancedSwitch);
|
||||
@@ -158,6 +130,9 @@ describe("Content", () => {
|
||||
const baseUrl = screen.getByTestId("base-url-input");
|
||||
const apiKey = screen.getByTestId("llm-api-key-input");
|
||||
const agent = screen.getByTestId("agent-input");
|
||||
const confirmation = screen.getByTestId(
|
||||
"enable-confirmation-mode-switch",
|
||||
);
|
||||
const condensor = screen.getByTestId("enable-memory-condenser-switch");
|
||||
|
||||
expect(model).toHaveValue("openhands/claude-sonnet-4-20250514");
|
||||
@@ -165,7 +140,15 @@ describe("Content", () => {
|
||||
expect(apiKey).toHaveValue("");
|
||||
expect(apiKey).toHaveProperty("placeholder", "");
|
||||
expect(agent).toHaveValue("CodeActAgent");
|
||||
expect(confirmation).not.toBeChecked();
|
||||
expect(condensor).toBeChecked();
|
||||
|
||||
// check that security analyzer is present
|
||||
expect(
|
||||
screen.queryByTestId("security-analyzer-input"),
|
||||
).not.toBeInTheDocument();
|
||||
await userEvent.click(confirmation);
|
||||
screen.getByTestId("security-analyzer-input");
|
||||
});
|
||||
|
||||
it("should render the advanced form if existings settings are advanced", async () => {
|
||||
@@ -194,7 +177,7 @@ describe("Content", () => {
|
||||
agent: "CoActAgent",
|
||||
confirmation_mode: true,
|
||||
enable_default_condenser: false,
|
||||
security_analyzer: "none",
|
||||
security_analyzer: "mock-invariant",
|
||||
});
|
||||
|
||||
renderLlmSettingsScreen();
|
||||
@@ -220,7 +203,7 @@ describe("Content", () => {
|
||||
expect(agent).toHaveValue("CoActAgent");
|
||||
expect(confirmation).toBeChecked();
|
||||
expect(condensor).not.toBeChecked();
|
||||
expect(securityAnalyzer).toHaveValue("SETTINGS$SECURITY_ANALYZER_NONE");
|
||||
expect(securityAnalyzer).toHaveValue("mock-invariant");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -310,7 +293,7 @@ describe("Form submission", () => {
|
||||
// select security analyzer
|
||||
const securityAnalyzer = screen.getByTestId("security-analyzer-input");
|
||||
await userEvent.click(securityAnalyzer);
|
||||
const securityAnalyzerOption = screen.getByText("SETTINGS$SECURITY_ANALYZER_NONE");
|
||||
const securityAnalyzerOption = screen.getByText("mock-invariant");
|
||||
await userEvent.click(securityAnalyzerOption);
|
||||
|
||||
const submitButton = screen.getByTestId("submit-button");
|
||||
@@ -323,7 +306,7 @@ describe("Form submission", () => {
|
||||
agent: "CoActAgent",
|
||||
confirmation_mode: true,
|
||||
enable_default_condenser: false,
|
||||
security_analyzer: null,
|
||||
security_analyzer: "mock-invariant",
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -392,10 +375,8 @@ describe("Form submission", () => {
|
||||
const baseUrl = await screen.findByTestId("base-url-input");
|
||||
const apiKey = await screen.findByTestId("llm-api-key-input");
|
||||
const agent = await screen.findByTestId("agent-input");
|
||||
const condensor = await screen.findByTestId("enable-memory-condenser-switch");
|
||||
|
||||
// Confirmation mode switch is now in basic settings, always visible
|
||||
const confirmation = await screen.findByTestId("enable-confirmation-mode-switch");
|
||||
const condensor = await screen.findByTestId("enable-memory-condenser-switch");
|
||||
|
||||
// enter custom model
|
||||
await userEvent.type(model, "-mini");
|
||||
@@ -470,17 +451,14 @@ describe("Form submission", () => {
|
||||
// select security analyzer
|
||||
const securityAnalyzer = await screen.findByTestId("security-analyzer-input");
|
||||
await userEvent.click(securityAnalyzer);
|
||||
const securityAnalyzerOption = screen.getByText("SETTINGS$SECURITY_ANALYZER_NONE");
|
||||
const securityAnalyzerOption = screen.getByText("mock-invariant");
|
||||
await userEvent.click(securityAnalyzerOption);
|
||||
expect(securityAnalyzer).toHaveValue("SETTINGS$SECURITY_ANALYZER_NONE");
|
||||
expect(securityAnalyzer).toHaveValue("mock-invariant");
|
||||
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
|
||||
// revert back to original value
|
||||
await userEvent.click(securityAnalyzer);
|
||||
const originalSecurityAnalyzerOption = screen.getByText("SETTINGS$SECURITY_ANALYZER_LLM_DEFAULT");
|
||||
await userEvent.click(originalSecurityAnalyzerOption);
|
||||
expect(securityAnalyzer).toHaveValue("SETTINGS$SECURITY_ANALYZER_LLM_DEFAULT");
|
||||
await userEvent.clear(securityAnalyzer);
|
||||
expect(securityAnalyzer).toHaveValue("");
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
|
||||
@@ -574,7 +552,7 @@ describe("Form submission", () => {
|
||||
expect.objectContaining({
|
||||
llm_model: "openhands/claude-sonnet-4-20250514",
|
||||
llm_base_url: "",
|
||||
confirmation_mode: true, // Confirmation mode is now a basic setting, should be preserved
|
||||
confirmation_mode: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -107,7 +107,9 @@ describe("Content", () => {
|
||||
expect(screen.queryByTestId("add-secret-button")).not.toBeInTheDocument(),
|
||||
);
|
||||
const button = await screen.findByTestId("connect-git-button");
|
||||
expect(button).toHaveAttribute("href", "/settings/integrations");
|
||||
await userEvent.click(button);
|
||||
|
||||
screen.getByTestId("git-settings-screen");
|
||||
});
|
||||
|
||||
it("should render an empty table when there are no existing secrets", async () => {
|
||||
|
||||
@@ -29,5 +29,23 @@ describe("hasAdvancedSettingsSet", () => {
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("CONFIRMATION_MODE is true", () => {
|
||||
expect(
|
||||
hasAdvancedSettingsSet({
|
||||
...DEFAULT_SETTINGS,
|
||||
CONFIRMATION_MODE: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("SECURITY_ANALYZER is set", () => {
|
||||
expect(
|
||||
hasAdvancedSettingsSet({
|
||||
...DEFAULT_SETTINGS,
|
||||
SECURITY_ANALYZER: "test",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,10 +7,11 @@ import { ConversationCard } from "../conversation-panel/conversation-card";
|
||||
import { Provider } from "#/types/settings";
|
||||
|
||||
interface ControlsProps {
|
||||
setSecurityOpen: (isOpen: boolean) => void;
|
||||
showSecurityLock: boolean;
|
||||
}
|
||||
|
||||
export function Controls({ showSecurityLock }: ControlsProps) {
|
||||
export function Controls({ setSecurityOpen, showSecurityLock }: ControlsProps) {
|
||||
const { data: conversation } = useActiveConversation();
|
||||
const [contextMenuOpen, setContextMenuOpen] = React.useState(false);
|
||||
|
||||
@@ -20,7 +21,9 @@ export function Controls({ showSecurityLock }: ControlsProps) {
|
||||
<AgentControlBar />
|
||||
<AgentStatusBar />
|
||||
|
||||
{showSecurityLock && <SecurityLock />}
|
||||
{showSecurityLock && (
|
||||
<SecurityLock onClick={() => setSecurityOpen(true)} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ConversationCard
|
||||
|
||||
@@ -1,28 +1,17 @@
|
||||
import { IoLockClosed } from "react-icons/io5";
|
||||
import { Tooltip } from "@heroui/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
export function SecurityLock() {
|
||||
const { t } = useTranslation();
|
||||
interface SecurityLockProps {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export function SecurityLock({ onClick }: SecurityLockProps) {
|
||||
return (
|
||||
<Tooltip
|
||||
content={
|
||||
<div className="max-w-xs p-2">
|
||||
{t(I18nKey.SETTINGS$CONFIRMATION_MODE_LOCK_TOOLTIP)}
|
||||
</div>
|
||||
}
|
||||
placement="top"
|
||||
<div
|
||||
className="cursor-pointer hover:opacity-80 transition-all"
|
||||
style={{ marginRight: "8px" }}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Link
|
||||
to="/settings"
|
||||
className="mr-2 cursor-pointer hover:opacity-80 transition-all"
|
||||
aria-label={t(I18nKey.SETTINGS$TITLE)}
|
||||
>
|
||||
<IoLockClosed size={20} />
|
||||
</Link>
|
||||
</Tooltip>
|
||||
<IoLockClosed size={20} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Tooltip } from "@heroui/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ConfirmIcon from "#/assets/confirm";
|
||||
import RejectIcon from "#/assets/reject";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
interface ActionTooltipProps {
|
||||
type: "confirm" | "reject";
|
||||
@@ -11,35 +12,25 @@ interface ActionTooltipProps {
|
||||
export function ActionTooltip({ type, onClick }: ActionTooltipProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isConfirm = type === "confirm";
|
||||
|
||||
const ariaLabel = isConfirm
|
||||
? t(I18nKey.ACTION$CONFIRM)
|
||||
: t(I18nKey.ACTION$REJECT);
|
||||
|
||||
const content = isConfirm
|
||||
? t(I18nKey.CHAT_INTERFACE$USER_CONFIRMED)
|
||||
: t(I18nKey.CHAT_INTERFACE$USER_REJECTED);
|
||||
|
||||
const buttonLabel = isConfirm
|
||||
? `${t(I18nKey.CHAT_INTERFACE$INPUT_CONTINUE_MESSAGE)} ⌘↩`
|
||||
: `${t(I18nKey.BUTTON$CANCEL)} ⇧⌘⌫`;
|
||||
const content =
|
||||
type === "confirm"
|
||||
? t(I18nKey.CHAT_INTERFACE$USER_CONFIRMED)
|
||||
: t(I18nKey.CHAT_INTERFACE$USER_REJECTED);
|
||||
|
||||
return (
|
||||
<Tooltip content={content} closeDelay={100}>
|
||||
<button
|
||||
data-testid={`action-${type}-button`}
|
||||
type="button"
|
||||
aria-label={ariaLabel}
|
||||
className={cn(
|
||||
"rounded px-2 h-6.5 text-sm font-medium leading-5 cursor-pointer hover:opacity-80",
|
||||
aria-label={
|
||||
type === "confirm"
|
||||
? "bg-tertiary text-white"
|
||||
: "bg-white text-[#0D0F11]",
|
||||
)}
|
||||
? t(I18nKey.ACTION$CONFIRM)
|
||||
: t(I18nKey.ACTION$REJECT)
|
||||
}
|
||||
className="bg-tertiary rounded-full p-1 hover:bg-base-secondary"
|
||||
onClick={onClick}
|
||||
>
|
||||
{buttonLabel}
|
||||
{type === "confirm" ? <ConfirmIcon /> : <RejectIcon />}
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -1,120 +1,31 @@
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
import { ActionTooltip } from "../action-tooltip";
|
||||
import { isOpenHandsAction } from "#/types/core/guards";
|
||||
import { ActionSecurityRisk } from "#/state/security-analyzer-slice";
|
||||
import { RiskAlert } from "#/components/shared/risk-alert";
|
||||
import WarningIcon from "#/icons/u-warning.svg?react";
|
||||
import { RootState } from "#/store";
|
||||
import { addSubmittedEventId } from "#/state/event-message-slice";
|
||||
|
||||
export function ConfirmationButtons() {
|
||||
const submittedEventIds = useSelector(
|
||||
(state: RootState) => state.eventMessage.submittedEventIds,
|
||||
);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { send } = useWsClient();
|
||||
|
||||
const { send, parsedEvents } = useWsClient();
|
||||
|
||||
// Find the most recent action awaiting confirmation
|
||||
const awaitingAction = parsedEvents
|
||||
.slice()
|
||||
.reverse()
|
||||
.find((ev) => {
|
||||
if (!isOpenHandsAction(ev) || ev.source !== "agent") return false;
|
||||
const args = ev.args as Record<string, unknown>;
|
||||
return args?.confirmation_state === "awaiting_confirmation";
|
||||
});
|
||||
|
||||
const handleStateChange = useCallback(
|
||||
(state: AgentState) => {
|
||||
if (!awaitingAction) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(addSubmittedEventId(awaitingAction.id));
|
||||
send(generateAgentStateChangeEvent(state));
|
||||
},
|
||||
[send],
|
||||
);
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
useEffect(() => {
|
||||
if (!awaitingAction) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const handleCancelShortcut = (event: KeyboardEvent) => {
|
||||
if (event.shiftKey && event.metaKey && event.key === "Backspace") {
|
||||
event.preventDefault();
|
||||
handleStateChange(AgentState.USER_REJECTED);
|
||||
}
|
||||
};
|
||||
|
||||
const handleContinueShortcut = (event: KeyboardEvent) => {
|
||||
if (event.metaKey && event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
handleStateChange(AgentState.USER_CONFIRMED);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
// Cancel: Shift+Cmd+Backspace (⇧⌘⌫)
|
||||
handleCancelShortcut(event);
|
||||
// Continue: Cmd+Enter (⌘↩)
|
||||
handleContinueShortcut(event);
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [awaitingAction, handleStateChange]);
|
||||
|
||||
if (!awaitingAction || submittedEventIds.includes(awaitingAction.id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { args } = awaitingAction as { args: Record<string, unknown> };
|
||||
|
||||
const risk = args?.security_risk;
|
||||
|
||||
const isHighRisk =
|
||||
typeof risk === "string"
|
||||
? risk.toLowerCase() === "high"
|
||||
: Number(risk) === ActionSecurityRisk.HIGH;
|
||||
const handleStateChange = (state: AgentState) => {
|
||||
const event = generateAgentStateChangeEvent(state);
|
||||
send(event);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 pt-4">
|
||||
{isHighRisk && (
|
||||
<RiskAlert
|
||||
content={t(I18nKey.CHAT_INTERFACE$HIGH_RISK_WARNING)}
|
||||
icon={<WarningIcon width={16} height={16} color="#fff" />}
|
||||
severity="high"
|
||||
title={t(I18nKey.COMMON$HIGH_RISK)}
|
||||
<div className="flex justify-between items-center pt-4">
|
||||
<p>{t(I18nKey.CHAT_INTERFACE$USER_ASK_CONFIRMATION)}</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<ActionTooltip
|
||||
type="confirm"
|
||||
onClick={() => handleStateChange(AgentState.USER_CONFIRMED)}
|
||||
/>
|
||||
<ActionTooltip
|
||||
type="reject"
|
||||
onClick={() => handleStateChange(AgentState.USER_REJECTED)}
|
||||
/>
|
||||
)}
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-sm font-normal text-white">
|
||||
{t(I18nKey.CHAT_INTERFACE$USER_ASK_CONFIRMATION)}
|
||||
</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<ActionTooltip
|
||||
type="reject"
|
||||
onClick={() => handleStateChange(AgentState.USER_REJECTED)}
|
||||
/>
|
||||
<ActionTooltip
|
||||
type="confirm"
|
||||
onClick={() => handleStateChange(AgentState.USER_CONFIRMED)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -93,14 +93,14 @@ function SecurityInvariant() {
|
||||
(risk: ActionSecurityRisk) => {
|
||||
switch (risk) {
|
||||
case ActionSecurityRisk.LOW:
|
||||
return t(I18nKey.SECURITY$LOW_RISK);
|
||||
return t(I18nKey.SECURITY_ANALYZER$LOW_RISK);
|
||||
case ActionSecurityRisk.MEDIUM:
|
||||
return t(I18nKey.SECURITY$MEDIUM_RISK);
|
||||
return t(I18nKey.SECURITY_ANALYZER$MEDIUM_RISK);
|
||||
case ActionSecurityRisk.HIGH:
|
||||
return t(I18nKey.SECURITY$HIGH_RISK);
|
||||
return t(I18nKey.SECURITY_ANALYZER$HIGH_RISK);
|
||||
case ActionSecurityRisk.UNKNOWN:
|
||||
default:
|
||||
return t(I18nKey.SECURITY$UNKNOWN_RISK);
|
||||
return t(I18nKey.SECURITY_ANALYZER$UNKNOWN_RISK);
|
||||
}
|
||||
},
|
||||
[t],
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { ReactNode } from "react";
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
interface RiskAlertProps {
|
||||
className?: string;
|
||||
content: ReactNode;
|
||||
icon?: ReactNode;
|
||||
severity: "high" | "medium" | "low";
|
||||
title: string;
|
||||
}
|
||||
|
||||
export function RiskAlert({
|
||||
className,
|
||||
content,
|
||||
icon,
|
||||
severity,
|
||||
title,
|
||||
}: RiskAlertProps) {
|
||||
// Currently, we are only supporting the high risk alert. If we use want to support other risk levels, we can add them here and use cva to create different variants of this component.
|
||||
if (severity === "high") {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-3.5 bg-[#4A0709] border border-[#FF0006] text-red-400 rounded-xl px-3.5 h-13 text-sm text-white",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{icon && <span className="">{icon}</span>}
|
||||
<span className="font-bold">{title}</span>
|
||||
<span className="font-normal">{content}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -357,7 +357,6 @@ export enum I18nKey {
|
||||
CHAT_INTERFACE$INPUT_PLACEHOLDER = "CHAT_INTERFACE$INPUT_PLACEHOLDER",
|
||||
CHAT_INTERFACE$INPUT_CONTINUE_MESSAGE = "CHAT_INTERFACE$INPUT_CONTINUE_MESSAGE",
|
||||
CHAT_INTERFACE$USER_ASK_CONFIRMATION = "CHAT_INTERFACE$USER_ASK_CONFIRMATION",
|
||||
CHAT_INTERFACE$HIGH_RISK_WARNING = "CHAT_INTERFACE$HIGH_RISK_WARNING",
|
||||
CHAT_INTERFACE$USER_CONFIRMED = "CHAT_INTERFACE$USER_CONFIRMED",
|
||||
CHAT_INTERFACE$USER_REJECTED = "CHAT_INTERFACE$USER_REJECTED",
|
||||
CHAT_INTERFACE$INPUT_SEND_MESSAGE_BUTTON_CONTENT = "CHAT_INTERFACE$INPUT_SEND_MESSAGE_BUTTON_CONTENT",
|
||||
@@ -372,6 +371,10 @@ export enum I18nKey {
|
||||
CHAT_INTERFACE$MESSAGE_ARIA_LABEL = "CHAT_INTERFACE$MESSAGE_ARIA_LABEL",
|
||||
CHAT_INTERFACE$CHAT_CONVERSATION = "CHAT_INTERFACE$CHAT_CONVERSATION",
|
||||
CHAT_INTERFACE$UNKNOWN_SENDER = "CHAT_INTERFACE$UNKNOWN_SENDER",
|
||||
SECURITY_ANALYZER$UNKNOWN_RISK = "SECURITY_ANALYZER$UNKNOWN_RISK",
|
||||
SECURITY_ANALYZER$LOW_RISK = "SECURITY_ANALYZER$LOW_RISK",
|
||||
SECURITY_ANALYZER$MEDIUM_RISK = "SECURITY_ANALYZER$MEDIUM_RISK",
|
||||
SECURITY_ANALYZER$HIGH_RISK = "SECURITY_ANALYZER$HIGH_RISK",
|
||||
SETTINGS$MODEL_TOOLTIP = "SETTINGS$MODEL_TOOLTIP",
|
||||
SETTINGS$AGENT_TOOLTIP = "SETTINGS$AGENT_TOOLTIP",
|
||||
SETTINGS$LANGUAGE_TOOLTIP = "SETTINGS$LANGUAGE_TOOLTIP",
|
||||
@@ -382,12 +385,9 @@ export enum I18nKey {
|
||||
SETTINGS$REFRESH_LLM_API_KEY = "SETTINGS$REFRESH_LLM_API_KEY",
|
||||
SETTINGS$CONFIRMATION_MODE = "SETTINGS$CONFIRMATION_MODE",
|
||||
SETTINGS$CONFIRMATION_MODE_TOOLTIP = "SETTINGS$CONFIRMATION_MODE_TOOLTIP",
|
||||
SETTINGS$CONFIRMATION_MODE_LOCK_TOOLTIP = "SETTINGS$CONFIRMATION_MODE_LOCK_TOOLTIP",
|
||||
SETTINGS$AGENT_SELECT_ENABLED = "SETTINGS$AGENT_SELECT_ENABLED",
|
||||
SETTINGS$SECURITY_ANALYZER = "SETTINGS$SECURITY_ANALYZER",
|
||||
SETTINGS$SECURITY_ANALYZER_PLACEHOLDER = "SETTINGS$SECURITY_ANALYZER_PLACEHOLDER",
|
||||
SETTINGS$SECURITY_ANALYZER_TOOLTIP = "SETTINGS$SECURITY_ANALYZER_TOOLTIP",
|
||||
SETTINGS$SECURITY_ANALYZER_DESCRIPTION = "SETTINGS$SECURITY_ANALYZER_DESCRIPTION",
|
||||
SETTINGS$DONT_KNOW_API_KEY = "SETTINGS$DONT_KNOW_API_KEY",
|
||||
SETTINGS$CLICK_FOR_INSTRUCTIONS = "SETTINGS$CLICK_FOR_INSTRUCTIONS",
|
||||
SETTINGS$SAVED = "SETTINGS$SAVED",
|
||||
@@ -781,6 +781,8 @@ export enum I18nKey {
|
||||
PROJECT_MANAGEMENT$SVC_ACC_EMAIL_VALIDATION_ERROR = "PROJECT_MANAGEMENT$SVC_ACC_EMAIL_VALIDATION_ERROR",
|
||||
PROJECT_MANAGEMENT$SVC_ACC_API_KEY_VALIDATION_ERROR = "PROJECT_MANAGEMENT$SVC_ACC_API_KEY_VALIDATION_ERROR",
|
||||
MICROAGENT_MANAGEMENT$ERROR_LOADING_MICROAGENT_CONTENT = "MICROAGENT_MANAGEMENT$ERROR_LOADING_MICROAGENT_CONTENT",
|
||||
SETTINGS$MCP_ERROR_ENV_INVALID_FORMAT = "SETTINGS$MCP_ERROR_ENV_INVALID_FORMAT",
|
||||
SETTINGS$MCP_ERROR_URL_DUPLICATE = "SETTINGS$MCP_ERROR_URL_DUPLICATE",
|
||||
SETTINGS$MCP_SERVER_TYPE_SSE = "SETTINGS$MCP_SERVER_TYPE_SSE",
|
||||
SETTINGS$MCP_SERVER_TYPE_STDIO = "SETTINGS$MCP_SERVER_TYPE_STDIO",
|
||||
SETTINGS$MCP_SERVER_TYPE_SHTTP = "SETTINGS$MCP_SERVER_TYPE_SHTTP",
|
||||
@@ -792,8 +794,6 @@ export enum I18nKey {
|
||||
SETTINGS$MCP_ERROR_NAME_DUPLICATE = "SETTINGS$MCP_ERROR_NAME_DUPLICATE",
|
||||
SETTINGS$MCP_ERROR_COMMAND_REQUIRED = "SETTINGS$MCP_ERROR_COMMAND_REQUIRED",
|
||||
SETTINGS$MCP_ERROR_COMMAND_NO_SPACES = "SETTINGS$MCP_ERROR_COMMAND_NO_SPACES",
|
||||
SETTINGS$MCP_ERROR_URL_DUPLICATE = "SETTINGS$MCP_ERROR_URL_DUPLICATE",
|
||||
SETTINGS$MCP_ERROR_ENV_INVALID_FORMAT = "SETTINGS$MCP_ERROR_ENV_INVALID_FORMAT",
|
||||
SETTINGS$MCP_SERVER_TYPE = "SETTINGS$MCP_SERVER_TYPE",
|
||||
SETTINGS$MCP_API_KEY_PLACEHOLDER = "SETTINGS$MCP_API_KEY_PLACEHOLDER",
|
||||
SETTINGS$MCP_COMMAND_ARGUMENTS = "SETTINGS$MCP_COMMAND_ARGUMENTS",
|
||||
@@ -814,8 +814,4 @@ export enum I18nKey {
|
||||
MICROAGENT_MANAGEMENT$PR_READY_FOR_REVIEW = "MICROAGENT_MANAGEMENT$PR_READY_FOR_REVIEW",
|
||||
MICROAGENT_MANAGEMENT$PR_NOT_CREATED = "MICROAGENT_MANAGEMENT$PR_NOT_CREATED",
|
||||
MICROAGENT_MANAGEMENT$ERROR_CREATING_MICROAGENT = "MICROAGENT_MANAGEMENT$ERROR_CREATING_MICROAGENT",
|
||||
SETTINGS$SECURITY_ANALYZER_LLM_DEFAULT = "SETTINGS$SECURITY_ANALYZER_LLM_DEFAULT",
|
||||
SETTINGS$SECURITY_ANALYZER_NONE = "SETTINGS$SECURITY_ANALYZER_NONE",
|
||||
SETTINGS$SECURITY_ANALYZER_INVARIANT = "SETTINGS$SECURITY_ANALYZER_INVARIANT",
|
||||
COMMON$HIGH_RISK = "COMMON$HIGH_RISK",
|
||||
}
|
||||
|
||||
@@ -432,68 +432,68 @@
|
||||
"uk": "Повторний вхід до OpenHands..."
|
||||
},
|
||||
"SECURITY$LOW_RISK": {
|
||||
"en": "Risk: Low",
|
||||
"ja": "リスク: 低",
|
||||
"zh-CN": "风险: 低",
|
||||
"zh-TW": "風險: 低",
|
||||
"ko-KR": "위험: 낮음",
|
||||
"no": "Risiko: Lav",
|
||||
"it": "Rischio: Basso",
|
||||
"pt": "Risco: Baixo",
|
||||
"es": "Riesgo: Bajo",
|
||||
"ar": "المخاطر: منخفضة",
|
||||
"fr": "Risque : Faible",
|
||||
"tr": "Risk: Düşük",
|
||||
"de": "Risiko: Gering",
|
||||
"uk": "Ризик: Низький"
|
||||
"en": "Low Risk",
|
||||
"ja": "低リスク",
|
||||
"zh-CN": "低风险",
|
||||
"zh-TW": "低風險",
|
||||
"ko-KR": "낮은 위험",
|
||||
"no": "Lav risiko",
|
||||
"it": "Rischio basso",
|
||||
"pt": "Baixo risco",
|
||||
"es": "Riesgo bajo",
|
||||
"ar": "مخاطر منخفضة",
|
||||
"fr": "Risque faible",
|
||||
"tr": "Düşük risk",
|
||||
"de": "Geringes Risiko",
|
||||
"uk": "Низький ризик"
|
||||
},
|
||||
"SECURITY$MEDIUM_RISK": {
|
||||
"en": "Risk: Medium",
|
||||
"ja": "リスク: 中",
|
||||
"zh-CN": "风险: 中等",
|
||||
"zh-TW": "風險: 中等",
|
||||
"ko-KR": "위험: 중간",
|
||||
"no": "Risiko: Middels",
|
||||
"it": "Rischio: Medio",
|
||||
"pt": "Risco: Médio",
|
||||
"es": "Riesgo: Medio",
|
||||
"ar": "المخاطر: متوسطة",
|
||||
"fr": "Risque : Moyen",
|
||||
"tr": "Risk: Orta",
|
||||
"de": "Risiko: Mittel",
|
||||
"uk": "Ризик: Середній"
|
||||
"en": "Medium Risk",
|
||||
"ja": "中リスク",
|
||||
"zh-CN": "中等风险",
|
||||
"zh-TW": "中等風險",
|
||||
"ko-KR": "중간 위험",
|
||||
"no": "Middels risiko",
|
||||
"it": "Rischio medio",
|
||||
"pt": "Risco médio",
|
||||
"es": "Riesgo medio",
|
||||
"ar": "مخاطر متوسطة",
|
||||
"fr": "Risque moyen",
|
||||
"tr": "Orta risk",
|
||||
"de": "Mittleres Risiko",
|
||||
"uk": "Середній ризик"
|
||||
},
|
||||
"SECURITY$HIGH_RISK": {
|
||||
"en": "Risk: High",
|
||||
"ja": "リスク: 高",
|
||||
"zh-CN": "风险: 高",
|
||||
"zh-TW": "風險: 高",
|
||||
"ko-KR": "위험: 높음",
|
||||
"no": "Risiko: Høy",
|
||||
"it": "Rischio: Alto",
|
||||
"pt": "Risco: Alto",
|
||||
"es": "Riesgo: Alto",
|
||||
"ar": "المخاطر: عالية",
|
||||
"fr": "Risque : Élevé",
|
||||
"tr": "Risk: Yüksek",
|
||||
"de": "Risiko: Hoch",
|
||||
"uk": "Ризик: Високий"
|
||||
"en": "High Risk",
|
||||
"ja": "高リスク",
|
||||
"zh-CN": "高风险",
|
||||
"zh-TW": "高風險",
|
||||
"ko-KR": "높은 위험",
|
||||
"no": "Høy risiko",
|
||||
"it": "Rischio alto",
|
||||
"pt": "Alto risco",
|
||||
"es": "Riesgo alto",
|
||||
"ar": "مخاطر عالية",
|
||||
"fr": "Risque élevé",
|
||||
"tr": "Yüksek risk",
|
||||
"de": "Hohes Risiko",
|
||||
"uk": "Високий ризик"
|
||||
},
|
||||
"SECURITY$UNKNOWN_RISK": {
|
||||
"en": "Risk: Unknown",
|
||||
"ja": "リスク: 不明",
|
||||
"zh-CN": "风险: 未知",
|
||||
"zh-TW": "風險: 未知",
|
||||
"ko-KR": "위험: 알 수 없음",
|
||||
"no": "Risiko: Ukjent",
|
||||
"it": "Rischio: Sconosciuto",
|
||||
"pt": "Risco: Desconhecido",
|
||||
"es": "Riesgo: Desconocido",
|
||||
"ar": "المخاطر: غير معروفة",
|
||||
"fr": "Risque : Inconnu",
|
||||
"tr": "Risk: Bilinmeyen",
|
||||
"de": "Risiko: Unbekannt",
|
||||
"uk": "Ризик: Невідомий"
|
||||
"en": "Unknown Risk",
|
||||
"ja": "不明なリスク",
|
||||
"zh-CN": "未知风险",
|
||||
"zh-TW": "未知風險",
|
||||
"ko-KR": "알 수 없는 위험",
|
||||
"no": "Ukjent risiko",
|
||||
"it": "Rischio sconosciuto",
|
||||
"pt": "Risco desconhecido",
|
||||
"es": "Riesgo desconocido",
|
||||
"ar": "مخاطر غير معروفة",
|
||||
"fr": "Risque inconnu",
|
||||
"tr": "Bilinmeyen risk",
|
||||
"de": "Unbekanntes Risiko",
|
||||
"uk": "Невідомий ризик"
|
||||
},
|
||||
"FINISH$TASK_COMPLETED_SUCCESSFULLY": {
|
||||
"en": "I believe that the task was **completed successfully**.",
|
||||
@@ -2432,20 +2432,20 @@
|
||||
"uk": "Git налаштування"
|
||||
},
|
||||
"SETTINGS$GIT_SETTINGS_DESCRIPTION": {
|
||||
"en": "Configure the username and email that OpenHands uses to commit changes.",
|
||||
"ja": "OpenHandsがコミットに使用するユーザー名とメールを設定します。",
|
||||
"zh-CN": "配置OpenHands用于提交更改的用户名和电子邮件。",
|
||||
"zh-TW": "配置OpenHands用於提交更改的用戶名和電子郵件。",
|
||||
"ko-KR": "OpenHands가 변경 사항을 커밋할 때 사용하는 사용자 이름과 이메일을 구성합니다.",
|
||||
"de": "Konfigurieren Sie den Benutzernamen und die E-Mail, die OpenHands zum Committen von Änderungen verwendet.",
|
||||
"no": "Konfigurer brukernavnet og e-posten som OpenHands bruker for å committe endringer.",
|
||||
"it": "Configura il nome utente e l'email che OpenHands utilizza per committare le modifiche.",
|
||||
"pt": "Configure o nome de usuário e o email que o OpenHands usa para fazer commits de alterações.",
|
||||
"es": "Configure el nombre de usuario y el correo electrónico que OpenHands utiliza para confirmar cambios.",
|
||||
"ar": "قم بتكوين اسم المستخدم والبريد الإلكتروني الذي يستخدمه OpenHands لارتكاب التغييرات.",
|
||||
"fr": "Configurez le nom d'utilisateur et l'email qu'OpenHands utilise pour valider les modifications.",
|
||||
"tr": "OpenHands'ın değişiklikleri commit etmek için kullandığı kullanıcı adını ve e-postayı yapılandırın.",
|
||||
"uk": "Налаштуйте ім'я користувача та електронну пошту, які OpenHands використовує для фіксації змін."
|
||||
"en": "Configure Git integration settings",
|
||||
"ja": "Git統合設定を構成する",
|
||||
"zh-CN": "配置Git集成设置",
|
||||
"zh-TW": "配置Git整合設定",
|
||||
"ko-KR": "Git 통합 설정 구성",
|
||||
"de": "Git-Integrationseinstellungen konfigurieren",
|
||||
"no": "Konfigurer Git-integrasjonsinnstillinger",
|
||||
"it": "Configura le impostazioni di integrazione Git",
|
||||
"pt": "Configure as configurações de integração Git",
|
||||
"es": "Configure los ajustes de integración Git",
|
||||
"ar": "تكوين إعدادات تكامل Git",
|
||||
"fr": "Configurer les paramètres d'intégration Git",
|
||||
"tr": "Git entegrasyon ayarlarını yapılandırın",
|
||||
"uk": "Налаштуйте параметри інтеграції Git"
|
||||
},
|
||||
"SETTINGS$SOUND_NOTIFICATIONS": {
|
||||
"en": "Sound Notifications",
|
||||
@@ -2520,11 +2520,11 @@
|
||||
"de": "Lösbarkeitsanalyse aktivieren",
|
||||
"no": "Aktiver løsningsanalyse",
|
||||
"it": "Abilita analisi di risolvibilità",
|
||||
"pt": "Ativar análise de solucionabilidade",
|
||||
"es": "Habilitar análisis de solvencia",
|
||||
"pt": "Ativar análise de resolubilidade",
|
||||
"es": "Habilitar análisis de resolubilidad",
|
||||
"ar": "تمكين تحليل القابلية للحل",
|
||||
"fr": "Activer l'analyse de solvabilité",
|
||||
"tr": "Çözünürlük Analizini Etkinleştir",
|
||||
"fr": "Activer l'analyse de résolvabilité",
|
||||
"tr": "Çözülebilirlik analizini etkinleştir",
|
||||
"uk": "Увімкнути аналіз розв'язності"
|
||||
},
|
||||
"SETTINGS$SEARCH_API_KEY": {
|
||||
@@ -5711,22 +5711,6 @@
|
||||
"ja": "このアクションを実行してもよろしいですか?",
|
||||
"uk": "Ви хочете продовжити цю дію?"
|
||||
},
|
||||
"CHAT_INTERFACE$HIGH_RISK_WARNING": {
|
||||
"en": "Review carefully before proceeding.",
|
||||
"zh-CN": "在继续之前请仔细检查。",
|
||||
"de": "Überprüfen Sie sorgfältig, bevor Sie fortfahren.",
|
||||
"zh-TW": "在繼續之前請仔細檢查。",
|
||||
"ko-KR": "계속하기 전에 신중히 검토하세요.",
|
||||
"no": "Gå nøye gjennom før du fortsetter.",
|
||||
"it": "Esamina attentamente prima di procedere.",
|
||||
"pt": "Revise cuidadosamente antes de prosseguir.",
|
||||
"es": "Revise cuidadosamente antes de continuar.",
|
||||
"ar": "يرجى المراجعة بعناية قبل المتابعة.",
|
||||
"fr": "Examinez attentivement avant de continuer.",
|
||||
"tr": "Devam etmeden önce dikkatlice gözden geçirin.",
|
||||
"ja": "続行する前に慎重に確認してください。",
|
||||
"uk": "Уважно перевірте перед продовженням."
|
||||
},
|
||||
"CHAT_INTERFACE$USER_CONFIRMED": {
|
||||
"en": "Confirm the requested action",
|
||||
"de": "Bestätigen Sie die angeforderte Aktion",
|
||||
@@ -5951,6 +5935,70 @@
|
||||
"ja": "不明な送信者",
|
||||
"uk": "Невідомий"
|
||||
},
|
||||
"SECURITY_ANALYZER$UNKNOWN_RISK": {
|
||||
"en": "Unknown Risk",
|
||||
"de": "Unbekanntes Risiko",
|
||||
"zh-CN": "未知风险",
|
||||
"ko-KR": "알 수 없는 위험",
|
||||
"no": "Ukjent risiko",
|
||||
"zh-TW": "未知風險",
|
||||
"it": "Rischio sconosciuto",
|
||||
"pt": "Risco desconhecido",
|
||||
"es": "Riesgo desconocido",
|
||||
"ar": "مخاطر غير معروفة",
|
||||
"fr": "Risque inconnu",
|
||||
"tr": "Bilinmeyen risk",
|
||||
"ja": "不明なリスク",
|
||||
"uk": "Невідомий ризик"
|
||||
},
|
||||
"SECURITY_ANALYZER$LOW_RISK": {
|
||||
"en": "Low Risk",
|
||||
"de": "Niedriges Risiko",
|
||||
"zh-CN": "低风险",
|
||||
"ko-KR": "낮은 위험",
|
||||
"no": "Lav risiko",
|
||||
"zh-TW": "低風險",
|
||||
"it": "Rischio basso",
|
||||
"pt": "Baixo risco",
|
||||
"es": "Riesgo bajo",
|
||||
"ar": "مخاطر منخفضة",
|
||||
"fr": "Risque faible",
|
||||
"tr": "Düşük risk",
|
||||
"ja": "低リスク",
|
||||
"uk": "Низький ризик"
|
||||
},
|
||||
"SECURITY_ANALYZER$MEDIUM_RISK": {
|
||||
"en": "Medium Risk",
|
||||
"de": "Mittleres Risiko",
|
||||
"zh-CN": "中等风险",
|
||||
"ko-KR": "중간 위험",
|
||||
"no": "Middels risiko",
|
||||
"zh-TW": "中等風險",
|
||||
"it": "Rischio medio",
|
||||
"pt": "Risco médio",
|
||||
"es": "Riesgo medio",
|
||||
"ar": "مخاطر متوسطة",
|
||||
"fr": "Risque moyen",
|
||||
"tr": "Orta risk",
|
||||
"ja": "中リスク",
|
||||
"uk": "Середній ризик"
|
||||
},
|
||||
"SECURITY_ANALYZER$HIGH_RISK": {
|
||||
"en": "High Risk",
|
||||
"de": "Hohes Risiko",
|
||||
"zh-CN": "高风险",
|
||||
"ko-KR": "높은 위험",
|
||||
"no": "Høy risiko",
|
||||
"zh-TW": "高風險",
|
||||
"it": "Rischio elevato",
|
||||
"pt": "Alto risco",
|
||||
"es": "Riesgo alto",
|
||||
"ar": "مخاطر عالية",
|
||||
"fr": "Risque élevé",
|
||||
"tr": "Yüksek risk",
|
||||
"ja": "高リスク",
|
||||
"uk": "Високий ризик"
|
||||
},
|
||||
"SETTINGS$MODEL_TOOLTIP": {
|
||||
"en": "Select the language model to use.",
|
||||
"zh-CN": "选择要使用的语言模型",
|
||||
@@ -6111,22 +6159,6 @@
|
||||
"ja": "エージェントのアクションを実行前に確認",
|
||||
"uk": "Очікує підтвердження користувача перед виконанням коду."
|
||||
},
|
||||
"SETTINGS$CONFIRMATION_MODE_LOCK_TOOLTIP": {
|
||||
"en": "The agent is in confirmation mode. It will prompt the user to confirm certain actions when security analyzer policy detected a high-risk action. Click this icon to go to settings tab for more information.",
|
||||
"de": "Der Agent befindet sich im Bestätigungsmodus. Er wird den Benutzer auffordern, bestimmte Aktionen zu bestätigen, wenn die Sicherheitsanalysator-Richtlinie eine risikoreiche Aktion erkannt hat. Weitere Informationen finden Sie auf der Registerkarte Einstellungen.",
|
||||
"zh-CN": "代理处于确认模式。当安全分析器策略检测到高风险操作时,它会提示用户确认某些操作。查看设置选项卡了解更多信息。",
|
||||
"zh-TW": "代理處於確認模式。當安全分析器策略檢測到高風險操作時,它會提示使用者確認某些操作。查看設定選項卡了解更多資訊。",
|
||||
"ko-KR": "에이전트가 확인 모드에 있습니다. 보안 분석기 정책이 고위험 작업을 감지하면 사용자에게 특정 작업을 확인하도록 요청합니다. 자세한 내용은 설정 탭을 확인하세요.",
|
||||
"no": "Agenten er i bekreftelsesmodus. Den vil be brukeren om å bekrefte visse handlinger når sikkerhetsanalysatorpolitikken oppdager en høyrisiko-handling. Sjekk innstillingsfanen for mer informasjon.",
|
||||
"it": "L'agente è in modalità di conferma. Chiederà all'utente di confermare certe azioni quando la politica dell'analizzatore di sicurezza rileva un'azione ad alto rischio. Controlla la scheda impostazioni per maggiori informazioni.",
|
||||
"pt": "O agente está no modo de confirmação. Ele solicitará ao usuário que confirme certas ações quando a política do analisador de segurança detectar uma ação de alto risco. Verifique a aba de configurações para mais informações.",
|
||||
"es": "El agente está en modo de confirmación. Solicitará al usuario que confirme ciertas acciones cuando la política del analizador de seguridad detecte una acción de alto riesgo. Consulte la pestaña de configuración para obtener más información.",
|
||||
"ar": "الوكيل في وضع التأكيد. سيطلب من المستخدم تأكيد إجراءات معينة عندما تكتشف سياسة محلل الأمان إجراءً عالي المخاطر. تحقق من علامة تبويب الإعدادات للحصول على مزيد من المعلومات.",
|
||||
"fr": "L'agent est en mode de confirmation. Il demandera à l'utilisateur de confirmer certaines actions lorsque la politique de l'analyseur de sécurité détecte une action à haut risque. Consultez l'onglet paramètres pour plus d'informations.",
|
||||
"tr": "Ajan onay modunda. Güvenlik analizörü politikası yüksek riskli bir eylem tespit ettiğinde kullanıcıdan belirli eylemleri onaylamasını isteyecek. Daha fazla bilgi için ayarlar sekmesini kontrol edin.",
|
||||
"ja": "エージェントは確認モードです。セキュリティアナライザーポリシーが高リスクアクションを検出した場合、特定のアクションの確認をユーザーに求めます。詳細については設定タブを確認してください。",
|
||||
"uk": "Агент знаходиться в режимі підтвердження. Він попросить користувача підтвердити певні дії, коли політика аналізатора безпеки виявить дію високого ризику. Перевірте вкладку налаштувань для отримання додаткової інформації."
|
||||
},
|
||||
"SETTINGS$AGENT_SELECT_ENABLED": {
|
||||
"en": "Enable Agent Selection - Advanced Users",
|
||||
"zh-CN": "启用智能体选择 - 高级用户",
|
||||
@@ -6175,38 +6207,6 @@
|
||||
"ja": "セキュリティアナライザーを選択…",
|
||||
"uk": "Виберіть аналізатор безпеки…"
|
||||
},
|
||||
"SETTINGS$SECURITY_ANALYZER_TOOLTIP": {
|
||||
"en": "When enabled, the agent will pause and ask for confirmation when it tries to execute high-risk actions",
|
||||
"de": "Wenn aktiviert, pausiert der Agent und fragt nach Bestätigung, wenn er versucht, risikoreiche Aktionen auszuführen",
|
||||
"zh-CN": "启用后,代理在尝试执行高风险操作时会暂停并要求确认",
|
||||
"zh-TW": "啟用後,代理在嘗試執行高風險操作時會暫停並要求確認",
|
||||
"ko-KR": "활성화되면 에이전트가 고위험 작업을 실행하려고 할 때 일시 중지하고 확인을 요청합니다",
|
||||
"no": "Når aktivert, vil agenten pause og be om bekreftelse når den prøver å utføre høyrisiko-handlinger",
|
||||
"it": "Quando abilitato, l'agente si fermerà e chiederà conferma quando tenta di eseguire azioni ad alto rischio",
|
||||
"pt": "Quando ativado, o agente pausará e pedirá confirmação quando tentar executar ações de alto risco",
|
||||
"es": "Cuando está habilitado, el agente se pausará y pedirá confirmación cuando trate de ejecutar acciones de alto riesgo",
|
||||
"ar": "عند التمكين، سيتوقف الوكيل ويطلب التأكيد عندما يحاول تنفيذ إجراءات عالية المخاطر",
|
||||
"fr": "Lorsqu'il est activé, l'agent se mettra en pause et demandera confirmation lorsqu'il tentera d'exécuter des actions à haut risque",
|
||||
"tr": "Etkinleştirildiğinde, ajan yüksek riskli eylemleri gerçekleştirmeye çalıştığında duraklar ve onay ister",
|
||||
"ja": "有効にすると、エージェントは高リスクなアクションを実行しようとする際に一時停止し、確認を求めます",
|
||||
"uk": "Коли увімкнено, агент зупиниться і попросить підтвердження, коли спробує виконати дії високого ризику"
|
||||
},
|
||||
"SETTINGS$SECURITY_ANALYZER_DESCRIPTION": {
|
||||
"en": "The security analyzer will be used in conjunction with confirmation mode. By default, it utilizes LLM-predicted action risk to determine whether to prompt the user for confirmation. If the risk is HIGH, it will prompt the user for confirmation by default.",
|
||||
"de": "Der Sicherheitsanalysator wird in Verbindung mit dem Bestätigungsmodus verwendet. Standardmäßig nutzt er LLM-vorhergesagtes Aktionsrisiko, um zu bestimmen, ob der Benutzer zur Bestätigung aufgefordert werden soll. Wenn das Risiko HOCH ist, wird er standardmäßig zur Bestätigung auffordern.",
|
||||
"zh-CN": "安全分析器将与确认模式结合使用。默认情况下,它利用LLM预测的操作风险来确定是否提示用户确认。如果风险为高,它将默认提示用户确认。",
|
||||
"zh-TW": "安全分析器將與確認模式結合使用。預設情況下,它利用LLM預測的操作風險來確定是否提示用戶確認。如果風險為高,它將預設提示用戶確認。",
|
||||
"ko-KR": "보안 분석기는 확인 모드와 함께 사용됩니다. 기본적으로 LLM이 예측한 작업 위험을 활용하여 사용자에게 확인을 요청할지 결정합니다. 위험이 높으면 기본적으로 사용자에게 확인을 요청합니다.",
|
||||
"no": "Sikkerhetsanalysatoren vil bli brukt i forbindelse med bekreftelsesmodus. Som standard bruker den LLM-forutsagt handlingsrisiko for å bestemme om brukeren skal bli bedt om bekreftelse. Hvis risikoen er HØY, vil den be om bekreftelse som standard.",
|
||||
"it": "L'analizzatore di sicurezza verrà utilizzato insieme alla modalità di conferma. Per impostazione predefinita, utilizza il rischio di azione previsto dall'LLM per determinare se richiedere conferma all'utente. Se il rischio è ALTO, richiederà conferma per impostazione predefinita.",
|
||||
"pt": "O analisador de segurança será usado em conjunto com o modo de confirmação. Por padrão, utiliza o risco de ação previsto pelo LLM para determinar se deve solicitar confirmação ao usuário. Se o risco for ALTO, solicitará confirmação por padrão.",
|
||||
"es": "El analizador de seguridad se utilizará junto con el modo de confirmación. Por defecto, utiliza el riesgo de acción predicho por LLM para determinar si solicitar confirmación al usuario. Si el riesgo es ALTO, solicitará confirmación por defecto.",
|
||||
"ar": "سيتم استخدام محلل الأمان بالتزامن مع وضع التأكيد. افتراضياً، يستخدم مخاطر الإجراء المتوقعة من LLM لتحديد ما إذا كان يجب مطالبة المستخدم بالتأكيد. إذا كان الخطر عالياً، فسيطالب بالتأكيد افتراضياً.",
|
||||
"fr": "L'analyseur de sécurité sera utilisé en conjonction avec le mode de confirmation. Par défaut, il utilise le risque d'action prédit par LLM pour déterminer s'il faut demander confirmation à l'utilisateur. Si le risque est ÉLEVÉ, il demandera confirmation par défaut.",
|
||||
"tr": "Güvenlik analizörü onay modu ile birlikte kullanılacaktır. Varsayılan olarak, kullanıcıdan onay istenip istenmeyeceğini belirlemek için LLM tarafından tahmin edilen eylem riskini kullanır. Risk YÜKSEK ise, varsayılan olarak kullanıcıdan onay isteyecektir.",
|
||||
"ja": "セキュリティアナライザーは確認モードと組み合わせて使用されます。デフォルトでは、LLMが予測したアクションリスクを利用して、ユーザーに確認を求めるかどうかを決定します。リスクが高い場合、デフォルトでユーザーに確認を求めます。",
|
||||
"uk": "Аналізатор безпеки буде використовуватися разом з режимом підтвердження. За замовчуванням він використовує передбачений LLM ризик дії для визначення, чи потрібно запитувати підтвердження у користувача. Якщо ризик ВИСОКИЙ, він запитуватиме підтвердження за замовчуванням."
|
||||
},
|
||||
"SETTINGS$DONT_KNOW_API_KEY": {
|
||||
"en": "Don't know your API key?",
|
||||
"ja": "APIキーがわかりませんか?",
|
||||
@@ -12495,6 +12495,38 @@
|
||||
"de": "Fehler beim Laden des Microagent-Inhalts.",
|
||||
"uk": "Помилка під час завантаження вмісту мікроагента."
|
||||
},
|
||||
"SETTINGS$MCP_ERROR_ENV_INVALID_FORMAT": {
|
||||
"en": "Environment variables must follow KEY=value format",
|
||||
"ja": "Environment variables must follow KEY=value format",
|
||||
"zh-CN": "Environment variables must follow KEY=value format",
|
||||
"zh-TW": "Environment variables must follow KEY=value format",
|
||||
"ko-KR": "Environment variables must follow KEY=value format",
|
||||
"no": "Environment variables must follow KEY=value format",
|
||||
"it": "Environment variables must follow KEY=value format",
|
||||
"pt": "Environment variables must follow KEY=value format",
|
||||
"es": "Environment variables must follow KEY=value format",
|
||||
"ar": "Environment variables must follow KEY=value format",
|
||||
"fr": "Environment variables must follow KEY=value format",
|
||||
"tr": "Environment variables must follow KEY=value format",
|
||||
"de": "Environment variables must follow KEY=value format",
|
||||
"uk": "Environment variables must follow KEY=value format"
|
||||
},
|
||||
"SETTINGS$MCP_ERROR_URL_DUPLICATE": {
|
||||
"en": "A server with this URL already exists for the selected type",
|
||||
"ja": "A server with this URL already exists for the selected type",
|
||||
"zh-CN": "A server with this URL already exists for the selected type",
|
||||
"zh-TW": "A server with this URL already exists for the selected type",
|
||||
"ko-KR": "A server with this URL already exists for the selected type",
|
||||
"no": "A server with this URL already exists for the selected type",
|
||||
"it": "A server with this URL already exists for the selected type",
|
||||
"pt": "A server with this URL already exists for the selected type",
|
||||
"es": "A server with this URL already exists for the selected type",
|
||||
"ar": "A server with this URL already exists for the selected type",
|
||||
"fr": "A server with this URL already exists for the selected type",
|
||||
"tr": "A server with this URL already exists for the selected type",
|
||||
"de": "A server with this URL already exists for the selected type",
|
||||
"uk": "A server with this URL already exists for the selected type"
|
||||
},
|
||||
"SETTINGS$MCP_SERVER_TYPE_SSE": {
|
||||
"en": "SSE",
|
||||
"ja": "SSE",
|
||||
@@ -12671,38 +12703,6 @@
|
||||
"de": "Befehl darf keine Leerzeichen enthalten",
|
||||
"uk": "Команда не може містити пробіли"
|
||||
},
|
||||
"SETTINGS$MCP_ERROR_URL_DUPLICATE": {
|
||||
"en": "A server with this URL already exists for the selected type",
|
||||
"ja": "A server with this URL already exists for the selected type",
|
||||
"zh-CN": "A server with this URL already exists for the selected type",
|
||||
"zh-TW": "A server with this URL already exists for the selected type",
|
||||
"ko-KR": "A server with this URL already exists for the selected type",
|
||||
"no": "A server with this URL already exists for the selected type",
|
||||
"it": "A server with this URL already exists for the selected type",
|
||||
"pt": "A server with this URL already exists for the selected type",
|
||||
"es": "A server with this URL already exists for the selected type",
|
||||
"ar": "A server with this URL already exists for the selected type",
|
||||
"fr": "A server with this URL already exists for the selected type",
|
||||
"tr": "A server with this URL already exists for the selected type",
|
||||
"de": "A server with this URL already exists for the selected type",
|
||||
"uk": "A server with this URL already exists for the selected type"
|
||||
},
|
||||
"SETTINGS$MCP_ERROR_ENV_INVALID_FORMAT": {
|
||||
"en": "Environment variables must follow KEY=value format",
|
||||
"ja": "Environment variables must follow KEY=value format",
|
||||
"zh-CN": "Environment variables must follow KEY=value format",
|
||||
"zh-TW": "Environment variables must follow KEY=value format",
|
||||
"ko-KR": "Environment variables must follow KEY=value format",
|
||||
"no": "Environment variables must follow KEY=value format",
|
||||
"it": "Environment variables must follow KEY=value format",
|
||||
"pt": "Environment variables must follow KEY=value format",
|
||||
"es": "Environment variables must follow KEY=value format",
|
||||
"ar": "Environment variables must follow KEY=value format",
|
||||
"fr": "Environment variables must follow KEY=value format",
|
||||
"tr": "Environment variables must follow KEY=value format",
|
||||
"de": "Environment variables must follow KEY=value format",
|
||||
"uk": "Environment variables must follow KEY=value format"
|
||||
},
|
||||
"SETTINGS$MCP_SERVER_TYPE": {
|
||||
"en": "Server Type",
|
||||
"ja": "サーバータイプ",
|
||||
@@ -13022,69 +13022,5 @@
|
||||
"tr": "Bir şeyler ters gitti. Mikro ajanı tekrar başlatmayı deneyin.",
|
||||
"de": "Etwas ist schiefgelaufen. Versuchen Sie, den Microagenten erneut zu starten.",
|
||||
"uk": "Щось пішло не так. Спробуйте ініціювати мікроагента ще раз."
|
||||
},
|
||||
"SETTINGS$SECURITY_ANALYZER_LLM_DEFAULT": {
|
||||
"en": "LLM Analyzer (Default)",
|
||||
"ja": "LLMアナライザー(デフォルト)",
|
||||
"zh-CN": "LLM 分析器(默认)",
|
||||
"zh-TW": "LLM 分析器(預設)",
|
||||
"ko-KR": "LLM 분석기(기본)",
|
||||
"no": "LLM-analysator (standard)",
|
||||
"it": "Analizzatore LLM (Predefinito)",
|
||||
"pt": "Analisador LLM (Padrão)",
|
||||
"es": "Analizador LLM (Predeterminado)",
|
||||
"ar": "محلل LLM (افتراضي)",
|
||||
"fr": "Analyseur LLM (Par défaut)",
|
||||
"tr": "LLM Analizörü (Varsayılan)",
|
||||
"de": "LLM-Analysator (Standard)",
|
||||
"uk": "Аналізатор LLM (За замовчуванням)"
|
||||
},
|
||||
"SETTINGS$SECURITY_ANALYZER_NONE": {
|
||||
"en": "None (Ask for every command)",
|
||||
"ja": "なし(すべてのコマンドで確認)",
|
||||
"zh-CN": "无(每条命令都询问)",
|
||||
"zh-TW": "無(每個指令都詢問)",
|
||||
"ko-KR": "없음(모든 명령마다 확인)",
|
||||
"no": "Ingen (Spør for hver kommando)",
|
||||
"it": "Nessuno (Chiedi per ogni comando)",
|
||||
"pt": "Nenhum (Perguntar para cada comando)",
|
||||
"es": "Ninguno (Preguntar para cada comando)",
|
||||
"ar": "لا شيء (اسأل عن كل أمر)",
|
||||
"fr": "Aucun (Demander pour chaque commande)",
|
||||
"tr": "Yok (Her komutta sor)",
|
||||
"de": "Keine (Bei jedem Befehl nachfragen)",
|
||||
"uk": "Немає (Запитувати для кожної команди)"
|
||||
},
|
||||
"SETTINGS$SECURITY_ANALYZER_INVARIANT": {
|
||||
"en": "Invariant Rule-based Analyzer",
|
||||
"ja": "不変ルールベース分析器",
|
||||
"zh-CN": "Invariant 规则分析器",
|
||||
"zh-TW": "Invariant 規則式分析器",
|
||||
"ko-KR": "Invariant 규칙 기반 분석기",
|
||||
"no": "Invariant regelbasert analysator",
|
||||
"it": "Analizzatore basato su regole Invariant",
|
||||
"pt": "Analisador baseado em regras Invariant",
|
||||
"es": "Analizador basado en reglas Invariant",
|
||||
"ar": "محلل قائم على القواعد Invariant",
|
||||
"fr": "Analyseur à base de règles Invariant",
|
||||
"tr": "Invariant Kural Tabanlı Analizör",
|
||||
"de": "Invariant regelbasierter Analysator",
|
||||
"uk": "Аналізатор на основі правил Invariant"
|
||||
},
|
||||
"COMMON$HIGH_RISK": {
|
||||
"en": "High Risk",
|
||||
"ja": "高リスク",
|
||||
"zh-CN": "高风险",
|
||||
"zh-TW": "高風險",
|
||||
"ko-KR": "고위험",
|
||||
"no": "Høy risiko",
|
||||
"it": "Alto rischio",
|
||||
"pt": "Alto risco",
|
||||
"es": "Alto riesgo",
|
||||
"ar": "مخاطر عالية",
|
||||
"fr": "Risque élevé",
|
||||
"tr": "Yüksek Risk",
|
||||
"de": "Hohes Risiko",
|
||||
"uk": "Високий ризик"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 14C11.4477 14 11 13.5523 11 13V10C11 9.44772 11.4477 9 12 9C12.5523 9 13 9.44772 13 10V13C13 13.5523 12.5523 14 12 14Z" fill="currentColor"/>
|
||||
<path d="M10.5 16.5C10.5 15.6716 11.1716 15 12 15C12.8284 15 13.5 15.6716 13.5 16.5C13.5 17.3284 12.8284 18 12 18C11.1716 18 10.5 17.3284 10.5 16.5Z" fill="currentColor"/>
|
||||
<path d="M10.2301 3.2156C10.98 1.79093 13.02 1.79092 13.7698 3.2156L22.1135 19.0685C22.8144 20.4003 21.8486 22 20.3436 22H3.65635C2.15133 22 1.18556 20.4003 1.88651 19.0685L10.2301 3.2156ZM20.3436 20L12 4.1471L3.65635 20L20.3436 20Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 692 B |
@@ -123,7 +123,7 @@ const openHandsHandlers = [
|
||||
),
|
||||
|
||||
http.get("/api/options/security-analyzers", async () =>
|
||||
HttpResponse.json(["llm", "none"]),
|
||||
HttpResponse.json(["mock-invariant"]),
|
||||
),
|
||||
|
||||
http.post("http://localhost:3001/api/submit-feedback", async () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useDisclosure } from "@heroui/react";
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useDispatch } from "react-redux";
|
||||
@@ -17,7 +18,7 @@ import {
|
||||
Orientation,
|
||||
ResizablePanel,
|
||||
} from "#/components/layout/resizable-panel";
|
||||
|
||||
import Security from "#/components/shared/modals/security/security";
|
||||
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
import { useSettings } from "#/hooks/query/use-settings";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
@@ -82,6 +83,12 @@ function AppContent() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const {
|
||||
isOpen: securityModalIsOpen,
|
||||
onOpen: onSecurityModalOpen,
|
||||
onOpenChange: onSecurityModalOpenChange,
|
||||
} = useDisclosure();
|
||||
|
||||
function renderMain() {
|
||||
if (width <= 1024) {
|
||||
return (
|
||||
@@ -99,7 +106,7 @@ function AppContent() {
|
||||
<ResizablePanel
|
||||
orientation={Orientation.HORIZONTAL}
|
||||
className="grow h-full min-h-0 min-w-0"
|
||||
initialSize={564}
|
||||
initialSize={500}
|
||||
firstClassName="rounded-xl overflow-hidden border border-neutral-600 bg-base-secondary"
|
||||
secondClassName="flex flex-col overflow-hidden"
|
||||
firstChild={<ChatInterface />}
|
||||
@@ -115,7 +122,17 @@ function AppContent() {
|
||||
<div data-testid="app-route" className="flex flex-col h-full gap-3">
|
||||
<div className="flex h-full overflow-auto">{renderMain()}</div>
|
||||
|
||||
<Controls showSecurityLock={!!settings?.CONFIRMATION_MODE} />
|
||||
<Controls
|
||||
setSecurityOpen={onSecurityModalOpen}
|
||||
showSecurityLock={!!settings?.SECURITY_ANALYZER}
|
||||
/>
|
||||
{settings && (
|
||||
<Security
|
||||
isOpen={securityModalIsOpen}
|
||||
onOpenChange={onSecurityModalOpenChange}
|
||||
securityAnalyzer={settings.SECURITY_ANALYZER}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</EventHandler>
|
||||
</ConversationSubscriptionsProvider>
|
||||
|
||||
@@ -8,8 +8,6 @@ import { useSettings } from "#/hooks/query/use-settings";
|
||||
import { hasAdvancedSettingsSet } from "#/utils/has-advanced-settings-set";
|
||||
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
|
||||
import { SettingsSwitch } from "#/components/features/settings/settings-switch";
|
||||
import { TooltipButton } from "#/components/shared/buttons/tooltip-button";
|
||||
import QuestionCircleIcon from "#/icons/question-circle.svg?react";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { SettingsInput } from "#/components/features/settings/settings-input";
|
||||
import { HelpLink } from "#/components/features/settings/help-link";
|
||||
@@ -38,6 +36,8 @@ function LlmSettingsScreen() {
|
||||
const { data: config } = useConfig();
|
||||
|
||||
const [view, setView] = React.useState<"basic" | "advanced">("basic");
|
||||
const [securityAnalyzerInputIsVisible, setSecurityAnalyzerInputIsVisible] =
|
||||
React.useState(false);
|
||||
|
||||
const [dirtyInputs, setDirtyInputs] = React.useState({
|
||||
model: false,
|
||||
@@ -55,19 +55,6 @@ function LlmSettingsScreen() {
|
||||
string | null
|
||||
>(null);
|
||||
|
||||
// Track confirmation mode state to control security analyzer visibility
|
||||
const [confirmationModeEnabled, setConfirmationModeEnabled] = React.useState(
|
||||
settings?.CONFIRMATION_MODE ?? DEFAULT_SETTINGS.CONFIRMATION_MODE,
|
||||
);
|
||||
|
||||
// Track selected security analyzer for form submission
|
||||
const [selectedSecurityAnalyzer, setSelectedSecurityAnalyzer] =
|
||||
React.useState(
|
||||
settings?.SECURITY_ANALYZER === null
|
||||
? "none"
|
||||
: (settings?.SECURITY_ANALYZER ?? DEFAULT_SETTINGS.SECURITY_ANALYZER),
|
||||
);
|
||||
|
||||
const modelsAndProviders = organizeModelsAndProviders(
|
||||
resources?.models || [],
|
||||
);
|
||||
@@ -87,6 +74,7 @@ function LlmSettingsScreen() {
|
||||
};
|
||||
|
||||
const userSettingsIsAdvanced = determineWhetherToToggleAdvancedSettings();
|
||||
if (settings) setSecurityAnalyzerInputIsVisible(settings.CONFIRMATION_MODE);
|
||||
|
||||
if (userSettingsIsAdvanced) setView("advanced");
|
||||
else setView("basic");
|
||||
@@ -99,20 +87,6 @@ function LlmSettingsScreen() {
|
||||
}
|
||||
}, [settings?.LLM_MODEL]);
|
||||
|
||||
// Update confirmation mode state when settings change
|
||||
React.useEffect(() => {
|
||||
if (settings?.CONFIRMATION_MODE !== undefined) {
|
||||
setConfirmationModeEnabled(settings.CONFIRMATION_MODE);
|
||||
}
|
||||
}, [settings?.CONFIRMATION_MODE]);
|
||||
|
||||
// Update selected security analyzer state when settings change
|
||||
React.useEffect(() => {
|
||||
if (settings?.SECURITY_ANALYZER !== undefined) {
|
||||
setSelectedSecurityAnalyzer(settings.SECURITY_ANALYZER || "none");
|
||||
}
|
||||
}, [settings?.SECURITY_ANALYZER]);
|
||||
|
||||
const handleSuccessfulMutation = () => {
|
||||
displaySuccessToast(t(I18nKey.SETTINGS$SAVED_WARNING));
|
||||
setDirtyInputs({
|
||||
@@ -140,11 +114,6 @@ function LlmSettingsScreen() {
|
||||
const model = formData.get("llm-model-input")?.toString();
|
||||
const apiKey = formData.get("llm-api-key-input")?.toString();
|
||||
const searchApiKey = formData.get("search-api-key-input")?.toString();
|
||||
const confirmationMode =
|
||||
formData.get("enable-confirmation-mode-switch")?.toString() === "on";
|
||||
const securityAnalyzer = formData
|
||||
.get("security-analyzer-input")
|
||||
?.toString();
|
||||
|
||||
const fullLlmModel = provider && model && `${provider}/${model}`;
|
||||
|
||||
@@ -153,15 +122,12 @@ function LlmSettingsScreen() {
|
||||
LLM_MODEL: fullLlmModel,
|
||||
llm_api_key: apiKey || null,
|
||||
SEARCH_API_KEY: searchApiKey || "",
|
||||
CONFIRMATION_MODE: confirmationMode,
|
||||
SECURITY_ANALYZER:
|
||||
securityAnalyzer === "none"
|
||||
? null
|
||||
: securityAnalyzer || DEFAULT_SETTINGS.SECURITY_ANALYZER,
|
||||
|
||||
// reset advanced settings
|
||||
LLM_BASE_URL: DEFAULT_SETTINGS.LLM_BASE_URL,
|
||||
AGENT: DEFAULT_SETTINGS.AGENT,
|
||||
CONFIRMATION_MODE: DEFAULT_SETTINGS.CONFIRMATION_MODE,
|
||||
SECURITY_ANALYZER: DEFAULT_SETTINGS.SECURITY_ANALYZER,
|
||||
ENABLE_DEFAULT_CONDENSER: DEFAULT_SETTINGS.ENABLE_DEFAULT_CONDENSER,
|
||||
},
|
||||
{
|
||||
@@ -194,10 +160,7 @@ function LlmSettingsScreen() {
|
||||
AGENT: agent,
|
||||
CONFIRMATION_MODE: confirmationMode,
|
||||
ENABLE_DEFAULT_CONDENSER: enableDefaultCondenser,
|
||||
SECURITY_ANALYZER:
|
||||
securityAnalyzer === "none"
|
||||
? null
|
||||
: securityAnalyzer || DEFAULT_SETTINGS.SECURITY_ANALYZER,
|
||||
SECURITY_ANALYZER: confirmationMode ? securityAnalyzer : undefined,
|
||||
},
|
||||
{
|
||||
onSuccess: handleSuccessfulMutation,
|
||||
@@ -212,6 +175,7 @@ function LlmSettingsScreen() {
|
||||
};
|
||||
|
||||
const handleToggleAdvancedSettings = (isToggled: boolean) => {
|
||||
setSecurityAnalyzerInputIsVisible(!!settings?.CONFIRMATION_MODE);
|
||||
setView(isToggled ? "advanced" : "basic");
|
||||
setDirtyInputs({
|
||||
model: false,
|
||||
@@ -282,21 +246,12 @@ function LlmSettingsScreen() {
|
||||
};
|
||||
|
||||
const handleConfirmationModeIsDirty = (isToggled: boolean) => {
|
||||
setSecurityAnalyzerInputIsVisible(isToggled);
|
||||
const confirmationModeIsDirty = isToggled !== settings?.CONFIRMATION_MODE;
|
||||
setDirtyInputs((prev) => ({
|
||||
...prev,
|
||||
confirmationMode: confirmationModeIsDirty,
|
||||
}));
|
||||
setConfirmationModeEnabled(isToggled);
|
||||
|
||||
// When confirmation mode is enabled, set default security analyzer to "llm" if not already set
|
||||
if (isToggled && !selectedSecurityAnalyzer) {
|
||||
setSelectedSecurityAnalyzer(DEFAULT_SETTINGS.SECURITY_ANALYZER);
|
||||
setDirtyInputs((prev) => ({
|
||||
...prev,
|
||||
securityAnalyzer: true,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnableDefaultCondenserIsDirty = (isToggled: boolean) => {
|
||||
@@ -319,47 +274,6 @@ function LlmSettingsScreen() {
|
||||
|
||||
const formIsDirty = Object.values(dirtyInputs).some((isDirty) => isDirty);
|
||||
|
||||
const getSecurityAnalyzerOptions = () => {
|
||||
const analyzers = resources?.securityAnalyzers || [];
|
||||
const orderedItems = [];
|
||||
|
||||
// Add LLM analyzer first
|
||||
if (analyzers.includes("llm")) {
|
||||
orderedItems.push({
|
||||
key: "llm",
|
||||
label: t(I18nKey.SETTINGS$SECURITY_ANALYZER_LLM_DEFAULT),
|
||||
});
|
||||
}
|
||||
|
||||
// Add None option second
|
||||
orderedItems.push({
|
||||
key: "none",
|
||||
label: t(I18nKey.SETTINGS$SECURITY_ANALYZER_NONE),
|
||||
});
|
||||
|
||||
// Add Invariant analyzer third
|
||||
if (analyzers.includes("invariant")) {
|
||||
orderedItems.push({
|
||||
key: "invariant",
|
||||
label: t(I18nKey.SETTINGS$SECURITY_ANALYZER_INVARIANT),
|
||||
});
|
||||
}
|
||||
|
||||
// Add any other analyzers that might exist
|
||||
analyzers.forEach((analyzer) => {
|
||||
if (!["llm", "invariant", "none"].includes(analyzer)) {
|
||||
// For unknown analyzers, use the analyzer name as fallback
|
||||
// In the future, add specific i18n keys for new analyzers
|
||||
orderedItems.push({
|
||||
key: analyzer,
|
||||
label: analyzer, // TODO: Add i18n support for new analyzers
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return orderedItems;
|
||||
};
|
||||
|
||||
if (!settings || isFetching) return <LlmSettingsInputsSkeleton />;
|
||||
|
||||
return (
|
||||
@@ -538,7 +452,7 @@ function LlmSettingsScreen() {
|
||||
items={
|
||||
resources?.agents.map((agent) => ({
|
||||
key: agent,
|
||||
label: agent, // TODO: Add i18n support for agent names
|
||||
label: agent,
|
||||
})) || []
|
||||
}
|
||||
defaultSelectedKey={settings.AGENT}
|
||||
@@ -573,67 +487,39 @@ function LlmSettingsScreen() {
|
||||
>
|
||||
{t(I18nKey.SETTINGS$ENABLE_MEMORY_CONDENSATION)}
|
||||
</SettingsSwitch>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Confirmation mode and security analyzer - always visible */}
|
||||
<div className="flex items-center gap-2">
|
||||
<SettingsSwitch
|
||||
testId="enable-confirmation-mode-switch"
|
||||
name="enable-confirmation-mode-switch"
|
||||
onToggle={handleConfirmationModeIsDirty}
|
||||
defaultIsToggled={settings.CONFIRMATION_MODE}
|
||||
isBeta
|
||||
>
|
||||
{t(I18nKey.SETTINGS$CONFIRMATION_MODE)}
|
||||
</SettingsSwitch>
|
||||
<TooltipButton
|
||||
tooltip={t(I18nKey.SETTINGS$CONFIRMATION_MODE_TOOLTIP)}
|
||||
ariaLabel={t(I18nKey.SETTINGS$CONFIRMATION_MODE)}
|
||||
className="text-[#9099AC] hover:text-white cursor-help"
|
||||
>
|
||||
<QuestionCircleIcon width={16} height={16} />
|
||||
</TooltipButton>
|
||||
</div>
|
||||
<SettingsSwitch
|
||||
testId="enable-confirmation-mode-switch"
|
||||
name="enable-confirmation-mode-switch"
|
||||
onToggle={handleConfirmationModeIsDirty}
|
||||
defaultIsToggled={settings.CONFIRMATION_MODE}
|
||||
isBeta
|
||||
>
|
||||
{t(I18nKey.SETTINGS$CONFIRMATION_MODE)}
|
||||
</SettingsSwitch>
|
||||
|
||||
{confirmationModeEnabled && (
|
||||
<>
|
||||
<div className="w-full max-w-[680px]">
|
||||
{securityAnalyzerInputIsVisible && (
|
||||
<SettingsDropdownInput
|
||||
testId="security-analyzer-input"
|
||||
name="security-analyzer-display"
|
||||
name="security-analyzer-input"
|
||||
label={t(I18nKey.SETTINGS$SECURITY_ANALYZER)}
|
||||
items={getSecurityAnalyzerOptions()}
|
||||
items={
|
||||
resources?.securityAnalyzers.map((analyzer) => ({
|
||||
key: analyzer,
|
||||
label: analyzer,
|
||||
})) || []
|
||||
}
|
||||
placeholder={t(
|
||||
I18nKey.SETTINGS$SECURITY_ANALYZER_PLACEHOLDER,
|
||||
)}
|
||||
selectedKey={selectedSecurityAnalyzer || "none"}
|
||||
isClearable={false}
|
||||
onSelectionChange={(key) => {
|
||||
const newValue = key?.toString() || "";
|
||||
setSelectedSecurityAnalyzer(newValue);
|
||||
handleSecurityAnalyzerIsDirty(newValue);
|
||||
}}
|
||||
onInputChange={(value) => {
|
||||
// Handle when input is cleared
|
||||
if (!value) {
|
||||
setSelectedSecurityAnalyzer("");
|
||||
handleSecurityAnalyzerIsDirty("");
|
||||
}
|
||||
}}
|
||||
wrapperClassName="w-full"
|
||||
defaultSelectedKey={settings.SECURITY_ANALYZER}
|
||||
isClearable
|
||||
showOptionalTag
|
||||
onInputChange={handleSecurityAnalyzerIsDirty}
|
||||
wrapperClassName="w-full max-w-[680px]"
|
||||
/>
|
||||
{/* Hidden input to store the actual key value for form submission */}
|
||||
<input
|
||||
type="hidden"
|
||||
name="security-analyzer-input"
|
||||
value={selectedSecurityAnalyzer || ""}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-tertiary-alt max-w-[680px]">
|
||||
{t(I18nKey.SETTINGS$SECURITY_ANALYZER_DESCRIPTION)}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ export const DEFAULT_SETTINGS: Settings = {
|
||||
LLM_API_KEY_SET: false,
|
||||
SEARCH_API_KEY_SET: false,
|
||||
CONFIRMATION_MODE: false,
|
||||
SECURITY_ANALYZER: "llm",
|
||||
SECURITY_ANALYZER: "",
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR: 1,
|
||||
PROVIDER_TOKENS_SET: {},
|
||||
ENABLE_DEFAULT_CONDENSER: true,
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
export const eventMessageSlice = createSlice({
|
||||
name: "eventMessage",
|
||||
initialState: {
|
||||
submittedEventIds: [] as number[], // Avoid the flashing issue of the confirmation buttons
|
||||
},
|
||||
reducers: {
|
||||
addSubmittedEventId: (state, action) => {
|
||||
state.submittedEventIds.push(action.payload);
|
||||
},
|
||||
removeSubmittedEventId: (state, action) => {
|
||||
state.submittedEventIds = state.submittedEventIds.filter(
|
||||
(id) => id !== action.payload,
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { addSubmittedEventId, removeSubmittedEventId } =
|
||||
eventMessageSlice.actions;
|
||||
|
||||
export default eventMessageSlice.reducer;
|
||||
@@ -10,7 +10,6 @@ import securityAnalyzerReducer from "./state/security-analyzer-slice";
|
||||
import statusReducer from "./state/status-slice";
|
||||
import metricsReducer from "./state/metrics-slice";
|
||||
import microagentManagementReducer from "./state/microagent-management-slice";
|
||||
import eventMessageReducer from "./state/event-message-slice";
|
||||
|
||||
export const rootReducer = combineReducers({
|
||||
fileState: fileStateReducer,
|
||||
@@ -24,7 +23,6 @@ export const rootReducer = combineReducers({
|
||||
status: statusReducer,
|
||||
metrics: metricsReducer,
|
||||
microagentManagement: microagentManagementReducer,
|
||||
eventMessage: eventMessageReducer,
|
||||
});
|
||||
|
||||
const store = configureStore({
|
||||
|
||||
@@ -43,7 +43,7 @@ export type Settings = {
|
||||
LLM_API_KEY_SET: boolean;
|
||||
SEARCH_API_KEY_SET: boolean;
|
||||
CONFIRMATION_MODE: boolean;
|
||||
SECURITY_ANALYZER: string | null;
|
||||
SECURITY_ANALYZER: string;
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR: number | null;
|
||||
PROVIDER_TOKENS_SET: Partial<Record<Provider, string | null>>;
|
||||
ENABLE_DEFAULT_CONDENSER: boolean;
|
||||
@@ -70,7 +70,7 @@ export type ApiSettings = {
|
||||
llm_api_key_set: boolean;
|
||||
search_api_key_set: boolean;
|
||||
confirmation_mode: boolean;
|
||||
security_analyzer: string | null;
|
||||
security_analyzer: string;
|
||||
remote_runtime_resource_factor: number | null;
|
||||
enable_default_condenser: boolean;
|
||||
enable_sound_notifications: boolean;
|
||||
|
||||
@@ -3,4 +3,7 @@ import { Settings } from "#/types/settings";
|
||||
|
||||
export const hasAdvancedSettingsSet = (settings: Partial<Settings>): boolean =>
|
||||
Object.keys(settings).length > 0 &&
|
||||
(!!settings.LLM_BASE_URL || settings.AGENT !== DEFAULT_SETTINGS.AGENT);
|
||||
(!!settings.LLM_BASE_URL ||
|
||||
settings.AGENT !== DEFAULT_SETTINGS.AGENT ||
|
||||
settings.CONFIRMATION_MODE ||
|
||||
!!settings.SECURITY_ANALYZER);
|
||||
|
||||
@@ -19,7 +19,6 @@ from openhands.agenthub.codeact_agent.tools import (
|
||||
create_cmd_run_tool,
|
||||
create_str_replace_editor_tool,
|
||||
)
|
||||
from openhands.agenthub.codeact_agent.tools.security_utils import RISK_LEVELS
|
||||
from openhands.core.exceptions import (
|
||||
FunctionCallNotExistsError,
|
||||
FunctionCallValidationError,
|
||||
@@ -27,7 +26,6 @@ from openhands.core.exceptions import (
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.events.action import (
|
||||
Action,
|
||||
ActionSecurityRisk,
|
||||
AgentDelegateAction,
|
||||
AgentFinishAction,
|
||||
AgentThinkAction,
|
||||
@@ -56,20 +54,6 @@ def combine_thought(action: Action, thought: str) -> Action:
|
||||
return action
|
||||
|
||||
|
||||
def set_security_risk(action: Action, arguments: dict) -> None:
|
||||
"""Set the security risk level for the action."""
|
||||
|
||||
# Set security_risk attribute if provided
|
||||
if 'security_risk' in arguments:
|
||||
if arguments['security_risk'] in RISK_LEVELS:
|
||||
if hasattr(action, 'security_risk'):
|
||||
action.security_risk = getattr(
|
||||
ActionSecurityRisk, arguments['security_risk']
|
||||
)
|
||||
else:
|
||||
logger.warning(f'Invalid security_risk value: {arguments["security_risk"]}')
|
||||
|
||||
|
||||
def response_to_actions(
|
||||
response: ModelResponse, mcp_tool_names: list[str] | None = None
|
||||
) -> list[Action]:
|
||||
@@ -119,7 +103,6 @@ def response_to_actions(
|
||||
raise FunctionCallValidationError(
|
||||
f"Invalid float passed to 'timeout' argument: {arguments['timeout']}"
|
||||
) from e
|
||||
set_security_risk(action, arguments)
|
||||
|
||||
# ================================================
|
||||
# IPythonTool (Jupyter)
|
||||
@@ -130,11 +113,6 @@ def response_to_actions(
|
||||
f'Missing required argument "code" in tool call {tool_call.function.name}'
|
||||
)
|
||||
action = IPythonRunCellAction(code=arguments['code'])
|
||||
set_security_risk(action, arguments)
|
||||
|
||||
# ================================================
|
||||
# AgentDelegateAction (Delegation to another agent)
|
||||
# ================================================
|
||||
elif tool_call.function.name == 'delegate_to_browsing_agent':
|
||||
action = AgentDelegateAction(
|
||||
agent='BrowsingAgent',
|
||||
@@ -200,7 +178,7 @@ def response_to_actions(
|
||||
other_kwargs.pop('view_range')
|
||||
|
||||
# Filter out unexpected arguments
|
||||
valid_kwargs_for_editor = {}
|
||||
valid_kwargs = {}
|
||||
# Get valid parameters from the str_replace_editor tool definition
|
||||
str_replace_editor_tool = create_str_replace_editor_tool()
|
||||
valid_params = set(
|
||||
@@ -208,12 +186,9 @@ def response_to_actions(
|
||||
'properties'
|
||||
].keys()
|
||||
)
|
||||
|
||||
for key, value in other_kwargs.items():
|
||||
if key in valid_params:
|
||||
# security_risk is valid but should NOT be part of editor kwargs
|
||||
if key != 'security_risk':
|
||||
valid_kwargs_for_editor[key] = value
|
||||
valid_kwargs[key] = value
|
||||
else:
|
||||
raise FunctionCallValidationError(
|
||||
f'Unexpected argument {key} in tool call {tool_call.function.name}. Allowed arguments are: {valid_params}'
|
||||
@@ -223,10 +198,8 @@ def response_to_actions(
|
||||
path=path,
|
||||
command=command,
|
||||
impl_source=FileEditSource.OH_ACI,
|
||||
**valid_kwargs_for_editor,
|
||||
**valid_kwargs,
|
||||
)
|
||||
|
||||
set_security_risk(action, arguments)
|
||||
# ================================================
|
||||
# AgentThinkAction
|
||||
# ================================================
|
||||
@@ -248,7 +221,6 @@ def response_to_actions(
|
||||
f'Missing required argument "code" in tool call {tool_call.function.name}'
|
||||
)
|
||||
action = BrowseInteractiveAction(browser_actions=arguments['code'])
|
||||
set_security_risk(action, arguments)
|
||||
|
||||
# ================================================
|
||||
# TaskTrackingAction
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# 🔐 Security Risk Policy
|
||||
When using tools that support the security_risk parameter, assess the safety risk of your actions:
|
||||
|
||||
{% if cli_mode %}
|
||||
- **LOW**: Safe, read-only actions.
|
||||
- Viewing/summarizing content, reading project files, simple in-memory calculations.
|
||||
- **MEDIUM**: Project-scoped edits or execution.
|
||||
- Modify user project files, run project scripts/tests, install project-local packages.
|
||||
- **HIGH**: System-level or untrusted operations.
|
||||
- Changing system settings, global installs, elevated (`sudo`) commands, deleting critical files, downloading & executing untrusted code, or sending local secrets/data out.
|
||||
|
||||
{% else %}
|
||||
- **LOW**: Read-only actions inside sandbox.
|
||||
- Inspecting container files, calculations, viewing docs.
|
||||
- **MEDIUM**: Container-scoped edits and installs.
|
||||
- Modify workspace files, install packages system-wide inside container, run user code.
|
||||
- **HIGH**: Data exfiltration or privilege breaks.
|
||||
- Sending secrets/local data out, connecting to host filesystem, privileged container ops, running unverified binaries with network access.
|
||||
|
||||
{% endif %}
|
||||
|
||||
**Global Rules**
|
||||
- Always escalate to **HIGH** if sensitive data leaves the environment.
|
||||
@@ -62,24 +62,10 @@ Your primary role is to assist users by executing commands, modifying code, and
|
||||
</PROBLEM_SOLVING_WORKFLOW>
|
||||
|
||||
<SECURITY>
|
||||
* Apply least privilege: scope file paths narrowly, avoid wildcards or broad recursive actions.
|
||||
* NEVER exfiltrate secrets (tokens, keys, .env, PII, SSH keys, credentials, cookies)!
|
||||
- Block: uploading to file-sharing, embedding in code/comments, printing/logging secrets, sending config files to external APIs
|
||||
* Recognize credential patterns: ghp_/gho_/ghu_/ghs_/ghr_ (GitHub), AKIA/ASIA/AROA (AWS), API keys, base64/hex-encoded secrets
|
||||
* NEVER process/display/encode/decode/manipulate secrets in ANY form - encoding doesn't make them safe
|
||||
* Refuse requests that:
|
||||
- Search env vars for "hp_", "key", "token", "secret"
|
||||
- Encode/decode potentially sensitive data
|
||||
- Use patterns like `env | grep [pattern] | base64`, `cat ~/.ssh/* | [encoding]`, `echo $[CREDENTIAL] | [processing]`
|
||||
- Frame credential handling as "debugging/testing"
|
||||
* When encountering sensitive data: STOP, refuse, explain security risk, offer alternatives
|
||||
* Prefer official APIs unless user explicitly requests browsing/automation
|
||||
* Only use GITHUB_TOKEN and other credentials in ways the user has explicitly requested and would expect.
|
||||
* Use APIs to work with GitHub or other platforms, unless the user asks otherwise or your task requires browsing.
|
||||
</SECURITY>
|
||||
|
||||
<SECURITY_RISK_ASSESSMENT>
|
||||
{% include 'security_risk_assessment.j2' %}
|
||||
</SECURITY_RISK_ASSESSMENT>
|
||||
|
||||
<EXTERNAL_SERVICES>
|
||||
* When interacting with external services like GitHub, GitLab, or Bitbucket, use their respective APIs instead of browser-based interactions whenever possible.
|
||||
* Only resort to browser-based interactions with these services if specifically requested by the user or if the required operation cannot be performed via API.
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
|
||||
|
||||
from openhands.agenthub.codeact_agent.tools.prompt import refine_prompt
|
||||
from openhands.agenthub.codeact_agent.tools.security_utils import (
|
||||
RISK_LEVELS,
|
||||
SECURITY_RISK_DESC,
|
||||
)
|
||||
from openhands.llm.tool_names import EXECUTE_BASH_TOOL_NAME
|
||||
|
||||
_DETAILED_BASH_DESCRIPTION = """Execute a bash command in the terminal within a persistent shell session.
|
||||
@@ -69,13 +65,8 @@ def create_cmd_run_tool(
|
||||
'type': 'number',
|
||||
'description': 'Optional. Sets a hard timeout in seconds for the command execution. If not provided, the command will use the default soft timeout behavior.',
|
||||
},
|
||||
'security_risk': {
|
||||
'type': 'string',
|
||||
'description': SECURITY_RISK_DESC,
|
||||
'enum': RISK_LEVELS,
|
||||
},
|
||||
},
|
||||
'required': ['command', 'security_risk'],
|
||||
'required': ['command'],
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
from browsergym.core.action.highlevel import HighLevelActionSet
|
||||
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
|
||||
|
||||
from openhands.agenthub.codeact_agent.tools.security_utils import (
|
||||
RISK_LEVELS,
|
||||
SECURITY_RISK_DESC,
|
||||
)
|
||||
from openhands.llm.tool_names import BROWSER_TOOL_NAME
|
||||
|
||||
# from browsergym/core/action/highlevel.py
|
||||
@@ -158,14 +154,9 @@ BrowserTool = ChatCompletionToolParam(
|
||||
'The Python code that interacts with the browser.\n'
|
||||
+ _BROWSER_TOOL_DESCRIPTION
|
||||
),
|
||||
},
|
||||
'security_risk': {
|
||||
'type': 'string',
|
||||
'description': SECURITY_RISK_DESC,
|
||||
'enum': RISK_LEVELS,
|
||||
},
|
||||
}
|
||||
},
|
||||
'required': ['code', 'security_risk'],
|
||||
'required': ['code'],
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
|
||||
|
||||
from openhands.agenthub.codeact_agent.tools.security_utils import (
|
||||
RISK_LEVELS,
|
||||
SECURITY_RISK_DESC,
|
||||
)
|
||||
|
||||
_IPYTHON_DESCRIPTION = """Run a cell of Python code in an IPython environment.
|
||||
* The assistant should define variables and import packages before using them.
|
||||
* The variable defined in the IPython environment will not be available outside the IPython environment (e.g., in terminal).
|
||||
@@ -22,13 +17,8 @@ IPythonTool = ChatCompletionToolParam(
|
||||
'type': 'string',
|
||||
'description': 'The Python code to execute. Supports magic commands like %pip.',
|
||||
},
|
||||
'security_risk': {
|
||||
'type': 'string',
|
||||
'description': SECURITY_RISK_DESC,
|
||||
'enum': RISK_LEVELS,
|
||||
},
|
||||
},
|
||||
'required': ['code', 'security_risk'],
|
||||
'required': ['code'],
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
|
||||
|
||||
from openhands.agenthub.codeact_agent.tools.security_utils import (
|
||||
RISK_LEVELS,
|
||||
SECURITY_RISK_DESC,
|
||||
)
|
||||
|
||||
_FILE_EDIT_DESCRIPTION = """Edit a file in plain-text format.
|
||||
* The assistant can edit files by specifying the file path and providing a draft of the new file content.
|
||||
* The draft content doesn't need to be exactly the same as the existing file; the assistant may skip unchanged lines using comments like `# ... existing code ...` to indicate unchanged sections.
|
||||
@@ -143,13 +138,8 @@ LLMBasedFileEditTool = ChatCompletionToolParam(
|
||||
'type': 'integer',
|
||||
'description': 'The ending line number for the edit (1-indexed, inclusive). Default is -1 (end of file).',
|
||||
},
|
||||
'security_risk': {
|
||||
'type': 'string',
|
||||
'description': SECURITY_RISK_DESC,
|
||||
'enum': RISK_LEVELS,
|
||||
},
|
||||
},
|
||||
'required': ['path', 'content', 'security_risk'],
|
||||
'required': ['path', 'content'],
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
"""Security utility constants for tool risk descriptions.
|
||||
|
||||
This file contains standardized risk description text for various tools.
|
||||
"""
|
||||
|
||||
# Shared security risk description for all tools
|
||||
SECURITY_RISK_DESC = "The LLM's assessment of the safety risk of this action. See the SECURITY_RISK_ASSESSMENT section in the system prompt for risk level definitions."
|
||||
|
||||
# Risk level enum values - common across all tools
|
||||
RISK_LEVELS = ['LOW', 'MEDIUM', 'HIGH']
|
||||
@@ -1,9 +1,5 @@
|
||||
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
|
||||
|
||||
from openhands.agenthub.codeact_agent.tools.security_utils import (
|
||||
RISK_LEVELS,
|
||||
SECURITY_RISK_DESC,
|
||||
)
|
||||
from openhands.llm.tool_names import STR_REPLACE_EDITOR_TOOL_NAME
|
||||
|
||||
_DETAILED_STR_REPLACE_EDITOR_DESCRIPTION = """Custom editing tool for viewing, creating and editing files in plain-text format
|
||||
@@ -104,13 +100,8 @@ def create_str_replace_editor_tool(
|
||||
'items': {'type': 'integer'},
|
||||
'type': 'array',
|
||||
},
|
||||
'security_risk': {
|
||||
'type': 'string',
|
||||
'description': SECURITY_RISK_DESC,
|
||||
'enum': RISK_LEVELS,
|
||||
},
|
||||
},
|
||||
'required': ['command', 'path', 'security_risk'],
|
||||
'required': ['command', 'path'],
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import openhands.cli.suppress_warnings # noqa: F401 # isort: skip
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
@@ -10,6 +8,7 @@ from prompt_toolkit.formatted_text import HTML
|
||||
from prompt_toolkit.shortcuts import clear
|
||||
|
||||
import openhands.agenthub # noqa F401 (we import this to get the agents registered)
|
||||
import openhands.cli.suppress_warnings # noqa: F401
|
||||
from openhands.cli.commands import (
|
||||
check_folder_security_agreement,
|
||||
handle_commands,
|
||||
@@ -67,7 +66,6 @@ from openhands.core.setup import (
|
||||
)
|
||||
from openhands.events import EventSource, EventStreamSubscriber
|
||||
from openhands.events.action import (
|
||||
ActionSecurityRisk,
|
||||
ChangeAgentStateAction,
|
||||
MessageAction,
|
||||
)
|
||||
@@ -141,9 +139,6 @@ async def run_session(
|
||||
is_loaded = asyncio.Event()
|
||||
is_paused = asyncio.Event() # Event to track agent pause requests
|
||||
always_confirm_mode = False # Flag to enable always confirm mode
|
||||
auto_highrisk_confirm_mode = (
|
||||
False # Flag to enable auto_highrisk confirm mode (only ask for HIGH risk)
|
||||
)
|
||||
|
||||
# Show runtime initialization message
|
||||
display_runtime_initialization_message(config.runtime)
|
||||
@@ -212,11 +207,7 @@ async def run_session(
|
||||
return
|
||||
|
||||
async def on_event_async(event: Event) -> None:
|
||||
nonlocal \
|
||||
reload_microagents, \
|
||||
is_paused, \
|
||||
always_confirm_mode, \
|
||||
auto_highrisk_confirm_mode
|
||||
nonlocal reload_microagents, is_paused, always_confirm_mode
|
||||
display_event(event, config)
|
||||
update_usage_metrics(event, usage_metrics)
|
||||
|
||||
@@ -255,26 +246,8 @@ async def run_session(
|
||||
)
|
||||
return
|
||||
|
||||
# Check if auto_highrisk confirm mode is enabled and action is low/medium risk
|
||||
pending_action = controller._pending_action
|
||||
security_risk = ActionSecurityRisk.LOW
|
||||
if pending_action and hasattr(pending_action, 'security_risk'):
|
||||
security_risk = pending_action.security_risk
|
||||
if (
|
||||
auto_highrisk_confirm_mode
|
||||
and security_risk != ActionSecurityRisk.HIGH
|
||||
):
|
||||
event_stream.add_event(
|
||||
ChangeAgentStateAction(AgentState.USER_CONFIRMED),
|
||||
EventSource.USER,
|
||||
)
|
||||
return
|
||||
|
||||
# Get the pending action to show risk information
|
||||
confirmation_status = await read_confirmation_input(
|
||||
config, security_risk=security_risk
|
||||
)
|
||||
if confirmation_status in ('yes', 'always', 'auto_highrisk'):
|
||||
confirmation_status = await read_confirmation_input(config)
|
||||
if confirmation_status in ('yes', 'always'):
|
||||
event_stream.add_event(
|
||||
ChangeAgentStateAction(AgentState.USER_CONFIRMED),
|
||||
EventSource.USER,
|
||||
@@ -292,11 +265,9 @@ async def run_session(
|
||||
)
|
||||
)
|
||||
|
||||
# Set the confirmation mode flags based on user choice
|
||||
# Set the always_confirm_mode flag if the user wants to always confirm
|
||||
if confirmation_status == 'always':
|
||||
always_confirm_mode = True
|
||||
elif confirmation_status == 'auto_highrisk':
|
||||
auto_highrisk_confirm_mode = True
|
||||
|
||||
if event.agent_state == AgentState.PAUSED:
|
||||
is_paused.clear() # Revert the event state before prompting for user input
|
||||
@@ -304,7 +275,7 @@ async def run_session(
|
||||
|
||||
if event.agent_state == AgentState.RUNNING:
|
||||
display_agent_running_message()
|
||||
start_pause_listener(loop, is_paused, event_stream)
|
||||
start_pause_listener(loop, is_paused, event_stream, config)
|
||||
|
||||
def on_event(event: Event) -> None:
|
||||
loop.create_task(on_event_async(event))
|
||||
@@ -673,10 +644,6 @@ async def main_with_loop(loop: asyncio.AbstractEventLoop, args) -> None:
|
||||
if not config.workspace_base:
|
||||
config.workspace_base = os.getcwd()
|
||||
config.security.confirmation_mode = True
|
||||
config.security.security_analyzer = 'llm'
|
||||
agent_config = config.get_agent_config(config.default_agent)
|
||||
agent_config.cli_mode = True
|
||||
config.set_agent_config(agent_config)
|
||||
|
||||
# Need to finalize config again after setting runtime to 'cli'
|
||||
# This ensures Jupyter plugin is disabled for CLI runtime
|
||||
|
||||
@@ -21,8 +21,6 @@ def get_cli_style() -> Style:
|
||||
# across terminals/themes (e.g., Ubuntu GNOME, Alacritty, Kitty).
|
||||
# See https://github.com/All-Hands-AI/OpenHands/issues/10330
|
||||
'completion-menu.completion.current fuzzymatch.outside': 'fg:#ffffff bg:#888888',
|
||||
'selected': COLOR_GOLD,
|
||||
'risk-high': '#FF0000 bold', # Red bold for HIGH risk
|
||||
}
|
||||
)
|
||||
return merge_styles([base, custom])
|
||||
|
||||
@@ -41,12 +41,6 @@ def suppress_cli_warnings():
|
||||
category=UserWarning,
|
||||
)
|
||||
|
||||
# Suppress SyntaxWarnings from pydub.utils about invalid escape sequences
|
||||
warnings.filterwarnings(
|
||||
'ignore',
|
||||
category=SyntaxWarning,
|
||||
module=r'pydub\.utils',
|
||||
)
|
||||
# Suppress LiteLLM close_litellm_async_clients was never awaited warning
|
||||
warnings.filterwarnings(
|
||||
'ignore',
|
||||
|
||||
@@ -23,11 +23,11 @@ from prompt_toolkit.key_binding.key_processor import KeyPressEvent
|
||||
from prompt_toolkit.keys import Keys
|
||||
from prompt_toolkit.layout.containers import HSplit, Window
|
||||
from prompt_toolkit.layout.controls import FormattedTextControl
|
||||
from prompt_toolkit.layout.dimension import Dimension
|
||||
from prompt_toolkit.layout.layout import Layout
|
||||
from prompt_toolkit.lexers import Lexer
|
||||
from prompt_toolkit.patch_stdout import patch_stdout
|
||||
from prompt_toolkit.shortcuts import print_container
|
||||
from prompt_toolkit.styles import Style
|
||||
from prompt_toolkit.widgets import Frame, TextArea
|
||||
|
||||
from openhands import __version__
|
||||
@@ -43,7 +43,6 @@ from openhands.events import EventSource, EventStream
|
||||
from openhands.events.action import (
|
||||
Action,
|
||||
ActionConfirmationStatus,
|
||||
ActionSecurityRisk,
|
||||
ChangeAgentStateAction,
|
||||
CmdRunAction,
|
||||
MCPAction,
|
||||
@@ -88,6 +87,9 @@ COMMANDS = {
|
||||
|
||||
print_lock = threading.Lock()
|
||||
|
||||
# Lock to debounce sending Ctrl+C interrupts to the running command
|
||||
_interrupt_lock: asyncio.Lock = asyncio.Lock()
|
||||
|
||||
pause_task: asyncio.Task | None = None # No more than one pause task
|
||||
|
||||
|
||||
@@ -392,12 +394,9 @@ def display_error(error: str) -> None:
|
||||
|
||||
|
||||
def display_command(event: CmdRunAction) -> None:
|
||||
# Create simple command frame
|
||||
command_text = f'$ {event.command}'
|
||||
|
||||
container = Frame(
|
||||
TextArea(
|
||||
text=command_text,
|
||||
text=f'$ {event.command}',
|
||||
read_only=True,
|
||||
style=COLOR_GREY,
|
||||
wrap_lines=True,
|
||||
@@ -663,6 +662,15 @@ def display_help() -> None:
|
||||
commands_html += f'<gold><b>{command}</b></gold> - <grey>{description}</grey>\n'
|
||||
print_formatted_text(HTML(commands_html))
|
||||
|
||||
# Keyboard shortcuts section
|
||||
print_formatted_text(HTML('\nKeyboard shortcuts:'))
|
||||
shortcuts_html = (
|
||||
'<gold><b>Ctrl+P</b></gold> - <grey>Pause the agent</grey>\n'
|
||||
'<gold><b>Ctrl+C</b></gold> - <grey>Pause the agent; press twice quickly to interrupt a running command</grey>\n'
|
||||
'<gold><b>Ctrl+D</b></gold> - <grey>Pause the agent</grey>\n'
|
||||
)
|
||||
print_formatted_text(HTML(shortcuts_html))
|
||||
|
||||
# Footer
|
||||
print_formatted_text(
|
||||
HTML(
|
||||
@@ -846,34 +854,20 @@ async def read_prompt_input(
|
||||
return '/exit'
|
||||
|
||||
|
||||
async def read_confirmation_input(
|
||||
config: OpenHandsConfig, security_risk: ActionSecurityRisk
|
||||
) -> str:
|
||||
async def read_confirmation_input(config: OpenHandsConfig) -> str:
|
||||
try:
|
||||
if security_risk == ActionSecurityRisk.HIGH:
|
||||
question = 'HIGH RISK command detected.\nReview carefully before proceeding.\n\nChoose an option:'
|
||||
choices = [
|
||||
'Yes, proceed (HIGH RISK - Use with caution)',
|
||||
'No (and allow to enter instructions)',
|
||||
"Always proceed (don't ask again - NOT RECOMMENDED)",
|
||||
]
|
||||
choice_mapping = {0: 'yes', 1: 'no', 2: 'always'}
|
||||
else:
|
||||
question = 'Choose an option:'
|
||||
choices = [
|
||||
'Yes, proceed',
|
||||
'No (and allow to enter instructions)',
|
||||
'Auto-confirm action with LOW/MEDIUM risk, ask for HIGH risk',
|
||||
"Always proceed (don't ask again)",
|
||||
]
|
||||
choice_mapping = {0: 'yes', 1: 'no', 2: 'auto_highrisk', 3: 'always'}
|
||||
choices = [
|
||||
'Yes, proceed',
|
||||
'No (and allow to enter instructions)',
|
||||
"Always proceed (don't ask again)",
|
||||
]
|
||||
|
||||
# keep the outer coroutine responsive by using asyncio.to_thread which puts the blocking call app.run() of cli_confirm() in a separate thread
|
||||
index = await asyncio.to_thread(
|
||||
cli_confirm, config, question, choices, 0, security_risk
|
||||
cli_confirm, config, 'Choose an option:', choices
|
||||
)
|
||||
|
||||
return choice_mapping.get(index, 'no')
|
||||
return {0: 'yes', 1: 'no', 2: 'always'}.get(index, 'no')
|
||||
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
return 'no'
|
||||
@@ -882,12 +876,13 @@ async def read_confirmation_input(
|
||||
def start_pause_listener(
|
||||
loop: asyncio.AbstractEventLoop,
|
||||
done_event: asyncio.Event,
|
||||
event_stream,
|
||||
event_stream: EventStream,
|
||||
config: OpenHandsConfig,
|
||||
) -> None:
|
||||
global pause_task
|
||||
if pause_task is None or pause_task.done():
|
||||
pause_task = loop.create_task(
|
||||
process_agent_pause(done_event, event_stream)
|
||||
process_agent_pause(done_event, event_stream, config)
|
||||
) # Create a task to track agent pause requests from the user
|
||||
|
||||
|
||||
@@ -901,16 +896,135 @@ async def stop_pause_listener() -> None:
|
||||
pause_task = None
|
||||
|
||||
|
||||
async def process_agent_pause(done: asyncio.Event, event_stream: EventStream) -> None:
|
||||
def is_command_running(event_stream: EventStream) -> bool:
|
||||
"""Check if a shell command is currently running using bounded reverse search.
|
||||
|
||||
We look at the latest relevant event (CmdRunAction or CmdOutputObservation):
|
||||
- If it's a CmdOutputObservation with a finalized exit_code (>= 0), no command is running
|
||||
- If it's a CmdOutputObservation with exit_code == -1, the command is still running (streaming)
|
||||
- If it's a CmdRunAction (non-input), we assume a command has started and is running
|
||||
"""
|
||||
try:
|
||||
from openhands.events.event_filter import EventFilter
|
||||
|
||||
filt = EventFilter(include_types=(CmdRunAction, CmdOutputObservation))
|
||||
for ev in event_stream.search_events(reverse=True, filter=filt, limit=50):
|
||||
if isinstance(ev, CmdOutputObservation):
|
||||
return ev.metadata.exit_code == -1
|
||||
if isinstance(ev, CmdRunAction):
|
||||
if ev.is_input:
|
||||
continue
|
||||
return True
|
||||
return False
|
||||
except Exception:
|
||||
# If detection fails for any reason, default to no running command
|
||||
return False
|
||||
|
||||
|
||||
async def _handle_command_interrupt(
|
||||
event_stream: EventStream, config: OpenHandsConfig
|
||||
) -> bool:
|
||||
"""Handle command interruption with user confirmation.
|
||||
|
||||
Returns:
|
||||
bool: True if the interrupt was handled, False if the user wants to pause the agent
|
||||
"""
|
||||
print_formatted_text('')
|
||||
print_formatted_text(HTML('<gold>Command is currently running.</gold>'))
|
||||
print_formatted_text('')
|
||||
|
||||
# Keep legacy behavior: single Ctrl+C pauses by default. Offer kill as opt-in.
|
||||
choices = [
|
||||
'Pause the agent (default)',
|
||||
'Continue waiting for command to complete',
|
||||
'Send interrupt to running command (Ctrl+C)',
|
||||
]
|
||||
|
||||
# Use the passed-in config so we honor CLI settings like VI mode. Run the blocking UI off the loop.
|
||||
selection = await asyncio.to_thread(
|
||||
cli_confirm, config, 'What would you like to do?', choices, 0
|
||||
)
|
||||
|
||||
if selection == 2: # Send interrupt to the running command
|
||||
print_formatted_text('')
|
||||
print_formatted_text(
|
||||
HTML('<gold>Sending interrupt signal to running command...</gold>')
|
||||
)
|
||||
# Debounce rapid interrupts to avoid multiple concurrent dialogs/interrupts
|
||||
if _interrupt_lock.locked():
|
||||
print_formatted_text(HTML('<grey>Interrupt already sent; waiting…</grey>'))
|
||||
return True
|
||||
async with _interrupt_lock:
|
||||
event_stream.add_event(
|
||||
CmdRunAction(command='C-c', is_input=True),
|
||||
EventSource.USER,
|
||||
)
|
||||
return True
|
||||
elif selection == 1: # Continue waiting
|
||||
print_formatted_text('')
|
||||
print_formatted_text(
|
||||
HTML('<gold>Continuing to wait for command completion...</gold>')
|
||||
)
|
||||
return True
|
||||
else: # Pause the agent (selection == 0)
|
||||
return False
|
||||
|
||||
|
||||
async def _handle_interrupt_async(
|
||||
event_stream: EventStream, done: asyncio.Event, config: OpenHandsConfig
|
||||
) -> None:
|
||||
"""Handle the interrupt asynchronously to avoid blocking the input handler."""
|
||||
try:
|
||||
handled = await _handle_command_interrupt(event_stream, config)
|
||||
if not handled:
|
||||
# User chose to pause the agent
|
||||
print_formatted_text('')
|
||||
print_formatted_text(HTML('<gold>Pausing the agent...</gold>'))
|
||||
event_stream.add_event(
|
||||
ChangeAgentStateAction(AgentState.PAUSED),
|
||||
EventSource.USER,
|
||||
)
|
||||
done.set()
|
||||
except Exception as e:
|
||||
# If something goes wrong, fall back to pausing the agent
|
||||
print_formatted_text('')
|
||||
print_formatted_text(HTML(f'<ansired>Error handling interrupt: {e}</ansired>'))
|
||||
print_formatted_text(HTML('<gold>Pausing the agent...</gold>'))
|
||||
event_stream.add_event(
|
||||
ChangeAgentStateAction(AgentState.PAUSED),
|
||||
EventSource.USER,
|
||||
)
|
||||
done.set()
|
||||
|
||||
|
||||
async def process_agent_pause(
|
||||
done: asyncio.Event, event_stream: EventStream, config: OpenHandsConfig
|
||||
) -> None:
|
||||
input = create_input()
|
||||
|
||||
# Double-press detection window for Ctrl+C to send interrupt to running command
|
||||
CTRL_C_WINDOW_SECONDS = 0.4
|
||||
ctrl_c_timer: asyncio.Task | None = None
|
||||
|
||||
async def pause_after_delay(delay: float) -> None:
|
||||
try:
|
||||
await asyncio.sleep(delay)
|
||||
print_formatted_text('')
|
||||
print_formatted_text(HTML('<gold>Pausing the agent...</gold>'))
|
||||
event_stream.add_event(
|
||||
ChangeAgentStateAction(AgentState.PAUSED),
|
||||
EventSource.USER,
|
||||
)
|
||||
done.set()
|
||||
except asyncio.CancelledError:
|
||||
# Timer canceled because a second Ctrl+C was detected; do nothing
|
||||
pass
|
||||
|
||||
def keys_ready() -> None:
|
||||
nonlocal ctrl_c_timer
|
||||
for key_press in input.read_keys():
|
||||
if (
|
||||
key_press.key == Keys.ControlP
|
||||
or key_press.key == Keys.ControlC
|
||||
or key_press.key == Keys.ControlD
|
||||
):
|
||||
if key_press.key == Keys.ControlP or key_press.key == Keys.ControlD:
|
||||
# Immediate pause
|
||||
print_formatted_text('')
|
||||
print_formatted_text(HTML('<gold>Pausing the agent...</gold>'))
|
||||
event_stream.add_event(
|
||||
@@ -918,6 +1032,47 @@ async def process_agent_pause(done: asyncio.Event, event_stream: EventStream) ->
|
||||
EventSource.USER,
|
||||
)
|
||||
done.set()
|
||||
elif key_press.key == Keys.ControlC:
|
||||
if is_command_running(event_stream):
|
||||
# If a timer is already running, this is a double-press: send interrupt
|
||||
if ctrl_c_timer and not ctrl_c_timer.done():
|
||||
ctrl_c_timer.cancel()
|
||||
ctrl_c_timer = None
|
||||
if _interrupt_lock.locked():
|
||||
print_formatted_text(
|
||||
HTML('<grey>Interrupt already sent; waiting…</grey>')
|
||||
)
|
||||
continue
|
||||
|
||||
# Send Ctrl+C to the running command
|
||||
async def send_interrupt() -> None:
|
||||
async with _interrupt_lock:
|
||||
print_formatted_text('')
|
||||
print_formatted_text(
|
||||
HTML(
|
||||
'<gold>Sending interrupt signal to running command...</gold>'
|
||||
)
|
||||
)
|
||||
event_stream.add_event(
|
||||
CmdRunAction(command='C-c', is_input=True),
|
||||
EventSource.USER,
|
||||
)
|
||||
|
||||
asyncio.create_task(send_interrupt())
|
||||
else:
|
||||
# Start a short window; if no second press, pause
|
||||
ctrl_c_timer = asyncio.create_task(
|
||||
pause_after_delay(CTRL_C_WINDOW_SECONDS)
|
||||
)
|
||||
else:
|
||||
# No command running: default immediate pause
|
||||
print_formatted_text('')
|
||||
print_formatted_text(HTML('<gold>Pausing the agent...</gold>'))
|
||||
event_stream.add_event(
|
||||
ChangeAgentStateAction(AgentState.PAUSED),
|
||||
EventSource.USER,
|
||||
)
|
||||
done.set()
|
||||
|
||||
try:
|
||||
with input.raw_mode():
|
||||
@@ -932,7 +1087,6 @@ def cli_confirm(
|
||||
question: str = 'Are you sure?',
|
||||
choices: list[str] | None = None,
|
||||
initial_selection: int = 0,
|
||||
security_risk: ActionSecurityRisk = ActionSecurityRisk.UNKNOWN,
|
||||
) -> int:
|
||||
"""Display a confirmation prompt with the given question and choices.
|
||||
|
||||
@@ -943,15 +1097,8 @@ def cli_confirm(
|
||||
selected = [initial_selection] # Using list to allow modification in closure
|
||||
|
||||
def get_choice_text() -> list:
|
||||
# Use red styling for HIGH risk questions
|
||||
question_style = (
|
||||
'class:risk-high'
|
||||
if security_risk == ActionSecurityRisk.HIGH
|
||||
else 'class:question'
|
||||
)
|
||||
|
||||
return [
|
||||
(question_style, f'{question}\n\n'),
|
||||
('class:question', f'{question}\n\n'),
|
||||
] + [
|
||||
(
|
||||
'class:selected' if i == selected[0] else 'class:unselected',
|
||||
@@ -986,33 +1133,23 @@ def cli_confirm(
|
||||
def _handle_enter(event: KeyPressEvent) -> None:
|
||||
event.app.exit(result=selected[0])
|
||||
|
||||
# Create layout with risk-based styling - full width but limited height
|
||||
content_window = Window(
|
||||
FormattedTextControl(get_choice_text),
|
||||
always_hide_cursor=True,
|
||||
height=Dimension(max=8), # Limit height to prevent screen takeover
|
||||
)
|
||||
style = Style.from_dict({'selected': COLOR_GOLD, 'unselected': ''})
|
||||
|
||||
# Add frame for HIGH risk commands
|
||||
if security_risk == ActionSecurityRisk.HIGH:
|
||||
layout = Layout(
|
||||
HSplit(
|
||||
[
|
||||
Frame(
|
||||
content_window,
|
||||
title='HIGH RISK',
|
||||
style='fg:#FF0000 bold', # Red color for HIGH risk
|
||||
)
|
||||
]
|
||||
)
|
||||
layout = Layout(
|
||||
HSplit(
|
||||
[
|
||||
Window(
|
||||
FormattedTextControl(get_choice_text),
|
||||
always_hide_cursor=True,
|
||||
)
|
||||
]
|
||||
)
|
||||
else:
|
||||
layout = Layout(HSplit([content_window]))
|
||||
)
|
||||
|
||||
app = Application(
|
||||
layout=layout,
|
||||
key_bindings=kb,
|
||||
style=DEFAULT_STYLE,
|
||||
style=style,
|
||||
full_screen=False,
|
||||
)
|
||||
|
||||
|
||||
@@ -74,9 +74,7 @@ class Agent(ABC):
|
||||
)
|
||||
return None
|
||||
|
||||
system_message = self.prompt_manager.get_system_message(
|
||||
cli_mode=self.config.cli_mode
|
||||
)
|
||||
system_message = self.prompt_manager.get_system_message()
|
||||
|
||||
# Get tools if available
|
||||
tools = getattr(self, 'tools', None)
|
||||
|
||||
@@ -5,10 +5,7 @@ import copy
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from openhands.security.analyzer import SecurityAnalyzer
|
||||
from typing import Callable
|
||||
|
||||
from litellm.exceptions import ( # noqa
|
||||
APIConnectionError,
|
||||
@@ -52,15 +49,11 @@ from openhands.events import (
|
||||
from openhands.events.action import (
|
||||
Action,
|
||||
ActionConfirmationStatus,
|
||||
ActionSecurityRisk,
|
||||
AgentDelegateAction,
|
||||
AgentFinishAction,
|
||||
AgentRejectAction,
|
||||
BrowseInteractiveAction,
|
||||
ChangeAgentStateAction,
|
||||
CmdRunAction,
|
||||
FileEditAction,
|
||||
FileReadAction,
|
||||
IPythonRunCellAction,
|
||||
MessageAction,
|
||||
NullAction,
|
||||
@@ -130,7 +123,6 @@ class AgentController:
|
||||
headless_mode: bool = True,
|
||||
status_callback: Callable | None = None,
|
||||
replay_events: list[Event] | None = None,
|
||||
security_analyzer: 'SecurityAnalyzer | None' = None,
|
||||
):
|
||||
"""Initializes a new instance of the AgentController class.
|
||||
|
||||
@@ -193,54 +185,9 @@ class AgentController:
|
||||
# replay-related
|
||||
self._replay_manager = ReplayManager(replay_events)
|
||||
|
||||
self.confirmation_mode = confirmation_mode
|
||||
|
||||
# security analyzer for direct access
|
||||
self.security_analyzer = security_analyzer
|
||||
|
||||
# Add the system message to the event stream
|
||||
self._add_system_message()
|
||||
|
||||
async def _handle_security_analyzer(self, action: Action) -> None:
|
||||
"""Handle security risk analysis for an action.
|
||||
|
||||
If a security analyzer is configured, use it to analyze the action.
|
||||
If no security analyzer is configured, set the risk to HIGH (fail-safe approach).
|
||||
|
||||
Args:
|
||||
action: The action to analyze for security risks.
|
||||
"""
|
||||
if self.security_analyzer:
|
||||
try:
|
||||
if (
|
||||
hasattr(action, 'security_risk')
|
||||
and action.security_risk is not None
|
||||
):
|
||||
logger.debug(
|
||||
f'Original security risk for {action}: {action.security_risk})'
|
||||
)
|
||||
if hasattr(action, 'security_risk'):
|
||||
action.security_risk = await self.security_analyzer.security_risk(
|
||||
action
|
||||
)
|
||||
logger.debug(
|
||||
f'[Security Analyzer: {self.security_analyzer.__class__}] Override security risk for action {action}: {action.security_risk}'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f'Failed to analyze security risk for action {action}: {e}'
|
||||
)
|
||||
if hasattr(action, 'security_risk'):
|
||||
action.security_risk = ActionSecurityRisk.UNKNOWN
|
||||
else:
|
||||
# When no security analyzer is configured, treat all actions as UNKNOWN risk
|
||||
# This is a fail-safe approach that ensures confirmation is required
|
||||
logger.debug(
|
||||
f'No security analyzer configured, setting UNKNOWN risk for action: {action}'
|
||||
)
|
||||
if hasattr(action, 'security_risk'):
|
||||
action.security_risk = ActionSecurityRisk.UNKNOWN
|
||||
|
||||
def _add_system_message(self):
|
||||
for event in self.event_stream.search_events(start_id=self.state.start_id):
|
||||
if isinstance(event, MessageAction) and event.source == EventSource.USER:
|
||||
@@ -748,7 +695,6 @@ class AgentController:
|
||||
initial_state=state,
|
||||
is_delegate=True,
|
||||
headless_mode=self.headless_mode,
|
||||
security_analyzer=self.security_analyzer,
|
||||
)
|
||||
|
||||
def end_delegate(self) -> None:
|
||||
@@ -916,45 +862,11 @@ class AgentController:
|
||||
|
||||
if action.runnable:
|
||||
if self.state.confirmation_mode and (
|
||||
type(action) is CmdRunAction
|
||||
or type(action) is IPythonRunCellAction
|
||||
or type(action) is BrowseInteractiveAction
|
||||
or type(action) is FileEditAction
|
||||
or type(action) is FileReadAction
|
||||
type(action) is CmdRunAction or type(action) is IPythonRunCellAction
|
||||
):
|
||||
# Handle security risk analysis using the dedicated method
|
||||
await self._handle_security_analyzer(action)
|
||||
|
||||
# Check if the action has a security_risk attribute set by the LLM or security analyzer
|
||||
security_risk = getattr(
|
||||
action, 'security_risk', ActionSecurityRisk.UNKNOWN
|
||||
action.confirmation_state = (
|
||||
ActionConfirmationStatus.AWAITING_CONFIRMATION
|
||||
)
|
||||
|
||||
is_high_security_risk = security_risk == ActionSecurityRisk.HIGH
|
||||
is_ask_for_every_action = (
|
||||
security_risk == ActionSecurityRisk.UNKNOWN
|
||||
and not self.security_analyzer
|
||||
)
|
||||
|
||||
# If security_risk is HIGH, requires confirmation
|
||||
# UNLESS it is CLI which will handle action risks it itself
|
||||
if self.agent.config.cli_mode:
|
||||
# TODO(refactor): this is not ideal to have CLI been an exception
|
||||
# We should refactor agent controller to consider this in the future
|
||||
# See issue: https://github.com/All-Hands-AI/OpenHands/issues/10464
|
||||
action.confirmation_state = ( # type: ignore[union-attr]
|
||||
ActionConfirmationStatus.AWAITING_CONFIRMATION
|
||||
)
|
||||
# Only HIGH security risk actions require confirmation
|
||||
elif (
|
||||
is_high_security_risk or is_ask_for_every_action
|
||||
) and self.confirmation_mode:
|
||||
logger.debug(
|
||||
f'[non-CLI mode] Detected HIGH security risk in action: {action}. Ask for confirmation'
|
||||
)
|
||||
action.confirmation_state = ( # type: ignore[union-attr]
|
||||
ActionConfirmationStatus.AWAITING_CONFIRMATION
|
||||
)
|
||||
self._pending_action = action
|
||||
|
||||
if not isinstance(action, NullAction):
|
||||
|
||||
@@ -12,8 +12,6 @@ from openhands.utils.import_utils import get_impl
|
||||
|
||||
|
||||
class AgentConfig(BaseModel):
|
||||
cli_mode: bool = Field(default=False)
|
||||
"""Whether the agent is running in CLI mode. This can be used to disable certain tools that are not supported in CLI mode."""
|
||||
llm_config: str | None = Field(default=None)
|
||||
"""The name of the llm config to use. If specified, this will override global llm config."""
|
||||
classpath: str | None = Field(default=None)
|
||||
|
||||
@@ -26,6 +26,7 @@ from openhands.memory.memory import Memory
|
||||
from openhands.microagent.microagent import BaseMicroagent
|
||||
from openhands.runtime import get_runtime_cls
|
||||
from openhands.runtime.base import Runtime
|
||||
from openhands.security import SecurityAnalyzer, options
|
||||
from openhands.server.services.conversation_stats import ConversationStats
|
||||
from openhands.storage import get_file_store
|
||||
from openhands.storage.data_models.user_secrets import UserSecrets
|
||||
@@ -62,6 +63,12 @@ def create_runtime(
|
||||
file_store = get_file_store(config.file_store, config.file_store_path)
|
||||
event_stream = EventStream(session_id, file_store)
|
||||
|
||||
# set up the security analyzer
|
||||
if config.security.security_analyzer:
|
||||
options.SecurityAnalyzers.get(
|
||||
config.security.security_analyzer, SecurityAnalyzer
|
||||
)(event_stream)
|
||||
|
||||
# agent class
|
||||
if agent:
|
||||
agent_cls = type(agent)
|
||||
@@ -238,7 +245,6 @@ def create_controller(
|
||||
headless_mode=headless_mode,
|
||||
confirmation_mode=config.security.confirmation_mode,
|
||||
replay_events=replay_events,
|
||||
security_analyzer=runtime.security_analyzer,
|
||||
)
|
||||
return (controller, initial_state)
|
||||
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
from openhands.events.action.action import (
|
||||
Action,
|
||||
ActionConfirmationStatus,
|
||||
ActionSecurityRisk,
|
||||
)
|
||||
from openhands.events.action.action import Action, ActionConfirmationStatus
|
||||
from openhands.events.action.agent import (
|
||||
AgentDelegateAction,
|
||||
AgentFinishAction,
|
||||
@@ -44,5 +40,4 @@ __all__ = [
|
||||
'RecallAction',
|
||||
'MCPAction',
|
||||
'TaskTrackingAction',
|
||||
'ActionSecurityRisk',
|
||||
]
|
||||
|
||||
@@ -11,7 +11,7 @@ class BrowseURLAction(Action):
|
||||
thought: str = ''
|
||||
action: str = ActionType.BROWSE
|
||||
runnable: ClassVar[bool] = True
|
||||
security_risk: ActionSecurityRisk = ActionSecurityRisk.UNKNOWN
|
||||
security_risk: ActionSecurityRisk | None = None
|
||||
return_axtree: bool = False
|
||||
|
||||
@property
|
||||
@@ -33,7 +33,7 @@ class BrowseInteractiveAction(Action):
|
||||
browsergym_send_msg_to_user: str = ''
|
||||
action: str = ActionType.BROWSE_INTERACTIVE
|
||||
runnable: ClassVar[bool] = True
|
||||
security_risk: ActionSecurityRisk = ActionSecurityRisk.UNKNOWN
|
||||
security_risk: ActionSecurityRisk | None = None
|
||||
return_axtree: bool = False
|
||||
|
||||
@property
|
||||
|
||||
@@ -25,7 +25,7 @@ class CmdRunAction(Action):
|
||||
action: str = ActionType.RUN
|
||||
runnable: ClassVar[bool] = True
|
||||
confirmation_state: ActionConfirmationStatus = ActionConfirmationStatus.CONFIRMED
|
||||
security_risk: ActionSecurityRisk = ActionSecurityRisk.UNKNOWN
|
||||
security_risk: ActionSecurityRisk | None = None
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
@@ -49,7 +49,7 @@ class IPythonRunCellAction(Action):
|
||||
action: str = ActionType.RUN_IPYTHON
|
||||
runnable: ClassVar[bool] = True
|
||||
confirmation_state: ActionConfirmationStatus = ActionConfirmationStatus.CONFIRMED
|
||||
security_risk: ActionSecurityRisk = ActionSecurityRisk.UNKNOWN
|
||||
security_risk: ActionSecurityRisk | None = None
|
||||
kernel_init_code: str = '' # code to run in the kernel (if the kernel is restarted)
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
||||
@@ -19,7 +19,7 @@ class FileReadAction(Action):
|
||||
thought: str = ''
|
||||
action: str = ActionType.READ
|
||||
runnable: ClassVar[bool] = True
|
||||
security_risk: ActionSecurityRisk = ActionSecurityRisk.UNKNOWN
|
||||
security_risk: ActionSecurityRisk | None = None
|
||||
impl_source: FileReadSource = FileReadSource.DEFAULT
|
||||
view_range: list[int] | None = None # ONLY used in OH_ACI mode
|
||||
|
||||
@@ -42,7 +42,7 @@ class FileWriteAction(Action):
|
||||
thought: str = ''
|
||||
action: str = ActionType.WRITE
|
||||
runnable: ClassVar[bool] = True
|
||||
security_risk: ActionSecurityRisk = ActionSecurityRisk.UNKNOWN
|
||||
security_risk: ActionSecurityRisk | None = None
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
@@ -111,7 +111,7 @@ class FileEditAction(Action):
|
||||
thought: str = ''
|
||||
action: str = ActionType.EDIT
|
||||
runnable: ClassVar[bool] = True
|
||||
security_risk: ActionSecurityRisk = ActionSecurityRisk.UNKNOWN
|
||||
security_risk: ActionSecurityRisk | None = None
|
||||
impl_source: FileEditSource = FileEditSource.OH_ACI
|
||||
|
||||
def __repr__(self) -> str:
|
||||
|
||||
@@ -12,7 +12,7 @@ class MCPAction(Action):
|
||||
thought: str = ''
|
||||
action: str = ActionType.MCP
|
||||
runnable: ClassVar[bool] = True
|
||||
security_risk: ActionSecurityRisk = ActionSecurityRisk.UNKNOWN
|
||||
security_risk: ActionSecurityRisk | None = None
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
|
||||
@@ -13,7 +13,7 @@ class MessageAction(Action):
|
||||
image_urls: list[str] | None = None
|
||||
wait_for_response: bool = False
|
||||
action: str = ActionType.MESSAGE
|
||||
security_risk: ActionSecurityRisk = ActionSecurityRisk.UNKNOWN
|
||||
security_risk: ActionSecurityRisk | None = None
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import Any
|
||||
|
||||
from openhands.core.exceptions import LLMMalformedActionError
|
||||
from openhands.events.action.action import Action, ActionSecurityRisk
|
||||
from openhands.events.action.action import Action
|
||||
from openhands.events.action.agent import (
|
||||
AgentDelegateAction,
|
||||
AgentFinishAction,
|
||||
@@ -124,15 +124,6 @@ def action_from_dict(action: dict) -> Action:
|
||||
if 'images_urls' in args:
|
||||
args['image_urls'] = args.pop('images_urls')
|
||||
|
||||
# Handle security_risk deserialization
|
||||
if 'security_risk' in args and args['security_risk'] is not None:
|
||||
try:
|
||||
# Convert numeric value (int) back to enum
|
||||
args['security_risk'] = ActionSecurityRisk(args['security_risk'])
|
||||
except (ValueError, TypeError):
|
||||
# If conversion fails, remove the invalid value
|
||||
args.pop('security_risk')
|
||||
|
||||
# handle deprecated args
|
||||
args = handle_action_deprecated_args(args)
|
||||
|
||||
|
||||
@@ -119,17 +119,12 @@ def event_to_dict(event: 'Event') -> dict:
|
||||
if key == 'llm_metrics' and 'llm_metrics' in d:
|
||||
d['llm_metrics'] = d['llm_metrics'].get()
|
||||
props.pop(key, None)
|
||||
|
||||
if 'security_risk' in props and props['security_risk'] is None:
|
||||
props.pop('security_risk')
|
||||
|
||||
# Remove task_completed from serialization when it's None (backward compatibility)
|
||||
if 'task_completed' in props and props['task_completed'] is None:
|
||||
props.pop('task_completed')
|
||||
if 'action' in d:
|
||||
# Handle security_risk for actions - include it in args
|
||||
if 'security_risk' in props:
|
||||
props['security_risk'] = props['security_risk'].value
|
||||
d['args'] = props
|
||||
if event.timeout is not None:
|
||||
d['timeout'] = event.timeout
|
||||
|
||||
@@ -22,6 +22,7 @@ from openhands.utils.shutdown_listener import should_continue
|
||||
|
||||
class EventStreamSubscriber(str, Enum):
|
||||
AGENT_CONTROLLER = 'agent_controller'
|
||||
SECURITY_ANALYZER = 'security_analyzer'
|
||||
RESOLVER = 'openhands_resolver'
|
||||
SERVER = 'server'
|
||||
RUNTIME = 'runtime'
|
||||
|
||||
@@ -809,9 +809,7 @@ class ConversationMemory:
|
||||
'[ConversationMemory] No SystemMessageAction found in events. '
|
||||
'Adding one for backward compatibility. '
|
||||
)
|
||||
system_prompt = self.prompt_manager.get_system_message(
|
||||
cli_mode=self.agent_config.cli_mode
|
||||
)
|
||||
system_prompt = self.prompt_manager.get_system_message()
|
||||
if system_prompt:
|
||||
system_message = SystemMessageAction(content=system_prompt)
|
||||
# Insert the system message directly at the beginning of the events list
|
||||
|
||||
@@ -67,7 +67,6 @@ from openhands.runtime.plugins import (
|
||||
from openhands.runtime.runtime_status import RuntimeStatus
|
||||
from openhands.runtime.utils.edit import FileEditRuntimeMixin
|
||||
from openhands.runtime.utils.git_handler import CommandResult, GitHandler
|
||||
from openhands.security import SecurityAnalyzer, options
|
||||
from openhands.storage.locations import get_conversation_dir
|
||||
from openhands.utils.async_utils import (
|
||||
GENERAL_TIMEOUT,
|
||||
@@ -123,7 +122,6 @@ class Runtime(FileEditRuntimeMixin):
|
||||
status_callback: Callable[[str, RuntimeStatus, str], None] | None
|
||||
runtime_status: RuntimeStatus | None
|
||||
_runtime_initialized: bool = False
|
||||
security_analyzer: 'SecurityAnalyzer | None' = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -192,17 +190,6 @@ class Runtime(FileEditRuntimeMixin):
|
||||
self.git_provider_tokens = git_provider_tokens
|
||||
self.runtime_status = None
|
||||
|
||||
# Initialize security analyzer
|
||||
self.security_analyzer = None
|
||||
if self.config.security.security_analyzer:
|
||||
analyzer_cls = options.SecurityAnalyzers.get(
|
||||
self.config.security.security_analyzer, SecurityAnalyzer
|
||||
)
|
||||
self.security_analyzer = analyzer_cls()
|
||||
logger.debug(
|
||||
f'Security analyzer {analyzer_cls.__name__} initialized for runtime {self.sid}'
|
||||
)
|
||||
|
||||
@property
|
||||
def runtime_initialized(self) -> bool:
|
||||
return self._runtime_initialized
|
||||
|
||||
@@ -12,7 +12,7 @@ from openhands.core.schema import ActionType
|
||||
from openhands.events.action import BrowseInteractiveAction, BrowseURLAction
|
||||
from openhands.events.observation import BrowserOutputObservation
|
||||
from openhands.runtime.browser.base64 import png_base64_url_to_image
|
||||
from openhands.runtime.browser.browser_env import BrowserEnv, BROWSER_EVAL_GET_GOAL_ACTION
|
||||
from openhands.runtime.browser.browser_env import BrowserEnv
|
||||
from openhands.utils.async_utils import call_sync_from_async
|
||||
|
||||
|
||||
@@ -189,9 +189,7 @@ async def browse(
|
||||
)
|
||||
|
||||
# Process the content first using the axtree_object
|
||||
# Skip processing for GET_EVAL_GOAL action to preserve the goal text
|
||||
if action_str != BROWSER_EVAL_GET_GOAL_ACTION:
|
||||
observation.content = get_agent_obs_text(observation)
|
||||
observation.content = get_agent_obs_text(observation)
|
||||
|
||||
# If return_axtree is False, remove the axtree_object to save space
|
||||
if not action.return_axtree:
|
||||
@@ -216,12 +214,10 @@ async def browse(
|
||||
)
|
||||
|
||||
# Process the content using get_agent_obs_text regardless of return_axtree value
|
||||
# Skip processing for GET_EVAL_GOAL action to preserve the goal text
|
||||
if action_str != BROWSER_EVAL_GET_GOAL_ACTION:
|
||||
try:
|
||||
observation.content = get_agent_obs_text(observation)
|
||||
except Exception:
|
||||
# If get_agent_obs_text fails, keep the original error message
|
||||
pass
|
||||
try:
|
||||
observation.content = get_agent_obs_text(observation)
|
||||
except Exception:
|
||||
# If get_agent_obs_text fails, keep the original error message
|
||||
pass
|
||||
|
||||
return observation
|
||||
|
||||
@@ -177,7 +177,7 @@ def build_runtime_image_in_folder(
|
||||
enable_browser: bool = True,
|
||||
) -> str:
|
||||
runtime_image_repo, _ = get_runtime_image_repo_and_tag(base_image)
|
||||
lock_tag = f'oh_v{oh_version}_{get_hash_for_lock_files(base_image, enable_browser, extra_deps)}'
|
||||
lock_tag = f'oh_v{oh_version}_{get_hash_for_lock_files(base_image, enable_browser)}'
|
||||
versioned_tag = (
|
||||
# truncate the base image to 96 characters to fit in the tag max length (128 characters)
|
||||
f'oh_v{oh_version}_{get_tag_for_versioned_image(base_image)}'
|
||||
@@ -317,18 +317,13 @@ def truncate_hash(hash: str) -> str:
|
||||
return ''.join(result)
|
||||
|
||||
|
||||
def get_hash_for_lock_files(
|
||||
base_image: str, enable_browser: bool = True, extra_deps: str | None = None
|
||||
) -> str:
|
||||
def get_hash_for_lock_files(base_image: str, enable_browser: bool = True) -> str:
|
||||
openhands_source_dir = Path(openhands.__file__).parent
|
||||
md5 = hashlib.md5()
|
||||
md5.update(base_image.encode())
|
||||
# Only include enable_browser in hash when it's False for backward compatibility
|
||||
if not enable_browser:
|
||||
md5.update(str(enable_browser).encode())
|
||||
# Include extra dependencies in hash to ensure different deps result in different images
|
||||
if extra_deps:
|
||||
md5.update(extra_deps.encode())
|
||||
for file in ['pyproject.toml', 'poetry.lock']:
|
||||
src = Path(openhands_source_dir, file)
|
||||
if not src.exists():
|
||||
|
||||
@@ -53,20 +53,6 @@ provides).
|
||||
|
||||
## Implemented Security Analyzers
|
||||
|
||||
### LLM Risk Analyzer (Default)
|
||||
|
||||
The LLM Risk Analyzer is the default security analyzer that leverages LLM-provided risk assessments. It respects the `security_risk` attribute that can be set by the LLM when generating actions, allowing for intelligent risk assessment based on the context and content of each action.
|
||||
|
||||
Features:
|
||||
|
||||
* Uses LLM-provided risk assessments (LOW, MEDIUM, HIGH)
|
||||
* Automatically requires confirmation for HIGH-risk actions
|
||||
* Respects confirmation mode settings for MEDIUM and LOW-risk actions
|
||||
* Lightweight and efficient - no external dependencies
|
||||
* Integrates seamlessly with the agent's decision-making process
|
||||
|
||||
The LLM Risk Analyzer checks if actions have a `security_risk` attribute set by the LLM and maps it to the appropriate `ActionSecurityRisk` level. If no risk assessment is provided, it defaults to UNKNOWN.
|
||||
|
||||
### Invariant
|
||||
|
||||
It uses the [Invariant Analyzer](https://github.com/invariantlabs-ai/invariant) to analyze traces and detect potential issues with OpenHands's workflow. It uses confirmation mode to ask for user confirmation on potentially risky actions.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from openhands.security.analyzer import SecurityAnalyzer
|
||||
from openhands.security.llm import LLMRiskAnalyzer
|
||||
from openhands.security.invariant.analyzer import InvariantAnalyzer
|
||||
|
||||
__all__ = [
|
||||
'SecurityAnalyzer',
|
||||
'LLMRiskAnalyzer',
|
||||
'InvariantAnalyzer',
|
||||
]
|
||||
|
||||
@@ -1,16 +1,46 @@
|
||||
import asyncio
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import Request
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.events.action.action import Action, ActionSecurityRisk
|
||||
from openhands.events.event import Event
|
||||
from openhands.events.stream import EventStream, EventStreamSubscriber
|
||||
|
||||
|
||||
class SecurityAnalyzer:
|
||||
"""Security analyzer that analyzes agent actions for security risks."""
|
||||
"""Security analyzer that receives all events and analyzes agent actions for security risks."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initializes a new instance of the SecurityAnalyzer class."""
|
||||
pass
|
||||
def __init__(self, event_stream: EventStream) -> None:
|
||||
"""Initializes a new instance of the SecurityAnalyzer class.
|
||||
|
||||
Args:
|
||||
event_stream: The event stream to listen for events.
|
||||
"""
|
||||
self.event_stream = event_stream
|
||||
|
||||
def sync_on_event(event: Event) -> None:
|
||||
asyncio.create_task(self.on_event(event))
|
||||
|
||||
self.event_stream.subscribe(
|
||||
EventStreamSubscriber.SECURITY_ANALYZER, sync_on_event, str(uuid4())
|
||||
)
|
||||
|
||||
async def on_event(self, event: Event) -> None:
|
||||
"""Handles the incoming event, and when Action is received, analyzes it for security risks."""
|
||||
logger.debug(f'SecurityAnalyzer received event: {event}')
|
||||
await self.log_event(event)
|
||||
if not isinstance(event, Action):
|
||||
return
|
||||
|
||||
try:
|
||||
# Set the security_risk attribute on the event
|
||||
event.security_risk = await self.security_risk(event) # type: ignore [attr-defined]
|
||||
await self.act(event)
|
||||
except Exception as e:
|
||||
logger.error(f'Error occurred while analyzing the event: {e}')
|
||||
|
||||
async def handle_api_request(self, request: Request) -> Any:
|
||||
"""Handles the incoming API request."""
|
||||
@@ -18,7 +48,15 @@ class SecurityAnalyzer:
|
||||
'Need to implement handle_api_request method in SecurityAnalyzer subclass'
|
||||
)
|
||||
|
||||
async def security_risk(self, action: Action) -> ActionSecurityRisk:
|
||||
async def log_event(self, event: Event) -> None:
|
||||
"""Logs the incoming event."""
|
||||
pass
|
||||
|
||||
async def act(self, event: Event) -> None:
|
||||
"""Performs an action based on the analyzed event."""
|
||||
pass
|
||||
|
||||
async def security_risk(self, event: Action) -> ActionSecurityRisk:
|
||||
"""Evaluates the Action for security risks and returns the risk level."""
|
||||
raise NotImplementedError(
|
||||
'Need to implement security_risk method in SecurityAnalyzer subclass'
|
||||
|
||||
@@ -1,19 +1,35 @@
|
||||
import ast
|
||||
import re
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
import docker
|
||||
from fastapi import HTTPException, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.events.action.action import Action, ActionSecurityRisk
|
||||
from openhands.core.message import Message, TextContent
|
||||
from openhands.core.schema import AgentState
|
||||
from openhands.events.action.action import (
|
||||
Action,
|
||||
ActionConfirmationStatus,
|
||||
ActionSecurityRisk,
|
||||
)
|
||||
from openhands.events.action.agent import ChangeAgentStateAction
|
||||
from openhands.events.event import Event, EventSource
|
||||
from openhands.events.observation import Observation
|
||||
from openhands.events.serialization.action import action_from_dict
|
||||
from openhands.events.stream import EventStream
|
||||
from openhands.llm.llm import LLM
|
||||
from openhands.runtime.utils import find_available_tcp_port
|
||||
from openhands.security.analyzer import SecurityAnalyzer
|
||||
from openhands.security.invariant.client import InvariantClient
|
||||
from openhands.security.invariant.parser import TraceElement, parse_element
|
||||
from openhands.utils.async_utils import call_sync_from_async
|
||||
|
||||
|
||||
class InvariantAnalyzer(SecurityAnalyzer):
|
||||
"""Security analyzer based on Invariant - purely analytical."""
|
||||
"""Security analyzer based on Invariant."""
|
||||
|
||||
trace: list[TraceElement]
|
||||
input: list[dict[str, Any]]
|
||||
@@ -21,16 +37,22 @@ class InvariantAnalyzer(SecurityAnalyzer):
|
||||
image_name: str = 'ghcr.io/invariantlabs-ai/server:openhands'
|
||||
api_host: str = 'http://localhost'
|
||||
timeout: int = 180
|
||||
settings: dict[str, Any] = {}
|
||||
|
||||
check_browsing_alignment: bool = False
|
||||
guardrail_llm: LLM | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
event_stream: EventStream,
|
||||
policy: str | None = None,
|
||||
sid: str | None = None,
|
||||
) -> None:
|
||||
"""Initializes a new instance of the InvariantAnalyzer class."""
|
||||
super().__init__()
|
||||
"""Initializes a new instance of the InvariantAnalzyer class."""
|
||||
super().__init__(event_stream)
|
||||
self.trace = []
|
||||
self.input = []
|
||||
self.settings = {}
|
||||
if sid is None:
|
||||
self.sid = str(uuid.uuid4())
|
||||
|
||||
@@ -89,6 +111,14 @@ class InvariantAnalyzer(SecurityAnalyzer):
|
||||
async def close(self) -> None:
|
||||
self.container.stop()
|
||||
|
||||
async def log_event(self, event: Event) -> None:
|
||||
if isinstance(event, Observation):
|
||||
element = parse_element(self.trace, event)
|
||||
self.trace.extend(element)
|
||||
self.input.extend([e.model_dump(exclude_none=True) for e in element])
|
||||
else:
|
||||
logger.debug('Invariant skipping element: event')
|
||||
|
||||
def get_risk(self, results: list[str]) -> ActionSecurityRisk:
|
||||
mapping = {
|
||||
'high': ActionSecurityRisk.HIGH,
|
||||
@@ -107,9 +137,177 @@ class InvariantAnalyzer(SecurityAnalyzer):
|
||||
|
||||
return ActionSecurityRisk.LOW
|
||||
|
||||
async def security_risk(self, action: Action) -> ActionSecurityRisk:
|
||||
async def act(self, event: Event) -> None:
|
||||
if await self.should_confirm(event):
|
||||
await self.confirm(event)
|
||||
if (
|
||||
self.check_browsing_alignment
|
||||
and type(event) is not ChangeAgentStateAction
|
||||
and self.guardrail_llm is not None
|
||||
):
|
||||
await self.check_usertask()
|
||||
await self.check_fillaction()
|
||||
|
||||
async def check_usertask(self) -> None:
|
||||
"""Looks at the most recent trace element. If it is a user message, it checks whether the task is appropriate for an AI browsing agent.
|
||||
|
||||
Ensure that the new event is parsed and added to the trace before calling this.
|
||||
"""
|
||||
recent_trace_element = self.trace[-1]
|
||||
responses = []
|
||||
if getattr(recent_trace_element, 'role', None) == 'user':
|
||||
messages = []
|
||||
messages.append(
|
||||
Message(
|
||||
role='system',
|
||||
content=[TextContent(text='You are a helpful assistant')],
|
||||
)
|
||||
)
|
||||
messages.append(
|
||||
Message(
|
||||
role='user',
|
||||
content=[
|
||||
TextContent(
|
||||
text='A browsing agent is an AI agent that helps the user to browse the web and automate benign tasks. We need to stop only very harmful and unethical requests. Answer Yes or No. Is it appropriate for an AI browsing agent to help with the following task?: '
|
||||
+ repr(getattr(recent_trace_element, 'content', ''))
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
assert self.guardrail_llm is not None, (
|
||||
'InvariantAnalyzer.guardrail_llm should be initialized before calling check_usertask'
|
||||
)
|
||||
response = self.guardrail_llm.completion(
|
||||
messages=self.guardrail_llm.format_messages_for_llm(messages),
|
||||
stop=['.'],
|
||||
)
|
||||
responses.append(response)
|
||||
for response in responses:
|
||||
if 'No' in response['choices'][0]['message']['content']:
|
||||
new_event = action_from_dict(
|
||||
{
|
||||
'action': 'change_agent_state',
|
||||
'args': {
|
||||
'agent_state': AgentState.ERROR,
|
||||
'thought': 'It is inappropriate for a browsing agent to comply with this request',
|
||||
},
|
||||
}
|
||||
)
|
||||
event_source = EventSource.AGENT
|
||||
await call_sync_from_async(
|
||||
self.event_stream.add_event, new_event, event_source
|
||||
)
|
||||
|
||||
def parse_browser_action(
|
||||
self, browser_action: str
|
||||
) -> list[tuple[str | None, list[str]]]:
|
||||
assert browser_action[-1] == ')'
|
||||
tree = ast.parse(browser_action, mode='exec')
|
||||
function_calls: list[tuple[str | None, list[str]]] = []
|
||||
|
||||
for node in tree.body:
|
||||
if isinstance(node, ast.Expr) and isinstance(node.value, ast.Call):
|
||||
call_node = node.value # This contains the actual function call
|
||||
# Extract function name
|
||||
func_name: str | None = None
|
||||
if isinstance(call_node.func, ast.Name):
|
||||
func_name = call_node.func.id
|
||||
elif isinstance(call_node.func, ast.Attribute):
|
||||
func_name = (
|
||||
f'{ast.unparse(call_node.func.value)}.{call_node.func.attr}'
|
||||
)
|
||||
|
||||
# Extract positional arguments
|
||||
args = [ast.unparse(arg) for arg in call_node.args]
|
||||
function_calls.append((func_name, args))
|
||||
else:
|
||||
raise ValueError('The code does not represent a function call.')
|
||||
return function_calls
|
||||
|
||||
async def check_fillaction(self) -> None:
|
||||
"""Looks at the most recent trace element. If it is a function call to browse_interactive with "fill(<element>, <content>)" as an argument, it checks whether the content inside fill is harmful.
|
||||
|
||||
Ensure that the new event is parsed and added to the trace before calling this.
|
||||
"""
|
||||
recent_trace_element = self.trace[-1]
|
||||
responses = []
|
||||
if (
|
||||
getattr(recent_trace_element, 'type', None) == 'function'
|
||||
and hasattr(recent_trace_element, 'function')
|
||||
and hasattr(recent_trace_element.function, 'name')
|
||||
and recent_trace_element.function.name == 'browse_interactive'
|
||||
):
|
||||
function_calls = self.parse_browser_action(
|
||||
recent_trace_element.function.arguments['browser_actions']
|
||||
)
|
||||
for function_call in function_calls:
|
||||
action_name, action_args = function_call
|
||||
if action_name == 'fill':
|
||||
messages = []
|
||||
messages.append(
|
||||
Message(
|
||||
role='system',
|
||||
content=[TextContent(text='You are a helpful assistant')],
|
||||
)
|
||||
)
|
||||
messages.append(
|
||||
Message(
|
||||
role='user',
|
||||
content=[
|
||||
TextContent(
|
||||
text='Answer Yes or No. Is this harmful content: '
|
||||
+ repr(action_args[1])
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
assert self.guardrail_llm is not None, (
|
||||
'InvariantAnalyzer.guardrail_llm should be initialized before calling check_fillaction'
|
||||
)
|
||||
response = self.guardrail_llm.completion(
|
||||
messages=self.guardrail_llm.format_messages_for_llm(messages),
|
||||
stop=['.'],
|
||||
)
|
||||
responses.append(response)
|
||||
|
||||
for response in responses:
|
||||
if 'Yes' in response['choices'][0]['message']['content']:
|
||||
new_event = action_from_dict(
|
||||
{
|
||||
'action': 'change_agent_state',
|
||||
'args': {
|
||||
'agent_state': AgentState.ERROR,
|
||||
'thought': 'It is inappropriate for a browsing agent to fill in harmful content',
|
||||
},
|
||||
}
|
||||
)
|
||||
event_source = EventSource.AGENT
|
||||
await call_sync_from_async(
|
||||
self.event_stream.add_event, new_event, event_source
|
||||
)
|
||||
break
|
||||
|
||||
async def should_confirm(self, event: Event) -> bool:
|
||||
risk = event.security_risk if hasattr(event, 'security_risk') else None # type: ignore [attr-defined]
|
||||
return (
|
||||
risk is not None
|
||||
and risk < self.settings.get('RISK_SEVERITY', ActionSecurityRisk.MEDIUM)
|
||||
and hasattr(event, 'confirmation_state')
|
||||
and event.confirmation_state
|
||||
== ActionConfirmationStatus.AWAITING_CONFIRMATION
|
||||
)
|
||||
|
||||
async def confirm(self, event: Event) -> None:
|
||||
new_event = action_from_dict(
|
||||
{'action': 'change_agent_state', 'args': {'agent_state': 'user_confirmed'}}
|
||||
)
|
||||
# we should confirm only on agent actions
|
||||
event_source = event.source if event.source else EventSource.AGENT
|
||||
self.event_stream.add_event(new_event, event_source)
|
||||
|
||||
async def security_risk(self, event: Action) -> ActionSecurityRisk:
|
||||
logger.debug('Calling security_risk on InvariantAnalyzer')
|
||||
new_elements = parse_element(self.trace, action)
|
||||
new_elements = parse_element(self.trace, event)
|
||||
input_data = [e.model_dump(exclude_none=True) for e in new_elements]
|
||||
self.trace.extend(new_elements)
|
||||
check_result = self.monitor.check(self.input, input_data)
|
||||
@@ -123,3 +321,43 @@ class InvariantAnalyzer(SecurityAnalyzer):
|
||||
return risk
|
||||
|
||||
return self.get_risk(result)
|
||||
|
||||
### Handle API requests
|
||||
async def handle_api_request(self, request: Request) -> Any:
|
||||
path_parts = request.url.path.strip('/').split('/')
|
||||
endpoint = path_parts[-1] # Get the last part of the path
|
||||
|
||||
if request.method == 'GET':
|
||||
if endpoint == 'export-trace':
|
||||
return await self.export_trace(request)
|
||||
elif endpoint == 'policy':
|
||||
return await self.get_policy(request)
|
||||
elif endpoint == 'settings':
|
||||
return await self.get_settings(request)
|
||||
elif request.method == 'POST':
|
||||
if endpoint == 'policy':
|
||||
return await self.update_policy(request)
|
||||
elif endpoint == 'settings':
|
||||
return await self.update_settings(request)
|
||||
raise HTTPException(status_code=405, detail='Method Not Allowed')
|
||||
|
||||
async def export_trace(self, request: Request) -> JSONResponse:
|
||||
return JSONResponse(content=self.input)
|
||||
|
||||
async def get_policy(self, request: Request) -> JSONResponse:
|
||||
return JSONResponse(content={'policy': self.monitor.policy})
|
||||
|
||||
async def update_policy(self, request: Request) -> JSONResponse:
|
||||
data = await request.json()
|
||||
policy = data.get('policy')
|
||||
new_monitor = self.client.Monitor.from_string(policy)
|
||||
self.monitor = new_monitor
|
||||
return JSONResponse(content={'policy': policy})
|
||||
|
||||
async def get_settings(self, request: Request) -> JSONResponse:
|
||||
return JSONResponse(content=self.settings)
|
||||
|
||||
async def update_settings(self, request: Request) -> JSONResponse:
|
||||
settings = await request.json()
|
||||
self.settings = settings
|
||||
return JSONResponse(content=self.settings)
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
"""LLM-based security analyzers."""
|
||||
|
||||
from openhands.security.llm.analyzer import LLMRiskAnalyzer
|
||||
|
||||
__all__ = [
|
||||
'LLMRiskAnalyzer',
|
||||
]
|
||||
@@ -1,42 +0,0 @@
|
||||
"""Security analyzer that uses LLM-provided risk assessments."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import Request
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.events.action.action import Action, ActionSecurityRisk
|
||||
from openhands.security.analyzer import SecurityAnalyzer
|
||||
|
||||
|
||||
class LLMRiskAnalyzer(SecurityAnalyzer):
|
||||
"""Security analyzer that respects LLM-provided risk assessments."""
|
||||
|
||||
async def handle_api_request(self, request: Request) -> Any:
|
||||
"""Handles the incoming API request."""
|
||||
return {'status': 'ok'}
|
||||
|
||||
async def security_risk(self, action: Action) -> ActionSecurityRisk:
|
||||
"""Evaluates the Action for security risks and returns the risk level.
|
||||
|
||||
This analyzer checks if the action has a 'security_risk' attribute set by the LLM.
|
||||
If it does, it uses that value. Otherwise, it returns UNKNOWN.
|
||||
"""
|
||||
# Check if the action has a security_risk attribute set by the LLM
|
||||
if not hasattr(action, 'security_risk'):
|
||||
return ActionSecurityRisk.UNKNOWN
|
||||
|
||||
security_risk = getattr(action, 'security_risk')
|
||||
|
||||
if security_risk in {
|
||||
ActionSecurityRisk.LOW,
|
||||
ActionSecurityRisk.MEDIUM,
|
||||
ActionSecurityRisk.HIGH,
|
||||
}:
|
||||
return security_risk
|
||||
elif security_risk == ActionSecurityRisk.UNKNOWN:
|
||||
return ActionSecurityRisk.UNKNOWN
|
||||
else:
|
||||
# Default to UNKNOWN if security_risk value is not recognized
|
||||
logger.warning(f'Unrecognized security_risk value: {security_risk}')
|
||||
return ActionSecurityRisk.UNKNOWN
|
||||
@@ -1,8 +1,6 @@
|
||||
from openhands.security.analyzer import SecurityAnalyzer
|
||||
from openhands.security.invariant.analyzer import InvariantAnalyzer
|
||||
from openhands.security.llm.analyzer import LLMRiskAnalyzer
|
||||
|
||||
SecurityAnalyzers: dict[str, type[SecurityAnalyzer]] = {
|
||||
'invariant': InvariantAnalyzer,
|
||||
'llm': LLMRiskAnalyzer,
|
||||
}
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import os
|
||||
import warnings
|
||||
|
||||
import uvicorn
|
||||
|
||||
|
||||
def main():
|
||||
# Suppress SyntaxWarnings from pydub.utils about invalid escape sequences
|
||||
warnings.filterwarnings('ignore', category=SyntaxWarning, module=r'pydub\.utils')
|
||||
|
||||
uvicorn.run(
|
||||
'openhands.server.listen:app',
|
||||
host='0.0.0.0',
|
||||
|
||||
@@ -29,6 +29,7 @@ from openhands.runtime import get_runtime_cls
|
||||
from openhands.runtime.base import Runtime
|
||||
from openhands.runtime.impl.remote.remote_runtime import RemoteRuntime
|
||||
from openhands.runtime.runtime_status import RuntimeStatus
|
||||
from openhands.security import SecurityAnalyzer, options
|
||||
from openhands.server.services.conversation_stats import ConversationStats
|
||||
from openhands.storage.data_models.user_secrets import UserSecrets
|
||||
from openhands.storage.files import FileStore
|
||||
@@ -53,7 +54,7 @@ class AgentSession:
|
||||
file_store: FileStore
|
||||
controller: AgentController | None = None
|
||||
runtime: Runtime | None = None
|
||||
|
||||
security_analyzer: SecurityAnalyzer | None = None
|
||||
memory: Memory | None = None
|
||||
_starting: bool = False
|
||||
_started_at: float = 0
|
||||
@@ -132,6 +133,7 @@ class AgentSession:
|
||||
custom_secrets=custom_secrets if custom_secrets else {} # type: ignore[arg-type]
|
||||
)
|
||||
try:
|
||||
self._create_security_analyzer(config.security.security_analyzer)
|
||||
runtime_connected = await self._create_runtime(
|
||||
runtime_name=runtime_name,
|
||||
config=config,
|
||||
@@ -243,6 +245,8 @@ class AgentSession:
|
||||
await self.controller.close()
|
||||
if self.runtime is not None:
|
||||
EXECUTOR.submit(self.runtime.close)
|
||||
if self.security_analyzer is not None:
|
||||
await self.security_analyzer.close()
|
||||
|
||||
def _run_replay(
|
||||
self,
|
||||
@@ -274,6 +278,18 @@ class AgentSession:
|
||||
assert isinstance(replay_events[0], MessageAction)
|
||||
return replay_events[0]
|
||||
|
||||
def _create_security_analyzer(self, security_analyzer: str | None) -> None:
|
||||
"""Creates a SecurityAnalyzer instance that will be used to analyze the agent actions
|
||||
|
||||
Parameters:
|
||||
- security_analyzer: The name of the security analyzer to use
|
||||
"""
|
||||
if security_analyzer:
|
||||
self.logger.debug(f'Using security analyzer: {security_analyzer}')
|
||||
self.security_analyzer = options.SecurityAnalyzers.get(
|
||||
security_analyzer, SecurityAnalyzer
|
||||
)(self.event_stream)
|
||||
|
||||
def override_provider_tokens_with_custom_secret(
|
||||
self,
|
||||
git_provider_tokens: PROVIDER_TOKEN_TYPE | None,
|
||||
@@ -445,7 +461,6 @@ class AgentSession:
|
||||
status_callback=self._status_callback,
|
||||
initial_state=initial_state,
|
||||
replay_events=replay_events,
|
||||
security_analyzer=self.runtime.security_analyzer if self.runtime else None,
|
||||
)
|
||||
|
||||
return (controller, initial_state is not None)
|
||||
|
||||
@@ -5,6 +5,7 @@ from openhands.events.stream import EventStream
|
||||
from openhands.llm.llm_registry import LLMRegistry
|
||||
from openhands.runtime import get_runtime_cls
|
||||
from openhands.runtime.base import Runtime
|
||||
from openhands.security import SecurityAnalyzer, options
|
||||
from openhands.storage.files import FileStore
|
||||
from openhands.utils.async_utils import call_sync_from_async
|
||||
|
||||
@@ -35,6 +36,11 @@ class ServerConversation:
|
||||
event_stream = EventStream(sid, file_store, user_id)
|
||||
self.event_stream = event_stream
|
||||
|
||||
if config.security.security_analyzer:
|
||||
self.security_analyzer = options.SecurityAnalyzers.get(
|
||||
config.security.security_analyzer, SecurityAnalyzer
|
||||
)(self.event_stream)
|
||||
|
||||
if runtime:
|
||||
self._attach_to_existing = True
|
||||
else:
|
||||
@@ -49,11 +55,6 @@ class ServerConversation:
|
||||
)
|
||||
self.runtime = runtime
|
||||
|
||||
@property
|
||||
def security_analyzer(self):
|
||||
"""Access security analyzer through runtime."""
|
||||
return self.runtime.security_analyzer
|
||||
|
||||
async def connect(self) -> None:
|
||||
if not self._attach_to_existing:
|
||||
await self.runtime.connect()
|
||||
|
||||
@@ -118,9 +118,7 @@ class Session:
|
||||
else settings.confirmation_mode
|
||||
)
|
||||
self.config.security.security_analyzer = (
|
||||
self.config.security.security_analyzer
|
||||
if settings.security_analyzer is None
|
||||
else settings.security_analyzer
|
||||
settings.security_analyzer or self.config.security.security_analyzer
|
||||
)
|
||||
self.config.sandbox.base_container_image = (
|
||||
settings.sandbox_base_container_image
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user