feat(api): auto-derive CORS origins from cao-server --host/--port#261
Merged
Merged
Conversation
Operators running cao-server on a non-default port previously had to also set CAO_CORS_ORIGINS for the browser-side UI to reach the API, even though the listen address was already known via --host/--port. Wire a helper that extends CORS_ORIGINS in place after argparse: loopback binds (127.0.0.1, localhost, 0.0.0.0, ::) yield both http://localhost:<port> and http://127.0.0.1:<port>; an explicit host yields http://<host>:<port>. Starlette's CORSMiddleware reads the list by reference, so mutating it before uvicorn.run reaches the already-installed middleware without re-wiring app construction. Closes awslabs#151.
Contributor
There was a problem hiding this comment.
Pull request overview
Adds automatic CORS origin derivation from the cao-server listen host/port so custom local ports work without requiring CAO_CORS_ORIGINS.
Changes:
- Adds
add_local_cors_origins(host, port)to mutateCORS_ORIGINSin place. - Calls the helper from
api.main.main()after parsing CLI arguments and before starting uvicorn. - Adds unit tests covering loopback, wildcard, custom host, idempotency, and in-place mutation behavior.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/cli_agent_orchestrator/constants.py |
Adds runtime CORS origin derivation helper. |
src/cli_agent_orchestrator/api/main.py |
Wires the helper into cao-server startup. |
test/test_constants.py |
Adds tests for derived CORS origins and mutation semantics. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…in() The CORS helper added in 43e14f7 produced unbracketed IPv6 origins (e.g. http://::1:9999) which can never match a real browser Origin header — browsers serialize IPv6 literals as http://[::1]:9999 — so same-host access still failed when cao-server ran on ::1 or any other v6 literal. Treat ::1 as a loopback alias alongside localhost and 127.0.0.1 so any of the three works, and bracket non-loopback IPv6 literals. Also asserts main() actually calls add_local_cors_origins with the resolved host/port before uvicorn.run; without this the helper could be silently disconnected from the CLI and every existing test would still pass.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #261 +/- ##
=======================================
Coverage ? 92.47%
=======================================
Files ? 69
Lines ? 6274
Branches ? 0
=======================================
Hits ? 5802
Misses ? 472
Partials ? 0
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Comment on lines
+1105
to
+1107
| assert mock_add.call_count == 1 | ||
| assert mock_uvicorn.call_count == 1 | ||
| assert mock_add.call_args_list[0].args == ("0.0.0.0", 9999) |
Address the two follow-ups Copilot left on the previous push: - The CI `Code Quality` job runs `black --check` on the whole tree; the helper added in 5a19d23 was not black-clean. Reformat. - The wiring test asserted that both `add_local_cors_origins` and `uvicorn.run` ran, but not their relative order. Since `uvicorn.run` blocks, mutating CORS_ORIGINS after it would never reach the first request. Attach both patches to a parent mock and assert `parent.mock_calls` shows the helper call first, the uvicorn call second.
haofeif
approved these changes
May 29, 2026
call-me-ram
added a commit
to call-me-ram/cli-agent-orchestrator
that referenced
this pull request
Jun 3, 2026
Bring the event-driven architecture branch up to date with main (98 commits) and reconcile the rewrite with features that landed after it forked: eager inbox delivery (awslabs#251), the OpenCode poller, env-var forwarding (awslabs#259), memory curation (awslabs#254/awslabs#262), CORS auto-derive (awslabs#261), DNS host validation (awslabs#124), and the self-send guard (awslabs#24). Highlights: - Providers adopt the async initialize() + get_status(buffer) contract; copilot_cli/opencode_cli converted; kiro keeps colour-only ANSI stripping so carriage-return-redraw permission prompts aren't misread as idle. - Event-driven InboxService.deliver_pending with the awslabs#251 eager gate and message-sender attribution; OpenCode poller retained as a status-driven method; the watchdog (PollingObserver/LogFileHandler) is removed. - terminal_service.create_terminal is async (FIFO + StatusMonitor wiring); session_service.create_session, flow_service.execute_flow, the API endpoints, and `cao flow run` updated to await. - memory_service curated path and the flow CLI fixed to the new contract. Full unit suite green (1908 passed); black + isort clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
add_local_cors_origins(host, port)inconstants.pythat extendsCORS_ORIGINSin place from the cao-server listen address.cao-server'smain()after argparse, so a custom--host/--portno longer requires the operator to also setCAO_CORS_ORIGINSfor same-host browser access.CAO_CORS_ORIGINSenv-var override shipped in fix(api): make network allowlists configurable via env vars #255; auto-derivation from--portwas the remaining follow-up.Behaviour
--host127.0.0.1,localhost,0.0.0.0,::http://localhost:<port>,http://127.0.0.1:<port>cao.internal)http://<host>:<port>Idempotent — repeat calls (or hitting an already-present default like
5173) do not duplicate entries.Implementation note
Starlette's
CORSMiddlewarestoresallow_originsby reference and re-reads it per request (starlette/middleware/cors.pylines 66, 104). MutatingCORS_ORIGINSin place after the middleware is installed but beforeuvicorn.runis therefore sufficient — no need to defer app construction or reorder middleware wiring.TrustedHostMiddlewaredoes copy its allowed-hosts list at init, soALLOWED_HOSTSis intentionally left alone here; that surface stays under operator control viaCAO_ALLOWED_HOSTSas today.Test plan
pytest test/test_constants.py— 6 new tests inTestAddLocalCorsOriginscover: loopback host,0.0.0.0wildcard, explicit hostname, idempotency, no-duplicate for default ports, and that mutation is visible via a previously-captured list reference (the contract that makes the in-place approach work).cao-server --port 9999, open the UI in a browser, confirm API calls succeed without settingCAO_CORS_ORIGINS.Closes #151.