JARVIS: asistente de voz local para Linux
Nucleo propio: oye con whisper.cpp, piensa con un modelo de Ollama, habla con Piper, y hace RAG sobre los apuntes del usuario. 100% local, sin cuentas ni claves. Escrito bajo una restriccion dura, 4 GB de VRAM: el cerebro y whisper comparten tarjeta y solo caben porque estan dimensionados para ello. El RAG usa embeddings estaticos con busqueda hibrida; la voz clonada se sirve de una cache de frases. Incluye instalador (install.sh), requisitos, y documentacion del stack, del manejo de root y de las acciones. Los apuntes indexados y el diario NO se incluyen: son privados y el .gitignore los bloquea.
This commit is contained in:
commit
8e4bc8ad94
125 changed files with 25033 additions and 0 deletions
134
nucleo/manos/sudo_root.py
Normal file
134
nucleo/manos/sudo_root.py
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
"""Ejecutar ordenes como root de forma controlada.
|
||||
|
||||
El problema que resuelve. Newelle convierte `sudo` en `sudo -S` (leer la
|
||||
contraseña de la entrada estandar), pero en el panel de voz no hay una entrada
|
||||
donde teclearla, asi que se queda colgado esperando una contraseña que no
|
||||
llega. Y las acciones marcadas para confirmar no tenian dialogo: solo decian
|
||||
"confirme antes" y no ejecutaban nada.
|
||||
|
||||
Como se resuelve, y las decisiones de seguridad detras:
|
||||
|
||||
- La contraseña NUNCA se escribe en disco. Vive en memoria de este proceso solo
|
||||
si el usuario activa el permiso de sesion, y se pasa a `sudo -S` por stdin.
|
||||
- El permiso de sesion CADUCA: a los 15 minutos de la ultima orden se olvida
|
||||
sola, y tambien al cerrar el panel. No es un "root para siempre".
|
||||
- CADA orden con root muestra su comando exacto en un dialogo antes de correr.
|
||||
El permiso de sesion solo ahorra re-teclear la contraseña, nunca salta el ver
|
||||
y aprobar lo que se va a ejecutar. La voz no puede autoaprobar root.
|
||||
|
||||
Alternativa descartada: apoyarse solo en el timestamp de sudo (`sudo -v`). Es
|
||||
mas limpio en teoria —lo gestiona el sistema— pero a traves de flatpak-spawn el
|
||||
tty cambia entre llamadas y el cacheo no es fiable. Mantener la contraseña en
|
||||
memoria del proceso, con caducidad y sin tocar disco, funciona siempre y el
|
||||
usuario opta a ello de forma explicita.
|
||||
"""
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
# A los 15 minutos sin usarla, la contraseña en memoria se olvida. Igual que el
|
||||
# valor por defecto de sudo, para no sorprender.
|
||||
CADUCIDAD = 15 * 60
|
||||
|
||||
# Detecta una invocacion real de sudo: al principio del comando o justo tras un
|
||||
# separador (;, |, &, &&, ||, salto de linea), no la palabra "sudo" dicha como
|
||||
# argumento ("echo sudo no cuenta"). Errar aqui solo pediria una contraseña de
|
||||
# mas, no es peligroso, pero molesta.
|
||||
_SUDO = re.compile(r"(?:^|[;&|\n]|&&|\|\|)\s*sudo(?:\s|$)")
|
||||
|
||||
|
||||
def necesita_root(comando: str) -> bool:
|
||||
"""True si el comando lanza algo con sudo."""
|
||||
return bool(comando and _SUDO.search(comando))
|
||||
|
||||
|
||||
class GestorRoot:
|
||||
"""Guarda —o no— la contraseña de sesion, y ejecuta con ella.
|
||||
|
||||
Una instancia por panel. Al cerrar el panel se llama a olvida().
|
||||
"""
|
||||
|
||||
def __init__(self, prefijo_host=None):
|
||||
# prefijo para salir del sandbox; se pasa como argumento para poder
|
||||
# probar esto sin flatpak
|
||||
self._prefijo = list(prefijo_host or [])
|
||||
self._password = None
|
||||
self._ultimo_uso = 0.0
|
||||
|
||||
# -------------------------------------------------------------- sesion
|
||||
|
||||
def sesion_activa(self) -> bool:
|
||||
"""Hay contraseña recordada y aun no ha caducado."""
|
||||
if self._password is None:
|
||||
return False
|
||||
if time.monotonic() - self._ultimo_uso > CADUCIDAD:
|
||||
self.olvida()
|
||||
return False
|
||||
return True
|
||||
|
||||
def olvida(self):
|
||||
"""Borra la contraseña de memoria. Se llama al cerrar el panel y al
|
||||
caducar. Se sobreescribe antes de soltarla, por si acaso."""
|
||||
if self._password is not None:
|
||||
self._password = "\x00" * len(self._password)
|
||||
self._password = None
|
||||
self._ultimo_uso = 0.0
|
||||
|
||||
def recuerda(self, password: str):
|
||||
self._password = password
|
||||
self._ultimo_uso = time.monotonic()
|
||||
|
||||
# ------------------------------------------------------------ ejecutar
|
||||
|
||||
def valida(self, password: str) -> bool:
|
||||
"""Comprueba la contraseña con `sudo -S -v`, sin ejecutar nada mas.
|
||||
|
||||
-k borra cualquier timestamp previo para que la comprobacion sea de
|
||||
verdad; -v solo actualiza credenciales. Si la contraseña es correcta
|
||||
devuelve True; nunca lanza ningun comando del usuario.
|
||||
"""
|
||||
try:
|
||||
r = subprocess.run(
|
||||
self._prefijo + ["sudo", "-S", "-k", "-p", "", "-v"],
|
||||
input=password + "\n", capture_output=True, text=True,
|
||||
timeout=10)
|
||||
return r.returncode == 0
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return False
|
||||
|
||||
def ejecuta(self, comando: str, password: str = None, timeout: int = 30):
|
||||
"""Ejecuta el comando dando la contraseña a los sudo que lleve.
|
||||
|
||||
password: si se pasa, se usa esa vez; si no, la de la sesion. La
|
||||
contraseña se pasa por stdin a `sudo -S`. No se registra en ningun
|
||||
sitio.
|
||||
|
||||
Devuelve (salida, codigo). codigo -1 = no habia contraseña disponible.
|
||||
"""
|
||||
clave = password if password is not None else self._password
|
||||
if clave is None:
|
||||
return "No hay contraseña disponible para root.", -1
|
||||
|
||||
# que todos los sudo del comando lean de stdin sin imprimir el prompt
|
||||
preparado = re.sub(r"(^|[;&|]|\s)sudo(\s)",
|
||||
r"\1sudo -S -p '' \2", comando)
|
||||
try:
|
||||
r = subprocess.run(
|
||||
self._prefijo + ["bash", "-lc", preparado],
|
||||
input=clave + "\n", capture_output=True, text=True,
|
||||
timeout=timeout)
|
||||
salida = (r.stdout or r.stderr or "").strip()
|
||||
# sudo escribe su prompt/errores en stderr aunque -p sea vacio;
|
||||
# se limpia lo que es ruido de sudo, no del comando
|
||||
salida = "\n".join(
|
||||
l for l in salida.splitlines()
|
||||
if l.strip() and not l.startswith(("[sudo]", "Sorry, try again")))
|
||||
self._ultimo_uso = time.monotonic()
|
||||
return salida, r.returncode
|
||||
except subprocess.TimeoutExpired:
|
||||
return "La orden con root ha tardado demasiado.", 124
|
||||
except OSError as e:
|
||||
return f"No se pudo ejecutar: {e}", 1
|
||||
finally:
|
||||
# no dejar la copia local viva mas de lo necesario
|
||||
clave = "\x00" * len(clave)
|
||||
Loading…
Add table
Add a link
Reference in a new issue