Skip to content

Commit dec5583

Browse files
author
bigkali
committed
feat: add persistent negative experience cache (Sovereign Agent v1)
1 parent 411d58e commit dec5583

2 files changed

Lines changed: 195 additions & 32 deletions

File tree

agent_cache.py

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import json
2+
import hashlib
3+
import os
4+
import logging
5+
from datetime import datetime
6+
7+
# ============================================================
8+
# 🧠 PERSISTENT NEGATIVE EXPERIENCE CACHE
9+
# PenMaster Security — Sovereign Agent Layer v1
10+
#
11+
# Analogy: a veteran soldier's scar tissue.
12+
# The agent remembers every failed approach across ALL sessions.
13+
# Try once → fail → retry once more.
14+
# Fail twice → permanently blacklisted. Never wasted on again.
15+
# ============================================================
16+
17+
CACHE_DIR = "/home/bigkali/security-agent"
18+
CACHE_FILE = os.path.join(CACHE_DIR, "failure_cache.json")
19+
20+
log = logging.getLogger("agent")
21+
22+
23+
class NegativeCache:
24+
"""
25+
Persistent cross-session failure memory.
26+
27+
Structure of cache.json:
28+
{
29+
"<fingerprint>": {
30+
"tool": "run_hydra",
31+
"summary": "hydra | target=192.168.64.3 service=ftp",
32+
"attempts": 2,
33+
"permanently_blocked": true,
34+
"first_seen": "2025-06-08 14:32:01",
35+
"last_seen": "2025-06-08 14:45:12",
36+
"reason": "stdout empty, status failed"
37+
},
38+
...
39+
}
40+
"""
41+
42+
def __init__(self):
43+
os.makedirs(CACHE_DIR, exist_ok=True)
44+
self._cache = self._load()
45+
log.info(f"[MEMORY] 🧠 Negative cache loaded — {len(self._cache)} blocked fingerprints")
46+
47+
# ----------------------------------------------------------
48+
# Internal helpers
49+
# ----------------------------------------------------------
50+
51+
def _load(self):
52+
if os.path.exists(CACHE_FILE):
53+
try:
54+
with open(CACHE_FILE, "r") as f:
55+
return json.load(f)
56+
except Exception:
57+
log.warning("[MEMORY] 😤 Cache file corrupt — starting fresh")
58+
return {}
59+
return {}
60+
61+
def _save(self):
62+
try:
63+
with open(CACHE_FILE, "w") as f:
64+
json.dump(self._cache, f, indent=2)
65+
except Exception as e:
66+
log.error(f"[ERROR] 😭🔥 Cache save failed: {e}")
67+
68+
def _fingerprint(self, step: dict) -> str:
69+
"""
70+
Stable hash of the tool call so identical attempts
71+
match across sessions regardless of field ordering.
72+
73+
We exclude fields that carry no semantic meaning
74+
(e.g. internal timestamps injected by wrappers).
75+
"""
76+
relevant = {k: v for k, v in step.items() if k != "_meta"}
77+
canonical = json.dumps(relevant, sort_keys=True)
78+
return hashlib.sha256(canonical.encode()).hexdigest()[:16]
79+
80+
def _summary(self, step: dict) -> str:
81+
tool = step.get("tool", "unknown")
82+
params = " | ".join(f"{k}={v}" for k, v in step.items() if k != "tool")
83+
return f"{tool} | {params}"
84+
85+
# ----------------------------------------------------------
86+
# Public API
87+
# ----------------------------------------------------------
88+
89+
def should_attempt(self, step: dict) -> bool:
90+
"""
91+
Returns True → go ahead, attempt this tool call.
92+
Returns False → permanently blocked, skip it entirely.
93+
94+
Call this BEFORE execute_step().
95+
"""
96+
fp = self._fingerprint(step)
97+
entry = self._cache.get(fp)
98+
if entry and entry.get("permanently_blocked"):
99+
log.warning(
100+
f"[MEMORY] 🚫 BLOCKED (seen {entry['attempts']}x) → {entry['summary']}"
101+
)
102+
return False
103+
return True
104+
105+
def record_failure(self, step: dict, reason: str = ""):
106+
"""
107+
Record one failed attempt.
108+
After 2 failures the fingerprint is permanently blocked.
109+
110+
Call this after a failed execute_step().
111+
"""
112+
fp = self._fingerprint(step)
113+
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
114+
115+
if fp not in self._cache:
116+
self._cache[fp] = {
117+
"tool": step.get("tool", "unknown"),
118+
"summary": self._summary(step),
119+
"attempts": 0,
120+
"permanently_blocked": False,
121+
"first_seen": now,
122+
"last_seen": now,
123+
"reason": reason,
124+
}
125+
126+
entry = self._cache[fp]
127+
entry["attempts"] += 1
128+
entry["last_seen"] = now
129+
if reason:
130+
entry["reason"] = reason
131+
132+
if entry["attempts"] >= 2:
133+
entry["permanently_blocked"] = True
134+
log.warning(
135+
f"[MEMORY] ☠️ PERMANENTLY BLOCKED after {entry['attempts']} failures → {entry['summary']}"
136+
)
137+
else:
138+
log.info(
139+
f"[MEMORY] 📝 Failure #{entry['attempts']} recorded (1 retry left) → {entry['summary']}"
140+
)
141+
142+
self._save()
143+
144+
def record_success(self, step: dict):
145+
"""
146+
If a previously-failed fingerprint suddenly works
147+
(e.g. different target context), clear its block.
148+
Uncommon but fair.
149+
"""
150+
fp = self._fingerprint(step)
151+
if fp in self._cache:
152+
log.info(f"[MEMORY] ✅ Clearing prior failure record — tool succeeded: {self._summary(step)}")
153+
del self._cache[fp]
154+
self._save()
155+
156+
def stats(self) -> dict:
157+
total = len(self._cache)
158+
blocked = sum(1 for e in self._cache.values() if e.get("permanently_blocked"))
159+
pending = total - blocked
160+
return {
161+
"total_fingerprints": total,
162+
"permanently_blocked": blocked,
163+
"one_strike_pending": pending,
164+
}
165+
166+
def dump(self):
167+
"""Pretty-print the full cache to the log — useful for debugging."""
168+
log.info(f"[MEMORY] 🧠 Cache dump ({len(self._cache)} entries):")
169+
for fp, entry in self._cache.items():
170+
status = "☠️ BLOCKED" if entry["permanently_blocked"] else f"⚠️ {entry['attempts']} attempt(s)"
171+
log.info(f" [{fp}] {status}{entry['summary']}")

