ltcai 4.7.2 → 5.1.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 (42) hide show
  1. package/README.md +59 -45
  2. package/docs/CHANGELOG.md +100 -0
  3. package/docs/TRUST_MODEL.md +66 -0
  4. package/docs/WHY_LATTICE.md +54 -0
  5. package/frontend/src/App.tsx +105 -70
  6. package/frontend/src/components/ProductFlow.tsx +102 -69
  7. package/frontend/src/components/primitives.tsx +1 -1
  8. package/frontend/src/i18n.ts +247 -0
  9. package/frontend/src/pages/System.tsx +1 -1
  10. package/frontend/src/store/appStore.ts +18 -0
  11. package/frontend/src/styles.css +36 -0
  12. package/lattice_brain/__init__.py +1 -1
  13. package/lattice_brain/portability.py +11 -7
  14. package/lattice_brain/runtime/multi_agent.py +1 -1
  15. package/latticeai/__init__.py +1 -1
  16. package/latticeai/api/chat.py +19 -11
  17. package/latticeai/api/models.py +6 -0
  18. package/latticeai/api/security_dashboard.py +3 -15
  19. package/latticeai/api/static_routes.py +16 -0
  20. package/latticeai/app_factory.py +114 -40
  21. package/latticeai/core/audit.py +3 -1
  22. package/latticeai/core/builtin_hooks.py +7 -9
  23. package/latticeai/core/logging_safety.py +5 -21
  24. package/latticeai/core/marketplace.py +1 -1
  25. package/latticeai/core/security.py +67 -9
  26. package/latticeai/core/workspace_os.py +1 -1
  27. package/package.json +2 -2
  28. package/scripts/clean_release_artifacts.mjs +16 -1
  29. package/scripts/com.pts.claudecode.discord.plist +31 -0
  30. package/scripts/pts-claudecode-discord-bridge.mjs +189 -0
  31. package/scripts/run_integration_tests.mjs +91 -0
  32. package/scripts/start-pts-claudecode-discord.sh +51 -0
  33. package/src-tauri/Cargo.lock +1 -1
  34. package/src-tauri/Cargo.toml +1 -1
  35. package/src-tauri/tauri.conf.json +3 -2
  36. package/static/app/asset-manifest.json +5 -5
  37. package/static/app/assets/index-DONOJfMn.js +16 -0
  38. package/static/app/assets/index-DONOJfMn.js.map +1 -0
  39. package/static/app/assets/{index-KlQ04wVv.css → index-DuYYT2oh.css} +1 -1
  40. package/static/app/index.html +2 -2
  41. package/static/app/assets/index-DdAB4yfa.js +0 -16
  42. package/static/app/assets/index-DdAB4yfa.js.map +0 -1
