[codex] Support regex CORS origins#3664
Conversation
Python API breakage checks — ✅ PASSEDResult: ✅ PASSED |
REST API breakage checks (OpenAPI) — ✅ PASSEDResult: ✅ PASSED |
Co-authored-by: openhands <openhands@all-hands.dev>
e1a3ea0 to
3bc1693
Compare
all-hands-bot
left a comment
There was a problem hiding this comment.
⚠️ QA Report: PASS WITH ISSUES
Regex CORS works end-to-end when configured via OH_ALLOW_CORS_ORIGIN_REGEX, but the README-documented JSON config path did not enable the new setting in my runtime.
Does this PR achieve its stated goal?
Partially. The core runtime behavior works via environment variable: compared with base branch wildcard CORS returning Access-Control-Allow-Origin: * with credentials, the PR branch echoes https://client.example with Access-Control-Allow-Credentials: true, and Origin: null is rejected without an allow-origin header. Existing explicit allowlist behavior and automatic localhost allowance also still worked. However, I could not get the documented JSON configuration-file route to apply allow_cors_origin_regex, so that part of the user-facing configuration story needs attention.
| Phase | Result |
|---|---|
| Environment Setup | ✅ make build completed and installed the uv-managed dev environment |
| CI Status | |
| Functional Verification |
Functional Verification
Test 1: Credential-compatible regex CORS echo on actual response
Step 1 — Reproduce / establish baseline without the fix:
Ran base branch (origin/main) with the old wildcard allowlist route:
git checkout --detach origin/main
OH_ALLOW_CORS_ORIGINS_0='*' OH_CONVERSATIONS_PATH=/tmp/oh-qa-base/conversations OH_WORKSPACE_PATH=/tmp/oh-qa-base/workspace uv run uvicorn openhands.agent_server.api:create_app --factory --host 127.0.0.1 --port 8130
curl -D - -o /tmp/body -H 'Origin: https://client.example' http://127.0.0.1:8130/server_infoObserved headers:
HTTP/1.1 200 OK
access-control-allow-origin: *
access-control-allow-credentials: true
This confirms the baseline problem described in the PR: the only broad-origin option emits literal * together with credentials, which credentialed browser clients reject.
Step 2 — Apply the PR's changes:
Checked out codex/cors-wildcard-echo-origins at dede767a82ef03a597427f0bc2be3a0f0fa73949 and configured the new env var.
Step 3 — Re-run with the fix in place:
OH_ALLOW_CORS_ORIGIN_REGEX='https?://.+' OH_CONVERSATIONS_PATH=/tmp/oh-qa-pr/conversations OH_WORKSPACE_PATH=/tmp/oh-qa-pr/workspace uv run uvicorn openhands.agent_server.api:create_app --factory --host 127.0.0.1 --port 8131
curl -D - -o /tmp/body -H 'Origin: https://client.example' http://127.0.0.1:8131/server_infoObserved headers:
HTTP/1.1 200 OK
access-control-allow-credentials: true
access-control-allow-origin: https://client.example
vary: Origin
This confirms the fix works for the main use case: the server echoes the concrete matching origin instead of returning *, while still allowing credentials.
Test 2: Origin: null rejection under regex configuration
With the PR server still configured as OH_ALLOW_CORS_ORIGIN_REGEX='https?://.+', I ran:
curl -D - -o /tmp/body -X OPTIONS -H 'Origin: null' -H 'Access-Control-Request-Method: POST' http://127.0.0.1:8131/api/conversationsObserved headers:
HTTP/1.1 400 Bad Request
vary: Origin
access-control-allow-credentials: true
There was no access-control-allow-origin header, which confirms Origin: null is rejected rather than matched by the broad HTTP(S) regex.
Test 3: Existing allowlist and localhost behavior
Started the PR server with only an explicit allowlist:
OH_ALLOW_CORS_ORIGINS_0='https://allowed.example' OH_CONVERSATIONS_PATH=/tmp/oh-qa-pr-allow/conversations OH_WORKSPACE_PATH=/tmp/oh-qa-pr-allow/workspace uv run uvicorn openhands.agent_server.api:create_app --factory --host 127.0.0.1 --port 8132Observed GET /server_info CORS headers:
Origin: https://allowed.example
HTTP/1.1 200 OK
access-control-allow-credentials: true
access-control-allow-origin: https://allowed.example
vary: Origin
Origin: https://blocked.example
HTTP/1.1 200 OK
access-control-allow-credentials: true
Origin: http://localhost:3000
HTTP/1.1 200 OK
access-control-allow-credentials: true
access-control-allow-origin: http://localhost:3000
vary: Origin
This shows explicit allowlists still echo allowed origins, unlisted origins do not receive access-control-allow-origin, and localhost remains automatically allowed.
Test 4: Documented JSON config-file path
I tried configuring the new setting through the documented JSON routes:
# Attempt A: explicit documented config path env var
OPENHANDS_AGENT_SERVER_CONFIG_PATH=/tmp/oh-qa-pr-config3/config.json uv run python -m openhands.agent_server --host 127.0.0.1 --port 8135
# Attempt B: documented default file location from a temp cwd
/tmp/oh-qa-pr-config4/workspace/openhands_agent_server_config.json
python -m openhands.agent_server --host 127.0.0.1 --port 8136The config file contained:
{"allow_cors_origin_regex": "https://.*\.example\.com"}In both attempts, curl -D - -H 'Origin: https://app.example.com' /server_info returned:
HTTP/1.1 200 OK
access-control-allow-credentials: true
There was no access-control-allow-origin, so I could not verify the README-documented JSON config route for the new field.
Issues Found
- 🟠 Issue: The env-var path works, but the documented JSON configuration-file path did not apply
allow_cors_origin_regexduring functional QA.
This QA review was created by an AI agent (OpenHands) on behalf of the user.
| { | ||
| "session_api_key": "your-secret-api-key", | ||
| "allow_cors_origins": ["https://your-frontend.com"], | ||
| "allow_cors_origin_regex": null, |
There was a problem hiding this comment.
🟠 Important: Functional QA following this README's documented JSON configuration path did not enable allow_cors_origin_regex. I started the packaged server with OPENHANDS_AGENT_SERVER_CONFIG_PATH=/tmp/.../config.json containing "allow_cors_origin_regex": "https://.*\\.example\\.com", and also tried the default workspace/openhands_agent_server_config.json location; in both cases GET /server_info with Origin: https://app.example.com returned 200 plus access-control-allow-credentials: true but no access-control-allow-origin. The env var path works, so either the JSON config route needs to apply this field too or the docs should avoid presenting it as JSON-configurable.
This comment was created by an AI agent (OpenHands) on behalf of the user.
Summary
allow_cors_origin_regex/OH_ALLOW_CORS_ORIGIN_REGEXfor regex-based CORS origin matching.DOCKER_HOST_ADDRbehavior unchanged.Origin: nullrejection, and env parsing.Root Cause
Starlette's literal wildcard CORS mode can emit
Access-Control-Allow-Origin: *on simple responses without cookies even whenAccess-Control-Allow-Credentials: trueis set. Browsers reject that combination for credentialed cross-origin clients. The regex path echoes the concrete origin, which supports any HTTP(S) origin without using literal wildcard response headers.Validation
uv run pytest tests/agent_server/test_cors.py tests/agent_server/test_env_parser.py -quv run pre-commit run --files openhands-agent-server/openhands/agent_server/README.md openhands-agent-server/openhands/agent_server/api.py openhands-agent-server/openhands/agent_server/config.py openhands-agent-server/openhands/agent_server/middleware.py tests/agent_server/test_cors.py tests/agent_server/test_env_parser.pyAgent Server images for this PR
• GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server
Variants & Base Images
eclipse-temurin:17-jdknikolaik/python-nodejs:python3.13-nodejs22-slimgolang:1.21-bookwormPull (multi-arch manifest)
# Each variant is a multi-arch manifest supporting both amd64 and arm64 docker pull ghcr.io/openhands/agent-server:dede767-pythonRun
All tags pushed for this build
About Multi-Architecture Support
dede767-python) is a multi-arch manifest supporting both amd64 and arm64dede767-python-amd64) are also available if needed