Skip to content

Add get_llm(), get_secrets(), and get_mcp_config() methods to RemoteWorkspace#3077

Merged
xingyaoww merged 6 commits intomainfrom
remote-workspace-settings-methods
May 6, 2026
Merged

Add get_llm(), get_secrets(), and get_mcp_config() methods to RemoteWorkspace#3077
xingyaoww merged 6 commits intomainfrom
remote-workspace-settings-methods

Conversation

@malhotra5
Copy link
Copy Markdown
Collaborator

@malhotra5 malhotra5 commented May 6, 2026

Summary

Add default implementations of get_llm(), get_secrets(), and get_mcp_config() methods to the base RemoteWorkspace class that fetch configuration from the agent-server's persisted settings endpoints.

Closes #3076

Fixes AGE-1451

Related

Changes

New Methods on RemoteWorkspace

  1. get_llm(**llm_kwargs) -> LLM

    • Calls GET /api/settings with X-Expose-Secrets: plaintext header
    • Extracts LLM configuration (model, api_key, base_url) from agent_settings.llm
    • Returns a configured LLM instance
    • Supports kwargs to override persisted settings
  2. get_secrets(names: list[str] | None = None) -> dict[str, LookupSecret]

    • Calls GET /api/settings/secrets to list available secrets
    • Returns LookupSecret references pointing to /api/settings/secrets/{name}
    • Supports filtering by specific secret names
    • Includes X-Session-API-Key header for authentication
  3. get_mcp_config() -> dict[str, Any]

    • Calls GET /api/settings with X-Expose-Secrets: plaintext header
    • Extracts MCP configuration from agent_settings.mcp_config
    • Returns dict compatible with MCPConfig.model_validate()

New Example

Added examples/02_remote_agent_server/13_workspace_get_llm.py demonstrating:

  • Spinning up an agent-server with session API key authentication
  • Configuring LLM settings via the Settings API
  • Using workspace.get_llm() to retrieve configured LLM
  • Starting a conversation with the retrieved LLM
  • Using workspace.get_secrets() and workspace.get_mcp_config()

Design Decision

These methods are placed on RemoteWorkspace (not a specific subclass like APIRemoteWorkspace) because:

  1. All remote workspaces connect to an agent-server - The agent-server has /api/settings endpoints
  2. DRY - DockerWorkspace, ApptainerWorkspace, APIRemoteWorkspace all inherit automatically
  3. Override pattern - OpenHandsCloudWorkspace already overrides with Cloud API calls
RemoteWorkspace (base - has default get_llm/get_secrets/get_mcp_config)
├── OpenHandsCloudWorkspace (OVERRIDES to use Cloud API)
├── APIRemoteWorkspace (inherits default implementations)
├── DockerWorkspace (inherits default implementations)
└── ApptainerWorkspace (inherits default implementations)

Usage Examples

# With DockerWorkspace
with DockerWorkspace(...) as workspace:
    llm = workspace.get_llm()
    secrets = workspace.get_secrets()
    mcp_config = workspace.get_mcp_config()

    conversation.update_secrets(secrets)
    agent = Agent(llm=llm, mcp_config=mcp_config, tools=...)

# With APIRemoteWorkspace
with APIRemoteWorkspace(...) as workspace:
    llm = workspace.get_llm(model="gpt-4o")  # Override model
    gh_token = workspace.get_secrets(names=["GITHUB_TOKEN"])

Testing

  • Added 11 new unit tests covering:
    • Successful LLM retrieval with persisted settings
    • Kwargs override for LLM settings
    • Secret list retrieval and filtering
    • MCP config retrieval
    • Error handling for undefined host
    • Empty responses handling

All 36 tests in test_remote_workspace.py pass.


This PR was created by an AI agent (OpenHands) on behalf of a user.


Agent Server images for this PR

GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22-slim Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:270c233-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-270c233-python \
  ghcr.io/openhands/agent-server:270c233-python

All tags pushed for this build

ghcr.io/openhands/agent-server:270c233-golang-amd64
ghcr.io/openhands/agent-server:270c233-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:270c233-golang-arm64
ghcr.io/openhands/agent-server:270c233-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:270c233-java-amd64
ghcr.io/openhands/agent-server:270c233-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:270c233-java-arm64
ghcr.io/openhands/agent-server:270c233-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:270c233-python-amd64
ghcr.io/openhands/agent-server:270c233-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-amd64
ghcr.io/openhands/agent-server:270c233-python-arm64
ghcr.io/openhands/agent-server:270c233-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-arm64
ghcr.io/openhands/agent-server:270c233-golang
ghcr.io/openhands/agent-server:270c233-java
ghcr.io/openhands/agent-server:270c233-python

