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.
- package/dashboard/api/agent_config_api.py +28 -7
- package/dashboard/api/agents.py +48 -10
- package/dashboard/api/agents_config.py +5 -1
- package/dashboard/api/chains.py +25 -5
- package/dashboard/api/collaboration.py +10 -9
- package/dashboard/api/debug_paths.py +5 -1
- package/dashboard/api/error_analysis.py +29 -11
- package/dashboard/api/errors.py +27 -11
- package/dashboard/api/fortify_routes.py +80 -0
- package/dashboard/api/input_safety.py +60 -0
- package/dashboard/api/performance.py +73 -53
- package/dashboard/api/subagents.py +95 -99
- package/dashboard/api/timeline.py +24 -3
- package/dashboard/api/version.py +2 -0
- package/dashboard/api/websocket.py +9 -7
- package/dashboard/core/__init__.py +1 -0
- package/dashboard/core/config_fortify.py +112 -0
- package/dashboard/core/error_handler.py +339 -0
- package/dashboard/core/fallback_manager.py +70 -0
- package/dashboard/core/safe_api_error.py +76 -0
- package/dashboard/core/schemas/__init__.py +16 -0
- package/dashboard/core/schemas/base.py +43 -0
- package/dashboard/core/schemas/session_schema.py +40 -0
- package/dashboard/core/schemas/subagent_schema.py +23 -0
- package/dashboard/data/agent_config_manager.py +6 -4
- package/dashboard/data/chain_reader.py +16 -12
- package/dashboard/data/error_analyzer.py +15 -11
- package/dashboard/data/session_reader.py +268 -46
- package/dashboard/data/subagent_reader.py +74 -49
- package/dashboard/data/timeline_reader.py +35 -49
- package/dashboard/main.py +24 -2
- package/dashboard/mechanism_reader.py +4 -5
- package/dashboard/mechanisms.py +2 -2
- package/dashboard/pytest.ini +3 -0
- package/dashboard/requirements.txt +5 -0
- package/dashboard/status/cache_fp_probe.py +40 -0
- package/dashboard/status/status_cache.py +199 -72
- package/dashboard/status/status_calculator.py +50 -30
- package/dashboard/tests/conftest.py +84 -0
- package/dashboard/tests/test_api_contracts.py +372 -0
- package/dashboard/tests/test_bench_fortify.py +176 -0
- package/dashboard/tests/test_fortify.py +741 -0
- package/dashboard/utils/__init__.py +1 -0
- package/dashboard/utils/data_repair.py +210 -0
- package/dashboard/watchers/file_watcher.py +367 -77
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/dashboard/agents.py +0 -74
- package/dashboard/collaboration.py +0 -407
- package/dashboard/errors.py +0 -63
- package/dashboard/performance.py +0 -474
- package/dashboard/session_reader.py +0 -240
- package/dashboard/status_calculator.py +0 -121
- 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
|
+
)
|