main
py 713 lines 24.5 KB
Raw
1 import importlib.util
2 import sys
3 import time
4 import uuid
5 from pathlib import Path
6 from types import SimpleNamespace
7
8 import yaml
9
10 PROJECT_ROOT = Path(__file__).resolve().parents[1]
11 if str(PROJECT_ROOT) not in sys.path:
12 sys.path.insert(0, str(PROJECT_ROOT))
13
14
15 def _restore_real_helpers_package() -> None:
16 helpers_module = sys.modules.get("helpers")
17 if (
18 helpers_module is None
19 or getattr(helpers_module, "__file__", "")
20 or list(getattr(helpers_module, "__path__", []))
21 ):
22 return
23
24 for name in list(sys.modules):
25 if name == "helpers" or name.startswith("helpers."):
26 del sys.modules[name]
27
28
29 _restore_real_helpers_package()
30
31 from plugins._a0_connector.helpers import ws_runtime
32
33
34 PROMPT_ROOT = PROJECT_ROOT / "plugins" / "_a0_connector" / "prompts"
35 REMOTE_PROMPT_FILES = {
36 "code_execution_remote": "agent.system.tool.code_execution_remote.md",
37 "computer_use_remote": "agent.system.tool.computer_use_remote.md",
38 "text_editor_remote": "agent.system.tool.text_editor_remote.md",
39 }
40 GATE_PATH = (
41 PROJECT_ROOT
42 / "plugins"
43 / "_a0_connector"
44 / "extensions"
45 / "python"
46 / "_functions"
47 / "_11_tools_prompt"
48 / "build_prompt"
49 / "end"
50 / "_70_include_remote_tool_stubs.py"
51 )
52
53
54 def _load_gate_class():
55 spec = importlib.util.spec_from_file_location(
56 "test_a0_connector_remote_tool_gate",
57 GATE_PATH,
58 )
59 module = importlib.util.module_from_spec(spec)
60 assert spec and spec.loader
61 sys.modules[spec.name] = module
62 spec.loader.exec_module(module)
63 return module.IncludeRemoteToolStubs
64
65
66 IncludeRemoteToolStubs = _load_gate_class()
67
68
69 class FakeContext:
70 def __init__(self, context_id: str):
71 self.id = context_id
72
73 def get_data(self, key: str, recursive: bool = True):
74 return None
75
76
77 class FakeAgent:
78 def __init__(self, context_id: str):
79 self.context = FakeContext(context_id)
80 self.config = SimpleNamespace(profile="default")
81
82 def read_prompt(self, file: str, **kwargs) -> str:
83 text = (PROMPT_ROOT / file).read_text(encoding="utf-8")
84 for key, value in kwargs.items():
85 text = text.replace("{{" + key + "}}", str(value))
86 return text
87
88
89 def _context_id() -> str:
90 return f"ctx-{uuid.uuid4()}"
91
92
93 def _sid() -> str:
94 return f"sid-{uuid.uuid4()}"
95
96
97 def _parse_skill_frontmatter(path: Path) -> dict:
98 text = path.read_text(encoding="utf-8")
99 assert text.startswith("---")
100 return yaml.safe_load(text.split("---", 2)[1]) or {}
101
102
103 def _remote_prompt_blob() -> str:
104 return "\n\n".join(
105 (PROMPT_ROOT / prompt_file).read_text(encoding="utf-8").strip()
106 for prompt_file in REMOTE_PROMPT_FILES.values()
107 )
108
109
110 def _apply_gate(context_id: str, *, include_standard_remote_prompts: bool = True) -> str:
111 result = "## available tools\nbase_tool"
112 if include_standard_remote_prompts:
113 result = f"{result}\n\n{_remote_prompt_blob()}"
114 data = {"result": result}
115 IncludeRemoteToolStubs(agent=FakeAgent(context_id)).execute(data=data)
116 return data["result"]
117
118
119 def _assert_remote_tool_absent(prompt: str, tool_name: str) -> None:
120 assert f'"tool_name": "{tool_name}"' not in prompt
121
122
123 def test_remote_tool_gate_hides_remote_prompts_without_connected_cli():
124 prompt = _apply_gate(_context_id())
125
126 for tool_name in REMOTE_PROMPT_FILES:
127 _assert_remote_tool_absent(prompt, tool_name)
128 assert "base_tool" in prompt
129
130
131 def test_remote_tool_gate_includes_file_prompt_for_read_only_connected_cli():
132 context_id = _context_id()
133 sid = _sid()
134 ws_runtime.register_sid(sid)
135 ws_runtime.store_sid_remote_file_metadata(
136 sid,
137 {"enabled": True, "write_enabled": False, "mode": "read_only"},
138 )
139 try:
140 prompt = _apply_gate(context_id)
141 finally:
142 ws_runtime.unregister_sid(sid)
143
144 assert '"tool_name": "text_editor_remote"' in prompt
145 _assert_remote_tool_absent(prompt, "code_execution_remote")
146 _assert_remote_tool_absent(prompt, "computer_use_remote")
147
148
149 def test_remote_tool_gate_requires_f4_enabled_remote_exec_metadata():
150 context_id = _context_id()
151 sid = _sid()
152 ws_runtime.register_sid(sid)
153 ws_runtime.store_sid_remote_file_metadata(
154 sid,
155 {"enabled": True, "write_enabled": True, "mode": "read_write"},
156 )
157 ws_runtime.store_sid_remote_exec_metadata(sid, {"enabled": False})
158 try:
159 prompt = _apply_gate(context_id)
160 _assert_remote_tool_absent(prompt, "code_execution_remote")
161
162 ws_runtime.store_sid_remote_exec_metadata(sid, {"enabled": True})
163 prompt = _apply_gate(context_id)
164 finally:
165 ws_runtime.unregister_sid(sid)
166
167 assert '"tool_name": "code_execution_remote"' in prompt
168
169
170 def test_remote_tool_gate_requires_enabled_computer_use_metadata():
171 context_id = _context_id()
172 sid = _sid()
173 ws_runtime.register_sid(sid)
174 ws_runtime.store_sid_computer_use_metadata(
175 sid,
176 {"supported": True, "enabled": False, "status": "off"},
177 )
178 try:
179 prompt = _apply_gate(context_id)
180 _assert_remote_tool_absent(prompt, "computer_use_remote")
181
182 ws_runtime.store_sid_computer_use_metadata(
183 sid,
184 {"supported": True, "enabled": True, "status": "ready"},
185 )
186 prompt = _apply_gate(context_id)
187 finally:
188 ws_runtime.unregister_sid(sid)
189
190 assert '"tool_name": "computer_use_remote"' in prompt
191 assert "### computer_use_remote" in prompt
192
193
194 def test_remote_tool_gate_hides_rearm_required_computer_use_prompt():
195 context_id = _context_id()
196 sid = _sid()
197 ws_runtime.register_sid(sid)
198 ws_runtime.store_sid_computer_use_metadata(
199 sid,
200 {
201 "supported": True,
202 "enabled": True,
203 "status": "rearm required",
204 "last_error": "permission expired",
205 },
206 )
207 try:
208 prompt = _apply_gate(context_id)
209 finally:
210 ws_runtime.unregister_sid(sid)
211
212 _assert_remote_tool_absent(prompt, "computer_use_remote")
213
214
215 def test_remote_tool_gate_appends_available_prompt_when_standard_prompt_missing():
216 context_id = _context_id()
217 sid = _sid()
218 ws_runtime.register_sid(sid)
219 ws_runtime.store_sid_remote_file_metadata(sid, {"enabled": True})
220 try:
221 prompt = _apply_gate(context_id, include_standard_remote_prompts=False)
222 finally:
223 ws_runtime.unregister_sid(sid)
224
225 assert '"tool_name": "text_editor_remote"' in prompt
226 _assert_remote_tool_absent(prompt, "code_execution_remote")
227 _assert_remote_tool_absent(prompt, "computer_use_remote")
228
229
230 def test_remote_tool_gate_does_not_readd_a_policy_blocked_prompt(monkeypatch):
231 context_id = _context_id()
232 sid = _sid()
233 monkeypatch.setitem(
234 IncludeRemoteToolStubs.execute.__globals__,
235 "resolve_tool",
236 lambda _agent, name: SimpleNamespace(allowed=name != "text_editor_remote"),
237 )
238 ws_runtime.register_sid(sid)
239 ws_runtime.store_sid_remote_file_metadata(sid, {"enabled": True})
240 try:
241 prompt = _apply_gate(context_id, include_standard_remote_prompts=False)
242 finally:
243 ws_runtime.unregister_sid(sid)
244
245 _assert_remote_tool_absent(prompt, "text_editor_remote")
246
247
248 def test_responses_function_tools_follow_remote_prompt_gate(monkeypatch):
249 from helpers import responses_tools
250
251 context_id = _context_id()
252 agent = FakeAgent(context_id)
253 monkeypatch.setattr(
254 responses_tools.subagents,
255 "get_paths",
256 lambda *args, **kwargs: [str(PROMPT_ROOT)],
257 )
258
259 names = {name for name, _prompt in responses_tools._local_tool_prompts(agent)}
260 assert names.isdisjoint(REMOTE_PROMPT_FILES)
261
262 sid = _sid()
263 ws_runtime.register_sid(sid)
264 ws_runtime.store_sid_remote_file_metadata(sid, {"enabled": True})
265 ws_runtime.store_sid_remote_exec_metadata(sid, {"enabled": True})
266 ws_runtime.store_sid_computer_use_metadata(
267 sid,
268 {"supported": True, "enabled": True, "status": "ready"},
269 )
270 try:
271 names = {name for name, _prompt in responses_tools._local_tool_prompts(agent)}
272 finally:
273 ws_runtime.unregister_sid(sid)
274
275 assert REMOTE_PROMPT_FILES.keys() <= names
276
277
278 def test_computer_use_remote_prompt_is_cli_session_wide_not_context_scoped():
279 prompt = (PROMPT_ROOT / "agent.system.tool.computer_use_remote.md").read_text(
280 encoding="utf-8"
281 )
282
283 assert "### computer_use_remote" in prompt
284 assert '"tool_name": "computer_use_remote"' in prompt
285 assert "scoped to the current CLI session" in prompt
286 assert "not scoped to a single chat context" in prompt
287
288
289 def test_computer_use_remote_prompt_keeps_runtime_failures_actionable():
290 prompt = (PROMPT_ROOT / "agent.system.tool.computer_use_remote.md").read_text(
291 encoding="utf-8"
292 )
293
294 assert "no CLI" in prompt
295 assert "disabled computer use" in prompt
296 assert "COMPUTER_USE_REARM_REQUIRED" in prompt
297 assert "/computer-use on" in prompt
298 assert "A0 Launcher chat" in prompt
299
300
301 def test_computer_use_remote_prompt_requires_visual_verification_after_actions():
302 prompt = (PROMPT_ROOT / "agent.system.tool.computer_use_remote.md").read_text(
303 encoding="utf-8"
304 )
305 skill = (
306 PROJECT_ROOT
307 / "plugins"
308 / "_a0_connector"
309 / "skills"
310 / "host-computer-use"
311 / "SKILL.md"
312 ).read_text(encoding="utf-8")
313 linux_skill = (
314 PROJECT_ROOT
315 / "plugins"
316 / "_a0_connector"
317 / "skills"
318 / "host-computer-use-linux"
319 / "SKILL.md"
320 ).read_text(encoding="utf-8")
321
322 assert "Treat key presses, clicks, scrolling, and typing" in prompt
323 assert "attempts, not success" in prompt
324 assert "visual verification is unavailable" in prompt
325 assert "do not continue by assuming the host state" in prompt
326 assert "Super+H" not in prompt
327 assert "Alt+F9" not in prompt
328 assert "hide" not in prompt.lower()
329 assert "minimize" not in prompt.lower()
330 assert "window-manager" not in prompt
331 assert "cannot actually see the image" in skill
332 assert "A `type` tool result confirms the destination only when" in skill
333 assert "visibly confirms" in skill
334 assert "target-verified-keyboard-input" in prompt
335 assert "focus_verified=true" in prompt
336 assert "do not repeat the same action with identical arguments" in prompt
337 assert "Pass the same verified active `window_id` to `type`" in linux_skill
338 assert "never use it on an application/frame/window" in linux_skill
339 assert "hide window" not in skill
340 assert "minimize window" not in skill
341 assert "hide/minimize" not in skill
342 assert "window-manager" not in skill
343
344
345 def test_remote_file_and_exec_tool_prompt_files_remain_standard_tool_prompts():
346 text_stub = (PROMPT_ROOT / "agent.system.tool.text_editor_remote.md").read_text(encoding="utf-8")
347 exec_stub = (PROMPT_ROOT / "agent.system.tool.code_execution_remote.md").read_text(encoding="utf-8")
348
349 assert '"tool_name": "text_editor_remote"' in text_stub
350 assert '"tool_name": "code_execution_remote"' in exec_stub
351 assert "Availability and permissions are checked when the tool runs" in text_stub
352 assert "Availability and permissions are checked when the tool runs" in exec_stub
353
354
355 def test_computer_use_remote_is_standard_prompt_with_runtime_checks():
356 skill = (
357 PROJECT_ROOT
358 / "plugins"
359 / "_a0_connector"
360 / "skills"
361 / "host-computer-use"
362 / "SKILL.md"
363 )
364 standard_prompt = PROMPT_ROOT / "agent.system.tool.computer_use_remote.md"
365
366 assert not (PROMPT_ROOT / "agent.system.runtime_tool.computer_use_remote.md").exists()
367 assert standard_prompt.exists()
368 assert '"tool_name": "computer_use_remote"' in standard_prompt.read_text(encoding="utf-8")
369 assert "checked when the tool runs" in standard_prompt.read_text(encoding="utf-8")
370 assert '"tool_name": "computer_use_remote"' in skill.read_text(encoding="utf-8")
371
372
373 def test_old_connector_prompt_files_removed():
374 assert not (PROMPT_ROOT / "agent.connector_tool.text_editor_remote.md").exists()
375 assert not (PROMPT_ROOT / "agent.connector_tool.code_execution_remote.md").exists()
376 assert not (PROMPT_ROOT / "agent.connector_tool.computer_use_remote.md").exists()
377
378
379 def test_remote_tool_selection_prefers_context_cli_then_global_cli():
380 context_id = _context_id()
381 sid_context = _sid()
382 sid_global = _sid()
383 for sid in (sid_context, sid_global):
384 ws_runtime.register_sid(sid)
385 ws_runtime.store_sid_remote_exec_metadata(sid, {"enabled": True})
386 ws_runtime.store_sid_remote_file_metadata(
387 sid,
388 {"enabled": True, "write_enabled": True, "mode": "read_write"},
389 )
390 ws_runtime.subscribe_sid_to_context(sid_context, context_id)
391 try:
392 assert ws_runtime.remote_tool_sids_for_context(context_id) == [
393 sid_context,
394 sid_global,
395 ]
396 assert ws_runtime.select_remote_exec_target_sid(context_id) == sid_context
397 assert (
398 ws_runtime.select_remote_exec_target_sid(context_id, require_writes=True)
399 == sid_context
400 )
401 assert ws_runtime.select_remote_file_target_sid(context_id) == sid_context
402 finally:
403 ws_runtime.unregister_sid(sid_context)
404 ws_runtime.unregister_sid(sid_global)
405
406
407 def test_remote_tool_selection_falls_back_to_global_cli():
408 context_id = _context_id()
409 sid = _sid()
410 ws_runtime.register_sid(sid)
411 ws_runtime.store_sid_remote_exec_metadata(sid, {"enabled": True})
412 ws_runtime.store_sid_remote_file_metadata(
413 sid,
414 {"enabled": True, "write_enabled": True, "mode": "read_write"},
415 )
416 try:
417 assert ws_runtime.select_remote_exec_target_sid(context_id) == sid
418 assert (
419 ws_runtime.select_remote_exec_target_sid(context_id, require_writes=True)
420 == sid
421 )
422 assert ws_runtime.select_remote_file_target_sid(context_id) == sid
423 finally:
424 ws_runtime.unregister_sid(sid)
425
426
427 def test_latest_remote_tree_falls_back_to_global_cli_snapshot():
428 context_id = _context_id()
429 sid = _sid()
430 ws_runtime.register_sid(sid)
431 ws_runtime.store_remote_tree_snapshot(
432 sid,
433 {
434 "root_path": "/home/example",
435 "tree": "README.md",
436 "generated_at": "2026-05-09T12:00:00Z",
437 },
438 )
439 try:
440 snapshot = ws_runtime.latest_remote_tree_for_context(
441 context_id,
442 max_age_seconds=90,
443 )
444 finally:
445 ws_runtime.unregister_sid(sid)
446
447 assert snapshot is not None
448 assert snapshot["sid"] == sid
449 assert snapshot["tree"] == "README.md"
450
451
452 def test_latest_remote_tree_prefers_context_cli_snapshot():
453 context_id = _context_id()
454 sid_context = _sid()
455 sid_global = _sid()
456 now = time.time()
457 for sid in (sid_context, sid_global):
458 ws_runtime.register_sid(sid)
459 ws_runtime.subscribe_sid_to_context(sid_context, context_id)
460 ws_runtime.store_remote_tree_snapshot(
461 sid_context,
462 {
463 "root_path": "/context",
464 "tree": "context.txt",
465 "generated_at": "2026-05-09T12:00:00Z",
466 },
467 )
468 ws_runtime.store_remote_tree_snapshot(
469 sid_global,
470 {
471 "root_path": "/global",
472 "tree": "global.txt",
473 "generated_at": "2026-05-09T12:00:01Z",
474 },
475 )
476 try:
477 # Make the global snapshot newer; context affinity should still win.
478 ws_runtime._remote_tree_snapshots[sid_global] = ws_runtime.RemoteTreeSnapshot(
479 sid=sid_global,
480 payload=ws_runtime._remote_tree_snapshots[sid_global].payload,
481 updated_at=now + 5,
482 )
483 snapshot = ws_runtime.latest_remote_tree_for_context(
484 context_id,
485 max_age_seconds=90,
486 )
487 finally:
488 ws_runtime.unregister_sid(sid_context)
489 ws_runtime.unregister_sid(sid_global)
490
491 assert snapshot is not None
492 assert snapshot["sid"] == sid_context
493 assert snapshot["tree"] == "context.txt"
494
495
496 def test_remote_exec_mutating_runtime_requires_explicit_write_access():
497 context_id = _context_id()
498 sid = _sid()
499 ws_runtime.register_sid(sid)
500 ws_runtime.store_sid_remote_exec_metadata(sid, {"enabled": True})
501 try:
502 assert ws_runtime.select_remote_exec_target_sid(context_id) == sid
503 assert ws_runtime.select_remote_exec_target_sid(context_id, require_writes=True) is None
504 finally:
505 ws_runtime.unregister_sid(sid)
506
507
508 def test_remote_affordance_skills_parse():
509 legacy_connector_skill = (
510 PROJECT_ROOT
511 / "plugins"
512 / "_a0_connector"
513 / "skills"
514 / "a0-cli-remote-workflows"
515 / "SKILL.md"
516 )
517 text_editor_skill = _parse_skill_frontmatter(
518 PROJECT_ROOT
519 / "plugins"
520 / "_a0_connector"
521 / "skills"
522 / "host-file-editing"
523 / "SKILL.md"
524 )
525 code_execution_skill = _parse_skill_frontmatter(
526 PROJECT_ROOT
527 / "plugins"
528 / "_a0_connector"
529 / "skills"
530 / "host-code-execution"
531 / "SKILL.md"
532 )
533 computer_skill = _parse_skill_frontmatter(
534 PROJECT_ROOT
535 / "plugins"
536 / "_a0_connector"
537 / "skills"
538 / "host-computer-use"
539 / "SKILL.md"
540 )
541 macos_computer_skill = _parse_skill_frontmatter(
542 PROJECT_ROOT
543 / "plugins"
544 / "_a0_connector"
545 / "skills"
546 / "host-computer-use-macos"
547 / "SKILL.md"
548 )
549 windows_computer_skill = _parse_skill_frontmatter(
550 PROJECT_ROOT
551 / "plugins"
552 / "_a0_connector"
553 / "skills"
554 / "host-computer-use-windows"
555 / "SKILL.md"
556 )
557
558 assert not legacy_connector_skill.exists()
559 assert text_editor_skill["name"] == "host-file-editing"
560 assert "text_editor_remote" in text_editor_skill["description"]
561 assert "not Docker/server files" in text_editor_skill["description"]
562 assert code_execution_skill["name"] == "host-code-execution"
563 assert "code_execution_remote" in code_execution_skill["description"]
564 assert "not Docker" in code_execution_skill["description"]
565 assert computer_skill["name"] == "host-computer-use"
566 assert "computer_use_remote" in computer_skill["description"]
567 assert "Use instead of linux-desktop" in computer_skill["description"]
568 assert "host computer" in computer_skill["triggers"]
569 assert "Ubuntu Wayland desktop" in computer_skill["triggers"]
570 assert macos_computer_skill["name"] == "host-computer-use-macos"
571 assert "macOS guidance" in macos_computer_skill["description"]
572 assert windows_computer_skill["name"] == "host-computer-use-windows"
573 assert "Windows guidance" in windows_computer_skill["description"]
574
575
576 def test_remote_tool_stubs_are_self_contained_and_reference_per_tool_skills():
577 text_stub = (PROMPT_ROOT / "agent.system.tool.text_editor_remote.md").read_text(encoding="utf-8")
578 exec_stub = (PROMPT_ROOT / "agent.system.tool.code_execution_remote.md").read_text(encoding="utf-8")
579 computer_stub = (PROMPT_ROOT / "agent.system.tool.computer_use_remote.md").read_text(encoding="utf-8")
580 computer_skill = (
581 PROJECT_ROOT
582 / "plugins"
583 / "_a0_connector"
584 / "skills"
585 / "host-computer-use"
586 / "SKILL.md"
587 ).read_text(encoding="utf-8")
588 macos_computer_skill = (
589 PROJECT_ROOT
590 / "plugins"
591 / "_a0_connector"
592 / "skills"
593 / "host-computer-use-macos"
594 / "SKILL.md"
595 ).read_text(encoding="utf-8")
596 windows_computer_skill = (
597 PROJECT_ROOT
598 / "plugins"
599 / "_a0_connector"
600 / "skills"
601 / "host-computer-use-windows"
602 / "SKILL.md"
603 ).read_text(encoding="utf-8")
604
605 assert "optionally load skill `host-file-editing`" in text_stub
606 assert "optionally load skill `host-code-execution`" in exec_stub
607 assert '"tool_name": "text_editor_remote"' in text_stub
608 assert '"tool_name": "code_execution_remote"' in exec_stub
609 assert '"tool_name": "computer_use_remote"' in computer_stub
610 assert "load and follow skill `host-computer-use`" in computer_stub
611 assert "host-computer-use-macos" in computer_stub
612 assert "host-computer-use-windows" in computer_stub
613 assert "ax_snapshot" not in computer_stub
614 assert "ax_action" not in computer_stub
615 assert "uia_snapshot" not in computer_stub
616 assert "uia_action" not in computer_stub
617 assert "Do not substitute the `linux-desktop` skill" in computer_stub
618 assert '"tool_name": "computer_use_remote"' in computer_skill
619 assert '"tool_name": "computer_use_remote"' in macos_computer_skill
620 assert '"tool_name": "computer_use_remote"' in windows_computer_skill
621 assert "ax_snapshot" in macos_computer_skill
622 assert "ax_snapshot" not in computer_skill
623 assert "ax_action" not in computer_skill
624 assert "uia_snapshot" in windows_computer_skill
625 assert "uia_action" in windows_computer_skill
626 assert "focus_window" in windows_computer_skill
627 assert "minimize" in windows_computer_skill
628 assert "If a node offers `invoke`, use `invoke`, not `click`" in windows_computer_skill
629 assert "uia_snapshot" not in computer_skill
630 assert "uia_action" not in computer_skill
631 assert "Availability, backend support, and trust mode are checked when the tool runs" in computer_stub
632 assert "not `code_execution_tool`" in exec_stub
633 assert "not to" in exec_stub
634 assert "Docker/server/container execution" in exec_stub
635 assert "a0-cli-remote-workflows" not in text_stub
636 assert "a0-cli-remote-workflows" not in exec_stub
637 assert "a0-cli-remote-workflows" not in computer_stub
638 assert "a0-cli-remote-workflows" not in computer_skill
639
640
641 def test_host_browser_requests_route_to_browser_tool_not_desktop_or_shell_fallbacks():
642 browser_prompt = (
643 PROJECT_ROOT / "plugins" / "_browser" / "prompts" / "agent.system.tool.browser.md"
644 ).read_text(encoding="utf-8")
645 exec_stub = (PROMPT_ROOT / "agent.system.tool.code_execution_remote.md").read_text(encoding="utf-8")
646 exec_skill = (
647 PROJECT_ROOT
648 / "plugins"
649 / "_a0_connector"
650 / "skills"
651 / "host-code-execution"
652 / "SKILL.md"
653 ).read_text(encoding="utf-8")
654 computer_skill = (
655 PROJECT_ROOT
656 / "plugins"
657 / "_a0_connector"
658 / "skills"
659 / "host-computer-use"
660 / "SKILL.md"
661 ).read_text(encoding="utf-8")
662
663 assert 'When the user asks for "my browser"' in browser_prompt
664 assert "Do not substitute `computer_use_remote`" in browser_prompt
665 assert "code_execution_remote" in browser_prompt
666 assert "Python `webbrowser.open`" in browser_prompt
667 assert "chrome://inspect/#remote-debugging" in browser_prompt
668 assert "opera://inspect/#remote-debugging" in browser_prompt
669 assert "Do not start `computer_use_remote` for web-page navigation" in computer_skill
670 assert (
671 "Do not fall back to `code_execution_remote`, `xdg-open`, `sensible-browser`, "
672 "or Python `webbrowser.open`"
673 ) in computer_skill
674 assert "do not use shell launchers" in exec_skill
675 assert "Use a shell launcher only when the user explicitly wants" not in exec_skill
676 assert "Do not use this tool as a fallback for host-browser navigation/control" in exec_stub
677
678
679 def test_host_computer_use_does_not_fall_back_to_linux_desktop_skill():
680 computer_stub = (PROMPT_ROOT / "agent.system.tool.computer_use_remote.md").read_text(encoding="utf-8")
681 host_skill_path = (
682 PROJECT_ROOT
683 / "plugins"
684 / "_a0_connector"
685 / "skills"
686 / "host-computer-use"
687 / "SKILL.md"
688 )
689 linux_skill_path = (
690 PROJECT_ROOT
691 / "plugins"
692 / "_desktop"
693 / "skills"
694 / "linux-desktop"
695 / "SKILL.md"
696 )
697 host_skill = host_skill_path.read_text(encoding="utf-8")
698 linux_skill = linux_skill_path.read_text(encoding="utf-8")
699 linux_frontmatter = _parse_skill_frontmatter(linux_skill_path)
700
701 assert "only desktop-control path for the user's connected host/local computer" in computer_stub
702 assert "Do not substitute the `linux-desktop` skill" in computer_stub
703 assert "Never switch to `linux-desktop`" in host_skill
704 assert "Those paths only see the internal Agent Zero runtime" in host_skill
705 assert "built-in Docker/Xpra Linux Desktop" in linux_frontmatter["description"]
706 assert "Not for A0 CLI /computer-use" in linux_frontmatter["description"]
707 assert "A0 CLI /computer-use" in linux_frontmatter["description"]
708 assert "host-computer-use" in linux_skill
709 assert "computer_use_remote" in linux_skill
710 assert "`desktopctl.sh` only targets the internal Agent Zero Xpra display" in linux_skill
711 assert "use the OS" not in linux_frontmatter["triggers"]
712 assert "terminal app" not in linux_frontmatter["triggers"]
713 assert any("Xpra" in trigger for trigger in linux_frontmatter["triggers"])