Remove Launcher Host access controls from Core

Delete the Core WebUI gateway menu and its client-side control store so Host access stays owned by A0 Launcher. Keep /computer-use informational in WebUI and direct users to Launcher or A0 CLI for host changes.

Alessandro committed Jul 17, 2026 at 15:59 UTC 4b0c575f010a3169b0eac735dd8ceb41eca14494
8 files changed +26 -492
plugins/_a0_connector/AGENTS.md
+5 -17
@@ -42,23 +42,11 @@
42 - Launcher gateway scopes expose file reading and writing separately. File
43 writing depends on reading, and Code execution depends on file writing. Keep
44 older gateway declarations without `file_write` read/write compatible.
45 -- The `sync-status-end` Launcher gateway extension renders only when the user agent
46 - includes `A0-Launcher/`. It may show status, master/scope controls,
47 - preparation errors, and the connection action; standard browser sessions must
48 - not expose it. Keep its trigger inside the existing sync indicator, render one
49 - computer glyph per connected Launcher gateway, and retain one muted glyph
50 - while disconnected so the control remains reachable. Do not add a native
51 - tooltip, visible status label, count badge, or animated expansion.
52 - Keep the open popover above the compact right-canvas rail and report control
53 - failures through the shared notification system. Use the standard WebUI
54 - border-radius tokens for its controls and popover. After Disconnect is
55 - acknowledged, keep the popover open and turn that same action into Reconnect;
56 - when Host access was turned off in Launcher settings, open that Launcher-owned
57 - editor instead of silently re-granting access. Use only the narrow Launcher
58 - WebContents bridge so both actions also work in a detached Instance window.
59 - `/computer-use on` may use that same bounded bridge to request approval for
60 - the current lease, while `/computer-use off` disables only the gateway scope.
61 - Pausing remains the reversible master switch.
45 +- Agent Zero WebUI exposes no Launcher gateway icon, menu, status, or control
46 + bridge. Host access settings, Disconnect/Reconnect, scope changes, and
47 + Computer Use approval belong only to attached or detached A0 Launcher chrome.
48 + Keep the authenticated gateway HTTP/WebSocket protocol available for the
49 + Launcher and connector runtime without adding a Core WebUI surface.
50 - File operation results may arrive as chunked JSON/base64
51 `connector_file_op_result` frames; resolve the pending file operation only
52 after all chunks for the `op_id` are assembled.
plugins/_a0_connector/extensions/webui/sync-status-end/launcher-gateway.html deleted
-167
@@ -1,167 +0,0 @@
1 -<script type="module">
2 - import { store } from "/plugins/_a0_connector/webui/launcher-gateway-store.js";
3 -</script>
4 -
5 -<div
6 - x-data
7 - class="launcher-gateway-root"
8 - x-show="$store.launcherGateway?.visible"
9 - x-create="$store.launcherGateway?.onMount()"
10 - x-destroy="$store.launcherGateway?.cleanup()"
11 - @click.outside="$store.launcherGateway.open = false"
12 - @keydown.escape.window="$store.launcherGateway.open = false"
13 - style="display: none;"
14 ->
15 - <button
16 - type="button"
17 - class="launcher-gateway-trigger"
18 - :class="`is-${$store.launcherGateway.state}${$store.launcherGateway.open ? ' is-open' : ''}`"
19 - :aria-label="`Host access: ${$store.launcherGateway.stateLabel}`"
20 - aria-haspopup="dialog"
21 - :aria-expanded="$store.launcherGateway.open.toString()"
22 - @click.stop="$store.launcherGateway.open = !$store.launcherGateway.open"
23 - >
24 - <template x-for="(gateway, index) in ($store.launcherGateway.status?.gateways || [])" :key="gateway.id || index">
25 - <span class="material-symbols-outlined launcher-gateway-host-icon" aria-hidden="true">computer</span>
26 - </template>
27 - <span
28 - class="material-symbols-outlined launcher-gateway-host-icon is-placeholder"
29 - x-show="!($store.launcherGateway.status?.gateways || []).length"
30 - aria-hidden="true"
31 - >computer</span>
32 - </button>
33 -
34 - <section
35 - class="launcher-gateway-popover"
36 - role="dialog"
37 - aria-label="Launcher host access"
38 - x-show="$store.launcherGateway.open"
39 - x-transition.opacity.duration.120ms
40 - @click.stop
41 - style="display: none;"
42 - >
43 - <header class="launcher-gateway-header">
44 - <div>
45 - <strong x-text="$store.launcherGateway.hostLabel"></strong>
46 - <span x-text="$store.launcherGateway.stateLabel"></span>
47 - </div>
48 - <button type="button" class="launcher-gateway-close" aria-label="Close host access" @click="$store.launcherGateway.open = false">
49 - <span class="material-symbols-outlined" aria-hidden="true">close</span>
50 - </button>
51 - </header>
52 -
53 - <template x-if="$store.launcherGateway.gateway">
54 - <div class="launcher-gateway-controls">
55 - <div class="launcher-gateway-row is-master">
56 - <span>Host access</span>
57 - <label class="toggle launcher-gateway-switch">
58 - <input type="checkbox" aria-label="Host access" :checked="$store.launcherGateway.gateway.master_enabled" :disabled="$store.launcherGateway.saving" @change="$store.launcherGateway.setMaster($event.target.checked)">
59 - <span class="toggler"></span>
60 - </label>
61 - </div>
62 - <div class="launcher-gateway-row" :class="{ 'is-disabled': !$store.launcherGateway.gateway.master_enabled }">
63 - <span>Files read</span>
64 - <label class="toggle launcher-gateway-switch">
65 - <input type="checkbox" aria-label="Files read" :checked="$store.launcherGateway.gateway.scopes.files" :disabled="$store.launcherGateway.saving || !$store.launcherGateway.gateway.master_enabled" @change="$store.launcherGateway.setScope('files', $event.target.checked)">
66 - <span class="toggler"></span>
67 - </label>
68 - </div>
69 - <div class="launcher-gateway-row" :class="{ 'is-disabled': !$store.launcherGateway.gateway.master_enabled || !$store.launcherGateway.gateway.scopes.files }">
70 - <span>Files write</span>
71 - <label class="toggle launcher-gateway-switch">
72 - <input type="checkbox" aria-label="Files write" :checked="$store.launcherGateway.gateway.scopes.file_write" :disabled="$store.launcherGateway.saving || !$store.launcherGateway.gateway.master_enabled || !$store.launcherGateway.gateway.scopes.files" @change="$store.launcherGateway.setScope('file_write', $event.target.checked)">
73 - <span class="toggler"></span>
74 - </label>
75 - </div>
76 - <div class="launcher-gateway-row" :class="{ 'is-disabled': !$store.launcherGateway.gateway.master_enabled || !$store.launcherGateway.gateway.scopes.file_write }">
77 - <span>Code execution</span>
78 - <label class="toggle launcher-gateway-switch">
79 - <input type="checkbox" aria-label="Code execution" :checked="$store.launcherGateway.gateway.scopes.code_execution" :disabled="$store.launcherGateway.saving || !$store.launcherGateway.gateway.master_enabled || !$store.launcherGateway.gateway.scopes.file_write" @change="$store.launcherGateway.setScope('code_execution', $event.target.checked)">
80 - <span class="toggler"></span>
81 - </label>
82 - </div>
83 - <div class="launcher-gateway-row" :class="{ 'is-disabled': !$store.launcherGateway.gateway.master_enabled }">
84 - <span>Use my Browser</span>
85 - <label class="toggle launcher-gateway-switch">
86 - <input type="checkbox" aria-label="Use my Browser" :checked="$store.launcherGateway.gateway.scopes.browser" :disabled="$store.launcherGateway.saving || !$store.launcherGateway.gateway.master_enabled" @change="$store.launcherGateway.setScope('browser', $event.target.checked)">
87 - <span class="toggler"></span>
88 - </label>
89 - </div>
90 - <div class="launcher-gateway-row" :class="{ 'is-disabled': !$store.launcherGateway.gateway.master_enabled }">
91 - <span>Computer Use</span>
92 - <label class="toggle launcher-gateway-switch">
93 - <input type="checkbox" aria-label="Computer Use" :checked="$store.launcherGateway.gateway.scopes.computer_use" :disabled="$store.launcherGateway.saving || !$store.launcherGateway.gateway.master_enabled" @change="$store.launcherGateway.setScope('computer_use', $event.target.checked)">
94 - <span class="toggler"></span>
95 - </label>
96 - </div>
97 - </div>
98 - </template>
99 -
100 - <div class="launcher-gateway-notices" x-show="$store.launcherGateway.preparationMessages.length">
101 - <template x-for="message in $store.launcherGateway.preparationMessages" :key="message">
102 - <p><span class="material-symbols-outlined" aria-hidden="true">warning</span><span x-text="message"></span></p>
103 - </template>
104 - </div>
105 -
106 - <p
107 - class="launcher-gateway-empty"
108 - x-show="!$store.launcherGateway.gateway"
109 - x-text="$store.launcherGateway.unavailableMessage"
110 - ></p>
111 -
112 - <button
113 - type="button"
114 - class="launcher-gateway-connection-action"
115 - :class="{ 'is-reconnect': !$store.launcherGateway.gateway }"
116 - x-show="$store.launcherGateway.gateway || $store.launcherGateway.reconnectAvailable || $store.launcherGateway.canOpenSettings"
117 - :disabled="$store.launcherGateway.saving"
118 - @click="$store.launcherGateway.gateway ? $store.launcherGateway.emergencyDisconnect() : ($store.launcherGateway.reconnectAvailable ? $store.launcherGateway.reconnect() : $store.launcherGateway.openSettings())"
119 - x-text="$store.launcherGateway.saving ? ($store.launcherGateway.gateway ? 'Disconnecting…' : ($store.launcherGateway.reconnectAvailable ? 'Reconnecting…' : 'Opening…')) : ($store.launcherGateway.gateway ? 'Disconnect' : ($store.launcherGateway.reconnectAvailable ? 'Reconnect' : 'Open Host access'))"
120 - ></button>
121 - </section>
122 -</div>
123 -
124 -<style>
125 - #time-date-container:has(.launcher-gateway-trigger.is-open) { z-index: 3500; }
126 - .launcher-gateway-root { position: relative; display: inline-flex; align-items: center; }
127 - .launcher-gateway-trigger {
128 - display: inline-flex; align-items: center; gap: .12rem; min-height: 1.4rem;
129 - padding: .08rem; border: 0; border-radius: var(--border-radius-sm);
130 - color: var(--color-text-secondary); background: transparent; cursor: pointer;
131 - }
132 - .launcher-gateway-trigger:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
133 - .launcher-gateway-host-icon { font-size: 1rem; }
134 - .launcher-gateway-trigger.is-connected .launcher-gateway-host-icon { color: #00c340; }
135 - .launcher-gateway-trigger.is-connecting .launcher-gateway-host-icon { color: #5b9cff; }
136 - .launcher-gateway-trigger.is-paused .launcher-gateway-host-icon { color: #d6a43d; }
137 - .launcher-gateway-trigger.is-needs_action .launcher-gateway-host-icon,
138 - .launcher-gateway-trigger.is-multiple_hosts .launcher-gateway-host-icon { color: #e49a35; }
139 - .launcher-gateway-trigger.is-error .launcher-gateway-host-icon { color: #db5b61; }
140 - .launcher-gateway-host-icon.is-placeholder { opacity: .55; }
141 - .launcher-gateway-popover {
142 - position: absolute; top: calc(100% + .45rem); right: 0; z-index: 1200; width: min(20rem, calc(100vw - 2rem));
143 - padding: .8rem; border: 1px solid var(--color-border); border-radius: var(--border-radius-sm);
144 - background: var(--color-panel); box-shadow: 0 16px 42px rgba(0, 0, 0, .24);
145 - }
146 - .launcher-gateway-header { display: flex; justify-content: space-between; gap: .75rem; align-items: flex-start; }
147 - .launcher-gateway-header div { display: grid; gap: .1rem; }
148 - .launcher-gateway-header span { color: var(--color-text-secondary); font-size: .75rem; }
149 - .launcher-gateway-close { border: 0; background: transparent; color: inherit; cursor: pointer; padding: 0; }
150 - .launcher-gateway-close .material-symbols-outlined { font-size: 1.1rem; }
151 - .launcher-gateway-controls { display: grid; gap: .15rem; margin-top: .7rem; }
152 - .launcher-gateway-row { display: flex; justify-content: space-between; align-items: center; gap: 1rem; min-height: 2rem; font-size: .82rem; }
153 - .launcher-gateway-row.is-master { padding-bottom: .35rem; margin-bottom: .2rem; border-bottom: 1px solid var(--color-border); font-weight: 650; }
154 - .launcher-gateway-row.is-disabled { color: var(--color-text-secondary); opacity: .55; }
155 - .launcher-gateway-switch { flex: 0 0 auto; width: 2.2rem; height: 1.25rem; }
156 - .launcher-gateway-switch .toggler:before { width: .9rem; height: .9rem; left: .18rem; bottom: .18rem; }
157 - .launcher-gateway-switch input:checked + .toggler:before { transform: translateX(.94rem); }
158 - .launcher-gateway-switch input:focus-visible + .toggler { outline: 2px solid var(--color-primary); outline-offset: 2px; }
159 - .launcher-gateway-switch input:disabled + .toggler { cursor: default; }
160 - .launcher-gateway-notices { display: grid; gap: .35rem; margin-top: .55rem; }
161 - .launcher-gateway-notices p { display: flex; gap: .35rem; margin: 0; color: #d79b35; font-size: .75rem; line-height: 1.3; }
162 - .launcher-gateway-notices .material-symbols-outlined { font-size: .95rem; }
163 - .launcher-gateway-empty { margin: .7rem 0 0; color: var(--color-text-secondary); font-size: .78rem; }
164 - .launcher-gateway-connection-action { width: 100%; margin-top: .75rem; padding: .48rem .65rem; border: 1px solid color-mix(in srgb, #db5b61 55%, var(--color-border)); border-radius: var(--border-radius-sm); background: transparent; color: #db5b61; cursor: pointer; }
165 - .launcher-gateway-connection-action.is-reconnect { border-color: var(--color-highlight); background: var(--color-highlight); color: #fff; }
166 - .launcher-gateway-connection-action:disabled { opacity: .55; cursor: default; }
167 -</style>
plugins/_a0_connector/webui/launcher-gateway-store.js deleted
-223
@@ -1,223 +0,0 @@
1 -import { createStore } from "/js/AlpineStore.js";
2 -import { callJsonApi } from "/js/api.js";
3 -import { toastFrontendError } from "/components/notifications/notification-store.js";
4 -
5 -const STATUS_API = "/plugins/_a0_connector/v1/launcher_gateway_status";
6 -const CONTROL_API = "/plugins/_a0_connector/v1/launcher_gateway_control";
7 -
8 -const model = {
9 - status: { state: "disconnected", gateway: null },
10 - loading: false,
11 - saving: false,
12 - open: false,
13 - intervalId: null,
14 - reconnectAvailable: false,
15 -
16 - get visible() {
17 - return /(?:^|\s)A0-Launcher\/[^\s]+/.test(navigator.userAgent);
18 - },
19 -
20 - get canReconnect() {
21 - return typeof window.a0LauncherHost?.reconnect === "function";
22 - },
23 -
24 - get canOpenSettings() {
25 - return typeof window.a0LauncherHost?.openSettings === "function";
26 - },
27 -
28 - get gateway() {
29 - return this.state === "disconnected" ? null : this.status?.gateway || null;
30 - },
31 -
32 - get state() {
33 - return this.status?.state || "disconnected";
34 - },
35 -
36 - get stateLabel() {
37 - const labels = {
38 - connecting: "Connecting",
39 - connected: "Connected",
40 - paused: "Paused",
41 - needs_action: "Needs action",
42 - error: "Error",
43 - multiple_hosts: "Multiple hosts",
44 - disconnected: "Disconnected",
45 - };
46 - return labels[this.state] || "Disconnected";
47 - },
48 -
49 - get hostLabel() {
50 - return this.gateway?.host_label || "Launcher host";
51 - },
52 -
53 - get preparationMessages() {
54 - const status = this.gateway?.status || {};
55 - const messages = [];
56 - for (const key of ["browser", "computer_use"]) {
57 - const value = status[key];
58 - if (typeof value === "string" && value) messages.push(value);
59 - else if (value?.message) messages.push(value.message);
60 - else if (value?.error) messages.push(value.error);
61 - }
62 - if (this.status?.error) messages.push(this.status.error);
63 - return [...new Set(messages)];
64 - },
65 -
66 - get unavailableMessage() {
67 - if (this.reconnectAvailable) return "Host access was disconnected for this Launcher tab.";
68 - if (this.state === "disconnected") {
69 - return this.canOpenSettings
70 - ? "Host access is off in A0 Launcher."
71 - : "Host access is off. Use the computer icon beside Reload in the Launcher header to turn it on.";
72 - }
73 - return "Open Host access in A0 Launcher for connection details.";
74 - },
75 -
76 - onMount() {
77 - if (!this.visible || this.intervalId) return;
78 - void this.refresh();
79 - this.intervalId = window.setInterval(() => this.refresh(), 2000);
80 - },
81 -
82 - cleanup() {
83 - if (this.intervalId) window.clearInterval(this.intervalId);
84 - this.intervalId = null;
85 - },
86 -
87 - async refresh() {
88 - if (!this.visible || this.loading) return;
89 - this.loading = true;
90 - try {
91 - this.status = await callJsonApi(STATUS_API, {});
92 - } catch (error) {
93 - console.error("Failed to load Launcher host status:", error);
94 - this.status = { state: "error", gateway: null, error: error?.message || "Status unavailable" };
95 - } finally {
96 - this.loading = false;
97 - await this.refreshReconnectState();
98 - }
99 - },
100 -
101 - async refreshReconnectState() {
102 - try {
103 - const state = await window.a0LauncherHost?.getState?.();
104 - this.reconnectAvailable = state?.reconnectAvailable === true;
105 - } catch {
106 - this.reconnectAvailable = false;
107 - }
108 - },
109 -
110 - async setMaster(enabled) {
111 - await this.control({ action: "set_master", enabled: Boolean(enabled) });
112 - },
113 -
114 - async setScope(scope, enabled) {
115 - const current = this.gateway?.scopes || {};
116 - const scopes = {
117 - files: Boolean(current.files),
118 - file_write: Boolean(current.file_write ?? current.files),
119 - code_execution: Boolean(current.code_execution),
120 - browser: Boolean(current.browser),
121 - computer_use: Boolean(current.computer_use),
122 - [scope]: Boolean(enabled),
123 - };
124 - if (!scopes.files) scopes.file_write = false;
125 - if (!scopes.file_write) scopes.code_execution = false;
126 - return this.control({ action: "replace_scopes", scopes });
127 - },
128 -
129 - async setComputerUse(enabled) {
130 - if (!this.gateway) await this.refresh();
131 - if (!this.gateway) throw new Error("Launcher Host access is not connected.");
132 - if (!this.gateway.master_enabled) {
133 - throw new Error("Launcher Host access is paused. Resume it first.");
134 - }
135 -
136 - const requested = Boolean(enabled);
137 - if (requested && typeof window.a0LauncherHost?.rearmComputerUse !== "function") {
138 - throw new Error(
139 - "This A0 Launcher cannot request Computer Use permission. Update it or use A0 CLI.",
140 - );
141 - }
142 - if (Boolean(this.gateway.scopes?.computer_use) !== requested) {
143 - if (!await this.setScope("computer_use", requested)) return false;
144 - }
145 - if (!requested) return true;
146 -
147 - const response = await window.a0LauncherHost.rearmComputerUse();
148 - if (!response?.ok) {
149 - throw new Error(
150 - response?.message || "Computer Use permission could not be requested.",
151 - );
152 - }
153 - return true;
154 - },
155 -
156 - async emergencyDisconnect() {
157 - if (await this.control({ action: "emergency_disconnect" })) {
158 - this.reconnectAvailable = this.canReconnect;
159 - this.status = { ...this.status, state: "disconnected", connected: false, gateway: null, gateways: [] };
160 - if (!this.canReconnect) this.open = false;
161 - }
162 - },
163 -
164 - async reconnect() {
165 - if (this.saving) return;
166 - this.saving = true;
167 - try {
168 - const response = await window.a0LauncherHost?.reconnect?.();
169 - if (!response?.ok) throw new Error(response?.message || "Host access could not reconnect.");
170 - this.reconnectAvailable = false;
171 - this.status = { ...this.status, state: "connecting", connected: false, gateway: null, gateways: [] };
172 - } catch (error) {
173 - console.error("Failed to reconnect Launcher host:", error);
174 - void toastFrontendError(
175 - error?.message || "Host access could not reconnect.",
176 - "Host access",
177 - );
178 - await this.refreshReconnectState();
179 - } finally {
180 - this.saving = false;
181 - }
182 - },
183 -
184 - async openSettings() {
185 - if (this.saving) return;
186 - this.saving = true;
187 - try {
188 - const response = await window.a0LauncherHost?.openSettings?.();
189 - if (!response?.ok) throw new Error(response?.message || "Host access settings could not open.");
190 - this.open = false;
191 - } catch (error) {
192 - console.error("Failed to open Launcher host settings:", error);
193 - void toastFrontendError(
194 - error?.message || "Host access settings could not open.",
195 - "Host access",
196 - );
197 - } finally {
198 - this.saving = false;
199 - }
200 - },
201 -
202 - async control(payload) {
203 - if (this.saving) return;
204 - this.saving = true;
205 - try {
206 - const response = await callJsonApi(CONTROL_API, payload);
207 - this.status = response?.status || this.status;
208 - return true;
209 - } catch (error) {
210 - console.error("Failed to control Launcher host:", error);
211 - void toastFrontendError(
212 - error?.message || "Host access change failed.",
213 - "Host access",
214 - );
215 - await this.refresh();
216 - return false;
217 - } finally {
218 - this.saving = false;
219 - }
220 - },
221 -};
222 -
223 -export const store = createStore("launcherGateway", model);
plugins/_commands/AGENTS.md
+3 -3
@@ -32,9 +32,9 @@
32 - Script commands may emit `send_message` with `text` to submit the rendered composer text immediately after command resolution.
33 - Commands accept prefix syntax (`/goal objective`) and exact postfix syntax (`objective /goal`); ordinary mid-sentence mentions are not invocations.
34 - WebUI sends resolve through the picker effect path, while backend-originated messages resolve before reaching the agent.
35 -- Built-in `/computer-use on|off` emits a bounded `computer_use` effect. A
36 - Launcher-owned WebUI applies it to that tab's Host access lease; an ordinary
37 - WebUI directs the user to run the same command in A0 CLI.
35 +- Built-in `/computer-use on|off` emits a bounded `computer_use` effect. WebUI
36 + only directs the user to Host access in A0 Launcher or the same command in A0
37 + CLI; it never changes a Launcher gateway lease from Agent Zero page content.
38
39 ## Work Guidance
40
plugins/_commands/commands/connector_commands.py
+2 -1
@@ -160,7 +160,8 @@ def _handle_computer_use(context_id: str, raw_args: str) -> dict[str, Any]:
160 "enabled": enabled,
161 "fallback": (
162 "Computer Use permissions are controlled on the connected host. "
163 - f"Run `/computer-use {command}` in the A0 CLI terminal."
163 + "Use Host access in A0 Launcher, or run "
164 + f"`/computer-use {command}` in the A0 CLI terminal."
165 ),
166 }
167 )
plugins/_commands/tests/test_commands_plugin.py
+3 -2
@@ -268,7 +268,7 @@ def test_models_command_always_opens_modal():
268
269
270 @pytest.mark.parametrize(("argument", "enabled"), [("on", True), ("off", False)])
271 -def test_computer_use_command_dispatches_to_launcher_or_cli(
271 +def test_computer_use_command_guides_launcher_or_cli(
272 argument: str,
273 enabled: bool,
274 ):
@@ -289,7 +289,8 @@ def test_computer_use_command_dispatches_to_launcher_or_cli(
289 "enabled": enabled,
290 "fallback": (
291 "Computer Use permissions are controlled on the connected host. "
292 - f"Run `/computer-use {argument}` in the A0 CLI terminal."
292 + "Use Host access in A0 Launcher, or run "
293 + f"`/computer-use {argument}` in the A0 CLI terminal."
294 ),
295 }
296 ]
plugins/_commands/webui/commands-slash-store.js
+4 -25
@@ -441,31 +441,10 @@ const model = {
441 }
442 if (type === "computer_use") {
443 hadToast = true;
444 - if (typeof window.a0LauncherHost?.getState !== "function") {
445 - notifyInfo(
446 - "Computer Use",
447 - String(effect.fallback || "Run this command in A0 CLI."),
448 - );
449 - continue;
450 - }
451 - try {
452 - const { store } = await import(
453 - "/plugins/_a0_connector/webui/launcher-gateway-store.js"
454 - );
455 - const enabled = Boolean(effect.enabled);
456 - if (!await store.setComputerUse(enabled)) {
457 - hadError = true;
458 - continue;
459 - }
460 - notifySuccess(
461 - enabled
462 - ? "Computer Use permission requested. Approve the system dialog."
463 - : "Computer Use is off for this Launcher host.",
464 - );
465 - } catch (error) {
466 - hadError = true;
467 - notifyError(error?.message || "Computer Use could not be changed.");
468 - }
444 + notifyInfo(
445 + "Computer Use",
446 + String(effect.fallback || "Use Host access in A0 Launcher, or run this command in A0 CLI."),
447 + );
448 continue;
449 }
450 if (type === "goal_changed") {
tests/test_a0_connector_launcher_gateway.py
+9 -54
@@ -45,21 +45,9 @@ def test_launcher_gateway_features_are_negotiated_on_http_and_websocket() -> Non
45 assert LauncherGatewayStatus.requires_auth() is True
46
47
48 -def test_launcher_gateway_control_errors_use_webui_notifications() -> None:
49 - source = (
50 - Path(__file__).parents[1]
51 - / "plugins"
52 - / "_a0_connector"
53 - / "webui"
54 - / "launcher-gateway-store.js"
55 - ).read_text(encoding="utf-8")
56 - assert 'import { toastFrontendError }' in source
57 - assert 'void toastFrontendError(' in source
58 -
59 -
60 -def test_launcher_gateway_indicator_joins_sync_status_without_visual_noise() -> None:
48 +def test_launcher_gateway_has_no_agent_zero_webui_controls() -> None:
49 root = Path(__file__).parents[1]
62 - source = (
50 + extension = (
51 root
52 / "plugins"
53 / "_a0_connector"
@@ -67,55 +55,22 @@ def test_launcher_gateway_indicator_joins_sync_status_without_visual_noise() ->
55 / "webui"
56 / "sync-status-end"
57 / "launcher-gateway.html"
70 - ).read_text(encoding="utf-8")
71 - sync_status = (root / "webui" / "components" / "sync" / "sync-status.html").read_text(
72 - encoding="utf-8"
58 )
74 -
75 - assert '<x-extension id="sync-status-end"></x-extension>' in sync_status
76 - assert sync_status.count("<circle") == 1
77 - assert ":title=" not in sync_status
78 - assert 'x-for="(gateway, index)' in source
79 - assert "launcher-gateway-trigger-label" not in source
80 - assert "launcher-gateway-dot" not in source
81 - assert ":title=" not in source
82 - assert "'Disconnect' : ($store.launcherGateway.reconnectAvailable ? 'Reconnect' : 'Open Host access')" in source
83 - assert "$store.launcherGateway.reconnectAvailable ? $store.launcherGateway.reconnect() : $store.launcherGateway.openSettings()" in source
84 - assert "'Reconnect' : 'Open Host access'" in source
85 - assert "Emergency disconnect" not in source
86 - popover_css = source.split(".launcher-gateway-popover {", 1)[1].split("}", 1)[0]
87 - assert "border-radius: var(--border-radius-sm);" in popover_css
88 -
89 -
90 -def test_launcher_gateway_disconnect_becomes_reconnect_through_the_launcher_bridge() -> None:
91 - source = (
92 - Path(__file__).parents[1]
59 + store = (
60 + root
61 / "plugins"
62 / "_a0_connector"
63 / "webui"
64 / "launcher-gateway-store.js"
97 - ).read_text(encoding="utf-8")
98 - assert "window.a0LauncherHost?.getState?.()" in source
99 - assert "window.a0LauncherHost?.reconnect?.()" in source
100 - assert "window.a0LauncherHost?.openSettings?.()" in source
101 - assert 'typeof window.a0LauncherHost?.reconnect === "function"' in source
102 - assert 'typeof window.a0LauncherHost?.openSettings === "function"' in source
103 - assert "Host access is off in A0 Launcher." in source
104 - assert 'state: "disconnected", connected: false, gateway: null' in source
105 - assert 'state: "connecting", connected: false, gateway: null' in source
106 -
107 -
108 -def test_launcher_computer_use_command_uses_the_bounded_approval_bridge() -> None:
109 - root = Path(__file__).parents[1]
110 - gateway_source = (
111 - root / "plugins" / "_a0_connector" / "webui" / "launcher-gateway-store.js"
112 - ).read_text(encoding="utf-8")
65 + )
66 commands_source = (
67 root / "plugins" / "_commands" / "webui" / "commands-slash-store.js"
68 ).read_text(encoding="utf-8")
69
117 - assert "async setComputerUse(enabled)" in gateway_source
118 - assert "window.a0LauncherHost.rearmComputerUse()" in gateway_source
70 + assert not extension.exists()
71 + assert not store.exists()
72 + assert "a0LauncherHost" not in commands_source
73 + assert "launcher-gateway-store.js" not in commands_source
74 assert 'type === "computer_use"' in commands_source
75
76