Repository navigation
Expand file tree
/
Copy pathexecutor.py
More file actions
235 lines (209 loc) · 8.67 KB
/
Copy pathexecutor.py
File metadata and controls
235 lines (209 loc) · 8.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
"""FiaOS Command Executor — NL → shell command interpreter with Claude/Ollama."""
import asyncio
import json
import os
import re
import shlex
import subprocess
import anthropic
# Commands that are always blocked
BLOCKED_PATTERNS = [
r"\brm\s+-rf\s+/\s*$", # rm -rf /
r"\brm\s+-rf\s+/\*", # rm -rf /*
r"\brm\s+-rf\s+~\s*$", # rm -rf ~
r"\bmkfs\b", # format disk
r"\bdd\s+if=.+of=/dev/", # dd to disk device
r":\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:", # fork bomb
r"\bshutdown\b", # shutdown
r"\breboot\b", # reboot
r"\bhalt\b", # halt
r"\bsudo\s+rm\s+-rf", # sudo rm -rf
r"\bnewfs\b", # newfs (macOS format)
r"\bdiskutil\s+eraseDisk", # erase disk
# Protect FiaOS services from being killed remotely
r"\blaunchctl\s+(unload|remove|stop).*fiaos", # can't unload FiaOS services
r"\bpkill.*server\.py", # can't kill FiaOS server
r"\bkill.*server\.py", # can't kill FiaOS server
r"\bpkill.*fiaos", # can't kill FiaOS
r"\bpkill.*personaplex", # managed by FiaOS, not user
r"\blaunchctl\s+(unload|remove|stop).*caffeinate", # keep display awake
]
SYSTEM_PROMPT = """You are a macOS command translator. The user gives you natural language instructions.
You respond with ONLY a JSON object — no markdown, no explanation.
Format:
{"command": "the shell command to run", "description": "one-line description of what this does"}
Rules:
- Output valid macOS (zsh/bash) commands
- For "open" requests, use the `open` command (e.g., `open -a Safari`)
- For file listing, use `ls`
- For system info, use appropriate macOS commands (sysctl, df, top, etc.)
- If the request is a question that doesn't need a command (like "how are you"), respond with:
{"command": null, "description": "your helpful answer here"}
- NEVER output dangerous commands (rm -rf /, shutdown, reboot, mkfs, dd to devices)
- Current working directory is the user's home: ~
- The user's projects are in ~/Desktop/PROJECTS/
- Keep commands simple and safe
"""
OLLAMA_MODEL = "qwen3-coder"
def _strip_quoted_strings(cmd: str) -> str:
"""Replace contents of double- and single-quoted strings with empty quotes,
so the safety regex matches command-position tokens, not data inside args.
Without this, words like 'shutdown' appearing inside `claude -p "..."` prompt
text would trip false positives even though they're never executed."""
no = re.sub(r'"(?:\\.|[^"\\])*"', '""', cmd)
no = re.sub(r"'[^']*'", "''", no)
return no
def is_command_safe(cmd: str) -> bool:
"""Check if a command is safe to execute. Scans command-position tokens only;
contents of quoted string args are ignored (they're never executed as code)."""
scan_target = _strip_quoted_strings(cmd)
for pattern in BLOCKED_PATTERNS:
if re.search(pattern, scan_target):
return False
return True
# Claude Code's binary. Found on PATH so this works wherever npm/installer put
# it; the ~/.local/bin fallback covers a login shell PATH that launchd didn't get.
import shutil
CLAUDE_BIN = (shutil.which("claude")
or os.path.expanduser("~/.local/bin/claude"))
async def interpret_with_claude(user_input: str) -> dict:
"""Use local Claude Code (Matt's Max plan, no API cost) to interpret natural
language into a JSON command. Runs `claude -p` as a subprocess on this mini —
keychain-unlocked under the GUI login that started FiaOS, so OAuth works."""
prompt = f"{SYSTEM_PROMPT}\n\nUser: {user_input}\n\nRespond with ONLY the JSON object:"
proc = await asyncio.create_subprocess_exec(
CLAUDE_BIN, "-p", prompt,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT,
)
try:
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=1800)
except asyncio.TimeoutError:
try:
proc.kill()
except Exception:
pass
raise
text = stdout.decode(errors="replace").strip()
# Strip markdown code fences if present
if text.startswith("```"):
text = re.sub(r"^```\w*\n?", "", text)
text = re.sub(r"\n?```$", "", text)
try:
return json.loads(text)
except json.JSONDecodeError:
match = re.search(r"\{[^}]+\}", text, re.DOTALL)
if match:
return json.loads(match.group())
raise
async def interpret_with_ollama(user_input: str) -> dict:
"""Fallback: use local Ollama to interpret commands."""
proc = await asyncio.create_subprocess_exec(
"ollama", "run", OLLAMA_MODEL,
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
prompt = f"{SYSTEM_PROMPT}\n\nUser: {user_input}\n\nRespond with ONLY the JSON object:"
stdout, _ = await proc.communicate(prompt.encode())
text = stdout.decode().strip()
# Strip markdown code fences if present
if text.startswith("```"):
text = re.sub(r"^```\w*\n?", "", text)
text = re.sub(r"\n?```$", "", text)
# Try to extract JSON from response
try:
return json.loads(text)
except json.JSONDecodeError:
# Try to find JSON in the response
match = re.search(r'\{[^}]+\}', text)
if match:
try:
return json.loads(match.group())
except json.JSONDecodeError:
pass
return {"command": None, "description": f"Ollama couldn't parse: {text[:200]}"}
async def execute_command(user_input: str) -> dict:
"""Main entry: interpret NL input and optionally execute."""
# RAW: prefix bypasses LLM interpretation. Used by automations like the
# morning briefing that already know the exact shell command they need to
# run, so they don't depend on Claude/Ollama being reachable.
if user_input.startswith("RAW:"):
cmd = user_input[4:].strip()
if not is_command_safe(cmd):
return {"input": user_input, "command": cmd,
"output": "BLOCKED: This command was flagged as potentially dangerous.",
"ai_mode": "raw"}
try:
proc = await asyncio.create_subprocess_shell(
cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT,
)
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=1800)
return {"input": user_input, "command": cmd,
"output": stdout.decode(errors="replace"),
"ai_mode": "raw"}
except Exception as e:
return {"input": user_input, "command": cmd,
"output": f"raw exec failed: {e}",
"ai_mode": "raw"}
ai_mode = "cloud"
# Try Claude first, fall back to Ollama
try:
result = await interpret_with_claude(user_input)
except Exception:
ai_mode = "local"
try:
result = await interpret_with_ollama(user_input)
except Exception as e:
return {
"input": user_input,
"command": None,
"output": f"Both Claude and Ollama failed: {e}",
"ai_mode": "error",
}
cmd = result.get("command")
description = result.get("description", "")
if cmd is None:
return {
"input": user_input,
"command": None,
"output": description,
"ai_mode": ai_mode,
}
# Safety check
if not is_command_safe(cmd):
return {
"input": user_input,
"command": cmd,
"output": "BLOCKED: This command was flagged as potentially dangerous.",
"ai_mode": ai_mode,
}
# Execute the command
try:
proc = await asyncio.create_subprocess_shell(
cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=os.path.expanduser("~"),
env={**os.environ, "HOME": os.path.expanduser("~")},
)
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=1800)
output = stdout.decode() if stdout else ""
if stderr:
output += "\n" + stderr.decode()
output = output.strip()
if not output:
output = f"Command executed successfully (exit code {proc.returncode})"
except asyncio.TimeoutError:
output = "Command timed out after 30 minutes"
except Exception as e:
output = f"Execution error: {e}"
return {
"input": user_input,
"command": cmd,
"description": description,
"output": output,
"ai_mode": ai_mode,
}