nexo-brain 7.34.0 → 7.35.0
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/package.json +1 -1
- package/src/db/_hot_context.py +30 -1
- package/src/db/_schema.py +63 -0
- package/src/email_sent_events.py +7 -0
- package/src/memory_forget.py +1249 -0
- package/src/plugins/protocol.py +30 -0
- package/src/plugins/schema_abstraction.py +66 -0
- package/src/schema_abstraction.py +763 -0
- package/src/scripts/nexo-daily-self-audit.py +40 -0
- package/src/server.py +54 -0
- package/src/tools_credentials.py +60 -2
- package/tool-enforcement-map.json +66 -0
|
@@ -2194,6 +2194,28 @@ def _run_protocol_debt_drain_inline() -> dict:
|
|
|
2194
2194
|
}
|
|
2195
2195
|
|
|
2196
2196
|
|
|
2197
|
+
def _run_schema_abstraction_distill_inline() -> dict:
|
|
2198
|
+
"""Ola 4 — distill recurring incident archetypes into diagnostic templates.
|
|
2199
|
+
|
|
2200
|
+
Runs the clustering pass over the failure-prevention ledger (+ self-error
|
|
2201
|
+
learnings) and mints/refreshes diagnostic templates idempotently. Guidance
|
|
2202
|
+
only; never blocks. Best-effort: any failure is reported, not raised.
|
|
2203
|
+
"""
|
|
2204
|
+
try:
|
|
2205
|
+
import schema_abstraction as sa
|
|
2206
|
+
|
|
2207
|
+
report = sa.distill_templates()
|
|
2208
|
+
except Exception as exc:
|
|
2209
|
+
return {"ok": False, "error": f"distill_failed: {exc}"}
|
|
2210
|
+
return {
|
|
2211
|
+
"ok": bool(report.get("ok")),
|
|
2212
|
+
"templates_created": int(report.get("templates_created") or 0),
|
|
2213
|
+
"templates_refreshed": int(report.get("templates_refreshed") or 0),
|
|
2214
|
+
"incidents": int(report.get("incidents") or 0),
|
|
2215
|
+
"clusters": int(report.get("clusters") or 0),
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
|
|
2197
2219
|
def _sanitize_watchdog_registry_inline() -> dict:
|
|
2198
2220
|
hash_registry = _hash_registry_path()
|
|
2199
2221
|
if not hash_registry.exists():
|
|
@@ -2329,6 +2351,24 @@ def run_mechanical_autofixes():
|
|
|
2329
2351
|
elif debt_drain.get("error"):
|
|
2330
2352
|
finding("WARN", "autofix", f"Protocol debt drain inline failed: {debt_drain['error']}")
|
|
2331
2353
|
|
|
2354
|
+
# Ola 4 SCHEMA-ABSTRACTION: distill recurring-incident archetypes into
|
|
2355
|
+
# reusable diagnostic templates (idempotent). Surfaces only when a new
|
|
2356
|
+
# template is minted/refreshed; silence otherwise (anti-noise).
|
|
2357
|
+
distill = _run_schema_abstraction_distill_inline()
|
|
2358
|
+
if distill.get("ok"):
|
|
2359
|
+
created = int(distill.get("templates_created") or 0)
|
|
2360
|
+
refreshed = int(distill.get("templates_refreshed") or 0)
|
|
2361
|
+
if created or refreshed:
|
|
2362
|
+
finding(
|
|
2363
|
+
"INFO",
|
|
2364
|
+
"autofix",
|
|
2365
|
+
f"Schema-abstraction distilled diagnostic templates: "
|
|
2366
|
+
f"created={created}, refreshed={refreshed} "
|
|
2367
|
+
f"(from {int(distill.get('incidents') or 0)} incidents)",
|
|
2368
|
+
)
|
|
2369
|
+
elif distill.get("error"):
|
|
2370
|
+
finding("WARN", "autofix", f"Schema-abstraction distill inline failed: {distill['error']}")
|
|
2371
|
+
|
|
2332
2372
|
if NEXO_DB.exists():
|
|
2333
2373
|
conn = sqlite3.connect(str(NEXO_DB))
|
|
2334
2374
|
conn.row_factory = sqlite3.Row
|
package/src/server.py
CHANGED
|
@@ -1843,6 +1843,60 @@ def nexo_memory_maintenance(
|
|
|
1843
1843
|
return handle_memory_maintenance(process_limit, retry_failed, backfill_sources, backfill_limit)
|
|
1844
1844
|
|
|
1845
1845
|
|
|
1846
|
+
@mcp.tool
|
|
1847
|
+
def nexo_memory_forget(
|
|
1848
|
+
value: str,
|
|
1849
|
+
mode: str = "secret",
|
|
1850
|
+
dry_run: bool = True,
|
|
1851
|
+
confirm: bool = False,
|
|
1852
|
+
use_regex: bool = False,
|
|
1853
|
+
reason: str = "",
|
|
1854
|
+
) -> str:
|
|
1855
|
+
"""SELECTIVE-FORGET: verifiably erase a revoked secret, or correct a fact.
|
|
1856
|
+
|
|
1857
|
+
Two modes, never mixed:
|
|
1858
|
+
* mode='secret' (HARD-FORGET): scrubs the secret from EVERY table of ALL
|
|
1859
|
+
LIVE DBs the agent retrieves from — nexo, cognitive, local-context,
|
|
1860
|
+
local-context-usage, and email — discovered by live introspection
|
|
1861
|
+
(sqlite_master + PRAGMA) over each subsystem's own canonical path
|
|
1862
|
+
resolver, not a curated list — plus every FTS index (incl.
|
|
1863
|
+
local_chunks_fts), on-disk transcripts, and legacy shadow DBs (cognitive
|
|
1864
|
+
+ local-context). The row is DELETED where it IS the secret (or carries
|
|
1865
|
+
an embedding/FTS copy), and REDACTED in place where the secret is
|
|
1866
|
+
embedded in an otherwise-useful record (diary, item_history.note,
|
|
1867
|
+
local_chunks.text, entity_facts.value, change_log...). It then
|
|
1868
|
+
RE-ENUMERATES and RE-SCANS everything and only returns complete=True at
|
|
1869
|
+
total zero; any survivor (even in a table not anticipated) is reported
|
|
1870
|
+
as complete=False with its <db>.<table> location. Use ONLY for revoked
|
|
1871
|
+
credentials/secrets or toxic data. Destructive run requires
|
|
1872
|
+
dry_run=False AND confirm=True (a dry-run count is returned otherwise).
|
|
1873
|
+
HNSW persisted indices are invalidated. SCOPE: point-in-time backups /
|
|
1874
|
+
snapshots are NOT swept (retention) — rotate the secret; the report's
|
|
1875
|
+
backup_scope field states this so complete=True never overclaims.
|
|
1876
|
+
* mode='fact' (CORRECT-FACT): does NOT delete. Useful memory is preserved
|
|
1877
|
+
via the existing reversible supersede; correct it with
|
|
1878
|
+
nexo_learning_update / supersede instead.
|
|
1879
|
+
|
|
1880
|
+
Args:
|
|
1881
|
+
value: the literal secret value to forget.
|
|
1882
|
+
mode: 'secret' (default, hard) or 'fact' (soft, reversible).
|
|
1883
|
+
dry_run: when True (default) only count matches; never mutate.
|
|
1884
|
+
confirm: required (with dry_run=False) to actually delete in secret mode.
|
|
1885
|
+
use_regex: also match generic secret-shaped tokens (secret mode only).
|
|
1886
|
+
reason: free-text audit note recorded in the ledger.
|
|
1887
|
+
"""
|
|
1888
|
+
import memory_forget
|
|
1889
|
+
|
|
1890
|
+
return memory_forget.handle_memory_forget(
|
|
1891
|
+
value=value,
|
|
1892
|
+
mode=mode,
|
|
1893
|
+
dry_run=dry_run,
|
|
1894
|
+
confirm=confirm,
|
|
1895
|
+
use_regex=use_regex,
|
|
1896
|
+
reason=reason,
|
|
1897
|
+
)
|
|
1898
|
+
|
|
1899
|
+
|
|
1846
1900
|
@mcp.tool
|
|
1847
1901
|
def nexo_memory_search(
|
|
1848
1902
|
query: str,
|
package/src/tools_credentials.py
CHANGED
|
@@ -320,13 +320,71 @@ def handle_credential_delete(service: str, key: str = '') -> str:
|
|
|
320
320
|
return f"Credential deleted."
|
|
321
321
|
return f"All BYOK credentials deleted ({removed} files)."
|
|
322
322
|
|
|
323
|
+
# Capture the secret value(s) BEFORE deletion so the forget sweep can scrub
|
|
324
|
+
# any grep-able copies that leaked into memory (FTS, KG, transcripts, etc.).
|
|
325
|
+
leaked_values: list[str] = []
|
|
326
|
+
try:
|
|
327
|
+
existing = get_credential(service, key if key else None) or []
|
|
328
|
+
for entry in existing:
|
|
329
|
+
val = str((entry or {}).get("value") or "").strip()
|
|
330
|
+
if len(val) >= 8:
|
|
331
|
+
leaked_values.append(val)
|
|
332
|
+
except Exception:
|
|
333
|
+
leaked_values = []
|
|
334
|
+
|
|
323
335
|
deleted = delete_credential(service, key if key else None)
|
|
324
336
|
if not deleted:
|
|
325
337
|
target = f"{service}/{key}" if key else service
|
|
326
338
|
return f"ERROR: No credentials found for '{target}'."
|
|
339
|
+
|
|
340
|
+
# Auto-trigger SELECTIVE-FORGET (hard, secret mode) over the revoked value(s)
|
|
341
|
+
# so revoking the credential leaves no leaked copies in memory. Best-effort:
|
|
342
|
+
# never let a sweep failure block the credential deletion.
|
|
343
|
+
forgotten = 0
|
|
344
|
+
swept = False
|
|
345
|
+
incomplete = False
|
|
346
|
+
residual_detail = ""
|
|
347
|
+
for val in leaked_values:
|
|
348
|
+
try:
|
|
349
|
+
import memory_forget
|
|
350
|
+
|
|
351
|
+
sweep = memory_forget.sweep_revoked_secret(val, reason=f"credential_deleted:{service}")
|
|
352
|
+
if sweep.get("destructive"):
|
|
353
|
+
swept = True
|
|
354
|
+
# Count BOTH deleted rows and redacted-in-place rows: a leaked
|
|
355
|
+
# copy embedded in a useful record is scrubbed via redaction, not
|
|
356
|
+
# deletion, so counting only deletions would under-report.
|
|
357
|
+
forgotten += int(sweep.get("deleted_total") or 0)
|
|
358
|
+
forgotten += int(sweep.get("redacted_total") or 0)
|
|
359
|
+
if not sweep.get("complete", True):
|
|
360
|
+
incomplete = True
|
|
361
|
+
verification = sweep.get("verification") or {}
|
|
362
|
+
survivors = {
|
|
363
|
+
**(verification.get("residual_stores") or {}),
|
|
364
|
+
**(verification.get("residual_fts") or {}),
|
|
365
|
+
}
|
|
366
|
+
if verification.get("residual_shadows"):
|
|
367
|
+
survivors["shadow_dbs"] = len(verification["residual_shadows"])
|
|
368
|
+
if verification.get("residual_transcripts"):
|
|
369
|
+
survivors["transcripts"] = len(verification["residual_transcripts"])
|
|
370
|
+
if survivors and not residual_detail:
|
|
371
|
+
residual_detail = ", ".join(f"{k}={v}" for k, v in survivors.items())
|
|
372
|
+
except Exception:
|
|
373
|
+
continue
|
|
374
|
+
|
|
375
|
+
suffix = ""
|
|
376
|
+
if swept:
|
|
377
|
+
suffix = f" Forget sweep removed {forgotten} leaked copy/copies from memory."
|
|
378
|
+
if incomplete:
|
|
379
|
+
suffix += (
|
|
380
|
+
" WARNING: forget sweep reported RESIDUAL matches — the secret is "
|
|
381
|
+
"STILL grep-able and was NOT fully removed. Verify manually"
|
|
382
|
+
)
|
|
383
|
+
suffix += f" ({residual_detail})." if residual_detail else "."
|
|
384
|
+
|
|
327
385
|
if key:
|
|
328
|
-
return f"Credential deleted."
|
|
329
|
-
return f"All credentials for service deleted."
|
|
386
|
+
return f"Credential deleted.{suffix}"
|
|
387
|
+
return f"All credentials for service deleted.{suffix}"
|
|
330
388
|
|
|
331
389
|
|
|
332
390
|
def handle_credential_list(service: str = '') -> str:
|
|
@@ -2052,6 +2052,72 @@
|
|
|
2052
2052
|
},
|
|
2053
2053
|
"triggers_after": []
|
|
2054
2054
|
},
|
|
2055
|
+
"nexo_schema_abstraction_distill": {
|
|
2056
|
+
"description": "Distill recurring incident archetypes into reusable diagnostic templates (idempotent clustering pass)",
|
|
2057
|
+
"category": "failure_prevention",
|
|
2058
|
+
"source": "plugin:schema_abstraction",
|
|
2059
|
+
"requires": [
|
|
2060
|
+
"failure_prevention_cases"
|
|
2061
|
+
],
|
|
2062
|
+
"provides": [
|
|
2063
|
+
"diagnostic_templates"
|
|
2064
|
+
],
|
|
2065
|
+
"internal_calls": [
|
|
2066
|
+
"learning_resolver.candidate_similarity"
|
|
2067
|
+
],
|
|
2068
|
+
"enforcement": {
|
|
2069
|
+
"level": "none",
|
|
2070
|
+
"rules": []
|
|
2071
|
+
},
|
|
2072
|
+
"triggers_after": []
|
|
2073
|
+
},
|
|
2074
|
+
"nexo_schema_abstraction_templates": {
|
|
2075
|
+
"description": "List distilled diagnostic templates",
|
|
2076
|
+
"category": "failure_prevention",
|
|
2077
|
+
"source": "plugin:schema_abstraction",
|
|
2078
|
+
"requires": [
|
|
2079
|
+
"diagnostic_templates"
|
|
2080
|
+
],
|
|
2081
|
+
"provides": [],
|
|
2082
|
+
"internal_calls": [],
|
|
2083
|
+
"enforcement": {
|
|
2084
|
+
"level": "none",
|
|
2085
|
+
"rules": []
|
|
2086
|
+
},
|
|
2087
|
+
"triggers_after": []
|
|
2088
|
+
},
|
|
2089
|
+
"nexo_schema_abstraction_match": {
|
|
2090
|
+
"description": "Match an action against diagnostic-template archetypes (primed diagnosis)",
|
|
2091
|
+
"category": "failure_prevention",
|
|
2092
|
+
"source": "plugin:schema_abstraction",
|
|
2093
|
+
"requires": [
|
|
2094
|
+
"diagnostic_templates"
|
|
2095
|
+
],
|
|
2096
|
+
"provides": [],
|
|
2097
|
+
"internal_calls": [],
|
|
2098
|
+
"enforcement": {
|
|
2099
|
+
"level": "none",
|
|
2100
|
+
"rules": []
|
|
2101
|
+
},
|
|
2102
|
+
"triggers_after": []
|
|
2103
|
+
},
|
|
2104
|
+
"nexo_schema_abstraction_retire": {
|
|
2105
|
+
"description": "Retire a diagnostic template (lifecycle, guidance-only)",
|
|
2106
|
+
"category": "failure_prevention",
|
|
2107
|
+
"source": "plugin:schema_abstraction",
|
|
2108
|
+
"requires": [
|
|
2109
|
+
"diagnostic_templates"
|
|
2110
|
+
],
|
|
2111
|
+
"provides": [
|
|
2112
|
+
"diagnostic_templates"
|
|
2113
|
+
],
|
|
2114
|
+
"internal_calls": [],
|
|
2115
|
+
"enforcement": {
|
|
2116
|
+
"level": "none",
|
|
2117
|
+
"rules": []
|
|
2118
|
+
},
|
|
2119
|
+
"triggers_after": []
|
|
2120
|
+
},
|
|
2055
2121
|
"nexo_goal_engine_status": {
|
|
2056
2122
|
"description": "Goal Engine telemetry readiness",
|
|
2057
2123
|
"category": "goal",
|