ltcai 4.2.0 → 4.3.1

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 (51) hide show
  1. package/README.md +28 -21
  2. package/bin/ltcai.js +6 -2
  3. package/docs/CHANGELOG.md +72 -0
  4. package/docs/V4_3_PORTABILITY_ARCHITECTURE.md +69 -0
  5. package/docs/V4_3_PRIVACY_AUDIT.md +60 -0
  6. package/docs/V4_3_PRODUCT_HARDENING_REPORT.md +53 -0
  7. package/docs/V4_3_VALIDATION_REPORT.md +58 -0
  8. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +19 -25
  9. package/frontend/openapi.json +213 -1
  10. package/frontend/src/App.tsx +15 -1
  11. package/frontend/src/api/client.ts +26 -1
  12. package/frontend/src/api/openapi.ts +268 -0
  13. package/frontend/src/pages/Act.tsx +63 -2
  14. package/frontend/src/pages/Library.tsx +9 -3
  15. package/frontend/src/pages/System.tsx +58 -0
  16. package/lattice_brain/__init__.py +1 -1
  17. package/lattice_brain/archive.py +360 -47
  18. package/lattice_brain/storage/sqlite.py +15 -2
  19. package/latticeai/__init__.py +1 -1
  20. package/latticeai/api/admin.py +11 -0
  21. package/latticeai/api/agents.py +3 -1
  22. package/latticeai/api/models.py +66 -18
  23. package/latticeai/api/portability.py +59 -2
  24. package/latticeai/app_factory.py +9 -0
  25. package/latticeai/brain/projection.py +12 -2
  26. package/latticeai/brain/retrieval.py +10 -0
  27. package/latticeai/brain/store.py +6 -1
  28. package/latticeai/core/config.py +4 -2
  29. package/latticeai/core/marketplace.py +1 -1
  30. package/latticeai/core/multi_agent.py +1 -1
  31. package/latticeai/core/product_hardening.py +218 -0
  32. package/latticeai/core/workspace_os.py +1 -1
  33. package/latticeai/services/agent_runtime.py +52 -12
  34. package/latticeai/services/kg_portability.py +147 -4
  35. package/latticeai/services/model_runtime.py +83 -2
  36. package/ltcai_cli.py +16 -4
  37. package/package.json +5 -4
  38. package/requirements.txt +17 -0
  39. package/scripts/clean_release_artifacts.mjs +27 -0
  40. package/scripts/lint_frontend.mjs +5 -0
  41. package/scripts/validate_release_artifacts.py +10 -0
  42. package/src-tauri/Cargo.lock +1 -1
  43. package/src-tauri/Cargo.toml +1 -1
  44. package/src-tauri/src/main.rs +356 -24
  45. package/src-tauri/tauri.conf.json +20 -1
  46. package/static/app/asset-manifest.json +5 -5
  47. package/static/app/assets/{index-C_HAkbAg.js → index-BhPuj8rT.js} +45 -45
  48. package/static/app/assets/index-BhPuj8rT.js.map +1 -0
  49. package/static/app/assets/{index-CDjiH_se.css → index-yZswHE3d.css} +1 -1
  50. package/static/app/index.html +2 -2
  51. package/static/app/assets/index-C_HAkbAg.js.map +0 -1
@@ -207,6 +207,23 @@ export interface paths {
207
207
  patch?: never;
208
208
  trace?: never;
209
209
  };
