ltcai 0.1.8 → 0.1.11

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 (39) hide show
  1. package/README.md +141 -289
  2. package/docs/CHANGELOG.md +227 -0
  3. package/docs/architecture.md +121 -0
  4. package/docs/mcp-tools.md +116 -0
  5. package/docs/privacy.md +74 -0
  6. package/docs/public-deploy.md +137 -0
  7. package/docs/security-model.md +121 -0
  8. package/knowledge_graph.py +18 -5
  9. package/ltcai_cli.py +2 -2
  10. package/package.json +1 -1
  11. package/server.py +1140 -280
  12. package/skills/SKILL_TEMPLATE.md +61 -29
  13. package/skills/code_review/SKILL.md +28 -0
  14. package/skills/code_review/examples.md +59 -0
  15. package/skills/code_review/risk.json +9 -0
  16. package/skills/code_review/schema.json +65 -0
  17. package/skills/data_analysis/SKILL.md +28 -0
  18. package/skills/data_analysis/examples.md +62 -0
  19. package/skills/data_analysis/risk.json +9 -0
  20. package/skills/data_analysis/schema.json +61 -0
  21. package/skills/file_edit/SKILL.md +33 -0
  22. package/skills/file_edit/examples.md +45 -0
  23. package/skills/file_edit/risk.json +9 -0
  24. package/skills/file_edit/schema.json +60 -0
  25. package/skills/summarize_document/SKILL.md +68 -0
  26. package/skills/summarize_document/examples.md +65 -0
  27. package/skills/summarize_document/risk.json +9 -0
  28. package/skills/summarize_document/schema.json +71 -0
  29. package/skills/web_search/SKILL.md +28 -0
  30. package/skills/web_search/examples.md +61 -0
  31. package/skills/web_search/risk.json +9 -0
  32. package/skills/web_search/schema.json +62 -0
  33. package/tests/integration/__pycache__/__init__.cpython-314.pyc +0 -0
  34. package/tests/integration/__pycache__/test_api.cpython-314-pytest-9.0.3.pyc +0 -0
  35. package/tests/unit/__pycache__/test_security.cpython-314-pytest-9.0.3.pyc +0 -0
  36. package/tests/unit/__pycache__/test_tools.cpython-314-pytest-9.0.3.pyc +0 -0
  37. package/tests/unit/test_security.py +125 -0
  38. package/tests/unit/test_tools.py +194 -1
  39. package/tools.py +264 -4
