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
*