Skip to content

gateway: fix global Control UI 404s for symlinked wrappers and pnpm hardlinks#39856

Closed
LarytheLord wants to merge 5 commits intoopenclaw:mainfrom
LarytheLord:fix/control-ui-bun-symlink-39693
Closed

gateway: fix global Control UI 404s for symlinked wrappers and pnpm hardlinks#39856
LarytheLord wants to merge 5 commits intoopenclaw:mainfrom
LarytheLord:fix/control-ui-bun-symlink-39693

Conversation

@LarytheLord
Copy link
Copy Markdown

@LarytheLord LarytheLord commented Mar 8, 2026

Summary

Fixes the bundled Control UI 404 Not Found regression for global installs while preserving the configured-root hardlink boundary:

  1. symlinked global wrappers (for example ~/.bun/bin/openclaw) where unresolved argv1 can miss the real dist/control-ui path
  2. auto-detected package roots in pnpm/global-store layouts where bundled Control UI assets are hardlinked

Why

Reports in #39693 and #39621 show dashboard 404 despite valid assets on disk.

  • wrapper installs can resolve from wrapper paths instead of the real entrypoint path
  • package-manager global trees can expose bundled Control UI files through hardlinks
  • configured gateway.controlUi.root should remain on the stricter hardlink boundary rather than inheriting special trust from a path shape

Changes

1) Symlink-aware asset discovery

  • src/infra/control-ui-assets.ts
    • evaluate both path.resolve(argv1) and realpath(argv1) candidates
    • include realpath-derived fallback traversal candidates
    • include realpath-derived root candidates in resolveControlUiRootSync
    • add package-proven bundled root detection

2) Bundled vs resolved root handling

  • src/gateway/server.impl.ts
    • classify explicit gateway.controlUi.root overrides as resolved
    • classify auto-detected package-proven bundled roots as bundled
  • src/gateway/control-ui.ts
    • allow hardlinks only for bundled roots
    • keep hardlink rejection for configured/custom resolved roots

3) Regression tests

  • src/infra/control-ui-assets.test.ts
    • symlinked wrapper argv1 cases
    • package-proven root detection
  • src/gateway/control-ui.http.test.ts
    • rejects hardlinked assets for resolved roots
    • serves hardlinked assets for bundled roots
  • src/gateway/control-ui.auto-root.http.test.ts
    • bundled auto-root serves hardlinked assets and SPA fallback
    • non-package auto-root still rejects hardlinked assets
  • src/gateway/server.control-ui-root.test.ts
    • configured gateway.controlUi.root with a package-store hardlinked index still returns 404

Validation

  • pnpm test -- src/infra/control-ui-assets.test.ts src/gateway/control-ui.http.test.ts src/gateway/control-ui.auto-root.http.test.ts src/gateway/server.control-ui-root.test.ts

Fixes #39693
Refs #39621

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 8, 2026

Greptile Summary

This PR makes Control UI asset path resolution symlink-aware, fixing a regression where bun-installed global wrappers (which are symlinks like ~/.bun/bin/openclaw → .../dist/index.js) caused the dashboard to return 404 because directory-based heuristics evaluated the wrapper path instead of the real entrypoint.

Key changes:

  • resolveControlUiDistIndexPath: builds an entrypointCandidates array by also resolving argv1 via fs.realpathSync, then runs Case 1 and the fallback package-boundary traversal against all candidates
  • resolveControlUiRootSync: computes argv1RealpathDir from fs.realpathSync(argv1) and injects two extra candidate paths (<realpathDir>/dist/control-ui and <realpathDir>/control-ui) when the realpath differs from the nominal path
  • Two regression tests cover both code paths for the symlinked Bun-style wrapper case

The implementation correctly handles symlink resolution with try/catch guards and proper multi-candidate fallback logic.

Confidence Score: 4/5

  • Safe to merge; logic is sound, error cases are properly caught, and regression tests pass.
  • The implementation correctly handles symlink resolution with appropriate error boundaries. The fallback logic properly supports multiple candidate paths, and the change from hard returns to breaks ensures all candidates are tried. Both affected functions have comprehensive test coverage for the symlinked wrapper scenario. The design is intentional and well-tested.
  • No files require special attention; both changed files are straightforward and well-tested.

Last reviewed commit: ef6ab03

@steipete
Copy link
Copy Markdown
Contributor

steipete commented Mar 8, 2026

AI-generated review note.

This looks partial, not a full fix for the reported 404 regressions.

The PR improves Control UI asset discovery for symlinked Bun/global wrappers in src/infra/control-ui-assets.ts, but the explicit-gateway.controlUi.root failure mode reported in #39621 still bypasses this path entirely.

The remaining likely failure path is deeper in file-open validation / boundary checks when serving assets from package-store layouts (pnpm/global-store hardlink-heavy installs), not just argv1 resolution.

