ltcai 9.0.0 → 9.2.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 (189) hide show
  1. package/README.md +88 -46
  2. package/auto_setup.py +7 -853
  3. package/desktop/electron/README.md +9 -0
  4. package/desktop/electron/main.cjs +5 -3
  5. package/docs/CHANGELOG.md +178 -2
  6. package/docs/COMMUNITY_AND_PLUGINS.md +4 -2
  7. package/docs/DEVELOPMENT.md +53 -14
  8. package/docs/LEGACY_COMPATIBILITY.md +4 -4
  9. package/docs/ONBOARDING.md +10 -2
  10. package/docs/OPERATIONS.md +8 -4
  11. package/docs/PRODUCT_DIRECTION_REVIEW.md +4 -0
  12. package/docs/TRUST_MODEL.md +5 -2
  13. package/docs/WHY_LATTICE.md +15 -10
  14. package/docs/WORKFLOW_DESIGNER.md +22 -0
  15. package/docs/kg-schema.md +13 -2
  16. package/docs/mcp-tools.md +17 -6
  17. package/docs/privacy.md +19 -3
  18. package/docs/public-deploy.md +32 -3
  19. package/docs/security-model.md +46 -11
  20. package/lattice_brain/__init__.py +1 -1
  21. package/lattice_brain/archive.py +1 -6
  22. package/lattice_brain/context.py +66 -9
  23. package/lattice_brain/graph/_kg_fsutil.py +1 -5
  24. package/lattice_brain/graph/discovery_index.py +174 -20
  25. package/lattice_brain/graph/documents.py +44 -9
  26. package/lattice_brain/graph/ingest.py +47 -20
  27. package/lattice_brain/graph/provenance.py +13 -6
  28. package/lattice_brain/graph/retrieval.py +140 -39
  29. package/lattice_brain/graph/retrieval_docgen.py +37 -4
  30. package/lattice_brain/ingestion.py +4 -7
  31. package/lattice_brain/portability.py +28 -14
  32. package/lattice_brain/runtime/agent_runtime.py +5 -9
  33. package/lattice_brain/runtime/hooks.py +30 -13
  34. package/lattice_brain/runtime/multi_agent.py +27 -8
  35. package/lattice_brain/runtime/statuses.py +10 -0
  36. package/lattice_brain/utils.py +20 -2
  37. package/lattice_brain/workflow.py +1 -5
  38. package/latticeai/__init__.py +1 -1
  39. package/latticeai/api/admin.py +1 -1
  40. package/latticeai/api/agent_registry.py +10 -8
  41. package/latticeai/api/agents.py +22 -3
  42. package/latticeai/api/auth.py +78 -16
  43. package/latticeai/api/browser.py +303 -34
  44. package/latticeai/api/chat.py +320 -850
  45. package/latticeai/api/chat_agent_http.py +356 -0
  46. package/latticeai/api/chat_contracts.py +54 -0
  47. package/latticeai/api/chat_documents.py +256 -0
  48. package/latticeai/api/chat_helpers.py +28 -1
  49. package/latticeai/api/chat_history.py +99 -0
  50. package/latticeai/api/chat_intents.py +316 -0
  51. package/latticeai/api/chat_stream.py +115 -0
  52. package/latticeai/api/computer_use.py +62 -9
  53. package/latticeai/api/health.py +9 -3
  54. package/latticeai/api/hooks.py +17 -6
  55. package/latticeai/api/knowledge_graph.py +78 -14
  56. package/latticeai/api/local_files.py +7 -2
  57. package/latticeai/api/mcp.py +102 -23
  58. package/latticeai/api/models.py +72 -33
  59. package/latticeai/api/network.py +5 -5
  60. package/latticeai/api/permissions.py +67 -26
  61. package/latticeai/api/plugins.py +21 -7
  62. package/latticeai/api/portability.py +5 -5
  63. package/latticeai/api/realtime.py +33 -5
  64. package/latticeai/api/setup.py +2 -2
  65. package/latticeai/api/static_routes.py +123 -24
  66. package/latticeai/api/tools.py +96 -14
  67. package/latticeai/api/workflow_designer.py +37 -0
  68. package/latticeai/api/workspace.py +2 -1
  69. package/latticeai/app_factory.py +110 -52
  70. package/latticeai/core/agent.py +50 -13
  71. package/latticeai/core/agent_prompts.py +5 -0
  72. package/latticeai/core/agent_registry.py +1 -5
  73. package/latticeai/core/config.py +23 -3
  74. package/latticeai/core/context_builder.py +21 -3
  75. package/latticeai/core/file_generation.py +451 -0
  76. package/latticeai/core/invitations.py +1 -4
  77. package/latticeai/core/io_utils.py +9 -1
  78. package/latticeai/core/legacy_compatibility.py +24 -3
  79. package/latticeai/core/marketplace.py +1 -1
  80. package/latticeai/core/plugins.py +15 -0
  81. package/latticeai/core/policy.py +1 -1
  82. package/latticeai/core/realtime.py +38 -15
  83. package/latticeai/core/sessions.py +7 -2
  84. package/latticeai/core/timeutil.py +10 -0
  85. package/latticeai/core/tool_registry.py +90 -18
  86. package/latticeai/core/users.py +1 -5
  87. package/latticeai/core/workspace_graph_trace.py +31 -5
  88. package/latticeai/core/workspace_memory.py +3 -1
  89. package/latticeai/core/workspace_os.py +18 -7
  90. package/latticeai/core/workspace_os_utils.py +0 -5
  91. package/latticeai/core/workspace_permissions.py +2 -1
  92. package/latticeai/core/workspace_plugins.py +1 -1
  93. package/latticeai/core/workspace_runs.py +14 -5
  94. package/latticeai/core/workspace_skills.py +1 -1
  95. package/latticeai/core/workspace_snapshots.py +2 -1
  96. package/latticeai/core/workspace_timeline.py +2 -1
  97. package/latticeai/integrations/telegram_bot.py +94 -43
  98. package/latticeai/models/router.py +130 -36
  99. package/latticeai/runtime/access_runtime.py +62 -4
  100. package/latticeai/runtime/automation_runtime.py +13 -7
  101. package/latticeai/runtime/brain_runtime.py +19 -7
  102. package/latticeai/runtime/chat_wiring.py +2 -0
  103. package/latticeai/runtime/config_runtime.py +58 -26
  104. package/latticeai/runtime/context_runtime.py +16 -4
  105. package/latticeai/runtime/hooks_runtime.py +1 -1
  106. package/latticeai/runtime/model_wiring.py +33 -5
  107. package/latticeai/runtime/namespace_runtime.py +62 -72
  108. package/latticeai/runtime/platform_runtime_wiring.py +4 -3
  109. package/latticeai/runtime/review_wiring.py +1 -1
  110. package/latticeai/runtime/router_registration.py +34 -1
  111. package/latticeai/runtime/security_runtime.py +110 -13
  112. package/latticeai/runtime/stages.py +27 -0
  113. package/latticeai/server_app.py +5 -1
  114. package/latticeai/services/architecture_readiness.py +74 -5
  115. package/latticeai/services/brain_automation.py +3 -26
  116. package/latticeai/services/chat_service.py +205 -15
  117. package/latticeai/services/local_knowledge.py +423 -0
  118. package/latticeai/services/memory_service.py +55 -21
  119. package/latticeai/services/model_engines.py +48 -33
  120. package/latticeai/services/model_errors.py +17 -0
  121. package/latticeai/services/model_loading.py +28 -25
  122. package/latticeai/services/model_runtime.py +228 -162
  123. package/latticeai/services/p_reinforce.py +22 -3
  124. package/latticeai/services/platform_runtime.py +83 -23
  125. package/latticeai/services/product_readiness.py +19 -17
  126. package/latticeai/services/review_queue.py +12 -0
  127. package/latticeai/services/router_context.py +1 -0
  128. package/latticeai/services/run_executor.py +4 -9
  129. package/latticeai/services/search_service.py +203 -28
  130. package/latticeai/services/tool_dispatch.py +28 -5
  131. package/latticeai/services/triggers.py +53 -4
  132. package/latticeai/services/upload_service.py +13 -2
  133. package/latticeai/setup/__init__.py +25 -0
  134. package/latticeai/setup/auto_setup.py +857 -0
  135. package/latticeai/setup/wizard.py +1264 -0
  136. package/latticeai/tools/__init__.py +278 -0
  137. package/{tools → latticeai/tools}/commands.py +67 -7
  138. package/{tools → latticeai/tools}/computer.py +1 -1
  139. package/{tools → latticeai/tools}/documents.py +1 -1
  140. package/{tools → latticeai/tools}/filesystem.py +3 -3
  141. package/latticeai/tools/knowledge.py +178 -0
  142. package/{tools → latticeai/tools}/local_files.py +1 -1
  143. package/{tools → latticeai/tools}/network.py +0 -1
  144. package/local_knowledge_api.py +4 -342
  145. package/package.json +22 -2
  146. package/scripts/brain_quality_eval.py +3 -1
  147. package/scripts/bump_version.py +3 -0
  148. package/scripts/capture_release_evidence.mjs +180 -0
  149. package/scripts/check_current_release_docs.mjs +142 -0
  150. package/scripts/check_i18n_literals.mjs +107 -31
  151. package/scripts/check_openapi_drift.mjs +56 -0
  152. package/scripts/export_openapi.py +67 -7
  153. package/scripts/i18n_literal_allowlist.json +22 -24
  154. package/scripts/run_integration_tests.mjs +72 -10
  155. package/scripts/validate_release_artifacts.py +49 -7
  156. package/scripts/wheel_smoke.py +5 -0
  157. package/setup_wizard.py +3 -1260
  158. package/src-tauri/Cargo.lock +1 -1
  159. package/src-tauri/Cargo.toml +1 -1
  160. package/src-tauri/tauri.conf.json +1 -1
  161. package/static/app/asset-manifest.json +11 -11
  162. package/static/app/assets/Act-C3dBrWE-.js +1 -0
  163. package/static/app/assets/Brain-DBYgdcjt.js +321 -0
  164. package/static/app/assets/Capture-Cf3hqRtN.js +1 -0
  165. package/static/app/assets/Library-CFfkNn3s.js +1 -0
  166. package/static/app/assets/System-BOurbT-v.js +1 -0
  167. package/static/app/assets/index-A3M9sElj.js +17 -0
  168. package/static/app/assets/index-Bmx9rzTc.css +2 -0
  169. package/static/app/assets/primitives-DcUUmhdC.js +1 -0
  170. package/static/app/assets/textarea-BklR6zN4.js +1 -0
  171. package/static/app/index.html +2 -2
  172. package/static/sw.js +1 -1
  173. package/tools/__init__.py +21 -269
  174. package/docs/CODE_REVIEW_2026-07-06.md +0 -764
  175. package/latticeai/runtime/app_context_runtime.py +0 -13
  176. package/latticeai/runtime/tail_wiring.py +0 -21
  177. package/scripts/com.pts.claudecode.discord.plist +0 -31
  178. package/scripts/pts-claudecode-discord-bridge.mjs +0 -207
  179. package/scripts/start-pts-claudecode-discord.sh +0 -51
  180. package/static/app/assets/Act-21lIXx2E.js +0 -1
  181. package/static/app/assets/Brain-BqUd5UJJ.js +0 -321
  182. package/static/app/assets/Capture-BA7Z2Q1u.js +0 -1
  183. package/static/app/assets/Library-bFMtyni3.js +0 -1
  184. package/static/app/assets/System-K6krGCqn.js +0 -1
  185. package/static/app/assets/index-C4R3ws30.js +0 -17
  186. package/static/app/assets/index-ChSeOB02.css +0 -2
  187. package/static/app/assets/primitives-sQU3it5I.js +0 -1
  188. package/static/app/assets/textarea-DK3Fd_lR.js +0 -1
  189. package/tools/knowledge.py +0 -95
package/docs/mcp-tools.md CHANGED
@@ -1,4 +1,4 @@
1
- # MCP 도구 카탈로그
1
+ # MCP 도구 카탈로그 (v9.2.0)
2
2
 
3
3
  Lattice AI는 MCP(Model Context Protocol) 서버로 동작하여 Claude Desktop, Cursor 등에서 직접 도구를 사용할 수 있습니다.
4
4
 
@@ -31,7 +31,7 @@ Lattice AI는 MCP(Model Context Protocol) 서버로 동작하여 Claude Desktop,
31
31
 
32
32
  | 도구 | 설명 | 위험도 |
33
33
  |------|------|--------|
34
- | `run_command` | 명령 실행 (위험 패턴 차단) | 높음 |
34
+ | `run_command` | shell 없이 고정 read-only 명령 allowlist 실행 | 높음 |
35
35
  | `run_terminal_command` | 터미널 명령 (별칭) | 높음 |
36
36
 
37
37
  ### 작업 관리
@@ -45,10 +45,11 @@ Lattice AI는 MCP(Model Context Protocol) 서버로 동작하여 Claude Desktop,
45
45
 
46
46
  | 도구 | 설명 | 위험도 |
47
47
  |------|------|--------|
48
- | `computer_screenshot` | 화면 캡처 | 낮음 |
48
+ | `computer_screenshot` | 화면 캡처 (desktop-control capability 및 정책 승인 필요) | 높음 |
49
+ | `computer_status` | 데스크톱 제어 상태 조회 (동일 capability/policy 적용) | 중간 |
49
50
  | `computer_open_app` | 앱 실행 | 중간 |
50
51
  | `computer_open_url` | URL 열기 | 낮음 |
51
- | `network_status` | IP, Wi-Fi 정보 | 낮음 |
52
+ | `network_status` | IP, Wi-Fi 정보 (인증 및 ToolRegistry 정책 적용) | 중간 |
52
53
 
53
54
  ### 문서
54
55
 
@@ -91,13 +92,18 @@ curl -b "session=<token>" \
91
92
  curl -b "session=<token>" -X POST \
92
93
  http://localhost:4825/tools/run_command \
93
94
  -H "Content-Type: application/json" \
94
- -d '{"command": "python -m pytest tests/ -v"}'
95
+ -d '{"command": "rg TODO ."}'
95
96
  ```
96
97
 
98
+ `run_command`는 `pwd`, `ls`, `find`, `cat`, `head`, `tail`, `wc`, `rg`만 허용합니다.
99
+ Python/Node/npm/npx/sed, shell operator, 실행 파일 경로, 절대 경로, `..` traversal,
100
+ workspace 밖 symlink, `rg --pre`, `find -exec/-delete`는 거부됩니다. 빌드와 테스트는
101
+ 별도의 허용된 project-script 도구를 사용합니다.
102
+
97
103
  ## 도구 카탈로그 조회
98
104
 
99
105
  ```bash
100
- curl http://localhost:4825/mcp/tools
106
+ curl -b "session=<token>" http://localhost:4825/mcp/tools
101
107
  ```
102
108
 
103
109
  응답:
@@ -114,3 +120,8 @@ curl http://localhost:4825/mcp/tools
114
120
  ]
115
121
  }
116
122
  ```
123
+
124
+ 도구 카탈로그는 인증이 필요하며 서버의 절대 `AGENT_ROOT`를 반환하지 않습니다.
125
+ MCP/plugin dispatch도 각 도구의 사용자·workspace·capability·승인 정책을 우회할
126
+ 수 없습니다. knowledge/Obsidian 계열 읽기는 명시적 사용자 동의와 scope를
127
+ 사용합니다.
package/docs/privacy.md CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  ## 요약
4
4
 
5
- **Lattice AI는 데이터를 수집하거나 외부 서버로 전송하지 않습니다.**
5
+ **Lattice AI는 기본 상태에서 텔레메트리를 수집하거나 사용자 데이터를 외부로
6
+ 전송하지 않습니다.** 사용자가 명시적으로 선택한 클라우드 모델, 모델 다운로드,
7
+ 웹 페이지 읽기, Telegram/Brain Network 같은 외부 기능은 해당 대상과 통신합니다.
6
8
 
7
9
  모든 데이터는 사용자의 로컬 머신에만 저장됩니다.
8
10
 
@@ -11,7 +13,7 @@
11
13
  | 데이터 | 저장 위치 | 설명 |
12
14
  |--------|-----------|------|
13
15
  | 사용자 계정 | `~/.ltcai/users.json` | 이름, scrypt 해시 비밀번호, 역할 |
14
- | 세션 토큰 | `~/.ltcai/sessions.json` | UUID 토큰, 만료시간 |
16
+ | 세션 토큰 | `~/.ltcai/sessions.json` | SHA-256 토큰 해시, 만료/갱신 시각(평문 bearer 미저장) |
15
17
  | 채팅 히스토리 | `~/.ltcai/chat_history.json` | 사용자-AI 대화 내용 |
16
18
  | 지식 그래프 | `~/.ltcai/knowledge_graph.sqlite` | 채팅/문서/웹/탭 노드·엣지, 프로비넌스 |
17
19
  | 업로드/수집 파일 | `~/.ltcai/knowledge_graph_blobs/` | 원본 PDF/DOCX/웹 텍스트 등 |
@@ -38,11 +40,25 @@
38
40
 
39
41
  Apple Silicon MLX 로컬 모델 사용 시에는 프롬프트가 외부로 전송되지 않습니다.
40
42
 
43
+ ## Telegram 및 권한 알림
44
+
45
+ Telegram bridge는 사용자가 명시적으로 활성화하고 chat ID를
46
+ `LATTICEAI_TELEGRAM_ALLOWED_CHAT_IDS`에 등록한 경우에만 해당 chat의 메시지와
47
+ callback query를 처리합니다. 로컬 Lattice API 호출에는 전용
48
+ `LATTICEAI_SERVER_SESSION_TOKEN`을 사용하며 세션 저장 파일에서 bearer를 찾지
49
+ 않습니다.
50
+
51
+ Discord 등의 권한 요청 알림에는 전체 승인 토큰이 아닌 짧은 hint만 포함됩니다.
52
+ 운영자가 `LATTICEAI_PERMISSION_UI_URL`을 설정하면 검토 페이지 링크를 제공할 수
53
+ 있지만, 승인 토큰 자체는 메시지나 URL에 넣지 않습니다.
54
+
41
55
  ## 웹/브라우저 수집 (v3.6.0)
42
56
 
43
57
  - **URL 읽기**(`/api/browser/read-url`): 사용자가 명시적으로 요청한 URL을 **로컬
44
58
  런타임이 직접** 가져와 텍스트만 추출해 그래프에 색인합니다. Lattice AI가 임의로
45
- 크롤링하지 않으며, 가져온 페이지는 외부로 다시 전송되지 않습니다.
59
+ 크롤링하지 않으며, 가져온 페이지는 외부로 다시 전송되지 않습니다. private/local
60
+ 주소와 DNS rebinding을 차단하고 redirect마다 다시 검증하며 최대 4MB의 textual
61
+ 응답만 스트리밍합니다.
46
62
  - **브라우저 탭 수집**(`/api/browser/ingest-current-tab`) 및 Manifest V3 확장:
47
63
  확장 프로그램은 **오직 `127.0.0.1`(로컬)** 로만 전송합니다. 클라우드 엔드포인트가
48
64
  존재하지 않습니다(`browser-extension/` 소스에서 단일 `fetch` 대상 확인 가능).
@@ -7,7 +7,8 @@ Render, Fly.io, Railway, VPS 등 외부 서버에 Lattice AI를 배포할 때
7
7
  ```bash
8
8
  # 필수
9
9
  LATTICEAI_MODE=public
10
- LATTICEAI_INVITE_CODE=my-secret-invite-code # 회원가입 필요한 초대 코드
10
+ LATTICEAI_INVITE_GATE_ENABLED=true # 초대 온보딩을 사용할 때만 활성화
11
+ LATTICEAI_INVITE_CODE=my-random-invite-code # 권장: 운영자가 생성한 랜덤 초대 코드
11
12
 
12
13
  # 클라우드 모델 (최소 하나 이상)
13
14
  OPENAI_API_KEY=sk-...
@@ -24,8 +25,33 @@ LATTICEAI_ENABLE_TELEGRAM=false # Telegram 봇 비활성화
24
25
  LATTICEAI_ENABLE_GRAPH=false # Data Graph 비활성화
25
26
  LATTICEAI_DATA_DIR=/data # 데이터 디렉토리
26
27
  LATTICEAI_ADMIN_EMAILS=you@example.com # 어드민 이메일 고정
28
+ LATTICEAI_PERMISSION_UI_URL=https://example.com/admin/permissions # 선택
27
29
  ```
28
30
 
