Skip to content

feat: make OpenVSCode root configurable with PATH fallback#3061

Closed
hieptl wants to merge 2 commits into
mainfrom
hieptl/app-1555
Closed

feat: make OpenVSCode root configurable with PATH fallback#3061
hieptl wants to merge 2 commits into
mainfrom
hieptl/app-1555

Conversation

@hieptl

@hieptl hieptl commented May 5, 2026

Copy link
Copy Markdown
Contributor
  • A human has tested these changes.

Why

The agent-server hardcoded /openhands/.openvscode-server, which only exists inside the agent-server Docker image. On a non-Docker host the binary check fails, start() short-circuits, connection_token stays None, and /api/vscode/url returns {"url": null}. Downstream this breaks the Code tab in any GUI that runs the agent-server locally.

Summary

  • Add Config.vscode_server_root (OH_VSCODE_SERVER_ROOT) so the OpenVSCode Server install location is no longer hardcoded.
  • In VSCodeService._check_vscode_available, fall back to shutil.which("openvscode-server") when the binary is missing at the configured root. Root takes precedence over PATH.
  • Track the discovered binary in _resolved_binary so _start_vscode_process execs the actual found path instead of rebuilding the Docker-default path string.
  • Wire get_vscode_service to pass vscode_server_root from config.

How to Test

Please run the agent-server-gui locally using the agent server changes in this branch so we can review and validate them.

Video/Screenshots

app-1555

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

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:c5e8908-python

Run

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

All tags pushed for this build

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

About Multi-Architecture Support

  • Each variant tag (e.g., c5e8908-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., c5e8908-python-amd64) are also available if needed

@hieptl hieptl self-assigned this May 5, 2026
@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Python API breakage checks — ✅ PASSED

Result:PASSED

Action log

@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result:PASSED

Action log

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Taste Rating: 🟢 Good taste - Clean solution that solves a real problem

[RISK ASSESSMENT]

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

Configuration change with backward compatibility. Solves real problem (hardcoded Docker path breaks non-Docker hosts) with simple, well-tested fallback logic. Root precedence over PATH prevents security issues. No agent behavior changes, no eval impact.

VERDICT:
Worth merging - Clean implementation, comprehensive tests, backward compatible

KEY INSIGHT:
Elegant two-tier discovery (configured root → PATH fallback) with explicit precedence prevents both rigidity and security risks.

@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-agent-server/openhands/agent_server
   config.py69395%29, 42, 202
   vscode_service.py111991%78–80, 93–94, 207, 236–237, 264
TOTAL248301121154% 

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

✅ QA Report: PASS

All functional verification passed. The PR successfully achieves its goal: the agent-server can now find openvscode-server on non-Docker hosts via PATH fallback, and the root location is configurable via OH_VSCODE_SERVER_ROOT.

Does this PR achieve its stated goal?

Yes. The PR fixes the hardcoded /openhands/.openvscode-server path that broke the Code tab on non-Docker hosts. With this change:

  1. The root is now configurable via OH_VSCODE_SERVER_ROOT (defaults to Docker layout)
  2. When the binary is missing at the configured root, the service falls back to shutil.which('openvscode-server') to find it on PATH
  3. The configured root takes precedence over PATH (preventing stray system installs from overriding the configured location)
  4. The discovered binary path is tracked and used when spawning the process

All four behaviors were verified through functional testing on a non-Docker host.

Phase Result
Environment Setup ✅ Built successfully with make build
CI Status ✅ 18 checks passing, 8 build jobs in progress, 0 failing
Functional Verification ✅ All scenarios tested and working
Functional Verification

