nexo-brain 7.27.3 → 7.28.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 +5 -1
- package/bin/windows-wsl-bridge.js +9 -0
- package/package.json +1 -1
- package/src/causal_graph.py +763 -0
- package/src/classifier_local.py +44 -0
- package/src/cognitive/_core.py +3 -0
- package/src/cognitive_control_observatory.py +2 -0
- package/src/db/__init__.py +8 -0
- package/src/db/_commitments.py +344 -0
- package/src/db/_entities.py +98 -11
- package/src/db/_memory_v2.py +130 -2
- package/src/db/_schema.py +565 -0
- package/src/desktop_bridge.py +1 -1
- package/src/doctor/providers/runtime.py +9 -3
- package/src/enforcement_engine.py +128 -2
- package/src/entity_live_profile.py +1073 -0
- package/src/failure_prevention.py +1052 -0
- package/src/hook_guardrails.py +104 -0
- package/src/knowledge_graph.py +46 -9
- package/src/local_context/api.py +54 -22
- package/src/local_context/usage_events.py +273 -8
- package/src/memory_executive.py +620 -0
- package/src/memory_utility.py +952 -0
- package/src/plugin_loader.py +9 -5
- package/src/plugins/entities.py +84 -7
- package/src/plugins/entity_live_profile.py +101 -0
- package/src/plugins/failure_prevention.py +162 -0
- package/src/plugins/memory_export.py +55 -18
- package/src/plugins/protocol.py +133 -0
- package/src/plugins/semantic_layers.py +138 -0
- package/src/pre_answer_router.py +622 -28
- package/src/pre_answer_runtime.py +463 -18
- package/src/r14_correction_learning.py +3 -3
- package/src/requirements.txt +5 -1
- package/src/runtime_versioning.py +11 -1
- package/src/saved_not_used_audit.py +44 -3
- package/src/scripts/nexo-followup-runner.py +194 -0
- package/src/semantic_layers.py +1153 -0
- package/src/semantic_reasoner.py +2 -2
- package/src/semantic_router.py +58 -11
- package/src/server.py +41 -3
- package/src/tools_sessions.py +88 -31
- package/src/tools_transcripts.py +38 -22
- package/src/user_state_model.py +971 -0
- package/tool-enforcement-map.json +230 -0
|
@@ -0,0 +1,1052 @@
|
|
|
1
|
+
"""Failure prevention ledger for autopsy candidates and antibody proposals.
|
|
2
|
+
|
|
3
|
+
This module coordinates existing NEXO owners. It does not create canonical
|
|
4
|
+
learnings, outcomes, guard rules, protocol debt, benchmarks, or followups by
|
|
5
|
+
itself; it records a validated, redacted case and proposed owner actions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
import sqlite3
|
|
14
|
+
import time
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from db import get_db
|
|
19
|
+
from learning_resolver import resolve_learning_candidate
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
POLICY_VERSION = "failure_prevention.v1"
|
|
23
|
+
|
|
24
|
+
FAILURE_TYPES = {
|
|
25
|
+
"memory", "identity", "promise", "release", "server", "privacy",
|
|
26
|
+
"security", "test", "tool", "workflow", "communication", "performance",
|
|
27
|
+
"other",
|
|
28
|
+
}
|
|
29
|
+
SOURCE_TYPES = {
|
|
30
|
+
"francisco_correction", "explicit_instruction", "test_failure",
|
|
31
|
+
"release_gate_failure", "outcome_miss", "protocol_debt",
|
|
32
|
+
"guard_violation", "guard_check", "guardian_telemetry",
|
|
33
|
+
"error_repetition", "somatic_event", "hook_run", "immune_finding",
|
|
34
|
+
"watchdog_finding", "daily_self_audit", "deep_sleep_finding",
|
|
35
|
+
"manual_review",
|
|
36
|
+
}
|
|
37
|
+
INFERENCE_ONLY_SOURCES = {
|
|
38
|
+
"guardian_telemetry", "immune_finding", "watchdog_finding",
|
|
39
|
+
"daily_self_audit", "deep_sleep_finding",
|
|
40
|
+
}
|
|
41
|
+
SEVERITIES = {"p0", "p1", "p2", "p3", "p4"}
|
|
42
|
+
CASE_STATUSES = {
|
|
43
|
+
"candidate", "analyzing", "action_required", "antibody_pending",
|
|
44
|
+
"verifying", "verified", "resolved", "rejected", "false_positive",
|
|
45
|
+
"expired", "rolled_back", "conflict_review",
|
|
46
|
+
}
|
|
47
|
+
PRIVACY_LEVELS = {"public", "normal", "private", "sensitive", "secret"}
|
|
48
|
+
SURFACES = {"pre_action", "debug_local", "audit", "runtime_internal", "export"}
|
|
49
|
+
ACTION_TYPES = {
|
|
50
|
+
"learning_resolve", "test_add", "benchmark_case_add",
|
|
51
|
+
"guard_rule_proposal", "predictive_context_rule", "docs_update",
|
|
52
|
+
"skill_update", "followup_create", "outcome_register",
|
|
53
|
+
"release_gate_update", "immune_check_update", "watchdog_check_update",
|
|
54
|
+
}
|
|
55
|
+
TARGET_SYSTEMS = {
|
|
56
|
+
"learning_resolver", "learnings", "pytest", "runtime_pack", "guardian",
|
|
57
|
+
"pre_answer_router", "docs", "skills", "followups", "outcomes",
|
|
58
|
+
"release_readiness", "immune", "watchdog",
|
|
59
|
+
}
|
|
60
|
+
ACTION_STATUSES = {
|
|
61
|
+
"proposed", "approved", "applied", "verifying", "verified", "rejected",
|
|
62
|
+
"expired", "rolled_back", "false_positive",
|
|
63
|
+
}
|
|
64
|
+
ACTIVATION_POLICIES = {
|
|
65
|
+
"candidate_only", "shadow", "warn", "block_after_verification",
|
|
66
|
+
"manual_approval_required",
|
|
67
|
+
}
|
|
68
|
+
VERIFICATION_STATUSES = {"missing", "pending", "passed", "failed", "not_applicable"}
|
|
69
|
+
NOT_APPLICABLE_ACTIONS = {"docs_update", "followup_create"}
|
|
70
|
+
APPROVAL_REF_PREFIXES = {"evidence", "protocol_task", "guard_check", "change_log"}
|
|
71
|
+
|
|
72
|
+
SOURCE_REF_PREFIXES: dict[str, set[str]] = {
|
|
73
|
+
"francisco_correction": {"session_correction_requirement"},
|
|
74
|
+
"explicit_instruction": {"protocol_task", "evidence"},
|
|
75
|
+
"test_failure": {"test"},
|
|
76
|
+
"release_gate_failure": {"test", "evidence"},
|
|
77
|
+
"outcome_miss": {"outcome"},
|
|
78
|
+
"protocol_debt": {"protocol_debt"},
|
|
79
|
+
"guard_violation": {"protocol_debt", "guardian_rule"},
|
|
80
|
+
"guard_check": {"guard_check"},
|
|
81
|
+
"guardian_telemetry": {"guardian_telemetry"},
|
|
82
|
+
"error_repetition": {"error_repetition"},
|
|
83
|
+
"somatic_event": {"somatic_event"},
|
|
84
|
+
"hook_run": {"hook_run"},
|
|
85
|
+
"immune_finding": {"immune_finding"},
|
|
86
|
+
"watchdog_finding": {"watchdog_finding"},
|
|
87
|
+
"daily_self_audit": {"evidence", "protocol_debt"},
|
|
88
|
+
"deep_sleep_finding": {"evidence"},
|
|
89
|
+
"manual_review": {"evidence"},
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
DB_REF_TABLES: dict[str, tuple[str, str]] = {
|
|
93
|
+
"learning": ("learnings", "id"),
|
|
94
|
+
"error_repetition": ("error_repetitions", "id"),
|
|
95
|
+
"somatic_event": ("somatic_events", "id"),
|
|
96
|
+
"guard_check": ("guard_checks", "id"),
|
|
97
|
+
"protocol_debt": ("protocol_debt", "id"),
|
|
98
|
+
"session_correction_requirement": ("session_correction_requirements", "id"),
|
|
99
|
+
"outcome": ("outcomes", "id"),
|
|
100
|
+
"hook_run": ("hook_runs", "id"),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
SECRET_RE = re.compile(
|
|
104
|
+
r"(?i)\b(api[_-]?key|token|secret|password|authorization|bearer|credential|cred_ref)\b"
|
|
105
|
+
r"\s*[:=]\s*['\"]?[^'\"\s,;]+"
|
|
106
|
+
)
|
|
107
|
+
BEARER_RE = re.compile(r"(?i)\bbearer\s+[a-z0-9._~+/=-]{12,}")
|
|
108
|
+
IPV4_RE = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b")
|
|
109
|
+
ABS_PATH_RE = re.compile(r"(?<![\w.-])/(?:Users|home|var|etc|Volumes)/[^\s,;]+")
|
|
110
|
+
RAW_PAYLOAD_MARKER_RE = re.compile(r"(?i)\b(provider_payload|raw_prompt|raw_response|transcript)\b")
|
|
111
|
+
SENSITIVE_METADATA_KEY_RE = re.compile(
|
|
112
|
+
r"(?i)\b(api[_-]?key|token|secret|password|authorization|bearer|credential|cred_ref|"
|
|
113
|
+
r"idempotency[_-]?key|provider_payload|raw_prompt|raw_response|transcript)\b"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _now() -> float:
|
|
118
|
+
return time.time()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _stable_uid(*parts: object) -> str:
|
|
122
|
+
payload = "\0".join(str(part or "") for part in parts)
|
|
123
|
+
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _json(value: Any) -> str:
|
|
127
|
+
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _load_json(value: str, default: Any) -> Any:
|
|
131
|
+
try:
|
|
132
|
+
return json.loads(value or "")
|
|
133
|
+
except Exception:
|
|
134
|
+
return default
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _normalize_text(value: object) -> str:
|
|
138
|
+
clean = re.sub(r"\s+", " ", str(value or "").strip().lower())
|
|
139
|
+
return clean[:1000]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _clip(value: str, limit: int = 500) -> str:
|
|
143
|
+
clean = str(value or "").strip()
|
|
144
|
+
if len(clean) <= limit:
|
|
145
|
+
return clean
|
|
146
|
+
return clean[: limit - 3] + "..."
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _table_exists(conn: sqlite3.Connection, table: str) -> bool:
|
|
150
|
+
row = conn.execute(
|
|
151
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?",
|
|
152
|
+
(table,),
|
|
153
|
+
).fetchone()
|
|
154
|
+
return bool(row)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _ensure_tables(conn: sqlite3.Connection) -> None:
|
|
158
|
+
if _table_exists(conn, "failure_prevention_cases"):
|
|
159
|
+
return
|
|
160
|
+
from db._schema import run_migrations
|
|
161
|
+
|
|
162
|
+
run_migrations(conn)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _as_list(value: Any) -> list[str]:
|
|
166
|
+
if value is None:
|
|
167
|
+
return []
|
|
168
|
+
if isinstance(value, str):
|
|
169
|
+
if not value.strip():
|
|
170
|
+
return []
|
|
171
|
+
try:
|
|
172
|
+
parsed = json.loads(value)
|
|
173
|
+
if isinstance(parsed, list):
|
|
174
|
+
return [str(item).strip() for item in parsed if str(item).strip()]
|
|
175
|
+
except Exception:
|
|
176
|
+
pass
|
|
177
|
+
return [item.strip() for item in value.split(",") if item.strip()]
|
|
178
|
+
if isinstance(value, (list, tuple, set)):
|
|
179
|
+
return [str(item).strip() for item in value if str(item).strip()]
|
|
180
|
+
return [str(value).strip()]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _append_unique(existing_json: str, items: list[str]) -> str:
|
|
184
|
+
existing = _as_list(_load_json(existing_json, []))
|
|
185
|
+
seen = set(existing)
|
|
186
|
+
for item in items:
|
|
187
|
+
clean = str(item or "").strip()
|
|
188
|
+
if clean and clean not in seen:
|
|
189
|
+
existing.append(clean)
|
|
190
|
+
seen.add(clean)
|
|
191
|
+
return _json(existing)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _normalize_failure_type(value: str) -> str:
|
|
195
|
+
clean = str(value or "other").strip().lower()
|
|
196
|
+
return clean if clean in FAILURE_TYPES else "other"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _normalize_source_type(value: str) -> str:
|
|
200
|
+
clean = str(value or "").strip().lower()
|
|
201
|
+
return clean if clean in SOURCE_TYPES else ""
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _normalize_severity(value: str) -> str:
|
|
205
|
+
clean = str(value or "p3").strip().lower()
|
|
206
|
+
return clean if clean in SEVERITIES else "p3"
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _normalize_privacy(value: str) -> str:
|
|
210
|
+
clean = str(value or "normal").strip().lower()
|
|
211
|
+
return clean if clean in PRIVACY_LEVELS else "normal"
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _normalize_surface(value: str) -> str:
|
|
215
|
+
clean = str(value or "audit").strip().lower()
|
|
216
|
+
return clean if clean in SURFACES else "audit"
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _normalize_confidence(value: float | int | str) -> float:
|
|
220
|
+
try:
|
|
221
|
+
raw = float(value)
|
|
222
|
+
except Exception:
|
|
223
|
+
raw = 0.5
|
|
224
|
+
return max(0.0, min(1.0, raw))
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def redact_value(value: object) -> str:
|
|
228
|
+
"""Return a safe, local-only field preview."""
|
|
229
|
+
text = str(value or "")
|
|
230
|
+
if RAW_PAYLOAD_MARKER_RE.search(text):
|
|
231
|
+
return "[redacted_payload]"
|
|
232
|
+
text = SECRET_RE.sub(r"\1=[redacted]", text)
|
|
233
|
+
text = BEARER_RE.sub("Bearer [redacted]", text)
|
|
234
|
+
text = IPV4_RE.sub("[redacted_ip]", text)
|
|
235
|
+
text = ABS_PATH_RE.sub("[redacted_path]", text)
|
|
236
|
+
return _clip(re.sub(r"\s+", " ", text).strip())
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def sanitize_metadata(value: Any, *, _depth: int = 0) -> Any:
|
|
240
|
+
"""Recursively redact metadata before it can reach persistent storage."""
|
|
241
|
+
if _depth > 5:
|
|
242
|
+
return "[redacted_depth]"
|
|
243
|
+
if value is None or isinstance(value, (bool, int, float)):
|
|
244
|
+
return value
|
|
245
|
+
if isinstance(value, bytes):
|
|
246
|
+
return "[redacted_bytes]"
|
|
247
|
+
if isinstance(value, dict):
|
|
248
|
+
clean: dict[str, Any] = {}
|
|
249
|
+
for key, item in value.items():
|
|
250
|
+
key_text = str(key or "").strip()
|
|
251
|
+
if SENSITIVE_METADATA_KEY_RE.search(key_text):
|
|
252
|
+
clean[f"redacted:{_stable_uid(key_text)[:12]}"] = "[redacted]"
|
|
253
|
+
continue
|
|
254
|
+
clean_key = redact_value(key_text)[:120] or "field"
|
|
255
|
+
clean[clean_key] = sanitize_metadata(item, _depth=_depth + 1)
|
|
256
|
+
return clean
|
|
257
|
+
if isinstance(value, (list, tuple, set)):
|
|
258
|
+
return [sanitize_metadata(item, _depth=_depth + 1) for item in list(value)[:50]]
|
|
259
|
+
return redact_value(value)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _idempotency_marker(value: str) -> str:
|
|
263
|
+
clean = str(value or "").strip()
|
|
264
|
+
if not clean:
|
|
265
|
+
return ""
|
|
266
|
+
return f"sha256:{_stable_uid(POLICY_VERSION, 'idempotency_key', clean)[:24]}"
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def field_evidence(
|
|
270
|
+
value: object = "",
|
|
271
|
+
*,
|
|
272
|
+
source_refs: list[str] | None = None,
|
|
273
|
+
confidence: float = 0.5,
|
|
274
|
+
privacy_level: str = "normal",
|
|
275
|
+
value_ref: str = "",
|
|
276
|
+
) -> dict[str, Any]:
|
|
277
|
+
return {
|
|
278
|
+
"value_redacted": redact_value(value),
|
|
279
|
+
"value_ref": _sanitize_ref(value_ref, allow_empty=True),
|
|
280
|
+
"source_refs": [_sanitize_ref(ref) for ref in _as_list(source_refs)],
|
|
281
|
+
"confidence": _normalize_confidence(confidence),
|
|
282
|
+
"privacy_level": _normalize_privacy(privacy_level),
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _surface_allowed(surface: str, allowed_surfaces: Any, *, privacy_level: str) -> bool:
|
|
287
|
+
clean_surface = _normalize_surface(surface)
|
|
288
|
+
privacy = _normalize_privacy(privacy_level)
|
|
289
|
+
if privacy == "secret" and clean_surface not in {"audit", "runtime_internal"}:
|
|
290
|
+
return False
|
|
291
|
+
if privacy in {"private", "sensitive"} and clean_surface in {"pre_action", "export"}:
|
|
292
|
+
return False
|
|
293
|
+
allowed = set(_as_list(allowed_surfaces))
|
|
294
|
+
if not allowed:
|
|
295
|
+
allowed = set(_allowed_surfaces(privacy_level=privacy, status="candidate"))
|
|
296
|
+
return clean_surface in allowed
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _safe_refs(value: Any) -> list[str]:
|
|
300
|
+
return [redact_value(ref) for ref in _as_list(value) if redact_value(ref)]
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _safe_field(value: Any) -> Any:
|
|
304
|
+
if not isinstance(value, dict):
|
|
305
|
+
return value
|
|
306
|
+
clean = dict(value)
|
|
307
|
+
if "value_redacted" in clean:
|
|
308
|
+
clean["value_redacted"] = redact_value(clean.get("value_redacted"))
|
|
309
|
+
if "value_ref" in clean:
|
|
310
|
+
clean["value_ref"] = redact_value(clean.get("value_ref"))
|
|
311
|
+
if "source_refs" in clean:
|
|
312
|
+
clean["source_refs"] = _safe_refs(clean.get("source_refs"))
|
|
313
|
+
return clean
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _safe_case(case: dict[str, Any], *, surface: str = "audit") -> dict[str, Any]:
|
|
317
|
+
if not case:
|
|
318
|
+
return {}
|
|
319
|
+
if not _surface_allowed(surface, case.get("allowed_surfaces"), privacy_level=str(case.get("privacy_level") or "normal")):
|
|
320
|
+
return {}
|
|
321
|
+
clean = dict(case)
|
|
322
|
+
for key in ("entity_refs", "source_event_refs", "evidence_refs", "antibody_refs"):
|
|
323
|
+
clean[key] = _safe_refs(clean.get(key))
|
|
324
|
+
for key in ("primary_source_ref", "area"):
|
|
325
|
+
clean[key] = redact_value(clean.get(key))
|
|
326
|
+
for key in (
|
|
327
|
+
"symptom", "trigger", "missed_signal", "wrong_assumption",
|
|
328
|
+
"root_cause", "corrective_action",
|
|
329
|
+
):
|
|
330
|
+
clean[key] = _safe_field(clean.get(key))
|
|
331
|
+
clean["learning_resolution"] = sanitize_metadata(clean.get("learning_resolution") or {})
|
|
332
|
+
clean["metadata"] = sanitize_metadata(clean.get("metadata") or {})
|
|
333
|
+
return clean
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _safe_source_event(event: dict[str, Any]) -> dict[str, Any]:
|
|
337
|
+
if not event:
|
|
338
|
+
return {}
|
|
339
|
+
clean = dict(event)
|
|
340
|
+
clean["source_ref"] = redact_value(clean.get("source_ref"))
|
|
341
|
+
clean["evidence_refs"] = _safe_refs(clean.get("evidence_refs"))
|
|
342
|
+
clean["metadata"] = sanitize_metadata(clean.get("metadata") or {})
|
|
343
|
+
return clean
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _safe_antibody(antibody: dict[str, Any]) -> dict[str, Any]:
|
|
347
|
+
if not antibody:
|
|
348
|
+
return {}
|
|
349
|
+
clean = dict(antibody)
|
|
350
|
+
for key in ("target_ref", "action_payload_ref", "verification_ref", "approved_ref", "rollback_ref"):
|
|
351
|
+
clean[key] = redact_value(clean.get(key))
|
|
352
|
+
clean["metadata"] = sanitize_metadata(clean.get("metadata") or {})
|
|
353
|
+
return clean
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _ref_prefix(ref: str) -> str:
|
|
357
|
+
clean = str(ref or "").strip()
|
|
358
|
+
if clean.startswith("test:"):
|
|
359
|
+
return "test"
|
|
360
|
+
return clean.split(":", 1)[0].strip().lower() if ":" in clean else ""
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _ref_value(ref: str) -> str:
|
|
364
|
+
clean = str(ref or "").strip()
|
|
365
|
+
return clean.split(":", 1)[1].strip() if ":" in clean else ""
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _sanitize_ref(ref: str, *, allow_empty: bool = False) -> str:
|
|
369
|
+
clean = str(ref or "").strip()
|
|
370
|
+
if not clean:
|
|
371
|
+
if allow_empty:
|
|
372
|
+
return ""
|
|
373
|
+
raise ValueError("ref_required")
|
|
374
|
+
lower = clean.lower()
|
|
375
|
+
if SECRET_RE.search(clean) or BEARER_RE.search(clean) or "credential:" in lower or "cred_ref" in lower:
|
|
376
|
+
raise ValueError("ref_contains_secret")
|
|
377
|
+
if IPV4_RE.search(clean):
|
|
378
|
+
raise ValueError("ref_contains_ip")
|
|
379
|
+
if ABS_PATH_RE.search(clean):
|
|
380
|
+
raise ValueError("ref_contains_sensitive_path")
|
|
381
|
+
if RAW_PAYLOAD_MARKER_RE.search(clean):
|
|
382
|
+
raise ValueError("ref_contains_raw_payload_marker")
|
|
383
|
+
return clean[:300]
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _validate_test_ref(source_ref: str) -> tuple[bool, str]:
|
|
387
|
+
value = _ref_value(source_ref)
|
|
388
|
+
path_text = value.split("::", 1)[0].strip()
|
|
389
|
+
if not path_text or Path(path_text).is_absolute() or ".." in Path(path_text).parts:
|
|
390
|
+
return False, "test_ref_must_be_relative"
|
|
391
|
+
return True, "test_ref_format"
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _validate_db_ref(conn: sqlite3.Connection, prefix: str, source_ref: str, *, source_type: str) -> tuple[bool, str]:
|
|
395
|
+
table, column = DB_REF_TABLES[prefix]
|
|
396
|
+
if not _table_exists(conn, table):
|
|
397
|
+
return False, f"{table}_table_missing"
|
|
398
|
+
ref_value = _ref_value(source_ref)
|
|
399
|
+
try:
|
|
400
|
+
if prefix in {"protocol_debt", "session_correction_requirement", "outcome", "hook_run", "guard_check", "error_repetition", "somatic_event", "learning"}:
|
|
401
|
+
lookup_value: object = int(ref_value)
|
|
402
|
+
else:
|
|
403
|
+
lookup_value = ref_value
|
|
404
|
+
except Exception:
|
|
405
|
+
return False, f"{prefix}_id_invalid"
|
|
406
|
+
row = conn.execute(f"SELECT * FROM {table} WHERE {column} = ?", (lookup_value,)).fetchone()
|
|
407
|
+
if not row:
|
|
408
|
+
return False, f"{prefix}_not_found"
|
|
409
|
+
data = dict(row)
|
|
410
|
+
if source_type == "outcome_miss" and str(data.get("status") or "").lower() != "missed":
|
|
411
|
+
return False, "outcome_not_missed"
|
|
412
|
+
if prefix == "hook_run" and str(data.get("status") or "").lower() not in {"error", "timeout", "blocked", "failed", "fail"}:
|
|
413
|
+
return False, "hook_run_not_failed"
|
|
414
|
+
if prefix == "protocol_debt" and str(data.get("status") or "").lower() not in {"open", "resolved", "forgiven"}:
|
|
415
|
+
return False, "protocol_debt_status_invalid"
|
|
416
|
+
if prefix == "session_correction_requirement" and str(data.get("status") or "").lower() not in {"open", "resolved"}:
|
|
417
|
+
return False, "correction_requirement_status_invalid"
|
|
418
|
+
return True, "db_ref_exists"
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def validate_source_ref(
|
|
422
|
+
source_type: str,
|
|
423
|
+
source_ref: str,
|
|
424
|
+
*,
|
|
425
|
+
conn: sqlite3.Connection | None = None,
|
|
426
|
+
) -> dict[str, Any]:
|
|
427
|
+
"""Validate a source ref enough to decide whether it may reinforce a case."""
|
|
428
|
+
own_conn = conn is None
|
|
429
|
+
conn = conn or get_db()
|
|
430
|
+
clean_type = _normalize_source_type(source_type)
|
|
431
|
+
if not clean_type:
|
|
432
|
+
return {"validated": False, "validator": "source_type", "validation_error": "source_type_invalid"}
|
|
433
|
+
try:
|
|
434
|
+
clean_ref = _sanitize_ref(source_ref)
|
|
435
|
+
except ValueError as exc:
|
|
436
|
+
return {"validated": False, "validator": "ref_sanitize", "validation_error": str(exc)}
|
|
437
|
+
|
|
438
|
+
prefix = _ref_prefix(clean_ref)
|
|
439
|
+
allowed = SOURCE_REF_PREFIXES.get(clean_type, set())
|
|
440
|
+
if prefix not in allowed:
|
|
441
|
+
return {
|
|
442
|
+
"validated": False,
|
|
443
|
+
"validator": "ref_prefix",
|
|
444
|
+
"validation_error": f"ref_prefix_{prefix or 'missing'}_not_allowed_for_{clean_type}",
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
try:
|
|
448
|
+
if prefix in DB_REF_TABLES:
|
|
449
|
+
ok, reason = _validate_db_ref(conn, prefix, clean_ref, source_type=clean_type)
|
|
450
|
+
return {"validated": ok, "validator": "db_ref", "validation_error": "" if ok else reason}
|
|
451
|
+
if prefix == "test":
|
|
452
|
+
ok, reason = _validate_test_ref(clean_ref)
|
|
453
|
+
return {"validated": ok, "validator": "test_ref", "validation_error": "" if ok else reason}
|
|
454
|
+
if prefix in {"guardian_rule", "guardian_telemetry", "immune_finding", "watchdog_finding", "benchmark_case", "evidence"}:
|
|
455
|
+
return {"validated": True, "validator": "format_ref", "validation_error": ""}
|
|
456
|
+
finally:
|
|
457
|
+
if own_conn:
|
|
458
|
+
pass
|
|
459
|
+
|
|
460
|
+
return {"validated": False, "validator": "ref_prefix", "validation_error": f"unsupported_ref_prefix:{prefix}"}
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _status_for_case(*, source_type: str, severity: str, validated: bool, frequency_count: int) -> str:
|
|
464
|
+
if not validated:
|
|
465
|
+
return "candidate"
|
|
466
|
+
if source_type in INFERENCE_ONLY_SOURCES:
|
|
467
|
+
return "candidate"
|
|
468
|
+
if severity in {"p0", "p1"}:
|
|
469
|
+
return "analyzing"
|
|
470
|
+
if severity == "p2" and frequency_count >= 2:
|
|
471
|
+
return "analyzing"
|
|
472
|
+
if severity == "p4":
|
|
473
|
+
return "rejected"
|
|
474
|
+
return "candidate"
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _allowed_surfaces(*, privacy_level: str, status: str) -> list[str]:
|
|
478
|
+
privacy = _normalize_privacy(privacy_level)
|
|
479
|
+
if privacy == "secret":
|
|
480
|
+
return ["audit"]
|
|
481
|
+
if privacy in {"private", "sensitive"}:
|
|
482
|
+
return ["debug_local", "audit"]
|
|
483
|
+
if status in {"verified", "resolved"}:
|
|
484
|
+
return ["debug_local", "audit", "pre_action"]
|
|
485
|
+
return ["debug_local", "audit"]
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _case_from_row(row: sqlite3.Row | None) -> dict[str, Any]:
|
|
489
|
+
if not row:
|
|
490
|
+
return {}
|
|
491
|
+
data = dict(row)
|
|
492
|
+
for key in (
|
|
493
|
+
"entity_refs_json", "source_event_refs_json", "evidence_refs_json",
|
|
494
|
+
"symptom_json", "trigger_json", "missed_signal_json",
|
|
495
|
+
"wrong_assumption_json", "root_cause_json", "corrective_action_json",
|
|
496
|
+
"learning_resolution_json", "antibody_refs_json",
|
|
497
|
+
"allowed_surfaces_json", "metadata_json",
|
|
498
|
+
):
|
|
499
|
+
default = [] if key.endswith("_refs_json") or key in {"antibody_refs_json", "allowed_surfaces_json"} else {}
|
|
500
|
+
data[key[:-5] if key.endswith("_json") else key] = _load_json(str(data.pop(key) or ""), default)
|
|
501
|
+
return data
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _source_event_from_row(row: sqlite3.Row | None) -> dict[str, Any]:
|
|
505
|
+
if not row:
|
|
506
|
+
return {}
|
|
507
|
+
data = dict(row)
|
|
508
|
+
data["validated"] = bool(data.get("validated"))
|
|
509
|
+
data["evidence_refs"] = _load_json(str(data.pop("evidence_refs_json") or ""), [])
|
|
510
|
+
data["metadata"] = _load_json(str(data.pop("metadata_json") or ""), {})
|
|
511
|
+
return data
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _learning_resolution_for_source(conn: sqlite3.Connection, source_type: str, source_ref: str) -> dict[str, Any]:
|
|
515
|
+
if source_type != "outcome_miss" or _ref_prefix(source_ref) != "outcome":
|
|
516
|
+
return {"action": "none", "learning_id": None, "resolver_reason": ""}
|
|
517
|
+
try:
|
|
518
|
+
outcome_id = int(_ref_value(source_ref))
|
|
519
|
+
except Exception:
|
|
520
|
+
return {"action": "none", "learning_id": None, "resolver_reason": "outcome_ref_invalid"}
|
|
521
|
+
row = conn.execute("SELECT learning_id FROM outcomes WHERE id = ?", (outcome_id,)).fetchone()
|
|
522
|
+
if row and int(row["learning_id"] or 0) > 0:
|
|
523
|
+
return {
|
|
524
|
+
"action": "merge",
|
|
525
|
+
"learning_id": int(row["learning_id"]),
|
|
526
|
+
"resolver_reason": "outcome_already_linked_learning_no_duplicate",
|
|
527
|
+
}
|
|
528
|
+
return {"action": "none", "learning_id": None, "resolver_reason": "outcome_has_no_learning"}
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def ingest_failure(
|
|
532
|
+
*,
|
|
533
|
+
failure_type: str,
|
|
534
|
+
area: str,
|
|
535
|
+
primary_source_type: str,
|
|
536
|
+
primary_source_ref: str,
|
|
537
|
+
symptom: object,
|
|
538
|
+
trigger: object = "",
|
|
539
|
+
missed_signal: object = "",
|
|
540
|
+
wrong_assumption: object = "",
|
|
541
|
+
root_cause: object = "",
|
|
542
|
+
corrective_action: object = "",
|
|
543
|
+
severity: str = "p3",
|
|
544
|
+
confidence: float = 0.5,
|
|
545
|
+
entity_refs: list[str] | str | None = None,
|
|
546
|
+
evidence_refs: list[str] | str | None = None,
|
|
547
|
+
privacy_level: str = "normal",
|
|
548
|
+
observed_at: float | None = None,
|
|
549
|
+
idempotency_key: str = "",
|
|
550
|
+
metadata: dict[str, Any] | None = None,
|
|
551
|
+
conn: sqlite3.Connection | None = None,
|
|
552
|
+
) -> dict[str, Any]:
|
|
553
|
+
"""Create or reinforce a redacted failure-prevention case."""
|
|
554
|
+
conn = conn or get_db()
|
|
555
|
+
_ensure_tables(conn)
|
|
556
|
+
now = float(observed_at or _now())
|
|
557
|
+
clean_type = _normalize_failure_type(failure_type)
|
|
558
|
+
clean_source_type = _normalize_source_type(primary_source_type)
|
|
559
|
+
try:
|
|
560
|
+
clean_source_ref = _sanitize_ref(primary_source_ref)
|
|
561
|
+
clean_evidence_refs = [_sanitize_ref(ref) for ref in _as_list(evidence_refs)]
|
|
562
|
+
clean_entity_refs = [_sanitize_ref(ref) for ref in _as_list(entity_refs)]
|
|
563
|
+
except ValueError as exc:
|
|
564
|
+
return {"ok": False, "error": str(exc)}
|
|
565
|
+
clean_severity = _normalize_severity(severity)
|
|
566
|
+
clean_privacy = _normalize_privacy(privacy_level)
|
|
567
|
+
clean_confidence = _normalize_confidence(confidence)
|
|
568
|
+
clean_area = str(area or "").strip()[:160]
|
|
569
|
+
clean_metadata = sanitize_metadata(metadata or {})
|
|
570
|
+
if not isinstance(clean_metadata, dict):
|
|
571
|
+
clean_metadata = {"value": clean_metadata}
|
|
572
|
+
|
|
573
|
+
validation = validate_source_ref(clean_source_type, clean_source_ref, conn=conn)
|
|
574
|
+
validated = bool(validation.get("validated"))
|
|
575
|
+
symptom_field = field_evidence(symptom, source_refs=[clean_source_ref], confidence=clean_confidence, privacy_level=clean_privacy)
|
|
576
|
+
trigger_field = field_evidence(trigger, source_refs=[clean_source_ref], confidence=clean_confidence, privacy_level=clean_privacy)
|
|
577
|
+
missed_signal_field = field_evidence(missed_signal, source_refs=[clean_source_ref], confidence=clean_confidence, privacy_level=clean_privacy)
|
|
578
|
+
wrong_assumption_field = field_evidence(wrong_assumption, source_refs=[clean_source_ref], confidence=clean_confidence, privacy_level=clean_privacy)
|
|
579
|
+
root_cause_field = field_evidence(root_cause, source_refs=[clean_source_ref], confidence=clean_confidence, privacy_level=clean_privacy)
|
|
580
|
+
corrective_action_field = field_evidence(corrective_action, source_refs=[clean_source_ref], confidence=clean_confidence, privacy_level=clean_privacy)
|
|
581
|
+
failure_uid = _stable_uid(
|
|
582
|
+
POLICY_VERSION,
|
|
583
|
+
clean_type,
|
|
584
|
+
clean_area,
|
|
585
|
+
_normalize_text(symptom_field["value_redacted"]),
|
|
586
|
+
)
|
|
587
|
+
source_event_uid = _stable_uid(POLICY_VERSION, failure_uid, clean_source_type, clean_source_ref)
|
|
588
|
+
learning_resolution = _learning_resolution_for_source(conn, clean_source_type, clean_source_ref)
|
|
589
|
+
source_ref_token = f"{clean_source_type}:{clean_source_ref}"
|
|
590
|
+
|
|
591
|
+
conn.execute(
|
|
592
|
+
"""
|
|
593
|
+
INSERT OR IGNORE INTO failure_prevention_cases (
|
|
594
|
+
failure_uid, policy_version, failure_type, area, entity_refs_json,
|
|
595
|
+
primary_source_type, primary_source_ref, source_event_refs_json,
|
|
596
|
+
evidence_refs_json, symptom_json, trigger_json, missed_signal_json,
|
|
597
|
+
wrong_assumption_json, root_cause_json, corrective_action_json,
|
|
598
|
+
severity, frequency_count, confidence, status, learning_resolution_json,
|
|
599
|
+
antibody_refs_json, privacy_level, allowed_surfaces_json, opened_at,
|
|
600
|
+
updated_at, review_due_at, expires_at, false_positive_count,
|
|
601
|
+
last_triggered_at, metadata_json
|
|
602
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, 'candidate', ?, '[]', ?, ?, ?, ?, ?, ?, 0, ?, ?)
|
|
603
|
+
""",
|
|
604
|
+
(
|
|
605
|
+
failure_uid,
|
|
606
|
+
POLICY_VERSION,
|
|
607
|
+
clean_type,
|
|
608
|
+
clean_area,
|
|
609
|
+
_json(clean_entity_refs),
|
|
610
|
+
clean_source_type,
|
|
611
|
+
clean_source_ref,
|
|
612
|
+
_json([source_ref_token]),
|
|
613
|
+
_json(clean_evidence_refs),
|
|
614
|
+
_json(symptom_field),
|
|
615
|
+
_json(trigger_field),
|
|
616
|
+
_json(missed_signal_field),
|
|
617
|
+
_json(wrong_assumption_field),
|
|
618
|
+
_json(root_cause_field),
|
|
619
|
+
_json(corrective_action_field),
|
|
620
|
+
clean_severity,
|
|
621
|
+
clean_confidence,
|
|
622
|
+
_json(learning_resolution),
|
|
623
|
+
clean_privacy,
|
|
624
|
+
_json(_allowed_surfaces(privacy_level=clean_privacy, status="candidate")),
|
|
625
|
+
now,
|
|
626
|
+
now,
|
|
627
|
+
now + 14 * 86400,
|
|
628
|
+
now + 90 * 86400,
|
|
629
|
+
now,
|
|
630
|
+
_json(clean_metadata),
|
|
631
|
+
),
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
cursor = conn.execute(
|
|
635
|
+
"""
|
|
636
|
+
INSERT OR IGNORE INTO failure_source_events (
|
|
637
|
+
source_event_uid, failure_uid, policy_version, source_type,
|
|
638
|
+
source_ref, evidence_refs_json, observed_at, validated, validator,
|
|
639
|
+
validation_error, privacy_level, created_at, updated_at, metadata_json
|
|
640
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
641
|
+
""",
|
|
642
|
+
(
|
|
643
|
+
source_event_uid,
|
|
644
|
+
failure_uid,
|
|
645
|
+
POLICY_VERSION,
|
|
646
|
+
clean_source_type,
|
|
647
|
+
clean_source_ref,
|
|
648
|
+
_json(clean_evidence_refs),
|
|
649
|
+
now,
|
|
650
|
+
1 if validated else 0,
|
|
651
|
+
str(validation.get("validator") or ""),
|
|
652
|
+
str(validation.get("validation_error") or ""),
|
|
653
|
+
clean_privacy,
|
|
654
|
+
now,
|
|
655
|
+
now,
|
|
656
|
+
_json({"idempotency_key_hash": _idempotency_marker(idempotency_key), **clean_metadata}),
|
|
657
|
+
),
|
|
658
|
+
)
|
|
659
|
+
source_inserted = cursor.rowcount > 0
|
|
660
|
+
if source_inserted and validated:
|
|
661
|
+
conn.execute(
|
|
662
|
+
"""
|
|
663
|
+
UPDATE failure_prevention_cases
|
|
664
|
+
SET frequency_count = frequency_count + 1,
|
|
665
|
+
last_triggered_at = ?,
|
|
666
|
+
updated_at = ?,
|
|
667
|
+
source_event_refs_json = ?,
|
|
668
|
+
evidence_refs_json = ?,
|
|
669
|
+
learning_resolution_json = ?
|
|
670
|
+
WHERE failure_uid = ?
|
|
671
|
+
""",
|
|
672
|
+
(
|
|
673
|
+
now,
|
|
674
|
+
now,
|
|
675
|
+
_append_unique(
|
|
676
|
+
conn.execute(
|
|
677
|
+
"SELECT source_event_refs_json FROM failure_prevention_cases WHERE failure_uid = ?",
|
|
678
|
+
(failure_uid,),
|
|
679
|
+
).fetchone()["source_event_refs_json"],
|
|
680
|
+
[source_ref_token],
|
|
681
|
+
),
|
|
682
|
+
_append_unique(
|
|
683
|
+
conn.execute(
|
|
684
|
+
"SELECT evidence_refs_json FROM failure_prevention_cases WHERE failure_uid = ?",
|
|
685
|
+
(failure_uid,),
|
|
686
|
+
).fetchone()["evidence_refs_json"],
|
|
687
|
+
clean_evidence_refs,
|
|
688
|
+
),
|
|
689
|
+
_json(learning_resolution),
|
|
690
|
+
failure_uid,
|
|
691
|
+
),
|
|
692
|
+
)
|
|
693
|
+
elif source_inserted:
|
|
694
|
+
clean_metadata["validation_error"] = str(validation.get("validation_error") or "")
|
|
695
|
+
conn.execute(
|
|
696
|
+
"UPDATE failure_prevention_cases SET metadata_json = ?, updated_at = ? WHERE failure_uid = ?",
|
|
697
|
+
(_json(clean_metadata), now, failure_uid),
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
row = conn.execute("SELECT * FROM failure_prevention_cases WHERE failure_uid = ?", (failure_uid,)).fetchone()
|
|
701
|
+
frequency_count = int(row["frequency_count"] or 0) if row else 0
|
|
702
|
+
next_status = _status_for_case(
|
|
703
|
+
source_type=clean_source_type,
|
|
704
|
+
severity=clean_severity,
|
|
705
|
+
validated=validated,
|
|
706
|
+
frequency_count=frequency_count,
|
|
707
|
+
)
|
|
708
|
+
if row and str(row["status"] or "") not in {"verified", "resolved", "rolled_back"}:
|
|
709
|
+
conn.execute(
|
|
710
|
+
"""
|
|
711
|
+
UPDATE failure_prevention_cases
|
|
712
|
+
SET status = ?, allowed_surfaces_json = ?, updated_at = ?
|
|
713
|
+
WHERE failure_uid = ?
|
|
714
|
+
""",
|
|
715
|
+
(
|
|
716
|
+
next_status,
|
|
717
|
+
_json(_allowed_surfaces(privacy_level=clean_privacy, status=next_status)),
|
|
718
|
+
now,
|
|
719
|
+
failure_uid,
|
|
720
|
+
),
|
|
721
|
+
)
|
|
722
|
+
conn.commit()
|
|
723
|
+
case_row = conn.execute("SELECT * FROM failure_prevention_cases WHERE failure_uid = ?", (failure_uid,)).fetchone()
|
|
724
|
+
event_row = conn.execute("SELECT * FROM failure_source_events WHERE source_event_uid = ?", (source_event_uid,)).fetchone()
|
|
725
|
+
return {
|
|
726
|
+
"ok": True,
|
|
727
|
+
"failure_uid": failure_uid,
|
|
728
|
+
"source_event_uid": source_event_uid,
|
|
729
|
+
"source_event_inserted": source_inserted,
|
|
730
|
+
"validated": validated,
|
|
731
|
+
"validation_error": str(validation.get("validation_error") or ""),
|
|
732
|
+
"case": _safe_case(_case_from_row(case_row), surface="audit"),
|
|
733
|
+
"source_event": _safe_source_event(_source_event_from_row(event_row)),
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def _case_exists(conn: sqlite3.Connection, failure_uid: str) -> bool:
|
|
738
|
+
return bool(conn.execute("SELECT 1 FROM failure_prevention_cases WHERE failure_uid = ?", (failure_uid,)).fetchone())
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def _validate_action_ref(ref: str, *, field: str, allow_empty: bool = True) -> str:
|
|
742
|
+
if not ref and allow_empty:
|
|
743
|
+
return ""
|
|
744
|
+
clean = _sanitize_ref(ref, allow_empty=allow_empty)
|
|
745
|
+
return clean
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def _validate_antibody_policy(
|
|
749
|
+
*,
|
|
750
|
+
action_type: str,
|
|
751
|
+
target_system: str,
|
|
752
|
+
target_ref: str,
|
|
753
|
+
activation_policy: str,
|
|
754
|
+
verification_ref: str,
|
|
755
|
+
verification_status: str,
|
|
756
|
+
approved_ref: str,
|
|
757
|
+
rollback_ref: str,
|
|
758
|
+
source_type: str,
|
|
759
|
+
) -> None:
|
|
760
|
+
if action_type not in ACTION_TYPES:
|
|
761
|
+
raise ValueError("action_type_invalid")
|
|
762
|
+
if target_system not in TARGET_SYSTEMS:
|
|
763
|
+
raise ValueError("target_system_invalid")
|
|
764
|
+
if not target_ref.strip():
|
|
765
|
+
raise ValueError("target_ref_required")
|
|
766
|
+
if activation_policy not in ACTIVATION_POLICIES:
|
|
767
|
+
raise ValueError("activation_policy_invalid")
|
|
768
|
+
if verification_status not in VERIFICATION_STATUSES:
|
|
769
|
+
raise ValueError("verification_status_invalid")
|
|
770
|
+
if source_type in INFERENCE_ONLY_SOURCES and activation_policy != "candidate_only":
|
|
771
|
+
raise ValueError("inference_source_must_remain_candidate_only")
|
|
772
|
+
if verification_status == "not_applicable":
|
|
773
|
+
if action_type not in NOT_APPLICABLE_ACTIONS or activation_policy in {"warn", "block_after_verification"}:
|
|
774
|
+
raise ValueError("not_applicable_verification_not_allowed")
|
|
775
|
+
if activation_policy == "warn" and not verification_ref:
|
|
776
|
+
raise ValueError("warn_requires_verification_ref")
|
|
777
|
+
if activation_policy in {"warn", "block_after_verification"} and verification_ref and not _ref_prefix(verification_ref):
|
|
778
|
+
raise ValueError("verification_ref_must_be_traceable_ref")
|
|
779
|
+
if activation_policy == "block_after_verification":
|
|
780
|
+
if verification_status != "passed" or not verification_ref or not rollback_ref:
|
|
781
|
+
raise ValueError("block_requires_passed_verification_and_rollback")
|
|
782
|
+
if not _ref_prefix(rollback_ref):
|
|
783
|
+
raise ValueError("rollback_ref_must_be_traceable_ref")
|
|
784
|
+
if activation_policy == "manual_approval_required":
|
|
785
|
+
prefix = _ref_prefix(approved_ref)
|
|
786
|
+
if prefix not in APPROVAL_REF_PREFIXES:
|
|
787
|
+
raise ValueError("manual_approval_requires_traceable_approved_ref")
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def propose_antibody_action(
|
|
791
|
+
*,
|
|
792
|
+
failure_uid: str,
|
|
793
|
+
action_type: str,
|
|
794
|
+
target_system: str,
|
|
795
|
+
target_ref: str,
|
|
796
|
+
action_payload_ref: str = "",
|
|
797
|
+
activation_policy: str = "candidate_only",
|
|
798
|
+
required_verification: str = "",
|
|
799
|
+
verification_ref: str = "",
|
|
800
|
+
verification_status: str = "missing",
|
|
801
|
+
approved_by: str = "",
|
|
802
|
+
approved_ref: str = "",
|
|
803
|
+
rollback_ref: str = "",
|
|
804
|
+
privacy_level: str = "normal",
|
|
805
|
+
metadata: dict[str, Any] | None = None,
|
|
806
|
+
conn: sqlite3.Connection | None = None,
|
|
807
|
+
) -> dict[str, Any]:
|
|
808
|
+
"""Record a proposed owner action without executing it."""
|
|
809
|
+
conn = conn or get_db()
|
|
810
|
+
_ensure_tables(conn)
|
|
811
|
+
clean_failure_uid = str(failure_uid or "").strip()
|
|
812
|
+
if not _case_exists(conn, clean_failure_uid):
|
|
813
|
+
return {"ok": False, "error": "failure_case_not_found"}
|
|
814
|
+
case = conn.execute(
|
|
815
|
+
"SELECT primary_source_type FROM failure_prevention_cases WHERE failure_uid = ?",
|
|
816
|
+
(clean_failure_uid,),
|
|
817
|
+
).fetchone()
|
|
818
|
+
clean_action = str(action_type or "").strip()
|
|
819
|
+
clean_target_system = str(target_system or "").strip()
|
|
820
|
+
try:
|
|
821
|
+
clean_target_ref = _sanitize_ref(target_ref)
|
|
822
|
+
clean_payload_ref = _validate_action_ref(action_payload_ref, field="action_payload_ref")
|
|
823
|
+
clean_verification_ref = _validate_action_ref(verification_ref, field="verification_ref")
|
|
824
|
+
clean_approved_ref = _validate_action_ref(approved_ref, field="approved_ref")
|
|
825
|
+
clean_rollback_ref = _validate_action_ref(rollback_ref, field="rollback_ref")
|
|
826
|
+
except ValueError as exc:
|
|
827
|
+
return {"ok": False, "error": str(exc)}
|
|
828
|
+
clean_policy = str(activation_policy or "candidate_only").strip()
|
|
829
|
+
clean_verification_status = str(verification_status or "missing").strip()
|
|
830
|
+
clean_privacy = _normalize_privacy(privacy_level)
|
|
831
|
+
source_type = str(case["primary_source_type"] or "") if case else ""
|
|
832
|
+
|
|
833
|
+
try:
|
|
834
|
+
_validate_antibody_policy(
|
|
835
|
+
action_type=clean_action,
|
|
836
|
+
target_system=clean_target_system,
|
|
837
|
+
target_ref=clean_target_ref,
|
|
838
|
+
activation_policy=clean_policy,
|
|
839
|
+
verification_ref=clean_verification_ref,
|
|
840
|
+
verification_status=clean_verification_status,
|
|
841
|
+
approved_ref=clean_approved_ref,
|
|
842
|
+
rollback_ref=clean_rollback_ref,
|
|
843
|
+
source_type=source_type,
|
|
844
|
+
)
|
|
845
|
+
except ValueError as exc:
|
|
846
|
+
return {"ok": False, "error": str(exc)}
|
|
847
|
+
|
|
848
|
+
clean_metadata = sanitize_metadata(metadata or {})
|
|
849
|
+
if not isinstance(clean_metadata, dict):
|
|
850
|
+
clean_metadata = {"value": clean_metadata}
|
|
851
|
+
learning_resolution = None
|
|
852
|
+
if clean_action == "learning_resolve" and isinstance(clean_metadata.get("learning_candidate"), dict):
|
|
853
|
+
candidate = clean_metadata["learning_candidate"]
|
|
854
|
+
learning_resolution = resolve_learning_candidate(
|
|
855
|
+
category=str(candidate.get("category") or "nexo-ops"),
|
|
856
|
+
title=str(candidate.get("title") or ""),
|
|
857
|
+
content=str(candidate.get("content") or ""),
|
|
858
|
+
reasoning=str(candidate.get("reasoning") or ""),
|
|
859
|
+
prevention=str(candidate.get("prevention") or ""),
|
|
860
|
+
applies_to=str(candidate.get("applies_to") or ""),
|
|
861
|
+
priority=str(candidate.get("priority") or "medium"),
|
|
862
|
+
source_authority=str(candidate.get("source_authority") or "inference"),
|
|
863
|
+
conn=conn,
|
|
864
|
+
)
|
|
865
|
+
clean_metadata["learning_resolution"] = learning_resolution
|
|
866
|
+
conn.execute(
|
|
867
|
+
"UPDATE failure_prevention_cases SET learning_resolution_json = ?, updated_at = ? WHERE failure_uid = ?",
|
|
868
|
+
(_json(learning_resolution), _now(), clean_failure_uid),
|
|
869
|
+
)
|
|
870
|
+
|
|
871
|
+
now = _now()
|
|
872
|
+
antibody_uid = _stable_uid(POLICY_VERSION, clean_failure_uid, clean_action, clean_target_system, clean_target_ref)
|
|
873
|
+
status = "approved" if clean_policy == "manual_approval_required" else "proposed"
|
|
874
|
+
if clean_verification_status == "passed":
|
|
875
|
+
status = "verified"
|
|
876
|
+
conn.execute(
|
|
877
|
+
"""
|
|
878
|
+
INSERT OR IGNORE INTO antibody_actions (
|
|
879
|
+
antibody_uid, failure_uid, policy_version, action_type, target_system,
|
|
880
|
+
target_ref, action_payload_ref, status, activation_policy,
|
|
881
|
+
required_verification, verification_ref, verification_status,
|
|
882
|
+
approved_by, approved_ref, rollback_ref, review_due_at, expires_at,
|
|
883
|
+
privacy_level, created_at, updated_at, verified_at, metadata_json
|
|
884
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
885
|
+
""",
|
|
886
|
+
(
|
|
887
|
+
antibody_uid,
|
|
888
|
+
clean_failure_uid,
|
|
889
|
+
POLICY_VERSION,
|
|
890
|
+
clean_action,
|
|
891
|
+
clean_target_system,
|
|
892
|
+
clean_target_ref,
|
|
893
|
+
clean_payload_ref,
|
|
894
|
+
status,
|
|
895
|
+
clean_policy,
|
|
896
|
+
redact_value(str(required_verification or "").strip()),
|
|
897
|
+
clean_verification_ref,
|
|
898
|
+
clean_verification_status,
|
|
899
|
+
redact_value(str(approved_by or "").strip())[:160],
|
|
900
|
+
clean_approved_ref,
|
|
901
|
+
clean_rollback_ref,
|
|
902
|
+
now + 14 * 86400,
|
|
903
|
+
now + 90 * 86400,
|
|
904
|
+
clean_privacy,
|
|
905
|
+
now,
|
|
906
|
+
now,
|
|
907
|
+
now if clean_verification_status == "passed" else None,
|
|
908
|
+
_json(clean_metadata),
|
|
909
|
+
),
|
|
910
|
+
)
|
|
911
|
+
conn.execute(
|
|
912
|
+
"UPDATE failure_prevention_cases SET antibody_refs_json = ?, updated_at = ? WHERE failure_uid = ?",
|
|
913
|
+
(
|
|
914
|
+
_append_unique(
|
|
915
|
+
conn.execute(
|
|
916
|
+
"SELECT antibody_refs_json FROM failure_prevention_cases WHERE failure_uid = ?",
|
|
917
|
+
(clean_failure_uid,),
|
|
918
|
+
).fetchone()["antibody_refs_json"],
|
|
919
|
+
[f"antibody:{antibody_uid}"],
|
|
920
|
+
),
|
|
921
|
+
now,
|
|
922
|
+
clean_failure_uid,
|
|
923
|
+
),
|
|
924
|
+
)
|
|
925
|
+
conn.commit()
|
|
926
|
+
row = conn.execute("SELECT * FROM antibody_actions WHERE antibody_uid = ?", (antibody_uid,)).fetchone()
|
|
927
|
+
data = dict(row) if row else {}
|
|
928
|
+
if data:
|
|
929
|
+
data["metadata"] = _load_json(str(data.pop("metadata_json") or ""), {})
|
|
930
|
+
return {"ok": True, "antibody_uid": antibody_uid, "antibody": _safe_antibody(data), "learning_resolution": sanitize_metadata(learning_resolution)}
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def list_failure_cases(*, status: str = "", limit: int = 20, surface: str = "audit", conn: sqlite3.Connection | None = None) -> list[dict[str, Any]]:
|
|
934
|
+
conn = conn or get_db()
|
|
935
|
+
_ensure_tables(conn)
|
|
936
|
+
clauses = []
|
|
937
|
+
params: list[Any] = []
|
|
938
|
+
if status:
|
|
939
|
+
clauses.append("status = ?")
|
|
940
|
+
params.append(status)
|
|
941
|
+
where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
|
|
942
|
+
rows = conn.execute(
|
|
943
|
+
f"SELECT * FROM failure_prevention_cases {where} ORDER BY updated_at DESC LIMIT ?",
|
|
944
|
+
params + [max(1, int(limit or 20))],
|
|
945
|
+
).fetchall()
|
|
946
|
+
cases = [_safe_case(_case_from_row(row), surface=surface) for row in rows]
|
|
947
|
+
return [case for case in cases if case]
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def get_failure_case(failure_uid: str, *, surface: str = "audit", conn: sqlite3.Connection | None = None) -> dict[str, Any]:
|
|
951
|
+
conn = conn or get_db()
|
|
952
|
+
_ensure_tables(conn)
|
|
953
|
+
row = conn.execute("SELECT * FROM failure_prevention_cases WHERE failure_uid = ?", (failure_uid,)).fetchone()
|
|
954
|
+
return _safe_case(_case_from_row(row), surface=surface)
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
def mark_false_positive(
|
|
958
|
+
failure_uid: str,
|
|
959
|
+
*,
|
|
960
|
+
antibody_uid: str = "",
|
|
961
|
+
reason: str = "",
|
|
962
|
+
conn: sqlite3.Connection | None = None,
|
|
963
|
+
) -> dict[str, Any]:
|
|
964
|
+
conn = conn or get_db()
|
|
965
|
+
_ensure_tables(conn)
|
|
966
|
+
clean_failure_uid = str(failure_uid or "").strip()
|
|
967
|
+
if not _case_exists(conn, clean_failure_uid):
|
|
968
|
+
return {"ok": False, "error": "failure_case_not_found"}
|
|
969
|
+
now = _now()
|
|
970
|
+
conn.execute(
|
|
971
|
+
"""
|
|
972
|
+
UPDATE failure_prevention_cases
|
|
973
|
+
SET false_positive_count = false_positive_count + 1,
|
|
974
|
+
updated_at = ?
|
|
975
|
+
WHERE failure_uid = ?
|
|
976
|
+
""",
|
|
977
|
+
(now, clean_failure_uid),
|
|
978
|
+
)
|
|
979
|
+
row = conn.execute("SELECT false_positive_count FROM failure_prevention_cases WHERE failure_uid = ?", (clean_failure_uid,)).fetchone()
|
|
980
|
+
count = int(row["false_positive_count"] or 0)
|
|
981
|
+
if count >= 2:
|
|
982
|
+
conn.execute(
|
|
983
|
+
"UPDATE failure_prevention_cases SET status = 'conflict_review', allowed_surfaces_json = ?, updated_at = ? WHERE failure_uid = ?",
|
|
984
|
+
(_json(["debug_local", "audit"]), now, clean_failure_uid),
|
|
985
|
+
)
|
|
986
|
+
if antibody_uid:
|
|
987
|
+
conn.execute(
|
|
988
|
+
"""
|
|
989
|
+
UPDATE antibody_actions
|
|
990
|
+
SET status = 'false_positive',
|
|
991
|
+
activation_policy = 'candidate_only',
|
|
992
|
+
metadata_json = ?,
|
|
993
|
+
updated_at = ?
|
|
994
|
+
WHERE antibody_uid = ?
|
|
995
|
+
""",
|
|
996
|
+
(_json({"false_positive_reason": redact_value(reason)}), now, antibody_uid),
|
|
997
|
+
)
|
|
998
|
+
conn.commit()
|
|
999
|
+
return {"ok": True, "false_positive_count": count, "case": get_failure_case(clean_failure_uid, conn=conn)}
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
def rollback_antibody_action(
|
|
1003
|
+
antibody_uid: str,
|
|
1004
|
+
*,
|
|
1005
|
+
rollback_ref: str,
|
|
1006
|
+
reason: str = "",
|
|
1007
|
+
conn: sqlite3.Connection | None = None,
|
|
1008
|
+
) -> dict[str, Any]:
|
|
1009
|
+
conn = conn or get_db()
|
|
1010
|
+
_ensure_tables(conn)
|
|
1011
|
+
clean_uid = str(antibody_uid or "").strip()
|
|
1012
|
+
try:
|
|
1013
|
+
clean_rollback_ref = _validate_action_ref(rollback_ref, field="rollback_ref", allow_empty=False)
|
|
1014
|
+
except ValueError as exc:
|
|
1015
|
+
return {"ok": False, "error": str(exc)}
|
|
1016
|
+
row = conn.execute("SELECT failure_uid FROM antibody_actions WHERE antibody_uid = ?", (clean_uid,)).fetchone()
|
|
1017
|
+
if not row:
|
|
1018
|
+
return {"ok": False, "error": "antibody_not_found"}
|
|
1019
|
+
now = _now()
|
|
1020
|
+
conn.execute(
|
|
1021
|
+
"""
|
|
1022
|
+
UPDATE antibody_actions
|
|
1023
|
+
SET status = 'rolled_back',
|
|
1024
|
+
activation_policy = 'candidate_only',
|
|
1025
|
+
rollback_ref = ?,
|
|
1026
|
+
metadata_json = ?,
|
|
1027
|
+
updated_at = ?
|
|
1028
|
+
WHERE antibody_uid = ?
|
|
1029
|
+
""",
|
|
1030
|
+
(clean_rollback_ref, _json({"rollback_reason": redact_value(reason)}), now, clean_uid),
|
|
1031
|
+
)
|
|
1032
|
+
conn.execute(
|
|
1033
|
+
"UPDATE failure_prevention_cases SET status = 'rolled_back', updated_at = ? WHERE failure_uid = ?",
|
|
1034
|
+
(now, row["failure_uid"]),
|
|
1035
|
+
)
|
|
1036
|
+
conn.commit()
|
|
1037
|
+
return {"ok": True, "antibody_uid": clean_uid, "failure_uid": row["failure_uid"]}
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
__all__ = [
|
|
1041
|
+
"POLICY_VERSION",
|
|
1042
|
+
"field_evidence",
|
|
1043
|
+
"get_failure_case",
|
|
1044
|
+
"ingest_failure",
|
|
1045
|
+
"list_failure_cases",
|
|
1046
|
+
"mark_false_positive",
|
|
1047
|
+
"propose_antibody_action",
|
|
1048
|
+
"redact_value",
|
|
1049
|
+
"rollback_antibody_action",
|
|
1050
|
+
"sanitize_metadata",
|
|
1051
|
+
"validate_source_ref",
|
|
1052
|
+
]
|