Skip to content
22 changes: 15 additions & 7 deletions engraphis/classic_assets/dashboard.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 18 additions & 3 deletions engraphis/core/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
ResolutionOp,
resolve,
)
from engraphis.core.secrets import reject_secrets
from engraphis.core.secrets import redact_secrets as _redact_secrets, reject_secrets
from engraphis.core.store import (
Store,
_is_memory_database_path,
Expand Down Expand Up @@ -829,7 +829,8 @@ def remember(self, content: str, *, workspace_id: str, repo_id: Optional[str] =
valid_from: Optional[float] = None, resolve_conflicts: bool = True,
candidate_k: int = 5, subject_key: str = "", claim_kind: str = "",
_trusted_graph_keys: Optional[frozenset] = None,
_transactional_finalizer: Optional[Callable[[str], None]] = None) -> str:
_transactional_finalizer: Optional[Callable[[str], None]] = None,
redact_secrets: bool = False) -> str:
"""Store one memory. Returns the resulting record id: a new id for ADD/
INVALIDATE/quarantine, or the existing memory's id if this was resolved as a
NOOP (near-duplicate). See ``remember_with_resolution`` for decision detail.
Expand All @@ -842,6 +843,7 @@ def remember(self, content: str, *, workspace_id: str, repo_id: Optional[str] =
candidate_k=candidate_k, subject_key=subject_key, claim_kind=claim_kind,
_trusted_graph_keys=_trusted_graph_keys,
_transactional_finalizer=_transactional_finalizer,
redact_secrets=redact_secrets,
)["id"]

def remember_with_resolution(self, content: str, *, workspace_id: str,
Expand All @@ -855,7 +857,8 @@ def remember_with_resolution(self, content: str, *, workspace_id: str,
_trusted_graph_keys: Optional[frozenset] = None,
_approval_override: bool = False,
_transactional_finalizer: Optional[Callable[[str], None]] = None,
extra_neighbors: Optional[list] = None) -> dict:
extra_neighbors: Optional[list] = None,
redact_secrets: bool = False) -> dict:
"""Store one memory with deterministic conflict resolution.

