loki-mode 6.71.1 → 6.72.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.
Files changed (91) hide show
  1. package/README.md +9 -1
  2. package/SKILL.md +2 -2
  3. package/VERSION +1 -1
  4. package/autonomy/hooks/migration-hooks.sh +26 -0
  5. package/autonomy/loki +429 -92
  6. package/autonomy/run.sh +219 -38
  7. package/dashboard/__init__.py +1 -1
  8. package/dashboard/server.py +101 -19
  9. package/docs/INSTALLATION.md +20 -11
  10. package/docs/bug-fixes/agent-01-cli-fixes.md +101 -0
  11. package/docs/bug-fixes/agent-02-purplelab-fixes.md +88 -0
  12. package/docs/bug-fixes/agent-03-dashboard-fixes.md +119 -0
  13. package/docs/bug-fixes/agent-04-memory-fixes.md +105 -0
  14. package/docs/bug-fixes/agent-05-provider-fixes.md +86 -0
  15. package/docs/bug-fixes/agent-06-integration-fixes.md +101 -0
  16. package/docs/bug-fixes/agent-07-dash-run-fixes.md +101 -0
  17. package/docs/bug-fixes/agent-08-docker-fixes.md +164 -0
  18. package/docs/bug-fixes/agent-09-e2e-build-fixes.md +69 -0
  19. package/docs/bug-fixes/agent-10-e2e-fullstack-fixes.md +102 -0
  20. package/docs/bug-fixes/agent-11-e2e-session-fixes.md +70 -0
  21. package/docs/bug-fixes/agent-12-scenario-fixes.md +120 -0
  22. package/docs/bug-fixes/agent-13-enterprise-fixes.md +143 -0
  23. package/docs/bug-fixes/agent-14-uat-newuser-fixes.md +88 -0
  24. package/docs/bug-fixes/agent-15-uat-poweruser-fixes.md +132 -0
  25. package/docs/bug-fixes/agent-19-code-review.md +316 -0
  26. package/docs/bug-fixes/agent-20-architecture-review.md +331 -0
  27. package/docs/competitive/bolt-new-analysis.md +579 -0
  28. package/docs/competitive/emergence-others-analysis.md +605 -0
  29. package/docs/competitive/replit-lovable-analysis.md +622 -0
  30. package/docs/test-scenarios/edge-cases.md +813 -0
  31. package/docs/test-scenarios/enterprise-scenarios.md +732 -0
  32. package/mcp/__init__.py +1 -1
  33. package/mcp/server.py +49 -5
  34. package/memory/consolidation.py +33 -0
  35. package/memory/embeddings.py +10 -1
  36. package/memory/engine.py +83 -38
  37. package/memory/retrieval.py +36 -0
  38. package/memory/storage.py +56 -4
  39. package/memory/token_economics.py +14 -2
  40. package/memory/vector_index.py +36 -7
  41. package/package.json +1 -1
  42. package/providers/gemini.sh +89 -2
  43. package/templates/README.md +1 -1
  44. package/templates/cli-tool.md +30 -0
  45. package/templates/dashboard.md +4 -0
  46. package/templates/data-pipeline.md +4 -0
  47. package/templates/discord-bot.md +47 -0
  48. package/templates/game.md +4 -0
  49. package/templates/microservice.md +4 -0
  50. package/templates/npm-library.md +4 -0
  51. package/templates/rest-api-auth.md +50 -20
  52. package/templates/rest-api.md +15 -0
  53. package/templates/saas-starter.md +1 -1
  54. package/templates/slack-bot.md +36 -0
  55. package/templates/static-landing-page.md +9 -1
  56. package/templates/web-scraper.md +4 -0
  57. package/web-app/dist/assets/Badge-CeBkFjo6.js +1 -0
  58. package/web-app/dist/assets/Button-yuhqo8Fq.js +1 -0
  59. package/web-app/dist/assets/{Card-B1bV4syB.js → Card-BG17vsX0.js} +1 -1
  60. package/web-app/dist/assets/{HomePage-CZTV6Nea.js → HomePage-BMSQ7Apj.js} +3 -3
  61. package/web-app/dist/assets/{LoginPage-D4UdURJc.js → LoginPage-aH_6iolg.js} +1 -1
  62. package/web-app/dist/assets/{NotFoundPage-CCLSeL6j.js → NotFoundPage-Di8cNtB1.js} +1 -1
  63. package/web-app/dist/assets/ProjectPage-BtRssmw9.js +285 -0
  64. package/web-app/dist/assets/ProjectsPage-B-FTFagc.js +6 -0
  65. package/web-app/dist/assets/{SettingsPage-Xuv8EfAg.js → SettingsPage-DIJPBla4.js} +1 -1
  66. package/web-app/dist/assets/TeamsPage--19fNX7w.js +36 -0
  67. package/web-app/dist/assets/TemplatesPage-ChUQNOOv.js +11 -0
  68. package/web-app/dist/assets/TerminalOutput-Dwrzecyl.js +31 -0
  69. package/web-app/dist/assets/activity-BNRWeu9N.js +6 -0
  70. package/web-app/dist/assets/{arrow-left-CaGtolHc.js → arrow-left-Ce6g1_YE.js} +1 -1
  71. package/web-app/dist/assets/circle-alert-LIndawHL.js +11 -0
  72. package/web-app/dist/assets/clock-Bpj4VPlP.js +6 -0
  73. package/web-app/dist/assets/{external-link-CazyUyav.js → external-link-BhhdF0iQ.js} +1 -1
  74. package/web-app/dist/assets/folder-open-CM2LgfxI.js +11 -0
  75. package/web-app/dist/assets/index-8-KpWWq7.css +1 -0
  76. package/web-app/dist/assets/index-kPDW4e_b.js +236 -0
  77. package/web-app/dist/assets/lock-sAk3Xe54.js +16 -0
  78. package/web-app/dist/assets/search-CR-2i9by.js +6 -0
  79. package/web-app/dist/assets/server-DuFh4ymA.js +26 -0
  80. package/web-app/dist/assets/trash-2-BmkkT8V_.js +11 -0
  81. package/web-app/dist/index.html +2 -2
  82. package/web-app/server.py +1321 -53
  83. package/web-app/dist/assets/Badge-CBUx2PjL.js +0 -6
  84. package/web-app/dist/assets/Button-DsRiznlh.js +0 -21
  85. package/web-app/dist/assets/ProjectPage-D0w_X9tG.js +0 -237
  86. package/web-app/dist/assets/ProjectsPage-ByYxDlKC.js +0 -16
  87. package/web-app/dist/assets/TemplatesPage-BKWN07mc.js +0 -1
  88. package/web-app/dist/assets/TerminalOutput-Dj98V8Z-.js +0 -51
  89. package/web-app/dist/assets/clock-C_CDmobx.js +0 -11
  90. package/web-app/dist/assets/index-D452pFGl.css +0 -1
  91. package/web-app/dist/assets/index-Df4_kgLY.js +0 -196
@@ -93,8 +93,14 @@ def _safe_json_read(path: _Path, default: Any = None) -> Any:
93
93
 
94
94
 
95
95
  def _safe_read_text(path: _Path) -> str:
96
- """Read a text file with UTF-8 encoding, replacing non-UTF-8 bytes."""
97
- return _Path(path).read_text(encoding="utf-8", errors="replace")
96
+ """Read a text file with UTF-8 encoding, replacing non-UTF-8 bytes.
97
+
98
+ Returns empty string on any I/O or encoding error (truly safe).
99
+ """
100
+ try:
101
+ return _Path(path).read_text(encoding="utf-8", errors="replace")
102
+ except (OSError, IOError, ValueError):
103
+ return ""
98
104
 
99
105
 
100
106
  # ---------------------------------------------------------------------------
@@ -362,13 +368,39 @@ async def _push_loki_state_loop() -> None:
362
368
  try:
363
369
  raw = _safe_json_read(state_file, {})
364
370
  # Transform to StatusResponse-compatible format
371
+ # BUG-NEW-001: Validate agent PIDs (match get_status behavior)
365
372
  agents_list = raw.get("agents", [])
366
- running_agents = len(agents_list) if isinstance(agents_list, list) else 0
373
+ running_agents = 0
374
+ if isinstance(agents_list, list):
375
+ for _agent in agents_list:
376
+ _apid = _agent.get("pid") if isinstance(_agent, dict) else None
377
+ if _apid:
378
+ try:
379
+ os.kill(int(_apid), 0)
380
+ running_agents += 1
381
+ except (OSError, ValueError, TypeError):
382
+ pass
383
+ else:
384
+ running_agents += 1 # No PID field -- count as running (legacy)
367
385
  tasks = raw.get("tasks", {})
368
386
  pending = tasks.get("pending", [])
369
387
  in_prog = tasks.get("inProgress", [])
388
+ # BUG-NEW-006: Cross-check PID to avoid broadcasting
389
+ # stale "running" status when process has crashed
390
+ _pid_alive = False
391
+ _ws_pid_file = loki_dir / "loki.pid"
392
+ if _ws_pid_file.exists():
393
+ try:
394
+ _ws_pid = int(_ws_pid_file.read_text().strip())
395
+ os.kill(_ws_pid, 0)
396
+ _pid_alive = True
397
+ except (ValueError, OSError, ProcessLookupError):
398
+ pass
399
+
370
400
  status_str = raw.get("mode", "autonomous")
371
- if status_str == "paused":
401
+ if not _pid_alive:
402
+ status_str = "stopped"
403
+ elif status_str == "paused":
372
404
  status_str = "paused"
373
405
  elif status_str in ("stopped", ""):
374
406
  status_str = "stopped"
@@ -513,7 +545,7 @@ async def agent_card() -> dict:
513
545
  "multi_tenant": True,
514
546
  "rbac": True,
515
547
  "audit_log": True,
516
- "sso": False,
548
+ "sso": auth.is_oidc_mode(),
517
549
  },
