JARVIS/nucleo/manos/__init__.py
sito 3a2a1bb111 Root a demanda, lanzador de escritorio, captura nueva y ajustes
- Root: no se pide la contraseña al arrancar (echaba para atras). Se pide
  solo la primera vez que una accion necesita sudo, y se cachea. --candado
  para el modo seguro. Aviso de root prominente en el README.
- install.sh crea un lanzador de escritorio (jarvis.desktop) con el icono
  naranja: aparece en el menu de aplicaciones para arrancar con un clic.
- Captura del panel reemplazada por una mas limpia (solo la ventana).
- Quitado "Probado en ..." del README (informacion del equipo).
- ventanas.sh: aviso claro segun el compositor en Wayland.
2026-08-16 16:20:27 +02:00

124 lines
5.8 KiB
Python

"""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]