About Multi-Architecture Support

  • Each variant tag (e.g., 270c233-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., 270c233-python-amd64) are also available if needed

…orkspace

Add default implementations of settings methods to RemoteWorkspace base class
that fetch configuration from the agent-server's persisted settings endpoints.
This enables DockerWorkspace, APIRemoteWorkspace, and other RemoteWorkspace
subclasses to retrieve LLM config, secrets, and MCP config from the agent-server.

- get_llm(): Fetches LLM settings from /api/settings with X-Expose-Secrets: plaintext
- get_secrets(): Returns LookupSecret references for lazy secret resolution
- get_mcp_config(): Returns MCP configuration in MCPConfig-compatible format

OpenHandsCloudWorkspace already overrides these methods to use Cloud API endpoints.

Closes #3076

Co-authored-by: openhands <openhands@all-hands.dev>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

Python API breakage checks — ✅ PASSED

Result:PASSED

Action log

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

REST API breakage checks (OpenAPI) — ✅ PASSED

Result:PASSED

Action log

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-agent-server/openhands/agent_server
   settings_router.py1411092%86, 218, 289, 291–292, 390, 392–393, 431, 436
openhands-agent-server/openhands/agent_server/persistence
   models.py1452284%155, 187, 241, 276–282, 284, 286, 289, 293, 319, 336–337, 368–371, 374
openhands-sdk/openhands/sdk/settings
   api_models.py25484%69, 71, 79, 81
openhands-sdk/openhands/sdk/workspace/remote
   base.py1312084%34, 63–68, 109–113, 190–192, 206–208, 257, 434
TOTAL25705623975% 

Add example 13 showing how to:
1. Spin up an agent-server
2. Configure LLM settings via the Settings API
3. Use workspace.get_llm() to retrieve a configured LLM
4. Start a conversation using the retrieved LLM
5. Demonstrate get_secrets() and get_mcp_config() methods

Co-authored-by: openhands <openhands@all-hands.dev>
- Generate random session API key for agent-server
- Demonstrate 401 rejection without API key
- Show RemoteWorkspace.api_key usage for authenticated requests
- Verify LookupSecrets include auth headers for resolution

Co-authored-by: openhands <openhands@all-hands.dev>
@malhotra5 malhotra5 marked this pull request as ready for review May 6, 2026 15:00
Copy link
Copy Markdown
Collaborator

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Good taste - Clean implementation of settings retrieval methods with proper authentication, retry logic, and comprehensive tests.

[IMPROVEMENT OPPORTUNITIES]

  • [openhands-sdk/openhands/sdk/workspace/remote/base.py, Lines 307-313] Defensive Programming: The conditional dict building (if llm_config.get("model")) could hide configuration errors. If the server returns empty strings, they're silently skipped. Consider preserving all values and letting the LLM constructor validate them.

  • [openhands-sdk/openhands/sdk/workspace/remote/base.py, Line 432] Unclear Comment: The comment mentions "legacy format" but doesn't explain what it is. If no legacy format exists, this is dead code that should be removed. If there is a legacy format, document it.

