"""Lo que JARVIS puede HACER, y el unico sitio por donde pasa. `Manos` es la puerta: el catalogo entra por aqui, el cerebro entra por aqui y el panel entrara por aqui. Tener un solo punto importa porque es donde se decide que se ejecuta y que no, y esa decision no puede estar repartida. Las cuatro piezas de debajo vienen tal cual de la version sobre Newelle —`acciones.py`, `tareas.py`, `sudo_root.py`, `seleccion.py`— y se comprobo al copiarlas que 374 frases dan exactamente el mismo resultado en los dos sitios. """ import os import shlex import subprocess from .acciones import Catalogo, ejecuta, formatea AQUI = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) CATALOGO = os.path.join(AQUI, "datos", "acciones.json") PROPIO = os.path.expanduser("~/.config/jarvis/acciones.json") # Lo que no se ejecuta por mucho que lo pida el modelo. No es una lista de # seguridad completa —eso es imposible— sino un tope contra el accidente: el # modelo es servicial y a veces se inventa una limpieza que nadie pidio. PROHIBIDO = ( "rm -rf /", "mkfs", "dd if=", ":(){", "shutdown", "reboot", "halt", "> /dev/sd", "chmod -R 777 /", "chown -R", "userdel", "passwd", ) TOPE_SALIDA = 4000 class Manos: def __init__(self, catalogo=None, al_ejecutar=None): self.catalogo = catalogo or Catalogo([PROPIO, CATALOGO]) self.al_ejecutar = al_ejecutar # Root para la sesion: la primera vez que una accion necesita sudo se # pide la contraseña, y queda aqui hasta que caduque (15 min). No se # pide al arrancar: solo cuando de verdad hace falta. from manos.sudo_root import GestorRoot self.root = GestorRoot([]) # El panel conecta aqui como pedir la contraseña de root cuando haga # falta (muestra el dialogo). Sin panel (modo terminal) se queda en None. self.al_pedir_root = None # ------------------------------------------------------------- catalogo def busca(self, texto: str): return self.catalogo.busca(texto) def haz(self, accion, argumento="") -> tuple: """Ejecuta una accion ya emparejada. Devuelve (lo que se dice, crudo).""" dicho, salida = ejecuta(accion, [], argumento) if salida: dicho = formatea(accion.respuesta, salida, argumento) return dicho, salida def por_orden(self, orden: str, al_ejecutar=None) -> str: """Para el cerebro: empareja contra el catalogo y ejecuta. Devuelve texto para el modelo, no para el usuario: si no encaja, se le dice claramente para que pruebe con el shell en vez de insistir. """ if not orden.strip(): return "no me has dicho ninguna orden" # El modelo a veces cuela un comando aqui de todas formas. Se detecta y # se le devuelve el motivo, que es mas util que un "no encontrado". if any(c in orden for c in "|><;&$") or orden.split()[0] in ("df", "ls", "ps", "cat"): return ("eso es un comando de shell, no una orden del catalogo. " "Usa la herramienta comando_de_shell.") accion, argumento = self.catalogo.busca(orden) if accion is None: return (f"'{orden}' no esta en el catalogo. Si hace falta, usa " "comando_de_shell.") if al_ejecutar or self.al_ejecutar: (al_ejecutar or self.al_ejecutar)(accion.id, argumento) dicho, salida = self.haz(accion, argumento) return (salida or dicho)[:TOPE_SALIDA] # ---------------------------------------------------------------- shell def por_shell(self, comando: str, al_ejecutar=None) -> str: if not comando.strip(): return "no me has dicho ningun comando" bajo = comando.lower() for veneno in PROHIBIDO: if veneno in bajo: return (f"no ejecuto eso: contiene '{veneno}'. Si de verdad hace " "falta, que lo pida el usuario expresamente.") if al_ejecutar or self.al_ejecutar: (al_ejecutar or self.al_ejecutar)("shell", comando) from manos.sudo_root import necesita_root # Los comandos con sudo van por GestorRoot, que da la contraseña de la # sesion por stdin. Sin ella, `bash -c "sudo ..."` se colgaria pidiendo # una contraseña que nadie teclea. Y si el candado no se ha abierto, no # hay contraseña: el root queda cerrado hasta que el usuario desbloquee. if necesita_root(comando): if not self.root.sesion_activa(): # No hay root todavia: se pide la contraseña AHORA (una vez), no # al arrancar. El panel muestra el dialogo; el usuario la teclea # y repite la orden, que ya correra con la sesion abierta. if self.al_pedir_root: self.al_pedir_root(comando) return ("Esto necesita permiso de administrador, señor. " "Introduzca su contraseña y repitamelo.") return ("eso necesita permiso de administrador y no hay panel " "para pedir la contraseña.") try: r = self.root.ejecuta(comando, timeout=60) except Exception as e: return f"no se pudo ejecutar como root: {e}" else: try: r = subprocess.run(["bash", "-c", comando], capture_output=True, text=True, timeout=60) except subprocess.TimeoutExpired: return "el comando tardo mas de un minuto y lo he cortado" except OSError as e: return f"no se pudo ejecutar: {e}" salida = (r.stdout or r.stderr or "").strip() if not salida: return "hecho, sin salida" if r.returncode == 0 else f"fallo (codigo {r.returncode})" return salida[:TOPE_SALIDA]