@@ -0,0 +1,121 @@
1
+ # Lattice AI — 보안 모델
2
+
3
+ ## 설계 원칙
4
+
5
+ Lattice AI는 **개인 AI 워크스페이스**로 설계되었습니다. 기본값은 최대한 안전하게, 네트워크 노출은 명시적 opt-in으로만 허용합니다.
6
+
7
+ ## 네트워크 바인딩
8
+
9
+ | 설정 | 바인딩 | 용도 |
10
+ |------|--------|------|
11
+ | 기본 | `127.0.0.1:4825` | 로컬 전용, 외부 접근 불가 |
12
+ | `LATTICEAI_HOST=0.0.0.0` | `0.0.0.0:4825` | 같은 Wi-Fi 기기 접근 허용 |
13
+ | 퍼블릭 배포 | nginx/Caddy 뒤에 두기 | HTTPS 종단 + 리버스 프록시 |
14
+
15
+ ## 인증
16
+
17
+ ### 비밀번호
18
+
19
+ - scrypt 해싱 (`hashlib.scrypt`, N=2^14, r=8, p=1)
20
+ - `users.json`에 `{"hash": "<scrypt hex>"}` 형식 저장
21
+ - 평문 비밀번호는 메모리에도 저장되지 않음
22
+
23
+ ### 세션
24
+
25
+ - UUID 토큰, `~/.ltcai/sessions.json` 파일 저장
26
+ - TTL: 24시간 + sliding refresh (활동 시 자동 연장, 15분 단위 디스크 쓰기)
27
+ - 쿠키: `HttpOnly; SameSite=Lax; Path=/`
28
+ - 서버 재시작 후에도 유지 (파일 기반)
29
+
30
+ ### SSO (선택적)
31
+
32
+ - Entra ID / Okta OIDC (`OIDC_DISCOVERY_URL`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`)
33
+ - 콜백 후 내부 세션 토큰으로 변환
34
+ - 어드민 핸드오프: `sessionStorage` 1회 읽기 (URL 파라미터 노출 방지)
35
+
36
+ ## API 키 보안
37
+
38
+ - OS keyring (macOS Keychain, Windows Credential Manager, Linux Secret Service) 저장
39
+ - 평문 디스크 저장은 `LATTICEAI_ALLOW_PLAINTEXT_API_KEYS=true` 명시 시에만
40
+ - 채팅 히스토리 저장 전 API key/token/password 패턴 자동 마스킹
41
+
42
+ ## CORS
43
+
44
+ ```python
45
+ CORS_ALLOWED_ORIGINS = ["http://localhost:4825", "http://127.0.0.1:4825"]
46
+ ```
47
+
48
+ - 기본: localhost만 허용
49
+ - `LATTICEAI_CORS_ALLOW_NETWORK=true`: 같은 Wi-Fi 기기 허용
50
+ - 퍼블릭 배포: 리버스 프록시 도메인만 허용 권장
51
+
52
+ ## Rate Limiting
53
+
54
+ 토큰 버킷 알고리즘, per-user:
55
+
56
+ | 엔드포인트 | burst | 지속 |
57
+ |-----------|-------|------|
58
+ | `/chat` | 30 | 30/분 |
59
+ | `/agent` | 10 | 6/분 |
60
+ | `/upload` | 20 | 12/분 |
61
+
62
+ `LATTICEAI_RATE_LIMIT=0`으로 비활성화 (개발 환경용).
63
+
64
+ ## 파일 업로드
65
+
66
+ ```python
67
+ MAGIC_NUMBERS = {
68
+ ".pdf": b"%PDF",
69
+ ".docx": b"PK\x03\x04",
70
+ ".xlsx": b"PK\x03\x04",
71
+ ".pptx": b"PK\x03\x04",
72
+ ".png": b"\x89PNG",
73
+ ".jpg": b"\xff\xd8\xff",
74
+ ".zip": b"PK\x03\x04",
75
+ }
76
+ ```
77
+
78
+ - 업로드 시 파일 첫 바이트와 확장자 매핑 검증
79
+ - 불일치 시 400 에러
80
+
81
+ ## 에이전트 도구 샌드박스
82
+
83
+ ### `run_command()` 위험 플래그 차단
84
+
85
+ 다음 패턴이 포함된 명령 실행 거부:
86
+ - `rm -rf`, `sudo`, `chmod 777`, `curl | bash`, `wget | sh`
87
+ - `> /dev/sda`, `dd if=`, `mkfs`
88
+
89
+ ### `edit_file()` 유일성 검증
90
+
91
+ - `old_string`이 파일에 정확히 한 번만 존재해야 성공
92
+ - `replace_all=true`로 전체 치환 허용
93
+ - 워크스페이스 외부 경로 접근 차단 (`../../../etc/passwd` 등)
94
+
95
+ ### `grep()` 이진 디렉토리 제외
96
+
97
+ `node_modules`, `.git`, `venv`, `dist`, `__pycache__` 자동 제외
98
+
99
+ ## 감사 로그
100
+
101
+ - 어드민 세션 핸드오프 이벤트 로깅
102
+ - 평문 비밀번호 마이그레이션 이벤트: `password_migrated_from_plaintext`
103
+ - `server.log` 파일에 모든 요청 기록
104
+
105
+ ## 텔레메트리
106
+
107
+ **없음.** 모든 데이터는 로컬에만 저장됩니다. 외부 서버로 어떠한 사용 데이터도 전송되지 않습니다.
108
+
109
+ 예외: 사용자가 직접 설정한 클라우드 API(OpenAI, Groq 등)로의 프롬프트 전송은 해당 제공업체의 정책을 따릅니다.
110
+
111
+ ## 퍼블릭 배포 체크리스트
112
+
113
+ - [ ] `LATTICEAI_MODE=public`
114
+ - [ ] `LATTICEAI_INVITE_CODE` 비공개 값 설정
115
+ - [ ] HTTPS 리버스 프록시 (nginx/Caddy)
116
+ - [ ] `LATTICEAI_ENABLE_GRAPH=false` (필요 시)
117
+ - [ ] `/data` 영구 볼륨 마운트
118
+ - [ ] `LATTICEAI_ALLOW_LOCAL_MODELS=false`
119
+ - [ ] 방화벽에서 4825 포트 직접 노출 차단 (리버스 프록시 통해서만)
120
+
121
+ 자세한 내용: [public-deploy.md](public-deploy.md)
@@ -8,6 +8,7 @@ the ingestion contract.
8
8
 
9
9
  import hashlib
10
10
  import json
11
+ import logging
11
12
  import re
12
13
  import shutil
13
14
  import sqlite3
@@ -28,6 +29,18 @@ def _json(data: Optional[Dict[str, Any]]) -> str:
28
29
  return json.dumps(data or {}, ensure_ascii=False, sort_keys=True)
29
30
 
30
31
 
32
+ def _safe_loads(raw: Optional[str]) -> Dict[str, Any]:
33
+ """Tolerantly parse a metadata_json column — returns {} on corrupt rows."""
34
+ if not raw:
35
+ return {}
36
+ try:
37
+ value = json.loads(raw)
38
+ return value if isinstance(value, dict) else {}
39
+ except (json.JSONDecodeError, TypeError) as e:
40
+ logging.warning("knowledge_graph: corrupt metadata_json (%s) — using empty dict", e)
41
+ return {}
42
+
43
+
31
44
  def _slug(text: str, max_len: int = 96) -> str:
32
45
  value = re.sub(r"\s+", " ", str(text or "")).strip().lower()
33
46
  value = re.sub(r"[^0-9a-zA-Z가-힣._:@/-]+", "-", value).strip("-")
@@ -573,7 +586,7 @@ class KnowledgeGraphStore:
573
586
  "type": row["type"],
574
587
  "title": row["title"],
575
588
  "summary": row["summary"],
576
- "metadata": json.loads(row["metadata_json"] or "{}"),
589
+ "metadata": _safe_loads(row["metadata_json"]),
577
590
  }
578
591
  for row in conn.execute(
579
592
  "SELECT id, type, title, summary, metadata_json FROM nodes WHERE type != 'Chunk' ORDER BY updated_at DESC LIMIT ?",
@@ -588,7 +601,7 @@ class KnowledgeGraphStore:
588
601
  "to": row["to_node"],
589
602
  "type": row["type"],
590
603
  "weight": row["weight"],
591
- "metadata": json.loads(row["metadata_json"] or "{}"),
604
+ "metadata": _safe_loads(row["metadata_json"]),
592
605
  }
593
606
  for row in conn.execute(
594
607
  "SELECT id, from_node, to_node, type, weight, metadata_json FROM edges ORDER BY created_at DESC LIMIT ?",
@@ -655,7 +668,7 @@ class KnowledgeGraphStore:
655
668
  "type": row["type"],
656
669
  "title": row["title"],
657
670
  "summary": row["summary"],
658
- "metadata": json.loads(row["metadata_json"] or "{}"),
671
+ "metadata": _safe_loads(row["metadata_json"]),
659
672
  }
660
673
  for row in rows
661
674
  ],
@@ -694,7 +707,7 @@ class KnowledgeGraphStore:
694
707
  "type": row["type"],
695
708
  "title": row["title"],
696
709
  "summary": row["summary"],
697
- "metadata": json.loads(row["metadata_json"] or "{}"),
710
+ "metadata": _safe_loads(row["metadata_json"]),
698
711
  })
699
712
  if len(matches) >= limit:
700
713
  break
@@ -729,7 +742,7 @@ class KnowledgeGraphStore:
729
742
  "type": row["type"],
730
743
  "title": row["title"],
731
744
  "summary": row["summary"],
732
- "metadata": json.loads(row["metadata_json"] or "{}"),
745
+ "metadata": _safe_loads(row["metadata_json"]),
733
746
  }
734
747
  for row in conn.execute(
735
748
  f"SELECT id, type, title, summary, metadata_json FROM nodes WHERE id IN ({placeholders})",
package/ltcai_cli.py CHANGED
@@ -89,8 +89,8 @@ def main() -> None:
89
89
  parser = argparse.ArgumentParser(prog="LTCAI", description="Run the Lattice AI local server.")
90
90
  subparsers = parser.add_subparsers(dest="command")
91
91
  subparsers.add_parser("doctor", help="Check local runtime dependencies and configuration.")
92
- # Default to 0.0.0.0 so other devices on the same network can connect
93
- parser.add_argument("--host", default=os.getenv("LATTICEAI_HOST") or "0.0.0.0")
92
+ # Default to 127.0.0.1; set LATTICEAI_HOST=0.0.0.0 to expose to the local network
93
+ parser.add_argument("--host", default=os.getenv("LATTICEAI_HOST") or "127.0.0.1")
94
94
  parser.add_argument("--port", type=int, default=int(os.getenv("LATTICEAI_PORT") or "4825"))
95
95
  parser.add_argument("--reload", action="store_true", help="Enable uvicorn reload for local development.")
96
96
  args = parser.parse_args()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "0.1.8",
3
+ "version": "0.1.11",
4
4
  "description": "Lattice AI local MLX/cloud LLM workspace server",
5
5
  "bin": {
6
6
  "ltcai": "bin/ltcai.js",