nexo-brain 7.31.10 → 7.31.12
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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/bin/nexo-brain.js +13 -0
- package/package.json +1 -1
- package/src/auto_update.py +72 -14
- package/src/doctor/providers/boot.py +94 -0
- package/src/enforcement_engine.py +39 -12
- package/src/hooks/post_tool_use.py +2 -2
- package/src/hooks/stop.py +53 -4
- package/src/local_context/api.py +193 -10
- package/src/local_context/db.py +14 -1
- package/src/local_context/embeddings.py +18 -1
- package/src/local_context/extractors.py +6 -1
- package/src/local_context/privacy.py +19 -1
- package/src/plugins/protocol.py +123 -0
- package/src/plugins/update.py +18 -14
- package/src/presets/guardian_default.json +1 -1
- package/src/r23g_secrets_in_output.py +37 -0
- package/src/requirements.txt +17 -4
- package/src/scripts/nexo-daily-self-audit.py +50 -0
- package/src/scripts/nexo-immune.py +27 -2
- package/src/server.py +112 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.31.
|
|
3
|
+
"version": "7.31.12",
|
|
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,7 @@
|
|
|
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.31.
|
|
21
|
+
Version `7.31.12` is the current packaged-runtime line. Patch release over v7.31.11 - Local Memory core hardening (Release A: defensive cosine, stable chunk ids, iCloud dataless handling, performance PRAGMAs) plus an offline-first dependency installer. Version `7.31.11` was a patch release over v7.31.10 - MCP lifecycle robustness + guardrail precision.
|
|
22
22
|
|
|
23
23
|
Previously in `7.31.9`: patch release over v7.31.8 - UI release closeout now has to prove the original reported symptom was reopened with observable evidence before claiming the release is ready.
|
|
24
24
|
|
package/bin/nexo-brain.js
CHANGED
|
@@ -4103,6 +4103,19 @@ async function runSetup() {
|
|
|
4103
4103
|
log("Try manually: python3 -m venv ~/.nexo/.venv && ~/.nexo/.venv/bin/pip install -r src/requirements.txt");
|
|
4104
4104
|
process.exit(1);
|
|
4105
4105
|
}
|
|
4106
|
+
// Mirror the bundled wheels into NEXO_HOME so the Python runtime (startup
|
|
4107
|
+
// self-heal, update, cron) can reinstall any missing dep OFFLINE later, with no
|
|
4108
|
+
// access to the Desktop bundle path. auto_update._bundled_wheels_dir() looks in
|
|
4109
|
+
// <NEXO_HOME>/runtime/python-wheels. Works for WSL (linux) and macOS.
|
|
4110
|
+
try {
|
|
4111
|
+
if (fs.existsSync(bundledWheelsDir)) {
|
|
4112
|
+
const runtimeWheels = path.join(NEXO_HOME, "runtime", "python-wheels");
|
|
4113
|
+
fs.mkdirSync(runtimeWheels, { recursive: true });
|
|
4114
|
+
fs.cpSync(bundledWheelsDir, runtimeWheels, { recursive: true });
|
|
4115
|
+
}
|
|
4116
|
+
} catch (e) {
|
|
4117
|
+
log(" (note) could not mirror bundled wheels into runtime: " + (e && e.message));
|
|
4118
|
+
}
|
|
4106
4119
|
// Update python reference to use venv python for the rest of setup
|
|
4107
4120
|
if (fs.existsSync(venvPython)) {
|
|
4108
4121
|
python = venvPython;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.31.
|
|
3
|
+
"version": "7.31.12",
|
|
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",
|
package/src/auto_update.py
CHANGED
|
@@ -1058,8 +1058,46 @@ def _ensure_runtime_venv(runtime_root: Path = NEXO_HOME) -> Path | None:
|
|
|
1058
1058
|
return None
|
|
1059
1059
|
|
|
1060
1060
|
|
|
1061
|
+
def _bundled_wheels_dir() -> "Path | None":
|
|
1062
|
+
"""Locate the bundled Python wheels for offline install.
|
|
1063
|
+
|
|
1064
|
+
Priority: explicit NEXO_BUNDLED_WHEELS_DIR (set by the Desktop bundle) →
|
|
1065
|
+
a canonical runtime copy under NEXO_HOME. Returns the dir only if it holds
|
|
1066
|
+
at least one .whl, else None (caller falls back to PyPI).
|
|
1067
|
+
"""
|
|
1068
|
+
candidates = []
|
|
1069
|
+
env_dir = os.environ.get("NEXO_BUNDLED_WHEELS_DIR", "").strip()
|
|
1070
|
+
if env_dir:
|
|
1071
|
+
candidates.append(Path(env_dir).expanduser())
|
|
1072
|
+
candidates.append(NEXO_HOME / "runtime" / "python-wheels")
|
|
1073
|
+
for directory in candidates:
|
|
1074
|
+
try:
|
|
1075
|
+
if directory.is_dir() and any(directory.glob("*.whl")):
|
|
1076
|
+
return directory
|
|
1077
|
+
except Exception:
|
|
1078
|
+
continue
|
|
1079
|
+
return None
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
def _pip_install_argv(pip_bin, req_file, *, wheels_dir=None, use_python_m=False, break_system=False) -> list:
|
|
1083
|
+
"""Build a pip install argv. Offline (--no-index --find-links) when wheels_dir is set."""
|
|
1084
|
+
argv = [str(pip_bin)]
|
|
1085
|
+
if use_python_m:
|
|
1086
|
+
argv += ["-m", "pip"]
|
|
1087
|
+
argv += ["install", "--quiet", "-r", str(req_file)]
|
|
1088
|
+
if wheels_dir is not None:
|
|
1089
|
+
argv += ["--no-index", "--find-links", str(wheels_dir)]
|
|
1090
|
+
if break_system:
|
|
1091
|
+
argv.append("--break-system-packages")
|
|
1092
|
+
return argv
|
|
1093
|
+
|
|
1094
|
+
|
|
1061
1095
|
def _reinstall_pip_deps() -> bool:
|
|
1062
|
-
"""Reinstall Python deps from requirements.txt. Returns True on success.
|
|
1096
|
+
"""Reinstall Python deps from requirements.txt. Returns True on success.
|
|
1097
|
+
|
|
1098
|
+
Prefers the bundled wheels (offline) so a user with no internet still gets a
|
|
1099
|
+
self-repairing runtime; falls back to PyPI if the bundle can't satisfy it.
|
|
1100
|
+
"""
|
|
1063
1101
|
req_file = SRC_DIR / "requirements.txt"
|
|
1064
1102
|
if not req_file.exists():
|
|
1065
1103
|
return True
|
|
@@ -1069,24 +1107,32 @@ def _reinstall_pip_deps() -> bool:
|
|
|
1069
1107
|
alt_pip = NEXO_HOME / ".venv" / "bin" / "pip3"
|
|
1070
1108
|
if alt_pip.exists():
|
|
1071
1109
|
venv_pip = alt_pip
|
|
1110
|
+
wheels_dir = _bundled_wheels_dir()
|
|
1111
|
+
# Large wheel sets / slow links need more than the old 120s.
|
|
1112
|
+
timeout_s = 600
|
|
1113
|
+
use_python_m = not venv_pip.exists()
|
|
1114
|
+
if use_python_m and desktop_product_requested():
|
|
1115
|
+
_log(f"managed venv unavailable for Desktop dependency repair: {venv_python}")
|
|
1116
|
+
return False
|
|
1117
|
+
pip_bin = venv_pip if venv_pip.exists() else sys.executable
|
|
1072
1118
|
try:
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1119
|
+
argv = _pip_install_argv(
|
|
1120
|
+
pip_bin, req_file, wheels_dir=wheels_dir,
|
|
1121
|
+
use_python_m=use_python_m, break_system=use_python_m,
|
|
1122
|
+
)
|
|
1123
|
+
result = subprocess.run(argv, capture_output=True, text=True, timeout=timeout_s)
|
|
1124
|
+
if result.returncode != 0 and wheels_dir is not None:
|
|
1125
|
+
# Offline set couldn't satisfy it (missing/incompatible wheel) → retry online.
|
|
1126
|
+
_log(f"offline pip install failed, retrying via PyPI: {result.stderr or result.stdout}")
|
|
1127
|
+
argv_online = _pip_install_argv(
|
|
1128
|
+
pip_bin, req_file, wheels_dir=None,
|
|
1129
|
+
use_python_m=use_python_m, break_system=use_python_m,
|
|
1082
1130
|
)
|
|
1083
|
-
|
|
1084
|
-
_log(f"managed venv unavailable for Desktop dependency repair: {venv_python}")
|
|
1085
|
-
return False
|
|
1131
|
+
result = subprocess.run(argv_online, capture_output=True, text=True, timeout=timeout_s)
|
|
1086
1132
|
if result.returncode != 0:
|
|
1087
1133
|
_log(f"pip install failed (exit {result.returncode}): {result.stderr or result.stdout}")
|
|
1088
1134
|
return False
|
|
1089
|
-
_log("Reinstalled Python dependencies
|
|
1135
|
+
_log("Reinstalled Python dependencies" + (" (offline)" if wheels_dir is not None else ""))
|
|
1090
1136
|
return True
|
|
1091
1137
|
except Exception as e:
|
|
1092
1138
|
_log(f"pip reinstall failed: {e}")
|
|
@@ -6107,6 +6153,18 @@ def startup_preflight(*, entrypoint: str, interactive: bool = False) -> dict:
|
|
|
6107
6153
|
result["actions"].extend(extra_actions)
|
|
6108
6154
|
if reconcile_message:
|
|
6109
6155
|
_log(reconcile_message)
|
|
6156
|
+
# Self-heal: if the managed venv lost a critical importable module
|
|
6157
|
+
# (e.g. pypdf -> PDFs indexed empty), reinstall it automatically. No
|
|
6158
|
+
# user action, no prompt — the runtime repairs itself on startup.
|
|
6159
|
+
try:
|
|
6160
|
+
from doctor.providers.boot import check_managed_venv_modules
|
|
6161
|
+
|
|
6162
|
+
dep_check = check_managed_venv_modules(fix=True)
|
|
6163
|
+
if getattr(dep_check, "fixed", False):
|
|
6164
|
+
result["actions"].append("venv-deps-repaired")
|
|
6165
|
+
_log(f"Managed venv dependencies repaired on startup: {dep_check.summary}")
|
|
6166
|
+
except Exception as dep_exc:
|
|
6167
|
+
_log(f"managed venv module check skipped: {dep_exc}")
|
|
6110
6168
|
except Exception as e:
|
|
6111
6169
|
result["error"] = str(e)
|
|
6112
6170
|
_write_update_summary(result)
|
|
@@ -654,6 +654,99 @@ def check_managed_venv_python(fix: bool = False) -> DoctorCheck:
|
|
|
654
654
|
)
|
|
655
655
|
|
|
656
656
|
|
|
657
|
+
# Critical importable modules the managed venv must always have. A missing one
|
|
658
|
+
# fails silently (e.g. pypdf absent -> every PDF/XLSX/MSG indexed as empty text).
|
|
659
|
+
# Verified by importing INSIDE the managed venv, not the current interpreter.
|
|
660
|
+
MANAGED_VENV_REQUIRED_MODULES = (
|
|
661
|
+
"fastmcp",
|
|
662
|
+
"numpy",
|
|
663
|
+
"anthropic",
|
|
664
|
+
"openai",
|
|
665
|
+
"fastembed",
|
|
666
|
+
"pypdf",
|
|
667
|
+
"openpyxl",
|
|
668
|
+
"extract_msg",
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def _missing_venv_modules(venv_python: Path | str, modules) -> list[str]:
|
|
673
|
+
"""Return the subset of ``modules`` that ``venv_python`` cannot import."""
|
|
674
|
+
mods = [str(m) for m in modules if str(m).strip()]
|
|
675
|
+
if not mods:
|
|
676
|
+
return []
|
|
677
|
+
probe = (
|
|
678
|
+
"import importlib.util as u, sys\n"
|
|
679
|
+
"print('\\n'.join(m for m in sys.argv[1:] if u.find_spec(m) is None))"
|
|
680
|
+
)
|
|
681
|
+
try:
|
|
682
|
+
result = subprocess.run(
|
|
683
|
+
[str(venv_python), "-c", probe, *mods],
|
|
684
|
+
capture_output=True,
|
|
685
|
+
text=True,
|
|
686
|
+
timeout=30,
|
|
687
|
+
)
|
|
688
|
+
except Exception:
|
|
689
|
+
return []
|
|
690
|
+
if result.returncode != 0:
|
|
691
|
+
return []
|
|
692
|
+
return [line.strip() for line in (result.stdout or "").splitlines() if line.strip()]
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def _repair_managed_venv_deps() -> bool:
|
|
696
|
+
try:
|
|
697
|
+
import auto_update
|
|
698
|
+
|
|
699
|
+
return bool(auto_update._reinstall_pip_deps())
|
|
700
|
+
except Exception:
|
|
701
|
+
return False
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def check_managed_venv_modules(fix: bool = False) -> DoctorCheck:
|
|
705
|
+
"""Ensure the managed venv has every critical importable module.
|
|
706
|
+
|
|
707
|
+
A missing optional parser (pypdf/openpyxl/extract_msg) makes the local index
|
|
708
|
+
read PDF/XLSX/MSG as empty, silently. With ``fix=True`` (run automatically on
|
|
709
|
+
startup preflight) this reinstalls them, so the runtime repairs itself with no
|
|
710
|
+
user action.
|
|
711
|
+
"""
|
|
712
|
+
venv_python = _managed_venv_python_path()
|
|
713
|
+
if not venv_python.exists():
|
|
714
|
+
# The python-version check already reports a missing venv; stay quiet here.
|
|
715
|
+
return DoctorCheck(
|
|
716
|
+
id="boot.managed_venv_modules",
|
|
717
|
+
tier="boot",
|
|
718
|
+
status="healthy",
|
|
719
|
+
severity="info",
|
|
720
|
+
summary="Managed Python venv not present yet",
|
|
721
|
+
evidence=[str(venv_python)],
|
|
722
|
+
)
|
|
723
|
+
missing = _missing_venv_modules(venv_python, MANAGED_VENV_REQUIRED_MODULES)
|
|
724
|
+
if not missing:
|
|
725
|
+
return DoctorCheck(
|
|
726
|
+
id="boot.managed_venv_modules",
|
|
727
|
+
tier="boot",
|
|
728
|
+
status="healthy",
|
|
729
|
+
severity="info",
|
|
730
|
+
summary=f"All {len(MANAGED_VENV_REQUIRED_MODULES)} critical venv modules present",
|
|
731
|
+
evidence=[str(venv_python)],
|
|
732
|
+
)
|
|
733
|
+
if fix and _repair_managed_venv_deps():
|
|
734
|
+
post = check_managed_venv_modules(fix=False)
|
|
735
|
+
if post.status == "healthy":
|
|
736
|
+
post.fixed = True
|
|
737
|
+
post.summary += " (repaired missing modules)"
|
|
738
|
+
return post
|
|
739
|
+
return DoctorCheck(
|
|
740
|
+
id="boot.managed_venv_modules",
|
|
741
|
+
tier="boot",
|
|
742
|
+
status="degraded",
|
|
743
|
+
severity="warn",
|
|
744
|
+
summary=f"{len(missing)} critical venv module(s) missing: {', '.join(missing)}",
|
|
745
|
+
evidence=[str(venv_python), *missing],
|
|
746
|
+
repair_plan=["Run nexo doctor --tier boot --fix or nexo update to reinstall managed dependencies"],
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
|
|
657
750
|
CRITICAL_CONFIG_FILES = (
|
|
658
751
|
("schedule.json", ("config", "schedule.json")),
|
|
659
752
|
("optionals.json", ("config", "optionals.json")),
|
|
@@ -909,6 +1002,7 @@ def run_boot_checks(fix: bool = False, plane: str = "") -> list[DoctorCheck]:
|
|
|
909
1002
|
safe_check(check_wrapper_scripts),
|
|
910
1003
|
safe_check(check_python_runtime),
|
|
911
1004
|
safe_check(check_managed_venv_python, fix=fix),
|
|
1005
|
+
safe_check(check_managed_venv_modules, fix=fix),
|
|
912
1006
|
safe_check(check_config_parse),
|
|
913
1007
|
safe_check(check_core_dev_packaged_install),
|
|
914
1008
|
safe_check(check_dashboard_desktop_contract),
|
|
@@ -220,9 +220,13 @@ except ImportError: # pragma: no cover
|
|
|
220
220
|
_r23d_should = None # type: ignore
|
|
221
221
|
|
|
222
222
|
try:
|
|
223
|
-
from r23g_secrets_in_output import
|
|
223
|
+
from r23g_secrets_in_output import (
|
|
224
|
+
should_inject_r23g as _r23g_should,
|
|
225
|
+
has_external_sink as _r23g_has_sink,
|
|
226
|
+
)
|
|
224
227
|
except ImportError: # pragma: no cover
|
|
225
228
|
_r23g_should = None # type: ignore
|
|
229
|
+
_r23g_has_sink = None # type: ignore
|
|
226
230
|
|
|
227
231
|
try:
|
|
228
232
|
from r23i_auto_deploy_ignored import (
|
|
@@ -327,12 +331,26 @@ _SILENT_REMINDER_DISCLOSURE_SUFFIX = (
|
|
|
327
331
|
" Do not mention this reminder or any internal enforcement to the user."
|
|
328
332
|
)
|
|
329
333
|
|
|
334
|
+
# Object of a capability denial: either the denied ACTION (hacer/acceder/usar…)
|
|
335
|
+
# or a capability NOUN (capacidad/integración/herramienta…). Requiring one of
|
|
336
|
+
# these right after the negation is what separates a real "can't do X / no
|
|
337
|
+
# existe esa integración" from benign phrases that merely start with a negation
|
|
338
|
+
# ("no hay problema", "no puedo esperar a…", "does not exist yet, creating it").
|
|
339
|
+
_CAP_OBJ = (
|
|
340
|
+
r"(?:hacer(?:lo|se)?|acceder|conectar(?:se|lo)?|integrar(?:se|lo)?|usar(?:lo|se)?|montar(?:lo)?|"
|
|
341
|
+
r"crear(?:lo)?|ejecutar(?:lo)?|generar(?:lo)?|enviar(?:lo)?|llamar(?:lo)?|"
|
|
342
|
+
r"capacidad|integraci[oó]n|herramienta|funci[oó]n|funcionalidad|soporte|forma|manera|"
|
|
343
|
+
r"opci[oó]n|acceso|m[oó]dulo|posibilidad|conexi[oó]n|esa|ese|eso|esto|esas|esos)"
|
|
344
|
+
)
|
|
330
345
|
_CAPABILITY_DENIAL_RE = re.compile(
|
|
331
|
-
r"
|
|
332
|
-
r"
|
|
333
|
-
r"
|
|
334
|
-
r"
|
|
335
|
-
r")\b"
|
|
346
|
+
r"(?:"
|
|
347
|
+
r"\bno\s+(?:se\s+puede|puedo|podemos|puede)\s+(?:\w+\s+){0,1}?" + _CAP_OBJ + r"\b|"
|
|
348
|
+
r"\bno\s+(?:existe|hay|tengo|tenemos)\s+(?:\w+\s+){0,2}?" + _CAP_OBJ + r"\b|"
|
|
349
|
+
r"\bno\s+est[aá]\s+(?:soportad[oa]|montad[oa]|disponible|integrad[oa]|configurad[oa]|habilitad[oa])\b|"
|
|
350
|
+
r"\b(?:cannot|can'?t|can\s+not)\s+(?:\w+\s+){0,2}?(?:do\s+(?:that|it|this)|access|connect|integrate|use|create|run|support)\b|"
|
|
351
|
+
r"\bdoes\s+not\s+exist(?!\s+yet)\b|\bnot\s+supported\b|\bnot\s+possible\b|"
|
|
352
|
+
r"\bno\s+such\s+(?:capability|tool|integration|feature)\b"
|
|
353
|
+
r")",
|
|
336
354
|
re.IGNORECASE,
|
|
337
355
|
)
|
|
338
356
|
_CAPABILITY_REALITY_TOOLS = {
|
|
@@ -1914,21 +1932,30 @@ class HeadlessEnforcer:
|
|
|
1914
1932
|
if not should:
|
|
1915
1933
|
return
|
|
1916
1934
|
if mode == "shadow":
|
|
1935
|
+
# shadow → logs only. No enqueue, no followup, no side effects.
|
|
1917
1936
|
_logger.info("[R23g SHADOW] would inject")
|
|
1918
|
-
self._ensure_exposed_credential_followup(tool_input, reason="R23g shadow")
|
|
1919
1937
|
return
|
|
1920
1938
|
self._enqueue(prompt, "R23g_secrets_in_output", rule_id="R23g_secrets_in_output")
|
|
1921
|
-
self._ensure_exposed_credential_followup(tool_input
|
|
1939
|
+
self._ensure_exposed_credential_followup(tool_input)
|
|
1922
1940
|
_logger.info("[R23g %s] enqueued", mode.upper())
|
|
1923
1941
|
|
|
1924
|
-
def _ensure_exposed_credential_followup(self, tool_input
|
|
1942
|
+
def _ensure_exposed_credential_followup(self, tool_input) -> None:
|
|
1925
1943
|
if not isinstance(tool_input, dict):
|
|
1926
1944
|
return
|
|
1927
1945
|
cmd = tool_input.get("command")
|
|
1928
1946
|
if not isinstance(cmd, str) or not cmd.strip():
|
|
1929
1947
|
return
|
|
1948
|
+
# A critical "rotate the credential" followup is only warranted when the
|
|
1949
|
+
# secret is actually exfiltrated to a third party. A bare local read
|
|
1950
|
+
# (cat .env, env, printenv) exposes nothing and must NOT mint an
|
|
1951
|
+
# un-closeable critical followup. The soft reminder above already nudges
|
|
1952
|
+
# the agent; the persistent debt is reserved for real exposure.
|
|
1953
|
+
if _r23g_has_sink is None or not _r23g_has_sink(cmd):
|
|
1954
|
+
return
|
|
1930
1955
|
safe_cmd = _redact_for_log(cmd, max_len=160)
|
|
1931
|
-
|
|
1956
|
+
# Deterministic id seeded only by the (redacted) command, so the same
|
|
1957
|
+
# exfiltration dedups across shadow/soft/hard instead of duplicating.
|
|
1958
|
+
followup_id = _security_followup_id(safe_cmd)
|
|
1932
1959
|
try:
|
|
1933
1960
|
from db import create_followup, get_followup # type: ignore
|
|
1934
1961
|
|
|
@@ -1937,7 +1964,7 @@ class HeadlessEnforcer:
|
|
|
1937
1964
|
create_followup(
|
|
1938
1965
|
followup_id,
|
|
1939
1966
|
description=(
|
|
1940
|
-
"SEGURIDAD: credencial expuesta
|
|
1967
|
+
"SEGURIDAD: credencial expuesta a un tercero detectada por el guard. "
|
|
1941
1968
|
f"Origen: {safe_cmd}. Rotar/revocar la credencial y sustituirla en el gestor seguro."
|
|
1942
1969
|
),
|
|
1943
1970
|
date=time.strftime("%Y-%m-%d"),
|
|
@@ -1945,7 +1972,7 @@ class HeadlessEnforcer:
|
|
|
1945
1972
|
"Cierre solo con evidencia de revocación efectiva: llamada/API/HTTP 401 para la credencial antigua "
|
|
1946
1973
|
"o comprobación oficial equivalente, más nueva ubicación segura registrada."
|
|
1947
1974
|
),
|
|
1948
|
-
reasoning=
|
|
1975
|
+
reasoning="R23g detected secret exfiltration to a third party",
|
|
1949
1976
|
priority="critical",
|
|
1950
1977
|
internal=1,
|
|
1951
1978
|
owner="agent",
|
|
@@ -244,8 +244,8 @@ def _is_production_mutation_command(cmd: str) -> bool:
|
|
|
244
244
|
r"\bgcloud\s+run\s+(?:deploy|services\s+update|jobs\s+deploy|jobs\s+update)\b",
|
|
245
245
|
r"\bgcloud\s+dns\s+record-sets\s+transaction\s+execute\b",
|
|
246
246
|
r"\bg(?:sutil|cloud\s+storage)\b.*\b(?:cp|rsync)\b.*\b(?:release|stable|cdn|bucket|buckets)\b",
|
|
247
|
-
r"\b(?:rsync|scp)\b(?!.*--dry-run).+\s+\S+:(?:/[^ \n\r;]*)(?:public_html|httpdocs|www|webroot)\b",
|
|
248
|
-
r"\bssh\b[^'\"]*['\"][^'\"]*(?:sed\s+-i|tee\s+|>\s*\S|>>\s*\S|rm\s+-|mv\s+|cp\s+)[^'\"]*(?:public_html|httpdocs|/var/www|/opt/)[^'\"]*['\"]",
|
|
247
|
+
r"\b(?:rsync|scp)\b(?!.*--dry-run).+\s+\S+:(?:/[^ \n\r;]*)(?:public_html|httpdocs|www|webroot|home/nexodesk)\b",
|
|
248
|
+
r"\bssh\b[^'\"]*['\"][^'\"]*(?:sed\s+-i|tee\s+|>\s*\S|>>\s*\S|rm\s+-|mv\s+|cp\s+)[^'\"]*(?:public_html|httpdocs|/var/www|/opt/|/home/nexodesk)[^'\"]*['\"]",
|
|
249
249
|
r"\b(?:whmapi1|uapi|cpapi2)\b",
|
|
250
250
|
r"\b(?:cloudflare|cfcli)\b.*\b(?:dns|record)\b.*\b(?:create|delete|update|patch|put|post)\b",
|
|
251
251
|
r"\bcurl\b(?=.*api\.cloudflare\.com/client/v4/zones/.*/dns_records)(?=.*(?:-X|--request)\s*(?:POST|PUT|PATCH|DELETE)\b)",
|
package/src/hooks/stop.py
CHANGED
|
@@ -18,14 +18,24 @@ from pathlib import Path
|
|
|
18
18
|
|
|
19
19
|
_DIR = Path(__file__).resolve().parent
|
|
20
20
|
|
|
21
|
+
# Specific future-commitment phrases. Bare words like "pendiente" / "después"
|
|
22
|
+
# were removed: they appear constantly in ordinary conversation and, read over a
|
|
23
|
+
# GLOBAL rolling buffer, blocked closes spuriously. Each marker now expresses a
|
|
24
|
+
# real deferral, not an incidental adverb.
|
|
21
25
|
FUTURE_COMMITMENT_MARKERS = (
|
|
22
26
|
"lo dejo como seguimiento",
|
|
23
|
-
"cuando quieras",
|
|
24
|
-
"pendiente",
|
|
25
27
|
"lo cojo aparte",
|
|
26
|
-
"después",
|
|
27
|
-
"despues",
|
|
28
28
|
"bloqueado por auth",
|
|
29
|
+
"queda pendiente de",
|
|
30
|
+
"lo dejo pendiente",
|
|
31
|
+
"lo retomo más tarde",
|
|
32
|
+
"lo retomo mas tarde",
|
|
33
|
+
"lo vemos en otra sesión",
|
|
34
|
+
"lo vemos en otra sesion",
|
|
35
|
+
"te lo dejo para después",
|
|
36
|
+
"te lo dejo para despues",
|
|
37
|
+
"lo dejo para más tarde",
|
|
38
|
+
"lo dejo para mas tarde",
|
|
29
39
|
)
|
|
30
40
|
FOLLOWUP_CREATE_MARKERS = ("nexo_followup_create", "mcp__nexo__nexo_followup_create")
|
|
31
41
|
PARTIAL_TASK_CLOSE_RE = re.compile(
|
|
@@ -78,6 +88,42 @@ def _read_recent_lines(path: Path, max_lines: int = 800) -> list[str]:
|
|
|
78
88
|
return []
|
|
79
89
|
|
|
80
90
|
|
|
91
|
+
def _line_session_id(raw_line: str) -> str:
|
|
92
|
+
try:
|
|
93
|
+
payload = json.loads(raw_line)
|
|
94
|
+
except Exception:
|
|
95
|
+
return ""
|
|
96
|
+
if not isinstance(payload, dict):
|
|
97
|
+
return ""
|
|
98
|
+
for key in ("session_id", "sid", "claude_session_id", "sessionId"):
|
|
99
|
+
val = payload.get(key)
|
|
100
|
+
if isinstance(val, str) and val.strip():
|
|
101
|
+
return val.strip()
|
|
102
|
+
return ""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _scope_to_session(lines: list[str], sid: str) -> list[str]:
|
|
106
|
+
"""Keep only buffer lines belonging to ``sid``. ``session_buffer.jsonl`` is a
|
|
107
|
+
GLOBAL rolling log shared by every session/client, so without scoping the
|
|
108
|
+
closeout gate counts *other* sessions' commitments and blocks this close
|
|
109
|
+
spuriously. If the buffer carries no session ids at all we cannot scope and
|
|
110
|
+
fall back to every line (prior behaviour)."""
|
|
111
|
+
if not sid:
|
|
112
|
+
return lines
|
|
113
|
+
tagged = [(raw, _line_session_id(raw)) for raw in lines]
|
|
114
|
+
if not any(s for _, s in tagged):
|
|
115
|
+
return lines
|
|
116
|
+
return [raw for raw, s in tagged if s == sid]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _current_session_id() -> str:
|
|
120
|
+
for key in ("CLAUDE_SESSION_ID", "NEXO_SID", "NEXO_SESSION_ID"):
|
|
121
|
+
val = os.environ.get(key, "").strip()
|
|
122
|
+
if val:
|
|
123
|
+
return val
|
|
124
|
+
return ""
|
|
125
|
+
|
|
126
|
+
|
|
81
127
|
def _line_text(line: str) -> str:
|
|
82
128
|
try:
|
|
83
129
|
payload = json.loads(line)
|
|
@@ -133,8 +179,11 @@ def check_closeout_followups() -> dict:
|
|
|
133
179
|
if chunk:
|
|
134
180
|
lines.extend(chunk)
|
|
135
181
|
sources.append(str(path))
|
|
182
|
+
sid = _current_session_id()
|
|
183
|
+
lines = _scope_to_session(lines, sid)
|
|
136
184
|
result = scan_closeout_followup_gaps(lines)
|
|
137
185
|
result["sources"] = sources
|
|
186
|
+
result["session_scoped"] = bool(sid)
|
|
138
187
|
return result
|
|
139
188
|
|
|
140
189
|
|