nexo-brain 7.23.12 → 7.23.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.23.12",
3
+ "version": "7.23.13",
4
4
  "description": "Local cognitive runtime for Claude Code \u2014 persistent memory, overnight learning, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
5
5
  "author": {
6
6
  "name": "NEXO Brain",
package/README.md CHANGED
@@ -18,7 +18,9 @@
18
18
 
19
19
  [Watch the overview video](https://nexo-brain.com/watch/) · [Watch on YouTube](https://www.youtube.com/watch?v=i2lkGhKyVqI) · [Open the infographic](https://nexo-brain.com/assets/nexo-brain-infographic-v5.png)
20
20
 
21
- Version `7.23.12` is the current packaged-runtime line. Patch over v7.23.11 - protected database recovery now repairs degraded Brain tables from backup without rolling back newer rows.
21
+ Version `7.23.13` is the current packaged-runtime line. Patch over v7.23.12 - release guardrails now audit publish workflows for masked failures and add minimal-delta coverage for punctual UI edits.
22
+
23
+ Previously in `7.23.12`: patch over v7.23.11 - protected database recovery now repairs degraded Brain tables from backup without rolling back newer rows.
22
24
 
23
25
  Previously in `7.23.11`: patch over v7.23.10 - older installed runtimes can update safely even when `cognitive_paths.py` has not been synced yet.
24
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.23.12",
3
+ "version": "7.23.13",
4
4
  "mcpName": "io.github.wazionapps/nexo",
5
5
  "description": "NEXO Brain — Shared brain for AI agents. Persistent memory, semantic RAG, natural forgetting, metacognitive guard, trust scoring, 150+ MCP tools. Works with Claude Code, Codex, Claude Desktop & any MCP client. 100% local, free.",
6
6
  "homepage": "https://nexo-brain.com",
File without changes
@@ -0,0 +1,267 @@
1
+ """Minimal-delta pre-mutation guardrail (NF-DS-C3E64B2B).
2
+
3
+ Purpose
4
+ -------
5
+ Block scope creep when the operator describes a *punctual* UI change
6
+ ("add this text", "change the size", "adjust the color") but the agent
7
+ proposes a diff that touches many unrelated lines.
8
+
9
+ How it plugs in
10
+ ---------------
11
+ Wire ``check`` from ``hook_guardrails.process_pre_tool_event`` BEFORE the
12
+ ``Edit`` / ``Write`` call reaches the model. The hook should:
13
+
14
+ 1. Call ``classify_request(prompt_text)`` to detect punctual UI verbs.
15
+ 2. If punctual, capture the *target file* + *proposed new_string* from the
16
+ pending tool payload.
17
+ 3. Read ``read_file_history(path)`` to keep the prior context available
18
+ (it returns the last 5 commits + current text so the agent can reason
19
+ in bullet-by-bullet form when replying).
20
+ 4. Call ``check_diff(prompt_text, old_text, new_text)`` and act on the
21
+ returned ``GuardDecision``:
22
+
23
+ * ``decision == "allow"`` → let the tool through, no annotation.
24
+ * ``decision == "warn"`` → let it through but tell the agent in the
25
+ response payload how many extra lines it touched.
26
+ * ``decision == "block"`` → deny the tool with the included reason.
27
+ The agent must reply with a bullet-by-bullet diff against the prior
28
+ state and request explicit confirmation before retrying.
29
+
30
+ Why headless-friendly
31
+ ---------------------
32
+ This module is *pure*. No filesystem, no subprocess, no MCP call. The hook
33
+ layer wires it in. That keeps the gate testable in isolation and means a
34
+ wrong tuning here cannot brick the pre-tool pipeline.
35
+
36
+ Origin: Deep Sleep followup NF-DS-C3E64B2B (scope creep in punctual visual
37
+ changes).
38
+ """
39
+ from __future__ import annotations
40
+
41
+ import difflib
42
+ import re
43
+ import subprocess
44
+ from dataclasses import dataclass
45
+ from pathlib import Path
46
+ from typing import Iterable
47
+
48
+ # Verbs that signal a *punctual* UI change. Matched case-insensitive against
49
+ # the full request text. Spanish terms are intentionally included because the
50
+ # operator often reports UI issues in Spanish; output remains English.
51
+ PUNCTUAL_VERBS: tuple[str, ...] = (
52
+ "añade",
53
+ "anade",
54
+ "agrega",
55
+ "añadir",
56
+ "agrega el texto",
57
+ "cambia el texto",
58
+ "cambia el color",
59
+ "cambia el tamaño",
60
+ "cambia el tamano",
61
+ "ajusta el tamaño",
62
+ "ajusta el tamano",
63
+ "ajusta el padding",
64
+ "ajusta el margen",
65
+ "ajusta el color",
66
+ "renombra",
67
+ "rename",
68
+ "tweak",
69
+ "change the text",
70
+ "change the color",
71
+ "adjust the size",
72
+ "fix the label",
73
+ "fix the wording",
74
+ "add the text",
75
+ "swap the icon",
76
+ )
77
+
78
+ # File extensions considered UI surfaces. Anything else short-circuits to
79
+ # "not a UI mutation" so we never block server code on accident.
80
+ UI_EXTENSIONS: frozenset[str] = frozenset({
81
+ ".tsx", ".jsx", ".ts", ".js", ".vue", ".svelte", ".astro",
82
+ ".html", ".css", ".scss", ".sass", ".less",
83
+ ".liquid", ".njk", ".hbs",
84
+ })
85
+
86
+ # Default threshold for "unrelated lines". Tuned conservatively — a real UI
87
+ # tweak usually changes 1-3 contiguous lines. Anything > THRESHOLD blocks.
88
+ DEFAULT_THRESHOLD = 8
89
+
90
+
91
+ @dataclass(frozen=True)
92
+ class GuardDecision:
93
+ decision: str # "allow" | "warn" | "block"
94
+ matched_verb: str | None
95
+ changed_lines: int
96
+ threshold: int
97
+ reason: str
98
+
99
+ def to_payload(self) -> dict:
100
+ return {
101
+ "guard": "minimal-delta",
102
+ "decision": self.decision,
103
+ "matched_verb": self.matched_verb,
104
+ "changed_lines": self.changed_lines,
105
+ "threshold": self.threshold,
106
+ "reason": self.reason,
107
+ }
108
+
109
+
110
+ def classify_request(prompt_text: str) -> str | None:
111
+ """Return the matched punctual verb (lowercase) or ``None``."""
112
+ if not prompt_text:
113
+ return None
114
+ haystack = prompt_text.lower()
115
+ for verb in PUNCTUAL_VERBS:
116
+ if verb in haystack:
117
+ return verb
118
+ return None
119
+
120
+
121
+ def is_ui_path(path: str | Path) -> bool:
122
+ if not path:
123
+ return False
124
+ suffix = Path(str(path)).suffix.lower()
125
+ return suffix in UI_EXTENSIONS
126
+
127
+
128
+ def count_changed_lines(old_text: str, new_text: str) -> int:
129
+ """Count *non-context* diff lines (+/− only)."""
130
+ if old_text == new_text:
131
+ return 0
132
+ old_lines = old_text.splitlines()
133
+ new_lines = new_text.splitlines()
134
+ diff = difflib.unified_diff(old_lines, new_lines, n=0, lineterm="")
135
+ count = 0
136
+ for line in diff:
137
+ if line.startswith(("+++", "---", "@@")):
138
+ continue
139
+ if line.startswith(("+", "-")):
140
+ count += 1
141
+ return count
142
+
143
+
144
+ def check_diff(
145
+ prompt_text: str,
146
+ old_text: str,
147
+ new_text: str,
148
+ target_path: str | Path = "",
149
+ *,
150
+ threshold: int = DEFAULT_THRESHOLD,
151
+ ) -> GuardDecision:
152
+ verb = classify_request(prompt_text)
153
+ if verb is None:
154
+ return GuardDecision("allow", None, 0, threshold, "Not a punctual request — guard not applied.")
155
+ if target_path and not is_ui_path(target_path):
156
+ return GuardDecision(
157
+ "allow", verb, 0, threshold,
158
+ f"Target {target_path} is not a UI surface — guard not applied.",
159
+ )
160
+ changed = count_changed_lines(old_text, new_text)
161
+ if changed <= 2:
162
+ return GuardDecision(
163
+ "allow", verb, changed, threshold,
164
+ "Diff stays within 2 lines — within the punctual envelope.",
165
+ )
166
+ if changed <= threshold:
167
+ return GuardDecision(
168
+ "warn", verb, changed, threshold,
169
+ (
170
+ f"Punctual request ('{verb}') is changing {changed} lines. "
171
+ "Inside the soft envelope but please justify each line in the reply."
172
+ ),
173
+ )
174
+ return GuardDecision(
175
+ "block", verb, changed, threshold,
176
+ (
177
+ f"Punctual request ('{verb}') is changing {changed} lines (>{threshold}). "
178
+ "Read git log/blame for the file, present the prior state, list each proposed "
179
+ "change as a bullet, and ask the operator before applying. Override only with "
180
+ "explicit confirmation."
181
+ ),
182
+ )
183
+
184
+
185
+ def read_file_history(path: str | Path, *, max_commits: int = 5) -> dict:
186
+ """Return ``{"current": str, "log": [str], "blame": [str] | None}``.
187
+
188
+ Best-effort: missing git, missing file, or non-zero exit codes are
189
+ swallowed so the guardrail itself never errors out. The dictionary is
190
+ purely informational — the agent must surface it in its reply when the
191
+ guard fires.
192
+ """
193
+ path_obj = Path(path)
194
+ try:
195
+ current = path_obj.read_text(encoding="utf-8", errors="replace")
196
+ except OSError:
197
+ current = ""
198
+
199
+ log_lines: list[str] = []
200
+ try:
201
+ completed = subprocess.run(
202
+ ["git", "log", f"-{max_commits}", "--pretty=format:%h %ad %s", "--date=short", "--", str(path_obj)],
203
+ cwd=path_obj.parent if path_obj.parent.exists() else Path.cwd(),
204
+ capture_output=True,
205
+ text=True,
206
+ timeout=4,
207
+ check=False,
208
+ )
209
+ if completed.returncode == 0 and completed.stdout.strip():
210
+ log_lines = completed.stdout.splitlines()
211
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
212
+ log_lines = []
213
+
214
+ blame_lines: list[str] | None = None
215
+ if current:
216
+ try:
217
+ completed = subprocess.run(
218
+ ["git", "blame", "--line-porcelain", "-L", "1,40", str(path_obj)],
219
+ cwd=path_obj.parent if path_obj.parent.exists() else Path.cwd(),
220
+ capture_output=True,
221
+ text=True,
222
+ timeout=4,
223
+ check=False,
224
+ )
225
+ if completed.returncode == 0:
226
+ blame_lines = completed.stdout.splitlines()
227
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
228
+ blame_lines = None
229
+
230
+ return {"current": current, "log": log_lines, "blame": blame_lines}
231
+
232
+
233
+ def evaluate(
234
+ prompt_text: str,
235
+ target_path: str | Path,
236
+ old_text: str,
237
+ new_text: str,
238
+ *,
239
+ threshold: int = DEFAULT_THRESHOLD,
240
+ ) -> dict:
241
+ """Convenience wrapper used by hook_guardrails.
242
+
243
+ Returns a payload safe to JSON-serialize and embed in the deny reason
244
+ or the pass-through note.
245
+ """
246
+ decision = check_diff(prompt_text, old_text, new_text, target_path, threshold=threshold)
247
+ payload = decision.to_payload()
248
+ payload["target_path"] = str(target_path)
249
+ if decision.decision == "block":
250
+ history = read_file_history(target_path)
251
+ payload["history_log"] = history["log"]
252
+ payload["current_excerpt"] = (history["current"][:400] + "…") if history["current"] else ""
253
+ return payload
254
+
255
+
256
+ __all__ = [
257
+ "DEFAULT_THRESHOLD",
258
+ "GuardDecision",
259
+ "PUNCTUAL_VERBS",
260
+ "UI_EXTENSIONS",
261
+ "check_diff",
262
+ "classify_request",
263
+ "count_changed_lines",
264
+ "evaluate",
265
+ "is_ui_path",
266
+ "read_file_history",
267
+ ]