loki-mode 6.37.1 → 6.37.3
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/dashboard/__init__.py +1 -1
- package/dashboard/auth.py +28 -19
- package/dashboard/server.py +18 -8
- package/dashboard/static/index.html +20 -20
- package/docs/INSTALLATION.md +1 -1
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/web-app/dist/assets/index-BVw_Fxig.js +66 -0
- package/web-app/dist/assets/index-CDiM5Vh4.css +1 -0
- package/web-app/dist/index.html +2 -2
- package/web-app/server.py +123 -41
- package/web-app/dist/assets/index-Cm-odtQC.js +0 -66
- package/web-app/dist/assets/index-SenDDIyx.css +0 -1
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes PRD to deployed product with minimal human intervention. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v6.37.
|
|
6
|
+
# Loki Mode v6.37.3
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -267,4 +267,4 @@ The following features are documented in skill modules but not yet fully automat
|
|
|
267
267
|
| Quality gates 3-reviewer system | Implemented (v5.35.0) | 5 specialist reviewers in `skills/quality-gates.md`; execution in run.sh |
|
|
268
268
|
| Benchmarks (HumanEval, SWE-bench) | Infrastructure only | Runner scripts and datasets exist in `benchmarks/`; no published results |
|
|
269
269
|
|
|
270
|
-
**v6.37.
|
|
270
|
+
**v6.37.3 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
6.37.
|
|
1
|
+
6.37.3
|
package/dashboard/__init__.py
CHANGED
package/dashboard/auth.py
CHANGED
|
@@ -14,6 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
import base64
|
|
16
16
|
import hashlib
|
|
17
|
+
import hmac
|
|
17
18
|
import json
|
|
18
19
|
import os
|
|
19
20
|
import secrets
|
|
@@ -103,6 +104,9 @@ def _save_tokens(tokens: dict) -> None:
|
|
|
103
104
|
TOKEN_FILE.touch(mode=0o600, exist_ok=True)
|
|
104
105
|
with open(TOKEN_FILE, "w") as f:
|
|
105
106
|
json.dump(tokens, f, indent=2, default=str)
|
|
107
|
+
# Enforce 0600 on every write, not just creation -- touch(mode=) only
|
|
108
|
+
# applies when the file is new, so an external chmod would persist.
|
|
109
|
+
os.chmod(TOKEN_FILE, 0o600)
|
|
106
110
|
|
|
107
111
|
|
|
108
112
|
def _hash_token(token: str, salt: str = None) -> tuple[str, str]:
|
|
@@ -123,7 +127,7 @@ def _hash_token(token: str, salt: str = None) -> tuple[str, str]:
|
|
|
123
127
|
|
|
124
128
|
def _constant_time_compare(a: str, b: str) -> bool:
|
|
125
129
|
"""Constant-time string comparison to prevent timing attacks."""
|
|
126
|
-
return
|
|
130
|
+
return hmac.compare_digest(a.encode(), b.encode())
|
|
127
131
|
|
|
128
132
|
|
|
129
133
|
def resolve_scopes(role_or_scopes) -> list[str]:
|
|
@@ -342,30 +346,35 @@ def validate_token(raw_token: str) -> Optional[dict]:
|
|
|
342
346
|
|
|
343
347
|
tokens = _load_tokens()
|
|
344
348
|
|
|
345
|
-
#
|
|
349
|
+
# Iterate ALL tokens to prevent timing side-channel that leaks token count.
|
|
350
|
+
# Do not short-circuit on match -- always hash and compare every entry.
|
|
351
|
+
matched_token: Optional[dict] = None
|
|
346
352
|
for token in tokens["tokens"].values():
|
|
347
353
|
stored_salt = token.get("salt", "")
|
|
348
354
|
token_hash, _ = _hash_token(raw_token, salt=stored_salt)
|
|
349
355
|
if _constant_time_compare(token["hash"], token_hash):
|
|
350
|
-
|
|
351
|
-
|
|
356
|
+
matched_token = token
|
|
357
|
+
|
|
358
|
+
if matched_token is not None:
|
|
359
|
+
# Check if revoked
|
|
360
|
+
if matched_token.get("revoked"):
|
|
361
|
+
return None
|
|
362
|
+
|
|
363
|
+
# Check expiration
|
|
364
|
+
if matched_token.get("expires_at"):
|
|
365
|
+
expires = datetime.fromisoformat(matched_token["expires_at"])
|
|
366
|
+
if datetime.now(timezone.utc) > expires:
|
|
352
367
|
return None
|
|
353
368
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
return {
|
|
365
|
-
"id": token["id"],
|
|
366
|
-
"name": token["name"],
|
|
367
|
-
"scopes": token["scopes"],
|
|
368
|
-
}
|
|
369
|
+
# Update last used
|
|
370
|
+
matched_token["last_used"] = datetime.now(timezone.utc).isoformat()
|
|
371
|
+
_save_tokens(tokens)
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
"id": matched_token["id"],
|
|
375
|
+
"name": matched_token["name"],
|
|
376
|
+
"scopes": matched_token["scopes"],
|
|
377
|
+
}
|
|
369
378
|
|
|
370
379
|
return None
|
|
371
380
|
|
package/dashboard/server.py
CHANGED
|
@@ -1284,20 +1284,23 @@ async def websocket_endpoint(websocket: WebSocket) -> None:
|
|
|
1284
1284
|
"data": {"message": "Connected to Loki Dashboard"},
|
|
1285
1285
|
})
|
|
1286
1286
|
|
|
1287
|
-
# Keep connection alive and handle incoming messages
|
|
1287
|
+
# Keep connection alive and handle incoming messages.
|
|
1288
|
+
# Close idle connections after ~60s of no response to pings.
|
|
1289
|
+
missed_pongs = 0
|
|
1288
1290
|
while True:
|
|
1289
1291
|
try:
|
|
1290
1292
|
data = await asyncio.wait_for(
|
|
1291
1293
|
websocket.receive_text(),
|
|
1292
|
-
timeout=30.0 # Ping every 30 seconds
|
|
1294
|
+
timeout=30.0 # Ping every 30 seconds of silence
|
|
1293
1295
|
)
|
|
1294
|
-
#
|
|
1296
|
+
missed_pongs = 0 # any message resets idle counter
|
|
1295
1297
|
try:
|
|
1296
1298
|
message = json.loads(data)
|
|
1297
1299
|
if message.get("type") == "ping":
|
|
1298
1300
|
await manager.send_personal(websocket, {"type": "pong"})
|
|
1301
|
+
elif message.get("type") == "pong":
|
|
1302
|
+
pass # client responded to our ping
|
|
1299
1303
|
elif message.get("type") == "subscribe":
|
|
1300
|
-
# Could implement channel subscriptions here
|
|
1301
1304
|
await manager.send_personal(websocket, {
|
|
1302
1305
|
"type": "subscribed",
|
|
1303
1306
|
"data": message.get("data", {}),
|
|
@@ -1305,8 +1308,15 @@ async def websocket_endpoint(websocket: WebSocket) -> None:
|
|
|
1305
1308
|
except json.JSONDecodeError as e:
|
|
1306
1309
|
logger.debug(f"WebSocket received invalid JSON: {e}")
|
|
1307
1310
|
except asyncio.TimeoutError:
|
|
1308
|
-
|
|
1309
|
-
|
|
1311
|
+
missed_pongs += 1
|
|
1312
|
+
if missed_pongs >= 2:
|
|
1313
|
+
# Two consecutive pings with no reply -- close idle connection
|
|
1314
|
+
logger.info("Closing idle WebSocket (no pong response)")
|
|
1315
|
+
break
|
|
1316
|
+
try:
|
|
1317
|
+
await manager.send_personal(websocket, {"type": "ping"})
|
|
1318
|
+
except Exception:
|
|
1319
|
+
break
|
|
1310
1320
|
|
|
1311
1321
|
except WebSocketDisconnect:
|
|
1312
1322
|
manager.disconnect(websocket)
|
|
@@ -1608,7 +1618,7 @@ class AuditQueryParams(BaseModel):
|
|
|
1608
1618
|
offset: int = 0
|
|
1609
1619
|
|
|
1610
1620
|
|
|
1611
|
-
@app.get("/api/enterprise/audit")
|
|
1621
|
+
@app.get("/api/enterprise/audit", dependencies=[Depends(auth.require_scope("audit"))])
|
|
1612
1622
|
async def query_audit_logs(
|
|
1613
1623
|
start_date: Optional[str] = None,
|
|
1614
1624
|
end_date: Optional[str] = None,
|
|
@@ -1640,7 +1650,7 @@ async def query_audit_logs(
|
|
|
1640
1650
|
)
|
|
1641
1651
|
|
|
1642
1652
|
|
|
1643
|
-
@app.get("/api/enterprise/audit/summary")
|
|
1653
|
+
@app.get("/api/enterprise/audit/summary", dependencies=[Depends(auth.require_scope("audit"))])
|
|
1644
1654
|
async def get_audit_summary(days: int = 7):
|
|
1645
1655
|
"""Get audit activity summary."""
|
|
1646
1656
|
if not audit.is_audit_enabled():
|