Returns ``{"id", "op", ...}`` where ``op`` is one of:
Expand All @@ -871,6 +874,10 @@ def remember_with_resolution(self, content: str, *, workspace_id: str,
* ``"quarantined"`` — an explicitly untrusted payload matched the deterministic
poisoning policy; retained only for governed historical inspection.
"""
if redact_secrets:
content = _redact_secrets(content)
if title:
title = _redact_secrets(title)
# Reject credentials before embedding, conflict resolution, graph extraction, or
# any SQLite mirror sees them. Store.add_memory repeats this for direct callers.
reject_secrets((("title", title), ("content", content), ("keywords", keywords),
Expand Down Expand Up @@ -1576,6 +1583,7 @@ def _resolve_and_store(self, content: str, *, text: str, vec: Optional[np.ndarra
embedding=None if poisoning.quarantined else vec,
)
invalidating = decision is not None and decision.op == ResolutionOp.INVALIDATE
predecessor: Optional[MemoryRecord] = None
try:
# A replacement and the predecessor interval it closes are one authoritative
# state transition. Portable vector/FTS mirrors participate in the same
Expand Down Expand Up @@ -1725,6 +1733,13 @@ def _resolve_and_store(self, content: str, *, text: str, vec: Optional[np.ndarra
) if trusted_write else []
out = {"id": mid, "op": "invalidate", "superseded": [decision.target_id],
"reason": decision.reason}
if predecessor is not None:
out["superseded_detail"] = {
"id": predecessor.id,
"content_preview": predecessor.content[:120],
"stability_days": predecessor.stability,
"access_count": predecessor.access_count,
}
if linked:
out["linked"] = linked
return out
Expand Down
2 changes: 2 additions & 0 deletions engraphis/core/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,11 +245,13 @@ class SearchFilter:
# Appended after every 1.x field so positional construction remains compatible.
valid_at: Optional[float] = None
known_at: Optional[float] = None
modified_since: Optional[float] = None

def __post_init__(self) -> None:
self.as_of = _finite_timestamp(self.as_of, "as_of")
self.valid_at = _finite_timestamp(self.valid_at, "valid_at")
self.known_at = _finite_timestamp(self.known_at, "known_at")
self.modified_since = _finite_timestamp(self.modified_since, "modified_since")
if self.as_of is not None and self.valid_at is not None:
if self.as_of != self.valid_at:
raise ValueError("as_of and valid_at must match when both are supplied")
Expand Down
4 changes: 2 additions & 2 deletions engraphis/core/retrieval_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
CANDIDATE_DEPTH_MODES = frozenset({"fixed", "adaptive"})

_CODE_RE = re.compile(
r"(?:\w+[./\\])+\w+|::|->|\b(?:class|def|function|import|module)\b|"
r"(?:\w+[./\\])+\w+|::|->|\b(?:class|def|function|import|interface|module|struct)\b|"
r"\b[A-Za-z_]\w*\([^)]*\)",
re.IGNORECASE,
)
_GRAPH_RE = re.compile(
r"\b(?:calls?|causes?|depends?|impact|path|related|relationship|why)\b",
r"\b(?:calls?|causes?|connected|connections?|depends?|impact|path|related|relationship|superseded|supersedes|why)\b",
re.IGNORECASE,
)
_LEXICAL_RE = re.compile(
Expand Down
130 changes: 122 additions & 8 deletions engraphis/core/secrets.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,85 @@ def __init__(self, field: str, kind: str) -> None:
# safe to block without a caller-supplied label. Assignment detection below catches
# generic credentials (including private deployment tokens) only when the field name
# explicitly says that it is a credential.
_PATTERNS: tuple[tuple[str, re.Pattern[str]], ...] = (
("private key", re.compile(r"-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----", re.I)),
_PEM_HEADER = "-----begin private key-----"
_PEM_HEADER_ALT = "-----begin "


def _contains_pem_header(value: str) -> bool:
"""O(N) PEM-header detection without regex backtracking.

Equivalent to ``re.search(r"-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----", re.I)``
but implemented as a case-normalized anchor scan: polynomial-ReDoS analysis
of the optional ``[A-Z0-9]+`` group is moot when no regex engine is involved.
Recognizes the canonical header and ``-----BEGIN X PRIVATE KEY-----`` variants
(RSA/EC/OPENSSH/ENCRYPTED/…).
"""
lowered = value.casefold()
start = 0
limit = len(lowered)
header_exhausted = False
header_pos = -1
while start <= limit:
# Walk both needles forward without ever rescanning earlier positions:
# whichever of the two anchors occurs first is the only candidate that
# can start a header there, so advance past it after inspection.
# ``find`` results are monotone in ``start``: a miss is final for every
# later start, and a hit at ``h`` is the answer for every start <= h.
# Caching both avoids re-scanning to a far-away hit (or to end-of-string
# on a miss) at every anchor — O(N) total instead of O(N*M).
if not header_exhausted:
if header_pos == -1 or start > header_pos:
header_pos = lowered.find(_PEM_HEADER, start)
if header_pos == -1:
header_exhausted = True
header = header_pos if (header_pos != -1 and start <= header_pos) else -1
anchor = lowered.find(_PEM_HEADER_ALT, start)
if header != -1 and (anchor == -1 or header <= anchor):
return True
if anchor == -1:
return False
tail_start = anchor + len(_PEM_HEADER_ALT)
if header == tail_start:
return True
tail = lowered[tail_start:tail_start + 40]
for token in ("rsa ", "ec ", "dsa ", "openssh ", "encrypted ",
"pgp ", "pkcs8 "):
if tail.startswith(token) and tail[len(token):].startswith(
"private key-----"):
return True
start = tail_start
return False


class _PEMHeaderPattern:
"""Minimal stand-in for the removed PEM regex: a linear-time ``search`` and ``sub``."""

@staticmethod
def search(value: str) -> _PEMMatch | None:
return _PEMMatch(True) if _contains_pem_header(value) else None

@staticmethod
def sub(repl: str, string: str, count: int = 0) -> str:
return _redact_pem(string)


class _PEMMatch:
"""Truthiness-compatible stand-in for a regex match object."""

__slots__ = ("_ok",)

def __init__(self, ok: bool) -> None:
self._ok = ok

def __bool__(self) -> bool:
return self._ok

def group(self, *args: object) -> str:
return _PEM_HEADER


_PATTERNS: tuple[tuple[str, Any], ...] = (
("private key", _PEMHeaderPattern()),
("AWS access key", re.compile(r"\b(?:AKIA|ASIA)[A-Z0-9]{16}\b")),
("GitHub token", re.compile(r"\bgh[pousr]_[A-Za-z0-9_]{20,}\b")),
("GitLab token", re.compile(r"\bglpat-[A-Za-z0-9_-]{20,}\b")),
Expand Down Expand Up @@ -73,14 +150,47 @@ def __init__(self, field: str, kind: str) -> None:
""",
)
_REDACTION = re.compile(r"^<?(?:redacted|removed|withheld|not[_ -]?set)>?$", re.I)
_PEM_BLOCK = re.compile(
r"-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----[\s\S]*?"
r"-----END(?: [A-Z0-9]+)? PRIVATE KEY-----",
re.I,
)
_REDACTED = "<redacted>"


def _redact_pem(text: str) -> str:
"""Safely redact PEM private key blocks in O(N) linear time without regex backtracking."""
out: list[str] = []
pos = 0
upper = text.upper()
while True:
begin = upper.find("-----BEGIN", pos)
if begin == -1:
out.append(text[pos:])
break
header_end = upper.find("-----", begin + 10)
if header_end == -1:
out.append(text[pos:])
break
header = upper[begin:header_end + 5]
if "PRIVATE KEY" not in header:
out.append(text[pos:header_end + 5])
pos = header_end + 5
continue
end = upper.find("-----END", header_end + 5)
if end == -1:
out.append(text[pos:])
break
footer_end = upper.find("-----", end + 8)
if footer_end == -1:
out.append(text[pos:])
break
footer = upper[end:footer_end + 5]
if "PRIVATE KEY" not in footer:
out.append(text[pos:footer_end + 5])
pos = footer_end + 5
continue
out.append(text[pos:begin])
out.append(_REDACTED)
pos = footer_end + 5
return "".join(out)