So this may be OK as a narrow wrapper-discovery improvement, but it should not be treated as the fix for #39621 / #39693 without an HTTP-level regression test that proves GET / works when gateway.controlUi.root points at a global install path.

@openclaw-barnacle openclaw-barnacle bot added gateway Gateway runtime size: M and removed size: S labels Mar 8, 2026
@LarytheLord
Copy link
Copy Markdown
Author

Thanks for calling this out — agreed the first revision was too narrow for the full #39621/#39693 surface.

I pushed a follow-up in commit 62424b6 that targets the explicit gateway.controlUi.root regression path as well.

What changed:

  • handleControlUiHttpRequest now allows hardlinked files only when the resolved UI root is an OpenClaw package root (.../dist/control-ui with package name openclaw).
  • Non-package roots keep the current hardlink rejection behavior.

Added regression coverage:

  • src/gateway/control-ui.http.test.ts
    • rejects hardlinked index.html for non-package roots
    • allows hardlinked index.html for OpenClaw package roots
  • src/gateway/server.control-ui-root.test.ts
    • configures gateway.controlUi.root to a pnpm/global-style path with hardlinked index.html
    • asserts GET / returns 200 and serves the dashboard HTML

This now exercises the explicit-root HTTP path directly, not only argv1 discovery.

@LarytheLord LarytheLord changed the title infra: resolve Control UI asset path for symlinked Bun global wrappers gateway: fix global Control UI 404s for symlinked wrappers and pnpm hardlinks Mar 8, 2026
@LarytheLord LarytheLord force-pushed the fix/control-ui-bun-symlink-39693 branch from 62424b6 to 66429ef Compare March 8, 2026 14:25
@LarytheLord
Copy link
Copy Markdown
Author

Follow-up to unblock CI:

  • rebased this branch onto latest openclaw/main
  • refreshed generated apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift in 66429ef

This addresses the failing check job (check:host-env-policy:swift reported the generated file as out-of-date).

@openclaw-barnacle openclaw-barnacle bot added the app: macos App: macos label Mar 8, 2026
@LarytheLord
Copy link
Copy Markdown
Author

Addressed the failing secrets check by refreshing .secrets.baseline in follow-up commit 98c57cbcc.

This is only a baseline line-number sync from detect-secrets (no functional code changes).

@LarytheLord LarytheLord force-pushed the fix/control-ui-bun-symlink-39693 branch from 98c57cb to b20f9e6 Compare March 8, 2026 14:51
@LarytheLord
Copy link
Copy Markdown
Author

Rebased this branch onto latest main and force-pushed as b20f9e6b4 to resolve merge conflicts (DIRTY state).

The only conflict was .secrets.baseline; I regenerated/staged it with detect-secrets during rebase. No functional code changes in this rebase update.

@velvet-shark velvet-shark self-assigned this Mar 8, 2026
@velvet-shark velvet-shark force-pushed the fix/control-ui-bun-symlink-39693 branch from b20f9e6 to 811b559 Compare March 9, 2026 00:06
@openclaw-barnacle openclaw-barnacle bot added channel: bluebubbles Channel integration: bluebubbles channel: discord Channel integration: discord channel: googlechat Channel integration: googlechat channel: imessage Channel integration: imessage channel: line Channel integration: line channel: matrix Channel integration: matrix channel: mattermost Channel integration: mattermost and removed app: macos App: macos labels Mar 9, 2026
@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

11 similar comments
@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle openclaw-barnacle bot closed this Mar 9, 2026
@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

9 similar comments
@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@velvet-shark
Copy link
Copy Markdown
Member

Superseded by #40385.

Barnacle closed this branch because the prep push picked up unrelated repo-wide changes. I recreated the Control UI fix on a clean branch from current main and kept only the intended 8-file payload:

  • symlink-aware bundled asset discovery for global wrappers
  • bundled-vs-resolved root classification for hardlink handling
  • regression coverage for bundled auto-roots vs explicit configured roots

The replacement PR is here: #40385

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: web-ui App: web-ui channel: bluebubbles Channel integration: bluebubbles channel: discord Channel integration: discord channel: feishu Channel integration: feishu channel: googlechat Channel integration: googlechat channel: imessage Channel integration: imessage channel: irc channel: line Channel integration: line channel: matrix Channel integration: matrix channel: mattermost Channel integration: mattermost channel: msteams Channel integration: msteams channel: nextcloud-talk Channel integration: nextcloud-talk channel: nostr Channel integration: nostr channel: signal Channel integration: signal channel: slack Channel integration: slack channel: telegram Channel integration: telegram channel: tlon Channel integration: tlon channel: twitch Channel integration: twitch channel: whatsapp-web Channel integration: whatsapp-web channel: zalo Channel integration: zalo channel: zalouser Channel integration: zalouser cli CLI command changes gateway Gateway runtime size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Global installation via Bun results in 404 Not Found on native Dashboard URL (Port 18789)

3 participants