post inicial

This commit is contained in:
lastdragon
2026-09-27 16:04:02 -06:00
parent d84587e1f3
commit 52864794f5
67 changed files with 15023 additions and 2 deletions
+717 -2
View File
@@ -1,3 +1,718 @@
# iasysadmin
# sysagent
Agente de IA para administrar sistemas operativos
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.