Fecha: 2026-05-15
Tipo: Crítica constructiva de funcionamiento, rendimiento y mantenibilidad
Nota: Los bugs de seguridad críticos ya están documentados en security_llm_review.md. Este documento se centra exclusivamente en lógica, arquitectura funcional, y oportunidades de mejora.
Problema:
_trim_context accede a self.messages[1] (la "misión") como invariante, pero chat_turn puede llamar a _trim_context sin haber añadido un segundo mensaje aún. Si el historial tiene un solo mensaje (el system prompt), self.messages[1] lanza IndexError.
# Actual — crash si len(messages) == 1
mission = self.messages[1]Mejora:
system = self.messages[0]
has_mission = len(self.messages) > 1 and self.messages[1]["role"] == "user"
mission = self.messages[1] if has_mission else None
reserved = [system] + ([mission] if mission else [])
keep_recent = MAX_TOTAL_CONTEXT - len(reserved) - 1 # -1 para el summary
recent = self.messages[-keep_recent:]
dropped = len(self.messages) - len(reserved) - len(recent)
summary_msg = {"role": "assistant", "content": f"[Context trimmed: {dropped} messages removed.]"}
self.messages = reserved + [summary_msg] + recentProblema:
Los bloques de ejecución de tools (resolución de función, _execute_with_scm, serialización del resultado, emisión de eventos, gestión de errores, deduplicación) están copiados palabra por palabra en ambos métodos. Son ~80 líneas idénticas duplicadas. Cualquier fix en uno no se propaga al otro (como se ve con BUG-11 de la primera review).
Mejora:
Extraer un método privado _execute_tool_call(tool_call) -> str que devuelva el result_str listo para añadir al historial, y que ambos métodos llamen:
async def _execute_tool_call(self, tool_call: dict, tools_called: set) -> tuple[str, bool]:
"""Returns (result_str, was_executed). False if duplicate."""
func_name = tool_call["function"]["name"]
args = tool_call["function"]["arguments"]
call_key = self._make_call_key(func_name, args)
if call_key in tools_called:
return f"SKIP: {func_name} already called with these arguments.", False
tools_called.add(call_key)
bus.emit(EventType.AGENT_TOOL_CALL, {"tool": func_name, "args": args})
try:
func = self.tools_registry.get(func_name)
if not func:
raise ValueError(f"Tool {func_name} not found.")
result = await anyio.to_thread.run_sync(
lambda f=func, a=args, n=func_name: self._execute_with_scm(n, f, a)
)
result_val = self._unwrap_result(result)
result_str = self._truncate_result(json.dumps(result_val, cls=DateTimeEncoder))
bus.emit(EventType.AGENT_TOOL_RESULT, {"tool": func_name, "result": result_str})
return result_str, True
except Exception as e:
error_msg = f"Error executing {func_name}: {e}"
logger.error(error_msg)
bus.emit(EventType.AGENT_TOOL_RESULT, {"tool": func_name, "error": error_msg})
return error_msg, TrueProblema:
La rama else (sin tool calls) de chat_turn tiene varias condiciones anidadas que se solapan y son difíciles de razonar: comprueba step > 0 AND consecutive_no_tool >= 1, pero también step == 0, y dentro del primer caso hace una subcomprobación de len(tools_called) == 0. El resultado es un comportamiento no intuitivo: si el agente no llama herramientas en el step 0, inyecta un nudge; si no llama en el step 1 pero ya había tools antes, rompe el loop. Pero si no hay tools y consecutive >= 1 y no hay tools llamadas, inyecta otro mensaje diferente. Con 3 rutas superpuestas es fácil introducir bucles infinitos.
Mejora:
Simplificar a una máquina de estados explícita:
# Pseudocódigo limpio
if tool_calls:
consecutive_no_tool = 0
await self._process_tool_calls(tool_calls, tools_called)
else:
consecutive_no_tool += 1
if "?" in content and step > 0:
return content # pregunta explícita → devolver control al usuario
if consecutive_no_tool == 1 and step == 0:
self._inject_directive("Execute the first technical step now.")
elif consecutive_no_tool == 2 and not tools_called:
self._inject_directive("No tools called yet. Execute immediately.")
elif consecutive_no_tool >= 2:
break # salir al bloque de síntesisProblema:
El fallback de parseo JSON en _call_ollama intenta primero json.loads directo, luego regex de bloques markdown, luego extracción fuzzy de {}. El problema es que en el bloque fuzzy, start puede sobreescribirse con un valor incorrecto si hay { y [ en el contenido, porque la lógica de priorización (qué viene antes, qué es más grande) está replicada en condicionales anidados difíciles de leer. Además, la variable first_match (ya documentada en la review de bugs) crea confusión al asignar start y luego sobreescribirlo.
Mejora:
Usar una función utilitaria ordenada en core/llm_utils.py:
def extract_json_blocks(text: str) -> list[Any]:
"""Extract all parseable JSON objects/arrays from text, in priority order."""
candidates = []
# 1. Whole string
try: candidates.append(json.loads(text.strip()))
except: pass
# 2. Markdown fenced blocks
for m in re.finditer(r"```(?:json)?\s*(.*?)\s*```", text, re.DOTALL):
try: candidates.append(json.loads(m.group(1)))
except: pass
# 3. Outermost balanced braces/brackets
for opener, closer in [('{', '}'), ('[', ']')]:
start = text.find(opener)
end = text.rfind(closer)
if start != -1 and end > start:
try: candidates.append(json.loads(text[start:end+1]))
except: pass
return candidatesProblema:
run_mission corre como asyncio.Task en el bridge. Si el usuario quiere cancelar una misión en curso (p.ej., porque está atacando el target equivocado), no hay endpoint ni señal para interrumpirla. El único control es esperar a que llegue al max_steps o a que el LLM emita MISSION_COMPLETE.
Mejora:
Añadir un asyncio.Event de cancelación:
class PentestAgent:
def __init__(self, ...):
...
self._cancel_event = asyncio.Event()
def request_cancel(self):
self._cancel_event.set()
async def run_mission(self, prompt, max_steps=40):
self._cancel_event.clear()
...
for step in range(max_steps):
if self._cancel_event.is_set():
bus.emit(EventType.AGENT_STATUS, {"status": "Cancelled"})
self._save_state()
return
...Y en el bridge:
@app.post("/api/agent/cancel")
async def cancel_mission():
agent = Container.resolve(PentestAgent)
agent.request_cancel()
return {"status": "cancel_requested"}Problema:
_get_conn() llama a sqlite3.connect(self.db_path) cada vez. Sin WAL mode ni pool, cada operación abre y cierra el fichero. Con el agente ejecutando 10-20 tools en un solo paso y todas intentando escribir al inventario, se producen OperationalError: database is locked en operaciones concurrentes.
def _get_conn(self):
conn = sqlite3.connect(self.db_path) # nueva conexión cada llamada, sin WAL
conn.row_factory = sqlite3.Row
return connMejora:
Añadir WAL mode y timeout, e inicializar PRAGMA una sola vez:
def _get_conn(self) -> sqlite3.Connection:
conn = sqlite3.connect(self.db_path, timeout=30, check_same_thread=False)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA synchronous=NORMAL")
return connPara cargas altas, considerar un threading.local() con una conexión por hilo, que es el patrón estándar para SQLite concurrente sin pool.
Problema:
Las tablas findings, hosts, y services no tienen índices. Filtrar findings por engagement_id o severity, o buscar hosts por ip, hace full table scan. Para engagements con cientos de hosts y miles de findings (p.ej., durante un scan masivo con Nuclei), las queries se vuelven lentas.
Mejora:
Añadir índices al _init_db:
conn.execute("CREATE INDEX IF NOT EXISTS idx_findings_engagement ON findings(engagement_id)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_findings_severity ON findings(severity)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_hosts_engagement ON hosts(engagement_id)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_hosts_ip ON hosts(ip)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_services_host ON services(host_id)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_evidence_finding ON evidence(finding_id)")Problema:
El historial de eventos se implementa con una lista Python y pop(0) para mantener el límite de 1000:
self._history.append(payload)
if len(self._history) > 1000:
self._history.pop(0) # O(n) — desplaza toda la listaCon el agente emitiendo decenas de eventos por step (thoughts, tool calls, results), y bajo lock global de self._lock, esto bloquea brevemente todos los emisores.
Mejora:
Usar collections.deque(maxlen=1000):
from collections import deque
# En __init__:
cls._instance._history = deque(maxlen=1000)
# emit() — sin cambios: deque gestiona el límite automáticamente
self._history.append(payload) # O(1), sin pop() manualLa iteración en get_recent() funciona igual sobre deque.
Problema 1:
list_jobs usa List[Dict[str, Any]] como tipo de retorno pero List no está importado de typing (solo Dict, Any, Optional, Callable). Produce NameError en Python < 3.9 si se hace from __future__ import annotations activo.
Problema 2:
create_job lanza threading.Thread(target=wrapper, daemon=True) sin ningún límite. Si el agente crea 50 jobs de larga duración (Nikto, Nmap full-range, Nuclei), el proceso tiene 50 threads bloqueados, más los del pool de anyio y del EventBus.
Mejora:
from typing import Dict, Any, Optional, Callable, List # añadir List
# Usar ThreadPoolExecutor en lugar de un thread por job:
class JobManager:
def __init__(self, max_workers: int = 8):
self.jobs: Dict[str, Job] = {}
self._lock = threading.Lock()
self._executor = ThreadPoolExecutor(max_workers=max_workers, thread_name_prefix="job")
def create_job(self, tool: str, target_func: Callable, *args, **kwargs) -> str:
job_id = str(uuid.uuid4())[:8]
job = Job(id=job_id, tool=tool)
with self._lock:
self.jobs[job_id] = job
self._executor.submit(self._run_job, job, target_func, *args, **kwargs)
return job_id
def _run_job(self, job: Job, func: Callable, *args, **kwargs):
try:
job.status = "running"
job.result = func(*args, **kwargs)
job.status = "completed"
except Exception as e:
job.status = "failed"
job.error = str(e)
finally:
job.end_time = time.time()Problema:
core/task_queue.py implementa una cola async con workers, retry y cancelación — exactamente lo que JobManager necesita para no spawnar threads ilimitados. Sin embargo, TaskQueue no se registra en el Container, no se arranca en ningún lifespan, y nadie la usa. Es código muerto completo.
Mejora:
Dos opciones: (a) eliminar TaskQueue y extender JobManager con ThreadPoolExecutor (más simple, ver 2.4), o (b) migrar JobManager para usar TaskQueue como backend (más correcto a largo plazo). Si se elige (b):
# En setup_container():
task_queue = TaskQueue(max_workers=8)
Container.register_singleton(TaskQueue, task_queue)
# En el lifespan del bridge (o en un hook de arranque del MCP server):
@asynccontextmanager
async def lifespan(app):
setup_container(service_context="webapp")
tq = Container.resolve(TaskQueue)
await tq.start()
yield
await tq.stop()Problema:
El bridge permite CORS desde cualquier origen. Los endpoints /api/approvals/approve y /api/approvals/deny son operaciones de alta consecuencia (aprobar exploits, credential dumps) que pueden ser llamadas por cualquier página web abierta en el navegador del operador (CSRF).
Mejora:
Restringir a los orígenes conocidos, o añadir un token CSRF:
ALLOWED_ORIGINS = os.environ.get("ALLOWED_ORIGINS", "http://localhost:3000,http://localhost:8321").split(",")
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["GET", "POST"],
allow_headers=["Content-Type", "X-CSRF-Token"],
)Problema:
POST /api/approvals/approve acepta request_id y approver como query params libres. Cualquiera en la red que conozca un request_id puede aprobarse a sí mismo una operación CRITICAL. El approver puede ser cualquier string arbitrario.
Mejora:
Añadir un header de API key mínimo para el bridge en entornos no-localhost:
from fastapi import Header, HTTPException
BRIDGE_API_KEY = os.environ.get("BRIDGE_API_KEY", "")
def verify_api_key(x_api_key: str = Header(default="")):
if BRIDGE_API_KEY and x_api_key != BRIDGE_API_KEY:
raise HTTPException(status_code=403, detail="Invalid API key")
@app.post("/api/approvals/approve", dependencies=[Depends(verify_api_key)])
async def approve_request(request_id: str, approver: str = "operator:ui"):
...Problema:
El endpoint /ws recibe texto del cliente pero lo ignora completamente (solo hace logger.info). Toda la comunicación es unidireccional: el bridge emite eventos al cliente pero el cliente no puede enviar comandos por WebSocket. Si se quiere enviar un mensaje al agente desde la UI por WS, actualmente es imposible — hay que ir por REST.
Mejora:
Añadir dispatch de comandos via WS para reducir latencia en el flujo interactivo:
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await manager.connect(websocket)
try:
while True:
raw = await websocket.receive_text()
try:
msg = json.loads(raw)
cmd = msg.get("command")
if cmd == "chat":
asyncio.create_task(_handle_ws_chat(msg.get("message", ""), websocket))
elif cmd == "cancel":
agent = Container.resolve(PentestAgent)
agent.request_cancel()
except json.JSONDecodeError:
pass
except WebSocketDisconnect:
manager.disconnect(websocket)Problema:
El endpoint POST /api/agent/mission rechaza nuevas misiones si _active_missions no está vacío, pero no hay endpoint GET /api/agent/mission/status que permita al cliente saber si hay una misión activa, cuántos steps lleva, o cuál es el estado actual. El cliente solo recibe eventos por WS, y si se reconecta pierde el contexto.
Mejora:
@app.get("/api/agent/status")
async def agent_status():
return {
"active": bool(_active_missions),
"count": len(_active_missions),
"recent_events": bus.get_recent(EventType.AGENT_STATUS, limit=5)
}Problema:
Los perfiles y niveles de riesgo de cada tool están definidos en dos diccionarios separados. TOOL_METADATA en main.py tiene la clasificación correcta (p.ej., sqlmap_scan = EXPLOIT/L3); _AGENT_TOOL_METADATA en agent.py tiene la clasificación incorrecta (p.ej., sqlmap_scan = READ/L0, kali_exec = READ/L0). Cuando el agente ejecuta tools directamente, aplica la clasificación equivocada. Toda corrección en uno hay que recordar aplicarla en el otro.
Mejora:
Centralizar en un único módulo core/tool_registry.py e importar desde ambos puntos:
# core/tool_registry.py
TOOL_RISK_METADATA: dict[str, dict] = {
"port_scan": {"profile": "PROBE", "level": "L2"},
"nikto_scan": {"profile": "SCAN", "level": "L3"},
"kali_exec": {"profile": "SCAN", "level": "L2"},
"sqlmap_scan": {"profile": "EXPLOIT", "level": "L3"},
"hook_function":{"profile": "EXPLOIT", "level": "L3"},
...
}# En main.py y agent.py:
from core.tool_registry import TOOL_RISK_METADATAProblema:
tools/sessions.py tiene def __init__ y todos los métodos de SessionTools pero le falta la declaración class SessionTools:. El archivo arranca directamente en el __init__ como código suelto. Python lo parsea pero lanza SyntaxError o IndentationError dependiendo del contexto de importación. Si actualmente funciona, es porque register_all_tools lo importa dentro de una función, no a nivel de módulo — pero es una bomba de tiempo.
Corrección:
# Añadir antes del __init__:
class SessionTools:
def __init__(self, scm: SecurityControlModel, audit: AuditTrail,
approval: ApprovalManager, workspace_manager: WorkspaceManager):
...Problema:
plan_init y plan_update llaman a self.plan.save_to_file("state/mission_plan.yaml") directamente desde el thread del agente. Si el agente autónomo actualiza el plan varias veces en rápida sucesión (varios plan_update en el mismo step), pueden ocurrir escrituras concurrentes al mismo fichero YAML sin ningún lock, corrompiendo el fichero.
Mejora:
Añadir un lock a nivel de MissionPlan:
class MissionPlan:
def __init__(self, ...):
...
self._file_lock = threading.Lock()
def save_to_file(self, path: str):
with self._file_lock:
with open(path, "w") as f:
yaml.dump(self.model_dump(mode="json"), f)Problema:
core/llm_utils.py define call_ollama() con los mismos parámetros (modelo, temperatura, num_ctx, etc.) que OllamaClient.chat() en agent.py. ReversingTools usa OllamaClient directamente (instanciado en tools/__init__.py). Hay tres formas diferentes de llamar a Ollama en el codebase, ninguna compartiendo configuración. Si se cambia el modelo base, hay que actualizar tres sitios.
Mejora:
Consolidar en llm_utils.py con un cliente compartido:
# core/llm_utils.py — único cliente Ollama para todo el sistema
class OllamaClient:
def __init__(self, base_url: str = None, model: str = None):
cfg = Container.resolve(dict) # config.yaml
ollama_cfg = cfg.get("ollama", {})
self.base_url = base_url or ollama_cfg.get("url", "http://localhost:11435")
self.model = model or ollama_cfg.get("model", "qwen2.5-coder:7b-openclaw")
self._options = {
"temperature": ollama_cfg.get("temperature", 0.4),
"num_ctx": ollama_cfg.get("num_ctx", 24576),
"num_predict": ollama_cfg.get("num_predict", 4096),
"repeat_penalty": ollama_cfg.get("repeat_penalty", 1.1),
"top_p": ollama_cfg.get("top_p", 0.9),
}Y todos los demás módulos importan from core.llm_utils import OllamaClient.
El CircuitBreaker es una utilidad general pero solo se aplica al adapter de Ghidra. Adapters como Nmap, Nikto, Nuclei o Metasploit también pueden colgarse, timeout, o entrar en estados de error repetidos — precisamente los casos que el circuit breaker resuelve. Sería valioso aplicarlo en GenericDockerAdapter.execute() con un breaker por herramienta.
La consola lee config.yaml directamente para obtener el puerto del bridge, replicando el mismo bloque try/except que EventBus.__new__ y dependencies.py. Si se añade un campo nuevo a la config, hay que actualizarlo en tres sitios. Extraer una función load_config() en core/dependencies.py y exportarla para todos los consumidores (ya existe como función local, solo falta hacerla pública).
8.3 — AUTO_APPROVE_CRITICAL en producción: el hilo de auto-aprobación no loggea el contexto completo
El hilo de auto-aprobación (_approval_cli) aprueba silenciosamente todo lo que hay en cola bajo approver="admin-auto". Si este env var queda activo accidentalmente en producción (variable de entorno residual en CI/CD), no hay warning prominente en el arranque. Añadir:
if os.environ.get("AUTO_APPROVE_CRITICAL") == "1":
logger.warning("⚠️ AUTO_APPROVE_CRITICAL is ENABLED — all CRITICAL actions will be auto-approved. "
"DO NOT use in production environments.")Si el bridge se reinicia mientras hay una misión activa, el agente pierde estado de _active_missions y los events que estaba emitiendo. El lifespan actual solo llama setup_container() al arranque sin ninguna limpieza al parar. Añadir:
@asynccontextmanager
async def lifespan(app: FastAPI):
setup_container(service_context="webapp")
yield
# Cleanup
for task in list(_active_missions):
task.cancel()
await asyncio.gather(*_active_missions, return_exceptions=True)
bus.shutdown()| Prioridad | Área | Item |
|---|---|---|
| 🔴 Alta | agent.py | Duplicación run_mission/chat_turn — riesgo de divergencia |
| 🔴 Alta | sessions.py | Falta class SessionTools: — SyntaxError latente |
| 🔴 Alta | tool_registry | Doble TOOL_METADATA desincronizado — SCM bypass silencioso |
| 🟠 Media | agent.py | _trim_context crash si messages < 2 |
| 🟠 Media | agent.py | Sin mecanismo de cancelación de misión |
| 🟠 Media | bridge | CORS wildcard + endpoints de aprobación sin auth |
| 🟠 Media | inventory | Sin WAL mode ni índices — locks en cargas altas |
| 🟠 Media | jobs | Thread ilimitado por job + List no importado |
| 🟡 Baja | events | pop(0) O(n) → deque |
| 🟡 Baja | task_queue | TaskQueue nunca arrancada — código muerto |
| 🟡 Baja | planning | Race condition en save YAML |
| 🟡 Baja | llm_utils | Tres clientes Ollama duplicados |
| 🟡 Baja | circuit_breaker | Solo en GhidraAdapter — debería aplicarse en GenericDocker |
| 🟡 Baja | bridge | WS solo recibe, no procesa comandos |
| 🟡 Baja | bridge | Sin estado de misión activa accesible por API |
Durante una auditoría de tokens especiales generados y consumidos por el modelo a través de Ollama, se identificaron y resolvieron desajustes entre la plantilla de Go (template) utilizada por el modelo y el código:
Problema:
El SDK de Ollama y su template por defecto ya envuelven automáticamente el contenido de los mensajes con role="tool" dentro de las etiquetas <tool_response>...</tool_response> y asignan el rol <|im_start|>user. En agent_context.py, el código añadía manualmente estas etiquetas al texto. Esto generaba un doble anidamiento (<tool_response><tool_response>...</tool_response></tool_response>), lo cual degradaba la comprensión del modelo y desperdiciaba tokens.
Mejora:
Se eliminó la envoltura manual en agent_context.py. Ahora se deja que la plantilla de Ollama gestione los tags cuando el rol es "tool".
Problema:
La plantilla de Ollama ya inyecta automáticamente un bloque # Tools y explica explícitamente cómo debe formatearse un <tool_call> (en formato JSON) en el system prompt cuando se pasan herramientas. Sin embargo, prompt_builder.py inyectaba nuevamente un bloque extenso con instrucciones de formato de llamadas (TOOL CALL FORMAT), causando redundancia de ~100 tokens y ambigüedad potencial.
Mejora:
Se limpió el bloque de prompt_builder.py para dejar únicamente las directivas de comportamiento (WAIT_FOR_JOBS, job_status, MISSION_COMPLETE) en la sección TOOL CALL BEHAVIORS, eliminando las instrucciones estructurales redundantes.
Problema:
El blob del modelo qwen2.5-coder:7b-openclaw tiene una temperatura por defecto de 0.3 (optimizada para resultados analíticos y herramientas). llm_adapter.py y OllamaClient forzaban la temperatura a 0.4, incrementando la creatividad por encima de lo deseado para un agente de pentesting determinista.
Mejora:
Se alineó la temperatura hardcodeada a 0.3 en llm_adapter.py para coincidir con la recomendación intrínseca del creador del modelo.
Durante la optimización funcional del flujo interactivo de pentesting, se auditaron y corrigieron problemas graves relacionados con la ejecución en background y la limitación de comandos de los adapters:
10.1 — Bucle de Consulta Infinito en Capturas de Red (core/tool_registry.py & tools/network_analysis.py)
Problema:
El comando live_packet_analysis realiza su propia gestión de hilos y contenedores en segundo plano y transmite datos de forma nativa a través de eventos LIVE_PACKET_BATCH. Sin embargo, estaba registrado en el set de ASYNC_TOOLS de core/tool_registry.py. Esto causaba que agent.py envolviera el comando en una tarea del JobManager (ID 52eb6e1a), declarando el inicio exitoso inmediatamente como "completado". El LLM interpretaba esto como una tarea terminada e intentaba reiteradamente consultar su estado mediante job_status usando el ID interno sniff-XXXX (no registrado en el gestor global), cayendo en un bucle infinito de reintentos y errores.
Mejora:
- Se eliminó
live_packet_analysisdeASYNC_TOOLSpara permitir que el bridge y el agente lo traten como un proceso de streaming directo. - Se modificó el mensaje de éxito retornado por la función en
tools/network_analysis.pypara inyectar una directiva clara de comportamiento en el payload: "Captured packets will automatically stream into your chat context as LIVE_PACKET_BATCH events. Do NOT use the 'job_status' tool to poll this job...".
Problema:
El GenericDockerAdapter restringe la ejecución a una lista de comandos permitidos (whitelist) dentro de config.yaml. Sin embargo, múltiples scripts especializados (como webapp.py, recon_active.py, vuln_analysis.py) ejecutan comandos de apoyo y utilidades de shell/forenses de forma legítima (p.ej. cat, bash, python3, rabin2, radiff2) mediante self.generic.execute(). La ausencia de estas utilidades en la whitelist causaba excepciones inmediatas AdapterError bloqueando la ejecución completa del agente.
Mejora:
Se añadieron de manera segura las utilidades Linux necesarias (cat, bash, python3, rabin2, radiff2) a la lista de permitidos en la configuración del adapter genérico en config.yaml, evitando caídas críticas durante el análisis dinámico.
Problema:
El modelo no siempre consultaba el plan de la misión (plan_update) ni debatía con antelación el encadenamiento de exploits antes de lanzarse a la ejecución de comandos intrusivos de red, ignorando a menudo la evaluación de si sus objetivos intermedios ya se encontraban completados.
Mejora:
Se optimizaron las reglas estructurales de los system prompts (tanto en interactive como en autonomous de prompt_builder.py) agregando la obligación de invocar sequentialthinking o plan_update para debatir estratégicamente las fases del ataque y comprobar la compleción de objetivos antes de proseguir a una nueva fase.