● Zion Boggan
repos/Cyclops V2/memory/memory.py
zionboggan.com ↗
203 lines · python
History for this file →
1
"""
2
Cyclops v2 - unified memory layer.
3
 
4
Three stores, each with a distinct job (no overlap, all actually used):
5
  - KuzuDB graph  : structural knowledge + state. Targets, archetypes, sinks,
6
                    dataflow hypotheses, oracle proofs, findings, and CLEARED AXES.
7
                    This is what stops drift/tunnel-vision: the agent queries
8
                    "what have I already cleared on this target?" before acting.
9
  - Chroma        : semantic recall over prior reasoning / methods / failed attempts.
10
  - Redis         : working memory - the live job queue + content-addressed analysis
11
                    cache (never re-CPG the same commit) + per-run scratch.
12
 
13
Design rule (anti-slop): a FINDING node may only be created once an oracle PROOF
14
node with a real execution artifact is attached. A dataflow HYPOTHESIS is not a
15
finding. memory.promote_to_finding() enforces this.
16
"""
17
import os, sys, json, time, hashlib
18
import kuzu
19
import redis as redislib
20
 
21
CYCLOPS_HOME = os.environ.get("CYCLOPS_HOME") or os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
22
GRAPH_PATH   = os.path.join(CYCLOPS_HOME, "memory", "graph.kuzu")
23
REDIS_URL    = os.environ.get("CYCLOPS_REDIS", "redis://127.0.0.1:6379/3")
24
 
25
SCHEMA = [
26
    """CREATE NODE TABLE IF NOT EXISTS Archetype(
27
         name STRING, description STRING, PRIMARY KEY(name))""",
28
    """CREATE NODE TABLE IF NOT EXISTS Target(
29
         id STRING, repo STRING, archetype STRING, lang STRING,
30
         commit STRING, released_at STRING, active BOOLEAN, status STRING,
31
         first_seen STRING, PRIMARY KEY(id))""",
32
    """CREATE NODE TABLE IF NOT EXISTS SinkClass(
33
         name STRING, cwe STRING, impact_family STRING, PRIMARY KEY(name))""",
34
    """CREATE NODE TABLE IF NOT EXISTS Hypothesis(
35
         id STRING, target STRING, sink_class STRING, file STRING, line INT64,
36
         source STRING, sink STRING, tool STRING, created_at STRING,
37
         status STRING, PRIMARY KEY(id))""",
38
    """CREATE NODE TABLE IF NOT EXISTS Proof(
39
         id STRING, hypothesis STRING, label STRING, oracle STRING,
40
         artifact_path STRING, summary STRING, created_at STRING,
41
         PRIMARY KEY(id))""",
42
    """CREATE NODE TABLE IF NOT EXISTS Finding(
43
         id STRING, target STRING, impact_family STRING, severity STRING,
44
         title STRING, novel BOOLEAN, cve_checked BOOLEAN, created_at STRING,
45
         PRIMARY KEY(id))""",
46
    """CREATE NODE TABLE IF NOT EXISTS ClearedAxis(
47
         id STRING, target STRING, archetype STRING, axis STRING,
48
         result STRING, reason STRING, created_at STRING, PRIMARY KEY(id))""",
49
    "CREATE REL TABLE IF NOT EXISTS OF_ARCHETYPE(FROM Target TO Archetype)",
50
    "CREATE REL TABLE IF NOT EXISTS HAS_HYPOTHESIS(FROM Target TO Hypothesis)",
51
    "CREATE REL TABLE IF NOT EXISTS TARGETS_SINK(FROM Hypothesis TO SinkClass)",
52
    "CREATE REL TABLE IF NOT EXISTS PROVEN_BY(FROM Hypothesis TO Proof)",
53
    "CREATE REL TABLE IF NOT EXISTS YIELDED(FROM Proof TO Finding)",
54
    "CREATE REL TABLE IF NOT EXISTS CLEARED_ON(FROM Target TO ClearedAxis)",
55
]
56
 
57
def _now():
58
    return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
59
 
60
def _hash(*parts):
61
    return hashlib.sha1("|".join(str(p) for p in parts).encode()).hexdigest()[:16]
62
 
