ltcai 4.3.0 → 4.3.3
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 +186 -276
- package/bin/ltcai.js +6 -2
- package/docs/CHANGELOG.md +124 -3
- package/docs/V4_3_2_DEADCODE_AUDIT_REPORT.md +174 -0
- package/docs/V4_3_2_DOCUMENTATION_CLEANUP_REPORT.md +81 -0
- package/docs/V4_3_2_GITHUB_VERCEL_CHECK_REPORT.md +75 -0
- package/docs/V4_3_2_GRAPH_UX_REPORT.md +48 -0
- package/docs/V4_3_2_INDEPENDENT_AUDIT_PACKAGE.md +209 -0
- package/docs/V4_3_2_PRODUCT_POLISH_REPORT.md +57 -0
- package/docs/V4_3_2_SELF_AUDIT_REPORT.md +63 -0
- package/docs/V4_3_2_VALIDATION_REPORT.md +97 -0
- package/docs/V4_3_3_VALIDATION_REPORT.md +46 -0
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +18 -25
- package/frontend/openapi.json +11 -1
- package/frontend/src/App.tsx +15 -1
- package/frontend/src/api/client.ts +19 -1
- package/frontend/src/api/openapi.ts +10 -0
- package/frontend/src/components/primitives.tsx +92 -10
- package/frontend/src/pages/Act.tsx +72 -9
- package/frontend/src/pages/Ask.tsx +2 -2
- package/frontend/src/pages/Brain.tsx +607 -65
- package/frontend/src/pages/Capture.tsx +11 -7
- package/frontend/src/pages/Library.tsx +12 -6
- package/frontend/src/pages/System.tsx +186 -23
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/archive.py +3 -3
- package/lattice_brain/storage/sqlite.py +15 -2
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agents.py +3 -1
- package/latticeai/api/models.py +66 -18
- package/latticeai/brain/projection.py +12 -2
- package/latticeai/brain/retrieval.py +10 -0
- package/latticeai/brain/store.py +6 -1
- package/latticeai/core/config.py +3 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/multi_agent.py +1 -1
- package/latticeai/core/product_hardening.py +2 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/services/agent_runtime.py +52 -12
- package/latticeai/services/model_runtime.py +83 -2
- package/ltcai_cli.py +14 -3
- package/package.json +5 -7
- package/requirements.txt +17 -0
- package/scripts/build_vercel_static.mjs +77 -0
- package/scripts/check_markdown_links.mjs +75 -0
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/main.rs +269 -27
- package/src-tauri/tauri.conf.json +20 -1
- package/static/app/asset-manifest.json +5 -5
- package/static/app/assets/index-CHHal8Zl.css +2 -0
- package/static/app/assets/index-pdzil9ac.js +333 -0
- package/static/app/assets/index-pdzil9ac.js.map +1 -0
- package/static/app/index.html +2 -2
- package/latticeai/api/deps.py +0 -15
- package/scripts/capture/README.md +0 -28
- package/scripts/capture/capture_enterprise.js +0 -8
- package/scripts/capture/capture_graph.js +0 -8
- package/scripts/capture/capture_onboarding.js +0 -8
- package/scripts/capture/capture_page.js +0 -43
- package/scripts/capture/capture_release_media.js +0 -125
- package/scripts/capture/capture_skills.js +0 -8
- package/scripts/capture/capture_v340.js +0 -88
- package/scripts/capture/capture_workspace.js +0 -8
- package/scripts/generate_diagrams.py +0 -512
- package/scripts/release-0.3.1.sh +0 -105
- package/scripts/take_screenshots.js +0 -69
- package/static/app/assets/index-RiJTJliG.js +0 -333
- package/static/app/assets/index-RiJTJliG.js.map +0 -1
- package/static/app/assets/index-yZswHE3d.css +0 -2
- package/static/css/tokens.3ba22e37.css +0 -260
|
@@ -2,7 +2,7 @@ import * as React from "react";
|
|
|
2
2
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
3
|
import { FolderPlus, Globe2, HardDrive, Upload } from "lucide-react";
|
|
4
4
|
import { latticeApi } from "@/api/client";
|
|
5
|
-
import { ActionButton, DataPanel, EntityList,
|
|
5
|
+
import { ActionButton, DataPanel, EntityList, OperationResult, StructuredView, Tabs } from "@/components/primitives";
|
|
6
6
|
import { Button } from "@/components/ui/button";
|
|
7
7
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
8
8
|
import { Input } from "@/components/ui/input";
|
|
@@ -59,7 +59,11 @@ function FilesPanel() {
|
|
|
59
59
|
<span className="text-sm text-muted-foreground">PDF, DOCX, XLSX, PPTX, TXT, MD, CSV according to backend policy.</span>
|
|
60
60
|
<input type="file" multiple className="sr-only" onChange={(e) => e.target.files && upload.mutate(e.target.files)} />
|
|
61
61
|
</label>
|
|
62
|
-
{upload.data ?
|
|
62
|
+
{upload.data ? (
|
|
63
|
+
<div className="mt-3 space-y-2">
|
|
64
|
+
{upload.data.map((item, index) => <OperationResult key={index} result={item} successLabel="Upload completed" />)}
|
|
65
|
+
</div>
|
|
66
|
+
) : null}
|
|
63
67
|
</CardContent>
|
|
64
68
|
</Card>
|
|
65
69
|
<DataPanel title="Uploaded documents" result={docs.data}>
|
|
@@ -88,7 +92,7 @@ function LocalPanel() {
|
|
|
88
92
|
<CardContent className="space-y-3">
|
|
89
93
|
<Input value={path} onChange={(e) => setPath(e.target.value)} placeholder="/Users/me/Documents/project" />
|
|
90
94
|
<Button disabled={!path.trim() || connect.isPending} onClick={() => connect.mutate()}>Connect and watch</Button>
|
|
91
|
-
{connect.data ? <
|
|
95
|
+
{connect.data ? <OperationResult result={connect.data} successLabel="Folder connection requested" /> : null}
|
|
92
96
|
</CardContent>
|
|
93
97
|
</Card>
|
|
94
98
|
<DataPanel title="Connected sources" result={local.data}>
|
|
@@ -107,7 +111,7 @@ function LocalPanel() {
|
|
|
107
111
|
)}
|
|
108
112
|
</DataPanel>
|
|
109
113
|
<DataPanel title="Local runtime probe" result={agent.data} className="xl:col-span-2">
|
|
110
|
-
{(data) => <
|
|
114
|
+
{(data) => <StructuredView value={data} />}
|
|
111
115
|
</DataPanel>
|
|
112
116
|
</div>
|
|
113
117
|
);
|
|
@@ -127,7 +131,7 @@ function BrowserPanel() {
|
|
|
127
131
|
<Input value={url} onChange={(e) => setUrl(e.target.value)} placeholder="https://example.com/article" />
|
|
128
132
|
<Button disabled={!url.trim() || read.isPending} onClick={() => read.mutate()}>Capture URL</Button>
|
|
129
133
|
</div>
|
|
130
|
-
{read.data ? <
|
|
134
|
+
{read.data ? <OperationResult result={read.data} successLabel="URL capture requested" /> : null}
|
|
131
135
|
</CardContent>
|
|
132
136
|
</Card>
|
|
133
137
|
);
|
|
@@ -139,10 +143,10 @@ function PipelinePanel() {
|
|
|
139
143
|
return (
|
|
140
144
|
<div className="grid gap-4 xl:grid-cols-2">
|
|
141
145
|
<DataPanel title="Index pipeline" result={index.data}>
|
|
142
|
-
{(data) => <
|
|
146
|
+
{(data) => <StructuredView value={data} />}
|
|
143
147
|
</DataPanel>
|
|
144
148
|
<DataPanel title="Graph totals" result={stats.data}>
|
|
145
|
-
{(data) => <
|
|
149
|
+
{(data) => <StructuredView value={data} />}
|
|
146
150
|
</DataPanel>
|
|
147
151
|
<Card className="xl:col-span-2">
|
|
148
152
|
<CardHeader>
|
|
@@ -2,7 +2,7 @@ import * as React from "react";
|
|
|
2
2
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
3
|
import { Boxes, Cpu, PackagePlus, Plug, Puzzle } from "lucide-react";
|
|
4
4
|
import { latticeApi } from "@/api/client";
|
|
5
|
-
import { ActionButton, DataPanel, EntityList,
|
|
5
|
+
import { ActionButton, DataPanel, EntityList, OperationResult, StructuredView, Tabs } from "@/components/primitives";
|
|
6
6
|
import { Badge } from "@/components/ui/badge";
|
|
7
7
|
import { Button } from "@/components/ui/button";
|
|
8
8
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
@@ -40,7 +40,6 @@ export function LibraryPage({ initialTab }: { initialTab?: string }) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function ModelsPanel() {
|
|
43
|
-
const qc = useQueryClient();
|
|
44
43
|
const models = useQuery({ queryKey: ["models"], queryFn: latticeApi.models });
|
|
45
44
|
const emb = useQuery({ queryKey: ["embeddings"], queryFn: latticeApi.embeddingsStatus });
|
|
46
45
|
const catalog = [
|
|
@@ -55,18 +54,25 @@ function ModelsPanel() {
|
|
|
55
54
|
{(catalog.length ? catalog : asArray<Record<string, unknown>>((data as Record<string, unknown>).loaded)).slice(0, 14).map((model, index) => {
|
|
56
55
|
const id = String(model.id || model.model_id || model.name || index);
|
|
57
56
|
const loaded = asArray<string>((data as Record<string, unknown>).loaded).includes(id) || (data as Record<string, unknown>).current === id || model.state === "loaded";
|
|
57
|
+
const loadId = String(model.recommended_load_id || id);
|
|
58
|
+
const engine = String(model.recommended_engine || model.engine || "");
|
|
59
|
+
const loadAvailable = Boolean(model.load_available) || loaded;
|
|
60
|
+
const loadStatus = String(model.load_status || (loaded ? "loaded" : "unavailable"));
|
|
61
|
+
const unavailableReason = String(model.unavailable_reason || "Unavailable until the backend reports a local model/runtime ready.");
|
|
58
62
|
return (
|
|
59
63
|
<div key={id} className="flex flex-wrap items-center justify-between gap-3 rounded-md border border-border bg-background p-3">
|
|
60
64
|
<div>
|
|
61
65
|
<div className="font-medium">{String(model.name || id)}</div>
|
|
62
66
|
<div className="text-sm text-muted-foreground">{String(model.family || model.engine || model.recommended_engine || "local")}</div>
|
|
67
|
+
{!loaded && !loadAvailable ? <div className="mt-1 text-xs text-muted-foreground">{unavailableReason}</div> : null}
|
|
63
68
|
</div>
|
|
64
69
|
<div className="flex items-center gap-2">
|
|
65
|
-
<Badge variant={loaded ? "success" : "muted"}>{loaded ? "loaded" :
|
|
70
|
+
<Badge variant={loaded ? "success" : loadAvailable ? "muted" : "warning"}>{loaded ? "loaded" : loadStatus}</Badge>
|
|
66
71
|
<ActionButton
|
|
67
72
|
label={loaded ? "Unload" : "Load"}
|
|
68
|
-
action={() => loaded ? latticeApi.unloadModel(
|
|
73
|
+
action={() => loaded ? latticeApi.unloadModel(loadId) : latticeApi.loadModel(loadId, engine, false)}
|
|
69
74
|
invalidate={["models"]}
|
|
75
|
+
disabled={!loaded && !loadAvailable}
|
|
70
76
|
/>
|
|
71
77
|
</div>
|
|
72
78
|
</div>
|
|
@@ -76,7 +82,7 @@ function ModelsPanel() {
|
|
|
76
82
|
)}
|
|
77
83
|
</DataPanel>
|
|
78
84
|
<DataPanel title="Embedding provider" result={emb.data}>
|
|
79
|
-
{(data) => <
|
|
85
|
+
{(data) => <StructuredView value={data} />}
|
|
80
86
|
</DataPanel>
|
|
81
87
|
</div>
|
|
82
88
|
);
|
|
@@ -145,7 +151,7 @@ function McpPanel() {
|
|
|
145
151
|
<Input value={query} onChange={(e) => setQuery(e.target.value)} />
|
|
146
152
|
<Button onClick={() => rec.mutate()} disabled={!query.trim() || rec.isPending}>Recommend</Button>
|
|
147
153
|
</div>
|
|
148
|
-
{rec.data ? <
|
|
154
|
+
{rec.data ? <OperationResult result={rec.data} successLabel="Recommendation completed" /> : null}
|
|
149
155
|
</CardContent>
|
|
150
156
|
</Card>
|
|
151
157
|
</div>
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
|
-
import {
|
|
3
|
+
import { Network, ShieldCheck, UserCircle, Users } from "lucide-react";
|
|
4
4
|
import { latticeApi } from "@/api/client";
|
|
5
|
-
import { ActionButton, DataPanel,
|
|
5
|
+
import { ActionButton, DataPanel, EmptyState, EntityList, KeyValueList, OperationResult, StatGrid, StructuredView, Tabs } from "@/components/primitives";
|
|
6
6
|
import { Badge } from "@/components/ui/badge";
|
|
7
7
|
import { Button } from "@/components/ui/button";
|
|
8
8
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
9
9
|
import { Input } from "@/components/ui/input";
|
|
10
10
|
import { useAppStore } from "@/store/appStore";
|
|
11
|
-
import { asArray, titleize } from "@/lib/utils";
|
|
11
|
+
import { asArray, shortId, titleize } from "@/lib/utils";
|
|
12
12
|
|
|
13
13
|
type SystemTab = "account" | "workspaces" | "snapshots" | "activity" | "network" | "settings" | "admin";
|
|
14
14
|
|
|
@@ -84,11 +84,13 @@ function AccountPanel() {
|
|
|
84
84
|
<Button variant="outline" onClick={() => changePassword.mutate()} disabled={!password || !newPassword || changePassword.isPending}>Change password</Button>
|
|
85
85
|
<ActionButton label="Logout" action={() => latticeApi.logout()} invalidate={["profile"]} />
|
|
86
86
|
</div>
|
|
87
|
-
{[login.data, register.data, saveProfile.data, changePassword.data].filter(Boolean).map((item, i) =>
|
|
87
|
+
{[login.data, register.data, saveProfile.data, changePassword.data].filter(Boolean).map((item, i) => (
|
|
88
|
+
<OperationResult key={i} result={item} successLabel="Account request completed" />
|
|
89
|
+
))}
|
|
88
90
|
</CardContent>
|
|
89
91
|
</Card>
|
|
90
92
|
<DataPanel title="SSO config" result={sso.data} className="xl:col-span-2">
|
|
91
|
-
{(data) => <
|
|
93
|
+
{(data) => <StructuredView value={data} />}
|
|
92
94
|
</DataPanel>
|
|
93
95
|
</div>
|
|
94
96
|
);
|
|
@@ -195,7 +197,7 @@ function SnapshotsPanel() {
|
|
|
195
197
|
<Input value={after} onChange={(e) => setAfter(e.target.value)} placeholder="after id" />
|
|
196
198
|
</div>
|
|
197
199
|
<Button variant="outline" onClick={() => compare.mutate()} disabled={!before || !after || compare.isPending}>Compare</Button>
|
|
198
|
-
{compare.data ? <
|
|
200
|
+
{compare.data ? <OperationResult result={compare.data} successLabel="Snapshot comparison completed" /> : null}
|
|
199
201
|
</CardContent>
|
|
200
202
|
</Card>
|
|
201
203
|
<DataPanel title="Time machine" result={timeline.data} className="xl:col-span-2">
|
|
@@ -214,7 +216,7 @@ function ActivityPanel() {
|
|
|
214
216
|
{(data) => <EntityList items={(data as Record<string, unknown>).events} titleKey="event_type" metaKey="area" limit={14} />}
|
|
215
217
|
</DataPanel>
|
|
216
218
|
<DataPanel title="Presence" result={presence.data}>
|
|
217
|
-
{(data) => <
|
|
219
|
+
{(data) => <PresenceView data={data as Record<string, unknown>} />}
|
|
218
220
|
</DataPanel>
|
|
219
221
|
</div>
|
|
220
222
|
);
|
|
@@ -234,7 +236,7 @@ function NetworkPanel() {
|
|
|
234
236
|
return (
|
|
235
237
|
<div className="grid gap-4 xl:grid-cols-[0.8fr_1.2fr]">
|
|
236
238
|
<DataPanel title="Device identity" result={identity.data}>
|
|
237
|
-
{(data) => <
|
|
239
|
+
{(data) => <DeviceIdentityView data={data as Record<string, unknown>} />}
|
|
238
240
|
</DataPanel>
|
|
239
241
|
<Card>
|
|
240
242
|
<CardHeader>
|
|
@@ -246,7 +248,7 @@ function NetworkPanel() {
|
|
|
246
248
|
<Input value={baseUrl} onChange={(e) => setBaseUrl(e.target.value)} placeholder="http://peer.local:8765" />
|
|
247
249
|
<Input value={publicKey} onChange={(e) => setPublicKey(e.target.value)} placeholder="trusted public key" />
|
|
248
250
|
<Button disabled={!name || !baseUrl || !publicKey || pair.isPending} onClick={() => pair.mutate()}>Pair</Button>
|
|
249
|
-
{pair.data ? <
|
|
251
|
+
{pair.data ? <OperationResult result={pair.data} successLabel="Peer pairing request completed" /> : null}
|
|
250
252
|
</CardContent>
|
|
251
253
|
</Card>
|
|
252
254
|
<DataPanel title="Peers" result={peers.data} className="xl:col-span-2">
|
|
@@ -289,6 +291,7 @@ function SettingsPanel() {
|
|
|
289
291
|
const [restorePath, setRestorePath] = React.useState("");
|
|
290
292
|
const [archivePassphrase, setArchivePassphrase] = React.useState("");
|
|
291
293
|
const [restoreConfirm, setRestoreConfirm] = React.useState(false);
|
|
294
|
+
const [importConfirm, setImportConfirm] = React.useState(false);
|
|
292
295
|
const docker = useMutation({ mutationFn: (consent: boolean) => latticeApi.dockerPostgres({ consent, dry_run: !consent, port: 5432 }) });
|
|
293
296
|
const migration = useMutation({
|
|
294
297
|
mutationFn: () => latticeApi.migratePostgres({ dsn, schema_name: schema || "lattice_brain", dry_run: true }),
|
|
@@ -313,6 +316,16 @@ function SettingsPanel() {
|
|
|
313
316
|
qc.invalidateQueries({ queryKey: ["backupHealth"] });
|
|
314
317
|
},
|
|
315
318
|
});
|
|
319
|
+
const archiveImportDryRun = useMutation({
|
|
320
|
+
mutationFn: () => latticeApi.brainArchiveImport({ path: restorePath, passphrase: archivePassphrase, dry_run: true, confirm: false }),
|
|
321
|
+
});
|
|
322
|
+
const archiveImport = useMutation({
|
|
323
|
+
mutationFn: () => latticeApi.brainArchiveImport({ path: restorePath, passphrase: archivePassphrase, dry_run: false, confirm: importConfirm }),
|
|
324
|
+
onSuccess: () => {
|
|
325
|
+
qc.invalidateQueries({ queryKey: ["brainStorage"] });
|
|
326
|
+
qc.invalidateQueries({ queryKey: ["backupHealth"] });
|
|
327
|
+
},
|
|
328
|
+
});
|
|
316
329
|
return (
|
|
317
330
|
<div className="grid gap-4 xl:grid-cols-3">
|
|
318
331
|
<Card>
|
|
@@ -329,16 +342,16 @@ function SettingsPanel() {
|
|
|
329
342
|
</CardContent>
|
|
330
343
|
</Card>
|
|
331
344
|
<DataPanel title="Server health" result={health.data}>
|
|
332
|
-
{(data) => <
|
|
345
|
+
{(data) => <HealthView data={data as Record<string, unknown>} />}
|
|
333
346
|
</DataPanel>
|
|
334
347
|
<DataPanel title="Host telemetry" result={sys.data}>
|
|
335
|
-
{(data) => <
|
|
348
|
+
{(data) => <StructuredView value={data} />}
|
|
336
349
|
</DataPanel>
|
|
337
350
|
<DataPanel title="Brain storage" result={storage.data} className="xl:col-span-3">
|
|
338
|
-
{(data) => <
|
|
351
|
+
{(data) => <StorageView data={data as Record<string, unknown>} />}
|
|
339
352
|
</DataPanel>
|
|
340
353
|
<DataPanel title="Backup health" result={backupHealth.data} className="xl:col-span-3">
|
|
341
|
-
{(data) => <
|
|
354
|
+
{(data) => <BackupHealthView data={data as Record<string, unknown>} />}
|
|
342
355
|
</DataPanel>
|
|
343
356
|
<Card className="xl:col-span-3">
|
|
344
357
|
<CardHeader>
|
|
@@ -356,14 +369,20 @@ function SettingsPanel() {
|
|
|
356
369
|
<Button variant="outline" onClick={() => archiveInspect.mutate()} disabled={!restorePath || archiveInspect.isPending}>Inspect</Button>
|
|
357
370
|
<Button variant="outline" onClick={() => archiveVerify.mutate()} disabled={!restorePath || !archivePassphrase || archiveVerify.isPending}>Verify</Button>
|
|
358
371
|
<Button variant="outline" onClick={() => archiveDryRun.mutate()} disabled={!restorePath || !archivePassphrase || archiveDryRun.isPending}>Restore dry run</Button>
|
|
372
|
+
<Button variant="outline" onClick={() => archiveImportDryRun.mutate()} disabled={!restorePath || !archivePassphrase || archiveImportDryRun.isPending}>Import dry run</Button>
|
|
359
373
|
<label className="flex items-center gap-2 rounded-md border border-border px-3 py-2 text-sm">
|
|
360
374
|
<input type="checkbox" checked={restoreConfirm} onChange={(e) => setRestoreConfirm(e.target.checked)} />
|
|
361
375
|
Confirm restore
|
|
362
376
|
</label>
|
|
363
377
|
<Button variant="destructive" onClick={() => archiveRestore.mutate()} disabled={!restorePath || !archivePassphrase || !restoreConfirm || archiveRestore.isPending}>Restore</Button>
|
|
378
|
+
<label className="flex items-center gap-2 rounded-md border border-border px-3 py-2 text-sm">
|
|
379
|
+
<input type="checkbox" checked={importConfirm} onChange={(e) => setImportConfirm(e.target.checked)} />
|
|
380
|
+
Confirm import
|
|
381
|
+
</label>
|
|
382
|
+
<Button variant="outline" onClick={() => archiveImport.mutate()} disabled={!restorePath || !archivePassphrase || !importConfirm || archiveImport.isPending}>Import</Button>
|
|
364
383
|
</div>
|
|
365
|
-
{[archiveCreate.data, archiveInspect.data, archiveVerify.data, archiveDryRun.data, archiveRestore.data].filter(Boolean).map((item, i) => (
|
|
366
|
-
<
|
|
384
|
+
{[archiveCreate.data, archiveInspect.data, archiveVerify.data, archiveDryRun.data, archiveRestore.data, archiveImportDryRun.data, archiveImport.data].filter(Boolean).map((item, i) => (
|
|
385
|
+
<OperationResult key={i} result={item} successLabel="Archive request completed" />
|
|
367
386
|
))}
|
|
368
387
|
</CardContent>
|
|
369
388
|
</Card>
|
|
@@ -386,14 +405,14 @@ function SettingsPanel() {
|
|
|
386
405
|
<Button onClick={() => docker.mutate(true)} disabled={!dockerConsent || docker.isPending}>Start Docker</Button>
|
|
387
406
|
<Button variant="outline" onClick={() => migration.mutate()} disabled={!dsn || migration.isPending}>Plan migration</Button>
|
|
388
407
|
</div>
|
|
389
|
-
{docker.data ? <
|
|
390
|
-
{migration.data ? <
|
|
408
|
+
{docker.data ? <OperationResult result={docker.data} successLabel="Docker setup request completed" /> : null}
|
|
409
|
+
{migration.data ? <OperationResult result={migration.data} successLabel="Migration plan completed" /> : null}
|
|
391
410
|
</CardContent>
|
|
392
411
|
</Card>
|
|
393
412
|
<DataPanel title="Computer memory" result={comp.data} className="xl:col-span-3">
|
|
394
413
|
{(data) => (
|
|
395
414
|
<div className="space-y-3">
|
|
396
|
-
<
|
|
415
|
+
<StructuredView value={data} />
|
|
397
416
|
<div className="flex gap-2">
|
|
398
417
|
<ActionButton label="Enable memory" action={() => latticeApi.setComputerMemory(true)} invalidate={["computerMemory"]} />
|
|
399
418
|
<ActionButton label="Disable memory" action={() => latticeApi.setComputerMemory(false)} invalidate={["computerMemory"]} variant="destructive" />
|
|
@@ -405,6 +424,150 @@ function SettingsPanel() {
|
|
|
405
424
|
);
|
|
406
425
|
}
|
|
407
426
|
|
|
427
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
428
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function textValue(value: unknown, fallback = "not reported") {
|
|
432
|
+
if (value === null || value === undefined || value === "") return fallback;
|
|
433
|
+
if (typeof value === "boolean") return value ? "enabled" : "disabled";
|
|
434
|
+
return String(value);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function PresenceView({ data }: { data: Record<string, unknown> }) {
|
|
438
|
+
const rows = asArray<Record<string, unknown>>(data.presence || data.clients || data);
|
|
439
|
+
if (!rows.length) return <EmptyState title="No active presence" detail="No live collaborators or realtime clients are currently reported." />;
|
|
440
|
+
return <EntityList items={rows} titleKey="user" metaKey="workspace_id" />;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function DeviceIdentityView({ data }: { data: Record<string, unknown> }) {
|
|
444
|
+
const publicKey = textValue(data.public_key, "");
|
|
445
|
+
return (
|
|
446
|
+
<div className="space-y-3">
|
|
447
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
448
|
+
<Badge variant="success">local device</Badge>
|
|
449
|
+
<Badge variant="muted">{textValue(data.algorithm, "identity key")}</Badge>
|
|
450
|
+
</div>
|
|
451
|
+
<KeyValueList data={{
|
|
452
|
+
device_id: data.device_id || data.id || "not reported",
|
|
453
|
+
fingerprint: data.fingerprint || "not reported",
|
|
454
|
+
public_key: publicKey ? shortId(publicKey.replace(/\s+/g, " "), 72) : "not reported",
|
|
455
|
+
}} />
|
|
456
|
+
</div>
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function HealthView({ data }: { data: Record<string, unknown> }) {
|
|
461
|
+
return (
|
|
462
|
+
<div className="space-y-3">
|
|
463
|
+
<StatGrid stats={[
|
|
464
|
+
{ label: "Status", value: data.status || data.ok || "reported" },
|
|
465
|
+
{ label: "Version", value: data.version || "not reported" },
|
|
466
|
+
{ label: "Mode", value: data.mode || data.environment || "local" },
|
|
467
|
+
{ label: "Port", value: data.port || data.backend_port || "configured" },
|
|
468
|
+
]} />
|
|
469
|
+
<StructuredView value={data} />
|
|
470
|
+
</div>
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function StorageView({ data }: { data: Record<string, unknown> }) {
|
|
475
|
+
const active = isRecord(data.active) ? data.active : data;
|
|
476
|
+
const postgres = isRecord(data.postgres) ? data.postgres : {};
|
|
477
|
+
const backup = isRecord(data.backup_health) ? data.backup_health : {};
|
|
478
|
+
const vector = active.vector_search || active.vector || data.vector_search || data.sqlite_vec;
|
|
479
|
+
const postgresAvailable = Boolean(postgres.available || postgres.connected || postgres.enabled);
|
|
480
|
+
return (
|
|
481
|
+
<div className="space-y-4">
|
|
482
|
+
<StatGrid stats={[
|
|
483
|
+
{ label: "Active engine", value: active.engine || data.engine || "sqlite" },
|
|
484
|
+
{ label: "SQLite default", value: active.engine === "postgres" ? "scale mode" : "enabled" },
|
|
485
|
+
{ label: "Vector search", value: vector || "not reported" },
|
|
486
|
+
{ label: "Postgres", value: postgresAvailable ? "available" : "optional" },
|
|
487
|
+
]} />
|
|
488
|
+
<div className="grid gap-3 md:grid-cols-3">
|
|
489
|
+
<StatusCard title="SQLite" status={active.available === false ? "unavailable" : "default"} detail={textValue(active.reason || active.path || data.path, "Local brain storage is active by default.")} />
|
|
490
|
+
<StatusCard title="Vector search" status={textValue(vector, "reported")} detail={textValue(active.vector_reason || active.sqlite_vec_reason || data.vector_reason, "Uses the configured local vector capability or reports why it is unavailable.")} />
|
|
491
|
+
<StatusCard title="Postgres" status={postgresAvailable ? "available" : "not enabled"} detail={textValue(postgres.reason || postgres.dsn || postgres.status, "Postgres scale mode is opt-in and never required for local use.")} />
|
|
492
|
+
</div>
|
|
493
|
+
{Object.keys(backup).length ? <StructuredView value={{ backup_health: backup }} /> : null}
|
|
494
|
+
</div>
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function BackupHealthView({ data }: { data: Record<string, unknown> }) {
|
|
499
|
+
return (
|
|
500
|
+
<div className="space-y-3">
|
|
501
|
+
<StatGrid stats={[
|
|
502
|
+
{ label: "Available", value: data.available === false ? "no" : "yes" },
|
|
503
|
+
{ label: "Backups", value: data.count || data.backups || 0 },
|
|
504
|
+
{ label: "Encrypted", value: data.encrypted_archives || 0 },
|
|
505
|
+
{ label: "Zip backups", value: data.zip_backups || 0 },
|
|
506
|
+
]} />
|
|
507
|
+
<KeyValueList data={{
|
|
508
|
+
directory: data.directory || "not reported",
|
|
509
|
+
latest: data.latest || "none reported",
|
|
510
|
+
last_verified: data.last_verified || data.verified_at || "not reported",
|
|
511
|
+
failure: data.error || data.reason || "none reported",
|
|
512
|
+
}} />
|
|
513
|
+
</div>
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function StatusCard({ title, status, detail }: { title: string; status: string; detail: string }) {
|
|
518
|
+
const variant = /unavailable|failed|denied|disabled|not enabled/i.test(status) ? "warning" : "success";
|
|
519
|
+
return (
|
|
520
|
+
<div className="rounded-md border border-border bg-background p-3">
|
|
521
|
+
<div className="flex items-center justify-between gap-2">
|
|
522
|
+
<div className="font-medium">{title}</div>
|
|
523
|
+
<Badge variant={variant}>{status}</Badge>
|
|
524
|
+
</div>
|
|
525
|
+
<p className="mt-2 text-sm text-muted-foreground">{detail}</p>
|
|
526
|
+
</div>
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function HardeningView({ data }: { data: Record<string, unknown> }) {
|
|
531
|
+
const startup = isRecord(data.startup) ? data.startup : {};
|
|
532
|
+
const privacy = isRecord(data.privacy) ? data.privacy : {};
|
|
533
|
+
const storage = isRecord(data.storage) ? data.storage : {};
|
|
534
|
+
const backup = isRecord(data.backup) ? data.backup : {};
|
|
535
|
+
const identity = isRecord(data.device_identity) ? data.device_identity : {};
|
|
536
|
+
const permissions = isRecord(data.permissions) ? data.permissions : {};
|
|
537
|
+
return (
|
|
538
|
+
<div className="space-y-3">
|
|
539
|
+
<StatGrid stats={[
|
|
540
|
+
{ label: "Version", value: data.version || "reported" },
|
|
541
|
+
{ label: "Local only", value: privacy.local_only_default ?? startup.local_only_default ?? "reported" },
|
|
542
|
+
{ label: "Storage", value: isRecord(storage.active) ? (storage.active as Record<string, unknown>).engine : "reported" },
|
|
543
|
+
{ label: "Backups", value: backup.count || backup.available || "reported" },
|
|
544
|
+
]} />
|
|
545
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
546
|
+
<StatusCard title="Startup" status={startup.network_exposed ? "network exposed" : "local-only"} detail={`Host ${textValue(startup.host, "127.0.0.1")} on port ${textValue(startup.port, "configured")}.`} />
|
|
547
|
+
<StatusCard title="Integrations" status={privacy.local_only_default === false ? "review required" : "opt-in"} detail="External integrations remain disabled until the user explicitly enables them." />
|
|
548
|
+
<StatusCard title="Device identity" status={textValue(identity.algorithm || identity.fingerprint, "reported")} detail={textValue(identity.storage, "Stored locally and used for signed bundle exchange.")} />
|
|
549
|
+
<StatusCard title="Permissions" status={permissions.destructive_restore_requires_confirmation === false ? "review required" : "guarded"} detail="Export, import, and destructive restore permissions are surfaced through admin status." />
|
|
550
|
+
</div>
|
|
551
|
+
</div>
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function SecurityView({ data }: { data: Record<string, unknown> }) {
|
|
556
|
+
const cards = isRecord(data.cards) ? data.cards : {};
|
|
557
|
+
const severities = isRecord(data.severity_counts) ? data.severity_counts : {};
|
|
558
|
+
return (
|
|
559
|
+
<div className="space-y-3">
|
|
560
|
+
<StatGrid stats={[
|
|
561
|
+
{ label: "Events today", value: cards.events_today || 0 },
|
|
562
|
+
{ label: "High risk", value: cards.high_risk_events || severities.high || 0 },
|
|
563
|
+
{ label: "Review", value: cards.review_required || 0 },
|
|
564
|
+
{ label: "Risk rate", value: data.risk_rate || 0 },
|
|
565
|
+
]} />
|
|
566
|
+
<StructuredView value={{ severity_counts: severities, sensitive_fields: data.field_counts || {} }} />
|
|
567
|
+
</div>
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
408
571
|
function AdminPanel() {
|
|
409
572
|
const summary = useQuery({ queryKey: ["adminSummary"], queryFn: latticeApi.adminSummary });
|
|
410
573
|
const users = useQuery({ queryKey: ["adminUsers"], queryFn: latticeApi.adminUsers });
|
|
@@ -419,15 +582,15 @@ function AdminPanel() {
|
|
|
419
582
|
<DataPanel title="Admin summary" result={summary.data}>{(data) => <KeyValueList data={data as Record<string, unknown>} />}</DataPanel>
|
|
420
583
|
<DataPanel title="Users" result={users.data}>{(data) => <EntityList items={data} titleKey="email" metaKey="role" />}</DataPanel>
|
|
421
584
|
<DataPanel title="Audit" result={audit.data}>{(data) => <EntityList items={(data as Record<string, unknown>).recent_events || data} titleKey="act" metaKey="sev" />}</DataPanel>
|
|
422
|
-
<DataPanel title="Roles" result={roles.data}>{(data) => <
|
|
423
|
-
<DataPanel title="Policies" result={policies.data}>{(data) => <
|
|
424
|
-
<DataPanel title="Product hardening" result={hardening.data}>{(data) => <
|
|
425
|
-
<DataPanel title="Security overview" result={security.data}>{(data) => <
|
|
585
|
+
<DataPanel title="Roles" result={roles.data}>{(data) => <EntityList items={(data as Record<string, unknown>).roles || data} titleKey="role" metaKey="members" />}</DataPanel>
|
|
586
|
+
<DataPanel title="Policies" result={policies.data}>{(data) => <EntityList items={(data as Record<string, unknown>).policies || data} titleKey="label" metaKey="enforced" />}</DataPanel>
|
|
587
|
+
<DataPanel title="Product hardening" result={hardening.data}>{(data) => <HardeningView data={data as Record<string, unknown>} />}</DataPanel>
|
|
588
|
+
<DataPanel title="Security overview" result={security.data}>{(data) => <SecurityView data={data as Record<string, unknown>} />}</DataPanel>
|
|
426
589
|
<DataPanel title="Private VPC" result={vpc.data} className="xl:col-span-2">
|
|
427
590
|
{(data) => (
|
|
428
591
|
<div className="space-y-2">
|
|
429
592
|
<Badge variant="muted">Community-disabled features remain honest unavailable states.</Badge>
|
|
430
|
-
<
|
|
593
|
+
<StructuredView value={data} />
|
|
431
594
|
</div>
|
|
432
595
|
)}
|
|
433
596
|
</DataPanel>
|
package/lattice_brain/archive.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""Encrypted .latticebrain archive support.
|
|
2
2
|
|
|
3
3
|
The archive is intentionally self-contained and local-only: the encrypted
|
|
4
|
-
payload holds the SQLite brain, blob store, portable JSON state,
|
|
5
|
-
bundles, and public metadata needed to inspect/verify/
|
|
6
|
-
machine without contacting a service.
|
|
4
|
+
payload holds the SQLite brain, blob store, portable JSON state, workspace
|
|
5
|
+
export bundles when present, and public metadata needed to inspect/verify/
|
|
6
|
+
restore on another machine without contacting a service.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
@@ -77,7 +77,12 @@ class SQLiteEngine(StorageEngine):
|
|
|
77
77
|
backup_restore=True,
|
|
78
78
|
migrations=True,
|
|
79
79
|
encrypted_archives=True,
|
|
80
|
-
metadata={
|
|
80
|
+
metadata={
|
|
81
|
+
"db_path": str(self.db_path),
|
|
82
|
+
"sqlite_vec_loaded": False,
|
|
83
|
+
"vector_mode": "fallback",
|
|
84
|
+
"honest_fallback": "sqlite-vec has not been probed yet; vector search uses the real brute-force cosine fallback until sqlite-vec is loaded.",
|
|
85
|
+
},
|
|
81
86
|
)
|
|
82
87
|
# Probe on demand so status is accurate even before the graph opens.
|
|
83
88
|
try:
|
|
@@ -94,7 +99,11 @@ class SQLiteEngine(StorageEngine):
|
|
|
94
99
|
return StorageCapabilities(
|
|
95
100
|
engine=self.name,
|
|
96
101
|
available=True,
|
|
97
|
-
reason=None if self._sqlite_vec_loaded else
|
|
102
|
+
reason=None if self._sqlite_vec_loaded else (
|
|
103
|
+
f"{self._sqlite_vec_reason}; using real brute-force cosine fallback, not sqlite-vec ANN"
|
|
104
|
+
if self._sqlite_vec_reason
|
|
105
|
+
else "sqlite-vec unavailable; using real brute-force cosine fallback, not sqlite-vec ANN"
|
|
106
|
+
),
|
|
98
107
|
vector_backend=vector_backend,
|
|
99
108
|
vector_available=True,
|
|
100
109
|
backup_restore=True,
|
|
@@ -103,6 +112,10 @@ class SQLiteEngine(StorageEngine):
|
|
|
103
112
|
metadata={
|
|
104
113
|
"db_path": str(self.db_path),
|
|
105
114
|
"sqlite_vec_loaded": self._sqlite_vec_loaded,
|
|
115
|
+
"sqlite_vec_ann_available": self._sqlite_vec_loaded,
|
|
116
|
+
"vector_mode": "sqlite-vec" if self._sqlite_vec_loaded else "fallback",
|
|
117
|
+
"degraded": not self._sqlite_vec_loaded,
|
|
118
|
+
"honest_fallback": None if self._sqlite_vec_loaded else "Vector search is available through the deterministic brute-force cosine backend. sqlite-vec ANN is unavailable.",
|
|
106
119
|
},
|
|
107
120
|
)
|
|
108
121
|
|
package/latticeai/__init__.py
CHANGED
package/latticeai/api/agents.py
CHANGED
|
@@ -46,7 +46,7 @@ def create_agents_router(
|
|
|
46
46
|
run_executor: Any = None,
|
|
47
47
|
) -> APIRouter:
|
|
48
48
|
from latticeai.core.multi_agent import AGENT_ROLES, ROLE_AGENT_IDS
|
|
49
|
-
from latticeai.services.agent_runtime import AgentRuntime
|
|
49
|
+
from latticeai.services.agent_runtime import AgentRuntime, AgentRuntimeUnavailable
|
|
50
50
|
|
|
51
51
|
# Single AgentRuntime boundary: the router (and via it, the frontend) talks
|
|
52
52
|
# to this façade instead of reaching into the orchestrator/store directly.
|
|
@@ -186,6 +186,8 @@ def create_agents_router(
|
|
|
186
186
|
)
|
|
187
187
|
except ValueError as exc:
|
|
188
188
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
189
|
+
except AgentRuntimeUnavailable as exc:
|
|
190
|
+
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
|
189
191
|
except PermissionError as exc:
|
|
190
192
|
# A pre_run hook gated this run (e.g. a policy/permission hook).
|
|
191
193
|
raise HTTPException(status_code=403, detail=str(exc)) from exc
|