Fix unsecured connection settings link

Route the Configure credentials banner link through the welcome banner action dispatcher so it opens Settings directly at External Services > Authentication.\n\nAdd coverage for the unsecured connection banner and document the structured banner action contract for future banner links.

Alessandro committed Jun 23, 2026 at 18:47 UTC 4ba591358e8021ec567960dfa67729f34f9d608d
7 files changed +72 -3
extensions/python/banners/AGENTS.md
+1
@@ -12,6 +12,7 @@
12
13 - Banner IDs must be unique and stable.
14 - Use supported banner/card fields and types only.
15 +- Banner HTML links that trigger WebUI behavior should use supported structured actions, such as `data-banner-action`, instead of inline JavaScript handlers.
16 - Do not expose secrets, local paths, or raw system diagnostics in banner text.
17
18 ## Work Guidance
extensions/python/banners/_10_unsecured_connection.py
+1 -1
@@ -24,7 +24,7 @@ class UnsecuredConnectionCheck(Extension):
24 "priority": 80,
25 "title": "Unsecured Connection",
26 "html": """You are accessing Agent Zero from a non-local address without authentication.
27 - <a href="#" onclick="document.getElementById('settings').click(); return false;">
27 + <a href="#section-auth" data-banner-action="open-modal:settings/settings.html#section-auth">
28 Configure credentials</a> in Settings → External Services → Authentication.""",
29 "dismissible": True,
30 "source": "backend"
tests/test_unsecured_connection_banner.py new
+31
@@ -0,0 +1,31 @@
1 +import asyncio
2 +import sys
3 +from pathlib import Path
4 +
5 +
6 +PROJECT_ROOT = Path(__file__).resolve().parents[1]
7 +if str(PROJECT_ROOT) not in sys.path:
8 + sys.path.insert(0, str(PROJECT_ROOT))
9 +
10 +from extensions.python.banners import _10_unsecured_connection as unsecured_connection
11 +
12 +
13 +def test_unsecured_connection_banner_opens_authentication_settings(monkeypatch):
14 + monkeypatch.setattr(
15 + unsecured_connection.dotenv,
16 + "get_dotenv_value",
17 + lambda key, default="": "",
18 + )
19 +
20 + banners = []
21 + asyncio.run(
22 + unsecured_connection.UnsecuredConnectionCheck(agent=None).execute(
23 + banners=banners,
24 + frontend_context={"hostname": "agent.example.com", "protocol": "http:"},
25 + )
26 + )
27 +
28 + row = next(banner for banner in banners if banner.get("id") == "unsecured-connection")
29 + assert 'href="#section-auth"' in row["html"]
30 + assert 'data-banner-action="open-modal:settings/settings.html#section-auth"' in row["html"]
31 + assert "onclick" not in row["html"]
tests/test_welcome_composer_static.py
+5
@@ -58,7 +58,12 @@ def test_welcome_screen_embeds_shared_new_chat_composer() -> None:
58 assert 'class="welcome-banner-cta"' in welcome
59 assert "executeBannerAction(banner.cta_action)" in welcome
60 assert "executeBannerAction(action)" in welcome_store
61 + assert "handleBannerHtmlClick($event)" in welcome
62 + assert "handleBannerHtmlClick(event)" in welcome_store
63 + assert "[data-banner-action]" in welcome_store
64 assert 'action.startsWith("open-modal:")' in welcome_store
65 + assert 'const hashIndex = path.indexOf("#");' in welcome_store
66 + assert 'history.replaceState(null, "", `#${hash}`);' in welcome_store
67 assert ".welcome-banner-html .onboarding-banner-btn-container" in welcome
68 assert "justify-content: flex-end;" in welcome
69 assert "justify-content: flex-start;" in welcome
webui/components/welcome/AGENTS.md
+2
@@ -13,6 +13,8 @@
13
14 - Keep banner and discovery card behavior compatible with Python `banners` extensions.
15 - Supported banner/card CTA actions must stay synchronized with plugin discovery contracts.
16 +- Banner body links may use `data-banner-action`; these actions route through the same welcome action dispatcher as CTA buttons.
17 +- `open-modal:` banner actions may include a `#section-id` fragment, which updates the page hash before opening the modal so settings sections can deep-link correctly.
18 - Do not show setup prompts for already configured plugins when backend status can prevent it.
19 - The welcome screen mounts the shared chat composer to start a new chat; keep it mutually exclusive with the normal chat input DOM.
20 - Render `system-resources` as the dedicated System Resources panel, not as a generic alert banner.
webui/components/welcome/welcome-screen.html
+3 -1
@@ -55,7 +55,9 @@
55 </div>
56 <div class="welcome-banner-content">
57 <div class="welcome-banner-title" x-text="banner.title"></div>
58 - <div class="welcome-banner-html" x-html="banner.html"></div>
58 + <div class="welcome-banner-html"
59 + x-html="banner.html"
60 + @click="$store.welcomeStore.handleBannerHtmlClick($event)"></div>
61 </div>
62 <button class="welcome-banner-cta"
63 type="button"
webui/components/welcome/welcome-store.js
+29 -1
@@ -179,7 +179,7 @@ const model = {
179
180 if (action.startsWith("open-modal:")) {
181 const path = action.slice("open-modal:".length);
182 - if (path) window.openModal(path);
182 + this.openModalPath(path);
183 return;
184 }
185
@@ -189,6 +189,34 @@ const model = {
189 }
190 },
191
192 + handleBannerHtmlClick(event) {
193 + const actionTarget = event?.target?.closest?.("[data-banner-action]");
194 + if (!actionTarget) return;
195 + const action = actionTarget.getAttribute("data-banner-action");
196 + if (!action) return;
197 +
198 + event.preventDefault();
199 + event.stopPropagation();
200 + this.executeBannerAction(action);
201 + },
202 +
203 + openModalPath(path) {
204 + if (!path) return;
205 +
206 + let modalPath = path;
207 + let hash = "";
208 + const hashIndex = path.indexOf("#");
209 + if (hashIndex !== -1) {
210 + modalPath = path.slice(0, hashIndex);
211 + hash = path.slice(hashIndex + 1);
212 + }
213 +
214 + if (hash) {
215 + history.replaceState(null, "", `#${hash}`);
216 + }
217 + if (modalPath) window.openModal(modalPath);
218 + },
219 +
220 /**
221 * Dismiss a banner by ID.
222 *