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.
134 lines
5.6 KiB
Python
134 lines
5.6 KiB
Python
"""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)
|