63
 
64
class Memory:
65
    def __init__(self):
66
        self.db = kuzu.Database(GRAPH_PATH)
67
        self.g = kuzu.Connection(self.db)
68
        for stmt in SCHEMA:
69
            self.g.execute(stmt)
70
        self.r = redislib.from_url(REDIS_URL, decode_responses=True)
71
 
72
    def _exec(self, q, params=None):
73
        return self.g.execute(q, parameters=params or {})
74
 
75
    def upsert_archetype(self, name, description=""):
76
        self._exec("MERGE (a:Archetype {name:$n}) SET a.description=$d",
77
                   {"n": name, "d": description})
78
 
79
    def upsert_target(self, repo, archetype, lang="", commit="", released_at="",
80
                      active=True, status="DISCOVERED"):
81
        tid = _hash(repo, commit)
82
        self._exec("""MERGE (t:Target {id:$id})
83
                      SET t.repo=$repo, t.archetype=$arch, t.lang=$lang,
84
                          t.commit=$commit, t.released_at=$rel, t.active=$active,
85
                          t.status=$status, t.first_seen=coalesce(t.first_seen,$now)""",
86
                   {"id": tid, "repo": repo, "arch": archetype, "lang": lang,
87
                    "commit": commit, "rel": released_at, "active": active,
88
                    "status": status, "now": _now()})
89
        self.upsert_archetype(archetype)
90
        self._exec("""MATCH (t:Target {id:$id}),(a:Archetype {name:$arch})
91
                      MERGE (t)-[:OF_ARCHETYPE]->(a)""", {"id": tid, "arch": archetype})
92
        return tid
93
 
94
    def add_hypothesis(self, target_id, sink_class, file, line, source, sink, tool):
95
        hid = _hash(target_id, file, line, sink, source)
96
        self._exec("""MERGE (h:Hypothesis {id:$id})
97
                      SET h.target=$t, h.sink_class=$sc, h.file=$f, h.line=$ln,
98
                          h.source=$src, h.sink=$snk, h.tool=$tool,
99
                          h.created_at=$now, h.status='REACHABLE'""",
100
                   {"id": hid, "t": target_id, "sc": sink_class, "f": file,
101
                    "ln": int(line), "src": source, "snk": sink, "tool": tool,
102
                    "now": _now()})
103
        self._exec("""MATCH (t:Target {id:$t}),(h:Hypothesis {id:$id})
104
                      MERGE (t)-[:HAS_HYPOTHESIS]->(h)""", {"t": target_id, "id": hid})
105
        return hid
106
 
107
    def add_proof(self, hypothesis_id, label, oracle, artifact_path, summary):
108
        """label in {SOURCE_CONFIRMED, REPLICA_CONFIRMED, LIVE_CANARY_CONFIRMED, DISPROVEN}"""
109
        pid = _hash(hypothesis_id, label, oracle, artifact_path)
110
        self._exec("""MERGE (p:Proof {id:$id})
111
                      SET p.hypothesis=$h, p.label=$lbl, p.oracle=$o,
112
                          p.artifact_path=$ap, p.summary=$s, p.created_at=$now""",
113
                   {"id": pid, "h": hypothesis_id, "lbl": label, "o": oracle,
114
                    "ap": artifact_path, "s": summary, "now": _now()})
115
        self._exec("""MATCH (h:Hypothesis {id:$h}),(p:Proof {id:$id})
116
                      MERGE (h)-[:PROVEN_BY]->(p)
117
                      SET h.status=$st""",
118
                   {"h": hypothesis_id, "id": pid,
119
                    "st": "DISPROVEN" if label == "DISPROVEN" else "PROVEN"})
120
        return pid
121
 
122
    def promote_to_finding(self, hypothesis_id, impact_family, severity, title,
123
                           novel=False, cve_checked=False):
124
        """ANTI-SLOP GATE: refuses unless a non-DISPROVEN Proof exists."""
125
        res = self._exec("""MATCH (h:Hypothesis {id:$h})-[:PROVEN_BY]->(p:Proof)
126
                            WHERE p.label <> 'DISPROVEN'
127
                            RETURN p.id, h.target LIMIT 1""", {"h": hypothesis_id})
