Skip to content

Commit 07bb5e5

Browse files
Add OAuth 2.0 Device Flow CLI authentication (#192)
* Add OAuth 2.0 Device Flow CLI authentication - Implement 'openhands login' command with OAuth 2.0 Device Flow - Add secure encrypted token storage for API keys - Implement 'openhands logout' command for credential management - Add user settings storage for future synchronization - Integrate authentication commands into main CLI parser - Add required dependencies: httpx, cryptography, python-dotenv This enables CLI users to authenticate with OpenHands Cloud and receive API keys for accessing cloud features. Co-authored-by: openhands <openhands@all-hands.dev> * Simplify OAuth 2.0 Device Flow client implementation - Remove client_id and scope requirements from device authorization requests - Simplify DeviceFlowClient constructor to only require server_url - Replace complex encrypted token storage with simple plain text API key storage - Store API key directly in PERSISTENCE_DIR/api_key.txt for simplicity - Maintain backward compatibility with legacy token storage methods - Match simplified server-side implementation that no longer requires client_id/scope Co-authored-by: openhands <openhands@all-hands.dev> * Add configurable cloud URL support via OPENHANDS_CLOUD_URL environment variable - Add OPENHANDS_CLOUD_URL environment variable support for login command - Default remains https://app.all-hands.dev if environment variable is not set - Environment variable can be overridden by --server-url command line argument - Update help text to document the environment variable option Co-authored-by: openhands <openhands@all-hands.dev> * Remove grant_type from device token requests Simplify the OAuth Device Flow client by removing the unnecessary grant_type field from token polling requests. The server no longer requires or validates this field. Co-authored-by: openhands <openhands@all-hands.dev> * Update uv.lock * Implement LLM API key and settings fetch after OAuth completion - Add OpenHandsApiClient class to fetch LLM API key and user settings - Enhance login flow to call /api/keys/llm/byor and /api/settings endpoints - Create Agent object with OpenHands provider and fetched configuration - Add LLM summarizing condenser by default - Save Agent configuration using AgentStore - Add comprehensive error handling with user-friendly messages - Create utility functions for fetching user data Co-authored-by: openhands <openhands@all-hands.dev> * Improve login flow: pull settings from remote when already logged in Instead of exiting early and asking users to logout first when they are already logged in, the login command now: 1. Detects existing authentication tokens 2. Uses existing tokens to pull latest settings from remote 3. Synchronizes LLM API key and user settings locally 4. Provides appropriate feedback to the user This provides a better user experience by allowing users to refresh their local settings without having to logout and login again. Also fixes a bug in UserSettings._get_encryption_key() that was trying to call a non-existent method on TokenStorage. Co-authored-by: openhands <openhands@all-hands.dev> * Remove encryption from settings and token storage - Remove all encryption code from UserSettings class - Store user settings as plain JSON files instead of encrypted data - Remove cryptography dependency from pyproject.toml - Update uv.lock to reflect dependency changes - Token storage already used plain text files This simplifies the codebase and removes the complexity of encryption for local settings storage. Settings files are still protected by file system permissions (600). Co-authored-by: openhands <openhands@all-hands.dev> * Remove custom UserSettings class - replaced with Agent object and AgentStore.save The login command now uses Agent object with values from BYOR endpoint (LLM key) and /settings endpoint (model value), then saves using AgentStore.save(). This removes the custom settings class in favor of the standard Agent approach. Co-authored-by: openhands <openhands@all-hands.dev> * Add detailed logging when saving agent configuration Shows what configurations were saved (model, provider, tools count, condenser settings) and where they were saved, while keeping the LLM API key secure. Co-authored-by: openhands <openhands@all-hands.dev> * Create separate LLM objects for agent and condenser Ensures different usage tracking by creating separate LLM instances for the main agent and the condenser, allowing proper differentiation in usage analytics. Co-authored-by: openhands <openhands@all-hands.dev> * Fix LLM configuration: remove custom provider and add usage_id - Remove custom_llm_provider='openhands' since openhands is a basic provider - Add usage_id='agent' for main LLM and usage_id='condenser' for condenser LLM - Update logging to show usage_id instead of provider Co-authored-by: openhands <openhands@all-hands.dev> * Add base_url to LLM configurations Set base_url to 'https://llm-proxy.app.all-hands.dev/' for both agent and condenser LLMs to route requests through OpenHands LLM proxy. Also added base_url to configuration logging output. Co-authored-by: openhands <openhands@all-hands.dev> * rm lib * Refactor auth parsers into separate file - Move login and logout parser definitions to auth_parser.py - Update main_parser.py to use helper functions for auth parsers - Fix server parser to use helper function instead of inline definition - Remove unused os import from main_parser.py Co-authored-by: openhands <openhands@all-hands.dev> * add back acp parser * Delete __init__.py * Simplify and deduplicate OAuth Device Flow implementation - Create shared BaseHttpClient for centralized HTTP handling and error extraction - Refactor DeviceFlowClient and OpenHandsApiClient to use BaseHttpClient - Remove legacy token storage methods (store_tokens, get_tokens, etc.) - Remove global get_token_storage() function, use direct TokenStorage() instantiation - Update all callers to use simplified API key interface - Net code reduction: 118 lines (216 deletions - 98 insertions) Co-authored-by: openhands <openhands@all-hands.dev> * Fix formatting after merge Co-authored-by: openhands <openhands@all-hands.dev> * fix import * cleanup client * Delete user_data.py * Update device_flow.py * simplify * Add comprehensive unit tests for OAuth device flow authentication - Created tests/auth/ directory with 83 unit tests covering all auth modules - test_token_storage.py: 15 tests for token storage, validation, and edge cases - test_http_client.py: 16 tests for HTTP requests, error handling, timeouts - test_device_flow.py: 20 tests for OAuth 2.0 Device Flow and token polling - test_api_client.py: 14 tests for API client and user data fetching - test_login_command.py: 11 tests for login flows and error scenarios - test_logout_command.py: 7 tests for logout scenarios and cleanup - All tests pass with comprehensive mocking and async/await support - Covers critical paths, error handling, and security scenarios Co-authored-by: openhands <openhands@all-hands.dev> * Fix linting issues in auth tests - Remove unused mock_print variables - Fix line length issues in comments - Fix HTTPStatusError constructor in HTTP client test - Add missing MagicMock import Co-authored-by: openhands <openhands@all-hands.dev> * feat: enhance device flow with automatic browser opening and URL user_code parameter - Update device flow URL to include user_code parameter: {server_url}/oauth/device/verify?user_code={USER_CODE} - Add automatic browser opening using webbrowser module - Remove manual code entry instructions from user flow - Add graceful fallback when browser opening fails - Update tests to cover new browser functionality - Maintain backward compatibility and error handling Co-authored-by: openhands <openhands@all-hands.dev> * Fix auth messaging and refactor polling - Fix LLM API key display to show only 3 characters instead of 10 - Simplify login/logout messages to use 'OpenHands Cloud' instead of server URLs - Add helpful error message for sync failures suggesting re-login - Refactor poll_for_token from attempt-based to timeout-based polling (600s default) - Create shared utils.py with _p function to deduplicate across auth files - Update all tests to match new messaging - All linting checks pass Co-authored-by: openhands <openhands@all-hands.dev> * Fix formatting: add newline at end of utils.py Co-authored-by: openhands <openhands@all-hands.dev> * Replace exit(1) with sys.exit(1) in simple_main.py - Add sys import to simple_main.py - Replace exit(1) calls with sys.exit(1) for consistency - All other files already use sys.exit(1) correctly Co-authored-by: openhands <openhands@all-hands.dev> * Refactor auth module to improve separation of concerns - Move agent creation logic from auth module to AgentStore - Add create_and_save_from_settings method to AgentStore - Remove duplicate DEFAULT_MODEL and DEFAULT_LLM_BASE_URL constants - Update default model to claude-sonnet-4-5-20250929 - Improve condenser handling with proper isinstance checks - Simplify condenser creation to use class defaults - Update tests to match refactored function signatures Co-authored-by: openhands <openhands@all-hands.dev> * Update login_command.py * Fix OAuth device flow token endpoint to use form data - Update HTTP client to support application/x-www-form-urlencoded data - Change device flow client to send form data instead of JSON to /oauth/device/token - Update tests to reflect the new form data usage - Ensures RFC 8628 compliance with server endpoint changes Co-authored-by: openhands <openhands@all-hands.dev> * Fix linting issues: shorten docstring lines - Shortened form_data parameter descriptions to fit line length limit - All linting checks now pass Co-authored-by: openhands <openhands@all-hands.dev> * Fix code formatting in test file - Apply automatic formatting to test_http_client.py - Ensures consistent code style Co-authored-by: openhands <openhands@all-hands.dev> * Update token storage path to .openhands/cloud and add secure permissions - Change TokenStorage to store API keys in ~/.openhands/cloud/ instead of ~/.openhands/ - Add chmod 600 permissions when storing API keys for better security - Update tests to reflect new path structure - Add test for file permissions verification Co-authored-by: openhands <openhands@all-hands.dev> * Add user consent before overriding existing agent settings - Modified create_and_save_from_settings to check for existing configurations - Added interactive prompt asking user permission before overwriting - Shows comparison between current and new settings - Added force_overwrite parameter to skip consent when needed - Updated API client to handle user consent gracefully - Preserves existing configuration when user declines Fixes issue where OAuth login would silently overwrite manually configured settings * Refactor consent logic: move UI interaction from AgentStore to API client - Moved user consent logic from AgentStore.create_and_save_from_settings to API client - AgentStore now only handles data persistence (load/save operations) - API client handles user interaction and consent before calling AgentStore - Added proper HTML escaping for model names and URLs in consent prompt - Updated test to mock the new load() method call - Maintains separation of concerns: data layer vs presentation layer This addresses the architectural concern about UI logic being in the data store. Co-authored-by: openhands <openhands@all-hands.dev> * Fix line length linting issue - Split long HTML string in consent prompt to comply with 88 character limit Co-authored-by: openhands <openhands@all-hands.dev> * use simple input * Replace hardcoded colors with OPENHANDS_THEME in auth module - Replace all hardcoded color tags (<green>, <red>, <yellow>, <cyan>, <white>) with OPENHANDS_THEME variables - Fix missing f-string prefixes in 5 locations where {OPENHANDS_THEME.*} variables weren't being interpolated - Simplify _p() function in auth/utils.py to only handle console printing - Fix all line length violations (E501) by breaking long lines appropriately - All auth modules now use rich library with theme-based colors for consistency - All linting checks now pass (ruff format, ruff lint, pycodestyle, pyright) Files modified: - openhands_cli/auth/utils.py: Simplified _p() function - openhands_cli/auth/device_flow.py: Theme colors + formatting fixes - openhands_cli/auth/login_command.py: Theme colors + formatting fixes - openhands_cli/auth/logout_command.py: Theme colors + formatting fixes - openhands_cli/auth/api_client.py: Theme colors + formatting fixes Color mapping: - <green> → [{OPENHANDS_THEME.success}] (#ffe165) - <red> → [{OPENHANDS_THEME.error}] (#ff6b6b) - <yellow> → [{OPENHANDS_THEME.warning}] (#ffe165) - <cyan> → [{OPENHANDS_THEME.accent}] (#277dff) - <white> → [{OPENHANDS_THEME.secondary}] (#ffffff) * fix imports * update messaging * Fix failing test in test_login_command.py - Update test_fetch_user_data_with_context_new_login_success to check for the correct message - The test was expecting 'cloud features' but the function actually prints 'synchronized successfully' - The 'cloud features' message is printed in the main login_command function, not in _fetch_user_data_with_context Co-authored-by: openhands <openhands@all-hands.dev> --------- Co-authored-by: openhands <openhands@all-hands.dev>
1 parent d801798 commit 07bb5e5

22 files changed

Lines changed: 2802 additions & 10 deletions
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import argparse
2+
3+
from openhands_cli.argparsers.util import add_confirmation_mode_args
4+
5+
6+
def add_acp_parser(subparsers: argparse._SubParsersAction) -> argparse.ArgumentParser:
7+
# Add ACP subcommand
8+
acp_parser = subparsers.add_parser(
9+
"acp", help="Start OpenHands as an Agent Client Protocol (ACP) agent"
10+
)
11+
12+
# ACP confirmation mode options (mutually exclusive)
13+
acp_confirmation_group = acp_parser.add_mutually_exclusive_group()
14+
add_confirmation_mode_args(acp_confirmation_group)
15+
16+
return acp_parser
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""Argument parser for authentication subcommands."""
2+
3+
import argparse
4+
import os
5+
6+
7+
def add_login_parser(subparsers: argparse._SubParsersAction) -> argparse.ArgumentParser:
8+
"""Add login subcommand parser.
9+
10+
Args:
11+
subparsers: The subparsers object to add the login parser to
12+
13+
Returns:
14+
The login argument parser
15+
"""
16+
login_parser = subparsers.add_parser(
17+
"login", help="Authenticate with OpenHands Cloud using OAuth 2.0 Device Flow"
18+
)
19+
default_cloud_url = os.getenv("OPENHANDS_CLOUD_URL", "https://app.all-hands.dev")
20+
login_parser.add_argument(
21+
"--server-url",
22+
type=str,
23+
default=default_cloud_url,
24+
help=(
25+
f"OpenHands server URL (default: {default_cloud_url}, "
26+
"configurable via OPENHANDS_CLOUD_URL env var)"
27+
),
28+
)
29+
return login_parser
30+
31+
32+
def add_logout_parser(
33+
subparsers: argparse._SubParsersAction,
34+
) -> argparse.ArgumentParser:
35+
"""Add logout subcommand parser.
36+
37+
Args:
38+
subparsers: The subparsers object to add the logout parser to
39+
40+
Returns:
41+
The logout argument parser
42+
"""
43+
logout_parser = subparsers.add_parser("logout", help="Log out from OpenHands Cloud")
44+
logout_parser.add_argument(
45+
"--server-url",
46+
type=str,
47+
help=(
48+
"OpenHands server URL to log out from "
49+
"(if not specified, logs out from all servers)"
50+
),
51+
)
52+
return logout_parser

openhands_cli/argparsers/main_parser.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import argparse
44

55
from openhands_cli import __version__
6+
from openhands_cli.argparsers.acp_parser import add_acp_parser
7+
from openhands_cli.argparsers.auth_parser import add_login_parser, add_logout_parser
68
from openhands_cli.argparsers.mcp_parser import add_mcp_parser
79
from openhands_cli.argparsers.serve_parser import add_serve_parser
810
from openhands_cli.argparsers.utils import add_confirmation_mode_args
@@ -35,6 +37,8 @@ def create_main_parser() -> argparse.ArgumentParser:
3537
openhands serve --gpu # Launch with GPU support
3638
openhands acp # Agent-Client Protocol
3739
server (e.g., Zed IDE)
40+
openhands login # Authenticate with OpenHands Cloud
41+
openhands logout # Log out from OpenHands Cloud
3842
""",
3943
)
4044

