Files
iasysadmin/README.md
T
2026-09-27 16:04:02 -06:00

719 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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, <tool_call>)
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 <repository>
cd <repository>
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: <motivo>` 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 `<tool_call>...</tool_call>` (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/<versión>`, 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/<nombre>`; 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/<fecha>/` el parche aplicado y
las copias originales (`orig/…`); los archivos fuera del proyecto también
reciben su backup `.sysagent.<fecha>.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/<nombre>.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-<fecha>` 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 `<think>` | 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.