128
        if not res.has_next():
129
            raise PermissionError(
130
                f"REFUSED: hypothesis {hypothesis_id} has no execution proof. "
131
                "A reachable dataflow path is NOT a finding. Run the oracle first.")
132
        row = res.get_next()
133
        pid, target = row[0], row[1]
134
        fid = _hash(hypothesis_id, impact_family, title)
135
        self._exec("""MERGE (f:Finding {id:$id})
136
                      SET f.target=$t, f.impact_family=$fam, f.severity=$sev,
137
                          f.title=$title, f.novel=$novel, f.cve_checked=$cc,
138
                          f.created_at=$now""",
139
                   {"id": fid, "t": target, "fam": impact_family, "sev": severity,
140
                    "title": title, "novel": novel, "cc": cve_checked, "now": _now()})
141
        self._exec("""MATCH (p:Proof {id:$p}),(f:Finding {id:$f})
142
                      MERGE (p)-[:YIELDED]->(f)""", {"p": pid, "f": fid})
143
        return fid
144
 
145
    def clear_axis(self, target_id, archetype, axis, result, reason=""):
146
        aid = _hash(target_id, axis)
147
        self._exec("""MERGE (c:ClearedAxis {id:$id})
148
                      SET c.target=$t, c.archetype=$arch, c.axis=$axis,
149
                          c.result=$res, c.reason=$reason, c.created_at=$now""",
150
                   {"id": aid, "t": target_id, "arch": archetype, "axis": axis,
151
                    "res": result, "reason": reason, "now": _now()})
152
        self._exec("""MATCH (t:Target {id:$t}),(c:ClearedAxis {id:$id})
153
                      MERGE (t)-[:CLEARED_ON]->(c)""", {"t": target_id, "id": aid})
154
        return aid
155
 
156
    def cleared_axes(self, target_id):
157
        res = self._exec("""MATCH (t:Target {id:$t})-[:CLEARED_ON]->(c:ClearedAxis)
158
                            RETURN c.axis, c.result, c.reason ORDER BY c.created_at""",
159
                         {"t": target_id})
160
        out = []
161
        while res.has_next():
162
            r = res.get_next(); out.append({"axis": r[0], "result": r[1], "reason": r[2]})
163
        return out
164
 
165
    def is_axis_cleared(self, target_id, axis):
166
        res = self._exec("""MATCH (t:Target {id:$t})-[:CLEARED_ON]->(c:ClearedAxis {axis:$a})
167
                            RETURN c.result LIMIT 1""", {"t": target_id, "a": axis})
168
        return res.get_next()[0] if res.has_next() else None
169
 
170
    def cache_get(self, key):
171
        return self.r.get(f"cyclops:cache:{key}")
172
 
173
    def cache_put(self, key, value, ttl=86400 * 30):
174
        self.r.set(f"cyclops:cache:{key}", value, ex=ttl)
175
 
176
    def analysis_done(self, target_id, phase):
177
        """content-addressed: never re-run the same phase on the same commit."""
178
        return self.r.sismember("cyclops:analysis_done", f"{target_id}:{phase}")
179
 
180
    def mark_analysis_done(self, target_id, phase):
181
        self.r.sadd("cyclops:analysis_done", f"{target_id}:{phase}")
182
 
183
    def enqueue(self, queue, job: dict):
184
        self.r.lpush(f"cyclops:q:{queue}", json.dumps(job))
185
 
186
    def dequeue(self, queue, timeout=5):
187
        item = self.r.brpop(f"cyclops:q:{queue}", timeout=timeout)
188
        return json.loads(item[1]) if item else None
189
 
190
    def stats(self):
191
        res = self._exec("""MATCH (n) RETURN label(n) AS l, count(*) AS c""")
192
        out = {}
193
        while res.has_next():
194
            r = res.get_next(); out[r[0]] = r[1]
195
        return out
196
 
197
 
198
if __name__ == "__main__":
199
    m = Memory()
200
    print("Cyclops v2 memory online.")
201
    print("graph:", GRAPH_PATH)
202
    print("redis:", REDIS_URL, "ping=", m.r.ping())
203
    print("node counts:", m.stats())