Skip to content

feat(gateway): support configurable trusted control-UI origins (#232) - #313

Merged
KrasimirKralev merged 5 commits into
betafrom
fix/configurable-trusted-origins-v2
Aug 8, 2026
Merged

KrasimirKralev merged 5 commits into
betafrom
fix/configurable-trusted-origins-v2

Conversation

@KrasimirKralev

@KrasimirKralev KrasimirKralev commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #232. Supersedes #295 — same feature (@jamesachurchill's commits, authorship preserved), re-homed onto current beta (auto-merged cleanly with the #306 SecretRef and #308 breaker changes to gateway-proxy.ts / gateway-pre-start.sh) and run through our flow.

What

Operators can register extra trusted control-UI origins (VPN / MagicDNS names, a stable HTTPS origin) via data/control-ui-origins.json. Validated extras merge into controlUi.allowedOrigins; every invalid form — bare host, wildcard, path, credentials, wrong scheme, out-of-range port — is rejected so a configured value can never quietly widen cross-origin access. Validation lives in an importable, unit-tested Python module (gateway_origins.py) and a matching TS enforcer (control-ui-origins.ts + gateway-proxy.ts), with a pre-parse filter against the urlsplit-vs-WHATWG parser differential.

Why it is needed

isReflectableHost() only reflects ALLOWED_HOSTS + the mDNS .local name + bare IPv4 — an arbitrary DNS name (Tailscale *.ts.net, a reverse-proxy domain) is bounced, and gateway-pre-start.sh rewrites controlUi.allowedOrigins on every boot, so a hand-edit is wiped. This is the only durable, boot-surviving way to register such an origin.

Verified on the Jetson (aarch64)

Summary by CodeRabbit

  • New Features

    • Added support for configuring additional trusted Control UI origins through a JSON configuration file.
    • Added validation, normalization, de-duplication, and exact scheme/host/port matching for configured origins.
    • Configuration changes are detected automatically, with warnings for invalid or unreadable entries.
  • Bug Fixes

    • Improved gateway redirect handling to reject untrusted or mismatched origins while preserving existing valid behavior.
  • Documentation

    • Documented configuration, validation rules, and missing-file behavior.
  • Tests

    • Added comprehensive coverage for configuration loading, validation, gateway integration, and refresh behavior.

Baxter and others added 2 commits August 8, 2026 19:10
Add a narrow, strictly-validated escape hatch for genuine cross-origin/
custom-origin Control UI deployments: operator-supplied origins in
data/control-ui-origins.json (or CLAWBOX_CONTROL_UI_ORIGINS_FILE) are
merged into the gateway's generated allowedOrigins and honored by the
Next.js proxy's redirect-origin reflection, with exact scheme+host+port
matching so a configured hostname can't be reflected across other
schemes or ports. Same-origin .local/.ts.net/private access is
unaffected and normally needs no entry.
@KrasimirKralev
KrasimirKralev requested a review from a team as a code owner August 8, 2026 16:12
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@KrasimirKralev, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 26 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 38c7cf2c-210e-406c-b570-bf4a14e412a3

📥 Commits

Reviewing files that changed from the base of the PR and between 9660443 and 63c7bc0.

📒 Files selected for processing (4)
  • src/lib/control-ui-origins.ts
  • src/lib/gateway-proxy.ts
  • src/tests/unit/control-ui-origins.test.ts
  • src/tests/unit/gateway-proxy-origins.test.ts
📝 Walkthrough

Walkthrough

The PR adds configurable trusted Control UI origins. It validates and normalizes JSON entries, merges them into gateway configuration, applies exact proxy matching, reloads changed files, documents the option, and adds Python and TypeScript test coverage.

Changes

Trusted Control UI origins

Layer / File(s) Summary
Origin validation and configuration loading
scripts/gateway_origins.py, src/lib/control-ui-origins.ts, src/tests/unit/gateway-origins.test.ts, src/tests/unit/control-ui-origins.test.ts
Both loaders resolve the configured file, validate HTTP(S) origins, normalize values, report warnings, and preserve ordered de-duplication.
Gateway startup origin wiring
scripts/gateway-pre-start.sh, src/tests/unit/gateway-origins.test.ts, README.md
Gateway startup loads configured origins, handles helper and configuration failures, merges extra origins with defaults, and documents the configuration contract.
Gateway proxy exact-origin redirects
src/lib/gateway-proxy.ts, src/tests/unit/gateway-proxy-origins.test.ts
The proxy caches and refreshes configured origins, logs warnings, and requires exact scheme, host, and port matches for configured hosts while retaining existing fallback behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant GatewayPreStart
  participant GatewayOrigins
  participant GatewayConfig
  participant GatewayProxy
  Operator->>GatewayPreStart: Provide origins JSON file
  GatewayPreStart->>GatewayOrigins: Load and validate origins
  GatewayOrigins-->>GatewayPreStart: Normalized origins and warnings
  GatewayPreStart->>GatewayConfig: Merge origins with defaults
  GatewayProxy->>GatewayOrigins: Reload changed configuration
  GatewayProxy-->>Operator: Reflect exact trusted origin
Loading

Possibly related PRs

Suggested labels: area: gateway, area: docs

Suggested reviewers: yalexx

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: configurable trusted Control UI origins for the gateway.
Description check ✅ Passed The description provides a clear summary, rationale, implementation details, and testing evidence, but omits the repository template sections and checklist.
Linked Issues check ✅ Passed The changes satisfy issue #232 by adding documented configuration, strict validation, merging, runtime enforcement, warnings, and comprehensive tests.
Out of Scope Changes check ✅ Passed The README, loaders, gateway wiring, proxy enforcement, and tests all support the linked issue objectives; no unrelated changes are evident.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/configurable-trusted-origins-v2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

🦀 ClawReview

Your friendly reef crab, here with the lay of the land.

Adds a durable operator escape hatch for registering extra trusted control-UI origins (Tailscale .ts.net names, reverse-proxy hostnames) via data/control-ui-origins.json. The configured origins are strictly validated and normalized in a new Python module (gateway_origins.py) and a matching TypeScript module (control-ui-origins.ts), then merged into controlUi.allowedOrigins at boot and checked inside gateway-proxy.ts's redirect logic. This is a clean re-base of @jamesachurchill's work from #295 onto current beta, with authorship preserved.

At a glance

  • ✨ Feature · touches gateway proxy + boot script (control-UI origin reflection)
  • Base branch: beta · +567 source / +952 tests across 8 files
  • ✅ base beta matches the beta-first convention
  • ✅ conventional PR title
  • ✅ source changes come with test changes
  • 🟡 large PR (1528 lines changed) — consider splitting
  • ℹ️ touches security-sensitive paths (scripts/gateway-pre-start.sh) — review with extra care

Good to know

  • 🟡 Supersedes open PR Support trusted control UI origins #295 ('Support trusted control UI origins') — that PR should be closed once this lands to avoid confusion.
  • 🟡 Ships two parallel implementations of the same validation logic (Python for gateway-pre-start.sh, TypeScript for gateway-proxy.ts) that must be kept in sync manually.
  • 🟡 Modifies gateway-pre-start.sh, which rewrites openclaw.json on every device boot — changes here affect all customer Jetson devices on restart.
  • ℹ️ Comes with 952 lines of tests across 3 new test files, including wiring tests that extract and execute the actual heredoc snippets from the shell script.

— ClawReview 🦀. I set the scene; CodeRabbit reviews the code; you decide. Conventions: docs.

@github-actions github-actions Bot added area: docs Auto-triage area area: install Auto-triage area area: gateway Auto-triage area labels Aug 8, 2026
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

CI Summary

✅ Tests

  • Result: passed
  • View run
  • Coverage: statements 70.51%, branches 60.88%, functions 66.31%, lines 72.42%

✅ E2E

✅ E2E Install

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/control-ui-origins.ts`:
- Around line 77-123: Update src/lib/control-ui-origins.ts in the URL validation
flow to reject origins when parsed url.hostname differs from the verbatim host
text in the raw input, and check url.username/url.password by presence so empty
credentials are rejected; update scripts/gateway_origins.py in its origin
validator to reject host forms that are neither plain DNS labels nor dotted-quad
IPv4, keeping both validators aligned for numeric and hexadecimal host inputs.

In `@src/lib/gateway-proxy.ts`:
- Around line 112-129: The redirect path in the host-reflection logic must
validate hostHeader before constructing the redirect URL. Update the reflectable
branch around isReflectableHost and NextResponse.redirect to reject out-of-range
or otherwise malformed ports, or safely handle URL construction failures, while
preserving valid configured-host and IPv4 redirects.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 54fdc5ba-72a9-4064-93db-2720179d4319

📥 Commits

Reviewing files that changed from the base of the PR and between 0e7dfb1 and 9660443.

📒 Files selected for processing (8)
  • README.md
  • scripts/gateway-pre-start.sh
  • scripts/gateway_origins.py
  • src/lib/control-ui-origins.ts
  • src/lib/gateway-proxy.ts
  • src/tests/unit/control-ui-origins.test.ts
  • src/tests/unit/gateway-origins.test.ts
  • src/tests/unit/gateway-proxy-origins.test.ts

Comment on lines +77 to +123
let url: URL;
try {
url = new URL(value);
} catch {
return { origin: null, warning: `origin is not a valid URL: ${JSON.stringify(raw)}` };
}

const scheme = url.protocol.slice(0, -1).toLowerCase();
if (scheme !== "http" && scheme !== "https") {
return { origin: null, warning: `origin scheme must be http or https: ${JSON.stringify(raw)}` };
}

if (url.username || url.password) {
return { origin: null, warning: `origin must not contain credentials: ${JSON.stringify(raw)}` };
}

if (url.pathname !== "" && url.pathname !== "/") {
return { origin: null, warning: `origin must not contain a path: ${JSON.stringify(raw)}` };
}
if (url.search) {
return { origin: null, warning: `origin must not contain a query string: ${JSON.stringify(raw)}` };
}
if (url.hash) {
return { origin: null, warning: `origin must not contain a fragment: ${JSON.stringify(raw)}` };
}

const hostname = url.hostname.toLowerCase();
if (!hostname) {
return { origin: null, warning: `origin is missing a host: ${JSON.stringify(raw)}` };
}

let hostPart: string;
if (hostname.startsWith("[") && hostname.endsWith("]")) {
const bare = hostname.slice(1, -1);
if (!net.isIPv6(bare)) {
return { origin: null, warning: `origin has an invalid IPv6 host: ${JSON.stringify(raw)}` };
}
hostPart = `[${bare}]`;
} else {
if (!HOSTNAME_RE.test(hostname)) {
return { origin: null, warning: `origin has an invalid host: ${JSON.stringify(raw)}` };
}
if (/^[0-9.]+$/.test(hostname) && !net.isIPv4(hostname)) {
return { origin: null, warning: `origin has an invalid IPv4 host: ${JSON.stringify(raw)}` };
}
hostPart = hostname;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Loader parity: the TypeScript and Python validators accept different host forms. Both modules validate the same control-ui-origins.json contract and both headers state they must stay in sync. They use different URL parsers. The WHATWG parser rewrites numeric and hex host forms before validation (http://12345 becomes 0.0.48.57, http://0x7f.1 becomes 127.0.0.1), while urllib.parse.urlsplit leaves the host text unchanged. The gateway allowlist and the proxy reflection then disagree for the same file entry.

  • src/lib/control-ui-origins.ts#L77-L123: reject an origin whose parsed url.hostname does not appear verbatim in the raw input, and compare url.username/url.password for presence rather than truthiness so http://@example.com is rejected as it is in Python.
  • scripts/gateway_origins.py#L51-L121: add the matching rejection for host forms that are not plain DNS labels or dotted-quad IPv4, so 0x7f.1 is not accepted verbatim here while the TypeScript loader resolves it to 127.0.0.1.
📍 Affects 2 files
  • src/lib/control-ui-origins.ts#L77-L123 (this comment)
  • scripts/gateway_origins.py#L51-L121
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/control-ui-origins.ts` around lines 77 - 123, Update
src/lib/control-ui-origins.ts in the URL validation flow to reject origins when
parsed url.hostname differs from the verbatim host text in the raw input, and
check url.username/url.password by presence so empty credentials are rejected;
update scripts/gateway_origins.py in its origin validator to reject host forms
that are neither plain DNS labels nor dotted-quad IPv4, keeping both validators
aligned for numeric and hexadecimal host inputs.

Comment thread src/lib/gateway-proxy.ts Outdated
KrasimirKralev and others added 3 commits August 8, 2026 19:27
- redirectToSetup: a default-reflectable host (LAN IP / localhost / mDNS)
  keeps its broad reflection even when an operator also configures an exact
  origin for it — configuring https://10.42.0.1 no longer breaks plain
  http://10.42.0.1 on the SoftAP (which would dead-end at clawbox.local).
- control-ui-origins normalizeOrigin: reject the lenient WHATWG forms the
  Python gateway loader rejects — IPv4 shorthand/integer/octal (2130706433,
  127.1, 010.0.0.1) and empty userinfo (http://@host) — so the proxy never
  trusts an origin the gateway will refuse (half-working deployments).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- gateway-proxy-origins: assert exact-match for a non-default configured host,
  and (regression) that a default host keeps broad reflection when a matching
  origin is configured.
- control-ui-origins: assert empty-userinfo and IPv4-shorthand origins are
  rejected like the Python gateway loader.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CodeRabbit: rawHost strips the port before isReflectableHost(), so a Host like
'clawbox.local:99999' reaches the reflect path; new URL('http://clawbox.local:99999/setup')
then throws and the request 500s instead of redirecting. Wrap the reflected
redirect in try/catch and fall through to the canonical origin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@KrasimirKralev
KrasimirKralev force-pushed the fix/configurable-trusted-origins-v2 branch from 7be1483 to 63c7bc0 Compare August 8, 2026 16:45
@KrasimirKralev

Copy link
Copy Markdown
Contributor Author

CodeRabbit's two points are both addressed in code:

  • Loader parity (control-ui-origins.ts): normalizeOrigin now rejects empty userinfo (http://@host, via a raw-authority @ check that url.username can't see) and IPv4 shorthand/integer/octal (2130706433, 127.1, 010.0.0.1, via a raw-host-verbatim check), matching the Python gateway loader. Covered by new tests.
  • Malformed Host port (gateway-proxy.ts): the reflected redirect is wrapped in try/catch so an out-of-range port like clawbox.local:99999 falls back to the canonical origin instead of throwing a 500. Covered by a new test.

These also cover the /code-review findings (additive reflection so a configured origin can't strip a default host's LAN/SoftAP reflection). 97 tests green on the Jetson.

@KrasimirKralev
KrasimirKralev merged commit 0623092 into beta Aug 8, 2026
7 checks passed
@KrasimirKralev
KrasimirKralev deleted the fix/configurable-trusted-origins-v2 branch August 8, 2026 16:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: docs Auto-triage area area: gateway Auto-triage area area: install Auto-triage area

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant