loki-mode 7.81.0 → 7.82.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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +88 -27
- package/autonomy/lib/config-map.sh +61 -15
- package/autonomy/loki +172 -27
- package/autonomy/run.sh +34 -13
- package/completions/_loki +63 -3
- package/completions/loki.bash +6 -3
- package/dashboard/__init__.py +1 -1
- package/dashboard/audit.py +36 -10
- package/dashboard/registry.py +67 -24
- package/dashboard/server.py +131 -74
- package/docs/INSTALLATION.md +2 -2
- package/events/bus.py +17 -1
- package/events/emit.sh +15 -1
- package/loki-ts/dist/loki.js +2 -2
- package/lokistore/cloud.py +17 -1
- package/lokistore/factory.py +6 -1
- package/lokistore/local.py +11 -3
- package/mcp/__init__.py +1 -1
- package/mcp/server.py +16 -4
- package/memory/cross_project.py +74 -5
- package/memory/embeddings.py +19 -1
- package/memory/retrieval.py +81 -153
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/codex.sh +9 -4
- package/memory/tree_index.py +0 -499
- package/memory/tree_search.py +0 -305
package/dashboard/server.py
CHANGED
|
@@ -164,6 +164,21 @@ class _RateLimiter:
|
|
|
164
164
|
_control_limiter = _RateLimiter(max_calls=10, window_seconds=60)
|
|
165
165
|
_read_limiter = _RateLimiter(max_calls=60, window_seconds=60)
|
|
166
166
|
|
|
167
|
+
|
|
168
|
+
def _rate_key(base: str, request: Optional[Request]) -> str:
|
|
169
|
+
"""Build a per-client rate-limit key.
|
|
170
|
+
|
|
171
|
+
Static literal keys make the read limiter a single global cap: one client
|
|
172
|
+
can exhaust the window for everyone else. Mirror the /ws path, which keys
|
|
173
|
+
by client.host, so the cap is enforced per client. Falls back to the bare
|
|
174
|
+
base key when the client address is unavailable (preserving prior
|
|
175
|
+
behaviour rather than failing open).
|
|
176
|
+
"""
|
|
177
|
+
host = None
|
|
178
|
+
if request is not None and request.client is not None:
|
|
179
|
+
host = request.client.host
|
|
180
|
+
return f"{base}_{host}" if host else base
|
|
181
|
+
|
|
167
182
|
# Set up logging
|
|
168
183
|
logger = logging.getLogger(__name__)
|
|
169
184
|
|
|
@@ -1402,7 +1417,7 @@ async def get_status() -> StatusResponse:
|
|
|
1402
1417
|
|
|
1403
1418
|
|
|
1404
1419
|
# Project endpoints
|
|
1405
|
-
@app.get("/api/projects", response_model=list[ProjectResponse])
|
|
1420
|
+
@app.get("/api/projects", response_model=list[ProjectResponse], dependencies=[Depends(auth.require_scope("read"))])
|
|
1406
1421
|
async def list_projects(
|
|
1407
1422
|
status: Optional[str] = Query(None),
|
|
1408
1423
|
limit: int = Query(default=50, ge=1, le=500),
|
|
@@ -1521,7 +1536,7 @@ async def create_project(
|
|
|
1521
1536
|
)
|
|
1522
1537
|
|
|
1523
1538
|
|
|
1524
|
-
@app.get("/api/projects/{project_id}", response_model=ProjectResponse)
|
|
1539
|
+
@app.get("/api/projects/{project_id}", response_model=ProjectResponse, dependencies=[Depends(auth.require_scope("read"))])
|
|
1525
1540
|
async def get_project(
|
|
1526
1541
|
project_id: int,
|
|
1527
1542
|
db: AsyncSession = Depends(get_db),
|
|
@@ -1736,7 +1751,7 @@ def _apply_task_section(task: dict, section: str, lines: list):
|
|
|
1736
1751
|
|
|
1737
1752
|
|
|
1738
1753
|
# Task endpoints - reads from .loki/dashboard-state.json
|
|
1739
|
-
@app.get("/api/tasks")
|
|
1754
|
+
@app.get("/api/tasks", dependencies=[Depends(auth.require_scope("read"))])
|
|
1740
1755
|
async def list_tasks(
|
|
1741
1756
|
project_id: Optional[int] = Query(None),
|
|
1742
1757
|
status: Optional[str] = Query(None),
|
|
@@ -1959,7 +1974,7 @@ async def create_task(
|
|
|
1959
1974
|
return _task_response_from_db(db_task)
|
|
1960
1975
|
|
|
1961
1976
|
|
|
1962
|
-
@app.get("/api/tasks/{task_id}", response_model=TaskResponse)
|
|
1977
|
+
@app.get("/api/tasks/{task_id}", response_model=TaskResponse, dependencies=[Depends(auth.require_scope("read"))])
|
|
1963
1978
|
async def get_task(
|
|
1964
1979
|
task_id: int,
|
|
1965
1980
|
db: AsyncSession = Depends(get_db),
|
|
@@ -2282,7 +2297,7 @@ class HealthResponse(BaseModel):
|
|
|
2282
2297
|
checks: dict
|
|
2283
2298
|
|
|
2284
2299
|
|
|
2285
|
-
@app.get("/api/registry/projects", response_model=list[RegisteredProjectResponse])
|
|
2300
|
+
@app.get("/api/registry/projects", response_model=list[RegisteredProjectResponse], dependencies=[Depends(auth.require_scope("read"))])
|
|
2286
2301
|
async def list_registered_projects(include_inactive: bool = False):
|
|
2287
2302
|
"""List all registered projects."""
|
|
2288
2303
|
projects = registry.list_projects(include_inactive=include_inactive)
|
|
@@ -2303,7 +2318,7 @@ async def register_project(request: RegisterProjectRequest):
|
|
|
2303
2318
|
raise HTTPException(status_code=400, detail=str(e))
|
|
2304
2319
|
|
|
2305
2320
|
|
|
2306
|
-
@app.get("/api/registry/projects/{identifier}", response_model=RegisteredProjectResponse)
|
|
2321
|
+
@app.get("/api/registry/projects/{identifier}", response_model=RegisteredProjectResponse, dependencies=[Depends(auth.require_scope("read"))])
|
|
2307
2322
|
async def get_registered_project(identifier: str):
|
|
2308
2323
|
"""Get a registered project by ID, path, or alias."""
|
|
2309
2324
|
project = registry.get_project(identifier)
|
|
@@ -2326,7 +2341,7 @@ async def unregister_project(identifier: str, request: Request):
|
|
|
2326
2341
|
)
|
|
2327
2342
|
|
|
2328
2343
|
|
|
2329
|
-
@app.get("/api/registry/projects/{identifier}/health", response_model=HealthResponse)
|
|
2344
|
+
@app.get("/api/registry/projects/{identifier}/health", response_model=HealthResponse, dependencies=[Depends(auth.require_scope("read"))])
|
|
2330
2345
|
async def get_project_health(identifier: str):
|
|
2331
2346
|
"""Check the health of a registered project."""
|
|
2332
2347
|
health = registry.check_project_health(identifier)
|
|
@@ -2344,7 +2359,7 @@ async def update_project_access(identifier: str):
|
|
|
2344
2359
|
return project
|
|
2345
2360
|
|
|
2346
2361
|
|
|
2347
|
-
@app.get("/api/registry/discover", response_model=list[DiscoverResponse])
|
|
2362
|
+
@app.get("/api/registry/discover", response_model=list[DiscoverResponse], dependencies=[Depends(auth.require_scope("read"))])
|
|
2348
2363
|
async def discover_projects(max_depth: int = Query(default=3, ge=1, le=10)):
|
|
2349
2364
|
"""Discover projects with .loki directories."""
|
|
2350
2365
|
max_depth = min(max_depth, 10)
|
|
@@ -2374,7 +2389,7 @@ async def sync_registry():
|
|
|
2374
2389
|
}
|
|
2375
2390
|
|
|
2376
2391
|
|
|
2377
|
-
@app.get("/api/registry/tasks")
|
|
2392
|
+
@app.get("/api/registry/tasks", dependencies=[Depends(auth.require_scope("read"))])
|
|
2378
2393
|
async def get_cross_project_tasks(project_ids: Optional[str] = None):
|
|
2379
2394
|
"""Get tasks from multiple projects for unified view."""
|
|
2380
2395
|
ids = project_ids.split(",") if project_ids else None
|
|
@@ -2382,7 +2397,7 @@ async def get_cross_project_tasks(project_ids: Optional[str] = None):
|
|
|
2382
2397
|
return tasks
|
|
2383
2398
|
|
|
2384
2399
|
|
|
2385
|
-
@app.get("/api/registry/learnings")
|
|
2400
|
+
@app.get("/api/registry/learnings", dependencies=[Depends(auth.require_scope("read"))])
|
|
2386
2401
|
async def get_cross_project_learnings():
|
|
2387
2402
|
"""Get learnings from the global learnings database."""
|
|
2388
2403
|
learnings = registry.get_cross_project_learnings()
|
|
@@ -2551,7 +2566,7 @@ async def set_focus(request: FocusRequest):
|
|
|
2551
2566
|
return {"project_dir": _active_project_dir, "loki_dir": str(_get_loki_dir())}
|
|
2552
2567
|
|
|
2553
2568
|
|
|
2554
|
-
@app.get("/api/focus")
|
|
2569
|
+
@app.get("/api/focus", dependencies=[Depends(auth.require_scope("read"))])
|
|
2555
2570
|
async def get_focus():
|
|
2556
2571
|
"""Get the currently focused project directory."""
|
|
2557
2572
|
return {
|
|
@@ -2870,7 +2885,7 @@ async def set_session_model(request: SessionModelRequest):
|
|
|
2870
2885
|
return {"model": model, "effective": effective, "clamped": clamped}
|
|
2871
2886
|
|
|
2872
2887
|
|
|
2873
|
-
@app.get("/api/running-projects")
|
|
2888
|
+
@app.get("/api/running-projects", dependencies=[Depends(auth.require_scope("read"))])
|
|
2874
2889
|
async def list_running_projects():
|
|
2875
2890
|
"""List registered projects enriched with live status for the dashboard
|
|
2876
2891
|
project switcher (v7.7.29 multi-project support).
|
|
@@ -2961,10 +2976,13 @@ class StartBuildRequest(BaseModel):
|
|
|
2961
2976
|
def validate_provider(self) -> None:
|
|
2962
2977
|
"""Validate provider is from the supported list.
|
|
2963
2978
|
|
|
2964
|
-
Mirrors
|
|
2965
|
-
|
|
2979
|
+
Mirrors providers/loader.sh SUPPORTED_PROVIDERS so the dashboard
|
|
2980
|
+
rejects providers the runtime rejects. gemini was deprecated in
|
|
2981
|
+
v7.5.18 (runtime removed); accepting it here let the dashboard
|
|
2982
|
+
report a false "Build started" while run.sh killed the child on the
|
|
2983
|
+
deprecation guard.
|
|
2966
2984
|
"""
|
|
2967
|
-
allowed = ["claude", "codex", "
|
|
2985
|
+
allowed = ["claude", "codex", "cline", "aider"]
|
|
2968
2986
|
if self.provider not in allowed:
|
|
2969
2987
|
raise ValueError(
|
|
2970
2988
|
f"Invalid provider: {self.provider}. "
|
|
@@ -3138,6 +3156,26 @@ async def start_build(request: Request, body: StartBuildRequest):
|
|
|
3138
3156
|
except (OSError, subprocess.SubprocessError) as e:
|
|
3139
3157
|
raise HTTPException(status_code=500, detail=f"Failed to start build: {e}")
|
|
3140
3158
|
|
|
3159
|
+
# Liveness check: a provider that dies on a startup guard (e.g. an
|
|
3160
|
+
# unsupported provider, a missing CLI, or a preflight failure in run.sh)
|
|
3161
|
+
# would otherwise let us report a false "Build started". Poll briefly and
|
|
3162
|
+
# surface an honest error if the child exits immediately.
|
|
3163
|
+
early_exit = None
|
|
3164
|
+
for _ in range(3):
|
|
3165
|
+
await asyncio.sleep(0.1)
|
|
3166
|
+
early_exit = process.poll()
|
|
3167
|
+
if early_exit is not None:
|
|
3168
|
+
break
|
|
3169
|
+
if early_exit is not None and early_exit != 0:
|
|
3170
|
+
raise HTTPException(
|
|
3171
|
+
status_code=500,
|
|
3172
|
+
detail=(
|
|
3173
|
+
f"Build process exited immediately (code {early_exit}). "
|
|
3174
|
+
f"The '{body.provider}' provider or run.sh preflight may have "
|
|
3175
|
+
f"rejected the request."
|
|
3176
|
+
),
|
|
3177
|
+
)
|
|
3178
|
+
|
|
3141
3179
|
# Persist provider for status tracking (same as control.py).
|
|
3142
3180
|
try:
|
|
3143
3181
|
state_dir = loki_dir / "state"
|
|
@@ -3872,7 +3910,7 @@ _COMPLIANCE_TYPES = ("soc2", "iso27001", "gdpr")
|
|
|
3872
3910
|
|
|
3873
3911
|
|
|
3874
3912
|
@app.get("/api/compliance", dependencies=[Depends(auth.require_scope("audit"))])
|
|
3875
|
-
def get_compliance_status(report_type: str = Query("soc2", alias="type")):
|
|
3913
|
+
def get_compliance_status(request: Request, report_type: str = Query("soc2", alias="type")):
|
|
3876
3914
|
"""Live compliance status for the active project's agent audit chain.
|
|
3877
3915
|
|
|
3878
3916
|
Auth/tenant scoping: requires the `audit` scope (same gate as the
|
|
@@ -3890,7 +3928,7 @@ def get_compliance_status(report_type: str = Query("soc2", alias="type")):
|
|
|
3890
3928
|
available:false payload (HTTP 200) rather than masquerading as "no
|
|
3891
3929
|
compliance".
|
|
3892
3930
|
"""
|
|
3893
|
-
if not _read_limiter.check("compliance"):
|
|
3931
|
+
if not _read_limiter.check(_rate_key("compliance", request)):
|
|
3894
3932
|
raise HTTPException(status_code=429, detail="Rate limit exceeded")
|
|
3895
3933
|
if report_type not in _COMPLIANCE_TYPES:
|
|
3896
3934
|
raise HTTPException(
|
|
@@ -5339,9 +5377,9 @@ async def get_learning_aggregation():
|
|
|
5339
5377
|
|
|
5340
5378
|
|
|
5341
5379
|
@app.post("/api/learning/aggregate", dependencies=[Depends(auth.require_scope("control"))])
|
|
5342
|
-
async def trigger_aggregation():
|
|
5380
|
+
async def trigger_aggregation(request: Request):
|
|
5343
5381
|
"""Aggregate learning signals from events.jsonl into structured metrics."""
|
|
5344
|
-
if not _read_limiter.check("learning_aggregate"):
|
|
5382
|
+
if not _read_limiter.check(_rate_key("learning_aggregate", request)):
|
|
5345
5383
|
raise HTTPException(status_code=429, detail="Rate limit exceeded")
|
|
5346
5384
|
|
|
5347
5385
|
# Reads up to 10 MB of events.jsonl, parses every line, then writes the
|
|
@@ -5828,7 +5866,7 @@ def _calculate_model_cost(model: str, input_tokens: int, output_tokens: int) ->
|
|
|
5828
5866
|
return round(input_cost + output_cost, 6)
|
|
5829
5867
|
|
|
5830
5868
|
|
|
5831
|
-
@app.get("/api/cost")
|
|
5869
|
+
@app.get("/api/cost", dependencies=[Depends(auth.require_scope("read"))])
|
|
5832
5870
|
async def get_cost():
|
|
5833
5871
|
"""Get cost visibility data from .loki/metrics/efficiency/ and budget.json.
|
|
5834
5872
|
|
|
@@ -5955,7 +5993,7 @@ def _compute_cost_snapshot() -> dict:
|
|
|
5955
5993
|
}
|
|
5956
5994
|
|
|
5957
5995
|
|
|
5958
|
-
@app.get("/api/budget")
|
|
5996
|
+
@app.get("/api/budget", dependencies=[Depends(auth.require_scope("read"))])
|
|
5959
5997
|
async def get_budget():
|
|
5960
5998
|
"""Get current budget status from .loki/metrics/budget.json and cost data."""
|
|
5961
5999
|
loki_dir = _get_loki_dir()
|
|
@@ -6011,6 +6049,25 @@ async def get_budget():
|
|
|
6011
6049
|
budget_limit_f = _to_float(budget_limit, None) if budget_limit is not None else None
|
|
6012
6050
|
budget_used_f = _to_float(budget_used, 0.0)
|
|
6013
6051
|
|
|
6052
|
+
# current_cost must reflect real live spend, not the static budget.json
|
|
6053
|
+
# field which only updates when run.sh persists it. The same divergence
|
|
6054
|
+
# made the widget show $0 mid-run while /api/cost summed real spend.
|
|
6055
|
+
# Derive from _compute_budget_snapshot (sums live efficiency records,
|
|
6056
|
+
# the single source of truth shared with /api/cost/timeline and the WS
|
|
6057
|
+
# push); keep budget.json's value only as a fallback when no live spend
|
|
6058
|
+
# has been recorded yet.
|
|
6059
|
+
try:
|
|
6060
|
+
snapshot = _compute_budget_snapshot(loki_dir)
|
|
6061
|
+
live_used = snapshot.get("used")
|
|
6062
|
+
if isinstance(live_used, (int, float)) and live_used > 0:
|
|
6063
|
+
budget_used_f = float(live_used)
|
|
6064
|
+
if budget_limit_f is None and snapshot.get("limit") is not None:
|
|
6065
|
+
budget_limit_f = _to_float(snapshot.get("limit"), None)
|
|
6066
|
+
except Exception:
|
|
6067
|
+
# Never let the live computation break the endpoint; fall back to the
|
|
6068
|
+
# static budget.json value already loaded above.
|
|
6069
|
+
pass
|
|
6070
|
+
|
|
6014
6071
|
remaining = None
|
|
6015
6072
|
if budget_limit_f is not None:
|
|
6016
6073
|
remaining = max(0.0, budget_limit_f - budget_used_f)
|
|
@@ -6115,7 +6172,7 @@ def _compute_budget_snapshot(loki_dir: _Path) -> dict:
|
|
|
6115
6172
|
}
|
|
6116
6173
|
|
|
6117
6174
|
|
|
6118
|
-
@app.get("/api/cost/timeline")
|
|
6175
|
+
@app.get("/api/cost/timeline", dependencies=[Depends(auth.require_scope("read"))])
|
|
6119
6176
|
async def get_cost_timeline():
|
|
6120
6177
|
"""Cost over time: intra-run per-iteration series + per-run history.
|
|
6121
6178
|
|
|
@@ -6275,7 +6332,7 @@ def _load_trust_module():
|
|
|
6275
6332
|
return None
|
|
6276
6333
|
|
|
6277
6334
|
|
|
6278
|
-
@app.get("/api/trust/trajectory")
|
|
6335
|
+
@app.get("/api/trust/trajectory", dependencies=[Depends(auth.require_scope("read"))])
|
|
6279
6336
|
async def get_trust_trajectory():
|
|
6280
6337
|
"""Per-project trust trajectory derived from proof-of-run history.
|
|
6281
6338
|
|
|
@@ -6336,7 +6393,7 @@ _MODEL_PROVIDERS = {
|
|
|
6336
6393
|
}
|
|
6337
6394
|
|
|
6338
6395
|
|
|
6339
|
-
@app.get("/api/pricing")
|
|
6396
|
+
@app.get("/api/pricing", dependencies=[Depends(auth.require_scope("read"))])
|
|
6340
6397
|
async def get_pricing():
|
|
6341
6398
|
"""Get current model pricing. Reads from .loki/pricing.json if available, falls back to static defaults."""
|
|
6342
6399
|
loki_dir = _get_loki_dir()
|
|
@@ -6383,7 +6440,7 @@ async def get_pricing():
|
|
|
6383
6440
|
# Completion Council API (v5.25.0)
|
|
6384
6441
|
# =============================================================================
|
|
6385
6442
|
|
|
6386
|
-
@app.get("/api/council/state")
|
|
6443
|
+
@app.get("/api/council/state", dependencies=[Depends(auth.require_scope("read"))])
|
|
6387
6444
|
async def get_council_state():
|
|
6388
6445
|
"""Get current Completion Council state."""
|
|
6389
6446
|
state_file = _get_loki_dir() / "council" / "state.json"
|
|
@@ -6395,7 +6452,7 @@ async def get_council_state():
|
|
|
6395
6452
|
return {"enabled": False, "total_votes": 0, "verdicts": []}
|
|
6396
6453
|
|
|
6397
6454
|
|
|
6398
|
-
@app.get("/api/council/verdicts")
|
|
6455
|
+
@app.get("/api/council/verdicts", dependencies=[Depends(auth.require_scope("read"))])
|
|
6399
6456
|
async def get_council_verdicts(limit: int = Query(default=20, ge=1, le=1000)):
|
|
6400
6457
|
"""Get council vote history (decision log).
|
|
6401
6458
|
|
|
@@ -6452,7 +6509,7 @@ async def get_council_verdicts(limit: int = Query(default=20, ge=1, le=1000)):
|
|
|
6452
6509
|
return await asyncio.to_thread(_collect_verdicts)
|
|
6453
6510
|
|
|
6454
6511
|
|
|
6455
|
-
@app.get("/api/council/convergence")
|
|
6512
|
+
@app.get("/api/council/convergence", dependencies=[Depends(auth.require_scope("read"))])
|
|
6456
6513
|
async def get_council_convergence():
|
|
6457
6514
|
"""Get convergence tracking data for visualization."""
|
|
6458
6515
|
convergence_file = _get_loki_dir() / "council" / "convergence.log"
|
|
@@ -6474,7 +6531,7 @@ async def get_council_convergence():
|
|
|
6474
6531
|
return {"dataPoints": data_points}
|
|
6475
6532
|
|
|
6476
6533
|
|
|
6477
|
-
@app.get("/api/council/report")
|
|
6534
|
+
@app.get("/api/council/report", dependencies=[Depends(auth.require_scope("read"))])
|
|
6478
6535
|
async def get_council_report():
|
|
6479
6536
|
"""Get the final council completion report."""
|
|
6480
6537
|
report_file = _get_loki_dir() / "council" / "report.md"
|
|
@@ -6494,7 +6551,7 @@ async def force_council_review():
|
|
|
6494
6551
|
return {"success": True, "message": "Council review requested"}
|
|
6495
6552
|
|
|
6496
6553
|
|
|
6497
|
-
@app.get("/api/council/transcripts")
|
|
6554
|
+
@app.get("/api/council/transcripts", dependencies=[Depends(auth.require_scope("read"))])
|
|
6498
6555
|
async def get_council_transcripts(
|
|
6499
6556
|
limit: int = Query(default=20, ge=1, le=200),
|
|
6500
6557
|
since: Optional[str] = Query(default=None),
|
|
@@ -6571,7 +6628,7 @@ async def get_council_transcripts(
|
|
|
6571
6628
|
return response
|
|
6572
6629
|
|
|
6573
6630
|
|
|
6574
|
-
@app.get("/api/council/transcripts/{iteration_id}")
|
|
6631
|
+
@app.get("/api/council/transcripts/{iteration_id}", dependencies=[Depends(auth.require_scope("read"))])
|
|
6575
6632
|
async def get_council_transcript(iteration_id: str):
|
|
6576
6633
|
"""Fetch a single council transcript by iteration_id.
|
|
6577
6634
|
|
|
@@ -6603,7 +6660,7 @@ async def get_council_transcript(iteration_id: str):
|
|
|
6603
6660
|
# Context Window Tracking API (v5.40.0)
|
|
6604
6661
|
# =============================================================================
|
|
6605
6662
|
|
|
6606
|
-
@app.get("/api/context")
|
|
6663
|
+
@app.get("/api/context", dependencies=[Depends(auth.require_scope("read"))])
|
|
6607
6664
|
async def get_context():
|
|
6608
6665
|
"""Get context window tracking data from .loki/context/tracking.json."""
|
|
6609
6666
|
loki_dir = _get_loki_dir()
|
|
@@ -6639,7 +6696,7 @@ async def get_context():
|
|
|
6639
6696
|
# Notification Trigger API (v5.40.0)
|
|
6640
6697
|
# =============================================================================
|
|
6641
6698
|
|
|
6642
|
-
@app.get("/api/notifications")
|
|
6699
|
+
@app.get("/api/notifications", dependencies=[Depends(auth.require_scope("read"))])
|
|
6643
6700
|
async def get_notifications(
|
|
6644
6701
|
severity: Optional[str] = Query(None, pattern="^(critical|warning|info)$"),
|
|
6645
6702
|
unread_only: bool = Query(False),
|
|
@@ -6676,7 +6733,7 @@ async def get_notifications(
|
|
|
6676
6733
|
}
|
|
6677
6734
|
|
|
6678
6735
|
|
|
6679
|
-
@app.get("/api/notifications/triggers")
|
|
6736
|
+
@app.get("/api/notifications/triggers", dependencies=[Depends(auth.require_scope("read"))])
|
|
6680
6737
|
async def get_notification_triggers():
|
|
6681
6738
|
"""Get notification trigger configuration from .loki/notifications/triggers.json."""
|
|
6682
6739
|
loki_dir = _get_loki_dir()
|
|
@@ -6786,7 +6843,7 @@ def _sanitize_checkpoint_id(checkpoint_id: str) -> str:
|
|
|
6786
6843
|
return checkpoint_id
|
|
6787
6844
|
|
|
6788
6845
|
|
|
6789
|
-
@app.get("/api/checkpoints")
|
|
6846
|
+
@app.get("/api/checkpoints", dependencies=[Depends(auth.require_scope("read"))])
|
|
6790
6847
|
async def list_checkpoints(limit: int = Query(default=20, ge=1, le=200)):
|
|
6791
6848
|
"""List recent checkpoints from index.jsonl, enriched with metadata when available.
|
|
6792
6849
|
|
|
@@ -6853,7 +6910,7 @@ def _collect_checkpoints(limit: int) -> list:
|
|
|
6853
6910
|
return checkpoints[:limit]
|
|
6854
6911
|
|
|
6855
6912
|
|
|
6856
|
-
@app.get("/api/checkpoints/{checkpoint_id}")
|
|
6913
|
+
@app.get("/api/checkpoints/{checkpoint_id}", dependencies=[Depends(auth.require_scope("read"))])
|
|
6857
6914
|
async def get_checkpoint(checkpoint_id: str):
|
|
6858
6915
|
"""Get checkpoint details by ID."""
|
|
6859
6916
|
checkpoint_id = _sanitize_checkpoint_id(checkpoint_id)
|
|
@@ -7926,7 +7983,7 @@ async def prometheus_metrics():
|
|
|
7926
7983
|
# PRD Checklist Endpoints (v5.44.0)
|
|
7927
7984
|
# =============================================================================
|
|
7928
7985
|
|
|
7929
|
-
@app.get("/api/checklist")
|
|
7986
|
+
@app.get("/api/checklist", dependencies=[Depends(auth.require_scope("read"))])
|
|
7930
7987
|
async def get_checklist():
|
|
7931
7988
|
"""Get full PRD checklist with verification status."""
|
|
7932
7989
|
loki_dir = _get_loki_dir()
|
|
@@ -7939,7 +7996,7 @@ async def get_checklist():
|
|
|
7939
7996
|
return {"status": "error", "categories": [], "summary": {"total": 0, "verified": 0, "failing": 0, "pending": 0}}
|
|
7940
7997
|
|
|
7941
7998
|
|
|
7942
|
-
@app.get("/api/usage")
|
|
7999
|
+
@app.get("/api/usage", dependencies=[Depends(auth.require_scope("read"))])
|
|
7943
8000
|
async def get_usage_doc():
|
|
7944
8001
|
"""v7.7.1 F-1 follow-up: return the auto-generated USAGE.md from the
|
|
7945
8002
|
project root so Dashboard + Lab can surface "how to run / test the app".
|
|
@@ -7979,7 +8036,7 @@ async def get_usage_doc():
|
|
|
7979
8036
|
return out
|
|
7980
8037
|
|
|
7981
8038
|
|
|
7982
|
-
@app.get("/api/checklist/summary")
|
|
8039
|
+
@app.get("/api/checklist/summary", dependencies=[Depends(auth.require_scope("read"))])
|
|
7983
8040
|
async def get_checklist_summary():
|
|
7984
8041
|
"""Get checklist verification summary."""
|
|
7985
8042
|
loki_dir = _get_loki_dir()
|
|
@@ -7992,7 +8049,7 @@ async def get_checklist_summary():
|
|
|
7992
8049
|
return {"status": "error", "summary": {"total": 0, "verified": 0, "failing": 0, "pending": 0}}
|
|
7993
8050
|
|
|
7994
8051
|
|
|
7995
|
-
@app.get("/api/prd-observations")
|
|
8052
|
+
@app.get("/api/prd-observations", dependencies=[Depends(auth.require_scope("read"))])
|
|
7996
8053
|
async def get_prd_observations():
|
|
7997
8054
|
"""Get PRD quality analysis observations."""
|
|
7998
8055
|
loki_dir = _get_loki_dir()
|
|
@@ -8010,7 +8067,7 @@ async def get_prd_observations():
|
|
|
8010
8067
|
# Checklist Waiver Management Endpoints (Phase 4)
|
|
8011
8068
|
# =============================================================================
|
|
8012
8069
|
|
|
8013
|
-
@app.get("/api/checklist/waivers")
|
|
8070
|
+
@app.get("/api/checklist/waivers", dependencies=[Depends(auth.require_scope("read"))])
|
|
8014
8071
|
async def get_checklist_waivers():
|
|
8015
8072
|
"""Get all checklist waivers."""
|
|
8016
8073
|
waivers_file = _get_loki_dir() / "checklist" / "waivers.json"
|
|
@@ -8132,7 +8189,7 @@ _DEFAULT_QUALITY_GATES = [
|
|
|
8132
8189
|
]
|
|
8133
8190
|
|
|
8134
8191
|
|
|
8135
|
-
@app.get("/api/council/gate")
|
|
8192
|
+
@app.get("/api/council/gate", dependencies=[Depends(auth.require_scope("read"))])
|
|
8136
8193
|
async def get_council_gate():
|
|
8137
8194
|
"""Get council hard gate status.
|
|
8138
8195
|
|
|
@@ -8757,7 +8814,7 @@ def _discover_compose_app_runner_state_uncached(project_dir):
|
|
|
8757
8814
|
return None
|
|
8758
8815
|
|
|
8759
8816
|
|
|
8760
|
-
@app.get("/api/app-runner/status")
|
|
8817
|
+
@app.get("/api/app-runner/status", dependencies=[Depends(auth.require_scope("read"))])
|
|
8761
8818
|
async def get_app_runner_status():
|
|
8762
8819
|
"""Get app runner current status (with dead-run liveness reconciliation).
|
|
8763
8820
|
|
|
@@ -8834,7 +8891,7 @@ def _get_log_redactor():
|
|
|
8834
8891
|
return redactor
|
|
8835
8892
|
|
|
8836
8893
|
|
|
8837
|
-
@app.get("/api/app-runner/logs")
|
|
8894
|
+
@app.get("/api/app-runner/logs", dependencies=[Depends(auth.require_scope("read"))])
|
|
8838
8895
|
async def get_app_runner_logs(lines: int = Query(default=100, ge=1, le=1000)):
|
|
8839
8896
|
"""Get last N lines of app runner logs (redacted)."""
|
|
8840
8897
|
loki_dir = _get_loki_dir()
|
|
@@ -8853,7 +8910,7 @@ async def get_app_runner_logs(lines: int = Query(default=100, ge=1, le=1000)):
|
|
|
8853
8910
|
return {"lines": []}
|
|
8854
8911
|
|
|
8855
8912
|
|
|
8856
|
-
@app.get("/api/app-runner/errors")
|
|
8913
|
+
@app.get("/api/app-runner/errors", dependencies=[Depends(auth.require_scope("read"))])
|
|
8857
8914
|
async def get_app_runner_errors(lines: int = Query(default=50, ge=1, le=500)):
|
|
8858
8915
|
"""Get the last N lines of app runner output, redacted, plus crash state.
|
|
8859
8916
|
|
|
@@ -8928,7 +8985,7 @@ async def control_app_stop(request: Request):
|
|
|
8928
8985
|
# Playwright Verification Endpoints (v5.46.0)
|
|
8929
8986
|
# =============================================================================
|
|
8930
8987
|
|
|
8931
|
-
@app.get("/api/playwright/results")
|
|
8988
|
+
@app.get("/api/playwright/results", dependencies=[Depends(auth.require_scope("read"))])
|
|
8932
8989
|
async def get_playwright_results():
|
|
8933
8990
|
"""Get latest Playwright smoke test results."""
|
|
8934
8991
|
loki_dir = _get_loki_dir()
|
|
@@ -8941,7 +8998,7 @@ async def get_playwright_results():
|
|
|
8941
8998
|
return {"status": "error"}
|
|
8942
8999
|
|
|
8943
9000
|
|
|
8944
|
-
@app.get("/api/playwright/screenshot")
|
|
9001
|
+
@app.get("/api/playwright/screenshot", dependencies=[Depends(auth.require_scope("read"))])
|
|
8945
9002
|
async def get_playwright_screenshot():
|
|
8946
9003
|
"""Get path to latest Playwright screenshot."""
|
|
8947
9004
|
loki_dir = _get_loki_dir()
|
|
@@ -8981,12 +9038,12 @@ def _get_prompt_optimizer():
|
|
|
8981
9038
|
return _prompt_optimizer
|
|
8982
9039
|
|
|
8983
9040
|
|
|
8984
|
-
@app.get("/api/failures")
|
|
8985
|
-
def get_failures(sessions: int = 10):
|
|
9041
|
+
@app.get("/api/failures", dependencies=[Depends(auth.require_scope("read"))])
|
|
9042
|
+
def get_failures(request: Request, sessions: int = 10):
|
|
8986
9043
|
"""Get failure patterns from recent sessions."""
|
|
8987
9044
|
if sessions < 1 or sessions > 1000:
|
|
8988
9045
|
raise HTTPException(status_code=400, detail="sessions must be between 1 and 1000")
|
|
8989
|
-
if not _read_limiter.check("failures"):
|
|
9046
|
+
if not _read_limiter.check(_rate_key("failures", request)):
|
|
8990
9047
|
raise HTTPException(status_code=429, detail="Rate limit exceeded")
|
|
8991
9048
|
try:
|
|
8992
9049
|
return _get_failure_extractor().extract(sessions=sessions)
|
|
@@ -8995,7 +9052,7 @@ def get_failures(sessions: int = 10):
|
|
|
8995
9052
|
raise HTTPException(status_code=500, detail="Failed to extract failure patterns")
|
|
8996
9053
|
|
|
8997
9054
|
|
|
8998
|
-
@app.get("/api/prompt-versions")
|
|
9055
|
+
@app.get("/api/prompt-versions", dependencies=[Depends(auth.require_scope("read"))])
|
|
8999
9056
|
def get_prompt_versions():
|
|
9000
9057
|
"""Get current prompt optimization status."""
|
|
9001
9058
|
if not _read_limiter.check("prompt_versions"):
|
|
@@ -9070,10 +9127,10 @@ if STATIC_DIR:
|
|
|
9070
9127
|
# Activity Logger & Session Diff
|
|
9071
9128
|
# ---------------------------------------------------------------------------
|
|
9072
9129
|
|
|
9073
|
-
@app.get("/api/activity")
|
|
9074
|
-
def get_activity(since: Optional[str] = None, limit: int = Query(default=100, ge=1, le=1000)):
|
|
9130
|
+
@app.get("/api/activity", dependencies=[Depends(auth.require_scope("read"))])
|
|
9131
|
+
def get_activity(request: Request, since: Optional[str] = None, limit: int = Query(default=100, ge=1, le=1000)):
|
|
9075
9132
|
"""Get activity log entries, optionally filtered by timestamp."""
|
|
9076
|
-
if not _read_limiter.check("activity"):
|
|
9133
|
+
if not _read_limiter.check(_rate_key("activity", request)):
|
|
9077
9134
|
raise HTTPException(status_code=429, detail="Rate limit exceeded")
|
|
9078
9135
|
try:
|
|
9079
9136
|
activity_logger = get_activity_logger()
|
|
@@ -9088,7 +9145,7 @@ def get_activity(since: Optional[str] = None, limit: int = Query(default=100, ge
|
|
|
9088
9145
|
raise HTTPException(status_code=500, detail="Failed to read activity log")
|
|
9089
9146
|
|
|
9090
9147
|
|
|
9091
|
-
@app.get("/api/session-diff")
|
|
9148
|
+
@app.get("/api/session-diff", dependencies=[Depends(auth.require_scope("read"))])
|
|
9092
9149
|
def get_session_diff(since: Optional[str] = None):
|
|
9093
9150
|
"""Get structured session diff since timestamp. Defaults to last 24h."""
|
|
9094
9151
|
if not _read_limiter.check("session-diff"):
|
|
@@ -9139,7 +9196,7 @@ async def serve_favicon():
|
|
|
9139
9196
|
# Serve the self-contained cost + observability panel (R3). Zero-build
|
|
9140
9197
|
# standalone page that fetches /api/cost/timeline. Mirrors the proofs.html
|
|
9141
9198
|
# pattern: works without the SPA build.
|
|
9142
|
-
@app.get("/cost", include_in_schema=False)
|
|
9199
|
+
@app.get("/cost", include_in_schema=False, dependencies=[Depends(auth.require_scope("read"))])
|
|
9143
9200
|
async def serve_cost_panel():
|
|
9144
9201
|
"""Serve the standalone cost + observability HTML panel."""
|
|
9145
9202
|
if STATIC_DIR:
|
|
@@ -9151,7 +9208,7 @@ async def serve_cost_panel():
|
|
|
9151
9208
|
|
|
9152
9209
|
# R4: standalone trust-trajectory page that fetches /api/trust/trajectory.
|
|
9153
9210
|
# Mirrors the cost.html / /cost pattern: works without the SPA build.
|
|
9154
|
-
@app.get("/trust", include_in_schema=False)
|
|
9211
|
+
@app.get("/trust", include_in_schema=False, dependencies=[Depends(auth.require_scope("read"))])
|
|
9155
9212
|
async def serve_trust_panel():
|
|
9156
9213
|
"""Serve the standalone trust-trajectory HTML panel."""
|
|
9157
9214
|
if STATIC_DIR:
|
|
@@ -9162,7 +9219,7 @@ async def serve_trust_panel():
|
|
|
9162
9219
|
|
|
9163
9220
|
|
|
9164
9221
|
# Serve index.html or standalone HTML for root
|
|
9165
|
-
@app.get("/", include_in_schema=False)
|
|
9222
|
+
@app.get("/", include_in_schema=False, dependencies=[Depends(auth.require_scope("read"))])
|
|
9166
9223
|
async def serve_index():
|
|
9167
9224
|
"""Serve the frontend SPA or standalone HTML."""
|
|
9168
9225
|
# Try multiple index file locations
|
|
@@ -9211,10 +9268,10 @@ def _get_rigour() -> "RigourIntegration":
|
|
|
9211
9268
|
return _rigour
|
|
9212
9269
|
|
|
9213
9270
|
|
|
9214
|
-
@app.get("/api/quality-score")
|
|
9215
|
-
def get_quality_score():
|
|
9271
|
+
@app.get("/api/quality-score", dependencies=[Depends(auth.require_scope("read"))])
|
|
9272
|
+
def get_quality_score(request: Request):
|
|
9216
9273
|
"""Get current quality score from the most recent Rigour scan."""
|
|
9217
|
-
if not _read_limiter.check("quality-score"):
|
|
9274
|
+
if not _read_limiter.check(_rate_key("quality-score", request)):
|
|
9218
9275
|
raise HTTPException(status_code=429, detail="Rate limit exceeded")
|
|
9219
9276
|
try:
|
|
9220
9277
|
rigour = _get_rigour()
|
|
@@ -9224,7 +9281,7 @@ def get_quality_score():
|
|
|
9224
9281
|
raise HTTPException(status_code=500, detail="Failed to read quality score")
|
|
9225
9282
|
|
|
9226
9283
|
|
|
9227
|
-
@app.get("/api/quality-score/history")
|
|
9284
|
+
@app.get("/api/quality-score/history", dependencies=[Depends(auth.require_scope("read"))])
|
|
9228
9285
|
def get_quality_score_history(limit: int = Query(50, ge=1, le=500)):
|
|
9229
9286
|
"""Get quality score trend over time."""
|
|
9230
9287
|
if not _read_limiter.check("quality-history"):
|
|
@@ -9257,7 +9314,7 @@ async def run_quality_scan(preset: str = Query("default")):
|
|
|
9257
9314
|
return result
|
|
9258
9315
|
|
|
9259
9316
|
|
|
9260
|
-
@app.get("/api/quality-report")
|
|
9317
|
+
@app.get("/api/quality-report", dependencies=[Depends(auth.require_scope("read"))])
|
|
9261
9318
|
def get_quality_report(fmt: str = Query("json", alias="format", pattern="^(json|markdown|html)$")):
|
|
9262
9319
|
"""Get an exportable quality audit report."""
|
|
9263
9320
|
if not _read_limiter.check("quality-report"):
|
|
@@ -9617,7 +9674,7 @@ def _last_fallback_ts(events: list[dict[str, Any]]) -> Optional[str]:
|
|
|
9617
9674
|
return None
|
|
9618
9675
|
|
|
9619
9676
|
|
|
9620
|
-
@app.get("/api/managed/events")
|
|
9677
|
+
@app.get("/api/managed/events", dependencies=[Depends(auth.require_scope("read"))])
|
|
9621
9678
|
async def get_managed_events(
|
|
9622
9679
|
limit: int = Query(default=100, ge=1, le=_MANAGED_EVENTS_TAIL_MAX),
|
|
9623
9680
|
since: Optional[str] = Query(default=None),
|
|
@@ -9646,7 +9703,7 @@ async def get_managed_events(
|
|
|
9646
9703
|
return {"events": [], "count": 0, "error": str(exc)}
|
|
9647
9704
|
|
|
9648
9705
|
|
|
9649
|
-
@app.get("/api/managed/status")
|
|
9706
|
+
@app.get("/api/managed/status", dependencies=[Depends(auth.require_scope("read"))])
|
|
9650
9707
|
async def get_managed_status():
|
|
9651
9708
|
"""
|
|
9652
9709
|
Return the managed-agents flag snapshot plus last_fallback_ts.
|
|
@@ -9672,7 +9729,7 @@ async def get_managed_status():
|
|
|
9672
9729
|
return snapshot
|
|
9673
9730
|
|
|
9674
9731
|
|
|
9675
|
-
@app.get("/api/managed/memory_versions/{memory_id}")
|
|
9732
|
+
@app.get("/api/managed/memory_versions/{memory_id}", dependencies=[Depends(auth.require_scope("read"))])
|
|
9676
9733
|
async def list_managed_memory_versions(memory_id: str):
|
|
9677
9734
|
"""
|
|
9678
9735
|
Proxy to beta.memory_stores.memory_versions.list(memory_id=...).
|
|
@@ -9771,7 +9828,7 @@ async def list_managed_memory_versions(memory_id: str):
|
|
|
9771
9828
|
# ---------------------------------------------------------------------------
|
|
9772
9829
|
|
|
9773
9830
|
|
|
9774
|
-
@app.get("/api/findings/{iteration}")
|
|
9831
|
+
@app.get("/api/findings/{iteration}", dependencies=[Depends(auth.require_scope("read"))])
|
|
9775
9832
|
async def get_findings(iteration: int):
|
|
9776
9833
|
"""Read structured code-review findings for a given iteration."""
|
|
9777
9834
|
base = _get_loki_dir()
|
|
@@ -9800,7 +9857,7 @@ async def get_findings(iteration: int):
|
|
|
9800
9857
|
detail=f"No findings for iteration {iteration}")
|
|
9801
9858
|
|
|
9802
9859
|
|
|
9803
|
-
@app.get("/api/quality/architecture")
|
|
9860
|
+
@app.get("/api/quality/architecture", dependencies=[Depends(auth.require_scope("read"))])
|
|
9804
9861
|
async def get_quality_architecture():
|
|
9805
9862
|
"""Return the sentrux architectural-drift series.
|
|
9806
9863
|
|
|
@@ -9862,7 +9919,7 @@ async def get_quality_architecture():
|
|
|
9862
9919
|
return {"series": series, "current": current, "samples": len(series)}
|
|
9863
9920
|
|
|
9864
9921
|
|
|
9865
|
-
@app.get("/api/learnings")
|
|
9922
|
+
@app.get("/api/learnings", dependencies=[Depends(auth.require_scope("read"))])
|
|
9866
9923
|
async def get_learnings(limit: int = 50):
|
|
9867
9924
|
"""Read recent learnings (newest first)."""
|
|
9868
9925
|
base = _get_loki_dir()
|
|
@@ -9881,7 +9938,7 @@ async def get_learnings(limit: int = 50):
|
|
|
9881
9938
|
"learnings": sliced}
|
|
9882
9939
|
|
|
9883
9940
|
|
|
9884
|
-
@app.get("/api/escalations")
|
|
9941
|
+
@app.get("/api/escalations", dependencies=[Depends(auth.require_scope("read"))])
|
|
9885
9942
|
async def list_escalations():
|
|
9886
9943
|
"""List handoff documents under .loki/escalations/."""
|
|
9887
9944
|
base = _get_loki_dir()
|
|
@@ -9913,7 +9970,7 @@ async def list_escalations():
|
|
|
9913
9970
|
return {"escalations": items}
|
|
9914
9971
|
|
|
9915
9972
|
|
|
9916
|
-
@app.get("/api/escalations/{filename}")
|
|
9973
|
+
@app.get("/api/escalations/{filename}", dependencies=[Depends(auth.require_scope("read"))])
|
|
9917
9974
|
async def get_escalation(filename: str):
|
|
9918
9975
|
"""Read one handoff document. Path-traversal-safe."""
|
|
9919
9976
|
if "\\" in filename or filename.startswith("."):
|
|
@@ -9975,7 +10032,7 @@ def _safe_proof_run_dir(run_id: str) -> _Path:
|
|
|
9975
10032
|
return _Path(target)
|
|
9976
10033
|
|
|
9977
10034
|
|
|
9978
|
-
@app.get("/api/proofs")
|
|
10035
|
+
@app.get("/api/proofs", dependencies=[Depends(auth.require_scope("read"))])
|
|
9979
10036
|
async def list_proofs():
|
|
9980
10037
|
"""List proof-of-run artifacts for the active project's .loki/proofs/."""
|
|
9981
10038
|
proofs_dir = _proofs_dir()
|
|
@@ -10007,7 +10064,7 @@ async def list_proofs():
|
|
|
10007
10064
|
return {"proofs": items}
|
|
10008
10065
|
|
|
10009
10066
|
|
|
10010
|
-
@app.get("/api/proofs/{run_id}")
|
|
10067
|
+
@app.get("/api/proofs/{run_id}", dependencies=[Depends(auth.require_scope("read"))])
|
|
10011
10068
|
async def get_proof(run_id: str):
|
|
10012
10069
|
"""Return the redacted proof.json for one run."""
|
|
10013
10070
|
run_dir = _safe_proof_run_dir(run_id)
|
|
@@ -10020,7 +10077,7 @@ async def get_proof(run_id: str):
|
|
|
10020
10077
|
return JSONResponse(content=data)
|
|
10021
10078
|
|
|
10022
10079
|
|
|
10023
|
-
@app.get("/api/proofs/{run_id}/html")
|
|
10080
|
+
@app.get("/api/proofs/{run_id}/html", dependencies=[Depends(auth.require_scope("read"))])
|
|
10024
10081
|
async def get_proof_html(run_id: str):
|
|
10025
10082
|
"""Serve the self-contained shareable proof page for one run."""
|
|
10026
10083
|
run_dir = _safe_proof_run_dir(run_id)
|
|
@@ -10159,7 +10216,7 @@ async def post_wiki_ask(req: WikiAskRequest):
|
|
|
10159
10216
|
# or static asset mounts. This lets the dashboard UI handle client-side routing.
|
|
10160
10217
|
# Must be registered LAST so it never shadows an API endpoint.
|
|
10161
10218
|
# ---------------------------------------------------------------------------
|
|
10162
|
-
@app.get("/{full_path:path}", include_in_schema=False)
|
|
10219
|
+
@app.get("/{full_path:path}", include_in_schema=False, dependencies=[Depends(auth.require_scope("read"))])
|
|
10163
10220
|
async def serve_spa_catchall(full_path: str):
|
|
10164
10221
|
"""Serve static files or fall back to index.html for SPA routing.
|
|
10165
10222
|
|