# sysagent Agente autónomo de administración de sistemas escrito en Go. Usa un modelo de lenguaje accesible mediante una API compatible con OpenAI y trabaja según las reglas del `agent.md` del directorio desde el que se ejecuta. > **Estado: completo (fases 1–5 de la especificación).** El agente ejecuta > comandos y herramientas de archivos bajo una política declarada en `agent.md`, > mantiene `contexto.mem` y `memoria.md`, comprime el contexto automáticamente, > investiga en Internet, descarga con verificación de checksum, aplica y genera > parches, pide contraseñas sin eco y documenta sus propias herramientas. > Ver [Limitaciones conocidas](#limitaciones-conocidas). La especificación completa del proyecto está en `Agent.md`. ## Descripción `sysagent` administra, diagnostica y mantiene sistemas Linux bajo autorización del propietario. Se ejecuta desde un directorio de proyecto (PROJECT_ROOT) que contiene su configuración (`init.conf`), sus reglas y permisos (`agent.md`) y, opcionalmente, tareas (`01-*.md`, `02-*.md`...). Puede: - procesar tareas de forma autónoma (`sysagent`) o conversar (`sysagent --cli`); - leer y modificar el sistema mediante herramientas controladas por la política: comandos, archivos (escritura atómica, backup, validación), parches; - investigar lo que no sabe (documentación, CVE, avisos de seguridad) y descargar con verificación de checksum; - recordar lo aprendido (`memoria.md`) y el histórico de lo hecho (`contexto.mem`), comprimiendo el contexto cuando se acerca al límite; - verificar sus acciones y distinguir PLAN / EXECUTED / VERIFIED. ## Arquitectura ```text cmd/sysagent/ punto de entrada: flags, señales, código de salida internal/config/ parser de init.conf, enmascarado de secretos internal/policy/ política (bloque ```policy), clasificación de comandos, análisis de shell y Guard internal/project/ PROJECT_ROOT: agent.md, descubrimiento de tareas, aislamiento, memoria.md, .agent/ internal/llm/ interfaz Client + cliente OpenAI-compatible (reintentos, descubrimiento de contexto) internal/ctxmgr/ conversación, estimación calibrada de tokens, división segura y compresor de contexto internal/memory/ memoria.md estructurada (remember/forget) y contexto.mem (estado persistente) internal/fsutil/ escritura atómica y backups compartidos internal/term/ lectura sin eco del terminal (ioctl, sin dependencias) internal/patch/ aplicar y generar diffs unificados (Go puro, sin patch/git) internal/tools/ herramientas: exec, shell, archivos, memoria, web, CVE, descargas, parches internal/toolcall/ parser del protocolo de herramientas por texto (```tool, ) internal/audit/ logging por niveles y agent.log, con redacción de secretos internal/agent/ arranque, prompt de sistema, ciclo del agente, modo tareas y modo CLI examples/ agent.md de ejemplo, unidades systemd (service + timer) Makefile atajos opcionales: build, check, race, cross, install, live ``` El programa solo depende de la biblioteca estándar de Go (sin dependencias externas; `go.sum` no es necesario). Orden de arranque (estricto): PROJECT_ROOT → `init.conf` → `agent.md` → política → `memoria.md` → `contexto.mem` → modo → tareas → contexto inicial → modelo → ciclo del agente. ## Requisitos - Go **1.24** o superior (probado con 1.24.4 de Debian 13). En Debian/Ubuntu: `sudo apt install golang-go`. - Un servidor de inferencia con API estilo OpenAI (`POST /chat/completions`): llama.cpp `llama-server`, vLLM, Ollama, LM Studio, o un servicio remoto. - Linux (amd64 o arm64). - Opcional: `make` para usar el `Makefile`; `gcc` para `go test -race`. ## Compilación ```bash git clone cd go mod download # no descarga nada: no hay dependencias externas go test ./... go build -o sysagent ./cmd/sysagent ``` Pruebas opcionales contra servicios reales (NVD, Debian, buscador, descargas), para comprobar que los formatos externos no han cambiado: ```bash go test -tags live -run Live -v ./internal/tools ``` Con `make` (opcional): `make check` (gofmt + vet + tests), `make build`, `make race`, `make cross`, `make install`, `make live`. La versión se toma de `git describe` o de `VERSION=...`. Con versión inyectada: ```bash go build -ldflags "-X main.version=0.1.0" -o sysagent ./cmd/sysagent ``` Compilación cruzada: ```bash GOOS=linux GOARCH=amd64 go build -o sysagent-amd64 ./cmd/sysagent GOOS=linux GOARCH=arm64 go build -o sysagent-arm64 ./cmd/sysagent ``` ## Instalación ```bash sudo install -m 0755 sysagent /usr/local/bin/sysagent ``` Preparar un proyecto: ```bash mkdir -p /root/admin-agent && cd /root/admin-agent cp /ruta/al/repo/init.conf.example init.conf && chmod 600 init.conf cp /ruta/al/repo/examples/agent.md.example agent.md $EDITOR init.conf agent.md ``` ## init.conf Una directiva `KEY=VALUE` por línea; se permiten comentarios `#` y líneas vacías. Las claves repetidas son un error. Las claves desconocidas se reportan y se ignoran. | Clave | Obligatoria | Descripción | |---|---|---| | `API_URL` | sí | URL base, p.ej. `http://127.0.0.1:8080/v1` | | `API_KEY` | no | Clave; se envía como `Authorization: Bearer`. Se muestra enmascarada (`sk-2f1...8a9`) | | `MODEL` | sí | Nombre exacto del modelo | | `CONTEXT_SIZE` | no | Tokens máximos. `0`/ausente: valor del servidor | | `REQUEST_TIMEOUT` | no | Segundos por petición al modelo (300 por defecto) | | `TOOL_CALLING` | no | `auto` (defecto), `native` o `json`. Ver [Tool calling](#tool-calling) | | `SEARCH_URL` | no | Plantilla del buscador con `{query}` (p.ej. SearXNG JSON). Vacío: DuckDuckGo HTML | Con `CONTEXT_SIZE=0` el agente consulta `GET {API_URL}/models` (`max_model_len`, `context_length`, `meta.n_ctx_train`) y `GET /props` (`n_ctx` de llama.cpp). Si no obtiene nada, el tamaño queda como `UNKNOWN`: nunca se inventa. ## agent.md Autoridad primaria del agente: identidad, propósito, límites y política. Si no existe, el programa se niega a arrancar (`ERROR: agent.md not found in PROJECT_ROOT`). Se acepta una única variante que solo difiera en mayúsculas (p.ej. `Agent.md`), con una advertencia. ### Bloque de política Solo se interpretan las directivas dentro de un bloque cercado con info string `policy`. Así, los ejemplos `KEY=VALUE` del texto libre nunca conceden permisos por accidente: ````markdown ```policy EXECUTION_MODE=privileged ALLOW_NETWORK=true ALLOW_DELETE=false ``` ```` | Directiva | Valores | Por defecto | |---|---|---| | `EXECUTION_MODE` | `guarded`, `privileged`, `unrestricted` | `guarded` | | `ALLOW_NETWORK`, `ALLOW_WEB_RESEARCH`, `ALLOW_DOWNLOADS`, `ALLOW_PACKAGE_INSTALL`, `ALLOW_SERVICE_RESTART`, `ALLOW_OUTSIDE_PROJECT_READ`, `ALLOW_OUTSIDE_PROJECT_WRITE`, `ALLOW_DELETE`, `ALLOW_REBOOT`, `ALLOW_SELF_TOOL_CREATION` | `true`/`false` (también `yes/no`, `1/0`, `on/off`) | `false` | | `BACKUP_BEFORE_SYSTEM_EDIT` | booleano | `true` | | `DEFAULT_COMMAND_TIMEOUT` | segundos (> 0) | `60` | | `MAX_STEPS` | 1–1000 llamadas al modelo por tarea o mensaje | `40` | | `MAX_DOWNLOAD_SIZE` | bytes (sufijos `K`, `M`, `G`) | `1G` | | `TASK_ORDER` | lista separada por comas | orden lexicográfico | Reglas: sin bloque se aplican los valores por defecto; varios bloques, una directiva repetida o un valor inválido impiden arrancar; las directivas desconocidas se reportan y se ignoran. `unrestricted` solo se activa si se escribe literalmente, y no implica ninguna capacidad `ALLOW_*`. ## Herramientas | Herramienta | Qué hace | |---|---| | `exec` | Ejecuta un programa con argumentos (sin shell; `exec.CommandContext`) | | `shell` | Ejecuta una expresión con `bash -lc` (tuberías, redirecciones, bucles) | | `read_file` | Lee un archivo de texto con números de línea (`offset`/`limit`) | | `write_file` | Crea o reemplaza un archivo de forma atómica, con backup y validación opcional | | `edit_file` | Reemplaza un fragmento exacto de texto (atómico, backup, validación) | | `append_file` | Añade texto al final (atómico, backup) | | `copy_file` / `move_file` | Copia (atómica) o mueve/renombra | | `remove_file` | Borra archivo o directorio (`recursive`); requiere `ALLOW_DELETE` | | `mkdir`, `list_dir`, `stat`, `chmod`, `chown` | Operaciones de sistema de archivos | | `system_info` | OS, kernel, CPU, RAM, uid/euid, sudo/doas, init, gestores de paquetes, montajes, interfaces | | `remember` / `forget` | Guardar/eliminar conocimiento persistente en `memoria.md` (ver abajo) | | `web_search` | Búsqueda web: títulos, URLs y fragmentos | | `web_fetch` | Descarga una página y la convierte a texto con enlaces numerados (o `raw`) | | `cve_lookup` | CVE en NVD (descripción, CVSS, rangos afectados, referencias) + Debian Security Tracker (versión corregida por release) | | `download` | Descarga HTTP(S) con límite de tamaño y verificación SHA256/SHA512 o `checksum_url` | | `apply_patch` | Aplica un diff unificado: todo o nada, archivado y con copias para rollback | | `diff_files` | Genera un diff unificado entre dos archivos (documentar cambios, crear parches) | | `register_tool` | Documenta en `memoria.md` una herramienta creada por el agente | | `find_program` | Comprueba qué programas existen y cómo encontrar el paquete que falta | | `ask_secret` | Pide una contraseña al operador sin eco (solo `--cli`); el modelo solo ve `{{secret:nombre}}` | Comandos (`exec`/`shell`): - Timeout obligatorio (`DEFAULT_COMMAND_TIMEOUT` o `timeout_seconds`, máx. 2 h). El proceso corre en su propio grupo y al vencer el timeout (o con Ctrl+C) se mata el grupo completo, incluidos los hijos. - `stdin` es `/dev/null` y se fuerzan `DEBIAN_FRONTEND=noninteractive`, `PAGER=cat`, `SYSTEMD_PAGER=cat`, etc., para que nada espere entrada. - `cwd` configurable sin cambiar el directorio del agente. - stdout y stderr se capturan por separado. Si superan 16 KiB, el modelo recibe los primeros 8 KiB y los últimos 4 KiB, y la salida completa se guarda en `.agent/tmp/` para consultarla con `read_file` o `grep`. Archivos: - Escritura atómica: temporal en el mismo directorio → fsync → permisos/propietario del original → validación → `rename` → fsync del directorio. - `validate`: comando que se ejecuta sobre el temporal **antes** de aplicarlo (`{file}` se sustituye por su ruta), p. ej. `{"command":"sshd","args":["-t","-f","{file}"]}`. Si falla, el original no se toca. - Con `BACKUP_BEFORE_SYSTEM_EDIT=true`, todo archivo existente **fuera** de PROJECT_ROOT se copia antes de modificarlo o borrarlo a `archivo.sysagent.YYYYMMDD-HHMMSS.bak` (mismos permisos y propietario). El resultado incluye la instrucción de rollback. - Idempotencia: si el contenido ya es el pedido, no se escribe nada. - Argumentos validados estrictamente: un campo desconocido o JSON inválido es un error. - Con `reason`, el motivo del cambio queda en `agent.log`. ## Aplicación de la política Todas las herramientas pasan por `policy.Guard`. Los comandos se clasifican por riesgo (desenvolviendo `sudo`, `env`, `timeout`, `nice`, `xargs`, `find -exec`, `bash -c`...); en `shell` se analiza el script completo, incluidas `$(...)`, `` `...` ``, `<(...)`, heredocs, redirecciones y cambios de directorio con `cd`. | Riesgo | Ejemplos | Requiere | |---|---|---| | read-only | `ls`, `cat`, `systemctl status`, `ip addr`, `dpkg -l`, `nft list`, `journalctl` | — | | network | `ping`, `dig`, `curl`, `ssh` | `ALLOW_NETWORK` | | download | `wget`, `curl -o` | `ALLOW_DOWNLOADS` (+ network) | | write | `cp`, `mv`, `mkdir`, `sed -i`, `tar -x`, `make`, `go build` | `privileged`; en `guarded` solo dentro de PROJECT_ROOT | | delete | `rm`, `rmdir`, `find -delete`, `journalctl --vacuum-*` | `ALLOW_DELETE` | | package | `apt install`, `dpkg -i`, `dnf`, `pip install` | `ALLOW_PACKAGE_INSTALL` | | service | `systemctl restart`, `service x reload`, `nginx -s reload` | `ALLOW_SERVICE_RESTART` | | admin | `useradd`, `chmod`, `ip route add`, `nft add`, `mount`, `sysctl -w`, `kill` | `privileged` | | destructive | `dd`, `mkfs`, `wipefs`, `fdisk`, `userdel`, `shred` | `unrestricted` | | reboot | `reboot`, `shutdown`, `systemctl poweroff` | `ALLOW_REBOOT` (en cualquier modo) | | unknown | programas no catalogados, `python`, `awk`, `$CMD` | `privileged` | Rutas: | Regla | Detalle | |---|---| | Lectura fuera de PROJECT_ROOT | `ALLOW_OUTSIDE_PROJECT_READ` | | Escritura fuera de PROJECT_ROOT | `ALLOW_OUTSIDE_PROJECT_WRITE` | | Escritura en `PROJECT_ROOT/tools/` | `ALLOW_SELF_TOOL_CREATION` | | Symlinks | Se evalúa el destino real: un enlace del proyecto hacia `/etc` cuenta como `/etc` | | Protegidos siempre | `agent.md`, `init.conf` (tampoco se puede **leer**: contiene la API key), `agent.log`, `contexto.mem`, `.context-history` (tampoco se puede leer) y cualquier `*.md` de la raíz salvo `memoria.md`. El agente no puede editar sus instrucciones ni crearse tareas nuevas | | Nunca borrables | `/`, `/etc`, `/usr`, `/var`, `/home`, `/boot`... PROJECT_ROOT, `.agent/`, y comodines sobre ellos (`rm -rf /etc/*`) | Cuando algo se deniega, el modelo recibe `denied by policy: ` y se le indica que no intente rodearlo. > **Importante:** el control de política es una barrera contra errores del > modelo, no un sandbox frente a un adversario. El análisis de shell es > aproximado y los programas `unknown` (en `privileged`) pueden hacer cualquier > cosa que permita el usuario del sistema. Para aislamiento real, ejecute el > agente con un usuario sin privilegios, en un contenedor o una VM. ## Tool calling - `TOOL_CALLING=auto` (defecto): se envían las herramientas con el campo `tools` de la API. Si el servidor responde con un error que indica que no lo soporta, el agente cambia al protocolo de texto para el resto de la sesión. - Protocolo de texto (`json`): el prompt de sistema describe las herramientas y el modelo responde con bloques: ````text ```tool {"tool": "exec", "arguments": {"command": "systemctl", "args": ["status", "nginx"]}} ``` ```` También se aceptan `...` (plantillas Hermes/Qwen), incluso en modo nativo, porque muchos modelos locales escriben así la llamada. Un bloque mal formado se devuelve al modelo como error; nunca se ejecuta texto libre. ## Ciclo del agente Para cada tarea (o mensaje del CLI): el modelo razona y pide herramientas → se validan y ejecutan en orden → los resultados vuelven al modelo → se repite hasta que responde sin pedir herramientas. - Límite `MAX_STEPS` por tarea/mensaje. - Detección de bucles: la misma llamada con el mismo resultado 3 veces genera un aviso al modelo; 5 veces detiene la tarea como `BLOCKED`. - En modo tareas, la respuesta final debe terminar en `TASK_STATUS: DONE|BLOCKED|FAILED` (se pide una vez si falta). Una tarea `BLOCKED`/`FAILED` no detiene las siguientes; un error del modelo o Ctrl+C sí. - Contexto: ver [Compresión de contexto](#compresión-de-contexto). ## Investigación en Internet Requiere `ALLOW_NETWORK=true` y `ALLOW_WEB_RESEARCH=true`. - `web_search` usa por defecto el HTML de DuckDuckGo. Los buscadores públicos pueden limitar las consultas automáticas; para uso intensivo, configure una instancia SearXNG propia en `init.conf`: `SEARCH_URL=https://searx.example.com/search?q={query}&format=json`. - `web_fetch` elimina scripts, estilos y navegación, conserva párrafos, listas y tablas, y numera los enlaces (`texto [3]` + lista `LINKS`) para poder seguirlos. Máx. 5 MiB por página; el modelo recibe ~12 KB y el texto completo se guarda en `.agent/tmp/`. - `cve_lookup` consulta la API 2.0 de NVD (las entradas con rango de versiones aparecen primero) y la página del CVE en el Debian Security Tracker, que da la versión corregida por cada release de Debian. Siempre recuerda comprobar la versión instalada antes de concluir. - Todo el contenido web llega marcado como `UNTRUSTED WEB CONTENT`: el modelo lo trata como dato, nunca como instrucciones (§59). - El prompt de sistema indica el orden de fuentes (§38): documentación oficial, avisos del fabricante, repositorios oficiales, NVD, tracker de la distribución, código upstream, issues oficiales y, por último, fuentes secundarias. Cliente HTTP único (§67): pool de conexiones, TLS ≥ 1.2, `User-Agent: sysagent/`, timeouts de conexión/TLS/cabeceras de 15–30 s, máx. 10 redirecciones solo a http(s), y respeta `HTTPS_PROXY`/`NO_PROXY`. ## Descargas Requiere `ALLOW_NETWORK=true` y `ALLOW_DOWNLOADS=true`. - Destino por defecto `.agent/downloads/`; otro destino pasa por la política de rutas (fuera del proyecto requiere `ALLOW_OUTSIDE_PROJECT_WRITE`). - Límite `MAX_DOWNLOAD_SIZE` (agent.md, por defecto 1 GiB; admite `512M`, `2G`). - Verificación: `sha256`, `sha512` o `checksum_url` (archivos `SHA256SUMS`, `*.sha256`, formato GNU o BSD). Se descarga a un temporal calculando el hash; **solo** si tamaño y checksum son correctos se mueve al destino. Un `CHECKSUM MISMATCH` descarta el archivo. - Sin checksum, la descarga se permite pero el resultado indica `NOT VERIFIED` y muestra el SHA-256 calculado. ## Parches `internal/patch` aplica y genera diffs unificados en Go, así que no necesita `patch` ni `git` (una instalación mínima de Debian no trae `patch`). - `apply_patch` acepta `diff -u` y `git diff` (creación y borrado de archivos, `\ No newline at end of file`, desplazamientos de posición). No hay "fuzz": el contexto debe coincidir exactamente. - **Todo o nada**: todos los hunks de todos los archivos se calculan en memoria; si uno falla, no se escribe nada. `check_only=true` solo comprueba. - Antes de escribir se guarda en `.agent/patches//` el parche aplicado y las copias originales (`orig/…`); los archivos fuera del proyecto también reciben su backup `.sysagent..bak`. El resultado incluye los pasos de rollback. - Cada archivo pasa por la política (no se puede parchear `agent.md`, rutas con `..` se rechazan, borrar requiere `ALLOW_DELETE`). - `diff_files` documenta exactamente un cambio (p.ej. backup frente a actual) y puede guardarlo como `.agent/patches/.patch`. Flujo de parche propio (§40), indicado al modelo en el prompt: código fuente legítimo → causa → parche mínimo → `apply_patch` (con `check_only` antes) → compilar → pruebas → comparar antes/después → conservar el original → documentar. Siempre se prefiere el parche oficial. ## Herramientas propias Con `ALLOW_SELF_TOOL_CREATION=true` el agente puede escribir en `PROJECT_ROOT/tools/`. Orden de preferencia (§42): herramienta instalada → paquete de la distribución → upstream confiable → script propio → programa propio. `find_program` comprueba qué existe antes de asumirlo; `register_tool` documenta la herramienta en `memoria.md` (sección `Herramientas`: ruta, descripción, uso, lenguaje) para reutilizarla. Las herramientas no se borran al terminar. ## Contraseñas y secretos Si una tarea necesita una contraseña que no está disponible (§80), en `--cli` el modelo llama a `ask_secret`. El operador la escribe **sin eco**; el valor queda solo en memoria durante la sesión y el modelo recibe únicamente el marcador `{{secret:nombre}}`, que usa en sus llamadas: - `exec`: el marcador se sustituye en los argumentos al ejecutar. - `shell`: el valor viaja como variable de entorno (`SYSAGENT_SECRET_nombre`) y el marcador se convierte en una referencia a ella adaptada al contexto de comillas (sin comillas, `"..."`, `'...'`, `$(...)`, `` `...` ``). Un valor con comillas o `;` nunca se interpreta como código. - `write_file`/`edit_file`/`append_file`: el valor real se escribe en el archivo. El valor se enmascara (vuelve a aparecer como marcador) en todas las salidas, `agent.log`, `contexto.mem`, `memoria.md` y el historial; nunca se envía al modelo. En modo tareas no hay operador: `ask_secret` falla y el agente debe informar del bloqueo. `/secrets` lista los nombres. > Los argumentos de un comando son visibles para otros usuarios locales en la > lista de procesos. Para contraseñas, es preferible `shell` (variable de > entorno) o un archivo con permisos restrictivos. La lectura sin eco usa `ioctl(TCGETS/TCSETS)` y siempre restaura el terminal, también con Ctrl+C. Si la entrada no es un terminal (tubería), se lee sin más; si es un terminal en el que no se puede desactivar el eco, se rechaza. ## Tareas En modo normal se cargan los `*.md` ubicados **directamente** en PROJECT_ROOT, excepto `agent.md`, `memoria.md` y `README.md` (sin distinguir mayúsculas), en orden lexicográfico (o `TASK_ORDER`). No hay búsqueda recursiva, y los symlinks que apuntan fuera del proyecto se ignoran con una advertencia: ningún archivo externo puede convertirse en instrucción. Las tareas completadas (`DONE`) se registran en `contexto.mem` con el hash de su contenido; en la siguiente ejecución **no se repiten** si el archivo no cambió. Para repetirlas: `sysagent --rerun`. Las `BLOCKED`/`FAILED` se reintentan siempre. ## memoria.md Conocimiento persistente descubierto durante las tareas (§21): IPs y roles de hosts, puertos, rutas de configuración, forma correcta de reiniciar un servicio, distribución y kernel, dependencias, herramientas creadas, decisiones. No es un log (eso es `contexto.mem`). - Se crea con permisos `0600` si no existe; se reescribe siempre con `0600` de forma atómica. Si ya existía con permisos más abiertos se emite una advertencia. - El modelo la actualiza con la herramienta `remember` (`section`, `entry`, `facts`) y elimina datos obsoletos con `forget`: ```markdown ## Hosts ### db01 - IP: 10.10.10.15 - Servicio: PostgreSQL - Puerto: 5432 ``` - **Sin duplicados** (§82): un hecho `Etiqueta: valor` actualiza la línea con la misma etiqueta (`IP: ...` cambia la IP existente); un hecho idéntico no se añade de nuevo. `replace=true` reescribe la entrada. - El contenido escrito a mano por el operador (texto libre, otras secciones, bloques de código) se conserva literalmente. - Credenciales (§23): un hecho que parece contraseña, token o clave se rechaza salvo `sensitive=true`; el modelo debe preferir referencias (`Password: secret-tool lookup service postgres-admin`). La API key nunca se guarda (se sustituye por su versión enmascarada). - Tras cada cambio, el prompt de sistema se regenera con la memoria nueva. - `/memory` en el CLI la muestra releyéndola del disco. ## contexto.mem Histórico operativo (§17), en JSON, con permisos `0600` y escritura atómica. Se guarda tras cada mensaje/tarea, en cada cambio de estado de tarea, tras cada compactación y al salir (también con Ctrl+C/SIGTERM). | Campo | Contenido | |---|---| | `summary` | Contexto histórico comprimido con las 14 secciones de §19 | | `compactions` | Número de compactaciones realizadas | | `journal` | Diario registrado **por el programa** (no por el modelo): inicio/fin de sesión, herramientas ejecutadas con resultado, cambios de archivos con sus backups y motivo, denegaciones, errores, estados de tareas, respuestas finales, compactaciones. Máx. 400 eventos; se descartan primero los ya resumidos | | `recent` | Últimos mensajes aún no resumidos (máx. 60, recortados a 4 KB cada uno) | | `tasks` | Estado, hash y fecha de cada tarea | Al arrancar, el resumen se incluye en el prompt como `CONTEXTO HISTÓRICO` y los mensajes recientes como `ÚLTIMOS MENSAJES DE LA SESIÓN ANTERIOR` (en texto, para no depender del protocolo de herramientas de la sesión anterior). Si no caben en ~20 % del contexto, se compactan en la primera petición. Si el archivo no es JSON (p.ej. notas escritas a mano) su texto se importa como resumen. Si es JSON corrupto se renombra a `contexto.mem.corrupt-` y se empieza de cero con una advertencia; nunca se pierde en silencio. ## Compresión de contexto El uso de contexto se **estima** (~3 bytes/token, más el esquema de herramientas en modo nativo) y se **calibra** con los `prompt_tokens` que reporta el servidor tras cada petición. Nunca se presenta como exacto (§84). 1. Al **80 %** del contexto (§18) el historial se divide en una parte antigua y una reciente (~25 % del contexto). El corte nunca separa una llamada a herramienta de su resultado. 2. La parte antigua, el resumen previo, el diario sin resumir y los mensajes de sesiones anteriores se envían al modelo con un prompt dedicado (§83) que exige conservar rutas, IPs, puertos, versiones, decisiones, errores sin resolver, cambios con sus backups y comandos exactos, y no inventar nada. Si no cabe en una petición, se resume por partes encadenando el resumen parcial. 3. La salida se valida (deben estar las secciones `OBJECTIVE`, `CURRENT_STATE`, `COMPLETED_TASKS`, `PENDING_TASKS`, `IMPORTANT_FACTS`, `SYSTEM_STATE`, `FILES_CHANGED`, `COMMANDS_OF_IMPORTANCE`, `ERRORS`, `DECISIONS`, `CREDENTIAL_REFERENCES`, `HOSTS`, `SERVICES`, `LESSONS_LEARNED`; se toleran 3 ausentes) y sustituye a la parte antigua. Si la parte reciente no empieza por un mensaje del usuario, se antepone uno que repite la tarea en curso. 4. Si la compresión falla o no basta (≥ 95 %), se sustituyen salidas antiguas de herramientas por un marcador. 5. Si el servidor rechaza una petición por exceso de contexto (p.ej. con `CONTEXT_SIZE` desconocido), se compacta y se reintenta una vez. En el CLI, `/compact` fuerza una compactación y `/context summary` muestra el resumen. ## Modo --cli ```text $ cd /root/admin-agent $ sysagent --cli System Agent Model: qwen3.5 Mode: privileged Project: /root/admin-agent agent> revisa por qué nginx no inicia ``` Carga `init.conf`, `agent.md`, `memoria.md` y `contexto.mem`, e ignora los archivos de tareas. | Comando | Descripción | |---|---| | `/help` | Ayuda | | `/status` | Modelo, API (clave enmascarada), modo, uso de contexto, root | | `/context` | Uso de contexto, resumen histórico, diario; `/context summary` imprime el resumen | | `/memory` | Muestra `memoria.md` | | `/policy` | Política efectiva | | `/history` | Conversación de la sesión | | `/journal [n]` | Últimos n eventos del diario de `contexto.mem` (20 por defecto) | | `/compact` | Compacta ya la conversación antigua en el resumen histórico | | `/secrets` | Nombres de los secretos introducidos en la sesión (nunca los valores) | | `/clear` | Vacía la conversación reciente (se conservan resumen, memoria y diario) | | `/quit`, `/exit`, Ctrl+D | Salir | Durante un mensaje, la consola muestra cada herramienta (`→ exec: ...`) y su resultado (`← ok`, `← error`, `← denied`). Ctrl+C cancela el mensaje en curso (petición al modelo y procesos hijos); en el prompt, sale. El uso de contexto se muestra como *reported by server* cuando el servidor devuelve `usage`, o *estimated* (estimación conservadora de ~3 bytes/token). No hay edición de línea ni historial con flechas; se puede usar `rlwrap sysagent --cli`. ## Otras opciones ```text --version muestra la versión --verbose muestra logs DEBUG/INFO en consola (y DEBUG en agent.log) --rerun repite tareas que contexto.mem registra como DONE --dry-run las herramientas que modifican el sistema solo describen lo que harían; los comandos clasificados como solo lectura/red sí se ejecutan (diagnóstico) ``` ## Ejemplos ```bash # Modo normal: procesa 01-*.md, 02-*.md... cd /root/admin-agent && sysagent # Interactivo cd /root/admin-agent && sysagent --cli ``` Con llama.cpp local: ```bash llama-server -m qwen3-8b.gguf -c 32768 --port 8080 # init.conf: API_URL=http://127.0.0.1:8080/v1 MODEL=qwen3 CONTEXT_SIZE=0 ``` ## Permisos y archivos generados | Ruta | Permisos | Contenido | |---|---|---| | `agent.log` | 0600 | Auditoría: timestamps RFC3339, acciones, resultados | | `memoria.md` | 0600 | Conocimiento persistente | | `contexto.mem` | 0600 | Resumen histórico, diario operativo, estado de tareas | | `.agent/{tmp,downloads,patches,logs}` | 0700 | `tmp/`: salidas y páginas extensas; `downloads/`: descargas; `patches/`: parches aplicados, originales y diffs | | `*.sysagent.YYYYMMDD-HHMMSS.bak` | los del original | Backups junto al archivo modificado | | `.context-history` | 0600 | Entradas del operador en el CLI con fecha (§79); se omiten líneas que parecen credenciales y nunca incluye lo tecleado en `ask_secret` | | `.agent/lock` | 0600 | Bloqueo de instancia única (PID) | Se recomienda `chmod 600 init.conf`. El programa detecta UID/EUID y lo informa (`Running as root`) pero no asume privilegios. ## Seguridad - **Aislamiento de instrucciones**: solo `agent.md`, las tareas de PROJECT_ROOT y el operador del CLI dan instrucciones. Todo lo demás (archivos externos, salidas de comandos, páginas web) es dato no confiable y se marca como tal. - **Jerarquía de autoridad** incluida en el prompt de sistema: reglas internas > agent.md > operador CLI > tareas > memoria.md > contexto.mem > sistema > Internet. - **Fail closed**: capacidades no declaradas = denegadas; errores de configuración o de política impiden arrancar. - **El agente no puede cambiar sus propias reglas**: `agent.md`, `init.conf`, `agent.log`, `contexto.mem`, `.context-history` y los `*.md` de la raíz están protegidos frente a sus herramientas (ver [Aplicación de la política](#aplicación-de-la-política)). - **Revisión al arrancar**: se niega a arrancar si PROJECT_ROOT, `agent.md`, `init.conf` o un archivo de tarea son escribibles por cualquier usuario, o si PROJECT_ROOT es un directorio compartido tipo `/tmp` (cualquiera podría dictar lo que hace un agente que corre como root). Advierte si son escribibles por el grupo, si pertenecen a otro usuario mientras el agente corre como root, o si `init.conf` con `API_KEY` es legible por otros. - **Instancia única** por PROJECT_ROOT (`flock` en `.agent/lock`): dos agentes a la vez corromperían `contexto.mem` y duplicarían acciones. - **Límites**: `agent.md` y cada tarea ≤ 1 MiB; argumentos de herramienta ≤ 40 MiB; respuestas del modelo ≤ 64 MiB; páginas web ≤ 5 MiB; descargas ≤ `MAX_DOWNLOAD_SIZE`; escritura de archivos ≤ 32 MiB. - **Secretos**: la API key solo viaja en la cabecera HTTP; un redactor la sustituye por su versión enmascarada en logs, consola, `contexto.mem`, `memoria.md` y mensajes al modelo. Los secretos de `ask_secret` reciben el mismo trato (ver [Contraseñas y secretos](#contraseñas-y-secretos)). - **Robustez**: un error interno de una herramienta se devuelve como error al modelo en lugar de terminar el proceso; SIGINT/SIGTERM cancelan procesos hijos y guardan el estado (un segundo Ctrl+C fuerza la salida). - **Registro de cambios** (§88): cada modificación de un archivo existente guarda en `agent.log` y en el diario de `contexto.mem` el diff exacto (valor anterior → nuevo), el motivo, la fecha y la ruta del backup. > El control de política es una barrera contra errores del modelo, no un > sandbox frente a un adversario. Para aislamiento real, ejecute el agente con > un usuario dedicado sin privilegios (con `sudo` restringido si hace falta), en > un contenedor o en una VM. ### Reintentos y privilegios - Errores transitorios que garantizan que el comando no hizo nada se reintentan con backoff (§54): bloqueo de apt/dpkg (3 reintentos: 5 s, 15 s, 30 s) y fallos temporales de DNS (2 reintentos: 2 s, 5 s). Los errores permanentes no se reintentan. El resultado indica cada reintento. - Si un comando falla por permisos (§46) y el agente no es root, el resultado incluye qué mecanismos de escalada hay (`sudo`/`doas` y si funcionan sin contraseña, comprobado con `-n`; `su`). Si ya es root, se sugieren otras causas (sistema de archivos de solo lectura, SELinux/AppArmor, atributo inmutable, contenedor). ### Conflictos entre tareas Las tareas se ejecutan en el orden de `TASK_ORDER` o lexicográfico (§61). El modelo conoce la lista de tareas de la ejecución y, si la actual contradice a una anterior, no deshace a ciegas: sigue el orden y lo indica con una línea `CONFLICT: ...`, que se registra en `agent.log` y en el diario. ## Ejecución periódica con systemd `examples/sysagent.service` y `examples/sysagent.timer` ejecutan las tareas de un proyecto cada hora. Como las tareas `DONE` sin cambios no se repiten, el agente solo actúa cuando hay tareas nuevas, modificadas o pendientes: ```bash sudo cp examples/sysagent.service examples/sysagent.timer /etc/systemd/system/ sudo systemctl edit sysagent.service # ajustar WorkingDirectory sudo systemctl daemon-reload && sudo systemctl enable --now sysagent.timer journalctl -u sysagent.service ``` ## Troubleshooting | Síntoma | Causa / solución | |---|---| | `ERROR: agent.md not found in PROJECT_ROOT` | Ejecutar desde el directorio del proyecto; crear `agent.md` | | `init.conf not found in PROJECT_ROOT` | Copiar `init.conf.example` a `init.conf` | | `API_URL must be an http(s) URL` | Incluir el esquema: `http://host:puerto/v1` | | `multiple policy blocks` | Dejar un solo bloque ```` ```policy ```` en agent.md | | `could not query model server` | Aviso: el servidor no expone `/models`; no impide arrancar | | `connection refused` en cada petición | El servidor de inferencia no está en marcha o la URL es incorrecta. Hay 3 reintentos con backoff (1s, 2s, 4s) | | `HTTP 404` en `/chat/completions` | `API_URL` debe terminar en el prefijo de la API (normalmente `/v1`) | | Uso de contexto `UNKNOWN` | Definir `CONTEXT_SIZE` en init.conf | | Respuestas con `` | Se eliminan automáticamente del contenido mostrado | | `denied by policy: ...` | La política de agent.md no lo permite; ajustar el bloque ```` ```policy ```` si procede | | El modelo no usa herramientas o escribe JSON suelto | Probar `TOOL_CALLING=json`; en llama.cpp, iniciar `llama-server` con `--jinja` para tool calling nativo | | `malformed tool call` | El modelo generó JSON inválido; se le devuelve el error y suele corregirse | | `loop detected` | El modelo repitió la misma acción; revisar la tarea o usar un modelo más capaz | | `MAX_STEPS=... reached` | Aumentar `MAX_STEPS` en agent.md o dividir la tarea | | `context compaction failed: ... missing sections` | El modelo no siguió el formato del resumen; se recortan salidas en su lugar. Un modelo más capaz o más contexto lo evita | | Una tarea no se ejecuta (`DONE, skipped`) | Ya se completó y no cambió; usar `--rerun` o editar el archivo | | `contexto.mem could not be parsed` | Se movió a `contexto.mem.corrupt-*`; revisarlo y, si procede, copiar su resumen a mano | | `insecure project: ... writable by any user` | Corregir permisos: `chmod o-w agent.md init.conf *.md` y del directorio | | `another sysagent instance is running` | Otra ejecución usa el proyecto (PID indicado); esperar o detenerla. El bloqueo se libera solo si el proceso muere | | `ask_secret ... no interactive operator` | Las contraseñas solo pueden pedirse en `--cli`; en modo tareas use un almacén (p.ej. `secret-tool`) y guarde en memoria.md dónde está | | `unknown secret(s)` | El modelo usó un marcador que no se pidió en esta sesión (los secretos no se persisten) | | Reiniciar el histórico | Borrar (o mover) `contexto.mem`; `memoria.md` es independiente | | `search provider returned no parseable results` | El buscador limita consultas automáticas; configurar `SEARCH_URL` (SearXNG) | | `NVD: HTTP 403/429` | NVD limita clientes anónimos; reintentar más tarde (Debian tracker sigue funcionando) | | `CHECKSUM MISMATCH` | El archivo no coincide con el checksum: se descartó. Verificar la URL y la fuente del checksum | | `hunk N does not apply: context not found` | El archivo no coincide con el que se usó para crear el parche; revisar `base_dir`/`strip` o regenerar el parche | ## Estado de implementación | Fase | Contenido | Estado | |---|---|---| | 1 | init.conf, agent.md, política, tareas, cliente LLM, `--cli` | hecho | | 2 | ejecución de comandos, herramientas de archivos, tool calling, aplicación de política, `--dry-run` | hecho | | 3 | contexto.mem, memoria.md automática (remember/forget), compresión de contexto | hecho | | 4 | investigación web, CVE, descargas verificadas, parches, herramientas propias | hecho | | 5 | endurecimiento, secretos sin eco, reintentos, registro de cambios, conflictos, systemd, documentación | hecho | Pruebas: `go test ./...` cubre, entre otros, el parser de `init.conf`, el de la política, el descubrimiento de tareas, la validación de rutas y comandos, la compresión de contexto, la memoria, el parser de tool calls, las herramientas (con servidores HTTP locales) y el ciclo completo del agente con un modelo simulado. Los comandos de prueba son inofensivos (`printf`, `echo`, `true`, `false`) y todo ocurre en directorios temporales (§73, §74). ## Limitaciones conocidas - **No es un sandbox.** La clasificación de comandos es un catálogo y el análisis de shell es aproximado; los programas no catalogados (`unknown`, incluidos `python`, `awk` y las herramientas de `tools/`) se bloquean en `guarded` pero se permiten en `privileged`. - La calidad del resumen de contexto depende del modelo: se valida su estructura, no su exactitud. El diario del programa (datos exactos) se conserva aparte en `contexto.mem` hasta 400 eventos. - `web_fetch` no ejecuta JavaScript; los buscadores públicos pueden bloquear consultas automáticas (use `SEARCH_URL` con SearXNG); NVD limita clientes anónimos. - `apply_patch` no admite parches binarios ni renombrados de `git`. - Los secretos de `ask_secret` duran solo la sesión y solo existen en `--cli`. - El CLI no tiene edición de línea ni historial con flechas: use `rlwrap sysagent --cli`. - El recuento de tokens es siempre una estimación (calibrada con el servidor cuando este informa `usage`). - Solo Linux (amd64, arm64). Distribuciones priorizadas: Debian y Ubuntu; la detección de gestor de paquetes cubre apt, dnf/yum, pacman, zypper y apk.