Test 1: PATH Fallback (configured root doesn't exist)

Step 1 — Establish baseline (simulate non-Docker host):

Checked if the hardcoded path exists:

/openhands/.openvscode-server/bin/openvscode-server: False

This confirms the bug exists on non-Docker hosts — the old code would fail here because it only checked the hardcoded Docker path.

Step 2 — Apply the PR's changes:

The PR branch is already checked out (commit 8cc507b7).

Step 3 — Test PATH fallback with the fix:

Created a mock openvscode-server binary on PATH and ran:

service = VSCodeService(
    openvscode_server_root="/nonexistent/openvscode",
    port=8001,
    connection_token="test-token"
)
available = service._check_vscode_available()

Output:

Configured root: /nonexistent/openvscode
Binary available: True
Resolve binary: /tmp/qa-vscode-test/bin/openvscode-server
✅ PASS: Service found openvscode-server on PATH as fallback

This shows the fix works: when the binary isn't at the configured root, the service falls back to PATH and succeeds.


Test 2: Root Precedence (configured root takes priority)

Step 1 — Setup:

Created two binaries:

  • One at a configured root: /tmp/qa-vscode-test/configured-root/bin/openvscode-server
  • One on PATH: /tmp/qa-vscode-test/bin/openvscode-server

Step 2 — Test precedence:

Ran:

service = VSCodeService(
    openvscode_server_root="/tmp/qa-vscode-test/configured-root",
    port=8002,
    connection_token="test-token-2"
)
available = service._check_vscode_available()

Output:

Configured root: /tmp/qa-vscode-test/configured-root
Binary available: True
Resolved binary: /tmp/qa-vscode-test/configured-root/bin/openvscode-server
✅ PASS: Service used configured root, not PATH

This confirms the configured root takes precedence, preventing a stray system install from overriding the configured location.


Test 3: Configuration via Environment Variable

Step 1 — Test default value:

config = Config()
print(config.vscode_server_root)

Output:

/openhands/.openvscode-server
✅ Default value is correct (preserves Docker layout)

Step 2 — Test environment variable override:

export OH_VSCODE_SERVER_ROOT="/custom/vscode/root"
config = from_env(Config, "OH")
print(config.vscode_server_root)

Output:

/custom/vscode/root
✅ Environment variable override works

This confirms the configuration option works as documented.


Test 4: Integration with get_vscode_service()

Step 1 — Configure environment:

export OH_ENABLE_VSCODE=true
export OH_VSCODE_SERVER_ROOT="/tmp/qa-vscode-test/configured-root"
export OH_VSCODE_PORT=8001

Step 2 — Get service via factory function:

service = get_vscode_service()
available = service._check_vscode_available()

Output:

Service created: True
Configured root: /tmp/qa-vscode-test/configured-root
Port: 8001
Binary available: True
Resolved binary: /tmp/qa-vscode-test/configured-root/bin/openvscode-server
✅ PASS: get_vscode_service() correctly wired custom configuration

This confirms the configuration flows through the entire stack correctly.


Test 5: Unit Tests

Ran all new unit tests added in this PR:

uv run pytest tests/agent_server/test_vscode_service.py::test_check_vscode_available_uses_shutil_which_fallback \
                tests/agent_server/test_vscode_service.py::test_check_vscode_available_root_takes_precedence_over_path \
                tests/agent_server/test_vscode_service.py::test_start_vscode_process_uses_resolved_binary \
                tests/agent_server/test_vscode_service.py::test_get_vscode_service_passes_server_root_from_config \
                tests/agent_server/test_vscode_service.py::test_vscode_server_root_configuration -v

Output:

4 passed, 5 warnings in 0.04s
✅ All new unit tests pass

Issues Found

None. The implementation works as documented and all tests pass.

@VascoSch92 VascoSch92 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I tested locally that the fix works correctly, i.e., env-var override works and shutil.which fallback works.

Just a couple of nits, but otherwise LGTM.

assert service.process is None


def test_check_vscode_available_false(vscode_service, tmp_path):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Note that this test will now fail on any host with openvscode-server on PATH.

consider to patch shutil.which to return None in the assertion



@pytest.mark.asyncio
async def test_start_no_binary(vscode_service, tmp_path):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we have the same problem here.

)
cmd = (
f"exec {self.openvscode_server_root}/bin/openvscode-server "
f"exec {binary_path} "

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

f"exec {binary_path} ..." is passed to asyncio.create_subprocess_shell. If a developer sets OH_VSCODE_SERVER_ROOT=/opt/My App/openvscode (or shutil.which returns a path with spaces, common on macOS under Application Support), the resulting command line breaks. The previous Docker-only path made this impossible; the new PATH-discovery path makes it plausible.

If this is a real concern: use shlex.quote(str(binary_path))

@tofarr tofarr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🍰

@hieptl hieptl closed this May 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants