@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.
Files changed (134) hide show
  1. package/CHANGELOG.md +187 -5
  2. package/LICENSE +66 -21
  3. package/README.md +143 -23
  4. package/aesop.config.example.json +14 -1
  5. package/bin/CLAUDE.md +42 -5
  6. package/bin/cli.js +479 -95
  7. package/daemons/CLAUDE.md +5 -2
  8. package/daemons/backup-fleet.sh +209 -51
  9. package/daemons/run-watchdog.sh +211 -62
  10. package/dash/dash-extra.mjs +73 -4
  11. package/dash/watchdog-gui.sh +41 -35
  12. package/docs/ARCHITECTURE.md +296 -0
  13. package/docs/CONCEPTS.md +254 -0
  14. package/docs/CONFIGURE.md +256 -0
  15. package/docs/FIRST-WAVE.md +276 -0
  16. package/docs/INSTALL.md +224 -0
  17. package/docs/README.md +176 -27
  18. package/docs/RELEASING.md +159 -0
  19. package/docs/archive/README.md +3 -0
  20. package/docs/{spikes → archive/spikes}/tiered-cognition/README.md +1 -3
  21. package/docs/autonomous-swe.md +149 -0
  22. package/docs/case-study-portfolio.md +61 -0
  23. package/docs/reproduce.md +121 -0
  24. package/docs/self-stats-data.json +11 -0
  25. package/docs/templates/FLEET-OPS-ANALYSIS.example.md +27 -0
  26. package/docs/templates/FLEET-OPS-RECOMMENDATIONS.example.md +24 -0
  27. package/docs/templates/PROPOSALS-LOG.example.md +64 -0
  28. package/hooks/CLAUDE.md +51 -0
  29. package/hooks/install-waveguard.sh +68 -0
  30. package/hooks/pre-commit-waveguard.sh +26 -0
  31. package/hooks/pre-push-policy.sh +253 -15
  32. package/mcp/CLAUDE.md +213 -0
  33. package/mcp/package.json +26 -0
  34. package/mcp/server.mjs +543 -0
  35. package/monitor/CHARTER.md +76 -110
  36. package/monitor/CLAUDE.md +3 -3
  37. package/monitor/collect-signals.mjs +218 -20
  38. package/package.json +17 -6
  39. package/scan/CLAUDE.md +30 -0
  40. package/scan/fleet-scan.example.mjs +292 -0
  41. package/skills/CLAUDE.md +1 -0
  42. package/skills/healthcheck/SKILL.md +44 -0
  43. package/state_store/CLAUDE.md +39 -0
  44. package/state_store/__init__.py +24 -0
  45. package/state_store/api.py +35 -0
  46. package/state_store/export.py +19 -0
  47. package/state_store/ingest.py +41 -0
  48. package/state_store/projections.py +126 -0
  49. package/state_store/store.py +197 -0
  50. package/tools/CLAUDE.md +37 -149
  51. package/tools/alert_bridge.py +453 -0
  52. package/tools/bench_runner.py +438 -0
  53. package/tools/buildlog.py +4 -7
  54. package/tools/ci_merge_wait.py +308 -14
  55. package/tools/common.py +62 -0
  56. package/tools/cost_ceiling.py +191 -0
  57. package/tools/dash.js +102 -0
  58. package/tools/doctor.js +220 -0
  59. package/tools/fleet_ledger.py +189 -17
  60. package/tools/fleet_prompt_extractor.py +134 -0
  61. package/tools/halt.py +172 -0
  62. package/tools/healthcheck.py +299 -0
  63. package/tools/heartbeat.py +4 -7
  64. package/tools/metrics_gate.py +8 -2
  65. package/tools/orchestrator_status.py +4 -9
  66. package/tools/reconcile.py +277 -0
  67. package/tools/rotate_logs.py +152 -62
  68. package/tools/scanner_selftest.py +28 -3
  69. package/tools/secret_scan.py +367 -91
  70. package/tools/self_stats.py +797 -0
  71. package/tools/session_usage_summary.py +198 -0
  72. package/tools/status.js +187 -0
  73. package/tools/svg_to_png.mjs +50 -0
  74. package/tools/transcript_replay.py +236 -0
  75. package/tools/transcript_timeline.py +184 -0
  76. package/tools/verify_agent_inspector.py +289 -0
  77. package/tools/verify_dash.py +361 -542
  78. package/tools/verify_prboard.py +344 -0
  79. package/tools/verify_submit_encoding.py +12 -4
  80. package/tools/watch.js +49 -0
  81. package/ui/CLAUDE.md +59 -41
  82. package/ui/agents.py +337 -34
  83. package/ui/api/submit.py +44 -5
  84. package/ui/api/tracker.py +15 -7
  85. package/ui/collectors.py +294 -52
  86. package/ui/config.py +31 -2
  87. package/ui/cost.py +355 -0
  88. package/ui/csrf.py +9 -4
  89. package/ui/handler.py +450 -12
  90. package/ui/render.py +26 -6
  91. package/ui/sse.py +114 -13
  92. package/ui/wave_prs.py +230 -0
  93. package/ui/web/dist/assets/index-0qQYnvMC.js +9 -0
  94. package/ui/web/dist/assets/index-BdIlFieV.css +1 -0
  95. package/ui/web/dist/index.html +14 -0
  96. package/monitor/.signal-state.json +0 -3
  97. package/monitor/ACTIONS.log +0 -1
  98. package/monitor/BRIEF.md +0 -24
  99. package/monitor/SIGNALS.json +0 -54
  100. package/tools/__pycache__/buildlog.cpython-314.pyc +0 -0
  101. package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
  102. package/tools/__pycache__/ensure_state.cpython-314.pyc +0 -0
  103. package/tools/__pycache__/eod_sweep.cpython-314.pyc +0 -0
  104. package/tools/__pycache__/fleet_ledger.cpython-314.pyc +0 -0
  105. package/tools/__pycache__/heartbeat.cpython-314.pyc +0 -0
  106. package/tools/__pycache__/inbox_drain.cpython-314.pyc +0 -0
  107. package/tools/__pycache__/launch_tui.cpython-314.pyc +0 -0
  108. package/tools/__pycache__/orchestrator_status.cpython-314.pyc +0 -0
  109. package/tools/__pycache__/power_selftest.cpython-314.pyc +0 -0
  110. package/tools/__pycache__/prepublish_scan.cpython-314.pyc +0 -0
  111. package/tools/__pycache__/rotate_logs.cpython-314.pyc +0 -0
  112. package/tools/__pycache__/scanner_selftest.cpython-314.pyc +0 -0
  113. package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
  114. package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
  115. package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
  116. package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
  117. package/ui/__pycache__/agents.cpython-314.pyc +0 -0
  118. package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
  119. package/ui/__pycache__/config.cpython-314.pyc +0 -0
  120. package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
  121. package/ui/__pycache__/handler.cpython-314.pyc +0 -0
  122. package/ui/__pycache__/render.cpython-314.pyc +0 -0
  123. package/ui/__pycache__/serve.cpython-314.pyc +0 -0
  124. package/ui/__pycache__/sse.cpython-314.pyc +0 -0
  125. package/ui/api/__pycache__/__init__.cpython-314.pyc +0 -0
  126. package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
  127. package/ui/api/__pycache__/tracker.cpython-314.pyc +0 -0
  128. package/ui/templates/dashboard.html +0 -1202
  129. /package/docs/{spikes → archive/spikes}/tiered-cognition/ACTIVATION.md +0 -0
  130. /package/docs/{spikes → archive/spikes}/tiered-cognition/DESIGN.md +0 -0
  131. /package/docs/{spikes → archive/spikes}/tiered-cognition/FINDINGS.md +0 -0
  132. /package/docs/{spikes → archive/spikes}/tiered-cognition/aesop-cognition.example.md +0 -0
  133. /package/docs/{spikes → archive/spikes}/tiered-cognition/force-model-policy.merged.mjs +0 -0
  134. /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> or http://localhost:<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("http://[::1]:") # IPv6 localhost
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 != SESSION_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)