Agent Zero AI framework
Apply Launcher scope changes before routing
Update remote file and execution metadata from the acknowledged Launcher gateway state before resolving the control request. Add regression coverage for the read/write to read-only transition so writes fail immediately while reads remain routed.
Alessandro committed
Jul 17, 2026 at 15:18 UTC
02e1a98604219cfa098e47754dcf6d946cca75ce
3 files changed
+65
-3
plugins/_a0_connector/AGENTS.md
+4
-2
@@ -35,8 +35,10 @@
35
master state, complete scope replacement, and Disconnect
36
(`emergency_disconnect` on the wire). Protected
37
WebUI mutations require CSRF, await the matching acknowledgement, and return
38
- refreshed status. Never let the WebUI select a host folder or personal
39
- browser profile.
38
+ refreshed status. Apply acknowledged master and scope state to remote file
39
+ and execution routing before resolving the control request; the follow-up
40
+ `connector_hello` only reconciles metadata. Never let the WebUI select a host
41
+ folder or personal browser profile.
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.
plugins/_a0_connector/helpers/ws_runtime.py
+17
-1
@@ -1085,7 +1085,23 @@ def resolve_pending_gateway_control(
1085
) -> bool:
1086
gateway = payload.get("gateway")
1087
if isinstance(gateway, dict):
1088
- store_sid_launcher_gateway_metadata(sid, gateway)
1088
+ stored = store_sid_launcher_gateway_metadata(sid, gateway)
1089
+ if stored is not None:
1090
+ active = stored.master_enabled and stored.state != "disconnected"
1091
+ files_enabled = active and stored.scopes["files"]
1092
+ writes_enabled = files_enabled and stored.scopes["file_write"]
1093
+ store_sid_remote_file_metadata(
1094
+ sid,
1095
+ {
1096
+ "enabled": files_enabled,
1097
+ "write_enabled": writes_enabled,
1098
+ "mode": "read_write" if writes_enabled else "read_only",
1099
+ },
1100
+ )
1101
+ store_sid_remote_exec_metadata(
1102
+ sid,
1103
+ {"enabled": active and stored.scopes["code_execution"]},
1104
+ )
1105
return _resolve_pending(_pending_gateway_controls, request_id, sid=sid, payload=payload)
1106
1107
tests/test_a0_connector_launcher_gateway.py
+44
@@ -274,6 +274,50 @@ def test_gateway_control_requires_csrf_and_waits_for_ack(monkeypatch) -> None:
274
ws_runtime.unregister_sid(sid)
275
276
277
+def test_gateway_scope_ack_updates_file_routing_before_follow_up_hello(monkeypatch) -> None:
278
+ sid = _sid("scope-transition")
279
+ ws_runtime.register_sid(sid)
280
+ ws_runtime.store_sid_launcher_gateway_metadata(sid, _gateway("installation-a"))
281
+ ws_runtime.store_sid_remote_file_metadata(
282
+ sid,
283
+ {"enabled": True, "write_enabled": True, "mode": "read_write"},
284
+ )
285
+ ws_runtime.store_sid_remote_exec_metadata(sid, {"enabled": True})
286
+
287
+ class FakeManager:
288
+ async def emit_to(self, _namespace, target_sid, _event, data, **_kwargs):
289
+ assert target_sid == sid
290
+ updated = _gateway("installation-a", file_write=False)
291
+ ws_runtime.resolve_pending_gateway_control(
292
+ data["request_id"],
293
+ sid=sid,
294
+ payload={
295
+ "request_id": data["request_id"],
296
+ "ok": True,
297
+ "gateway": updated,
298
+ },
299
+ )
300
+
301
+ monkeypatch.setattr(launcher_gateway_control, "get_shared_ws_manager", lambda: FakeManager())
302
+ handler = launcher_gateway_control.LauncherGatewayControl(None, None)
303
+ try:
304
+ result = asyncio.run(
305
+ handler.process(
306
+ {
307
+ "action": "replace_scopes",
308
+ "scopes": _gateway("installation-a", file_write=False)["scopes"],
309
+ },
310
+ None,
311
+ )
312
+ )
313
+ assert result["ok"] is True
314
+ assert ws_runtime.select_remote_file_target_sid("ctx", require_writes=False) == sid
315
+ assert ws_runtime.select_remote_file_target_sid("ctx", require_writes=True) is None
316
+ assert ws_runtime.select_remote_exec_target_sid("ctx") is None
317
+ finally:
318
+ ws_runtime.unregister_sid(sid)
319
+
320
+
321
def test_gateway_scope_control_requires_explicit_file_write() -> None:
322
handler = launcher_gateway_control.LauncherGatewayControl(None, None)
323
result = asyncio.run(