[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🟢 LOW

Additive feature that provides convenience methods for fetching settings from agent-server. Well-tested with proper authentication and retry logic. No existing behavior modified.

VERDICT:
Worth merging: Core logic is sound, minor improvements suggested

KEY INSIGHT:
Provides a clean abstraction for remote workspaces to fetch LLM/secrets/MCP configuration from agent-server settings, following the existing pattern where OpenHandsCloudWorkspace can override for Cloud API endpoints.

@malhotra5 malhotra5 added the test-examples Run all applicable "examples/" files. Expensive operation. label May 6, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

🔄 Running Examples with openhands/claude-haiku-4-5-20251001

Generated: 2026-05-06 15:31:27 UTC

Example Status Duration Cost
01_standalone_sdk/02_custom_tools.py ✅ PASS 24.9s $0.03
01_standalone_sdk/03_activate_skill.py ✅ PASS 22.1s $0.03
01_standalone_sdk/05_use_llm_registry.py ✅ PASS 11.5s $0.01
01_standalone_sdk/07_mcp_integration.py ✅ PASS 37.0s $0.03
01_standalone_sdk/09_pause_example.py ✅ PASS 14.5s $0.01
01_standalone_sdk/10_persistence.py ✅ PASS 47.2s $0.04
01_standalone_sdk/11_async.py ✅ PASS 33.0s $0.04
01_standalone_sdk/12_custom_secrets.py ✅ PASS 9.3s $0.00
01_standalone_sdk/13_get_llm_metrics.py ✅ PASS 30.6s $0.02
01_standalone_sdk/14_context_condenser.py ✅ PASS 3m 7s $0.20
01_standalone_sdk/17_image_input.py ✅ PASS 22.6s $0.02
01_standalone_sdk/18_send_message_while_processing.py ✅ PASS 25.8s $0.02
01_standalone_sdk/19_llm_routing.py ✅ PASS 18.0s $0.02
01_standalone_sdk/20_stuck_detector.py ✅ PASS 14.9s $0.02
01_standalone_sdk/21_generate_extraneous_conversation_costs.py ✅ PASS 10.4s $0.00
01_standalone_sdk/22_anthropic_thinking.py ✅ PASS 15.7s $0.01
01_standalone_sdk/23_responses_reasoning.py ✅ PASS 1m 48s $0.02
01_standalone_sdk/24_planning_agent_workflow.py ✅ PASS 5m 22s $0.35
01_standalone_sdk/25_agent_delegation.py ✅ PASS 54.4s $0.06
01_standalone_sdk/26_custom_visualizer.py ✅ PASS 17.6s $0.02
01_standalone_sdk/28_ask_agent_example.py ✅ PASS 44.1s $0.05
01_standalone_sdk/29_llm_streaming.py ✅ PASS 38.9s $0.03
01_standalone_sdk/30_tom_agent.py ✅ PASS 9.4s $0.01
01_standalone_sdk/31_iterative_refinement.py ✅ PASS 4m 56s $0.37
01_standalone_sdk/32_configurable_security_policy.py ✅ PASS 17.4s $0.02
01_standalone_sdk/34_critic_example.py ✅ PASS 6m 24s $0.58
01_standalone_sdk/36_event_json_to_openai_messages.py ✅ PASS 9.8s $0.01
01_standalone_sdk/37_llm_profile_store/main.py ✅ PASS 4.0s $0.00
01_standalone_sdk/38_browser_session_recording.py ✅ PASS 38.3s $0.03
01_standalone_sdk/39_llm_fallback.py ✅ PASS 10.2s $0.01
01_standalone_sdk/40_acp_agent_example.py ✅ PASS 45.4s $0.13
01_standalone_sdk/41_task_tool_set.py ✅ PASS 31.5s $0.03
01_standalone_sdk/42_file_based_subagents.py ✅ PASS 1m 25s $0.08
01_standalone_sdk/43_mixed_marketplace_skills/main.py ✅ PASS 6.9s $0.00
01_standalone_sdk/44_model_switching_in_convo.py ✅ PASS 8.5s $0.01
01_standalone_sdk/45_parallel_tool_execution.py ✅ PASS 3m 25s $0.57
01_standalone_sdk/46_agent_settings.py ✅ PASS 11.6s $0.01
01_standalone_sdk/47_defense_in_depth_security.py ✅ PASS 3.3s $0.00
01_standalone_sdk/48_conversation_fork.py ✅ PASS 12.4s $0.00
02_remote_agent_server/01_convo_with_local_agent_server.py ✅ PASS 35.2s $0.02
02_remote_agent_server/02_convo_with_docker_sandboxed_server.py ✅ PASS 1m 33s $0.05
02_remote_agent_server/03_browser_use_with_docker_sandboxed_server.py ✅ PASS 1m 42s --
02_remote_agent_server/04_convo_with_api_sandboxed_server.py ✅ PASS 1m 21s $0.04
02_remote_agent_server/07_convo_with_cloud_workspace.py ✅ PASS 29.5s $0.04
02_remote_agent_server/08_convo_with_apptainer_sandboxed_server.py ✅ PASS 3m 22s $0.02
02_remote_agent_server/09_acp_agent_with_remote_runtime.py ✅ PASS 1m 11s $0.12
02_remote_agent_server/10_cloud_workspace_share_credentials.py ✅ PASS 1m 23s $0.06
02_remote_agent_server/11_conversation_fork.py ✅ PASS 32.9s $0.00
02_remote_agent_server/12_settings_and_secrets_api.py ✅ PASS 2m 10s $0.02
02_remote_agent_server/13_workspace_get_llm.py ✅ PASS 19.4s $0.01
04_llm_specific_tools/01_gpt5_apply_patch_preset.py ✅ PASS 20.5s $0.01
04_llm_specific_tools/02_gemini_file_tools.py ✅ PASS 43.8s $0.08
05_skills_and_plugins/01_loading_agentskills/main.py ✅ PASS 16.0s $0.02
05_skills_and_plugins/02_loading_plugins/main.py ✅ PASS 22.2s $0.02

✅ All tests passed!

Total: 54 | Passed: 54 | Failed: 0 | Total Cost: $3.44

View full workflow run

Comment thread openhands-sdk/openhands/sdk/workspace/remote/base.py Outdated
Comment thread openhands-sdk/openhands/sdk/workspace/remote/base.py Outdated
Address review feedback:
1. Pass all llm_config fields to LLM constructor, not just model/api_key/base_url
2. Move response models from agent-server to SDK for sharing:
   - SettingsResponse, SettingsUpdateRequest
   - SecretsListResponse, SecretItemResponse, SecretCreateRequest
3. Use Pydantic model_validate() for proper response validation
4. Agent-server now imports these models from SDK

This enables SDK clients (RemoteWorkspace) to validate responses using
the same models used by agent-server endpoints, eliminating duplication
and ensuring contract consistency.

Co-authored-by: openhands <openhands@all-hands.dev>
Comment thread examples/02_remote_agent_server/13_workspace_get_llm.py
Build on the previous commit's SettingsResponse validation by also
validating the agent_settings dict through validate_agent_settings(),
which produces a fully typed AgentSettingsConfig (OpenHandsAgentSettings
or ACPAgentSettings).

This gives:
- get_llm(): returns settings.llm directly (a real LLM instance with
  all persisted fields, not just model/api_key/base_url)
- get_mcp_config(): accesses settings.mcp_config (typed MCPConfig),
  uses isinstance check to correctly handle ACPAgentSettings which
  has no mcp_config field
- _fetch_agent_settings(): shared helper that calls GET /api/settings,
  validates the outer SettingsResponse, then validates the inner
  agent_settings dict through the SDK's discriminated union

Co-authored-by: openhands <openhands@all-hands.dev>
Comment thread openhands-sdk/openhands/sdk/settings/api_models.py
…afety

- Add get_agent_settings() method that validates and returns AgentSettingsConfig
- Add get_conversation_settings() method that validates and returns ConversationSettings
- Keep dict[str, Any] fields since server needs to control secret serialization via context
- Document why typed fields aren't used (FastAPI serialization loses context)
- Provide examples in docstrings for client usage

Co-authored-by: openhands <openhands@all-hands.dev>
Copy link
Copy Markdown
Collaborator

@xingyaoww xingyaoww left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM once you've confirmed example 02-13 works!

@malhotra5 malhotra5 added test-examples Run all applicable "examples/" files. Expensive operation. and removed test-examples Run all applicable "examples/" files. Expensive operation. labels May 6, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

🔄 Running Examples with openhands/claude-haiku-4-5-20251001

Generated: 2026-05-06 21:15:58 UTC

Example Status Duration Cost
01_standalone_sdk/02_custom_tools.py ✅ PASS 23.0s $0.03
01_standalone_sdk/03_activate_skill.py ✅ PASS 23.6s $0.03
01_standalone_sdk/05_use_llm_registry.py ✅ PASS 12.9s $0.01
01_standalone_sdk/07_mcp_integration.py ✅ PASS 33.8s $0.03
01_standalone_sdk/09_pause_example.py ✅ PASS 13.2s $0.01
01_standalone_sdk/10_persistence.py ✅ PASS 31.9s $0.03
01_standalone_sdk/11_async.py ✅ PASS 34.7s $0.04
01_standalone_sdk/12_custom_secrets.py ✅ PASS 9.9s $0.00
01_standalone_sdk/13_get_llm_metrics.py ✅ PASS 41.4s $0.04
01_standalone_sdk/14_context_condenser.py ✅ PASS 3m 29s $0.22
01_standalone_sdk/17_image_input.py ✅ PASS 20.8s $0.02
01_standalone_sdk/18_send_message_while_processing.py ✅ PASS 16.5s $0.01
01_standalone_sdk/19_llm_routing.py ✅ PASS 16.0s $0.02
01_standalone_sdk/20_stuck_detector.py ✅ PASS 15.4s $0.02
01_standalone_sdk/21_generate_extraneous_conversation_costs.py ✅ PASS 10.7s $0.00
01_standalone_sdk/22_anthropic_thinking.py ✅ PASS 21.4s $0.01
01_standalone_sdk/23_responses_reasoning.py ✅ PASS 1m 26s $0.01
01_standalone_sdk/24_planning_agent_workflow.py ✅ PASS 4m 3s $0.31
01_standalone_sdk/25_agent_delegation.py ✅ PASS 1m 4s $0.07
01_standalone_sdk/26_custom_visualizer.py ✅ PASS 19.5s $0.02
01_standalone_sdk/28_ask_agent_example.py ✅ PASS 32.9s $0.04
01_standalone_sdk/29_llm_streaming.py ✅ PASS 37.4s $0.03
01_standalone_sdk/30_tom_agent.py ✅ PASS 9.1s $0.01
01_standalone_sdk/31_iterative_refinement.py ✅ PASS 5m 20s $0.36
01_standalone_sdk/32_configurable_security_policy.py ✅ PASS 18.1s $0.02
01_standalone_sdk/34_critic_example.py ✅ PASS 1m 25s $0.11
01_standalone_sdk/36_event_json_to_openai_messages.py ✅ PASS 12.8s $0.01
01_standalone_sdk/37_llm_profile_store/main.py ✅ PASS 7.6s $0.00
01_standalone_sdk/38_browser_session_recording.py ✅ PASS 34.2s $0.03
01_standalone_sdk/39_llm_fallback.py ✅ PASS 9.8s $0.01
01_standalone_sdk/40_acp_agent_example.py ✅ PASS 27.0s $0.13
01_standalone_sdk/41_task_tool_set.py ✅ PASS 20.2s $0.02
01_standalone_sdk/42_file_based_subagents.py ✅ PASS 1m 26s $0.08
01_standalone_sdk/43_mixed_marketplace_skills/main.py ✅ PASS 6.2s $0.00
01_standalone_sdk/44_model_switching_in_convo.py ✅ PASS 7.8s $0.01
01_standalone_sdk/45_parallel_tool_execution.py ✅ PASS 3m 35s $0.56
01_standalone_sdk/46_agent_settings.py ✅ PASS 10.2s $0.01
01_standalone_sdk/47_defense_in_depth_security.py ✅ PASS 3.2s $0.00
01_standalone_sdk/48_conversation_fork.py ✅ PASS 13.3s $0.00
02_remote_agent_server/01_convo_with_local_agent_server.py ✅ PASS 32.0s $0.03
02_remote_agent_server/02_convo_with_docker_sandboxed_server.py ✅ PASS 1m 38s $0.07
02_remote_agent_server/03_browser_use_with_docker_sandboxed_server.py ✅ PASS 1m 1s $0.06
02_remote_agent_server/04_convo_with_api_sandboxed_server.py ✅ PASS 1m 11s $0.04
02_remote_agent_server/07_convo_with_cloud_workspace.py ✅ PASS 26.4s $0.03
02_remote_agent_server/08_convo_with_apptainer_sandboxed_server.py ✅ PASS 3m 34s $0.02
02_remote_agent_server/09_acp_agent_with_remote_runtime.py ✅ PASS 1m 12s $0.14
02_remote_agent_server/10_cloud_workspace_share_credentials.py ✅ PASS 42.2s $0.06
02_remote_agent_server/11_conversation_fork.py ✅ PASS 42.5s $0.00
02_remote_agent_server/12_settings_and_secrets_api.py ✅ PASS 2m 9s $0.02
02_remote_agent_server/13_workspace_get_llm.py ✅ PASS 24.8s $0.01
04_llm_specific_tools/01_gpt5_apply_patch_preset.py ✅ PASS 19.9s $0.02
04_llm_specific_tools/02_gemini_file_tools.py ✅ PASS 37.6s $0.08
05_skills_and_plugins/01_loading_agentskills/main.py ✅ PASS 21.4s $0.02
05_skills_and_plugins/02_loading_plugins/main.py ✅ PASS 20.3s $0.02

✅ All tests passed!

Total: 54 | Passed: 54 | Failed: 0 | Total Cost: $2.99

View full workflow run

@xingyaoww xingyaoww merged commit fe7aece into main May 6, 2026
55 of 57 checks passed
@xingyaoww xingyaoww deleted the remote-workspace-settings-methods branch May 6, 2026 21:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test-examples Run all applicable "examples/" files. Expensive operation.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add get_llm(), get_secrets(), and get_mcp_config() methods to RemoteWorkspace

4 participants