connector: gate remote tool guidance on active permissions

Move the heavy remote-tool operating guidance out of the always-on tool prompts and inject it only when the current context can actually use those tools. - add extras prompts for computer_use_remote, code_execution_remote, and text_editor_remote - trim the base tool prompts down to the stable contract and minimal notes - inject detailed guidance from message-loop extensions instead of always paying the token cost - store remote_files and remote_exec hello metadata alongside computer_use metadata - make code_execution_remote follow the real F4 exec-enabled state - make text_editor_remote follow the real F3 read-only vs read-write state - surface read-only mode in the injected text-editor guidance and suppress write guidance there - keep legacy fallback behavior for older CLIs that do not yet advertise the new hello metadata

Alessandro committed Apr 19, 2026 at 22:06 UTC a5d733c85f8b5719ae80579a1dbeb33cdba97c4b
13 files changed +463 -146
plugins/_a0_connector/api/ws_connector.py
+16
@@ -13,6 +13,8 @@ from plugins._a0_connector.helpers.event_bridge import get_context_log_entries
13 from plugins._a0_connector.helpers.ws_runtime import (
14 clear_remote_tree_snapshot,
15 clear_sid_computer_use_metadata,
16 + clear_sid_remote_exec_metadata,
17 + clear_sid_remote_file_metadata,
18 fail_pending_computer_use_ops_for_sid,
19 fail_pending_file_ops_for_sid,
20 fail_pending_exec_ops_for_sid,
@@ -22,6 +24,8 @@ from plugins._a0_connector.helpers.ws_runtime import (
24 resolve_pending_file_op,
25 store_remote_tree_snapshot,
26 store_sid_computer_use_metadata,
27 + store_sid_remote_exec_metadata,
28 + store_sid_remote_file_metadata,
29 subscribe_sid_to_context,
30 subscribed_contexts_for_sid,
31 subscribed_sids_for_context,
@@ -81,6 +85,8 @@ class WsConnector(WsHandler):
85 error="CLI disconnected before completing the requested computer-use operation",
86 )
87 clear_sid_computer_use_metadata(sid)
88 + clear_sid_remote_file_metadata(sid)
89 + clear_sid_remote_exec_metadata(sid)
90 PrintStyle.debug(f"[a0-connector] /ws disconnected: {sid}")
91
92 async def process(
@@ -91,10 +97,20 @@ class WsConnector(WsHandler):
97 ) -> dict[str, Any] | WsResult | None:
98 if event == "connector_hello":
99 computer_use = data.get("computer_use")
100 + remote_files = data.get("remote_files")
101 + remote_exec = data.get("remote_exec")
102 if isinstance(computer_use, dict):
103 store_sid_computer_use_metadata(sid, computer_use)
104 else:
105 clear_sid_computer_use_metadata(sid)
106 + if isinstance(remote_files, dict):
107 + store_sid_remote_file_metadata(sid, remote_files)
108 + else:
109 + clear_sid_remote_file_metadata(sid)
110 + if isinstance(remote_exec, dict):
111 + store_sid_remote_exec_metadata(sid, remote_exec)
112 + else:
113 + clear_sid_remote_exec_metadata(sid)
114 return {
115 "protocol": PROTOCOL_VERSION,
116 "features": WS_FEATURES,
plugins/_a0_connector/extensions/python/message_loop_prompts_after/_77_include_computer_use_remote.py new
+50
@@ -0,0 +1,50 @@
1 +from __future__ import annotations
2 +
3 +from agent import LoopData
4 +from helpers.extension import Extension
5 +
6 +from plugins._a0_connector.helpers.ws_runtime import (
7 + computer_use_metadata_for_sid,
8 + select_computer_use_target_sid,
9 +)
10 +
11 +
12 +class IncludeComputerUseRemote(Extension):
13 + async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
14 + if not self.agent:
15 + return
16 +
17 + context_id = getattr(self.agent.context, "id", "")
18 + if not context_id:
19 + return
20 +
21 + sid = select_computer_use_target_sid(context_id)
22 + if not sid:
23 + return
24 +
25 + metadata = computer_use_metadata_for_sid(sid)
26 + if not metadata or not metadata.get("supported") or not metadata.get("enabled"):
27 + return
28 +
29 + backend_id = str(metadata.get("backend_id") or "").strip() or "unknown"
30 + backend_family = str(metadata.get("backend_family") or "").strip()
31 + backend = backend_id if not backend_family else f"{backend_id}/{backend_family}"
32 + trust_mode = str(metadata.get("trust_mode") or "").strip() or "unknown"
33 + support_reason = str(metadata.get("support_reason") or "").strip() or "No support details available."
34 +
35 + features_value = metadata.get("features")
36 + if isinstance(features_value, (list, tuple)):
37 + features = ", ".join(str(item).strip() for item in features_value if str(item).strip())
38 + else:
39 + features = ""
40 + if not features:
41 + features = "none advertised"
42 +
43 + prompt = self.agent.read_prompt(
44 + "agent.extras.computer_use_remote.md",
45 + backend=backend,
46 + trust_mode=trust_mode,
47 + features=features,
48 + support_reason=support_reason,
49 + )
50 + loop_data.extras_temporary["computer_use_remote"] = prompt
plugins/_a0_connector/extensions/python/message_loop_prompts_after/_78_include_code_execution_remote.py new
+48
@@ -0,0 +1,48 @@
1 +from __future__ import annotations
2 +
3 +from agent import LoopData
4 +from helpers.extension import Extension
5 +
6 +from plugins._a0_connector.helpers.exec_config import build_exec_config
7 +from plugins._a0_connector.helpers.ws_runtime import select_remote_exec_target_sid
8 +
9 +
10 +def _format_timeouts(payload: dict[str, int]) -> str:
11 + return ", ".join(f"{key}={value}" for key, value in payload.items()) or "none"
12 +
13 +
14 +def _format_patterns(value: object) -> str:
15 + if isinstance(value, (list, tuple)):
16 + items = [str(item).strip() for item in value if str(item).strip()]
17 + else:
18 + items = []
19 + return ", ".join(items) or "none"
20 +
21 +
22 +class IncludeCodeExecutionRemote(Extension):
23 + async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
24 + if not self.agent:
25 + return
26 +
27 + context_id = getattr(self.agent.context, "id", "")
28 + if not context_id or not select_remote_exec_target_sid(context_id):
29 + return
30 +
31 + exec_config = build_exec_config(agent=self.agent)
32 + code_exec_timeouts = exec_config.get("code_exec_timeouts")
33 + output_timeouts = exec_config.get("output_timeouts")
34 + prompt_patterns = exec_config.get("prompt_patterns")
35 + dialog_patterns = exec_config.get("dialog_patterns")
36 +
37 + prompt = self.agent.read_prompt(
38 + "agent.extras.code_execution_remote.md",
39 + code_exec_timeouts=_format_timeouts(
40 + code_exec_timeouts if isinstance(code_exec_timeouts, dict) else {}
41 + ),
42 + output_timeouts=_format_timeouts(
43 + output_timeouts if isinstance(output_timeouts, dict) else {}
44 + ),
45 + prompt_patterns=_format_patterns(prompt_patterns),
46 + dialog_patterns=_format_patterns(dialog_patterns),
47 + )
48 + loop_data.extras_temporary["code_execution_remote"] = prompt
plugins/_a0_connector/extensions/python/message_loop_prompts_after/_79_include_text_editor_remote.py new
+98
@@ -0,0 +1,98 @@
1 +from __future__ import annotations
2 +
3 +from agent import LoopData
4 +from helpers.extension import Extension
5 +
6 +from plugins._a0_connector.helpers.ws_runtime import (
7 + remote_file_metadata_for_sid,
8 + select_remote_file_target_sid,
9 +)
10 +
11 +
12 +class IncludeTextEditorRemote(Extension):
13 + async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
14 + if not self.agent:
15 + return
16 +
17 + context_id = getattr(self.agent.context, "id", "")
18 + if not context_id:
19 + return
20 +
21 + sid = select_remote_file_target_sid(context_id)
22 + if not sid:
23 + return
24 +
25 + metadata = remote_file_metadata_for_sid(sid)
26 + if metadata is None:
27 + access_mode = "Read&Write (legacy/unknown)"
28 + write_guidance = (
29 + "- Writes and patches are expected to be available, but this CLI did not "
30 + "advertise an explicit F3 access mode."
31 + )
32 + write_examples = """```json
33 +{
34 + "tool_name": "text_editor_remote",
35 + "tool_args": {
36 + "op": "write",
37 + "path": "/path/on/remote/machine/file.py",
38 + "content": "import os\\nprint('hello')\\n"
39 + }
40 +}
41 +```
42 +
43 +```json
44 +{
45 + "tool_name": "text_editor_remote",
46 + "tool_args": {
47 + "op": "patch",
48 + "path": "/path/on/remote/machine/file.py",
49 + "edits": [
50 + {"from": 5, "to": 5, "content": " if x == 2:\\n"}
51 + ]
52 + }
53 +}
54 +```"""
55 + elif metadata.get("write_enabled"):
56 + access_mode = "Read&Write"
57 + write_guidance = (
58 + "- Use `write` only when replacing or creating the full file is the right operation.\n"
59 + "- Use `patch` for surgical line-range edits. Keep the edit set tight and based on the latest remote read.\n"
60 + "- Freshness-aware patching may reject stale edits. If a patch requires a reread, read the file again and then retry with updated ranges."
61 + )
62 + write_examples = """```json
63 +{
64 + "tool_name": "text_editor_remote",
65 + "tool_args": {
66 + "op": "write",
67 + "path": "/path/on/remote/machine/file.py",
68 + "content": "import os\\nprint('hello')\\n"
69 + }
70 +}
71 +```
72 +
73 +```json
74 +{
75 + "tool_name": "text_editor_remote",
76 + "tool_args": {
77 + "op": "patch",
78 + "path": "/path/on/remote/machine/file.py",
79 + "edits": [
80 + {"from": 5, "to": 5, "content": " if x == 2:\\n"}
81 + ]
82 + }
83 +}
84 +```"""
85 + else:
86 + access_mode = "Read only"
87 + write_guidance = (
88 + "- Writes and patches are disabled in this CLI session. Press F3 to switch the host machine to Read&Write before attempting `write` or `patch`."
89 + )
90 + write_examples = ""
91 +
92 + prompt = self.agent.read_prompt(
93 + "agent.extras.text_editor_remote.md",
94 + access_mode=access_mode,
95 + write_guidance=write_guidance,
96 + write_examples=write_examples,
97 + )
98 + loop_data.extras_temporary["text_editor_remote"] = prompt
plugins/_a0_connector/helpers/ws_runtime.py
+111
@@ -51,6 +51,20 @@ class ComputerUseMetadata:
51 updated_at: float
52
53
54 +@dataclass(frozen=True)
55 +class RemoteFileMetadata:
56 + enabled: bool
57 + write_enabled: bool
58 + mode: str
59 + updated_at: float
60 +
61 +
62 +@dataclass(frozen=True)
63 +class RemoteExecMetadata:
64 + enabled: bool
65 + updated_at: float
66 +
67 +
68 _context_subscriptions: dict[str, set[str]] = {}
69 _sid_contexts: dict[str, set[str]] = {}
70 _pending_file_ops: dict[str, PendingFileOperation] = {}
@@ -58,6 +72,8 @@ _pending_exec_ops: dict[str, PendingExecOperation] = {}
72 _pending_computer_use_ops: dict[str, PendingComputerUseOperation] = {}
73 _remote_tree_snapshots: dict[str, RemoteTreeSnapshot] = {}
74 _sid_computer_use_metadata: dict[str, ComputerUseMetadata] = {}
75 +_sid_remote_file_metadata: dict[str, RemoteFileMetadata] = {}
76 +_sid_remote_exec_metadata: dict[str, RemoteExecMetadata] = {}
77 _state_lock = threading.RLock()
78
79
@@ -71,6 +87,8 @@ def unregister_sid(sid: str) -> set[str]:
87 contexts = _sid_contexts.pop(sid, set())
88 _remote_tree_snapshots.pop(sid, None)
89 _sid_computer_use_metadata.pop(sid, None)
90 + _sid_remote_file_metadata.pop(sid, None)
91 + _sid_remote_exec_metadata.pop(sid, None)
92 for context_id in contexts:
93 subscribers = _context_subscriptions.get(context_id)
94 if not subscribers:
@@ -167,6 +185,99 @@ def select_target_sid(context_id: str) -> str | None:
185 return sorted(subscribers)[0]
186
187
188 +def store_sid_remote_file_metadata(sid: str, payload: dict[str, Any]) -> RemoteFileMetadata:
189 + write_enabled = bool(payload.get("write_enabled"))
190 + mode = str(payload.get("mode", "") or "").strip().lower()
191 + if mode not in {"read_only", "read_write"}:
192 + mode = "read_write" if write_enabled else "read_only"
193 + metadata = RemoteFileMetadata(
194 + enabled=bool(payload.get("enabled", True)),
195 + write_enabled=write_enabled,
196 + mode=mode,
197 + updated_at=time.time(),
198 + )
199 + with _state_lock:
200 + _sid_remote_file_metadata[sid] = metadata
201 + return metadata
202 +
203 +
204 +def clear_sid_remote_file_metadata(sid: str) -> None:
205 + with _state_lock:
206 + _sid_remote_file_metadata.pop(sid, None)
207 +
208 +
209 +def remote_file_metadata_for_sid(sid: str) -> dict[str, Any] | None:
210 + with _state_lock:
211 + metadata = _sid_remote_file_metadata.get(sid)
212 + if metadata is None:
213 + return None
214 + return {
215 + "enabled": metadata.enabled,
216 + "write_enabled": metadata.write_enabled,
217 + "mode": metadata.mode,
218 + "updated_at": metadata.updated_at,
219 + }
220 +
221 +
222 +def select_remote_file_target_sid(context_id: str, *, require_writes: bool = False) -> str | None:
223 + with _state_lock:
224 + subscribers = sorted(_context_subscriptions.get(context_id, set()))
225 + fallback_sid: str | None = None
226 + for sid in subscribers:
227 + metadata = _sid_remote_file_metadata.get(sid)
228 + if metadata is None:
229 + if fallback_sid is None:
230 + fallback_sid = sid
231 + continue
232 + if not metadata.enabled:
233 + continue
234 + if require_writes and not metadata.write_enabled:
235 + continue
236 + return sid
237 + return fallback_sid
238 +
239 +
240 +def store_sid_remote_exec_metadata(sid: str, payload: dict[str, Any]) -> RemoteExecMetadata:
241 + metadata = RemoteExecMetadata(
242 + enabled=bool(payload.get("enabled")),
243 + updated_at=time.time(),
244 + )
245 + with _state_lock:
246 + _sid_remote_exec_metadata[sid] = metadata
247 + return metadata
248 +
249 +
250 +def clear_sid_remote_exec_metadata(sid: str) -> None:
251 + with _state_lock:
252 + _sid_remote_exec_metadata.pop(sid, None)
253 +
254 +
255 +def remote_exec_metadata_for_sid(sid: str) -> dict[str, Any] | None:
256 + with _state_lock:
257 + metadata = _sid_remote_exec_metadata.get(sid)
258 + if metadata is None:
259 + return None
260 + return {
261 + "enabled": metadata.enabled,
262 + "updated_at": metadata.updated_at,
263 + }
264 +
265 +
266 +def select_remote_exec_target_sid(context_id: str) -> str | None:
267 + with _state_lock:
268 + subscribers = sorted(_context_subscriptions.get(context_id, set()))
269 + fallback_sid: str | None = None
270 + for sid in subscribers:
271 + metadata = _sid_remote_exec_metadata.get(sid)
272 + if metadata is None:
273 + if fallback_sid is None:
274 + fallback_sid = sid
275 + continue
276 + if metadata.enabled:
277 + return sid
278 + return fallback_sid
279 +
280 +
281 def store_sid_computer_use_metadata(sid: str, payload: dict[str, Any]) -> ComputerUseMetadata:
282 features_value = payload.get("features")
283 if isinstance(features_value, (list, tuple)):
plugins/_a0_connector/prompts/agent.extras.code_execution_remote.md new
+52
@@ -0,0 +1,52 @@
1 +## code_execution_remote guidance
2 +
3 +Remote code execution is currently available in this context through the connected CLI.
4 +
5 +Execution config:
6 +- code execution timeouts: `{{code_exec_timeouts}}`
7 +- output polling timeouts: `{{output_timeouts}}`
8 +- prompt patterns: `{{prompt_patterns}}`
9 +- dialog patterns: `{{dialog_patterns}}`
10 +
11 +- Use this tool for shell-backed execution on the remote CLI machine, not on the Agent Zero server.
12 +- Session ids are frontend-local and persistent across calls. Reuse the same `session` when continuing a workflow.
13 +- Use `runtime=terminal` for shell commands, `runtime=python` for Python snippets, and `runtime=nodejs` for Node.js snippets.
14 +- Use `runtime=output` to poll a running session after a prior call returned before the shell settled.
15 +- Use `runtime=reset` when a session is stuck or you need a clean shell.
16 +- `runtime=input` is only a deprecated compatibility alias for sending one line of keyboard input into a running shell session.
17 +- Frontend execution may still be locally disabled in the CLI session. If so, expect a structured `{ok: false}` error instead of a fallback runtime.
18 +- Prefer concise, self-checking commands. For multi-step work, inspect output and continue in the same session instead of restarting from scratch.
19 +
20 +Examples:
21 +
22 +```json
23 +{
24 + "tool_name": "code_execution_remote",
25 + "tool_args": {
26 + "runtime": "terminal",
27 + "session": 0,
28 + "code": "pwd && ls -la"
29 + }
30 +}
31 +```
32 +
33 +```json
34 +{
35 + "tool_name": "code_execution_remote",
36 + "tool_args": {
37 + "runtime": "python",
38 + "session": 0,
39 + "code": "import os\nprint(os.getcwd())"
40 + }
41 +}
42 +```
43 +
44 +```json
45 +{
46 + "tool_name": "code_execution_remote",
47 + "tool_args": {
48 + "runtime": "output",
49 + "session": 0
50 + }
51 +}
52 +```
plugins/_a0_connector/prompts/agent.extras.computer_use_remote.md new
+21
@@ -0,0 +1,21 @@
1 +## computer_use_remote guidance
2 +
3 +Computer use is currently available in this context.
4 +Backend: `{{backend}}`
5 +Trust mode: `{{trust_mode}}`
6 +Features: `{{features}}`
7 +Support note: `{{support_reason}}`
8 +
9 +- Use this for local desktop and native UI tasks on the connected machine.
10 +- If the task is browser-only and the user is flexible, prefer `browser_agent` because it is usually more reliable and token-efficient than screenshot-driven desktop control.
11 +- Use `start_session` before interactive desktop actions. `status` is for inspection; `stop_session` ends the session.
12 +- Base every decision on the latest screenshot or a definitive tool result, not memory.
13 +- Successful `start_session`, `move`, `click`, `scroll`, `key`, and `type` calls already attach a fresh screenshot.
14 +- Use `capture` only when you need a screen refresh without taking another action.
15 +- Prefer keyboard actions over pointer actions when there is a reliable keyboard path.
16 +- Treat menus and popups as transient UI. If a click dismisses one without visible progress, treat that attempt as failed and switch approach.
17 +- If the same approach has already failed twice without visible progress, stop repeating it and try a different strategy.
18 +- For browser work done through this tool, only claim success when the page content area visibly shows the expected destination or result.
19 +- Use `type(..., submit=true)` only for navigation-style entry such as an address bar or command box. For ordinary text fields, type first and send `enter` separately only if needed.
20 +- In `free_run`, do not expect a fresh approval prompt. If silent restore is no longer valid, expect `COMPUTER_USE_REARM_REQUIRED`.
21 +- Treat user interventions as high-priority control signals. If the user says `stop`, `pause`, `abort`, `hold`, `don't continue`, or equivalent, stop using computer-use tools until the user explicitly resumes.
plugins/_a0_connector/prompts/agent.extras.text_editor_remote.md new
+25
@@ -0,0 +1,25 @@
1 +## text_editor_remote guidance
2 +
3 +Remote file editing is currently available in this context through the connected CLI.
4 +Current access mode: `{{access_mode}}`
5 +
6 +- Use `text_editor_remote` when the user asks you to edit files on their local machine while connected via the CLI.
7 +- Paths are evaluated on the remote CLI machine's filesystem, not on the Agent Zero server.
8 +- Prefer `read` before `patch` so you have current line numbers and freshness metadata.
9 +- `read` is always the safest first step for inspecting the local file.
10 +{{write_guidance}}
11 +
12 +Examples:
13 +
14 +```json
15 +{
16 + "tool_name": "text_editor_remote",
17 + "tool_args": {
18 + "op": "read",
19 + "path": "/path/on/remote/machine/file.py",
20 + "line_from": 1,
21 + "line_to": 50
22 + }
23 +}
24 +```
25 +{{write_examples}}
plugins/_a0_connector/prompts/agent.system.tool.code_execution_remote.md
+2 -76
@@ -1,9 +1,8 @@
1 # code_execution_remote tool
2
3 This tool runs shell-backed execution on the **remote machine where the CLI is running**.
4 -It converges onto Agent Zero Core's persistent local-shell model, so the frontend session
5 -can execute terminal commands and shell-launched `python` / `nodejs` snippets while keeping
6 -session ids stable across calls.
4 +Detailed usage guidance is injected separately only when the current context has a
5 +subscribed CLI, so the base system prompt stays small when remote execution is not in play.
6
7 ## Requirements
8 - A CLI client must be connected to this context via the shared `/ws` namespace.
@@ -22,79 +21,6 @@ Runtime-specific fields:
21 - `input`: requires `keyboard` (or `code` as fallback)
22 - `reset`: optional `reason`
23
25 -## Usage
26 -
27 -### Execute a terminal command
28 -```json
29 -{
30 - "tool_name": "code_execution_remote",
31 - "tool_args": {
32 - "runtime": "terminal",
33 - "session": 0,
34 - "code": "pwd && ls -la"
35 - }
36 -}
37 -```
38 -
39 -### Execute Python through the shell-backed runtime
40 -```json
41 -{
42 - "tool_name": "code_execution_remote",
43 - "tool_args": {
44 - "runtime": "python",
45 - "session": 0,
46 - "code": "import os\nprint(os.getcwd())"
47 - }
48 -}
49 -```
50 -
51 -### Execute Node.js through the shell-backed runtime
52 -```json
53 -{
54 - "tool_name": "code_execution_remote",
55 - "tool_args": {
56 - "runtime": "nodejs",
57 - "session": 0,
58 - "code": "console.log(process.cwd())"
59 - }
60 -}
61 -```
62 -
63 -### Poll output from a running session
64 -```json
65 -{
66 - "tool_name": "code_execution_remote",
67 - "tool_args": {
68 - "runtime": "output",
69 - "session": 0
70 - }
71 -}
72 -```
73 -
74 -### Send keyboard input to a running session
75 -```json
76 -{
77 - "tool_name": "code_execution_remote",
78 - "tool_args": {
79 - "runtime": "input",
80 - "session": 0,
81 - "keyboard": "yes"
82 - }
83 -}
84 -```
85 -
86 -### Reset a session
87 -```json
88 -{
89 - "tool_name": "code_execution_remote",
90 - "tool_args": {
91 - "runtime": "reset",
92 - "session": 0,
93 - "reason": "stuck process"
94 - }
95 -}
96 -```
97 -
24 ## Notes
25 - Session state is frontend-local and shell-backed.
26 - `output` is for long-running operations where a prior call returned control before the
plugins/_a0_connector/prompts/agent.system.tool.computer_use_remote.md
+6 -15
@@ -2,25 +2,12 @@
2
3 Use the connected CLI host machine as a local desktop target.
4
5 -## Preferred Scope
6 -- Use this for local desktop and native UI tasks on the connected machine.
7 -- For ordinary website browsing, search, form filling, and web downloads, prefer `browser_agent`.
8 -- If the user is flexible and the task is browser-only, briefly guide them toward browser tools because they are usually more reliable and token-efficient than screenshot-driven computer use.
9 -- Before doing real computer-use work, load the `computer-use-remote` skill and follow it.
5 +This tool is only usable when the current context has a subscribed CLI with enabled local computer use.
6 +Detailed operating guidance is injected separately only when that condition is true, so the base system prompt stays small when computer use is not in play.
7
8 ## Requirements
9 - A CLI client must be connected to this context via the shared `/ws` namespace.
10 - The CLI must advertise `computer_use_remote` support and local computer use must be enabled there.
14 -- In `free_run`, do not expect a fresh approval prompt. If restore is no longer valid, the tool will surface `COMPUTER_USE_REARM_REQUIRED`.
15 -
16 -## Minimal Rules
17 -- Treat user interventions as high-priority control signals.
18 -- If the user says `stop`, `pause`, `abort`, `hold`, `don't continue`, or equivalent, halt immediately and do not use computer-use tools again until the user explicitly resumes.
19 -- Call `start_session` first. It automatically attaches the current screen.
20 -- Decide from the latest screenshot, not from memory.
21 -- Interactive actions (`move`, `click`, `scroll`, `key`, `type`) automatically attach a fresh screenshot after they run.
22 -- Use `capture` only when you need another screen refresh without taking an action.
23 -- Prefer keyboard actions over pointer actions whenever a reliable keyboard path exists.
11
12 ## Arguments
13 - `action`: one of `start_session`, `status`, `capture`, `move`, `click`, `scroll`, `key`, `type`, `stop_session`
@@ -32,3 +19,7 @@ Action-specific fields:
19 - `scroll`: `dx`, `dy`
20 - `key`: `key` or `keys`
21 - `type`: `text`, optional `submit` boolean
22 +
23 +## Runtime Notes
24 +- Successful `start_session`, `move`, `click`, `scroll`, `key`, and `type` calls automatically attach a fresh screenshot.
25 +- `status` reports the current computer-use state without starting a session.
plugins/_a0_connector/prompts/agent.system.tool.text_editor_remote.md
+5 -46
@@ -2,59 +2,18 @@
2
3 This tool allows you to read, write, and patch files on the **remote machine where the CLI is running**.
4 This is different from `text_editor` which operates on the Agent Zero server's filesystem.
5 -
6 -Use `text_editor_remote` when the user asks you to edit files on their local machine while connected via the CLI.
5 +Detailed usage guidance is injected separately only when the current context has a
6 +subscribed CLI, so the base system prompt stays small when remote editing is not in play.
7
8 ## Requirements
9 - A CLI client must be connected to this context via the shared `/ws` namespace.
10 - The CLI client must have enabled remote file editing support.
11
12 ## Operations
13 -
14 -### Read a file
15 -```json
16 -{
17 - "tool_name": "text_editor_remote",
18 - "tool_args": {
19 - "op": "read",
20 - "path": "/path/on/remote/machine/file.py",
21 - "line_from": 1,
22 - "line_to": 50
23 - }
24 -}
25 -```
26 -Returns file content with line numbers. `line_from` and `line_to` are optional.
27 -
28 -### Write a file
29 -```json
30 -{
31 - "tool_name": "text_editor_remote",
32 - "tool_args": {
33 - "op": "write",
34 - "path": "/path/on/remote/machine/file.py",
35 - "content": "import os\nprint('hello')\n"
36 - }
37 -}
38 -```
39 -Creates or overwrites the file on the remote machine.
40 -
41 -### Patch a file
42 -```json
43 -{
44 - "tool_name": "text_editor_remote",
45 - "tool_args": {
46 - "op": "patch",
47 - "path": "/path/on/remote/machine/file.py",
48 - "edits": [
49 - {"from": 5, "to": 5, "content": " if x == 2:\n"}
50 - ]
51 - }
52 -}
53 -```
54 -Applies line-range patches to the file. Use the same format as the standard `text_editor:patch` tool.
13 +- `read`: optional `line_from`, `line_to`
14 +- `write`: requires `content`
15 +- `patch`: requires `edits`
16
17 ## Notes
57 -- Always read the file first before patching to get current line numbers.
18 - Paths are evaluated on the **remote machine's filesystem**, not the Agent Zero server.
59 -- If no CLI is connected, the tool will return an error message.
19 - The transport uses `connector_file_op` and `connector_file_op_result` with a shared `op_id`.
plugins/_a0_connector/tools/code_execution_remote.py
+8 -3
@@ -11,8 +11,9 @@ from helpers.ws_manager import ConnectionNotFoundError, get_shared_ws_manager
11
12 from plugins._a0_connector.helpers.ws_runtime import (
13 clear_pending_exec_op,
14 - select_target_sid,
14 + select_remote_exec_target_sid,
15 store_pending_exec_op,
16 + subscribed_sids_for_context,
17 )
18
19
@@ -60,11 +61,15 @@ class CodeExecutionRemote(Tool):
61 )
62
63 context_id = self.agent.context.id
63 - sid = select_target_sid(context_id)
64 + subscribers = subscribed_sids_for_context(context_id)
65 + sid = select_remote_exec_target_sid(context_id)
66 if not sid:
67 return Response(
68 message=(
67 - "code_execution_remote: no CLI client connected to this context. "
69 + "code_execution_remote: no subscribed CLI in this context currently has "
70 + "remote execution enabled. Connect the CLI and press F4 to switch exec on."
71 + if subscribers
72 + else "code_execution_remote: no CLI client connected to this context. "
73 "Make sure the CLI is connected and subscribed."
74 ),
75 break_loop=False,
plugins/_a0_connector/tools/text_editor_remote.py
+21 -6
@@ -18,8 +18,9 @@ from plugins._a0_connector.helpers.text_editor_freshness import (
18 )
19 from plugins._a0_connector.helpers.ws_runtime import (
20 clear_pending_file_op,
21 - select_target_sid,
21 + select_remote_file_target_sid,
22 store_pending_file_op,
23 + subscribed_sids_for_context,
24 )
25
26
@@ -125,14 +126,28 @@ class TextEditorRemote(Tool):
126 **payload_extra: Any,
127 ) -> dict[str, Any]:
128 context_id = self.agent.context.id
128 - sid = select_target_sid(context_id)
129 + require_writes = op in {"write", "patch"}
130 + subscribers = subscribed_sids_for_context(context_id)
131 + sid = select_remote_file_target_sid(context_id, require_writes=require_writes)
132 if not sid:
130 - return {
131 - "ok": False,
132 - "error": (
133 + if not subscribers:
134 + error = (
135 "text_editor_remote: no CLI client connected to this context. "
136 "Make sure the CLI is connected and subscribed."
135 - ),
137 + )
138 + elif require_writes:
139 + error = (
140 + "text_editor_remote: no subscribed CLI in this context currently allows "
141 + "remote file writes. Press F3 to switch the CLI to Read&Write."
142 + )
143 + else:
144 + error = (
145 + "text_editor_remote: no subscribed CLI in this context currently advertises "
146 + "remote file access."
147 + )
148 + return {
149 + "ok": False,
150 + "error": error,
151 }
152
153 op_id = str(uuid.uuid4())