agent_loop.py

Lines changed: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@
44
import re
55
import os
66
from datetime import datetime
7-
8-
# ============================================================
9-
# 🚀 LOGGING SETUP - Human Readable Session Logs
10-
# ============================================================
7+
from agent_cache import NegativeCache
118

129
LOG_DIR = "/home/bigkali/security-agent/logs"
1310
os.makedirs(LOG_DIR, exist_ok=True)
@@ -67,10 +64,6 @@ def setup_logger():
6764
log.info(f"[START] SECURITY AGENT SESSION {SESSION_ID}")
6865
log.info(f"[FILE] Log file: {LOG_FILE}")
6966

70-
# ============================================================
71-
# ⚙️ CONFIG
72-
# ============================================================
73-
7467
OLLAMA_URL = "http://192.168.0.39:1234/v1/chat/completions"
7568
MCP_URL = "http://localhost:8000"
7669

@@ -115,11 +108,6 @@ def setup_logger():
115108
NO explanations. NO markdown. ONLY JSON."""
116109

117110

118-
# ============================================================
119-
# 🤖 MODEL + TOOL EXECUTION
120-
# ============================================================
121-
122-
123111
class AgentMemory:
124112
def __init__(self):
125113
self.open_ports = []
@@ -222,10 +210,6 @@ def execute_step(step):
222210
log.error(f"[ERROR] 😭🔥 {tool} exception: {e}")
223211
return "", False
224212

225-
# ============================================================
226-
# 🔍 RECON + ⚔️ ATTACK
227-
# ============================================================
228-
229213
def run_recon(target, memory):
230214
log.info(f"[SCAN] Starting recon on {target}")
231215
goal = f"Scan {target} with masscan then nmap to find all open ports and services. JSON only."
@@ -240,7 +224,7 @@ def run_recon(target, memory):
240224
else:
241225
log.warning(f"[SCAN] 😤💀 No ports found in output")
242226

243-
def run_attack_loop(target, memory):
227+
def run_attack_loop(target, memory, cache=None):
244228
log.info(f"[ATTACK] Starting attack loop on {target}")
245229
while memory.has_untried_ports():
246230
port = memory.next_untried_port()
@@ -254,10 +238,21 @@ def run_attack_loop(target, memory):
254238
continue
255239
success = False
256240
for step in chain:
241+
# ── Negative cache gate ──────────────────────────────
242+
if cache and not cache.should_attempt(step):
243+
log.warning(f"[MEMORY] 🚫 Skipping permanently blocked step: {step.get('tool')}")
244+
continue
245+
# ────────────────────────────────────────────────────
257246
output, ok = execute_step(step)
258247
if ok and output and any(x in output.lower() for x in ["password", "login", "session", "shell", "success", "found", "valid"]):
259248
success = True
249+
if cache:
250+
cache.record_success(step)
260251
memory.add_finding(port, step.get("tool"), output[:2000])
252+
elif not ok:
253+
if cache:
254+
reason = f"tool={step.get('tool')} port={port} output_empty={not bool(output)}"
255+
cache.record_failure(step, reason=reason)
261256
memory.mark_tried(port, success=success)
262257
log.info(f"[ATTACK] Attack loop complete")
263258
summary = memory.summary()
@@ -269,24 +264,27 @@ def run_attack_loop(target, memory):
269264

270265
def run_full_engagement(target):
271266
memory = AgentMemory()
267+
cache = NegativeCache()
272268
log.info(f"[ENGAGE] 💣 Full engagement started on {target}")
273269
run_recon(target, memory)
274270
if memory.open_ports:
275-
run_attack_loop(target, memory)
271+
run_attack_loop(target, memory, cache)
276272
else:
277273
log.warning(f"[FAIL] 😤💀 No open ports found — aborting engagement")
278274
return memory
279275

280-
# ============================================================
281-
# 🎯 MAIN
282-
# ============================================================
283-
284-
def execute_chain(chain):
276+
def execute_chain(chain, cache=None):
285277
for i, step in enumerate(chain, 1):
286278
log.info(f"[CHAIN] 🔗 Step {i} of {len(chain)}: {step.get('tool')}")
287-
execute_step(step)
279+
if cache and not cache.should_attempt(step):
280+
log.warning(f"[MEMORY] 🚫 Skipping permanently blocked step: {step.get('tool')}")
281+
continue
282+
output, ok = execute_step(step)
283+
if not ok and cache:
284+
cache.record_failure(step, reason=f"manual chain failure, step {i}")
288285

289286
def main():
287+
cache = NegativeCache()
290288
log.info("[START] 🚀 AUTONOMOUS SECURITY AGENT ONLINE")
291289
print("=" * 60)
292290
print("⚔️ AUTONOMOUS SECURITY AGENT")
@@ -303,7 +301,6 @@ def main():
303301
goal = input(">>> ").strip()
304302
if goal.lower() == "exit":
305303
log.info("[START] Agent shutdown. Goodbye! 👋")
306-
307304
break
308305
if not goal:
309306
continue
@@ -316,7 +313,7 @@ def main():
316313
data = call_model(goal)
317314
chain = data.get("chain", [])
318315
if chain:
319-
execute_chain(chain)
316+
execute_chain(chain, cache=cache)
320317
else:
321318
log.warning("[FAIL] 😤💀 No tool chain generated")
322319
except KeyboardInterrupt:
@@ -330,8 +327,3 @@ def main():
330327

331328
if __name__ == "__main__":
332329
main()
333-
334-
# ============================================================
335-
# 🧠 AGENT MEMORY
336-
# ============================================================
337-

0 commit comments

Comments
 (0)