From c889c59e419e14aa5777e0c99bc7775b568e3504 Mon Sep 17 00:00:00 2001 From: "Tao Sun (from Dev Box)" Date: Thu, 23 Jul 2026 15:03:20 +0800 Subject: [PATCH 1/2] fix(skills): remove unavailable-reason tooltip from Ask Agent button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Ask Agent button on unavailable skills was wrapped in an el-tooltip showing the missing-requirement reason, which is redundant — the same reason is already shown on the 'Unavailable' badge next to the skill name. Unwrap the button so hovering it no longer shows the tooltip. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- desktop/renderer/src/views/SettingsView.vue | 26 +++++++++++++-------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/desktop/renderer/src/views/SettingsView.vue b/desktop/renderer/src/views/SettingsView.vue index 5060c7b..76f2159 100644 --- a/desktop/renderer/src/views/SettingsView.vue +++ b/desktop/renderer/src/views/SettingsView.vue @@ -418,11 +418,14 @@ {{ skill.description }} - - - {{ t("settings.askAgent") }} - - + + {{ t("settings.askAgent") }} + {{ skill.description }} - - - {{ t("settings.askAgent") }} - - + + {{ t("settings.askAgent") }} + Date: Fri, 24 Jul 2026 11:36:02 +0800 Subject: [PATCH 2/2] fix(deployer): probe npm registries before OpenClaw install; fail fast when all are blocked On corporate networks (e.g. Microsoft) the public npm registries the deployer falls back to (registry.npmjs.org, registry.npmmirror.com) are blocked by network policy (Defender Network Protection 'NPM URL Block'). Previously the installer handed each blocked registry to 'npm install -g openclaw' with a 900s timeout, so it would hang for many minutes per registry AND trigger a Windows Security 'blocked by your IT admin' toast for every blocked host before eventually falling through to a reachable mirror. Add a fast parallel reachability probe (per-registry /-/ping, 2.5s timeout) and only attempt registries that respond, fastest-first. If no candidate registry is reachable, log a clear, actionable message and return failure immediately so the install fail screen shows right away instead of making the user wait through repeated long npm hangs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- deployer/windows_setup.py | 60 ++++++++++++++++++++++++++++- tests/test_windows_setup_upgrade.py | 11 ++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/deployer/windows_setup.py b/deployer/windows_setup.py index fa88ca0..82529a9 100644 --- a/deployer/windows_setup.py +++ b/deployer/windows_setup.py @@ -1307,6 +1307,53 @@ def _npm_registry_candidates(self) -> list[str]: registries.append(registry) return registries + def _reachable_npm_registries(self, candidates: list[str], timeout: float = 2.5) -> list[str]: + """Filter ``candidates`` down to registries that answer a quick probe. + + Each candidate's ``/-/ping`` endpoint is probed in parallel with a + short per-registry ``timeout``. A registry counts as reachable if the + host responds at all — even a 4xx/5xx is fine, since that still proves + the TCP+TLS handshake completed. Registries that time out or fail to + connect (DNS, SSL, or a corporate ``NPM URL Block`` network policy) + are dropped so we never hand them to ``npm install``, which would + otherwise hang for up to 15 minutes retrying a blocked host. + + Returns the reachable subset ordered fastest-first; unreachable + registries are omitted entirely. + """ + import concurrent.futures as _cf + + def _measure(registry: str) -> tuple[str, float]: + url = registry.rstrip("/") + "/-/ping" + req = urllib.request.Request(url, headers={"User-Agent": "OpenClawDeployer/1.0"}) + start = time.monotonic() + try: + with urllib.request.urlopen(req, timeout=timeout) as resp: + resp.read(64) + return registry, time.monotonic() - start + except urllib.error.HTTPError: + # A 404/403 still means the host is reachable. + return registry, time.monotonic() - start + except Exception: + return registry, float("inf") + + if not candidates: + return [] + + results: list[tuple[str, float]] = [] + try: + with _cf.ThreadPoolExecutor(max_workers=len(candidates)) as pool: + futures = [pool.submit(_measure, r) for r in candidates] + for f in _cf.as_completed(futures, timeout=timeout + 1.0): + results.append(f.result()) + except Exception as e: + self.log.debug(f"npm registry reachability probe failed: {e}") + + reachable = sorted((r for r in results if r[1] != float("inf")), key=lambda x: x[1]) + for registry, latency in reachable: + self.log.debug(f" npm registry {registry}: {int(latency * 1000)} ms") + return [registry for registry, _ in reachable] + def _install_openclaw_from_registry( self, install_prefix: Path, registry: str ) -> OpenClawInstallAttempt: @@ -1366,7 +1413,18 @@ def _install_openclaw_with_registry_fallback(self, install_prefix: Path) -> bool ) channel = self.cfg.get("openclaw.channel", "stable") expected_version = OPENCLAW_TARGET_VERSION if channel == "stable" else None - for registry in self._npm_registry_candidates(): + candidates = self._npm_registry_candidates() + registries = self._reachable_npm_registries(candidates) + if not registries: + self.log.error( + "Cannot reach any npm registry to install OpenClaw. Every candidate " + "registry failed a quick reachability probe, which usually means they " + "are blocked by your network or IT policy (e.g. a corporate " + "'NPM URL Block'). Tried: " + ", ".join(candidates) + ". " + "Configure an allowed npm registry via 'npm.registry' and retry." + ) + return False + for registry in registries: self.log.info(f" npm registry attempt: {registry}") attempt = self._install_openclaw_from_registry(install_prefix, registry) installed = attempt.installed_version diff --git a/tests/test_windows_setup_upgrade.py b/tests/test_windows_setup_upgrade.py index c21fc3e..5ff07b4 100644 --- a/tests/test_windows_setup_upgrade.py +++ b/tests/test_windows_setup_upgrade.py @@ -306,6 +306,7 @@ def test_prepare_rechecks_gateway_after_acquiring_upgrade_lock(self): def test_registry_tls_failure_retries_next_registry(self): self.ws.cfg = _Config({"npm.registry": "https://registry.npmmirror.com"}) + self.ws._reachable_npm_registries = lambda candidates, **_kw: list(candidates) self.ws._install_openclaw_from_registry = unittest.mock.Mock( side_effect=[ SimpleNamespace( @@ -326,6 +327,7 @@ def test_registry_tls_failure_retries_next_registry(self): def test_registry_policy_or_missing_package_retries_next_registry(self): self.ws.cfg = _Config({"npm.registry": "https://packagefeedproxy.microsoft.io/npm/"}) + self.ws._reachable_npm_registries = lambda candidates, **_kw: list(candidates) for output in ( "npm error code E404\nnpm error 404 Not Found - GET", @@ -354,6 +356,7 @@ def test_registry_policy_or_missing_package_retries_next_registry(self): def test_non_network_npm_failure_does_not_change_registry(self): self.ws.cfg = _Config() + self.ws._reachable_npm_registries = lambda candidates, **_kw: list(candidates) self.ws._install_openclaw_from_registry = unittest.mock.Mock( return_value=SimpleNamespace( installed_version=None, @@ -365,6 +368,14 @@ def test_non_network_npm_failure_does_not_change_registry(self): self.assertFalse(self.ws._install_openclaw_with_registry_fallback(self.appdata / "npm")) self.assertEqual(self.ws._install_openclaw_from_registry.call_count, 1) + def test_all_registries_unreachable_fails_fast_without_install(self): + self.ws.cfg = _Config() + self.ws._reachable_npm_registries = lambda candidates, **_kw: [] + self.ws._install_openclaw_from_registry = unittest.mock.Mock() + + self.assertFalse(self.ws._install_openclaw_with_registry_fallback(self.appdata / "npm")) + self.ws._install_openclaw_from_registry.assert_not_called() + def test_automatic_registry_fallbacks_are_https(self): self.ws.cfg = _Config()