@@ -102,19 +106,17 @@ def create_main_parser() -> argparse.ArgumentParser:
102106
# Subcommands
103107
subparsers = parser.add_subparsers(dest="command", help="Additional commands")
104108

109+
# Add acp subcommands
110+
add_acp_parser(subparsers)
111+
105112
# Add serve subcommand
106113
add_serve_parser(subparsers)
107114

108-
# Add ACP subcommand
109-
acp_parser = subparsers.add_parser(
110-
"acp", help="Start OpenHands as an Agent Client Protocol (ACP) agent"
111-
)
112-
113-
# ACP confirmation mode options (mutually exclusive)
114-
acp_confirmation_group = acp_parser.add_mutually_exclusive_group()
115-
add_confirmation_mode_args(acp_confirmation_group)
116-
117115
# Add MCP subcommand
118116
add_mcp_parser(subparsers)
119117

118+
# Add authentication subcommands
119+
add_login_parser(subparsers)
120+
add_logout_parser(subparsers)
121+
120122
return parser

openhands_cli/argparsers/util.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import argparse
2+
3+
4+
def add_confirmation_mode_args(
5+
parser_or_group: argparse.ArgumentParser | argparse._MutuallyExclusiveGroup,
6+
) -> None:
7+
"""Add confirmation mode arguments to a parser or mutually exclusive group.
8+
9+
Args:
10+
parser_or_group: Either an ArgumentParser or a mutually exclusive group
11+
"""
12+
parser_or_group.add_argument(
13+
"--always-approve",
14+
action="store_true",
15+
help="Auto-approve all actions without asking for confirmation",
16+
)
17+
parser_or_group.add_argument(
18+
"--llm-approve",
19+
action="store_true",
20+
help=(
21+
"Enable LLM-based security analyzer "
22+
"(only confirm LLM-predicted high-risk actions)"
23+
),
24+
)

0 commit comments

Comments
 (0)