ltcai 5.0.0 → 5.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.
- package/README.md +75 -55
- package/docs/CHANGELOG.md +96 -2354
- package/docs/TRUST_MODEL.md +66 -0
- package/docs/V4_1_VALIDATION_REPORT.md +1 -1
- package/docs/V4_3_PRODUCT_HARDENING_REPORT.md +2 -2
- package/docs/V4_5_1_VALIDATION_REPORT.md +2 -1
- package/docs/WHY_LATTICE.md +54 -0
- package/frontend/src/App.tsx +1 -1
- package/frontend/src/components/primitives.tsx +1 -1
- package/frontend/src/i18n.ts +6 -4
- package/frontend/src/pages/Library.tsx +29 -4
- package/frontend/src/pages/System.tsx +1 -1
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/portability.py +11 -7
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/chat.py +19 -11
- package/latticeai/api/marketplace.py +2 -2
- package/latticeai/api/models.py +26 -4
- package/latticeai/api/security_dashboard.py +3 -15
- package/latticeai/api/static_routes.py +16 -0
- package/latticeai/app_factory.py +114 -40
- package/latticeai/core/audit.py +3 -1
- package/latticeai/core/builtin_hooks.py +7 -9
- package/latticeai/core/logging_safety.py +5 -21
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/security.py +67 -9
- package/latticeai/core/workspace_os.py +18 -4
- package/latticeai/services/model_capability_registry.py +483 -0
- package/latticeai/services/model_catalog.py +99 -96
- package/latticeai/services/model_recommendation.py +12 -1
- package/package.json +2 -2
- package/scripts/clean_release_artifacts.mjs +16 -1
- package/scripts/com.pts.claudecode.discord.plist +31 -0
- package/scripts/pts-claudecode-discord-bridge.mjs +189 -0
- package/scripts/run_integration_tests.mjs +91 -0
- package/scripts/start-pts-claudecode-discord.sh +51 -0
- package/scripts/verify_hf_model_registry.py +308 -0
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +3 -2
- package/static/app/asset-manifest.json +5 -5
- package/static/app/assets/index-CQmHhk8Q.css +2 -0
- package/static/app/assets/{index-FR1UZkCD.js → index-DsnfomFs.js} +2 -2
- package/static/app/assets/index-DsnfomFs.js.map +1 -0
- package/static/app/index.html +2 -2
- package/static/app/assets/index-DuYYT2oh.css +0 -2
- package/static/app/assets/index-FR1UZkCD.js.map +0 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Lattice AI Trust Model
|
|
2
|
+
|
|
3
|
+
Lattice AI's trust model is local-first, opt-in for external communication, and
|
|
4
|
+
honest when something is unavailable.
|
|
5
|
+
|
|
6
|
+
## Local By Default
|
|
7
|
+
|
|
8
|
+
By default, Lattice AI binds the API to `127.0.0.1`, stores Brain data under the
|
|
9
|
+
local data directory, and does not send prompts, documents, graph content, or
|
|
10
|
+
archives to Lattice-owned servers.
|
|
11
|
+
|
|
12
|
+
Local data includes:
|
|
13
|
+
|
|
14
|
+
- local profile and sessions;
|
|
15
|
+
- conversations and memory records;
|
|
16
|
+
- Knowledge Graph nodes, edges, provenance, and search indexes;
|
|
17
|
+
- uploaded document blobs;
|
|
18
|
+
- audit and admin operation logs;
|
|
19
|
+
- backups and encrypted `.latticebrain` archives.
|
|
20
|
+
|
|
21
|
+
## Explicit External Paths
|
|
22
|
+
|
|
23
|
+
Some features can contact third parties, but they require explicit user/admin
|
|
24
|
+
action or configuration:
|
|
25
|
+
|
|
26
|
+
- model downloads from model registries;
|
|
27
|
+
- cloud model API calls after keys are configured and a cloud model is chosen;
|
|
28
|
+
- Telegram bridge after the integration is enabled;
|
|
29
|
+
- Brain Network peer actions after pairing/initiating network flows;
|
|
30
|
+
- Docker/Postgres setup after opt-in scale configuration;
|
|
31
|
+
- update checks only when update checking is enabled;
|
|
32
|
+
- remote marketplace/registry refreshes only through explicit user actions.
|
|
33
|
+
|
|
34
|
+
Token presence alone must not start external communication.
|
|
35
|
+
|
|
36
|
+
## Consent And Honesty Gates
|
|
37
|
+
|
|
38
|
+
Lattice AI should fail closed or report unavailable state for:
|
|
39
|
+
|
|
40
|
+
- no model loaded;
|
|
41
|
+
- local model not installed;
|
|
42
|
+
- installed model not loaded;
|
|
43
|
+
- missing cloud key;
|
|
44
|
+
- deterministic/model-free preview;
|
|
45
|
+
- dry-run versus real execution;
|
|
46
|
+
- no graph/context evidence available;
|
|
47
|
+
- unavailable external integration;
|
|
48
|
+
- wrong archive passphrase;
|
|
49
|
+
- archive path traversal or tampering.
|
|
50
|
+
|
|
51
|
+
## Admin Boundary
|
|
52
|
+
|
|
53
|
+
The normal user product is Brain Chat, memory, topics, relationships, graph
|
|
54
|
+
exploration, model state, and Brain ownership. Admin Console is for users,
|
|
55
|
+
roles, audit logs, security events, retention, and operations. Admin visibility
|
|
56
|
+
does not mean secrets should appear in clear text.
|
|
57
|
+
|
|
58
|
+
## Known Limitations
|
|
59
|
+
|
|
60
|
+
- Local files are only as protected as the user's machine, account, backups, and
|
|
61
|
+
disk encryption.
|
|
62
|
+
- Cloud model prompts follow the selected provider's policy.
|
|
63
|
+
- A local admin can inspect local files and process memory outside Lattice AI.
|
|
64
|
+
- Marketplace and model registries are third-party services when explicitly
|
|
65
|
+
contacted.
|
|
66
|
+
|
|
@@ -43,5 +43,5 @@ FastAPI backend compatibility, release artifacts, and installed-wheel smoke.
|
|
|
43
43
|
`block v0.1.6`; the current Tauri 2.0 build passes on Rust 1.96.
|
|
44
44
|
- Release artifact validation warns that historical artifacts remain in
|
|
45
45
|
`dist/`; this is expected and reinforces the rule to upload only exact
|
|
46
|
-
v4.1.0 filenames, never `dist
|
|
46
|
+
v4.1.0 filenames, never a wildcard from the `dist` directory.
|
|
47
47
|
- No external registry publish was performed.
|
|
@@ -44,8 +44,8 @@ architecture.
|
|
|
44
44
|
- Release artifact validation now checks the exact Tauri DMG path.
|
|
45
45
|
- Release artifact build script cleans only target-version outputs before
|
|
46
46
|
rebuilding.
|
|
47
|
-
- Historical artifacts remain visible so
|
|
48
|
-
detectable.
|
|
47
|
+
- Historical artifacts remain visible so wildcard upload mistakes from `dist`
|
|
48
|
+
are still detectable.
|
|
49
49
|
|
|
50
50
|
## Registry Policy
|
|
51
51
|
|
|
@@ -51,4 +51,5 @@ Expected exact-version artifacts:
|
|
|
51
51
|
|
|
52
52
|
`npm run release:validate` confirmed all exact-version RC artifacts are present.
|
|
53
53
|
It also warned that historical artifacts remain in `dist/`, so publish commands
|
|
54
|
-
must continue to use explicit v4.5.1 filenames rather than a
|
|
54
|
+
must continue to use explicit v4.5.1 filenames rather than a wildcard from the
|
|
55
|
+
`dist` directory.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Why Lattice AI Exists
|
|
2
|
+
|
|
3
|
+
**Your private AI memory layer. Keep your knowledge. Switch any model.**
|
|
4
|
+
|
|
5
|
+
**모델은 바꿔도, 내 지식은 남는 로컬 AI 브레인.**
|
|
6
|
+
|
|
7
|
+
AI models change quickly. A model you use today may be replaced next month, and
|
|
8
|
+
the conversation history, project context, decisions, and sources you built
|
|
9
|
+
around it can become scattered across tools. Lattice AI exists so the durable
|
|
10
|
+
asset is not the model. The durable asset is your Brain.
|
|
11
|
+
|
|
12
|
+
## The Problem
|
|
13
|
+
|
|
14
|
+
Most AI products begin with a model and treat your context as temporary prompt
|
|
15
|
+
material. That works for short questions, but it fails for long-running work:
|
|
16
|
+
|
|
17
|
+
- project decisions disappear into old chats;
|
|
18
|
+
- documents and notes are disconnected from conversations;
|
|
19
|
+
- switching models often means losing useful context;
|
|
20
|
+
- graph or database tools expose implementation details before user value;
|
|
21
|
+
- cloud-only products make it hard to inspect, back up, or move your knowledge.
|
|
22
|
+
|
|
23
|
+
## The Lattice Answer
|
|
24
|
+
|
|
25
|
+
Lattice AI is a local-first private AI memory layer. It keeps conversations,
|
|
26
|
+
documents, decisions, relationships, and project history in a Brain that belongs
|
|
27
|
+
to the user. Models can be local, cloud, current, or future. The Brain remains.
|
|
28
|
+
|
|
29
|
+
The graph is real, but it is not the product identity. Users start with Brain
|
|
30
|
+
Chat, memory, topics, relationships, ownership, backup, and graph exploration.
|
|
31
|
+
Advanced admin logs, roles, hooks, workflows, Telegram, Brain Network, Docker,
|
|
32
|
+
Postgres, and plugin details stay outside the normal user flow.
|
|
33
|
+
|
|
34
|
+
## Practical Reasons To Use It
|
|
35
|
+
|
|
36
|
+
- Ask what the team decided last week and see the source.
|
|
37
|
+
- Drop in documents and build a searchable personal memory.
|
|
38
|
+
- Prepare for a meeting from past notes, project decisions, and files.
|
|
39
|
+
- Preserve context when moving from one model to another.
|
|
40
|
+
- Export or back up the Brain as an encrypted `.latticebrain` archive.
|
|
41
|
+
- Use Korean or English without changing the underlying Brain.
|
|
42
|
+
- Avoid fake answers when no model or evidence is available.
|
|
43
|
+
|
|
44
|
+
## What Lattice AI Is Not
|
|
45
|
+
|
|
46
|
+
- Not a hosted SaaS by default.
|
|
47
|
+
- Not just a model launcher.
|
|
48
|
+
- Not just a graph viewer.
|
|
49
|
+
- Not a generic dashboard.
|
|
50
|
+
- Not a note-taking clone.
|
|
51
|
+
- Not a ChatGPT or Claude clone.
|
|
52
|
+
|
|
53
|
+
Lattice AI is for people who want their knowledge to survive model changes.
|
|
54
|
+
|
package/frontend/src/App.tsx
CHANGED
|
@@ -245,7 +245,7 @@ function BrainHome({
|
|
|
245
245
|
if (result.error) {
|
|
246
246
|
setMessages((items) => {
|
|
247
247
|
const next = [...items];
|
|
248
|
-
next[next.length - 1] = { role: "assistant", content:
|
|
248
|
+
next[next.length - 1] = { role: "assistant", content: `${t(language, "brain.unavailable")}: ${result.error}` };
|
|
249
249
|
return next;
|
|
250
250
|
});
|
|
251
251
|
} else {
|
|
@@ -316,7 +316,7 @@ export function ModeGate({
|
|
|
316
316
|
<div className="text-lg font-semibold">{title}</div>
|
|
317
317
|
<p className="mt-1 max-w-2xl text-sm leading-6 text-muted-foreground">{detail}</p>
|
|
318
318
|
</div>
|
|
319
|
-
<Button onClick={() => setMode(target)}>{target === "admin" ? "Switch to Admin" : "Switch to Advanced"}</Button>
|
|
319
|
+
<Button onClick={() => setMode(target)}>{target === "admin" ? "Switch to Admin Console" : "Switch to Advanced"}</Button>
|
|
320
320
|
</CardContent>
|
|
321
321
|
</Card>
|
|
322
322
|
);
|
package/frontend/src/i18n.ts
CHANGED
|
@@ -27,7 +27,7 @@ export const COPY: Record<Language, TextMap> = {
|
|
|
27
27
|
"brain.local": "로컬 우선",
|
|
28
28
|
"brain.portable": "이동 가능",
|
|
29
29
|
"brain.private": "개인 소유",
|
|
30
|
-
"brain.admin": "관리자",
|
|
30
|
+
"brain.admin": "관리자 콘솔",
|
|
31
31
|
"brain.empty.kicker": "내 오래가는 기억",
|
|
32
32
|
"brain.empty.title": "잊으면 안 되는 일부터 말해 주세요.",
|
|
33
33
|
"brain.empty.body": "문서, 대화, 프로젝트, 결정이 Brain에 쌓이고 나중에 주제와 관계로 다시 보입니다.",
|
|
@@ -36,6 +36,7 @@ export const COPY: Record<Language, TextMap> = {
|
|
|
36
36
|
"brain.prompt.plan": "이 프로젝트 맥락을 계획으로 바꿔줘: ",
|
|
37
37
|
"brain.placeholder": "Brain에게 말하기...",
|
|
38
38
|
"brain.image": "이미지",
|
|
39
|
+
"brain.unavailable": "지금은 답할 수 없음",
|
|
39
40
|
"brain.imageAttached": "이미지 첨부됨",
|
|
40
41
|
"brain.send": "보내기",
|
|
41
42
|
"brain.saved": "기억에 저장됨 · 연결된 주제 {topics}개 · 관련 기억 {memories}개",
|
|
@@ -74,7 +75,7 @@ export const COPY: Record<Language, TextMap> = {
|
|
|
74
75
|
"admin.kicker": "분리된 관리자 작업공간",
|
|
75
76
|
"admin.title": "Admin Console",
|
|
76
77
|
"admin.body": "사용자, 로그, 보안, Brain 상태는 일반 사용자 화면과 분리됩니다.",
|
|
77
|
-
"flow.shell": "내 Brain
|
|
78
|
+
"flow.shell": "내 로컬 Brain 만들기",
|
|
78
79
|
"flow.login.title": "내 Brain을 시작합니다.",
|
|
79
80
|
"flow.login.body": "모델은 바뀔 수 있지만, 내 문서와 대화, 결정, 기억은 사라지면 안 됩니다. Lattice는 이 지식을 내가 소유하는 개인 Brain으로 모읍니다.",
|
|
80
81
|
"flow.name": "이름",
|
|
@@ -139,7 +140,7 @@ export const COPY: Record<Language, TextMap> = {
|
|
|
139
140
|
"brain.local": "Local-first",
|
|
140
141
|
"brain.portable": "Portable",
|
|
141
142
|
"brain.private": "Private",
|
|
142
|
-
"brain.admin": "Admin",
|
|
143
|
+
"brain.admin": "Admin Console",
|
|
143
144
|
"brain.empty.kicker": "Durable memory",
|
|
144
145
|
"brain.empty.title": "Start with what should not be forgotten.",
|
|
145
146
|
"brain.empty.body": "Documents, conversations, projects, and decisions accumulate in your Brain, then reappear as topics and relationships.",
|
|
@@ -148,6 +149,7 @@ export const COPY: Record<Language, TextMap> = {
|
|
|
148
149
|
"brain.prompt.plan": "Turn this project context into a plan: ",
|
|
149
150
|
"brain.placeholder": "Talk to your Brain...",
|
|
150
151
|
"brain.image": "Image",
|
|
152
|
+
"brain.unavailable": "Unavailable",
|
|
151
153
|
"brain.imageAttached": "Image attached",
|
|
152
154
|
"brain.send": "Send",
|
|
153
155
|
"brain.saved": "Saved to memory · {topics} linked topics · {memories} related memories",
|
|
@@ -186,7 +188,7 @@ export const COPY: Record<Language, TextMap> = {
|
|
|
186
188
|
"admin.kicker": "Separate admin workspace",
|
|
187
189
|
"admin.title": "Admin Console",
|
|
188
190
|
"admin.body": "Users, logs, security, and Brain health stay out of the normal user experience.",
|
|
189
|
-
"flow.shell": "
|
|
191
|
+
"flow.shell": "Create your local Brain",
|
|
190
192
|
"flow.login.title": "Start your Brain.",
|
|
191
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.",
|
|
192
194
|
"flow.name": "Name",
|
|
@@ -103,7 +103,7 @@ function ModelsPanel() {
|
|
|
103
103
|
return (
|
|
104
104
|
<div className="grid gap-4 xl:grid-cols-[1.2fr_0.8fr]">
|
|
105
105
|
<div className="space-y-4">
|
|
106
|
-
<DataPanel title="Guided model setup" description="
|
|
106
|
+
<DataPanel title="Guided model setup" description="5.2 registry: hardware-fit multimodal models with HF verification, download/load strategies, and clear RAM notes. Analyze, consent, download only on click." result={recs.data}>
|
|
107
107
|
{(data) => {
|
|
108
108
|
const recommendation = (data as Record<string, unknown>).recommendations as Record<string, unknown> | undefined;
|
|
109
109
|
const profile = (data as Record<string, unknown>).profile as Record<string, unknown> | undefined;
|
|
@@ -160,6 +160,13 @@ function ModelsPanel() {
|
|
|
160
160
|
const loadId = String(model.recommended_load_id || id);
|
|
161
161
|
const engine = String(model.recommended_engine || model.engine || "");
|
|
162
162
|
const recommendation = recommendationById.get(id) || recommendationById.get(loadId) || {};
|
|
163
|
+
const modelVerification = asRecord(model.verification);
|
|
164
|
+
const recommendationVerification = asRecord(recommendation.verification);
|
|
165
|
+
const modelHardware = asRecord(model.hardware);
|
|
166
|
+
const recommendationHardware = asRecord(recommendation.hardware);
|
|
167
|
+
const hardwareNote = modelHardware.notes || recommendationHardware.notes || (modelHardware.recommended_ram_gb ? `~${modelHardware.recommended_ram_gb}GB RAM rec` : "");
|
|
168
|
+
const safetyNotes = model.safety_notes || recommendation.safety_notes;
|
|
169
|
+
const licenseText = model.license || recommendation.license;
|
|
163
170
|
const compatibility = (model.runtime_compatibility || recommendation.runtime_compatibility || {}) as Record<string, unknown>;
|
|
164
171
|
const fallbackAvailable = String(compatibility.status || "") === "fallback_available";
|
|
165
172
|
const unsupported = model.load_status === "unsupported" || compatibility.supported === false;
|
|
@@ -176,7 +183,9 @@ function ModelsPanel() {
|
|
|
176
183
|
<div className="min-w-0">
|
|
177
184
|
<div className="flex flex-wrap items-center gap-2">
|
|
178
185
|
<div className="text-base font-semibold">{String(model.name || id)}</div>
|
|
179
|
-
{topPick?.id === id ? <Badge variant="success">recommended</Badge> : null}
|
|
186
|
+
{topPick?.id === id || model.recommended_default ? <Badge variant="success">recommended</Badge> : null}
|
|
187
|
+
{String(model.modality || recommendation.modality || "").includes("multi") || String(model.modality || "") === "multimodal" ? <Badge variant="muted">multimodal</Badge> : null}
|
|
188
|
+
{modelVerification.verified || recommendationVerification.verified ? <Badge variant="success" title="HF verified (config+tokenizer present)">✓ HF</Badge> : null}
|
|
180
189
|
</div>
|
|
181
190
|
<div className="mt-1 text-sm text-muted-foreground">
|
|
182
191
|
{mode === "basic"
|
|
@@ -186,6 +195,11 @@ function ModelsPanel() {
|
|
|
186
195
|
].filter(Boolean).map(String).join(" · ")
|
|
187
196
|
: [model.family || recommendation.family || "local", model.size || recommendation.size].filter(Boolean).map(String).join(" · ")}
|
|
188
197
|
</div>
|
|
198
|
+
{(model.hardware || recommendation.hardware) ? (
|
|
199
|
+
<div className="mt-1 text-[11px] text-muted-foreground/80">
|
|
200
|
+
{String(hardwareNote)}
|
|
201
|
+
</div>
|
|
202
|
+
) : null}
|
|
189
203
|
{unsupported ? (
|
|
190
204
|
<div className="mt-3 rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-sm">
|
|
191
205
|
<div className="font-medium">{mode === "basic" ? "Needs attention before loading" : actionLabel}</div>
|
|
@@ -198,8 +212,15 @@ function ModelsPanel() {
|
|
|
198
212
|
</div>
|
|
199
213
|
) : !loaded && !loadAvailable ? <div className="mt-1 text-xs text-muted-foreground">{unavailableReason}</div> : null}
|
|
200
214
|
{mode !== "basic" ? (
|
|
201
|
-
<div className="mt-2 text-xs text-muted-foreground">
|
|
202
|
-
|
|
215
|
+
<div className="mt-2 space-y-1 text-xs text-muted-foreground">
|
|
216
|
+
<div>
|
|
217
|
+
{runtimeLabel} · {loadId}
|
|
218
|
+
{model.load_strategy || recommendation.load_strategy ? ` · ${String(model.load_strategy || recommendation.load_strategy)}` : ""}
|
|
219
|
+
{modelVerification.notes ? ` · ${String(modelVerification.notes).slice(0,60)}` : ""}
|
|
220
|
+
</div>
|
|
221
|
+
{safetyNotes || licenseText ? (
|
|
222
|
+
<div>{[licenseText ? `License: ${String(licenseText)}` : "", safetyNotes ? String(safetyNotes) : ""].filter(Boolean).join(" · ")}</div>
|
|
223
|
+
) : null}
|
|
203
224
|
</div>
|
|
204
225
|
) : null}
|
|
205
226
|
{unsupported || fallbackAvailable ? <AlternativeModels compatibility={compatibility} /> : null}
|
|
@@ -277,6 +298,10 @@ function ModelRecovery({ error }: { error: Record<string, unknown> }) {
|
|
|
277
298
|
);
|
|
278
299
|
}
|
|
279
300
|
|
|
301
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
302
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
303
|
+
}
|
|
304
|
+
|
|
280
305
|
function SkillsPanel() {
|
|
281
306
|
const qc = useQueryClient();
|
|
282
307
|
const skills = useQuery({ queryKey: ["skills"], queryFn: latticeApi.skills });
|
|
@@ -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 }) {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
92
|
+
backup = backup_dir / sibling.name
|
|
93
|
+
try:
|
|
91
94
|
shutil.copy2(sibling, backup)
|
|
92
|
-
|
|
95
|
+
except FileNotFoundError:
|
|
96
|
+
continue
|
|
97
|
+
backups[sibling] = backup
|
|
93
98
|
for sibling in _sqlite_siblings(dest)[1:]:
|
|
94
|
-
|
|
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 = "5.
|
|
17
|
+
MULTI_AGENT_VERSION = "5.2.0"
|
|
18
18
|
|
|
19
19
|
AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
|
|
20
20
|
CORE_PIPELINE = ("planner", "executor", "reviewer")
|
package/latticeai/__init__.py
CHANGED
package/latticeai/api/chat.py
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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,
|
|
@@ -88,7 +88,7 @@ def create_marketplace_router(
|
|
|
88
88
|
@router.get("/marketplace/templates/registry")
|
|
89
89
|
async def template_registry(request: Request):
|
|
90
90
|
require_user(request)
|
|
91
|
-
gate_read(request)
|
|
92
|
-
return {"registry": store.list_template_registry()}
|
|
91
|
+
scope = gate_read(request)
|
|
92
|
+
return {"registry": store.list_template_registry(workspace_id=scope)}
|
|
93
93
|
|
|
94
94
|
return router
|
package/latticeai/api/models.py
CHANGED
|
@@ -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="모델 식별자가 비어 있습니다.")
|
|
@@ -419,6 +425,12 @@ def create_models_router(
|
|
|
419
425
|
loaded_ids=_router.loaded_model_ids,
|
|
420
426
|
current_id=_router.current_model_id,
|
|
421
427
|
)
|
|
428
|
+
# 5.2.0: surface structured registry info (verified status, hf, hardware, strategies) for UX
|
|
429
|
+
try:
|
|
430
|
+
from latticeai.services.model_catalog import get_verified_models
|
|
431
|
+
verified = get_verified_models()
|
|
432
|
+
except Exception:
|
|
433
|
+
verified = []
|
|
422
434
|
return {
|
|
423
435
|
"recommended": recommended,
|
|
424
436
|
"cloud": _router.detected_cloud_models(),
|
|
@@ -427,6 +439,12 @@ def create_models_router(
|
|
|
427
439
|
"current": _router.current_model_id,
|
|
428
440
|
"compat_profiles": _list_compat_profiles(),
|
|
429
441
|
"vision": _vision_capability(_router.current_model_id, engines),
|
|
442
|
+
# 5.2+ transparent model capability registry
|
|
443
|
+
"registry": {
|
|
444
|
+
"version": "5.2.0",
|
|
445
|
+
"verified_count": len(verified),
|
|
446
|
+
"verified": verified[:12], # compact; full via /models/recommendations or future dedicated
|
|
447
|
+
},
|
|
430
448
|
}
|
|
431
449
|
|
|
432
450
|
@router.get("/models/compat-profiles")
|
|
@@ -488,9 +506,8 @@ def create_models_router(
|
|
|
488
506
|
async def model_recommendations(request: Request, engine: str = "local_mlx"):
|
|
489
507
|
"""Hardware-aware tri-state model recommendation for this machine.
|
|
490
508
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
grouped by family. Used by the onboarding and model-picker UIs.
|
|
509
|
+
5.2.0: now includes rich capability fields (hf_repo_id, verification,
|
|
510
|
+
hardware, load_strategy, license, safety_notes) from the structured registry.
|
|
494
511
|
"""
|
|
495
512
|
require_user(request)
|
|
496
513
|
from auto_setup import probe as auto_setup_probe
|
|
@@ -498,6 +515,11 @@ def create_models_router(
|
|
|
498
515
|
|
|
499
516
|
profile = await asyncio.to_thread(lambda: auto_setup_probe().to_json())
|
|
500
517
|
catalog = recommend_catalog(profile, engine=engine)
|
|
501
|
-
|
|
518
|
+
try:
|
|
519
|
+
from latticeai.services.model_catalog import get_verified_models
|
|
520
|
+
reg_meta = {"version": "5.2.0", "verified_total": len(get_verified_models())}
|
|
521
|
+
except Exception:
|
|
522
|
+
reg_meta = {"version": "5.2.0"}
|
|
523
|
+
return {"profile": profile, "recommendations": catalog, "registry": reg_meta}
|
|
502
524
|
|
|
503
525
|
return router
|
|
@@ -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
|
-
|
|
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)
|