31
+ public/non-loopback 모드는 인증을 강제하고 공개 회원가입을 닫습니다. 초대
32
+ 온보딩이 필요할 때만 `LATTICEAI_INVITE_GATE_ENABLED=true`로 게이트를 켭니다.
33
+ 초대 게이트에는 공용 기본 코드가 없습니다. `LATTICEAI_INVITE_CODE`를 생략하면
34
+ 게이트 활성화 시 CSPRNG로 설치별 코드와 쿠키 서명 키를 만들고 데이터 디렉터리의
35
+ 권한 제한 파일에 보관합니다. 운영 환경에서는 직접 랜덤 값을 설정하거나 이 파일이
36
+ 영구 볼륨에 남도록 해야 합니다. `authorized=true` 같은 정적 쿠키는 인증되지 않습니다.
37
+ 서명된 초대 claim은 해당 회원가입 요청만 허용하며, claim 없는 `/register` 직접
38
+ 호출은 공개 회원가입이 닫힌 상태를 우회할 수 없습니다.
39
+
40
+ Telegram을 의도적으로 활성화할 때만 아래 세 값을 모두 설정합니다.
41
+
42
+ ```bash
43
+ LATTICEAI_ENABLE_TELEGRAM=true
44
+ LATTICEAI_TELEGRAM_BOT_TOKEN=...
45
+ LATTICEAI_TELEGRAM_ALLOWED_CHAT_IDS=123456789,987654321
46
+ LATTICEAI_SERVER_SESSION_TOKEN=...
47
+ ```
48
+
49
+ 허용목록은 메시지와 callback query 모두에 등록보다 먼저 적용됩니다. 비어 있으면
50
+ 모든 chat을 거부합니다. `LATTICEAI_SERVER_SESSION_TOKEN`은 봇 전용 인증 세션의
51
+ bearer이며 `sessions.json`에 저장된 해시를 복사해서는 안 됩니다.
52
+ `LATTICEAI_PERMISSION_UI_URL`은 선택적인 승인 검토 페이지 링크일 뿐이며, 알림에는
53
+ 전체 승인 토큰이 포함되지 않습니다.
54
+
29
55
  ## Docker
30
56
 
31
57
  ```dockerfile
@@ -38,6 +64,7 @@ docker run --rm \
38
64
  -p 4825:4825 \
39
65
  -e LATTICEAI_MODE=public \
40
66
  -e OPENAI_API_KEY="$OPENAI_API_KEY" \
67
+ -e LATTICEAI_INVITE_GATE_ENABLED=true \
41
68
  -e LATTICEAI_INVITE_CODE="my-secret-code" \
42
69
  -v "$PWD/.data:/data" \
43
70
  lattice-ai
@@ -56,7 +83,7 @@ docker run --rm \
56
83
 
57
84
  ```bash
58
85
  fly launch
59
- fly secrets set LATTICEAI_MODE=public OPENAI_API_KEY=sk-... LATTICEAI_INVITE_CODE=secret
86
+ fly secrets set LATTICEAI_MODE=public OPENAI_API_KEY=sk-... LATTICEAI_INVITE_GATE_ENABLED=true LATTICEAI_INVITE_CODE=secret
60
87
  fly volumes create ltcai_data --size 1
61
88
  fly deploy
62
89
  ```
@@ -117,13 +144,15 @@ yourdomain.com {
117
144
  ## 퍼블릭 배포 체크리스트
118
145
 
119
146
  - [ ] `LATTICEAI_MODE=public` 설정
120
- - [ ] `LATTICEAI_INVITE_CODE` 비공개 랜덤 값으로 설정
147
+ - [ ] 초대 온보딩이 필요하면 `LATTICEAI_INVITE_GATE_ENABLED=true`와 비공개
148
+ 랜덤 `LATTICEAI_INVITE_CODE` 설정(또는 생성된 설치별 secret 영구 보관)
121
149
  - [ ] HTTPS 리버스 프록시 구성 (nginx / Caddy)
122
150
  - [ ] 영구 볼륨 마운트 (`/data` 또는 `LATTICEAI_DATA_DIR`)
123
151
  - [ ] 방화벽에서 4825 포트 직접 노출 차단
124
152
  - [ ] `LATTICEAI_ALLOW_LOCAL_MODELS=false`
125
153
  - [ ] 최소 하나의 클라우드 API 키 설정
126
154
  - [ ] 첫 가입 후 어드민 계정 확인 (`http://yourdomain.com/admin`)
155
+ - [ ] Telegram 활성화 시 chat ID 허용목록과 전용 서버 세션 토큰을 모두 설정
127
156
 
128
157
  ## 지원 클라우드 모델 프리픽스
129
158
 
@@ -22,15 +22,23 @@ Lattice AI는 **개인 AI 워크스페이스**로 설계되었습니다. 기본
22
22
 
23
23
  ### 세션
24
24
 
25
- - UUID 토큰, `~/.ltcai/sessions.json` 파일 저장
25
+ - `secrets.token_urlsafe`로 생성한 토큰은 SHA-256 해시만
26
+ `~/.ltcai/sessions.json`에 저장(레거시 평문 토큰은 로드 시 자동 마이그레이션)
26
27
  - TTL: 24시간 + sliding refresh (활동 시 자동 연장, 15분 단위 디스크 쓰기)
27
28
  - 쿠키: `HttpOnly; SameSite=Lax; Path=/`
29
+ - public/non-loopback에서는 `Secure`도 강제(HTTPS 필요)
28
30
  - 서버 재시작 후에도 유지 (파일 기반)
31
+ - 삭제되거나 비활성화된 계정의 기존 세션은 다음 요청에서 즉시 거부
32
+ - POSIX에서 데이터 디렉터리 `0700`, atomic JSON/세션 파일 `0600`
33
+ - 초대 게이트 쿠키는 설치별 secret으로 HMAC 서명되고 만료 시각을 포함함.
34
+ `authorized=true` 리터럴은 신뢰하지 않으며, 공용 기본 invite code도 없음
29
35
 
30
36
  ### SSO (선택적)
31
37
 
32
38
  - Entra ID / Okta OIDC (`OIDC_DISCOVERY_URL`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`)
33
39
  - 콜백 후 내부 세션 토큰으로 변환
40
+ - 초대 게이트 활성화 시 신규 SSO JIT 계정은 서명 초대 권한이 필요하며,
41
+ 해당 권한은 서버 측 일회용 state/nonce/PKCE 트랜잭션에 바인딩됨
34
42
  - 어드민 핸드오프: `sessionStorage` 1회 읽기 (URL 파라미터 노출 방지)
35
43
 
36
44
  ## API 키 보안
@@ -83,15 +91,27 @@ MAGIC_NUMBERS = {
83
91
  - **수집 라이프사이클**: 모든 수집은 `IngestionPipeline.ingest` → `dispatch_tool`
84
92
  를 거쳐 `pre_tool`/`post_tool` 훅이 발화됩니다. `pre_tool`이 차단하면 수집은
85
93
  정직하게 `status="blocked"`로 거부됩니다(권한 게이트·민감정보 가드 적용).
86
- - **웹 URL 읽기**(`/api/browser/read-url`): `http(s)` 스킴만 허용(파일/기타 스킴
87
- 거부), 12초 타임아웃, 4MB 응답 상한, HTML/text 컨텐츠 타입만 처리. 차단/로그인
88
- 필요 페이지는 5xx가 아닌 **422로 우아하게 실패**합니다. 로컬 런타임이 사용자가
89
- 명시한 URL만 가져옵니다(자동 크롤링 없음).
94
+ - **웹 URL 읽기**(`/api/browser/read-url`): `http(s)`만 허용하고 DNS의 모든 결과에서
95
+ loopback/private/link-local/multicast/reserved 주소를 거부합니다. 검증한 IP에 연결을
96
+ 고정해 DNS rebinding을 막고, redirect마다 재검증하며 환경 proxy를 사용하지
97
+ 않습니다. 12초 타임아웃, 스트리밍된 4MB 응답 상한, textual content type만
98
+ 처리합니다. 차단/로그인 필요 페이지는 5xx가 아닌 **422로 실패**합니다.
90
99
  - **브라우저 탭 수집**(`/api/browser/ingest-current-tab`): payload 정화(스크립트/
91
100
  스타일 제거) + 페이로드 크기 상한(413). Manifest V3 확장은 **`127.0.0.1`로만**
92
101
  전송하며 클라우드 엔드포인트가 없습니다.
93
- - **포터빌리티 권한**: 그래프 export/status 읽기는 로그인 사용자, **import /
94
- backup / restore 는 admin 전용**(`require_admin`). 그래프는 머신-전역 자원입니다.
102
+ - **포터빌리티 권한**: 상태 읽기는 로그인 사용자, 전체 그래프 export/provenance와
103
+ **import / backup / restore는 admin 전용**(`require_admin`)입니다. 그래프는
104
+ 머신-전역 자원입니다.
105
+ - **워크스페이스 컨텍스트**: MCP 그래프 호출, 메모리 recall, hybrid search,
106
+ garden-note 컨텍스트, realtime presence는 인증된 사용자와 활성/허용 workspace에
107
+ 바인딩됩니다. 에이전트·플러그인 registry 및 graph curation 변경은 admin
108
+ 전용이며, MCP 환경 변수 값은 API 응답에 포함되지 않습니다. MCP/플러그인
109
+ 실행은 local file/document 도구를 호출할 수 없고 전용 승인 토큰 경로를
110
+ 사용해야 합니다. Document RAG와 answer trace도 동일한 workspace 범위로
111
+ 검색·저장됩니다.
112
+ - **fail-closed KG 범위**: workspace projection 조회 실패 또는 scope를 알 수 없는
113
+ v2 node는 반환하지 않습니다. legacy-global 데이터는
114
+ `include_legacy_global=True`를 명시한 호환 경로에서만 읽습니다.
95
115
  - **복원 무결성**: 백업 아카이브는 `manifest.json`의 sha256과 대조 검증 후에만
96
116
  복원되며, 불일치 시 거부됩니다.
97
117
 
@@ -99,9 +119,10 @@ MAGIC_NUMBERS = {
99
119
 
100
120
  ### `run_command()` 위험 플래그 차단
101
121
 
102
- 다음 패턴이 포함된 명령 실행 거부:
103
- - `rm -rf`, `sudo`, `chmod 777`, `curl | bash`, `wget | sh`
104
- - `> /dev/sda`, `dd if=`, `mkfs`
122
+ 고정 allowlist(`pwd`, `ls`, `find`, `cat`, `head`, `tail`, `wc`, `rg`)와 정화된
123
+ 환경만 사용합니다. Python/Node/npm/npx/sed 같은 일반 인터프리터, 실행 파일 경로,
124
+ 절대 경로·`..` traversal·workspace 밖 symlink, `rg --pre`, `find -exec/-delete`
125
+ 등은 실행 전에 거부합니다.
105
126
 
106
127
  ### `edit_file()` 유일성 검증
107
128
 
@@ -119,6 +140,17 @@ MAGIC_NUMBERS = {
119
140
  - 평문 비밀번호 마이그레이션 이벤트: `password_migrated_from_plaintext`
120
141
  - `server.log` 파일에 모든 요청 기록
121
142
 
143
+ ## Telegram 및 권한 알림
144
+
145
+ - Telegram은 `LATTICEAI_TELEGRAM_ALLOWED_CHAT_IDS`에 지정된 chat의 메시지와
146
+ callback query만 처리하며, 허용 전에 chat을 등록하지 않습니다.
147
+ - 봇의 로컬 API 호출에는 `LATTICEAI_SERVER_SESSION_TOKEN` 전용 bearer가
148
+ 필요합니다. 봇은 `sessions.json`을 스캔하지 않습니다.
149
+ - 권한 요청 알림은 전체 승인 토큰 대신 8자 hint만 포함합니다. 선택적인
150
+ `LATTICEAI_PERMISSION_UI_URL`은 사람의 검토 페이지로 연결하지만 토큰을 URL에
151
+ 넣지 않습니다.
152
+ - permission queue는 atomic write와 POSIX `0600`을 사용합니다.
153
+
122
154
  ## 텔레메트리
123
155
 
124
156
  **없음.** 모든 데이터는 로컬에만 저장됩니다. 외부 서버로 어떠한 사용 데이터도 전송되지 않습니다.
@@ -128,11 +160,14 @@ MAGIC_NUMBERS = {
128
160
  ## 퍼블릭 배포 체크리스트
129
161
 
130
162
  - [ ] `LATTICEAI_MODE=public`
131
- - [ ] `LATTICEAI_INVITE_CODE` 비공개 값 설정
163
+ - [ ] 초대 온보딩을 사용할 때만 `LATTICEAI_INVITE_GATE_ENABLED=true`와 비공개
164
+ `LATTICEAI_INVITE_CODE` 설정(미설정 시 생성되는 설치별 secret 영구 보관)
132
165
  - [ ] HTTPS 리버스 프록시 (nginx/Caddy)
133
166
  - [ ] `LATTICEAI_ENABLE_GRAPH=false` (필요 시)
134
167
  - [ ] `/data` 영구 볼륨 마운트
135
168
  - [ ] `LATTICEAI_ALLOW_LOCAL_MODELS=false`
136
169
  - [ ] 방화벽에서 4825 포트 직접 노출 차단 (리버스 프록시 통해서만)
170
+ - [ ] Telegram 활성화 시 `LATTICEAI_TELEGRAM_ALLOWED_CHAT_IDS`와
171
+ `LATTICEAI_SERVER_SESSION_TOKEN` 설정
137
172
 
138
173
  자세한 내용: [public-deploy.md](public-deploy.md)
@@ -26,7 +26,7 @@ from .storage import (
26
26
  storage_from_env,
27
27
  )
28
28
 
29
- __version__ = "9.0.0"
29
+ __version__ = "9.2.0"
30
30
 
31
31
  __all__ = [
32
32
  "AgentRuntime",
@@ -17,7 +17,6 @@ import sqlite3
17
17
  import tempfile
18
18
  import zipfile
19
19
  from dataclasses import dataclass
20
- from datetime import datetime, timezone
21
20
  from pathlib import Path, PurePosixPath
22
21
  from typing import Any, Dict, Iterable, List, Optional
23
22
 
@@ -26,7 +25,7 @@ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
26
25
  from cryptography.exceptions import InvalidTag
27
26
  from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
28
27
 
29
- from .utils import sha256_file as _sha256_file
28
+ from .utils import sha256_file as _sha256_file, utc_now_iso as _now
30
29
 
31
30
 
32
31
  ARCHIVE_FORMAT = "latticebrain.encrypted"
@@ -48,10 +47,6 @@ PORTABLE_DATA_FILES = (
48
47
  PORTABLE_EXPORT_SUFFIXES = (".json", ".zip")
49
48
 
50
49
 
51
- def _now() -> str:
52
- return datetime.now(timezone.utc).isoformat()
53
-
54
-
55
50
  def _stamp() -> str:
56
51
  return _now().replace(":", "").replace("-", "").replace(".", "")[:15]
57
52
 
@@ -13,6 +13,7 @@ design-review amendment T5).
13
13
 
14
14
  from __future__ import annotations
15
15
 
16
+ import inspect
16
17
  import logging
17
18
  from dataclasses import dataclass, field
18
19
  from typing import Any, Callable, Dict, List, Optional
@@ -23,6 +24,46 @@ def approx_tokens(text: str) -> int:
23
24
  return max(0, (len(text or "") + 3) // 4)
24
25
 
25
26
 
27
+ def _call_context_seam(callback: Callable[..., Any], query: str, **context: Any) -> Any:
28
+ """Pass identity/scope only when a legacy seam declares support for it.
29
+
30
+ Signature inspection preserves old one-argument adapters without catching
31
+ a callback's own ``TypeError`` and retrying it with less restrictive scope.
32
+ If a callable cannot be inspected, the secure behavior is to pass every
33
+ context field and let the caller's failure isolation omit that section.
34
+ """
35
+ try:
36
+ parameters = inspect.signature(callback).parameters
37
+ except (TypeError, ValueError):
38
+ return callback(query, **context)
39
+ accepts_kwargs = any(
40
+ item.kind is inspect.Parameter.VAR_KEYWORD
41
+ for item in parameters.values()
42
+ )
43
+ supported = context if accepts_kwargs else {
44
+ key: value for key, value in context.items()
45
+ if key in parameters
46
+ }
47
+ return callback(query, **supported)
48
+
49
+
50
+ def _call_keyword_seam(callback: Callable[..., Any], **context: Any) -> Any:
51
+ """Invoke a keyword-only seam while preserving legacy narrow signatures."""
52
+ try:
53
+ parameters = inspect.signature(callback).parameters
54
+ except (TypeError, ValueError):
55
+ return callback(**context)
56
+ accepts_kwargs = any(
57
+ item.kind is inspect.Parameter.VAR_KEYWORD
58
+ for item in parameters.values()
59
+ )
60
+ supported = context if accepts_kwargs else {
61
+ key: value for key, value in context.items()
62
+ if key in parameters
63
+ }
64
+ return callback(**supported)
65
+
66
+
26
67
  @dataclass
27
68
  class ContextSection:
28
69
  name: str
@@ -112,11 +153,11 @@ class ContextAssembler:
112
153
  if self._memory_recall is not None:
113
154
  sections.append(self._memories_section(query, user_email, workspace_id, memory_limit))
114
155
  if self._hybrid_search is not None:
115
- sections.append(self._knowledge_section(query, knowledge_limit, user_email))
156
+ sections.append(self._knowledge_section(query, knowledge_limit, user_email, workspace_id))
116
157
  if self._notes_context is not None:
117
- sections.append(self._notes_section(query))
158
+ sections.append(self._notes_section(query, user_email, workspace_id))
118
159
  if self._recent_chat is not None:
119
- sections.append(self._recent_section(user_email, conversation_id))
160
+ sections.append(self._recent_section(user_email, conversation_id, workspace_id))
120
161
 
121
162
  sections = [s for s in sections if s.content.strip()]
122
163
  self._apply_budget(sections, budget)
@@ -141,9 +182,15 @@ class ContextAssembler:
141
182
  ],
142
183
  )
143
184
 
144
- def _knowledge_section(self, query, limit, user_email=None) -> ContextSection:
185
+ def _knowledge_section(self, query, limit, user_email=None, workspace_id=None) -> ContextSection:
145
186
  try:
146
- hybrid = self._hybrid_search(query, limit=limit, user_email=user_email)
187
+ hybrid = _call_context_seam(
188
+ self._hybrid_search,
189
+ query,
190
+ limit=limit,
191
+ user_email=user_email,
192
+ workspace_id=workspace_id,
193
+ )
147
194
  matches = hybrid.get("matches", [])[:limit]
148
195
  except Exception as exc:
149
196
  logging.debug("context: hybrid search failed: %s", exc)
@@ -166,9 +213,14 @@ class ContextAssembler:
166
213
  provenance=provenance,
167
214
  )
168
215
 
169
- def _notes_section(self, query) -> ContextSection:
216
+ def _notes_section(self, query, user_email=None, workspace_id=None) -> ContextSection:
170
217
  try:
171
- content = self._notes_context(query) or ""
218
+ content = _call_context_seam(
219
+ self._notes_context,
220
+ query,
221
+ user_email=user_email,
222
+ workspace_id=workspace_id,
223
+ ) or ""
172
224
  except Exception as exc:
173
225
  logging.debug("context: notes context failed: %s", exc)
174
226
  content = ""
@@ -179,9 +231,14 @@ class ContextAssembler:
179
231
  provenance=[{"source": "garden", "included": bool(content)}],
180
232
  )
181
233
 
182
- def _recent_section(self, user_email, conversation_id) -> ContextSection:
234
+ def _recent_section(self, user_email, conversation_id, workspace_id=None) -> ContextSection:
183
235
  try:
184
- content = self._recent_chat(user_email=user_email, conversation_id=conversation_id) or ""
236
+ content = _call_keyword_seam(
237
+ self._recent_chat,
238
+ user_email=user_email,
239
+ conversation_id=conversation_id,
240
+ workspace_id=workspace_id,
241
+ ) or ""
185
242
  except Exception as exc:
186
243
  logging.debug("context: recent chat failed: %s", exc)
187
244
  content = ""
@@ -22,11 +22,7 @@ from pathlib import Path
22
22
  from typing import Any, Dict, List, Optional, Tuple
23
23
 
24
24
  from ._kg_constants import * # noqa: F401,F403
25
- from ..utils import parse_iso as _parse_iso
26
-
27
-
28
- def _now() -> str:
29
- return datetime.now().isoformat()
25
+ from ..utils import now_iso as _now, parse_iso as _parse_iso
30
26
 
31
27
 
32
28
  def _recency_score(