diff --git a/README.md b/README.md index a05d057..d1094ef 100644 --- a/README.md +++ b/README.md @@ -59,11 +59,6 @@ Solo si vas MUY justo de VRAM y el cerebro y whisper se pelean por ella: fijar comentarios del propio `Modelfile`. En una torre con una tarjeta normal **no hace falta tocar nada**. -### Probado en - -NVIDIA T1200 (4 GB), 62 GB RAM, Debian 12 con GNOME/X11. Es el equipo del autor, -el caso más ajustado; en una torre con más VRAM sobra sitio. - ## Instalación Necesita, en la máquina: [Ollama](https://ollama.com), whisper.cpp (compilado @@ -109,21 +104,26 @@ preguntas de pentesting, la recuperación sube de **hit@1 48 % → 82 %** y **hit@5 77 % → 95 %**. El método, el pipeline y esos números están en [docs/rag.md](docs/rag.md). -## Órdenes con root +## Órdenes con root — importante -Algunas acciones necesitan `sudo`. El manejo es explícito y con límites, no un -"root para siempre": +Algunas órdenes que gestionan el sistema (instalar un paquete, reiniciar un +servicio, montar un disco) necesitan `sudo`. Conviene saber cómo se maneja, +porque es donde un asistente de voz puede hacer daño: -- La contraseña **nunca se escribe en disco**. Vive en memoria del proceso solo - si activas el permiso de sesión, y se pasa a `sudo -S` por la entrada estándar. -- El permiso de sesión **caduca a los 15 minutos** de la última orden, y también - al cerrar el panel. -- **Cada** orden con root muestra su comando exacto en un diálogo antes de - ejecutarlo. El permiso de sesión solo ahorra re-teclear la contraseña; nunca - salta el ver y aprobar. **La voz no puede autoaprobar root.** +- **No pide la contraseña al arrancar.** Molestar de entrada echa para atrás, y + la mayoría de lo que se le pide no necesita root. Arranca y funciona sin nada. +- **Se pide solo cuando hace falta, y una vez.** La primera vez que una orden + necesita `sudo`, aparece un diálogo pidiendo tu contraseña. La tecleas, la + repites, y a partir de ahí el permiso queda abierto para la sesión. +- **Caduca a los 15 minutos** de inactividad, y al cerrar el panel. No es un + "root para siempre". +- La contraseña **nunca se escribe en disco**: vive solo en la memoria del + proceso mientras dura la sesión, y se pasa a `sudo -S` por la entrada estándar. +- **La voz no puede autoaprobar root.** El permiso de sesión ahorra re-teclear + la contraseña, nunca salta el pedirla la primera vez. -Con el candado echado, el asistente no obedece órdenes que toquen el sistema: -las pide desbloquear primero. +Si prefieres el modo seguro (bloquear el panel y pedir la contraseña al abrir, +para que nadie que pase por delante dé órdenes), arranca con `--candado`. ## Privacidad diff --git a/config/ventanas.sh b/config/ventanas.sh index 38752fb..92b7b72 100755 --- a/config/ventanas.sh +++ b/config/ventanas.sh @@ -7,7 +7,12 @@ # recompilar el flatpak por cada ajuste. Aqui se edita y ya esta, igual que los # envoltorios de voz. # -# Requiere X11 (en Wayland no hay forma de mover ventanas ajenas) y xdotool. +# Motor X11 (xdotool). En Wayland, mover ventanas AJENAS depende del compositor +# —cada uno tiene su via (swaymsg, hyprctl...) o directamente lo prohibe por su +# modelo de seguridad (GNOME/KDE), y los tiling ni tienen "mitad izquierda"—, asi +# que no hay una via universal. Aqui se detecta el entorno y se avisa claro; el +# resto de JARVIS funciona igual, solo estas ordenes de ventanas quedan fuera en +# Wayland. Soporte por compositor: pendiente (ver el roadmap del README). # # ventanas.sh pantallas lista las pantallas # ventanas.sh listar lista las ventanas movibles @@ -26,8 +31,21 @@ set -uo pipefail RAIZ=$(cd -P "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd) ALIAS_FICHERO=${JARVIS_ALIAS_PANTALLAS:-$HOME/.config/jarvis-pantallas.json} -hay_x11() { [ -n "${DISPLAY:-}" ] && command -v xdotool >/dev/null; } -hay_x11 || { echo "no hay X11 o falta xdotool"; exit 1; } +hay_x11() { [ -n "${DISPLAY:-}" ] && command -v xdotool >/dev/null 2>&1; } +if ! hay_x11; then + # Diagnostico segun el entorno, no un "no X11" seco. + if [ "${XDG_SESSION_TYPE:-}" = "wayland" ] || [ -n "${WAYLAND_DISPLAY:-}" ]; then + echo "las acciones de ventanas usan xdotool (X11) y estas en Wayland" \ + "(${XDG_CURRENT_DESKTOP:-tu compositor}). En Wayland mover ventanas" \ + "ajenas depende del compositor; por ahora estas ordenes quedan fuera." \ + "El resto de JARVIS funciona igual." + elif [ -n "${DISPLAY:-}" ]; then + echo "estas en X11 pero falta xdotool. Instalalo con tu gestor de paquetes." + else + echo "no hay sesion grafica (ni DISPLAY ni WAYLAND_DISPLAY)." + fi + exit 1 +fi # ---------------------------------------------------------------- pantallas diff --git a/docs/panel.png b/docs/panel.png index d714159..95b7a95 100644 Binary files a/docs/panel.png and b/docs/panel.png differ diff --git a/install.sh b/install.sh index a64455e..3ea64fa 100755 --- a/install.sh +++ b/install.sh @@ -73,7 +73,38 @@ else fi echo -# ── 4. Siguiente paso ─────────────────────────────────────────────────────── -echo "Hecho. Para arrancar:" -echo " ./nucleo/saber/indexa.py # (opcional) indexa tus apuntes para el RAG" -echo " ./nucleo/arranca.sh # el asistente y el panel en 127.0.0.1" +# ── 4. Lanzador de escritorio (icono en el menu de aplicaciones) ──────────── +echo "4. Lanzador de escritorio" +APPS="$HOME/.local/share/applications" +ICONO="$AQUI/jarvis-icon.png" +[ -f "$ICONO" ] || ICONO="$AQUI/jarvis-icon.svg" +mkdir -p "$APPS" +cat > "$APPS/jarvis.desktop" </dev/null +command -v update-desktop-database >/dev/null && update-desktop-database "$APPS" 2>/dev/null +ok "creado $APPS/jarvis.desktop — busca «JARVIS» en el menu de aplicaciones" +echo + +# ── 5. Siguiente paso ─────────────────────────────────────────────────────── +echo "Hecho. Ya puedes:" +echo " · buscar JARVIS en el menu de aplicaciones y pulsar el icono, o" +echo " · ./nucleo/arranca.sh # arrancar a mano (panel en 127.0.0.1)" +echo " · ./nucleo/saber/indexa.py # (opcional) indexar tus apuntes para el RAG" diff --git a/jarvis-icon.png b/jarvis-icon.png new file mode 100644 index 0000000..1ef1feb Binary files /dev/null and b/jarvis-icon.png differ diff --git a/nucleo/cara/panel.py b/nucleo/cara/panel.py index 386749d..10b22fd 100644 --- a/nucleo/cara/panel.py +++ b/nucleo/cara/panel.py @@ -246,6 +246,11 @@ class Panel: # (accion, para_que, hasta_cuando). para_que es "argumento" o "confirmar". self.pendiente = None + # Root a demanda: las manos avisan aqui cuando una accion necesita sudo + # y no hay sesion. En vez de pedir la contraseña al arrancar, se pide + # solo entonces, una vez, y se cachea. Ver _pide_root. + self.manos.al_pedir_root = self._pide_root + # -------------------------------------------------------------- empujar def arranca(self): @@ -528,6 +533,22 @@ class Panel: self.tareas.lanza(accion, arg) return True + def _pide_root(self, comando: str = ""): + """Muestra el dialogo de contraseña porque una accion necesita root. + + No es el candado del arranque —eso pedia la contraseña de primeras y + echaba para atras—: esto es a demanda, la PRIMERA vez que hace falta + sudo en la sesion. La contraseña que se teclee la valida y la cachea + _intenta_desbloquear (por el mismo canal "desbloquear"), asi que a + partir de ahi el root ya esta abierto y no se vuelve a pedir hasta que + caduque. Corre desde el hilo de las manos, asi que solo manda al HUD. + """ + que = (comando or "").strip()[:60] + self.cara.manda("bloqueo", { + "mensaje": f"Contraseña para: {que}" if que + else "Contraseña de administrador", + "root": True}) + def _intenta_desbloquear(self, clave: str): if not clave: return diff --git a/nucleo/jarvis.py b/nucleo/jarvis.py index 91844cc..3c99716 100755 --- a/nucleo/jarvis.py +++ b/nucleo/jarvis.py @@ -219,7 +219,10 @@ def escucha(boca: Boca, modelo="small", fuente=None, callado=False, def panel(boca: Boca, cerebro=None, escuchando=False, modelo="small", fuente=None, abre=True, centinela=None, sin_candado=False) -> int: """Levanta el panel y, si se pide, ademas escucha el microfono.""" - manos = Manos(catalogo()) + # Un SOLO Manos, compartido con el cerebro: asi hay una unica sesion de root + # y un unico sitio donde pedir la contraseña. Antes el cerebro tenia su Manos + # y el panel otro, con roots separados: desbloquear en uno no abria el otro. + manos = cerebro.manos if (cerebro and getattr(cerebro, "manos", None)) else Manos(catalogo()) cara = Cara() p = Panel(cara, boca, manos, cerebro, candado=not sin_candado, centinela=centinela) @@ -273,8 +276,10 @@ def main() -> int: help="levantar el panel pero no abrir el navegador") p.add_argument("--palabra", action="store_true", help=f"exigir que le llamen «{NOMBRE}» antes de cada orden") - p.add_argument("--sin-candado", action="store_true", - help="no pedir la contraseña al abrir (solo para probar)") + p.add_argument("--candado", action="store_true", + help="pedir la contraseña al abrir y bloquear el panel. Por " + "defecto NO: no molesta al arrancar, y el root se pide " + "solo la primera vez que una accion lo necesita") a = p.parse_args() boca = Boca(a.voz or "davefx") @@ -322,7 +327,7 @@ def main() -> int: if a.panel: return panel(boca, cerebro, a.escucha, a.modelo, a.fuente, abre=not a.sin_abrir, centinela=centinela, - sin_candado=a.sin_candado) + sin_candado=not a.candado) if a.escucha: return escucha(boca, a.modelo, a.fuente, a.callado, cerebro, diff --git a/nucleo/manos/__init__.py b/nucleo/manos/__init__.py index ad165f1..af6f744 100644 --- a/nucleo/manos/__init__.py +++ b/nucleo/manos/__init__.py @@ -33,11 +33,14 @@ 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: cuando el usuario desbloquea el candado con su - # contraseña, esa misma queda aqui y el cerebro puede hacer sudo hasta - # que caduque. Sin desbloquear, no hay root: el candado es la puerta. + # 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 @@ -93,8 +96,15 @@ class Manos: # hay contraseña: el root queda cerrado hasta que el usuario desbloquee. if necesita_root(comando): if not self.root.sesion_activa(): - return ("eso necesita root y el candado no esta abierto. " - "Desbloquee con su contraseña y lo repito.") + # 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: