Skip to content

[codex] Support regex CORS origins#3664

Open
neubig wants to merge 2 commits into
mainfrom
codex/cors-wildcard-echo-origins
Open

[codex] Support regex CORS origins#3664
neubig wants to merge 2 commits into
mainfrom
codex/cors-wildcard-echo-origins

Conversation

@neubig

@neubig neubig commented Jun 11, 2026

Copy link
Copy Markdown
Member

Summary

  • Add allow_cors_origin_regex / OH_ALLOW_CORS_ORIGIN_REGEX for regex-based CORS origin matching.
  • Wire the regex through the agent-server CORS middleware while keeping explicit origin allowlists, localhost, and DOCKER_HOST_ADDR behavior unchanged.
  • Add regression coverage for credential-compatible regex-origin actual responses, Origin: null rejection, and env parsing.

Root Cause

Starlette's literal wildcard CORS mode can emit Access-Control-Allow-Origin: * on simple responses without cookies even when Access-Control-Allow-Credentials: true is 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 -q
  • uv 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.py

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

Run

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

All tags pushed for this build

ghcr.io/openhands/agent-server:dede767-golang-amd64
ghcr.io/openhands/agent-server:dede767a82ef03a597427f0bc2be3a0f0fa73949-golang-amd64
ghcr.io/openhands/agent-server:codex-cors-wildcard-echo-origins-golang-amd64
ghcr.io/openhands/agent-server:dede767-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:dede767-golang-arm64
ghcr.io/openhands/agent-server:dede767a82ef03a597427f0bc2be3a0f0fa73949-golang-arm64
ghcr.io/openhands/agent-server:codex-cors-wildcard-echo-origins-golang-arm64
ghcr.io/openhands/agent-server:dede767-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:dede767-java-amd64
ghcr.io/openhands/agent-server:dede767a82ef03a597427f0bc2be3a0f0fa73949-java-amd64
ghcr.io/openhands/agent-server:codex-cors-wildcard-echo-origins-java-amd64
ghcr.io/openhands/agent-server:dede767-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:dede767-java-arm64
ghcr.io/openhands/agent-server:dede767a82ef03a597427f0bc2be3a0f0fa73949-java-arm64
ghcr.io/openhands/agent-server:codex-cors-wildcard-echo-origins-java-arm64
ghcr.io/openhands/agent-server:dede767-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:dede767-python-amd64
ghcr.io/openhands/agent-server:dede767a82ef03a597427f0bc2be3a0f0fa73949-python-amd64
ghcr.io/openhands/agent-server:codex-cors-wildcard-echo-origins-python-amd64
ghcr.io/openhands/agent-server:dede767-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-amd64
ghcr.io/openhands/agent-server:dede767-python-arm64
ghcr.io/openhands/agent-server:dede767a82ef03a597427f0bc2be3a0f0fa73949-python-arm64
ghcr.io/openhands/agent-server:codex-cors-wildcard-echo-origins-python-arm64
ghcr.io/openhands/agent-server:dede767-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-arm64
ghcr.io/openhands/agent-server:dede767-golang
ghcr.io/openhands/agent-server:dede767a82ef03a597427f0bc2be3a0f0fa73949-golang
ghcr.io/openhands/agent-server:codex-cors-wildcard-echo-origins-golang
ghcr.io/openhands/agent-server:dede767-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:dede767-java
ghcr.io/openhands/agent-server:dede767a82ef03a597427f0bc2be3a0f0fa73949-java
ghcr.io/openhands/agent-server:codex-cors-wildcard-echo-origins-java
ghcr.io/openhands/agent-server:dede767-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:dede767-python
ghcr.io/openhands/agent-server:dede767a82ef03a597427f0bc2be3a0f0fa73949-python
ghcr.io/openhands/agent-server:codex-cors-wildcard-echo-origins-python
ghcr.io/openhands/agent-server:dede767-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim

About Multi-Architecture Support

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

@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Python API breakage checks — ✅ PASSED

Result:PASSED

Action log

@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result:PASSED

Action log

@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-agent-server/openhands/agent_server
   api.py2552191%104, 106–111, 113, 115, 117, 151, 163, 178, 184, 463, 466, 470–472, 474, 481
   config.py73297%29, 42
TOTAL308581346456% 

Co-authored-by: openhands <openhands@all-hands.dev>
@neubig neubig force-pushed the codex/cors-wildcard-echo-origins branch from e1a3ea0 to 3bc1693 Compare June 11, 2026 18:10
@neubig neubig changed the title [codex] Echo wildcard CORS origins [codex] Support regex CORS origins Jun 11, 2026
@neubig neubig marked this pull request as ready for review June 11, 2026 18:32
@neubig neubig requested a review from tofarr June 11, 2026 18:33

@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 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 ⚠️ At time checked: 0 failing, 11 successful, 2 skipped, 16 pending
Functional Verification ⚠️ Env-var regex path works; JSON config-file path did not apply the setting
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_info

Observed 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_info

Observed 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/conversations

Observed 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 8132

Observed 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 8136

The 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_regex during 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,

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.

🟠 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.

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.

2 participants