@@ -0,0 +1,247 @@
1
+ export type Language = "ko" | "en";
2
+
3
+ export type TextMap = Record<string, string>;
4
+
5
+ export const LANGUAGE_LABELS: Record<Language, string> = {
6
+ ko: "한국어",
7
+ en: "English",
8
+ };
9
+
10
+ export const COPY: Record<Language, TextMap> = {
11
+ ko: {
12
+ "language.label": "언어",
13
+ "language.ko": "한국어",
14
+ "language.en": "English",
15
+ "brain.level": "단계",
16
+ "brain.depth.1": "Living Brain",
17
+ "brain.depth.2": "Memory Layer",
18
+ "brain.depth.3": "Knowledge Layer",
19
+ "brain.depth.4": "Relationship Layer",
20
+ "brain.depth.5": "Knowledge Graph",
21
+ "brain.view.memories": "기억 보기",
22
+ "brain.view.topics": "주제 보기",
23
+ "brain.view.relationships": "관계 보기",
24
+ "brain.view.graph": "그래프로 보기",
25
+ "brain.surface": "돌아가기",
26
+ "brain.title": "Lattice Brain",
27
+ "brain.local": "로컬 우선",
28
+ "brain.portable": "이동 가능",
29
+ "brain.private": "개인 소유",
30
+ "brain.admin": "관리자 콘솔",
31
+ "brain.empty.kicker": "내 오래가는 기억",
32
+ "brain.empty.title": "잊으면 안 되는 일부터 말해 주세요.",
33
+ "brain.empty.body": "문서, 대화, 프로젝트, 결정이 Brain에 쌓이고 나중에 주제와 관계로 다시 보입니다.",
34
+ "brain.prompt.remember": "이 결정을 기억해줘: ",
35
+ "brain.prompt.know": "내가 이미 알고 있는 것은? ",
36
+ "brain.prompt.plan": "이 프로젝트 맥락을 계획으로 바꿔줘: ",
37
+ "brain.placeholder": "Brain에게 말하기...",
38
+ "brain.image": "이미지",
39
+ "brain.unavailable": "지금은 답할 수 없음",
40
+ "brain.imageAttached": "이미지 첨부됨",
41
+ "brain.send": "보내기",
42
+ "brain.saved": "기억에 저장됨 · 연결된 주제 {topics}개 · 관련 기억 {memories}개",
43
+ "brain.recalled": "기억을 다시 꺼냈습니다: {title}",
44
+ "brain.overview.kicker": "Brain 한눈에 보기",
45
+ "brain.overview.title": "기억과 주제를 바로 확인하세요.",
46
+ "brain.overview.graph": "전체 그래프",
47
+ "brain.overview.recent": "최근 기억",
48
+ "brain.overview.recentEmpty": "아직 최근 기억이 없습니다.",
49
+ "brain.overview.older": "이전 기억",
50
+ "brain.overview.olderEmpty": "대화가 쌓이면 과거 기억이 보입니다.",
51
+ "brain.overview.topics": "주요 주제",
52
+ "brain.overview.topicsEmpty": "주제가 형성되는 중입니다.",
53
+ "brain.graph.empty": "아직 맞는 지식이 없습니다.",
54
+ "brain.graph.summaryFallback": "이 개념은 가장 깊은 지식 계층의 일부입니다.",
55
+ "brain.graph.focused": "대화와 문서에서 함께 나온 내용이 선으로 이어집니다.",
56
+ "brain.graph.emptyFocus": "대화, 문서, 프로젝트를 쌓으면 Brain 그래프가 자랍니다.",
57
+ "care.title": "내 Brain 돌보기",
58
+ "care.subtitle": "내 컴퓨터에 두고, 필요하면 옮길 수 있습니다.",
59
+ "care.private": "개인 보관",
60
+ "care.export": "내보내기",
61
+ "care.export.detail": "다른 곳으로 가져가기",
62
+ "care.backup": "백업",
63
+ "care.backup.detail": "복사본 저장",
64
+ "care.archive": "보관 파일",
65
+ "care.archive.detail": "암호화된 Brain",
66
+ "care.path.placeholder": "확인하거나 미리 볼 보관 파일 경로",
67
+ "care.path.label": "Brain 보관 파일 경로",
68
+ "care.passphrase.placeholder": "보관 파일 비밀번호",
69
+ "care.passphrase.label": "Brain 보관 파일 비밀번호",
70
+ "care.inspect": "확인",
71
+ "care.restorePreview": "복원 미리보기",
72
+ "care.note": "복원 미리보기는 Brain을 바꾸지 않고 보관 파일만 확인합니다. 실제 복원은 설정에서 확정합니다.",
73
+ "care.working": "진행 중",
74
+ "admin.back": "Brain",
75
+ "admin.kicker": "분리된 관리자 작업공간",
76
+ "admin.title": "Admin Console",
77
+ "admin.body": "사용자, 로그, 보안, Brain 상태는 일반 사용자 화면과 분리됩니다.",
78
+ "flow.shell": "내 로컬 Brain 만들기",
79
+ "flow.login.title": "내 Brain을 시작합니다.",
80
+ "flow.login.body": "모델은 바뀔 수 있지만, 내 문서와 대화, 결정, 기억은 사라지면 안 됩니다. Lattice는 이 지식을 내가 소유하는 개인 Brain으로 모읍니다.",
81
+ "flow.name": "이름",
82
+ "flow.email": "이메일",
83
+ "flow.password": "비밀번호",
84
+ "flow.password.placeholder": "로컬 Brain 비밀번호",
85
+ "flow.login.busy": "Brain 여는 중...",
86
+ "flow.login.submit": "내 Brain 시작하기",
87
+ "flow.login.note": "기존 Brain과 다른 이메일이면 새로 만들지 않고 먼저 확인합니다.",
88
+ "flow.login.missing": "이름과 이메일, 비밀번호를 입력하면 기존 Brain을 안전하게 확인합니다.",
89
+ "flow.login.otherEmail": "이 컴퓨터의 기존 Brain과 다른 이메일입니다. 오타인지 확인해 주세요.",
90
+ "flow.login.wrongPassword": "기존 Brain 이메일은 맞지만 비밀번호가 다릅니다. 비밀번호를 다시 확인해 주세요.",
91
+ "flow.login.unavailable": "로컬 프로필을 열 수 없습니다. 이메일과 비밀번호를 확인해 주세요.",
92
+ "flow.promise.memory.k": "오래 남는 지식",
93
+ "flow.promise.memory.v": "내 일이 장기 기억이 됩니다.",
94
+ "flow.promise.model.k": "교체 가능한 모델",
95
+ "flow.promise.model.v": "모델은 목소리이고, 자산은 Brain입니다.",
96
+ "flow.promise.ownership.k": "사용자 소유",
97
+ "flow.promise.ownership.v": "백업, 복원, 이동이 가능합니다.",
98
+ "flow.analysis.title": "이 컴퓨터를 확인합니다.",
99
+ "flow.analysis.body": "Brain이 클라우드에 의존하지 않고 이 컴퓨터에서 편하게 돌아갈 수 있는지 확인합니다.",
100
+ "flow.analysis.finding": "가장 편한 설정을 찾는 중...",
101
+ "flow.analysis.ready": "추천 모델을 바로 시작할 수 있게 준비했습니다.",
102
+ "flow.analysis.wait": "잠시만 기다리면 자동으로 정리됩니다.",
103
+ "flow.analysis.error": "Lattice가 이 컴퓨터를 끝까지 확인하지 못했습니다. 그래도 안전한 기본값으로 계속할 수 있습니다.",
104
+ "flow.analysis.continue": "추천 모델 보기",
105
+ "flow.recommend.title": "추천 모델로 시작하세요.",
106
+ "flow.recommend.body": "모델은 Brain의 현재 목소리입니다. 나중에 바꿔도 기억과 지식은 그대로 남습니다.",
107
+ "flow.recommend.primary": "추천대로 시작하기",
108
+ "flow.recommend.unsupported": "이 컴퓨터에서 추가 확인이 필요합니다",
109
+ "flow.recommend.back": "뒤로",
110
+ "flow.recommend.hint": "잘 모르겠다면 추천대로 시작하면 됩니다.",
111
+ "flow.install.title": "모델을 설치하고 시작합니다.",
112
+ "flow.install.body": "이 모델이 Brain의 로컬 목소리가 됩니다. 다운로드, 확인, 로드는 사용자가 누른 뒤에만 진행됩니다.",
113
+ "flow.install.wait": "Brain이 사용할 모델을 기다리고 있습니다.",
114
+ "flow.install.prepare": "Brain 준비 중입니다.",
115
+ "flow.install.done": "Brain이 준비되었습니다.",
116
+ "flow.install.note": "큰 모델은 다운로드에 몇 분 이상 걸릴 수 있습니다. 진행률은 모델 런타임이 알려주는 만큼 표시하고, 멈춘 것처럼 보여도 현재 단계가 유지됩니다.",
117
+ "flow.install.retry": "다른 모델을 고르거나 다시 시도할 수 있습니다.",
118
+ "flow.install.back": "다른 모델 고르기",
119
+ "flow.install.start": "다운로드하고 시작하기",
120
+ "flow.install.busy": "모델 준비 중...",
121
+ "flow.install.enter": "Brain으로 들어가기",
122
+ "flow.install.local": "모든 작업은 이 컴퓨터에서 진행됩니다.",
123
+ },
124
+ en: {
125
+ "language.label": "Language",
126
+ "language.ko": "한국어",
127
+ "language.en": "English",
128
+ "brain.level": "Level",
129
+ "brain.depth.1": "Living Brain",
130
+ "brain.depth.2": "Memory Layer",
131
+ "brain.depth.3": "Knowledge Layer",
132
+ "brain.depth.4": "Relationship Layer",
133
+ "brain.depth.5": "Knowledge Graph",
134
+ "brain.view.memories": "Memories",
135
+ "brain.view.topics": "Topics",
136
+ "brain.view.relationships": "Relationships",
137
+ "brain.view.graph": "Graph",
138
+ "brain.surface": "Back",
139
+ "brain.title": "Lattice Brain",
140
+ "brain.local": "Local-first",
141
+ "brain.portable": "Portable",
142
+ "brain.private": "Private",
143
+ "brain.admin": "Admin Console",
144
+ "brain.empty.kicker": "Durable memory",
145
+ "brain.empty.title": "Start with what should not be forgotten.",
146
+ "brain.empty.body": "Documents, conversations, projects, and decisions accumulate in your Brain, then reappear as topics and relationships.",
147
+ "brain.prompt.remember": "Remember this decision: ",
148
+ "brain.prompt.know": "What do I already know about ",
149
+ "brain.prompt.plan": "Turn this project context into a plan: ",
150
+ "brain.placeholder": "Talk to your Brain...",
151
+ "brain.image": "Image",
152
+ "brain.unavailable": "Unavailable",
153
+ "brain.imageAttached": "Image attached",
154
+ "brain.send": "Send",
155
+ "brain.saved": "Saved to memory · {topics} linked topics · {memories} related memories",
156
+ "brain.recalled": "Recalled this memory: {title}",
157
+ "brain.overview.kicker": "Brain at a glance",
158
+ "brain.overview.title": "See memories and topics immediately.",
159
+ "brain.overview.graph": "Full graph",
160
+ "brain.overview.recent": "Recent memories",
161
+ "brain.overview.recentEmpty": "No recent memories yet.",
162
+ "brain.overview.older": "Earlier memories",
163
+ "brain.overview.olderEmpty": "Earlier memories appear as conversations accumulate.",
164
+ "brain.overview.topics": "Main topics",
165
+ "brain.overview.topicsEmpty": "Topics are still forming.",
166
+ "brain.graph.empty": "No matching knowledge yet",
167
+ "brain.graph.summaryFallback": "This concept is part of the deepest knowledge layer.",
168
+ "brain.graph.focused": "Ideas that appeared together in conversations and documents are connected by lines.",
169
+ "brain.graph.emptyFocus": "Your Brain graph grows as you add conversations, documents, and projects.",
170
+ "care.title": "Care for my Brain",
171
+ "care.subtitle": "Own it locally. Keep it portable.",
172
+ "care.private": "Private",
173
+ "care.export": "Export",
174
+ "care.export.detail": "Take it with you",
175
+ "care.backup": "Backup",
176
+ "care.backup.detail": "Save a copy",
177
+ "care.archive": "Archive",
178
+ "care.archive.detail": "Encrypted Brain",
179
+ "care.path.placeholder": "Paste an archive path to inspect or preview",
180
+ "care.path.label": "Brain archive path",
181
+ "care.passphrase.placeholder": "Archive passphrase",
182
+ "care.passphrase.label": "Brain archive passphrase",
183
+ "care.inspect": "Inspect",
184
+ "care.restorePreview": "Restore preview",
185
+ "care.note": "Restore preview checks an archive without changing your Brain. Confirmed restore stays in Settings.",
186
+ "care.working": "Working",
187
+ "admin.back": "Brain",
188
+ "admin.kicker": "Separate admin workspace",
189
+ "admin.title": "Admin Console",
190
+ "admin.body": "Users, logs, security, and Brain health stay out of the normal user experience.",
191
+ "flow.shell": "Create your local Brain",
192
+ "flow.login.title": "Start your Brain.",
193
+ "flow.login.body": "Models can change, but your documents, conversations, decisions, and memories should stay yours. Lattice gathers them into a personal Brain you control.",
194
+ "flow.name": "Name",
195
+ "flow.email": "Email",
196
+ "flow.password": "Password",
197
+ "flow.password.placeholder": "Local Brain password",
198
+ "flow.login.busy": "Opening Brain...",
199
+ "flow.login.submit": "Start my Brain",
200
+ "flow.login.note": "If the email differs from this computer's Brain, Lattice checks before creating anything new.",
201
+ "flow.login.missing": "Enter your name, email, and password so Lattice can safely check your Brain.",
202
+ "flow.login.otherEmail": "This email differs from the Brain on this computer. Please check for a typo.",
203
+ "flow.login.wrongPassword": "The email matches this Brain, but the password is different. Please check it again.",
204
+ "flow.login.unavailable": "Could not open the local profile. Check your email and password.",
205
+ "flow.promise.memory.k": "Durable knowledge",
206
+ "flow.promise.memory.v": "Your work becomes long-term memory.",
207
+ "flow.promise.model.k": "Replaceable models",
208
+ "flow.promise.model.v": "The model is the voice; the Brain is the asset.",
209
+ "flow.promise.ownership.k": "User owned",
210
+ "flow.promise.ownership.v": "Back up, restore, and move it.",
211
+ "flow.analysis.title": "Checking this computer.",
212
+ "flow.analysis.body": "Lattice checks whether your Brain can run comfortably on this computer without depending on the cloud.",
213
+ "flow.analysis.finding": "Finding the easiest setup...",
214
+ "flow.analysis.ready": "Your recommended model is ready to start.",
215
+ "flow.analysis.wait": "This will be summarized automatically in a moment.",
216
+ "flow.analysis.error": "Lattice could not finish checking this computer. You can still continue with a safe default.",
217
+ "flow.analysis.continue": "View recommended models",
218
+ "flow.recommend.title": "Start with the recommended model.",
219
+ "flow.recommend.body": "The model is your Brain's current voice. You can change it later without losing memory or knowledge.",
220
+ "flow.recommend.primary": "Start with recommendation",
221
+ "flow.recommend.unsupported": "This computer needs one more check",
222
+ "flow.recommend.back": "Back",
223
+ "flow.recommend.hint": "If unsure, start with the recommendation.",
224
+ "flow.install.title": "Install the model and start.",
225
+ "flow.install.body": "This model becomes your Brain's local voice. Download, validation, and loading begin only after you choose to start.",
226
+ "flow.install.wait": "Waiting for the model your Brain will use.",
227
+ "flow.install.prepare": "Preparing your Brain.",
228
+ "flow.install.done": "Your Brain is ready.",
229
+ "flow.install.note": "Large models can take several minutes to download. Progress is shown when the model runtime reports it; a steady stage still means work is continuing.",
230
+ "flow.install.retry": "Choose another model or try again.",
231
+ "flow.install.back": "Choose another model",
232
+ "flow.install.start": "Download and start",
233
+ "flow.install.busy": "Preparing model...",
234
+ "flow.install.enter": "Enter Brain",
235
+ "flow.install.local": "Everything runs on this computer.",
236
+ },
237
+ };
238
+
239
+ export function t(language: Language, key: string, values?: Record<string, string | number>) {
240
+ let text = COPY[language]?.[key] || COPY.ko[key] || key;
241
+ if (values) {
242
+ for (const [name, value] of Object.entries(values)) {
243
+ text = text.replaceAll(`{${name}}`, String(value));
244
+ }
245
+ }
246
+ return text;
247
+ }
@@ -19,7 +19,7 @@ const tabs: Array<{ id: SystemTab; label: string }> = [
19
19
  { id: "activity", label: "History" },
20
20
  { id: "network", label: "Devices" },
21
21
  { id: "settings", label: "Preferences" },
22
- { id: "admin", label: "Admin" },
22
+ { id: "admin", label: "Admin Console" },
23
23
  ];
24
24
 
25
25
  export function SystemPage({ initialTab }: { initialTab?: string }) {
@@ -1,4 +1,5 @@
1
1
  import { create } from "zustand";
2
+ import type { Language } from "@/i18n";
2
3
 
3
4
  export type Theme = "dark" | "light";
4
5
  export type WorkspaceMode = "basic" | "advanced" | "admin";
@@ -8,10 +9,12 @@ type AppState = {
8
9
  mode: WorkspaceMode;
9
10
  workspaceId: string | null;
10
11
  apiBase: string | null;
12
+ language: Language;
11
13
  setTheme: (theme: Theme) => void;
12
14
  setMode: (mode: WorkspaceMode) => void;
13
15
  setWorkspaceId: (workspaceId: string | null) => void;
14
16
  setApiBase: (apiBase: string | null) => void;
17
+ setLanguage: (language: Language) => void;
15
18
  };
16
19
 
17
20
  function readTheme(): Theme {
@@ -37,11 +40,21 @@ function readWorkspaceId(): string | null {
37
40
  return null;
38
41
  }
39
42
 
43
+ function readLanguage(): Language {
44
+ try {
45
+ const saved = localStorage.getItem("lattice.language");
46
+ if (saved === "ko" || saved === "en") return saved;
47
+ } catch {}
48
+ const browser = typeof navigator !== "undefined" ? navigator.language.toLowerCase() : "";
49
+ return browser.startsWith("ko") ? "ko" : "en";
50
+ }
51
+
40
52
  export const useAppStore = create<AppState>((set) => ({
41
53
  theme: readTheme(),
42
54
  mode: readMode(),
43
55
  workspaceId: readWorkspaceId(),
44
56
  apiBase: null,
57
+ language: readLanguage(),
45
58
  setTheme: (theme) => {
46
59
  document.documentElement.dataset.theme = theme;
47
60
  try { localStorage.setItem("lattice.theme", theme); } catch {}
@@ -58,4 +71,9 @@ export const useAppStore = create<AppState>((set) => ({
58
71
  set({ workspaceId });
59
72
  },
60
73
  setApiBase: (apiBase) => set({ apiBase }),
74
+ setLanguage: (language) => {
75
+ document.documentElement.lang = language === "ko" ? "ko" : "en";
76
+ try { localStorage.setItem("lattice.language", language); } catch {}
77
+ set({ language });
78
+ },
61
79
  }));
@@ -1290,6 +1290,38 @@ body {
1290
1290
  outline: none;
1291
1291
  }
1292
1292
 
1293
+ .language-switcher {
1294
+ display: inline-flex;
1295
+ align-items: center;
1296
+ gap: 0.25rem;
1297
+ border: 1px solid hsl(var(--border) / 0.58);
1298
+ border-radius: 999px;
1299
+ background: hsl(var(--surface-glass));
1300
+ padding: 0.18rem;
1301
+ }
1302
+
1303
+ .language-switcher button {
1304
+ min-height: 1.65rem;
1305
+ border: 0;
1306
+ border-radius: 999px;
1307
+ background: transparent;
1308
+ color: hsl(var(--fg-muted));
1309
+ padding: 0 0.56rem;
1310
+ font-size: 0.68rem;
1311
+ font-weight: 820;
1312
+ letter-spacing: 0;
1313
+ cursor: pointer;
1314
+ }
1315
+
1316
+ .language-switcher button.is-active {
1317
+ background: hsl(var(--brain-core) / 0.14);
1318
+ color: hsl(var(--fg));
1319
+ }
1320
+
1321
+ .language-switcher.compact {
1322
+ flex: 0 0 auto;
1323
+ }
1324
+
1293
1325
  .brain-stream {
1294
1326
  flex: 1 1 auto;
1295
1327
  overflow-y: auto;
@@ -2854,6 +2886,10 @@ body {
2854
2886
  text-align: center;
2855
2887
  }
2856
2888
 
2889
+ .ritual-language {
2890
+ margin: 0 auto 1rem;
2891
+ }
2892
+
2857
2893
  .ritual-brain {
2858
2894
  margin: 0 auto 1.75rem;
2859
2895
  }
@@ -26,7 +26,7 @@ from .storage import (
26
26
  storage_from_env,
27
27
  )
28
28
 
29
- __version__ = "4.7.2"
29
+ __version__ = "5.1.0"
30
30
 
31
31
  __all__ = [
32
32
  "AgentRuntime",
@@ -75,8 +75,8 @@ def _sqlite_siblings(db_path: Path) -> tuple[Path, Path, Path]:
75
75
  def _restore_sibling(path: Path, backup: Path) -> None:
76
76
  if backup.exists():
77
77
  shutil.copy2(backup, path)
78
- elif path.exists():
79
- path.unlink()
78
+ else:
79
+ path.unlink(missing_ok=True)
80
80
 
81
81
 
82
82
  def _replace_sqlite_atomically(src: Path, dest: Path, backup_dir: Path) -> None:
@@ -85,14 +85,18 @@ def _replace_sqlite_atomically(src: Path, dest: Path, backup_dir: Path) -> None:
85
85
  shutil.copyfile(src, tmp)
86
86
  backups: dict[Path, Path] = {}
87
87
  try:
88
+ # -wal/-shm are transient: another live connection can checkpoint and
89
+ # remove them between exists() and the copy/unlink. Treat a vanished
90
+ # sibling as "nothing to preserve" instead of crashing the restore.
88
91
  for sibling in _sqlite_siblings(dest):
89
- if sibling.exists():
90
- backup = backup_dir / sibling.name
92
+ backup = backup_dir / sibling.name
93
+ try:
91
94
  shutil.copy2(sibling, backup)
92
- backups[sibling] = backup
95
+ except FileNotFoundError:
96
+ continue
97
+ backups[sibling] = backup
93
98
  for sibling in _sqlite_siblings(dest)[1:]:
94
- if sibling.exists():
95
- sibling.unlink()
99
+ sibling.unlink(missing_ok=True)
96
100
  os.replace(tmp, dest)
97
101
  except Exception:
98
102
  if tmp.exists():
@@ -14,7 +14,7 @@ from datetime import datetime
14
14
  from typing import Any, Callable, Dict, List, Optional
15
15
 
16
16
 
17
- MULTI_AGENT_VERSION = "4.7.2"
17
+ MULTI_AGENT_VERSION = "5.1.0"
18
18
 
19
19
  AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
20
20
  CORE_PIPELINE = ("planner", "executor", "reviewer")
@@ -1,3 +1,3 @@
1
1
  """Lattice AI - modular server package."""
2
2
 
3
- __version__ = "4.7.2"
3
+ __version__ = "5.1.0"
@@ -27,7 +27,7 @@ from latticeai.core.document_generator import DocumentGenerationSession, detect_
27
27
  from lattice_brain.runtime.hooks import dispatch_tool
28
28
  from latticeai.services.app_context import AppContext
29
29
  from latticeai.services.tool_dispatch import build_agent_runtime, collect_created_files
30
- from tools import AGENT_ROOT, ToolError, ensure_agent_root, execute_tool, knowledge_save, local_read, network_status
30
+ from tools import AGENT_ROOT, ToolError, ensure_agent_root, execute_tool, knowledge_save, network_status
31
31
 
32
32
  class ChatRequest(BaseModel):
33
33
  message: str
@@ -42,6 +42,7 @@ class ChatRequest(BaseModel):
42
42
  user_email: Optional[str] = None
43
43
  user_nickname: Optional[str] = None
44
44
  image_data: Optional[str] = None
45
+ allow_file_context: bool = False
45
46
 
46
47
 
47
48
  class AgentRequest(BaseModel):
@@ -450,16 +451,23 @@ def create_chat_router(context: AppContext) -> APIRouter:
450
451
 
451
452
  if CONFIG.auto_read_chat_paths:
452
453
  _file_path_re = re.compile(r'(?:^|[\s\'\"(])((~|/[\w.])[^\s\'")\]]*)', re.MULTILINE)
453
- for _m in _file_path_re.finditer(req.message or ""):
454
- _fpath = _m.group(1).strip()
455
- try:
456
- _result = local_read(_fpath)
457
- _fcontent = _result.get("content", "")
458
- if _fcontent:
459
- context += f"\n\n[FILE: {_fpath}]\n```\n{_fcontent[:6000]}\n```"
460
- print(f"📂 Auto-injected file context: {_fpath}")
461
- except Exception:
462
- pass
454
+ requested_paths = [_m.group(1).strip() for _m in _file_path_re.finditer(req.message or "")]
455
+ if requested_paths:
456
+ append_audit_event(
457
+ "auto_file_context_blocked",
458
+ user_email=effective_email,
459
+ path_count=len(requested_paths),
460
+ allow_file_context=req.allow_file_context,
461
+ reason="local file context requires an explicit approved file/tool flow",
462
+ )
463
+ if req.allow_file_context:
464
+ raise HTTPException(
465
+ status_code=400,
466
+ detail=(
467
+ "Automatic local file reads are disabled in chat. "
468
+ "Attach the file, upload it, or use an approved local-file tool flow."
469
+ ),
470
+ )
463
471
 
464
472
  trace_seed = CHAT_SERVICE.build_graph_trace(
465
473
  req.message,
@@ -77,6 +77,7 @@ class SetApiKeyRequest(BaseModel):
77
77
 
78
78
  class PullModelRequest(BaseModel):
79
79
  model: str
80
+ allow_download: bool = False
80
81
 
81
82
 
82
83
  class PrepareModelRequest(BaseModel):
@@ -291,6 +292,11 @@ def create_models_router(
291
292
  @router.post("/engines/pull-model")
292
293
  async def pull_ollama_model(req: PullModelRequest, request: Request):
293
294
  require_user(request)
295
+ if not req.allow_download:
296
+ raise HTTPException(
297
+ status_code=403,
298
+ detail="Model downloads require explicit user consent (allow_download=true).",
299
+ )
294
300
  model_ref = normalize_local_model_request(req.model, None)
295
301
  if not model_ref:
296
302
  raise HTTPException(status_code=400, detail="모델 식별자가 비어 있습니다.")
@@ -26,7 +26,6 @@ from __future__ import annotations
26
26
  import io
27
27
  import json
28
28
  import logging
29
- import re
30
29
  from collections import defaultdict
31
30
  from datetime import datetime
32
31
  from typing import Any, Callable, Dict, List, Optional
@@ -36,6 +35,7 @@ from fastapi.responses import Response
36
35
  from pydantic import BaseModel
37
36
 
38
37
  from ..core import timezones
38
+ from ..core.security import SECRET_TEXT_PATTERNS, redact_secret_text
39
39
 
40
40
  logger = logging.getLogger(__name__)
41
41
 
@@ -43,23 +43,11 @@ logger = logging.getLogger(__name__)
43
43
  # ── Hard secret patterns ──────────────────────────────────────────────────────
44
44
  # 이 값들은 관리자도 절대 원문으로 보면 안 된다.
45
45
 
46
- HARD_SECRET_PATTERNS = [
47
- re.compile(r"(?i)(api[_-]?key|secret|access[_-]?token|password|passwd|bearer)\s*[:=]\s*\S+"),
48
- re.compile(r"sk-[A-Za-z0-9]{20,}"),
49
- re.compile(r"ghp_[A-Za-z0-9]{30,}"),
50
- re.compile(r"xox[baprs]-[A-Za-z0-9-]{10,}"),
51
- re.compile(r"AKIA[0-9A-Z]{16}"),
52
- re.compile(r"-----BEGIN [A-Z ]+PRIVATE KEY-----[\s\S]+?-----END [A-Z ]+PRIVATE KEY-----"),
53
- ]
46
+ HARD_SECRET_PATTERNS = SECRET_TEXT_PATTERNS
54
47
 
55
48
 
56
49
  def redact_hard_secrets(text: str) -> str:
57
- if not text:
58
- return text or ""
59
- out = text
60
- for pat in HARD_SECRET_PATTERNS:
61
- out = pat.sub("[REDACTED_SECRET]", out)
62
- return out
50
+ return redact_secret_text(text)
63
51
 
64
52
 
65
53
  def soft_mask(text: str, *, keep: int = 4) -> str:
@@ -12,11 +12,27 @@ from fastapi.responses import FileResponse, HTMLResponse
12
12
 
13
13
  from latticeai.api.ui_redirects import app_redirect
14
14
 
15
+ PRODUCTION_CSP = (
16
+ "default-src 'self'; "
17
+ "script-src 'self'; "
18
+ "style-src 'self' 'unsafe-inline'; "
19
+ "img-src 'self' data: blob: http://127.0.0.1:*; "
20
+ "font-src 'self' data:; "
21
+ "connect-src 'self' http://127.0.0.1:* ws://127.0.0.1:*; "
22
+ "frame-src 'none'; "
23
+ "object-src 'none'; "
24
+ "base-uri 'none'; "
25
+ "form-action 'self'; "
26
+ "frame-ancestors 'none'"
27
+ )
28
+
29
+
15
30
  def ui_file_response(path: Path) -> FileResponse:
16
31
  response = FileResponse(path)
17
32
  response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
18
33
  response.headers["Pragma"] = "no-cache"
19
34
  response.headers["Expires"] = "0"
35
+ response.headers["Content-Security-Policy"] = PRODUCTION_CSP
20
36
  return response
21
37
 
22
38
  @dataclass(frozen=True)