def _text(value: Any) -> str:
if isinstance(value, str):
return value
Expand Down Expand Up @@ -157,8 +267,12 @@ def redact_secrets(text: str) -> str:
"""
if not isinstance(text, str) or not text:
return text
safe = _PEM_BLOCK.sub(_REDACTED, text)
safe = _redact_pem(text)
# PEM blocks are handled by the linear-time ``_redact_pem`` above; the
# detection entry in ``_PATTERNS`` is find-based and has no ``sub``.
for _kind, pattern in _PATTERNS:
if isinstance(pattern, _PEMHeaderPattern):
continue
safe = pattern.sub(_REDACTED, safe)
safe = _DSN.sub(_REDACTED, safe)
safe = _ASSIGNMENT.sub(_REDACTED, safe)
Expand Down
7 changes: 7 additions & 0 deletions engraphis/core/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,10 @@ def memory_matches_filter(rec: MemoryRecord, flt: Optional[SearchFilter], *,
return False
if flt.mtypes is not None and rec.mtype not in flt.mtypes:
return False
if flt.modified_since is not None:
mod_ts = max(rec.ingested_at or 0.0, getattr(rec, "updated_at", 0.0) or 0.0)
if mod_ts < flt.modified_since:
return False
if include_invalid:
return True
valid_at, known_at = _temporal_anchors(flt, valid_at=at)
Expand Down Expand Up @@ -9471,6 +9475,9 @@ def _where(self, flt: Optional[SearchFilter], include_invalid: bool,
marks = ",".join("?" for _ in flt.mtypes)
where.append(f"{p}mtype IN ({marks})")
params.extend(_enum(m) for m in flt.mtypes)
if flt.modified_since is not None:
where.append(f"{p}ingested_at>=?")
params.append(flt.modified_since)
if not include_invalid:
valid_at, known_at = _temporal_anchors(flt)
where.append(f"({p}valid_from IS NULL OR {p}valid_from<=?)")
Expand Down
31 changes: 27 additions & 4 deletions engraphis/dashboard_assets/engraphis-graph-every.js
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,26 @@
gl.bufferData(gl.ARRAY_BUFFER, visibility, gl.STATIC_DRAW);
state.edgeVertexCount = links * 2;
}
function uploadEdgePositions() {
if (!gl || !edgeProgram || !state.totalLinks || !state.edgeSources) return;
const links = state.totalLinks;
if (!state.edgePositions || state.edgePositions.length !== links * 4) {
state.edgePositions = new Float32Array(links * 4);
}
const positions = state.edgePositions;
const pos = state.positions;
const sources = state.edgeSources, targets = state.edgeTargets;
for (let index = 0; index < links; index += 1) {
const source = sources[index], target = targets[index];
const s = source * 2, t = target * 2, p = index * 4;
positions[p] = pos[s];
positions[p + 1] = pos[s + 1];
positions[p + 2] = pos[t];
positions[p + 3] = pos[t + 1];
}
gl.bindBuffer(gl.ARRAY_BUFFER, edgeBuffers.position);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.DYNAMIC_DRAW);
}

/* ── Community regions: soft district outlines that make the scene read as a map ── */
function computeCommunityRegions() {
Expand Down Expand Up @@ -793,11 +813,12 @@
const top = Math.floor((point[1] - 6 - fontPx / 2) / cell), bottom = Math.ceil((point[1] - 6 + fontPx / 2) / cell);
for (let gx = left; gx <= right; gx += 1) {
for (let gy = top; gy <= bottom; gy += 1) {
if (occupied.has(`${gx}:${gy}`)) return;
const key = ((gx + 32768) << 16) | ((gy + 32768) & 0xFFFF);
if (occupied.has(key)) return;
}
}
for (let gx = left; gx <= right; gx += 1) {
for (let gy = top; gy <= bottom; gy += 1) occupied.add(`${gx}:${gy}`);
for (let gy = top; gy <= bottom; gy += 1) occupied.add(((gx + 32768) << 16) | ((gy + 32768) & 0xFFFF));
}
labelContext.fillText(text, point[0] + 6, point[1] - 6);
state.labelLayout.push({ text, x: point[0] + 6, y: point[1] - 6 });
Expand Down Expand Up @@ -956,7 +977,7 @@
}
});
uploadNodePositions();
uploadEdges();
if (state.edgeVertexCount) uploadEdgePositions(); else uploadEdges();
state.pickDirty = true;
state.lastLabelKey = '';
if (doFit) fit(); else camera();
Expand Down Expand Up @@ -1055,7 +1076,9 @@
}
if (message.type === 'progress') {
state.layoutPending = Number(message.pass) < Number(message.total);
stats({ layoutPending: state.layoutPending });
if (Number(message.pass) % 4 === 0 || !state.layoutPending) {
stats({ layoutPending: state.layoutPending });
}
return;
}
if (message.type === 'layout') {
Expand Down
Loading