210
+ "/admin/product-hardening": {
211
+ parameters: {
212
+ query?: never;
213
+ header?: never;
214
+ path?: never;
215
+ cookie?: never;
216
+ };
217
+ /** Admin Product Hardening */
218
+ get: operations["admin_product_hardening_admin_product_hardening_get"];
219
+ put?: never;
220
+ post?: never;
221
+ delete?: never;
222
+ options?: never;
223
+ head?: never;
224
+ patch?: never;
225
+ trace?: never;
226
+ };
210
227
  "/admin/roles": {
211
228
  parameters: {
212
229
  query?: never;
@@ -1280,6 +1297,40 @@ export interface paths {
1280
1297
  patch?: never;
1281
1298
  trace?: never;
1282
1299
  };
1300
+ "/api/knowledge-graph/archive/import": {
1301
+ parameters: {
1302
+ query?: never;
1303
+ header?: never;
1304
+ path?: never;
1305
+ cookie?: never;
1306
+ };
1307
+ get?: never;
1308
+ put?: never;
1309
+ /** Import Encrypted Archive */
1310
+ post: operations["import_encrypted_archive_api_knowledge_graph_archive_import_post"];
1311
+ delete?: never;
1312
+ options?: never;
1313
+ head?: never;
1314
+ patch?: never;
1315
+ trace?: never;
1316
+ };
1317
+ "/api/knowledge-graph/archive/inspect": {
1318
+ parameters: {
1319
+ query?: never;
1320
+ header?: never;
1321
+ path?: never;
1322
+ cookie?: never;
1323
+ };
1324
+ get?: never;
1325
+ put?: never;
1326
+ /** Inspect Encrypted Archive */
1327
+ post: operations["inspect_encrypted_archive_api_knowledge_graph_archive_inspect_post"];
1328
+ delete?: never;
1329
+ options?: never;
1330
+ head?: never;
1331
+ patch?: never;
1332
+ trace?: never;
1333
+ };
1283
1334
  "/api/knowledge-graph/archive/restore": {
1284
1335
  parameters: {
1285
1336
  query?: never;
@@ -1297,6 +1348,23 @@ export interface paths {
1297
1348
  patch?: never;
1298
1349
  trace?: never;
1299
1350
  };
1351
+ "/api/knowledge-graph/archive/verify": {
1352
+ parameters: {
1353
+ query?: never;
1354
+ header?: never;
1355
+ path?: never;
1356
+ cookie?: never;
1357
+ };
1358
+ get?: never;
1359
+ put?: never;
1360
+ /** Verify Encrypted Archive */
1361
+ post: operations["verify_encrypted_archive_api_knowledge_graph_archive_verify_post"];
1362
+ delete?: never;
1363
+ options?: never;
1364
+ head?: never;
1365
+ patch?: never;
1366
+ trace?: never;
1367
+ };
1300
1368
  "/api/knowledge-graph/backup": {
1301
1369
  parameters: {
1302
1370
  query?: never;
@@ -1314,6 +1382,23 @@ export interface paths {
1314
1382
  patch?: never;
1315
1383
  trace?: never;
1316
1384
  };
1385
+ "/api/knowledge-graph/backup-health": {
1386
+ parameters: {
1387
+ query?: never;
1388
+ header?: never;
1389
+ path?: never;
1390
+ cookie?: never;
1391
+ };
1392
+ /** Backup Health */
1393
+ get: operations["backup_health_api_knowledge_graph_backup_health_get"];
1394
+ put?: never;
1395
+ post?: never;
1396
+ delete?: never;
1397
+ options?: never;
1398
+ head?: never;
1399
+ patch?: never;
1400
+ trace?: never;
1401
+ };
1317
1402
  "/api/knowledge-graph/export": {
1318
1403
  parameters: {
1319
1404
  query?: never;
@@ -5822,8 +5907,32 @@ export interface components {
5822
5907
  /** Path */
5823
5908
  path?: string | null;
5824
5909
  };
5910
+ /** EncryptedInspectRequest */
5911
+ EncryptedInspectRequest: {
5912
+ /** Passphrase */
5913
+ passphrase?: string | null;
5914
+ /** Path */
5915
+ path: string;
5916
+ };
5825
5917
  /** EncryptedRestoreRequest */
5826
5918
  EncryptedRestoreRequest: {
5919
+ /**
5920
+ * Confirm
5921
+ * @default false
5922
+ */
5923
+ confirm: boolean;
5924
+ /**
5925
+ * Dry Run
5926
+ * @default false
5927
+ */
5928
+ dry_run: boolean;
5929
+ /** Passphrase */
5930
+ passphrase: string;
5931
+ /** Path */
5932
+ path: string;
5933
+ };
5934
+ /** EncryptedVerifyRequest */
5935
+ EncryptedVerifyRequest: {
5827
5936
  /** Passphrase */
5828
5937
  passphrase: string;
5829
5938
  /** Path */
@@ -6074,6 +6183,11 @@ export interface components {
6074
6183
  LoadModelRequest: {
6075
6184
  /** Adapter Path */
6076
6185
  adapter_path?: string | null;
6186
+ /**
6187
+ * Allow Download
6188
+ * @default false
6189
+ */
6190
+ allow_download: boolean;
6077
6191
  /** Draft Model Id */
6078
6192
  draft_model_id?: string | null;
6079
6193
  /** Engine */
@@ -6307,6 +6421,11 @@ export interface components {
6307
6421
  };
6308
6422
  /** PrepareModelRequest */
6309
6423
  PrepareModelRequest: {
6424
+ /**
6425
+ * Allow Download
6426
+ * @default false
6427
+ */
6428
+ allow_download: boolean;
6310
6429
  /** Engine */
6311
6430
  engine?: string | null;
6312
6431
  /** Model */
@@ -6389,6 +6508,16 @@ export interface components {
6389
6508
  };
6390
6509
  /** RestoreRequest */
6391
6510
  RestoreRequest: {
6511
+ /**
6512
+ * Confirm
6513
+ * @default false
6514
+ */
6515
+ confirm: boolean;
6516
+ /**
6517
+ * Dry Run
6518
+ * @default false
6519
+ */
6520
+ dry_run: boolean;
6392
6521
  /** Path */
6393
6522
  path: string;
6394
6523
  /**
@@ -7458,6 +7587,26 @@ export interface operations {
7458
7587
  };
7459
7588
  };
7460
7589
  };
7590
+ admin_product_hardening_admin_product_hardening_get: {
7591
+ parameters: {
7592
+ query?: never;
7593
+ header?: never;
7594
+ path?: never;
7595
+ cookie?: never;
7596
+ };
7597
+ requestBody?: never;
7598
+ responses: {
7599
+ /** @description Successful Response */
7600
+ 200: {
7601
+ headers: {
7602
+ [name: string]: unknown;
7603
+ };
7604
+ content: {
7605
+ "application/json": unknown;
7606
+ };
7607
+ };
7608
+ };
7609
+ };
7461
7610
  admin_roles_admin_roles_get: {
7462
7611
  parameters: {
7463
7612
  query?: never;
@@ -9503,6 +9652,72 @@ export interface operations {
9503
9652
  };
9504
9653
  };
9505
9654
  };
9655
+ import_encrypted_archive_api_knowledge_graph_archive_import_post: {
9656
+ parameters: {
9657
+ query?: never;
9658
+ header?: never;
9659
+ path?: never;
9660
+ cookie?: never;
9661
+ };
9662
+ requestBody: {
9663
+ content: {
9664
+ "application/json": components["schemas"]["EncryptedRestoreRequest"];
9665
+ };
9666
+ };
9667
+ responses: {
9668
+ /** @description Successful Response */
9669
+ 200: {
9670
+ headers: {
9671
+ [name: string]: unknown;
9672
+ };
9673
+ content: {
9674
+ "application/json": unknown;
9675
+ };
9676
+ };
9677
+ /** @description Validation Error */
9678
+ 422: {
9679
+ headers: {
9680
+ [name: string]: unknown;
9681
+ };
9682
+ content: {
9683
+ "application/json": components["schemas"]["HTTPValidationError"];
9684
+ };
9685
+ };
9686
+ };
9687
+ };
9688
+ inspect_encrypted_archive_api_knowledge_graph_archive_inspect_post: {
9689
+ parameters: {
9690
+ query?: never;
9691
+ header?: never;
9692
+ path?: never;
9693
+ cookie?: never;
9694
+ };
9695
+ requestBody: {
9696
+ content: {
9697
+ "application/json": components["schemas"]["EncryptedInspectRequest"];
9698
+ };
9699
+ };
9700
+ responses: {
9701
+ /** @description Successful Response */
9702
+ 200: {
9703
+ headers: {
9704
+ [name: string]: unknown;
9705
+ };
9706
+ content: {
9707
+ "application/json": unknown;
9708
+ };
9709
+ };
9710
+ /** @description Validation Error */
9711
+ 422: {
9712
+ headers: {
9713
+ [name: string]: unknown;
9714
+ };
9715
+ content: {
9716
+ "application/json": components["schemas"]["HTTPValidationError"];
9717
+ };
9718
+ };
9719
+ };
9720
+ };
9506
9721
  restore_encrypted_archive_api_knowledge_graph_archive_restore_post: {
9507
9722
  parameters: {
9508
9723
  query?: never;
@@ -9536,6 +9751,39 @@ export interface operations {
9536
9751
  };
9537
9752
  };
9538
9753
  };
9754
+ verify_encrypted_archive_api_knowledge_graph_archive_verify_post: {
9755
+ parameters: {
9756
+ query?: never;
9757
+ header?: never;
9758
+ path?: never;
9759
+ cookie?: never;
9760
+ };
9761
+ requestBody: {
9762
+ content: {
9763
+ "application/json": components["schemas"]["EncryptedVerifyRequest"];
9764
+ };
9765
+ };
9766
+ responses: {
9767
+ /** @description Successful Response */
9768
+ 200: {
9769
+ headers: {
9770
+ [name: string]: unknown;
9771
+ };
9772
+ content: {
9773
+ "application/json": unknown;
9774
+ };
9775
+ };
9776
+ /** @description Validation Error */
9777
+ 422: {
9778
+ headers: {
9779
+ [name: string]: unknown;
9780
+ };
9781
+ content: {
9782
+ "application/json": components["schemas"]["HTTPValidationError"];
9783
+ };
9784
+ };
9785
+ };
9786
+ };
9539
9787
  backup_graph_api_knowledge_graph_backup_post: {
9540
9788
  parameters: {
9541
9789
  query?: never;
@@ -9569,6 +9817,26 @@ export interface operations {
9569
9817
  };
9570
9818
  };
9571
9819
  };
9820
+ backup_health_api_knowledge_graph_backup_health_get: {
9821
+ parameters: {
9822
+ query?: never;
9823
+ header?: never;
9824
+ path?: never;
9825
+ cookie?: never;
9826
+ };
9827
+ requestBody?: never;
9828
+ responses: {
9829
+ /** @description Successful Response */
9830
+ 200: {
9831
+ headers: {
9832
+ [name: string]: unknown;
9833
+ };
9834
+ content: {
9835
+ "application/json": unknown;
9836
+ };
9837
+ };
9838
+ };
9839
+ };
9572
9840
  export_graph_api_knowledge_graph_export_post: {
9573
9841
  parameters: {
9574
9842
  query?: never;
@@ -58,6 +58,11 @@ function AgentsPanel() {
58
58
  mutationFn: () => latticeApi.registerAgent({ name: agentName, type: "custom", capabilities: [] }),
59
59
  onSuccess: () => qc.invalidateQueries({ queryKey: ["agentRegistry"] }),
60
60
  });
61
+ const runtimeData = (runtime.data?.data || {}) as Record<string, unknown>;
62
+ const runtimeMeta = (runtimeData.runtime || {}) as Record<string, unknown>;
63
+ const runtimeReady = Boolean(runtimeMeta.ready);
64
+ const runtimeReason = String(runtimeMeta.unavailable_reason || "Load an LLM-backed model before running agents.");
65
+ const canRunAgent = Boolean(goal.trim()) && runtimeReady && !run.isPending;
61
66
  return (
62
67
  <div className="grid gap-4 xl:grid-cols-[0.9fr_1.1fr]">
63
68
  <Card>
@@ -67,7 +72,15 @@ function AgentsPanel() {
67
72
  </CardHeader>
68
73
  <CardContent className="space-y-3">
69
74
  <Textarea value={goal} onChange={(e) => setGoal(e.target.value)} placeholder="Describe the objective..." />
70
- <Button disabled={!goal.trim() || run.isPending} onClick={() => run.mutate()}><Play className="h-4 w-4" /> Run planner/executor/reviewer</Button>
75
+ {!runtimeReady ? <Badge variant="warning">{runtimeReason}</Badge> : null}
76
+ <Button
77
+ className="w-full"
78
+ variant={runtimeReady ? "default" : "outline"}
79
+ disabled={!canRunAgent}
80
+ onClick={() => run.mutate()}
81
+ >
82
+ <Play className="h-4 w-4" /> {runtimeReady ? "Run pipeline" : "Agent execution unavailable"}
83
+ </Button>
71
84
  {run.data ? <JsonView value={run.data.data || run.data.error} /> : null}
72
85
  </CardContent>
73
86
  </Card>
@@ -162,8 +175,26 @@ function RunList({ runs, kind }: { runs: Array<Record<string, unknown>>; kind: "
162
175
  }
163
176
 
164
177
  function WorkflowsPanel() {
178
+ const qc = useQueryClient();
165
179
  const defs = useQuery({ queryKey: ["workflowDefinitions"], queryFn: latticeApi.workflowDefinitions });
166
180
  const triggers = useQuery({ queryKey: ["workflowTriggers"], queryFn: latticeApi.workflowTriggers });
181
+ const [name, setName] = React.useState("Manual workflow");
182
+ const [importText, setImportText] = React.useState("");
183
+ const create = useMutation({
184
+ mutationFn: () => latticeApi.createWorkflow({
185
+ name: name.trim() || "Manual workflow",
186
+ nodes: manualWorkflowNodes(),
187
+ metadata: { created_from: "desktop-act-ui" },
188
+ }),
189
+ onSuccess: () => qc.invalidateQueries({ queryKey: ["workflowDefinitions"] }),
190
+ });
191
+ const importWorkflow = useMutation({
192
+ mutationFn: () => latticeApi.importWorkflow(JSON.parse(importText) as Record<string, unknown>),
193
+ onSuccess: () => {
194
+ setImportText("");
195
+ qc.invalidateQueries({ queryKey: ["workflowDefinitions"] });
196
+ },
197
+ });
167
198
  const workflows = asArray<Record<string, unknown>>((defs.data?.data as Record<string, unknown>)?.workflows);
168
199
  const nodes: Node[] = workflows.slice(0, 12).map((workflow, index) => ({
169
200
  id: String(workflow.id || workflow.workflow_id || index),
@@ -189,7 +220,17 @@ function WorkflowsPanel() {
189
220
  </Card>
190
221
  <DataPanel title="Definitions" result={defs.data}>
191
222
  {() => (
192
- <div className="space-y-2">
223
+ <div className="space-y-3">
224
+ <div className="grid gap-2 rounded-md border border-border p-3">
225
+ <div className="flex flex-wrap gap-2">
226
+ <Input value={name} onChange={(event) => setName(event.target.value)} placeholder="Workflow name" />
227
+ <Button disabled={create.isPending} onClick={() => create.mutate()}>Create</Button>
228
+ </div>
229
+ <Textarea value={importText} onChange={(event) => setImportText(event.target.value)} placeholder="Paste exported workflow JSON" />
230
+ <Button variant="outline" disabled={!importText.trim() || importWorkflow.isPending} onClick={() => importWorkflow.mutate()}>Import</Button>
231
+ {create.data ? <JsonView value={create.data.data || create.data.error} /> : null}
232
+ {importWorkflow.data ? <JsonView value={importWorkflow.data.data || importWorkflow.data.error} /> : null}
233
+ </div>
193
234
  {workflows.length ? workflows.map((workflow) => {
194
235
  const id = String(workflow.id || workflow.workflow_id);
195
236
  return (
@@ -197,6 +238,7 @@ function WorkflowsPanel() {
197
238
  <div className="font-medium">{String(workflow.name || id)}</div>
198
239
  <div className="mt-2 flex gap-2">
199
240
  <ActionButton label="Run" action={() => latticeApi.runWorkflow(id)} invalidate={["workflowRuns"]} />
241
+ <ActionButton label="Export" action={() => latticeApi.exportWorkflow(id)} />
200
242
  </div>
201
243
  </div>
202
244
  );
@@ -211,6 +253,25 @@ function WorkflowsPanel() {
211
253
  );
212
254
  }
213
255
 
256
+ function manualWorkflowNodes(): Array<Record<string, unknown>> {
257
+ return [
258
+ {
259
+ id: "trigger",
260
+ type: "trigger",
261
+ name: "Manual start",
262
+ config: { trigger: "manual" },
263
+ next: "output",
264
+ },
265
+ {
266
+ id: "output",
267
+ type: "output",
268
+ name: "Output",
269
+ config: { value: "Workflow completed" },
270
+ next: null,
271
+ },
272
+ ];
273
+ }
274
+
214
275
  function HooksPanel() {
215
276
  const hooks = useQuery({ queryKey: ["hooks"], queryFn: latticeApi.hooks });
216
277
  const runs = useQuery({ queryKey: ["hookRuns"], queryFn: latticeApi.hookRuns });
@@ -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" : "available"}</Badge>
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(id) : latticeApi.loadModel(id, String(model.recommended_engine || model.engine || ""))}
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>
@@ -275,18 +275,44 @@ function NetworkPanel() {
275
275
  }
276
276
 
277
277
  function SettingsPanel() {
278
+ const qc = useQueryClient();
278
279
  const { theme, setTheme, mode, setMode } = useAppStore();
279
280
  const health = useQuery({ queryKey: ["health"], queryFn: latticeApi.health });
280
281
  const sys = useQuery({ queryKey: ["sysinfo"], queryFn: latticeApi.sysinfo });
281
282
  const comp = useQuery({ queryKey: ["computerMemory"], queryFn: latticeApi.computerMemory });
282
283
  const storage = useQuery({ queryKey: ["brainStorage"], queryFn: latticeApi.brainStorage });
284
+ const backupHealth = useQuery({ queryKey: ["backupHealth"], queryFn: latticeApi.backupHealth });
283
285
  const [dsn, setDsn] = React.useState("");
284
286
  const [schema, setSchema] = React.useState("lattice_brain");
285
287
  const [dockerConsent, setDockerConsent] = React.useState(false);
288
+ const [archivePath, setArchivePath] = React.useState("");
289
+ const [restorePath, setRestorePath] = React.useState("");
290
+ const [archivePassphrase, setArchivePassphrase] = React.useState("");
291
+ const [restoreConfirm, setRestoreConfirm] = React.useState(false);
286
292
  const docker = useMutation({ mutationFn: (consent: boolean) => latticeApi.dockerPostgres({ consent, dry_run: !consent, port: 5432 }) });
287
293
  const migration = useMutation({
288
294
  mutationFn: () => latticeApi.migratePostgres({ dsn, schema_name: schema || "lattice_brain", dry_run: true }),
289
295
  });
296
+ const archiveCreate = useMutation({
297
+ mutationFn: () => latticeApi.brainArchive({ path: archivePath.trim() || null, passphrase: archivePassphrase }),
298
+ onSuccess: () => qc.invalidateQueries({ queryKey: ["backupHealth"] }),
299
+ });
300
+ const archiveInspect = useMutation({
301
+ mutationFn: () => latticeApi.brainArchiveInspect({ path: restorePath, passphrase: archivePassphrase || null }),
302
+ });
303
+ const archiveVerify = useMutation({
304
+ mutationFn: () => latticeApi.brainArchiveVerify({ path: restorePath, passphrase: archivePassphrase }),
305
+ });
306
+ const archiveDryRun = useMutation({
307
+ mutationFn: () => latticeApi.brainArchiveRestore({ path: restorePath, passphrase: archivePassphrase, dry_run: true, confirm: false }),
308
+ });
309
+ const archiveRestore = useMutation({
310
+ mutationFn: () => latticeApi.brainArchiveRestore({ path: restorePath, passphrase: archivePassphrase, dry_run: false, confirm: restoreConfirm }),
311
+ onSuccess: () => {
312
+ qc.invalidateQueries({ queryKey: ["brainStorage"] });
313
+ qc.invalidateQueries({ queryKey: ["backupHealth"] });
314
+ },
315
+ });
290
316
  return (
291
317
  <div className="grid gap-4 xl:grid-cols-3">
292
318
  <Card>
@@ -311,6 +337,36 @@ function SettingsPanel() {
311
337
  <DataPanel title="Brain storage" result={storage.data} className="xl:col-span-3">
312
338
  {(data) => <JsonView value={data} />}
313
339
  </DataPanel>
340
+ <DataPanel title="Backup health" result={backupHealth.data} className="xl:col-span-3">
341
+ {(data) => <JsonView value={data} />}
342
+ </DataPanel>
343
+ <Card className="xl:col-span-3">
344
+ <CardHeader>
345
+ <CardTitle>.latticebrain portability</CardTitle>
346
+ <CardDescription>Encrypted export, inspect, verify, dry-run restore, and confirmed restore use the Brain portability API.</CardDescription>
347
+ </CardHeader>
348
+ <CardContent className="grid gap-3">
349
+ <div className="grid gap-2 sm:grid-cols-[1fr_1fr]">
350
+ <Input value={archivePath} onChange={(e) => setArchivePath(e.target.value)} placeholder="export path (optional)" />
351
+ <Input value={restorePath} onChange={(e) => setRestorePath(e.target.value)} placeholder="archive path for inspect/restore" />
352
+ </div>
353
+ <Input type="password" value={archivePassphrase} onChange={(e) => setArchivePassphrase(e.target.value)} placeholder="archive passphrase" />
354
+ <div className="flex flex-wrap gap-2">
355
+ <Button onClick={() => archiveCreate.mutate()} disabled={!archivePassphrase || archiveCreate.isPending}>Export archive</Button>
356
+ <Button variant="outline" onClick={() => archiveInspect.mutate()} disabled={!restorePath || archiveInspect.isPending}>Inspect</Button>
357
+ <Button variant="outline" onClick={() => archiveVerify.mutate()} disabled={!restorePath || !archivePassphrase || archiveVerify.isPending}>Verify</Button>
358
+ <Button variant="outline" onClick={() => archiveDryRun.mutate()} disabled={!restorePath || !archivePassphrase || archiveDryRun.isPending}>Restore dry run</Button>
359
+ <label className="flex items-center gap-2 rounded-md border border-border px-3 py-2 text-sm">
360
+ <input type="checkbox" checked={restoreConfirm} onChange={(e) => setRestoreConfirm(e.target.checked)} />
361
+ Confirm restore
362
+ </label>
363
+ <Button variant="destructive" onClick={() => archiveRestore.mutate()} disabled={!restorePath || !archivePassphrase || !restoreConfirm || archiveRestore.isPending}>Restore</Button>
364
+ </div>
365
+ {[archiveCreate.data, archiveInspect.data, archiveVerify.data, archiveDryRun.data, archiveRestore.data].filter(Boolean).map((item, i) => (
366
+ <JsonView key={i} value={item?.data || item?.error} />
367
+ ))}
368
+ </CardContent>
369
+ </Card>
314
370
  <Card className="xl:col-span-3">
315
371
  <CardHeader>
316
372
  <CardTitle>Postgres scale mode</CardTitle>
@@ -355,6 +411,7 @@ function AdminPanel() {
355
411
  const audit = useQuery({ queryKey: ["adminAudit"], queryFn: latticeApi.adminAudit });
356
412
  const roles = useQuery({ queryKey: ["adminRoles"], queryFn: latticeApi.adminRoles });
357
413
  const policies = useQuery({ queryKey: ["adminPolicies"], queryFn: latticeApi.adminPolicies });
414
+ const hardening = useQuery({ queryKey: ["adminProductHardening"], queryFn: latticeApi.adminProductHardening });
358
415
  const security = useQuery({ queryKey: ["adminSecurity"], queryFn: latticeApi.adminSecurity });
359
416
  const vpc = useQuery({ queryKey: ["vpcStatus"], queryFn: latticeApi.vpcStatus });
360
417
  return (
@@ -364,6 +421,7 @@ function AdminPanel() {
364
421
  <DataPanel title="Audit" result={audit.data}>{(data) => <EntityList items={(data as Record<string, unknown>).recent_events || data} titleKey="act" metaKey="sev" />}</DataPanel>
365
422
  <DataPanel title="Roles" result={roles.data}>{(data) => <JsonView value={data} />}</DataPanel>
366
423
  <DataPanel title="Policies" result={policies.data}>{(data) => <JsonView value={data} />}</DataPanel>
424
+ <DataPanel title="Product hardening" result={hardening.data}>{(data) => <JsonView value={data} />}</DataPanel>
367
425
  <DataPanel title="Security overview" result={security.data}>{(data) => <JsonView value={data} />}</DataPanel>
368
426
  <DataPanel title="Private VPC" result={vpc.data} className="xl:col-span-2">
369
427
  {(data) => (
@@ -19,7 +19,7 @@ from .storage import (
19
19
  storage_from_env,
20
20
  )
21
21
 
22
- __version__ = "4.2.0"
22
+ __version__ = "4.3.1"
23
23
 
24
24
  __all__ = [
25
25
  "AssembledContext",