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