openclaw-agent-dashboard 1.0.39 → 1.0.40

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.
Files changed (54) hide show
  1. package/dashboard/api/agent_config_api.py +28 -7
  2. package/dashboard/api/agents.py +48 -10
  3. package/dashboard/api/agents_config.py +5 -1
  4. package/dashboard/api/chains.py +25 -5
  5. package/dashboard/api/collaboration.py +10 -9
  6. package/dashboard/api/debug_paths.py +5 -1
  7. package/dashboard/api/error_analysis.py +29 -11
  8. package/dashboard/api/errors.py +27 -11
  9. package/dashboard/api/fortify_routes.py +80 -0
  10. package/dashboard/api/input_safety.py +60 -0
  11. package/dashboard/api/performance.py +73 -53
  12. package/dashboard/api/subagents.py +95 -99
  13. package/dashboard/api/timeline.py +24 -3
  14. package/dashboard/api/version.py +2 -0
  15. package/dashboard/api/websocket.py +9 -7
  16. package/dashboard/core/__init__.py +1 -0
  17. package/dashboard/core/config_fortify.py +112 -0
  18. package/dashboard/core/error_handler.py +339 -0
  19. package/dashboard/core/fallback_manager.py +70 -0
  20. package/dashboard/core/safe_api_error.py +76 -0
  21. package/dashboard/core/schemas/__init__.py +16 -0
  22. package/dashboard/core/schemas/base.py +43 -0
  23. package/dashboard/core/schemas/session_schema.py +40 -0
  24. package/dashboard/core/schemas/subagent_schema.py +23 -0
  25. package/dashboard/data/agent_config_manager.py +6 -4
  26. package/dashboard/data/chain_reader.py +16 -12
  27. package/dashboard/data/error_analyzer.py +15 -11
  28. package/dashboard/data/session_reader.py +268 -46
  29. package/dashboard/data/subagent_reader.py +74 -49
  30. package/dashboard/data/timeline_reader.py +35 -49
  31. package/dashboard/main.py +24 -2
  32. package/dashboard/mechanism_reader.py +4 -5
  33. package/dashboard/mechanisms.py +2 -2
  34. package/dashboard/pytest.ini +3 -0
  35. package/dashboard/requirements.txt +5 -0
  36. package/dashboard/status/cache_fp_probe.py +40 -0
  37. package/dashboard/status/status_cache.py +199 -72
  38. package/dashboard/status/status_calculator.py +50 -30
  39. package/dashboard/tests/conftest.py +84 -0
  40. package/dashboard/tests/test_api_contracts.py +372 -0
  41. package/dashboard/tests/test_bench_fortify.py +176 -0
  42. package/dashboard/tests/test_fortify.py +741 -0
  43. package/dashboard/utils/__init__.py +1 -0
  44. package/dashboard/utils/data_repair.py +210 -0
  45. package/dashboard/watchers/file_watcher.py +367 -77
  46. package/openclaw.plugin.json +1 -1
  47. package/package.json +1 -1
  48. package/dashboard/agents.py +0 -74
  49. package/dashboard/collaboration.py +0 -407
  50. package/dashboard/errors.py +0 -63
  51. package/dashboard/performance.py +0 -474
  52. package/dashboard/session_reader.py +0 -240
  53. package/dashboard/status_calculator.py +0 -121
  54. package/dashboard/subagent_reader.py +0 -232
@@ -0,0 +1 @@
1
+ """Shared utilities (data repair, etc.)."""
@@ -0,0 +1,210 @@
1
+ """
2
+ JSON line repair (memory-first), optional write-back with backup, audit logging.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ import hashlib
7
+ import json
8
+ import logging
9
+ import re
10
+ import shutil
11
+ from pathlib import Path
12
+ from typing import Any, Dict, List, Optional, Tuple
13
+
14
+ from core.config_fortify import get_fortify_config
15
+ from core.schemas.base import SchemaValidator
16
+ from core.schemas.session_schema import session_envelope_schema, session_message_schema
17
+
18
+ _audit_log = logging.getLogger("openclaw.fortify.audit")
19
+
20
+
21
+ def _ensure_audit_logging() -> None:
22
+ if _audit_log.handlers:
23
+ return
24
+ h = logging.StreamHandler()
25
+ h.setFormatter(logging.Formatter("%(asctime)s | AUDIT | %(message)s"))
26
+ _audit_log.addHandler(h)
27
+ _audit_log.setLevel(logging.INFO)
28
+
29
+
30
+ def _truncate(s: str, max_len: int = 500) -> str:
31
+ s = s or ""
32
+ if len(s) <= max_len:
33
+ return s
34
+ return s[:max_len] + f"...({len(s)} chars)"
35
+
36
+
37
+ def audit_repair(
38
+ operation: str,
39
+ original_summary: str,
40
+ repaired_summary: str,
41
+ operator: str = "backend",
42
+ ) -> None:
43
+ _ensure_audit_logging()
44
+ _audit_log.info(
45
+ "audit_repair op=%s operator=%s original_sha256=%s repaired_sha256=%s original=%s repaired=%s",
46
+ operation,
47
+ operator,
48
+ hashlib.sha256(original_summary.encode("utf-8", errors="replace")).hexdigest()[:16],
49
+ hashlib.sha256(repaired_summary.encode("utf-8", errors="replace")).hexdigest()[:16],
50
+ _truncate(original_summary),
51
+ _truncate(repaired_summary),
52
+ )
53
+
54
+
55
+ def attempt_line_json_repair(raw: str, max_attempts: Optional[int] = None) -> Tuple[Optional[str], List[str]]:
56
+ """Heuristic repairs for a single JSONL line. Returns (fixed_line_or_none, attempts_log)."""
57
+ cfg = get_fortify_config()
58
+ attempts = cfg.max_repair_attempts if max_attempts is None else max_attempts
59
+ log: List[str] = []
60
+ s = raw
61
+ if not s or not s.strip():
62
+ return None, ["empty"]
63
+ for i in range(attempts):
64
+ try:
65
+ json.loads(s)
66
+ if i > 0:
67
+ log.append(f"ok_after_attempt_{i}")
68
+ return s, log
69
+ except json.JSONDecodeError as e:
70
+ log.append(f"attempt_{i}:{e.msg}")
71
+ # progressive fixes
72
+ if s.startswith("\ufeff"):
73
+ s = s[1:]
74
+ continue
75
+ s2 = re.sub(r",\s*}", "}", s)
76
+ s2 = re.sub(r",\s*]", "]", s2)
77
+ if s2 != s:
78
+ s = s2
79
+ continue
80
+ s2 = s.replace("'", '"')
81
+ if s2 != s:
82
+ s = s2
83
+ continue
84
+ break
85
+ return None, log
86
+
87
+
88
+ def parse_session_jsonl_line(
89
+ line: str,
90
+ *,
91
+ auto_repair: Optional[bool] = None,
92
+ json_strict: Optional[bool] = None,
93
+ ) -> Tuple[Optional[Dict[str, Any]], Optional[Dict[str, Any]]]:
94
+ """
95
+ Parse one JSONL line for session files.
96
+ Returns (envelope_dict, message_dict_or_none) for type=message; on total failure (None, None).
97
+ """
98
+ cfg = get_fortify_config()
99
+ use_repair = cfg.auto_repair_json if auto_repair is None else auto_repair
100
+ strict = cfg.json_strict if json_strict is None else json_strict
101
+ stripped = line.strip()
102
+ if not stripped:
103
+ return None, None
104
+
105
+ def _loads(s: str) -> Optional[Dict[str, Any]]:
106
+ try:
107
+ data = json.loads(s)
108
+ return data if isinstance(data, dict) else None
109
+ except json.JSONDecodeError:
110
+ return None
111
+
112
+ data = _loads(stripped)
113
+ if data is None and use_repair:
114
+ repaired, _ = attempt_line_json_repair(stripped)
115
+ if repaired:
116
+ data = _loads(repaired)
117
+ if data is not None:
118
+ audit_repair("json_line_memory", stripped, repaired)
119
+
120
+ if not data:
121
+ from core.error_handler import record_error
122
+
123
+ record_error("parsing-error", "json_decode session line", "session_jsonl")
124
+ return None, None
125
+
126
+ env_validator = SchemaValidator(session_envelope_schema, strict=strict)
127
+ env_res = env_validator.validate(data)
128
+ if not env_res.is_valid:
129
+ from core.error_handler import record_error
130
+
131
+ record_error("validation-error", env_res.error_message, "session_envelope")
132
+ if strict and not use_repair:
133
+ return None, None
134
+
135
+ if data.get("type") != "message":
136
+ return data, None
137
+
138
+ msg = data.get("message")
139
+ if not isinstance(msg, dict):
140
+ if use_repair:
141
+ msg = {}
142
+ data = {**data, "message": msg}
143
+ audit_repair("message_coerce", line, json.dumps(data, ensure_ascii=False)[:500])
144
+ else:
145
+ return data, None
146
+
147
+ msg_schema = dict(session_message_schema)
148
+ if not strict:
149
+ msg_schema = dict(msg_schema)
150
+ msg_schema.pop("required", None)
151
+ mv = SchemaValidator(msg_schema, strict=strict)
152
+ mv_res = mv.validate(msg)
153
+ if mv_res.is_valid:
154
+ return data, msg
155
+
156
+ if use_repair:
157
+ repaired_msg = dict(msg)
158
+ if "role" not in repaired_msg:
159
+ repaired_msg["role"] = "assistant"
160
+ relaxed = dict(msg_schema)
161
+ relaxed.pop("required", None)
162
+ mv2 = SchemaValidator(relaxed, strict=False)
163
+ if mv2.validate(repaired_msg).is_valid:
164
+ audit_repair("message_schema_repair", json.dumps(msg), json.dumps(repaired_msg))
165
+ return data, repaired_msg
166
+
167
+ from core.error_handler import record_error
168
+
169
+ record_error("validation-error", mv_res.error_message, "session_message")
170
+ if strict:
171
+ return data, None
172
+ return data, msg # loose mode: return raw message even if schema warnings
173
+
174
+
175
+ def validate_message_dict(msg: Dict[str, Any]) -> Tuple[bool, List[str]]:
176
+ cfg = get_fortify_config()
177
+ msg_schema = dict(session_message_schema)
178
+ if not cfg.json_strict:
179
+ msg_schema.pop("required", None)
180
+ mv = SchemaValidator(msg_schema, strict=cfg.json_strict)
181
+ r = mv.validate(msg)
182
+ return r.is_valid, r.errors
183
+
184
+
185
+ def write_repaired_json_file(
186
+ path: Path,
187
+ new_content: str,
188
+ *,
189
+ operator: str = "backend",
190
+ ) -> None:
191
+ """Write repaired file with mandatory backup when config enables write-back."""
192
+ cfg = get_fortify_config()
193
+ if not cfg.auto_repair_write_back:
194
+ raise RuntimeError("write-back disabled")
195
+ backup_root = cfg.repair_backup_path
196
+ if not backup_root:
197
+ raise RuntimeError("OPENCLAW_REPAIR_BACKUP required for write-back")
198
+ backup_dir = Path(backup_root)
199
+ backup_dir.mkdir(parents=True, exist_ok=True)
200
+ ts = __import__("time").strftime("%Y%m%d_%H%M%S")
201
+ backup_path = backup_dir / f"{path.name}.{ts}.bak"
202
+ if path.exists():
203
+ shutil.copy2(path, backup_path)
204
+ path.write_text(new_content, encoding="utf-8")
205
+ audit_repair(
206
+ "json_file_write_back",
207
+ f"path={path} backup={backup_path}",
208
+ _truncate(new_content, 800),
209
+ operator=operator,
210
+ )