518
550
  "authentication": {
519
551
  "schemes": ["bearer", "api-key"],
@@ -1077,7 +1109,15 @@ async def list_tasks(
1077
1109
  fpath = queue_dir / queue_file
1078
1110
  if fpath.exists():
1079
1111
  try:
1080
- items = json.loads(fpath.read_text())
1112
+ raw_items = json.loads(fpath.read_text())
1113
+ # BUG-NEW-002: Support both array [...] and object {"tasks": [...]} formats
1114
+ # (matches run.sh load_queue_tasks which supports both)
1115
+ if isinstance(raw_items, dict):
1116
+ items = raw_items.get("tasks", [])
1117
+ elif isinstance(raw_items, list):
1118
+ items = raw_items
1119
+ else:
1120
+ items = []
1081
1121
  if isinstance(items, list):
1082
1122
  for i, item in enumerate(items):
1083
1123
  if isinstance(item, dict):
@@ -1592,8 +1632,9 @@ async def sync_registry():
1592
1632
  raise HTTPException(status_code=429, detail="Rate limit exceeded")
1593
1633
 
1594
1634
  try:
1635
+ loop = asyncio.get_running_loop()
1595
1636
  result = await asyncio.wait_for(
1596
- asyncio.get_event_loop().run_in_executor(None, registry.sync_registry_with_discovery),
1637
+ loop.run_in_executor(None, registry.sync_registry_with_discovery),
1597
1638
  timeout=30.0,
1598
1639
  )
1599
1640
  except asyncio.TimeoutError:
@@ -1630,7 +1671,7 @@ class FocusRequest(BaseModel):
1630
1671
  project_dir: str
1631
1672
 
1632
1673
 
1633
- @app.post("/api/focus")
1674
+ @app.post("/api/focus", dependencies=[Depends(auth.require_scope("control"))])
1634
1675
  async def set_focus(request: FocusRequest):
1635
1676
  """Set the active project directory for .loki/ resolution.
1636
1677
 
@@ -1642,10 +1683,17 @@ async def set_focus(request: FocusRequest):
1642
1683
  project_dir = request.project_dir.strip()
1643
1684
  if not project_dir:
1644
1685
  raise HTTPException(status_code=400, detail="project_dir must not be empty")
1645
- p = _Path(project_dir)
1686
+ p = _Path(project_dir).resolve()
1646
1687
  if not p.is_dir():
1647
1688
  raise HTTPException(status_code=400, detail=f"Directory does not exist: {project_dir}")
1648
- _active_project_dir = str(p.resolve())
1689
+ # Require the target directory to contain a .loki/ subdirectory to prevent
1690
+ # pointing the dashboard at arbitrary filesystem locations.
1691
+ if not (p / ".loki").is_dir():
1692
+ raise HTTPException(
1693
+ status_code=400,
1694
+ detail=f"Directory does not contain a .loki/ subdirectory: {project_dir}"
1695
+ )
1696
+ _active_project_dir = str(p)
1649
1697
  return {"project_dir": _active_project_dir, "loki_dir": str(_get_loki_dir())}
1650
1698
 
1651
1699
 
@@ -1658,7 +1706,7 @@ async def get_focus():
1658
1706
  }
1659
1707
 
1660
1708
 
1661
- @app.delete("/api/focus")
1709
+ @app.delete("/api/focus", dependencies=[Depends(auth.require_scope("control"))])
1662
1710
  async def clear_focus():
1663
1711
  """Clear the active project directory override (revert to CWD-based resolution)."""
1664
1712
  global _active_project_dir
@@ -1752,7 +1800,7 @@ async def create_token(request: TokenCreateRequest):
1752
1800
  raise HTTPException(status_code=400, detail=str(e))
1753
1801
 
1754
1802
 
1755
- @app.get("/api/enterprise/tokens", response_model=list[TokenResponse])
1803
+ @app.get("/api/enterprise/tokens", response_model=list[TokenResponse], dependencies=[Depends(auth.require_scope("admin"))])
1756
1804
  async def list_tokens(include_revoked: bool = False):
1757
1805
  """List all API tokens (enterprise only)."""
1758
1806
  if not auth.is_enterprise_mode():
@@ -2347,7 +2395,7 @@ def _read_learning_signals(signal_type: Optional[str] = None, limit: int = 50) -
2347
2395
 
2348
2396
  @app.get("/api/learning/metrics")
2349
2397
  async def get_learning_metrics(
2350
- timeRange: str = "7d",
2398
+ timeRange: str = Query("7d", pattern=r"^\d{1,4}[hdm]$"),
2351
2399
  signalType: Optional[str] = None,
2352
2400
  source: Optional[str] = None,
2353
2401
  ):
@@ -2416,7 +2464,7 @@ async def get_learning_metrics(
2416
2464
 
2417
2465
  @app.get("/api/learning/trends")
2418
2466
  async def get_learning_trends(
2419
- timeRange: str = "7d",
2467
+ timeRange: str = Query("7d", pattern=r"^\d{1,4}[hdm]$"),
2420
2468
  signalType: Optional[str] = None,
2421
2469
  source: Optional[str] = None,
2422
2470
  ):
@@ -2436,7 +2484,7 @@ async def get_learning_trends(
2436
2484
 
2437
2485
  @app.get("/api/learning/signals")
2438
2486
  async def get_learning_signals(
2439
- timeRange: str = "7d",
2487
+ timeRange: str = Query("7d", pattern=r"^\d{1,4}[hdm]$"),
2440
2488
  signalType: Optional[str] = None,
2441
2489
  source: Optional[str] = None,
2442
2490
  limit: int = Query(default=50, ge=1, le=1000),
@@ -2876,14 +2924,30 @@ async def stop_session(request: Request):
2876
2924
  stop_file.parent.mkdir(parents=True, exist_ok=True)
2877
2925
  stop_file.write_text(datetime.now(timezone.utc).isoformat())
2878
2926
 
2879
- # Try to kill the process
2927
+ # BUG-ST-004: Send SIGTERM and wait for process to actually exit
2880
2928
  pid_file = _get_loki_dir() / "loki.pid"
2929
+ process_stopped = False
2881
2930
  if pid_file.exists():
2882
2931
  try:
2883
2932
  pid = int(pid_file.read_text().strip())
2884
2933
  os.kill(pid, 15) # SIGTERM
2934
+ # Wait up to 5s for graceful shutdown
2935
+ for _ in range(10):
2936
+ await asyncio.sleep(0.5)
2937
+ try:
2938
+ os.kill(pid, 0) # Check if still alive
2939
+ except OSError:
2940
+ process_stopped = True
2941
+ break
2942
+ if not process_stopped:
2943
+ # Process didn't exit gracefully, send SIGKILL
2944
+ try:
2945
+ os.kill(pid, 9)
2946
+ process_stopped = True
2947
+ except (OSError, ProcessLookupError):
2948
+ process_stopped = True
2885
2949
  except (ValueError, OSError, ProcessLookupError):
2886
- pass
2950
+ process_stopped = True
2887
2951
 
2888
2952
  # Mark session.json as stopped
2889
2953
  session_file = _get_loki_dir() / "session.json"
@@ -2895,7 +2959,21 @@ async def stop_session(request: Request):
2895
2959
  except Exception:
2896
2960
  pass
2897
2961
 
2898
- return {"success": True, "message": "Stop signal sent"}
2962
+ # BUG-NEW-005: Clean up orphaned per-iteration temp files left by killed process
2963
+ logs_dir = _get_loki_dir() / "logs"
2964
+ if logs_dir.exists():
2965
+ import glob as _glob_mod
2966
+ for orphan in _glob_mod.glob(str(logs_dir / "iter-output-*")):
2967
+ try:
2968
+ os.unlink(orphan)
2969
+ except OSError:
2970
+ pass
2971
+
2972
+ return {
2973
+ "success": True,
2974
+ "message": "Session stopped" if process_stopped else "Stop signal sent",
2975
+ "process_stopped": process_stopped,
2976
+ }
2899
2977
 
2900
2978
 
2901
2979
  # =============================================================================
@@ -4956,7 +5034,7 @@ async def run_quality_scan(preset: str = Query("default")):
4956
5034
 
4957
5035
 
4958
5036
  @app.get("/api/quality-report")
4959
- def get_quality_report(fmt: str = Query("json", alias="format")):
5037
+ def get_quality_report(fmt: str = Query("json", alias="format", pattern="^(json|markdown|html)$")):
4960
5038
  """Get an exportable quality audit report."""
4961
5039
  if not _read_limiter.check("quality-report"):
4962
5040
  raise HTTPException(status_code=429, detail="Rate limit exceeded")
@@ -5034,6 +5112,10 @@ def start_migration(request_body: dict):
5034
5112
  options = request_body.get("options", {})
5035
5113
  if not codebase_path or not target:
5036
5114
  raise HTTPException(status_code=400, detail="codebase_path and target are required")
5115
+ if not isinstance(codebase_path, str) or not isinstance(target, str):
5116
+ raise HTTPException(status_code=400, detail="codebase_path and target must be strings")
5117
+ if len(target) > 255:
5118
+ raise HTTPException(status_code=400, detail="target must be 255 characters or fewer")
5037
5119
  # Check raw input for traversal BEFORE resolving
5038
5120
  if '..' in codebase_path:
5039
5121
  raise HTTPException(status_code=400, detail="Path traversal not allowed")
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v6.71.1
5
+ **Version:** v6.72.0
6
6
 
7
7
  ---
8
8
 
@@ -476,7 +476,7 @@ loki-mode/
476
476
  │ └── INSTALLATION.md # This file
477
477
  ├── CHANGELOG.md # Version history
478
478
  ├── VERSION # Current version number
479
- ├── LICENSE # MIT License
479
+ ├── LICENSE # Business Source License 1.1
480
480
  ├── references/ # Agent and deployment references
481
481
  │ ├── agents.md
482
482
  │ ├── deployment.md
@@ -484,11 +484,11 @@ loki-mode/
484
484
  ├── autonomy/ # Autonomous runner (CLI only)
485
485
  │ ├── run.sh
486
486
  │ └── README.md
487
- ├── examples/ # Sample PRDs for testing
487
+ ├── templates/ # 22 PRD templates for project scaffolding
488
488
  │ ├── simple-todo-app.md
489
489
  │ ├── api-only.md
490
490
  │ ├── static-landing-page.md
491
- │ └── full-stack-demo.md
491
+ │ └── ... (22 templates total)
492
492
  ├── tests/ # Test suite (CLI only)
493
493
  │ ├── run-all-tests.sh
494
494
  │ ├── test-bootstrap.sh
@@ -497,7 +497,7 @@ loki-mode/
497
497
  └── vibe-kanban.md
498
498
  ```
499
499
 
500
- **Note:** Some files/directories (autonomy, tests, examples) are only available with full installation (Options A or B).
500
+ **Note:** Some files/directories (autonomy, tests, templates) are only available with full installation (Options A or B).
501
501
 
502
502
  ---
503
503
 
@@ -795,16 +795,25 @@ rm -rf ~/.claude/skills/loki-mode
795
795
 
796
796
  After installation:
797
797
 
798
- 1. **Quick Test:** Run a simple example
798
+ 1. **Verify Setup:** Check your environment is ready
799
799
  ```bash
800
- ./autonomy/run.sh examples/simple-todo-app.md
800
+ loki doctor
801
801
  ```
802
802
 
803
- 2. **Read Documentation:** Check out [README.md](README.md) for usage guides
803
+ 2. **Scaffold a Project:** Create a project from a template
804
+ ```bash
805
+ loki init my-app --template simple-todo-app
806
+ cd my-app
807
+ ```
808
+
809
+ 3. **Start Building:** Launch autonomous development
810
+ ```bash
811
+ loki start prd.md
812
+ ```
804
813
 
805
- 3. **Create Your First PRD:** See the Quick Start section in README
814
+ 4. **Read Documentation:** Check out [README.md](../README.md) for usage guides
806
815
 
807
- 4. **Join the Community:** Report issues or contribute at [GitHub](https://github.com/asklokesh/loki-mode)
816
+ 5. **Join the Community:** Report issues or contribute at [GitHub](https://github.com/asklokesh/loki-mode)
808
817
 
809
818
  ---
810
819
 
@@ -812,7 +821,7 @@ After installation:
812
821
 
813
822
  - **Issues/Bugs:** [GitHub Issues](https://github.com/asklokesh/loki-mode/issues)
814
823
  - **Discussions:** [GitHub Discussions](https://github.com/asklokesh/loki-mode/discussions)
815
- - **Documentation:** [README.md](README.md)
824
+ - **Documentation:** [README.md](../README.md)
816
825
 
817
826
  ---
818
827
 
@@ -0,0 +1,101 @@
1
+ # Agent 01 - CLI Functional Testing Bug Fixes
2
+
3
+ File modified: `autonomy/loki`
4
+ Total changes: 166 insertions, 39 deletions
5
+
6
+ ## Known Bugs Fixed
7
+
8
+ ### BUG-CLI-003 | PID recycling guard for `cmd_web_stop`
9
+ - **Location**: `cmd_web_stop()` PID file kill block
10
+ - **Fix**: Before killing a PID from the PID file, verify the process command matches `python`, `uvicorn`, or `Purple` via `ps -p PID -o comm=`. If the PID has been recycled by the OS to a non-Purple-Lab process, skip the kill and warn the user.
11
+
12
+ ### BUG-CLI-012 | Shell injection via unquoted Python file paths
13
+ - **Location**: Multiple `python3 -c` calls throughout the CLI
14
+ - **Fix**: Replaced all `open('$variable')` patterns in `python3 -c` calls with environment-variable-based parameter passing (`_VAR="$value" python3 -c "import os; open(os.environ['_VAR'])"`). Fixed 14 instances across:
15
+ - `cmd_status` context window (lines ~1741-1742)
16
+ - Healing report (friction map, failure modes)
17
+ - Trigger schedule count
18
+ - Failover chain providers
19
+ - Vector index stats (numpy)
20
+ - Context show (5 token metrics)
21
+ - Budget cost display (2 metrics)
22
+ - OTEL config endpoint
23
+ - Project description JSON output
24
+
25
+ ### BUG-CLI-005 | `cmd_web_status` wrong port and log paths
26
+ - **Location**: `cmd_web_status()`
27
+ - **Fix**: Port lookup now checks `$PURPLE_LAB_STATE_DIR/port` (home-based) first before CWD-based fallback. Log path resolves against home-based state dir first, falling back to CWD-based path.
28
+
29
+ ### BUG-CLI-006 | `loki logs` truncates to 50 lines silently
30
+ - **Location**: `cmd_logs()`
31
+ - **Fix**: Added full argument parsing with `--tail/-n`, `--follow/-f`, `--all/-a`, and `--help`. The 50-line default is now documented in a hint line shown with output. Added `--follow` for live streaming.
32
+
33
+ ### BUG-CLI-007 | `loki init` skips directory creation on failure
34
+ - **Location**: `cmd_init()` project directory creation
35
+ - **Fix**: Added `if ! mkdir -p` guards for both the project directory and `.loki` directory creation. Script now exits with an error message if directory creation fails.
36
+
37
+ ### BUG-CLI-008 | `loki export` overwrites without confirmation
38
+ - **Location**: `_export_json()`, `_export_markdown()`, `_export_csv()`, `_export_timeline()`
39
+ - **Fix**: Added `_export_check_overwrite()` helper that checks if the output file exists and prompts `Overwrite? [y/N]` before writing. Called in all four export format functions.
40
+
41
+ ### BUG-CLI-009 | `loki share` generates non-unique IDs
42
+ - **Location**: `cmd_share()` gist description
43
+ - **Fix**: Changed gist description timestamp from `%Y-%m-%d` (day granularity) to `%Y-%m-%dT%H:%M:%S` (second granularity) to avoid collisions when sharing multiple times per day.
44
+
45
+ ### BUG-CLI-010 | `loki config set` accepts any key without validation
46
+ - **Location**: `cmd_config_set()` wildcard case
47
+ - **Fix**: Changed from a warning that stores the value anyway to a hard error that lists valid keys and returns 1. Unknown keys are now rejected.
48
+
49
+ ### BUG-CLI-011 | `config_get` error handling
50
+ - **Location**: `cmd_config_get()`
51
+ - **Fix**: Wrapped the Python heredoc in a subshell capture with `|| { error; return 1; }` pattern to gracefully handle python3 failures without leaking state. Removed unnecessary `unset` of inline env vars.
52
+
53
+ ### BUG-CLI-008 (Medium) | `LOKI_DIR` not exported for Python heredocs
54
+ - **Location**: Line 129
55
+ - **Fix**: Added `export LOKI_DIR` immediately after assignment. This ensures all Python heredocs using `os.environ.get("LOKI_DIR", ".loki")` receive the correct value.
56
+
57
+ ### BUG-PAR-002 | `worktree list` branch pattern never matches
58
+ - **Location**: `cmd_worktree()` list subcommand
59
+ - **Fix**: Branch matching pattern changed from `loki-parallel-*` to match both `parallel-*` and `loki-parallel-*`. The actual branches created by `run.sh` use `parallel-<stream>` prefix (line 2130 of run.sh), not `loki-parallel-`.
60
+
61
+ ### BUG-PAR-010 | `worktree clean` doesn't kill running sessions
62
+ - **Location**: `cmd_worktree()` clean subcommand
63
+ - **Fix**: Before removing a worktree via `git worktree remove`, check for `.loki/loki.pid` in the worktree directory. If a session is running, send SIGTERM, wait 1 second, then SIGKILL if needed.
64
+
65
+ ## New Bugs Found and Fixed
66
+
67
+ ### NEW-CLI-001 | Division by zero in context percentage calculation
68
+ - **Location**: `cmd_status()` and `_context_show()`
69
+ - **Fix**: Added `if [ "$ctx_total" -gt 0 ]` / `if [ "$total_tokens" -gt 0 ]` guards before the `$((used * 100 / total))` arithmetic. If total is zero, percentage defaults to 0.
70
+
71
+ ### NEW-CLI-002 | Missing `loki web logs` subcommand
72
+ - **Location**: `cmd_web()` case dispatch
73
+ - **Fix**: Added `logs)` case to `cmd_web()` that displays Purple Lab server logs using `tail -n`. Defaults to 100 lines. Also checks home-based state dir and CWD-based fallback for log file location. Updated `cmd_web_help()` to list the `logs` subcommand.
74
+
75
+ ## Bugs Investigated But Not Fixed (Already Correct)
76
+
77
+ ### BUG-CLI-001 / BUG-CLI-002 | `--port` / `--prd` unbound variable
78
+ - **Status**: Already fixed in current code. `cmd_web_start()` initializes `port="${PURPLE_LAB_DEFAULT_PORT}"` and `prd_file=""` before the argument parsing loop. The `--port` and `--prd` handlers properly check `${2:-}` before accessing `$2`.
79
+
80
+ ### BUG-CLI-004 | `--no-open` flag ignored
81
+ - **Status**: Already working correctly. The `open_browser` variable is set to `false` by `--no-open` and checked with `if [ "$open_browser" = true ]` before any browser-open calls.
82
+
83
+ ### BUG-CLI-009 (Medium) | Empty array with `set -u` in `list_running_sessions`
84
+ - **Status**: Already fixed. Uses `${sessions[@]+"${sessions[@]}"}` pattern throughout, which is the correct `set -u`-safe array expansion.
85
+
86
+ ### BUG-CMD-001 | `cmd_web` wildcard duplicates arguments
87
+ - **Status**: Analyzed and found to be working correctly. The `*)` case properly shifts the subcommand-as-flag, then passes it along with remaining args to `cmd_web_start`.
88
+
89
+ ## Bugs Found But Not Fixed (Out of Scope or Extensive)
90
+
91
+ ### Missing `$2` guards in `shift 2` patterns
92
+ - **Location**: Multiple argument parsers (e.g., `cmd_state query`, trigger config parsing)
93
+ - **Issue**: Under `set -u`, if a flag like `--agent` is the last argument without a value, `$2` is unset, causing a crash. The pattern `shift 2 ;;` should be guarded with `${2:-}` checks.
94
+ - **Impact**: Low -- only triggers on malformed user input.
95
+ - **Scope**: 20+ occurrences across the file; would require systematic refactoring.
96
+
97
+ ### Remaining shell-expanded heredocs with `$variable` in `open()` calls
98
+ - **Location**: Lines 3745, 8002, 9332, 10228, 10262, 12604, 16844, etc.
99
+ - **Issue**: Double-quoted heredocs (`<< HEREDOC`) interpolate shell variables into Python `open()` calls. While the variables are internally-constructed paths (not user input), specially-crafted directory names with quotes could theoretically cause injection.
100
+ - **Impact**: Very low -- would require the user to be working in a directory with Python metacharacters in its path.
101
+ - **Scope**: 15+ occurrences; fixing all requires converting to single-quoted heredocs with env var passing.
@@ -0,0 +1,88 @@
1
+ # Agent 02: Purple Lab Functional Testing - Bug Fix Report
2
+
3
+ ## Summary
4
+
5
+ Audited `web-app/server.py` (5,679 lines) and all frontend components in `web-app/src/` (50 files).
6
+ Fixed 8 bugs (3 security, 2 resource leaks, 2 race conditions, 1 missing validation).
7
+ Verified all 13 bugs from BUG-AUDIT-v6.61.0 against the current codebase.
8
+
9
+ ## Bugs Fixed
10
+
11
+ ### FIX-1: `_save_secrets()` crashes when crypto module is missing (BUG-PL-013 related)
12
+ - **File**: `web-app/server.py:2431`
13
+ - **Severity**: Medium
14
+ - **Problem**: `_save_secrets()` calls `from crypto import encrypt_value, encryption_available` without try/except ImportError. When the crypto module is not installed, saving any secret (POST /api/secrets) crashes with an unhandled ImportError. The counterpart `_load_secrets()` already handles this correctly.
15
+ - **Fix**: Wrapped the crypto import in try/except ImportError with plaintext fallback, matching the pattern in `_load_secrets()`.
16
+
17
+ ### FIX-2: `fix_session` orphans child processes (BUG-PL-007 related)
18
+ - **File**: `web-app/server.py:4203`
19
+ - **Severity**: High
20
+ - **Problem**: `run_fix()` in the `/api/sessions/{id}/fix` endpoint spawns a subprocess but never calls `_track_child_pid()` or `_untrack_child_pid()`. When `loki web stop` runs, these fix processes are not found in the tracking list and remain as orphans, consuming resources. The chat endpoint (`run_chat()`) correctly tracks and untracks -- this was a missed pattern.
21
+ - **Fix**: Added `_track_child_pid(proc.pid)` after process creation and `_untrack_child_pid(proc.pid)` in the finally block.
22
+
23
+ ### FIX-3: Session history leaks full filesystem paths (BUG-PL-003 from task description)
24
+ - **File**: `web-app/server.py:3411`
25
+ - **Severity**: Medium (security/information disclosure)
26
+ - **Problem**: `get_sessions_history()` returned `"path": str(entry)` which exposes full paths like `/Users/lokesh/purple-lab-projects/project-123`. In deployed scenarios this leaks the server's filesystem structure, username, and directory layout.
27
+ - **Fix**: Replaced with sanitized relative paths using `~/` prefix (e.g., `~/purple-lab-projects/project-123`). This is safe because the session history path is display-only on the frontend.
28
+
29
+ ### FIX-4: `delete_session` leaks filesystem path in response
30
+ - **File**: `web-app/server.py:3495`
31
+ - **Severity**: Low (security/information disclosure)
32
+ - **Problem**: `delete_session()` returned `"path": str(target)` in its response, exposing the full filesystem path to the client.
33
+ - **Fix**: Changed to return `"session_id": session_id` instead.
34
+
35
+ ### FIX-5: `start_session` accepts arbitrary projectDir without validation
36
+ - **File**: `web-app/server.py:2510`
37
+ - **Severity**: High (security)
38
+ - **Problem**: The `projectDir` field from StartRequest was used directly without any path validation. A malicious client could pass `/etc`, `/root`, or any system path, and the server would create directories and write PRD files there. The `onboard_session` endpoint already validates paths correctly.
39
+ - **Fix**: Added validation ensuring user-supplied `projectDir` resolves to within the home directory, matching the pattern in `onboard_session`.
40
+
41
+ ### FIX-6: `create_session_file` missing content size limit (BUG-PL-005 from task description)
42
+ - **File**: `web-app/server.py:3691`
43
+ - **Severity**: Medium
44
+ - **Problem**: `create_session_file()` (POST) accepts file content without any size validation. The sibling `save_session_file()` (PUT) correctly enforces a 1MB limit. A client could create arbitrarily large files via the POST endpoint.
45
+ - **Fix**: Added `len(req.content.encode(...)) > 1_048_576` check matching the PUT endpoint.
46
+
47
+ ### FIX-7: `pause_session` and `resume_session` mutate state without lock
48
+ - **File**: `web-app/server.py:2972, 2989`
49
+ - **Severity**: Medium (race condition)
50
+ - **Problem**: Both `pause_session()` and `resume_session()` read `session.running`, `session.process`, and write `session.paused` without holding `session._lock`. Concurrent pause/resume and stop operations could produce torn state. The `stop_session` and `start_session` endpoints correctly use `async with session._lock`.
51
+ - **Fix**: Wrapped both endpoints' state access in `async with session._lock`.
52
+
53
+ ### FIX-8: `delete_secret` endpoint missing key format validation
54
+ - **File**: `web-app/server.py:4340`
55
+ - **Severity**: Low (defense in depth)
56
+ - **Problem**: The `delete_secret()` endpoint accepts arbitrary strings as the `key` parameter without validation. While not exploitable (used only as dict key lookup), the `set_secret()` endpoint validates key format -- the inconsistency could mask bugs.
57
+ - **Fix**: Added `re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', key)` validation matching `set_secret()`.
58
+
59
+ ## Bugs from Audit Already Fixed in Current Codebase
60
+
61
+ The following bugs from BUG-AUDIT-v6.61.0 were already fixed before this audit:
62
+
63
+ | Bug ID | Description | Status |
64
+ |--------|-------------|--------|
65
+ | BUG-PL-001 | Dead code after `stop_session` return | Fixed: no early return, full cleanup runs |
66
+ | BUG-PL-002 | `session.reset()` never called on stop | Fixed: `session.reset()` called at line 2742 |
67
+ | BUG-PL-003 (audit) | Reader task sets `running=False` without lock | Fixed: uses `async with session._lock` at line 2361 |
68
+ | BUG-PL-004 | Chat/fix missing secrets injection | Fixed: both endpoints call `_load_secrets()` |
69
+ | BUG-PL-005 (audit) | Pause state never tracked | Fixed: `session.paused` set in both pause/resume |
70
+ | BUG-PL-006 | `delete_session` can delete active session | Fixed: explicit active session check at line 3458 |
71
+ | BUG-PL-009 | Project dir name collision (second-granularity) | Fixed: uses milliseconds (`time.time() * 1000`) |
72
+ | BUG-PL-010 | `get_status` mutates `running` without lock | Fixed: uses local `is_running` variable, not session state |
73
+ | BUG-PL-011 | Session history breaks after first non-empty dir | Fixed: no early `break` in history loop |
74
+ | BUG-PL-012 | `cancel_chat` unhandled `TimeoutExpired` | Fixed: caught at line 4133 |
75
+
76
+ ## New Bugs Discovered (Beyond Audit)
77
+
78
+ All bugs listed in the "Bugs Fixed" section above (FIX-1 through FIX-8) are new discoveries not covered by the original BUG-AUDIT-v6.61.0 for the Purple Lab category. FIX-1, FIX-2, FIX-5, FIX-6, FIX-7, and FIX-8 are entirely new findings.
79
+
80
+ ## Verification
81
+
82
+ - Python syntax: `ast.parse()` passes on modified server.py
83
+ - Frontend build: `npm run build` succeeds with no new errors
84
+ - All changes are backward-compatible (no API contract changes except delete_session response field rename from `path` to `session_id`)
85
+
86
+ ## Files Modified
87
+
88
+ - `web-app/server.py` -- 8 bug fixes (50 insertions, 26 deletions)
@@ -0,0 +1,119 @@
1
+ # Agent 03: Dashboard API Functional Testing - Bug Fix Report
2
+
3
+ ## Scope
4
+ File: `dashboard/server.py` (~5,259 lines, FastAPI)
5
+ Focus: All API routes, WebSocket handlers, task board features, security audit
6
+
7
+ ## Bug Audit Status from BUG-AUDIT-v6.61.0.md
8
+
9
+ ### Already Fixed (verified in current codebase)
10
+
11
+ | Bug ID | Description | Status |
12
+ |--------|-------------|--------|
13
+ | BUG-DASH-001 | Token creation endpoint has no authentication | FIXED - `require_scope("admin")` dependency at line 1719 |
14
+ | BUG-DASH-002 | WebSocket rate-limit calls close() on unaccepted connection | FIXED - `accept()` then `close()` at lines 1396-1397 |
15
+ | BUG-DASH-003 | WebSocket connection limit rejection enters receive loop | FIXED - `connect()` returns False, caller returns at line 1421-1422 |
16
+ | BUG-DASH-004 | `create_project` doesn't validate tenant_id | FIXED - Tenant existence check + `Field(..., gt=0)` validation |
17
+ | BUG-DASH-005 | `update_task` doesn't clear completed_at on reopen | FIXED - `completed_at = None` in else branch at line 1254 |
18
+ | BUG-DASH-006 | Task state machine missing DONE transitions | FIXED - `DONE: {IN_PROGRESS, REVIEW}` at line 1316 |
19
+ | BUG-DASH-009 | ProjectUpdate.status allows arbitrary strings | FIXED - `Literal["active", "archived", "completed", "paused"]` at line 179 |
20
+ | BUG-DASH-010 | Audit log offset allows negative values | FIXED - `ge=0` constraint at line 1824 |
21
+ | BUG-DASH-011 | Learning signals offset allows negative values | FIXED - `ge=0` constraint at line 2443 |
22
+ | BUG-DASH-012 | WebSocket idle timeout doesn't call disconnect | FIXED - `finally: manager.disconnect(websocket)` at line 1469 |
23
+ | BUG-DASH-013 | GET /api/tasks ignores project_id parameter | FIXED - Filter applied at lines 1132-1134 |
24
+
25
+ ### Not Applicable (code removed)
26
+
27
+ | Bug ID | Description | Status |
28
+ |--------|-------------|--------|
29
+ | BUG-PL-001 | Dead code after stop_session return | N/A - Purple Lab code removed from server.py |
30
+ | BUG-PL-002 | session.reset() never called on stop | N/A - Purple Lab code removed from server.py |
31
+ | BUG-PL-003 | Reader task sets running=False without lock | N/A - Purple Lab code removed |
32
+ | BUG-PL-004 | Chat/fix/auto-fix missing secrets injection | N/A - Purple Lab code removed |
33
+ | BUG-PL-005 | Pause state never tracked | N/A - Purple Lab code removed |
34
+ | BUG-PL-006 | delete_session can delete active directory | N/A - Purple Lab code removed |
35
+ | BUG-PL-007 | Chat PIDs tracked but never untracked | N/A - Purple Lab code removed |
36
+ | BUG-DS-002 | Auto-fix restart double-wraps command | N/A - Dev server code removed |
37
+ | BUG-DS-003 | Overly broad port regex | N/A - Dev server code removed |
38
+ | BUG-DS-004 | pip install into server's own environment | N/A - Dev server code removed |
39
+ | BUG-DS-005 | Docker Compose port parsing crash | N/A - Dev server code removed |
40
+ | BUG-DASH-007 | pause_session polling loop is dead code | N/A - pause_session rewritten (no polling loop) |
41
+
42
+ ## Fixes Applied in This Session
43
+
44
+ ### 1. BUG-DASH-008: `_safe_read_text` not truly safe (lines 95-101)
45
+
46
+ **Problem**: `_safe_read_text` used `Path.read_text()` directly without exception handling. While `Path.read_text()` properly manages file handles (no leak), the function could raise `OSError` on permission errors or missing files. The "safe" name was misleading -- callers expected it to never raise.
47
+
48
+ **Fix**: Wrapped in try/except to return empty string on any I/O error, making the function truly safe as its name implies.
49
+
50
+ ### 2. SECURITY: `/api/focus` POST/DELETE missing authentication (lines 1637, 1672)
51
+
52
+ **Problem**: The `/api/focus` POST and DELETE endpoints had no auth dependency. Any network-reachable client could redirect the dashboard to read from an arbitrary project directory, potentially exposing data from other projects or causing the dashboard to process attacker-controlled state files.
53
+
54
+ **Fix**:
55
+ - Added `dependencies=[Depends(auth.require_scope("control"))]` to both POST and DELETE
56
+ - Added validation requiring the target directory to contain a `.loki/` subdirectory to prevent pointing the dashboard at arbitrary filesystem locations
57
+ - Changed to resolve path before checking, preventing TOCTOU issues
58
+
59
+ ### 3. SECURITY: `/api/enterprise/tokens` GET missing authentication (line 1766)
60
+
61
+ **Problem**: The token listing endpoint had no auth dependency. When enterprise mode was enabled, any client could enumerate all API tokens (names, scopes, creation dates, expiration). While raw token values are not returned, the metadata exposure is still a security risk for token enumeration and scope discovery.
62
+
63
+ **Fix**: Added `dependencies=[Depends(auth.require_scope("admin"))]` to require admin authentication.
64
+
65
+ ### 4. Deprecated `asyncio.get_event_loop()` in sync_registry (line 1600)
66
+
67
+ **Problem**: Used `asyncio.get_event_loop()` which is deprecated in Python 3.10+ for getting the running loop from within an async function. This could cause `DeprecationWarning` or incorrect behavior in future Python versions.
68
+
69
+ **Fix**: Changed to `asyncio.get_running_loop()` which is the correct API for async contexts.
70
+
71
+ ### 5. Input validation: timeRange parameters on learning endpoints (lines 2361, 2430, 2450)
72
+
73
+ **Problem**: The `timeRange` parameters on `/api/learning/metrics`, `/api/learning/trends`, and `/api/learning/signals` accepted arbitrary strings with no validation. While `_parse_time_range()` handles invalid input gracefully (returns None), passing malformed strings wastes processing and could be used for fuzzing.
74
+
75
+ **Fix**: Added `Query(..., pattern=r"^\d{1,4}[hdm]$")` constraint to validate format (1-4 digit number followed by h/d/m).
76
+
77
+ ### 6. Input validation: quality report format parameter (line 4970)
78
+
79
+ **Problem**: The `format` parameter on `/api/quality-report` accepted any string, passing it to `rigour.export_report()`. Arbitrary format strings could cause unexpected behavior in the export function.
80
+
81
+ **Fix**: Added `pattern="^(json|markdown|html)$"` constraint to limit to known formats.
82
+
83
+ ### 7. Input validation: migration start target and codebase_path (line 5048)
84
+
85
+ **Problem**: The `start_migration` endpoint accepted `codebase_path` and `target` without type checking. Non-string values (lists, dicts) from JSON body would cause cryptic errors downstream.
86
+
87
+ **Fix**: Added type validation (`isinstance` check for str) and length limit (255 chars) for `target`.
88
+
89
+ ## Security Audit Summary (OWASP Top 10)
90
+
91
+ | OWASP Category | Status | Notes |
92
+ |----------------|--------|-------|
93
+ | A01: Broken Access Control | FIXED | Added auth to /api/focus, /api/enterprise/tokens |
94
+ | A02: Cryptographic Failures | OK | Token generation uses `secrets` module |
95
+ | A03: Injection | OK | SQLAlchemy ORM prevents SQL injection; list-form subprocess calls prevent shell injection; realpath + regex prevent path traversal |
96
+ | A04: Insecure Design | FIXED | Focus endpoint now requires .loki/ subdirectory |
97
+ | A05: Security Misconfiguration | OK | CORS restricted to localhost; default bind to 127.0.0.1; TLS optional |
98
+ | A06: Vulnerable Components | N/A | Dependency audit out of scope |
99
+ | A07: Auth Failures | OK | Rate limiting on sensitive endpoints; token management requires admin |
100
+ | A08: Data Integrity | OK | Atomic file writes for state mutations (tmp + rename) |
101
+ | A09: Logging Failures | OK | Audit logging on destructive operations (delete, stop, kill) |
102
+ | A10: SSRF | MITIGATED | Focus endpoint restricted to dirs with .loki/ subdirectory |
103
+
104
+ ## Route Coverage Audit
105
+
106
+ Verified all 100+ routes in server.py for:
107
+ - Authentication requirements (auth scope dependencies)
108
+ - Input validation (Pydantic models, Query constraints, regex patterns)
109
+ - Error handling (try/except with proper HTTP status codes)
110
+ - Rate limiting (control and read limiters)
111
+ - Path traversal protection (realpath checks, SAFE_ID_RE regex)
112
+ - Resource cleanup (WebSocket disconnect in finally block)
113
+
114
+ ## Verification
115
+
116
+ ```
117
+ python3 -c "import ast; ast.parse(open('dashboard/server.py').read()); print('Syntax OK')"
118
+ # Output: Syntax OK
119
+ ```