@matt82198/aesop 0.1.0-beta.4 → 0.1.0-rc.1
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/CHANGELOG.md +187 -5
- package/LICENSE +66 -21
- package/README.md +143 -23
- package/aesop.config.example.json +14 -1
- package/bin/CLAUDE.md +42 -5
- package/bin/cli.js +479 -95
- package/daemons/CLAUDE.md +5 -2
- package/daemons/backup-fleet.sh +209 -51
- package/daemons/run-watchdog.sh +211 -62
- package/dash/dash-extra.mjs +73 -4
- package/dash/watchdog-gui.sh +41 -35
- package/docs/ARCHITECTURE.md +296 -0
- package/docs/CONCEPTS.md +254 -0
- package/docs/CONFIGURE.md +256 -0
- package/docs/FIRST-WAVE.md +276 -0
- package/docs/INSTALL.md +224 -0
- package/docs/README.md +176 -27
- package/docs/RELEASING.md +159 -0
- package/docs/archive/README.md +3 -0
- package/docs/{spikes → archive/spikes}/tiered-cognition/README.md +1 -3
- package/docs/autonomous-swe.md +149 -0
- package/docs/case-study-portfolio.md +61 -0
- package/docs/reproduce.md +121 -0
- package/docs/self-stats-data.json +11 -0
- package/docs/templates/FLEET-OPS-ANALYSIS.example.md +27 -0
- package/docs/templates/FLEET-OPS-RECOMMENDATIONS.example.md +24 -0
- package/docs/templates/PROPOSALS-LOG.example.md +64 -0
- package/hooks/CLAUDE.md +51 -0
- package/hooks/install-waveguard.sh +68 -0
- package/hooks/pre-commit-waveguard.sh +26 -0
- package/hooks/pre-push-policy.sh +253 -15
- package/mcp/CLAUDE.md +213 -0
- package/mcp/package.json +26 -0
- package/mcp/server.mjs +543 -0
- package/monitor/CHARTER.md +76 -110
- package/monitor/CLAUDE.md +3 -3
- package/monitor/collect-signals.mjs +218 -20
- package/package.json +17 -6
- package/scan/CLAUDE.md +30 -0
- package/scan/fleet-scan.example.mjs +292 -0
- package/skills/CLAUDE.md +1 -0
- package/skills/healthcheck/SKILL.md +44 -0
- package/state_store/CLAUDE.md +39 -0
- package/state_store/__init__.py +24 -0
- package/state_store/api.py +35 -0
- package/state_store/export.py +19 -0
- package/state_store/ingest.py +41 -0
- package/state_store/projections.py +126 -0
- package/state_store/store.py +197 -0
- package/tools/CLAUDE.md +37 -149
- package/tools/alert_bridge.py +453 -0
- package/tools/bench_runner.py +438 -0
- package/tools/buildlog.py +4 -7
- package/tools/ci_merge_wait.py +308 -14
- package/tools/common.py +62 -0
- package/tools/cost_ceiling.py +191 -0
- package/tools/dash.js +102 -0
- package/tools/doctor.js +220 -0
- package/tools/fleet_ledger.py +189 -17
- package/tools/fleet_prompt_extractor.py +134 -0
- package/tools/halt.py +172 -0
- package/tools/healthcheck.py +299 -0
- package/tools/heartbeat.py +4 -7
- package/tools/metrics_gate.py +8 -2
- package/tools/orchestrator_status.py +4 -9
- package/tools/reconcile.py +277 -0
- package/tools/rotate_logs.py +152 -62
- package/tools/scanner_selftest.py +28 -3
- package/tools/secret_scan.py +367 -91
- package/tools/self_stats.py +797 -0
- package/tools/session_usage_summary.py +198 -0
- package/tools/status.js +187 -0
- package/tools/svg_to_png.mjs +50 -0
- package/tools/transcript_replay.py +236 -0
- package/tools/transcript_timeline.py +184 -0
- package/tools/verify_agent_inspector.py +289 -0
- package/tools/verify_dash.py +361 -542
- package/tools/verify_prboard.py +344 -0
- package/tools/verify_submit_encoding.py +12 -4
- package/tools/watch.js +49 -0
- package/ui/CLAUDE.md +59 -41
- package/ui/agents.py +337 -34
- package/ui/api/submit.py +44 -5
- package/ui/api/tracker.py +15 -7
- package/ui/collectors.py +294 -52
- package/ui/config.py +31 -2
- package/ui/cost.py +355 -0
- package/ui/csrf.py +9 -4
- package/ui/handler.py +450 -12
- package/ui/render.py +26 -6
- package/ui/sse.py +114 -13
- package/ui/wave_prs.py +230 -0
- package/ui/web/dist/assets/index-0qQYnvMC.js +9 -0
- package/ui/web/dist/assets/index-BdIlFieV.css +1 -0
- package/ui/web/dist/index.html +14 -0
- package/monitor/.signal-state.json +0 -3
- package/monitor/ACTIONS.log +0 -1
- package/monitor/BRIEF.md +0 -24
- package/monitor/SIGNALS.json +0 -54
- package/tools/__pycache__/buildlog.cpython-314.pyc +0 -0
- package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
- package/tools/__pycache__/ensure_state.cpython-314.pyc +0 -0
- package/tools/__pycache__/eod_sweep.cpython-314.pyc +0 -0
- package/tools/__pycache__/fleet_ledger.cpython-314.pyc +0 -0
- package/tools/__pycache__/heartbeat.cpython-314.pyc +0 -0
- package/tools/__pycache__/inbox_drain.cpython-314.pyc +0 -0
- package/tools/__pycache__/launch_tui.cpython-314.pyc +0 -0
- package/tools/__pycache__/orchestrator_status.cpython-314.pyc +0 -0
- package/tools/__pycache__/power_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/prepublish_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/rotate_logs.cpython-314.pyc +0 -0
- package/tools/__pycache__/scanner_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
- package/ui/__pycache__/agents.cpython-314.pyc +0 -0
- package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
- package/ui/__pycache__/config.cpython-314.pyc +0 -0
- package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
- package/ui/__pycache__/handler.cpython-314.pyc +0 -0
- package/ui/__pycache__/render.cpython-314.pyc +0 -0
- package/ui/__pycache__/serve.cpython-314.pyc +0 -0
- package/ui/__pycache__/sse.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/tracker.cpython-314.pyc +0 -0
- package/ui/templates/dashboard.html +0 -1202
- /package/docs/{spikes → archive/spikes}/tiered-cognition/ACTIVATION.md +0 -0
- /package/docs/{spikes → archive/spikes}/tiered-cognition/DESIGN.md +0 -0
- /package/docs/{spikes → archive/spikes}/tiered-cognition/FINDINGS.md +0 -0
- /package/docs/{spikes → archive/spikes}/tiered-cognition/aesop-cognition.example.md +0 -0
- /package/docs/{spikes → archive/spikes}/tiered-cognition/force-model-policy.merged.mjs +0 -0
- /package/docs/{spikes → archive/spikes}/tiered-cognition/strip-tools-hook.mjs +0 -0
package/ui/cost.py
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Cost/scorecard collector — parse ledger and aggregate costs.
|
|
3
|
+
|
|
4
|
+
This module provides get_cost_summary() which parses the outcomes ledger
|
|
5
|
+
markdown table and returns per-model, per-day, and overall cost/token aggregations
|
|
6
|
+
with optional pricing estimates.
|
|
7
|
+
|
|
8
|
+
Ledger format (markdown table):
|
|
9
|
+
| ISO timestamp | agent_type | model | duration | tokens_in | tokens_out | verdict |
|
|
10
|
+
| 2026-07-11T22:08:17 | Agent | claude-haiku-4-5-20251001 | 0 | 8 | 186 | OK |
|
|
11
|
+
|
|
12
|
+
CostSummary JSON shape (returned by get_cost_summary()):
|
|
13
|
+
{
|
|
14
|
+
"models": {
|
|
15
|
+
"model-id": {
|
|
16
|
+
"runs": int,
|
|
17
|
+
"tokens_in": int,
|
|
18
|
+
"tokens_out": int,
|
|
19
|
+
"verdicts": {"OK": int, "FAILED": int, "EMPTY": int, "HUNG": int}
|
|
20
|
+
},
|
|
21
|
+
...
|
|
22
|
+
},
|
|
23
|
+
"daily_totals": {
|
|
24
|
+
"YYYY-MM-DD": {"tokens_in": int, "tokens_out": int},
|
|
25
|
+
...
|
|
26
|
+
},
|
|
27
|
+
"overall_scorecard": {
|
|
28
|
+
"total_runs": int,
|
|
29
|
+
"ok_count": int,
|
|
30
|
+
"failed_count": int,
|
|
31
|
+
"empty_count": int,
|
|
32
|
+
"hung_count": int,
|
|
33
|
+
"ok_rate": float (0.0-1.0),
|
|
34
|
+
"failed_rate": float,
|
|
35
|
+
"empty_rate": float,
|
|
36
|
+
"hung_rate": float
|
|
37
|
+
},
|
|
38
|
+
"skipped_lines": int,
|
|
39
|
+
"has_pricing": bool,
|
|
40
|
+
"estimates_by_model": {
|
|
41
|
+
"model-id": {
|
|
42
|
+
"input_cost": float (dollars),
|
|
43
|
+
"output_cost": float (dollars),
|
|
44
|
+
"total_cost": float (dollars)
|
|
45
|
+
},
|
|
46
|
+
...
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
Key behavior:
|
|
51
|
+
- Missing ledger file: returns empty summary with documented shape.
|
|
52
|
+
- Malformed lines: skipped and counted in skipped_lines field.
|
|
53
|
+
- Config read at CALL time (not import time) for test isolation.
|
|
54
|
+
- UTF-8 explicit encoding for all file operations.
|
|
55
|
+
- No external dependencies (pure stdlib).
|
|
56
|
+
- Pricing estimates ONLY if aesop.config.json has a "pricing" map.
|
|
57
|
+
"""
|
|
58
|
+
import json
|
|
59
|
+
from pathlib import Path
|
|
60
|
+
|
|
61
|
+
# Note: import config at module level, but read config.X at CALL time
|
|
62
|
+
# (not at import time) to ensure test-fixture isolation works.
|
|
63
|
+
import config
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _validate_ledger_format(lines):
|
|
67
|
+
"""Validate that ledger has the expected column structure.
|
|
68
|
+
|
|
69
|
+
Checks the first non-empty, non-separator, non-header line to ensure it looks like a data row
|
|
70
|
+
(has ISO timestamp, agent type, model, numeric fields, verdict). Returns (is_valid, error_message).
|
|
71
|
+
|
|
72
|
+
Expected format:
|
|
73
|
+
| ISO timestamp | agent_type | model | duration | tokens_in | tokens_out | verdict |
|
|
74
|
+
|---|---|---|---|---|---|---|
|
|
75
|
+
| 2026-07-11T22:08:17 | Agent | claude-haiku-4-5-20251001 | 0 | 8 | 186 | OK |
|
|
76
|
+
"""
|
|
77
|
+
for line in lines:
|
|
78
|
+
line = line.strip()
|
|
79
|
+
|
|
80
|
+
# Skip empty lines
|
|
81
|
+
if not line:
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
# Skip separator lines (all dashes, pipes, and spaces)
|
|
85
|
+
if all(c in '|- ' for c in line):
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
# This is the first real line; validate it
|
|
89
|
+
if not line.startswith('|') or not line.endswith('|'):
|
|
90
|
+
return False, "First data line does not start/end with pipe"
|
|
91
|
+
|
|
92
|
+
parts = [p.strip() for p in line.split('|')]
|
|
93
|
+
|
|
94
|
+
# Should have 9 parts: [empty, col1, col2, col3, col4, col5, col6, col7, empty]
|
|
95
|
+
if len(parts) != 9:
|
|
96
|
+
return False, f"Expected 7 columns, got {len(parts) - 2}"
|
|
97
|
+
|
|
98
|
+
# Extract columns and validate types
|
|
99
|
+
try:
|
|
100
|
+
timestamp = parts[1]
|
|
101
|
+
agent_type = parts[2]
|
|
102
|
+
model = parts[3]
|
|
103
|
+
duration_str = parts[4]
|
|
104
|
+
tokens_in_str = parts[5]
|
|
105
|
+
tokens_out_str = parts[6]
|
|
106
|
+
verdict = parts[7]
|
|
107
|
+
|
|
108
|
+
# Skip header line if timestamp column contains "timestamp" or "ISO"
|
|
109
|
+
if 'timestamp' in timestamp.lower() or 'iso' in timestamp.lower():
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
# Check if timestamp looks like ISO format (contains 'T' and '-')
|
|
113
|
+
if 'T' not in timestamp or '-' not in timestamp:
|
|
114
|
+
return False, f"First data line does not have ISO timestamp in column 1: {timestamp}"
|
|
115
|
+
|
|
116
|
+
# Check if tokens are numeric
|
|
117
|
+
try:
|
|
118
|
+
int(tokens_in_str)
|
|
119
|
+
int(tokens_out_str)
|
|
120
|
+
except ValueError:
|
|
121
|
+
return False, f"Token columns must be numeric, got tokens_in={tokens_in_str}, tokens_out={tokens_out_str}"
|
|
122
|
+
|
|
123
|
+
# Check if verdict is valid
|
|
124
|
+
if verdict not in ("OK", "FAILED", "EMPTY", "HUNG"):
|
|
125
|
+
return False, f"First data line has invalid verdict: {verdict}"
|
|
126
|
+
|
|
127
|
+
return True, ""
|
|
128
|
+
except IndexError:
|
|
129
|
+
return False, "First data line has missing columns"
|
|
130
|
+
|
|
131
|
+
# No data lines found
|
|
132
|
+
return True, "" # Empty ledger is ok
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_cost_summary():
|
|
136
|
+
"""Parse the outcomes ledger and return cost/token/verdict aggregations.
|
|
137
|
+
|
|
138
|
+
Reads from the ledger path exposed by config (config.STATE_DIR/ledger/OUTCOMES-LEDGER.md).
|
|
139
|
+
Returns an empty summary with documented shape if ledger is missing or empty.
|
|
140
|
+
Malformed lines are skipped and counted in skipped_lines.
|
|
141
|
+
|
|
142
|
+
Validates ledger format on first data line. If format is invalid, returns
|
|
143
|
+
a summary containing {"error": "ledger format invalid"} and logs to stderr.
|
|
144
|
+
|
|
145
|
+
All config paths are read at call time (not import time) to ensure
|
|
146
|
+
test-fixture isolation via config.reload().
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
dict: CostSummary with models, daily_totals, overall_scorecard,
|
|
150
|
+
skipped_lines, has_pricing, estimates_by_model (or error field if invalid).
|
|
151
|
+
"""
|
|
152
|
+
import sys
|
|
153
|
+
|
|
154
|
+
# Read ledger path at call time
|
|
155
|
+
ledger_file = config.STATE_DIR / "ledger" / "OUTCOMES-LEDGER.md"
|
|
156
|
+
|
|
157
|
+
# Initialize result structure
|
|
158
|
+
result = {
|
|
159
|
+
"models": {},
|
|
160
|
+
"daily_totals": {},
|
|
161
|
+
"overall_scorecard": {
|
|
162
|
+
"total_runs": 0,
|
|
163
|
+
"ok_count": 0,
|
|
164
|
+
"failed_count": 0,
|
|
165
|
+
"empty_count": 0,
|
|
166
|
+
"hung_count": 0,
|
|
167
|
+
"ok_rate": 0.0,
|
|
168
|
+
"failed_rate": 0.0,
|
|
169
|
+
"empty_rate": 0.0,
|
|
170
|
+
"hung_rate": 0.0,
|
|
171
|
+
},
|
|
172
|
+
"skipped_lines": 0,
|
|
173
|
+
"has_pricing": False,
|
|
174
|
+
"estimates_by_model": {},
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# If ledger file doesn't exist, return empty summary
|
|
178
|
+
if not ledger_file.exists():
|
|
179
|
+
return result
|
|
180
|
+
|
|
181
|
+
# Read and parse ledger with explicit UTF-8 encoding
|
|
182
|
+
try:
|
|
183
|
+
content = ledger_file.read_text(encoding='utf-8')
|
|
184
|
+
except Exception:
|
|
185
|
+
# Graceful: if read fails, return empty summary
|
|
186
|
+
return result
|
|
187
|
+
|
|
188
|
+
lines = content.strip().split('\n')
|
|
189
|
+
|
|
190
|
+
# Validate ledger format
|
|
191
|
+
is_valid, error_msg = _validate_ledger_format(lines)
|
|
192
|
+
if not is_valid:
|
|
193
|
+
print(f"[cost] Ledger format invalid: {error_msg}", file=sys.stderr, flush=True)
|
|
194
|
+
result["error"] = "ledger format invalid"
|
|
195
|
+
return result
|
|
196
|
+
|
|
197
|
+
# Parse each line
|
|
198
|
+
for line in lines:
|
|
199
|
+
line = line.strip()
|
|
200
|
+
|
|
201
|
+
# Skip empty lines
|
|
202
|
+
if not line:
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
# Skip header separator lines (all dashes, pipes, and spaces) — silently, don't count as skipped
|
|
206
|
+
if all(c in '|- ' for c in line):
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
# Parse pipe-delimited row
|
|
210
|
+
if not line.startswith('|') or not line.endswith('|'):
|
|
211
|
+
print(f"[cost] Skipping malformed line (no pipe delimiters): {line[:50]}", file=sys.stderr, flush=True)
|
|
212
|
+
result["skipped_lines"] += 1
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
# Skip separator lines (all dashes, pipes, and spaces) — silently, don't count as skipped
|
|
216
|
+
if all(c in '|- ' for c in line):
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
# Split by pipe and strip whitespace
|
|
220
|
+
parts = [p.strip() for p in line.split('|')]
|
|
221
|
+
|
|
222
|
+
# Markdown tables have empty strings at start and end after split
|
|
223
|
+
# Format: | col1 | col2 | col3 | col4 | col5 | col6 | col7 |
|
|
224
|
+
# After split: ['', 'col1', 'col2', 'col3', 'col4', 'col5', 'col6', 'col7', '']
|
|
225
|
+
if len(parts) < 9: # Need at least 9 parts (empty + 7 columns + empty)
|
|
226
|
+
print(f"[cost] Skipping line with too few columns ({len(parts) - 2}): {line[:50]}", file=sys.stderr, flush=True)
|
|
227
|
+
result["skipped_lines"] += 1
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
# Extract columns (skip leading/trailing empty)
|
|
231
|
+
try:
|
|
232
|
+
timestamp = parts[1] # ISO timestamp
|
|
233
|
+
agent_type = parts[2] # "Agent"
|
|
234
|
+
model = parts[3]
|
|
235
|
+
duration_str = parts[4]
|
|
236
|
+
tokens_in_str = parts[5]
|
|
237
|
+
tokens_out_str = parts[6]
|
|
238
|
+
verdict = parts[7]
|
|
239
|
+
except IndexError:
|
|
240
|
+
print(f"[cost] Skipping line with missing columns: {line[:50]}", file=sys.stderr, flush=True)
|
|
241
|
+
result["skipped_lines"] += 1
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
# Skip header line if timestamp column contains "timestamp" or "ISO" — silently, don't count as skipped
|
|
245
|
+
if 'timestamp' in timestamp.lower() or 'iso' in timestamp.lower():
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
# Parse numeric fields
|
|
249
|
+
try:
|
|
250
|
+
tokens_in = int(tokens_in_str)
|
|
251
|
+
tokens_out = int(tokens_out_str)
|
|
252
|
+
except ValueError:
|
|
253
|
+
print(f"[cost] Skipping line with non-numeric tokens (in={tokens_in_str}, out={tokens_out_str}): {line[:50]}", file=sys.stderr, flush=True)
|
|
254
|
+
result["skipped_lines"] += 1
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
# Validate verdict
|
|
258
|
+
if verdict not in ("OK", "FAILED", "EMPTY", "HUNG"):
|
|
259
|
+
print(f"[cost] Skipping line with invalid verdict ({verdict}): {line[:50]}", file=sys.stderr, flush=True)
|
|
260
|
+
result["skipped_lines"] += 1
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
# Extract date from ISO timestamp (YYYY-MM-DD)
|
|
264
|
+
try:
|
|
265
|
+
date_str = timestamp.split('T')[0]
|
|
266
|
+
except IndexError:
|
|
267
|
+
print(f"[cost] Skipping line with invalid timestamp format: {line[:50]}", file=sys.stderr, flush=True)
|
|
268
|
+
result["skipped_lines"] += 1
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
# Aggregate by model
|
|
272
|
+
if model not in result["models"]:
|
|
273
|
+
result["models"][model] = {
|
|
274
|
+
"runs": 0,
|
|
275
|
+
"tokens_in": 0,
|
|
276
|
+
"tokens_out": 0,
|
|
277
|
+
"verdicts": {"OK": 0, "FAILED": 0, "EMPTY": 0, "HUNG": 0},
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
result["models"][model]["runs"] += 1
|
|
281
|
+
result["models"][model]["tokens_in"] += tokens_in
|
|
282
|
+
result["models"][model]["tokens_out"] += tokens_out
|
|
283
|
+
result["models"][model]["verdicts"][verdict] += 1
|
|
284
|
+
|
|
285
|
+
# Aggregate by date
|
|
286
|
+
if date_str not in result["daily_totals"]:
|
|
287
|
+
result["daily_totals"][date_str] = {"tokens_in": 0, "tokens_out": 0}
|
|
288
|
+
|
|
289
|
+
result["daily_totals"][date_str]["tokens_in"] += tokens_in
|
|
290
|
+
result["daily_totals"][date_str]["tokens_out"] += tokens_out
|
|
291
|
+
|
|
292
|
+
# Count for overall scorecard
|
|
293
|
+
result["overall_scorecard"]["total_runs"] += 1
|
|
294
|
+
if verdict == "OK":
|
|
295
|
+
result["overall_scorecard"]["ok_count"] += 1
|
|
296
|
+
elif verdict == "FAILED":
|
|
297
|
+
result["overall_scorecard"]["failed_count"] += 1
|
|
298
|
+
elif verdict == "EMPTY":
|
|
299
|
+
result["overall_scorecard"]["empty_count"] += 1
|
|
300
|
+
elif verdict == "HUNG":
|
|
301
|
+
result["overall_scorecard"]["hung_count"] += 1
|
|
302
|
+
|
|
303
|
+
# Calculate rates
|
|
304
|
+
total = result["overall_scorecard"]["total_runs"]
|
|
305
|
+
if total > 0:
|
|
306
|
+
result["overall_scorecard"]["ok_rate"] = result["overall_scorecard"]["ok_count"] / total
|
|
307
|
+
result["overall_scorecard"]["failed_rate"] = result["overall_scorecard"]["failed_count"] / total
|
|
308
|
+
result["overall_scorecard"]["empty_rate"] = result["overall_scorecard"]["empty_count"] / total
|
|
309
|
+
result["overall_scorecard"]["hung_rate"] = result["overall_scorecard"]["hung_count"] / total
|
|
310
|
+
|
|
311
|
+
# Load pricing config at call time and compute estimates
|
|
312
|
+
pricing_map = _load_pricing_config()
|
|
313
|
+
if pricing_map:
|
|
314
|
+
result["has_pricing"] = True
|
|
315
|
+
for model, stats in result["models"].items():
|
|
316
|
+
if model in pricing_map:
|
|
317
|
+
pricing = pricing_map[model]
|
|
318
|
+
input_price = pricing.get("input_per_mtok", 0.0)
|
|
319
|
+
output_price = pricing.get("output_per_mtok", 0.0)
|
|
320
|
+
|
|
321
|
+
# Calculate costs: (tokens / 1_000_000) * price_per_mtok
|
|
322
|
+
input_cost = (stats["tokens_in"] * input_price) / 1_000_000
|
|
323
|
+
output_cost = (stats["tokens_out"] * output_price) / 1_000_000
|
|
324
|
+
total_cost = input_cost + output_cost
|
|
325
|
+
|
|
326
|
+
result["estimates_by_model"][model] = {
|
|
327
|
+
"input_cost": input_cost,
|
|
328
|
+
"output_cost": output_cost,
|
|
329
|
+
"total_cost": total_cost,
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return result
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _load_pricing_config():
|
|
336
|
+
"""Load pricing map from aesop.config.json at call time.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
dict or None: pricing map {model: {input_per_mtok: float, output_per_mtok: float}}
|
|
340
|
+
or None if no pricing config found.
|
|
341
|
+
"""
|
|
342
|
+
# Read config file at call time (not import time)
|
|
343
|
+
config_file = config.CONFIG_FILE
|
|
344
|
+
|
|
345
|
+
if not config_file.exists():
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
with open(config_file, encoding='utf-8') as f:
|
|
350
|
+
config_data = json.load(f)
|
|
351
|
+
except Exception:
|
|
352
|
+
# Graceful: if config read fails, no pricing
|
|
353
|
+
return None
|
|
354
|
+
|
|
355
|
+
return config_data.get("pricing", None)
|
package/ui/csrf.py
CHANGED
|
@@ -9,6 +9,7 @@ CRITICAL: This module reads config paths LIVE at call time via 'import config',
|
|
|
9
9
|
never 'from config import <path>'. This ensures paths are recomputed when
|
|
10
10
|
config.reload() is called (e.g., between test fixtures).
|
|
11
11
|
"""
|
|
12
|
+
import hmac
|
|
12
13
|
import os
|
|
13
14
|
import secrets
|
|
14
15
|
import sys
|
|
@@ -102,7 +103,7 @@ def validate_csrf_request(headers):
|
|
|
102
103
|
|
|
103
104
|
Performs two checks:
|
|
104
105
|
1. Origin/Referer validation: if Origin or Referer header is present, must be local
|
|
105
|
-
(http://127.0.0.1:<port>, http://localhost:<port>)
|
|
106
|
+
(http[s]://127.0.0.1:<port>, http[s]://localhost:<port>, http[s]://[::1]:<port>)
|
|
106
107
|
2. X-Aesop-Token validation: must match SESSION_TOKEN
|
|
107
108
|
|
|
108
109
|
Args:
|
|
@@ -120,11 +121,15 @@ def validate_csrf_request(headers):
|
|
|
120
121
|
# If Origin or Referer is present, validate it's local
|
|
121
122
|
if origin or referer:
|
|
122
123
|
check_value = origin or referer
|
|
123
|
-
# Check if it's a local origin: http://127.0.0.1:<PORT
|
|
124
|
+
# Check if it's a local origin: http[s]://127.0.0.1:<PORT>, http[s]://localhost:<PORT>,
|
|
125
|
+
# or http[s]://[::1]:<PORT> (both http:// and https:// schemes accepted for same hosts)
|
|
124
126
|
is_local = (
|
|
125
127
|
check_value.startswith("http://127.0.0.1:") or
|
|
128
|
+
check_value.startswith("https://127.0.0.1:") or
|
|
126
129
|
check_value.startswith("http://localhost:") or
|
|
127
|
-
check_value.startswith("
|
|
130
|
+
check_value.startswith("https://localhost:") or
|
|
131
|
+
check_value.startswith("http://[::1]:") or # IPv6 localhost
|
|
132
|
+
check_value.startswith("https://[::1]:") # IPv6 localhost HTTPS
|
|
128
133
|
)
|
|
129
134
|
if not is_local:
|
|
130
135
|
return (False, "Foreign Origin/Referer rejected")
|
|
@@ -134,7 +139,7 @@ def validate_csrf_request(headers):
|
|
|
134
139
|
if not token:
|
|
135
140
|
return (False, "Missing X-Aesop-Token header")
|
|
136
141
|
|
|
137
|
-
if token
|
|
142
|
+
if not hmac.compare_digest(token, SESSION_TOKEN):
|
|
138
143
|
return (False, "Invalid X-Aesop-Token")
|
|
139
144
|
|
|
140
145
|
return (True, None)
|