connector: block shell write actions inread-only mode
When code execution remote was enabled but CLI was in read-only mode, the shell could still write files to disk.
Alessandro committed
Apr 20, 2026 at 03:07 UTC
d28c21e1a070a6662f996b0845f26e533718336b
5 files changed
+132
-19
plugins/_a0_connector/extensions/python/message_loop_prompts_after/_78_include_code_execution_remote.py
+81
-2
@@ -4,7 +4,10 @@ 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
7
+from plugins._a0_connector.helpers.ws_runtime import (
8
+ remote_file_metadata_for_sid,
9
+ select_remote_exec_target_sid,
10
+)
11
12
13
def _format_timeouts(payload: dict[str, int]) -> str:
@@ -25,9 +28,82 @@ class IncludeCodeExecutionRemote(Extension):
28
return
29
30
context_id = getattr(self.agent.context, "id", "")
28
- if not context_id or not select_remote_exec_target_sid(context_id):
31
+ if not context_id:
32
return
33
34
+ sid = select_remote_exec_target_sid(context_id, require_writes=False)
35
+ if not sid:
36
+ return
37
+
38
+ metadata = remote_file_metadata_for_sid(sid)
39
+ if metadata is None:
40
+ access_mode = "Read&Write (legacy/unknown)"
41
+ write_runtime_guidance = (
42
+ "- `runtime=terminal`, `python`, `nodejs`, and `input` are expected to be "
43
+ "available, but this CLI did not advertise an explicit F3 access mode.\n"
44
+ "- Use shell syntax that matches the remote host (for example, PowerShell on "
45
+ "Windows)."
46
+ )
47
+ write_runtime_examples = """```json
48
+{
49
+ "tool_name": "code_execution_remote",
50
+ "tool_args": {
51
+ "runtime": "terminal",
52
+ "session": 0,
53
+ "code": "pwd"
54
+ }
55
+}
56
+```
57
+
58
+```json
59
+{
60
+ "tool_name": "code_execution_remote",
61
+ "tool_args": {
62
+ "runtime": "python",
63
+ "session": 0,
64
+ "code": "import os\\nprint(os.getcwd())"
65
+ }
66
+}
67
+```"""
68
+ elif metadata.get("write_enabled"):
69
+ access_mode = "Read&Write"
70
+ write_runtime_guidance = (
71
+ "- `runtime=terminal`, `python`, `nodejs`, and `input` may modify files on "
72
+ "the remote CLI machine. Use them only when shell-backed execution is the "
73
+ "right tool for the job.\n"
74
+ "- Use shell syntax that matches the remote host (for example, PowerShell on "
75
+ "Windows)."
76
+ )
77
+ write_runtime_examples = """```json
78
+{
79
+ "tool_name": "code_execution_remote",
80
+ "tool_args": {
81
+ "runtime": "terminal",
82
+ "session": 0,
83
+ "code": "pwd"
84
+ }
85
+}
86
+```
87
+
88
+```json
89
+{
90
+ "tool_name": "code_execution_remote",
91
+ "tool_args": {
92
+ "runtime": "python",
93
+ "session": 0,
94
+ "code": "import os\\nprint(os.getcwd())"
95
+ }
96
+}
97
+```"""
98
+ else:
99
+ access_mode = "Read only"
100
+ write_runtime_guidance = (
101
+ "- `runtime=terminal`, `python`, `nodejs`, and `input` are disabled while "
102
+ "local access is Read only. Press F3 to switch the host machine to Read&Write "
103
+ "before starting new shell-backed work that could modify files."
104
+ )
105
+ write_runtime_examples = ""
106
+
107
exec_config = build_exec_config(agent=self.agent)
108
code_exec_timeouts = exec_config.get("code_exec_timeouts")
109
output_timeouts = exec_config.get("output_timeouts")
@@ -36,6 +112,9 @@ class IncludeCodeExecutionRemote(Extension):
112
113
prompt = self.agent.read_prompt(
114
"agent.extras.code_execution_remote.md",
115
+ access_mode=access_mode,
116
+ write_runtime_guidance=write_runtime_guidance,
117
+ write_runtime_examples=write_runtime_examples,
118
code_exec_timeouts=_format_timeouts(
119
code_exec_timeouts if isinstance(code_exec_timeouts, dict) else {}
120
),
plugins/_a0_connector/helpers/ws_runtime.py
+7
-1
@@ -263,7 +263,7 @@ def remote_exec_metadata_for_sid(sid: str) -> dict[str, Any] | None:
263
}
264
265
266
-def select_remote_exec_target_sid(context_id: str) -> str | None:
266
+def select_remote_exec_target_sid(context_id: str, *, require_writes: bool = False) -> str | None:
267
with _state_lock:
268
subscribers = sorted(_context_subscriptions.get(context_id, set()))
269
fallback_sid: str | None = None
@@ -274,6 +274,12 @@ def select_remote_exec_target_sid(context_id: str) -> str | None:
274
fallback_sid = sid
275
continue
276
if metadata.enabled:
277
+ if require_writes:
278
+ file_metadata = _sid_remote_file_metadata.get(sid)
279
+ if file_metadata is not None and (
280
+ not file_metadata.enabled or not file_metadata.write_enabled
281
+ ):
282
+ continue
283
return sid
284
return fallback_sid
285
plugins/_a0_connector/prompts/agent.extras.code_execution_remote.md
+7
-14
@@ -1,6 +1,7 @@
1
## code_execution_remote guidance
2
3
Remote code execution is currently available in this context through the connected CLI.
4
+Current local access mode: `{{access_mode}}`
5
6
Execution config:
7
- code execution timeouts: `{{code_exec_timeouts}}`
@@ -16,6 +17,7 @@ Execution config:
17
- `runtime=input` is only a deprecated compatibility alias for sending one line of keyboard input into a running shell session.
18
- Frontend execution may still be locally disabled in the CLI session. If so, expect a structured `{ok: false}` error instead of a fallback runtime.
19
- Prefer concise, self-checking commands. For multi-step work, inspect output and continue in the same session instead of restarting from scratch.
20
+{{write_runtime_guidance}}
21
22
Examples:
23
@@ -23,9 +25,8 @@ Examples:
25
{
26
"tool_name": "code_execution_remote",
27
"tool_args": {
26
- "runtime": "terminal",
27
- "session": 0,
28
- "code": "pwd && ls -la"
28
+ "runtime": "output",
29
+ "session": 0
30
}
31
}
32
```
@@ -34,19 +35,11 @@ Examples:
35
{
36
"tool_name": "code_execution_remote",
37
"tool_args": {
37
- "runtime": "python",
38
+ "runtime": "reset",
39
"session": 0,
39
- "code": "import os\nprint(os.getcwd())"
40
+ "reason": "Start a clean shell for the next step."
41
}
42
}
43
```
44
44
-```json
45
-{
46
- "tool_name": "code_execution_remote",
47
- "tool_args": {
48
- "runtime": "output",
49
- "session": 0
50
- }
51
-}
52
-```
45
+{{write_runtime_examples}}
plugins/_a0_connector/prompts/agent.system.tool.code_execution_remote.md
+4
@@ -9,6 +9,9 @@ subscribed CLI, so the base system prompt stays small when remote execution is n
9
- The CLI client must support `connector_exec_op`.
10
- Frontend execution may be locally disabled in the CLI session; in that case the result is
11
a structured `{ok: false}` error and no fallback runtime is used.
12
+- Mutating runtimes (`terminal`, `python`, `nodejs`, and `input`) also require the CLI
13
+ session to advertise local access mode `Read&Write` via F3. `output` and `reset` can
14
+ still be used for existing sessions while the CLI is in `Read only`.
15
16
## Arguments
17
- `runtime`: one of `terminal`, `python`, `nodejs`, `output`, `reset`
@@ -25,4 +28,5 @@ Runtime-specific fields:
28
- Session state is frontend-local and shell-backed.
29
- `output` is for long-running operations where a prior call returned control before the
30
shell reached a prompt.
31
+- Use shell syntax that matches the remote host (for example, PowerShell on Windows).
32
- The transport uses `connector_exec_op` and `connector_exec_op_result` with shared `op_id`.
plugins/_a0_connector/tools/code_execution_remote.py
+33
-2
@@ -11,6 +11,8 @@ 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
+ remote_exec_metadata_for_sid,
15
+ remote_file_metadata_for_sid,
16
select_remote_exec_target_sid,
17
store_pending_exec_op,
18
subscribed_sids_for_context,
@@ -24,6 +26,10 @@ EXEC_OP_EVENT = "connector_exec_op"
26
class CodeExecutionRemote(Tool):
27
"""Send shell-backed frontend execution operations to the connected CLI machine."""
28
29
+ @staticmethod
30
+ def _runtime_requires_write_access(runtime: str) -> bool:
31
+ return runtime in {"terminal", "python", "nodejs", "input"}
32
+
33
def get_log_object(self):
34
import uuid
35
@@ -62,11 +68,36 @@ class CodeExecutionRemote(Tool):
68
69
context_id = self.agent.context.id
70
subscribers = subscribed_sids_for_context(context_id)
65
- sid = select_remote_exec_target_sid(context_id)
71
+ require_writes = self._runtime_requires_write_access(runtime)
72
+ sid = select_remote_exec_target_sid(context_id, require_writes=require_writes)
73
if not sid:
74
+ exec_enabled = False
75
+ write_blocked = False
76
+ for subscriber_sid in subscribers:
77
+ exec_metadata = remote_exec_metadata_for_sid(subscriber_sid)
78
+ if exec_metadata is None:
79
+ exec_enabled = True
80
+ continue
81
+ if not exec_metadata.get("enabled"):
82
+ continue
83
+ exec_enabled = True
84
+ if not require_writes:
85
+ break
86
+ file_metadata = remote_file_metadata_for_sid(subscriber_sid)
87
+ if file_metadata is not None and (
88
+ not file_metadata.get("enabled", True)
89
+ or not file_metadata.get("write_enabled")
90
+ ):
91
+ write_blocked = True
92
+
93
return Response(
94
message=(
69
- "code_execution_remote: no subscribed CLI in this context currently has "
95
+ "code_execution_remote: no subscribed CLI in this context currently allows "
96
+ "shell-backed execution that may modify local files. Press F3 to switch "
97
+ "the CLI to Read&Write. `runtime=output` and `runtime=reset` remain "
98
+ "available for existing sessions."
99
+ if subscribers and require_writes and exec_enabled and write_blocked
100
+ else "code_execution_remote: no subscribed CLI in this context currently has "
101
"remote execution enabled. Connect the CLI and press F4 to switch exec on."
102
if subscribers
103
else "code_execution_remote: no CLI client connected to this context. "