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.
- package/README.md +28 -21
- package/bin/ltcai.js +6 -2
- package/docs/CHANGELOG.md +72 -0
- package/docs/V4_3_PORTABILITY_ARCHITECTURE.md +69 -0
- package/docs/V4_3_PRIVACY_AUDIT.md +60 -0
- package/docs/V4_3_PRODUCT_HARDENING_REPORT.md +53 -0
- package/docs/V4_3_VALIDATION_REPORT.md +58 -0
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +19 -25
- package/frontend/openapi.json +213 -1
- package/frontend/src/App.tsx +15 -1
- package/frontend/src/api/client.ts +26 -1
- package/frontend/src/api/openapi.ts +268 -0
- package/frontend/src/pages/Act.tsx +63 -2
- package/frontend/src/pages/Library.tsx +9 -3
- package/frontend/src/pages/System.tsx +58 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/archive.py +360 -47
- package/lattice_brain/storage/sqlite.py +15 -2
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +11 -0
- package/latticeai/api/agents.py +3 -1
- package/latticeai/api/models.py +66 -18
- package/latticeai/api/portability.py +59 -2
- package/latticeai/app_factory.py +9 -0
- 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 +4 -2
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/multi_agent.py +1 -1
- package/latticeai/core/product_hardening.py +218 -0
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/services/agent_runtime.py +52 -12
- package/latticeai/services/kg_portability.py +147 -4
- package/latticeai/services/model_runtime.py +83 -2
- package/ltcai_cli.py +16 -4
- package/package.json +5 -4
- package/requirements.txt +17 -0
- package/scripts/clean_release_artifacts.mjs +27 -0
- package/scripts/lint_frontend.mjs +5 -0
- package/scripts/validate_release_artifacts.py +10 -0
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/main.rs +356 -24
- package/src-tauri/tauri.conf.json +20 -1
- package/static/app/asset-manifest.json +5 -5
- package/static/app/assets/{index-C_HAkbAg.js → index-BhPuj8rT.js} +45 -45
- package/static/app/assets/index-BhPuj8rT.js.map +1 -0
- package/static/app/assets/{index-CDjiH_se.css → index-yZswHE3d.css} +1 -1
- package/static/app/index.html +2 -2
- 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
|
-
|
|
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-
|
|
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" :
|
|
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>
|
|
@@ -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) => (
|