ltcai 11.5.2 → 11.7.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 +93 -148
- package/bin/ltcai.js +234 -24
- package/docs/CHANGELOG.md +119 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +8 -6
- package/docs/ENTERPRISE.md +2 -1
- package/docs/MULTI_AGENT_RUNTIME.md +12 -5
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +6 -3
- package/docs/REALTIME_COLLABORATION.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/WORKFLOW_DESIGNER.md +3 -2
- package/docs/kg-schema.md +1 -1
- package/docs/v11.6.0_ONE_DOOR_PLAN.md +170 -0
- package/lattice_brain/__init__.py +42 -79
- package/lattice_brain/graph/__init__.py +9 -24
- package/lattice_brain/graph/_kg_common/__init__.py +13 -24
- package/lattice_brain/ingestion/__init__.py +19 -58
- package/lattice_brain/ingestion/pipeline.py +20 -398
- package/lattice_brain/multimodal/__init__.py +7 -18
- package/lattice_brain/multimodal/images.py +6 -264
- package/lattice_brain/multimodal/video.py +8 -247
- package/lattice_brain/runtime/__init__.py +8 -79
- package/lattice_brain/runtime/hooks.py +22 -584
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agent_worker_seam.py +7 -80
- package/latticeai/api/health.py +6 -20
- package/latticeai/api/local_files.py +16 -613
- package/latticeai/api/models.py +12 -87
- package/latticeai/api/search.py +17 -255
- package/latticeai/api/tools.py +69 -750
- package/latticeai/api/voice_capture.py +8 -70
- package/latticeai/api/worker_compute.py +842 -0
- package/latticeai/api/worker_seams.py +216 -0
- package/latticeai/app_factory.py +23 -232
- package/latticeai/cli/entrypoint.py +36 -249
- package/latticeai/core/agent_permission.py +16 -91
- package/latticeai/core/messages.py +89 -531
- package/latticeai/runtime/access_runtime.py +0 -14
- package/latticeai/runtime/bootstrap.py +10 -21
- package/latticeai/runtime/brain_runtime.py +35 -43
- package/latticeai/runtime/build_phases/__init__.py +21 -37
- package/latticeai/runtime/build_phases/features.py +99 -389
- package/latticeai/runtime/build_phases/foundation.py +82 -392
- package/latticeai/runtime/build_phases/web.py +91 -408
- package/latticeai/runtime/build_phases/worker_profile.py +244 -0
- package/latticeai/runtime/lifespan_runtime.py +7 -16
- package/latticeai/runtime/platform_services_runtime.py +1 -15
- package/latticeai/runtime/runtime_context.py +13 -130
- package/latticeai/runtime/security_runtime.py +10 -103
- package/latticeai/services/architecture_readiness.py +82 -43
- package/latticeai/services/model_runtime/__init__.py +2 -11
- package/latticeai/services/model_runtime/service.py +4 -28
- package/latticeai/services/p_reinforce.py +10 -261
- package/latticeai/services/product_readiness.py +31 -38
- package/latticeai/services/search_service.py +37 -795
- package/latticeai/services/tool_dispatch.py +30 -386
- package/latticeai/services/voice_capture.py +13 -107
- package/latticeai/tools/__init__.py +22 -49
- package/latticeai/tools/commands.py +0 -163
- package/latticeai/tools/computer.py +0 -39
- package/latticeai/tools/documents.py +1 -134
- package/latticeai/tools/filesystem.py +1 -247
- package/latticeai/tools/knowledge.py +1 -52
- package/latticeai/tools/local_files.py +0 -20
- package/latticeai/worker_app.py +75 -0
- package/package.json +2 -3
- package/requirements.txt +0 -5
- package/scripts/agent_eval.py +16 -28
- package/scripts/brain_quality_eval.py +20 -183
- package/scripts/bump_version.py +5 -5
- package/scripts/check_current_release_docs.mjs +7 -4
- package/scripts/check_openapi_drift.mjs +10 -0
- package/scripts/check_server_i18n.mjs +2 -18
- package/scripts/compose_openapi.py +377 -0
- package/scripts/export_openapi.py +32 -4
- package/scripts/gen_messages_catalog_fixture.py +302 -0
- package/scripts/gen_openapi_fragments.py +365 -0
- package/scripts/gen_redact_fixture.py +196 -0
- package/scripts/gen_worker_allowlist_fixture.py +115 -0
- package/scripts/generate_agent_parity_fixtures.py +33 -14
- package/scripts/openapi_route_families.json +2161 -0
- package/scripts/release_screen_claims.json +54 -0
- package/scripts/run_integration_tests.mjs +134 -29
- package/scripts/run_sidecar_e2e.mjs +104 -11
- package/scripts/wheel_smoke.py +32 -22
- package/src-tauri/Cargo.lock +375 -9
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/backend.rs +151 -45
- package/src-tauri/src/main.rs +18 -18
- package/src-tauri/src/topology.rs +58 -88
- package/src-tauri/tauri.conf.json +1 -2
- package/static/app/asset-manifest.json +41 -41
- package/static/app/assets/{Act-DcQizkl1.js → Act-BPcVAbOL.js} +1 -1
- package/static/app/assets/AdminConsole-Bw1ATQL0.js +1 -0
- package/static/app/assets/{Brain-3VCSHFcn.js → Brain-CT92Kos0.js} +2 -2
- package/static/app/assets/{BrainHome-Qm8eaztx.js → BrainHome-CFBkt1K_.js} +1 -1
- package/static/app/assets/{BrainSignals-DS9BtKOW.js → BrainSignals-ReLWF2H8.js} +1 -1
- package/static/app/assets/Capture-BsTokYkk.js +1 -0
- package/static/app/assets/{Chronicle-BGvuAchH.js → Chronicle-B6f0T9id.js} +1 -1
- package/static/app/assets/{CommandPalette-Bqhm0Urn.js → CommandPalette-CuvjTv1u.js} +1 -1
- package/static/app/assets/{Library-BV6NnF0a.js → Library-BGJbG9Hd.js} +1 -1
- package/static/app/assets/{LivingBrain-GzenJchP.js → LivingBrain-DGYK_Jsa.js} +1 -1
- package/static/app/assets/{ProductFlow-DEP6-vML.js → ProductFlow-DXBC6brE.js} +1 -1
- package/static/app/assets/{ReviewCard-CNZ7XjWG.js → ReviewCard-HXRle3qq.js} +2 -2
- package/static/app/assets/System-CMHSO9qM.js +1 -0
- package/static/app/assets/arrow-left-BfmkskWx.js +1 -0
- package/static/app/assets/{bot-B_K1Tdmw.js → bot-Cn8bWRuq.js} +1 -1
- package/static/app/assets/{brain-DWyaV1L1.js → brain-CQJberbE.js} +1 -1
- package/static/app/assets/{button-aTn4s84A.js → button-Ct9f2_oT.js} +1 -1
- package/static/app/assets/circle-check-DruOxB-4.js +1 -0
- package/static/app/assets/{circle-pause-xKgeGXkT.js → circle-pause-CmzC_apg.js} +1 -1
- package/static/app/assets/{circle-play-DkT6tYPX.js → circle-play-D8mW2aQ7.js} +1 -1
- package/static/app/assets/{cpu-85xYObUC.js → cpu-DZcdd0PZ.js} +1 -1
- package/static/app/assets/{download-B5Fm7YXo.js → download-bv1KEPGQ.js} +1 -1
- package/static/app/assets/{folder-open-kk2Xa52u.js → folder-open-d-Pip5gr.js} +1 -1
- package/static/app/assets/{hard-drive-DkA3zBW_.js → hard-drive-D20iavUb.js} +1 -1
- package/static/app/assets/index-D9x-kSNy.css +2 -0
- package/static/app/assets/{index-BMPdTmlY.js → index-Do83hDzJ.js} +3 -3
- package/static/app/assets/{input-B0nRf2jO.js → input-BLXVNmj1.js} +1 -1
- package/static/app/assets/{link-2-Dwb4gnTc.js → link-2-BPJOFlAy.js} +1 -1
- package/static/app/assets/{permissionCopy-CQDUBrOZ.js → permissionCopy-ChdJd493.js} +1 -1
- package/static/app/assets/primitives-Cv5tbZBY.js +1 -0
- package/static/app/assets/search-CT9aho2j.js +1 -0
- package/static/app/assets/{share-2-BsrxFglO.js → share-2-YNX_NtMU.js} +1 -1
- package/static/app/assets/{shield-alert-5BStfp2_.js → shield-alert-DuQ3zrVL.js} +1 -1
- package/static/app/assets/{textarea-Cg8IUA-k.js → textarea-DqwLnli4.js} +1 -1
- package/static/app/assets/{useFocusTrap-CYKvE46M.js → useFocusTrap-ZVI98jaW.js} +1 -1
- package/static/app/assets/{useMutation-CSn9t1op.js → useMutation-CVC4qv_D.js} +1 -1
- package/static/app/assets/{useQuery-CY2OI2uy.js → useQuery-C7BeG4HU.js} +1 -1
- package/static/app/assets/{utils-Ddol2RWD.js → utils-CiFtIdZq.js} +1 -1
- package/static/app/assets/{workspace-BqDwOz_p.js → workspace-DQz9vIId.js} +1 -1
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/lattice_brain/archive.py +0 -522
- package/lattice_brain/context.py +0 -325
- package/lattice_brain/conversations.py +0 -382
- package/lattice_brain/core.py +0 -82
- package/lattice_brain/graph/_kg_contract.py +0 -249
- package/lattice_brain/graph/curator.py +0 -676
- package/lattice_brain/graph/discovery.py +0 -595
- package/lattice_brain/graph/discovery_index/__init__.py +0 -35
- package/lattice_brain/graph/discovery_index/cleanup.py +0 -182
- package/lattice_brain/graph/discovery_index/extract.py +0 -137
- package/lattice_brain/graph/discovery_index/scan.py +0 -411
- package/lattice_brain/graph/discovery_index/upsert.py +0 -495
- package/lattice_brain/graph/documents.py +0 -380
- package/lattice_brain/graph/fusion.py +0 -395
- package/lattice_brain/graph/identity.py +0 -175
- package/lattice_brain/graph/image_vectors.py +0 -230
- package/lattice_brain/graph/ingest.py +0 -829
- package/lattice_brain/graph/network.py +0 -205
- package/lattice_brain/graph/proactive.py +0 -724
- package/lattice_brain/graph/projection/__init__.py +0 -42
- package/lattice_brain/graph/projection/curation.py +0 -500
- package/lattice_brain/graph/projection/v2_schema.py +0 -518
- package/lattice_brain/graph/provenance.py +0 -524
- package/lattice_brain/graph/rerank.py +0 -163
- package/lattice_brain/graph/retrieval/__init__.py +0 -54
- package/lattice_brain/graph/retrieval/context.py +0 -197
- package/lattice_brain/graph/retrieval/graph_view.py +0 -319
- package/lattice_brain/graph/retrieval/hybrid.py +0 -488
- package/lattice_brain/graph/retrieval/maintenance.py +0 -121
- package/lattice_brain/graph/retrieval/signals.py +0 -95
- package/lattice_brain/graph/retrieval_docgen.py +0 -253
- package/lattice_brain/graph/retrieval_policy.py +0 -180
- package/lattice_brain/graph/retrieval_reads.py +0 -769
- package/lattice_brain/graph/retrieval_vector/__init__.py +0 -42
- package/lattice_brain/graph/retrieval_vector/fingerprint.py +0 -97
- package/lattice_brain/graph/retrieval_vector/indexing.py +0 -347
- package/lattice_brain/graph/retrieval_vector/search.py +0 -560
- package/lattice_brain/graph/retrieval_vector/status.py +0 -374
- package/lattice_brain/graph/schema.py +0 -792
- package/lattice_brain/graph/store.py +0 -268
- package/lattice_brain/graph/vector_index/__init__.py +0 -85
- package/lattice_brain/graph/vector_index/base.py +0 -167
- package/lattice_brain/graph/vector_index/brute_force.py +0 -110
- package/lattice_brain/graph/vector_index/hnsw.py +0 -290
- package/lattice_brain/graph/vector_index/jobs.py +0 -287
- package/lattice_brain/graph/vector_index/quantized.py +0 -148
- package/lattice_brain/graph/vector_index/selector.py +0 -161
- package/lattice_brain/graph/write_master.py +0 -308
- package/lattice_brain/ingestion/_contract.py +0 -90
- package/lattice_brain/ingestion/folder_scan.py +0 -57
- package/lattice_brain/ingestion/folders.py +0 -258
- package/lattice_brain/ingestion/jobs_api.py +0 -107
- package/lattice_brain/ingestion/routing.py +0 -295
- package/lattice_brain/ingestion_jobs.py +0 -380
- package/lattice_brain/memory.py +0 -75
- package/lattice_brain/portability/__init__.py +0 -90
- package/lattice_brain/portability/_contract.py +0 -42
- package/lattice_brain/portability/backups.py +0 -338
- package/lattice_brain/portability/bundles.py +0 -136
- package/lattice_brain/portability/constants.py +0 -93
- package/lattice_brain/portability/fsops.py +0 -138
- package/lattice_brain/portability/service.py +0 -41
- package/lattice_brain/portability/sharing.py +0 -710
- package/lattice_brain/quality.py +0 -543
- package/lattice_brain/retrieval_benchmark_fixtures.py +0 -92
- package/lattice_brain/runtime/agent_runtime.py +0 -859
- package/lattice_brain/runtime/contracts.py +0 -460
- package/lattice_brain/runtime/multi_agent.py +0 -942
- package/lattice_brain/runtime/statuses.py +0 -10
- package/lattice_brain/sealed_box.py +0 -240
- package/lattice_brain/self_model.py +0 -675
- package/lattice_brain/sensitivity.py +0 -94
- package/lattice_brain/storage/__init__.py +0 -22
- package/lattice_brain/storage/base.py +0 -100
- package/lattice_brain/storage/docker.py +0 -105
- package/lattice_brain/storage/factory.py +0 -31
- package/lattice_brain/storage/migration.py +0 -191
- package/lattice_brain/storage/postgres.py +0 -123
- package/lattice_brain/storage/sqlite.py +0 -143
- package/lattice_brain/synthesis.py +0 -824
- package/lattice_brain/workflow.py +0 -497
- package/latticeai/api/admin.py +0 -471
- package/latticeai/api/agent_registry.py +0 -105
- package/latticeai/api/agents.py +0 -228
- package/latticeai/api/auth.py +0 -383
- package/latticeai/api/automation_intelligence.py +0 -401
- package/latticeai/api/brain_intelligence.py +0 -199
- package/latticeai/api/browser.py +0 -493
- package/latticeai/api/change_proposals.py +0 -89
- package/latticeai/api/chat.py +0 -572
- package/latticeai/api/chat_agent_http.py +0 -892
- package/latticeai/api/chat_contracts.py +0 -73
- package/latticeai/api/chat_documents.py +0 -276
- package/latticeai/api/chat_helpers.py +0 -460
- package/latticeai/api/chat_history.py +0 -101
- package/latticeai/api/chat_hybrid.py +0 -113
- package/latticeai/api/chat_intents.py +0 -646
- package/latticeai/api/chat_stream.py +0 -216
- package/latticeai/api/chronicle.py +0 -63
- package/latticeai/api/command_center.py +0 -51
- package/latticeai/api/computer_use.py +0 -474
- package/latticeai/api/evidence_actions.py +0 -48
- package/latticeai/api/features.py +0 -70
- package/latticeai/api/funnel_metrics.py +0 -31
- package/latticeai/api/garden.py +0 -34
- package/latticeai/api/hooks.py +0 -165
- package/latticeai/api/index_jobs.py +0 -145
- package/latticeai/api/invitations.py +0 -100
- package/latticeai/api/knowledge_graph.py +0 -536
- package/latticeai/api/marketplace.py +0 -105
- package/latticeai/api/mcp.py +0 -482
- package/latticeai/api/memory.py +0 -270
- package/latticeai/api/network.py +0 -81
- package/latticeai/api/network_boundary.py +0 -225
- package/latticeai/api/permission_mode.py +0 -61
- package/latticeai/api/permissions.py +0 -436
- package/latticeai/api/plugins.py +0 -126
- package/latticeai/api/portability.py +0 -391
- package/latticeai/api/project_sessions.py +0 -114
- package/latticeai/api/realtime.py +0 -118
- package/latticeai/api/review_queue.py +0 -364
- package/latticeai/api/security_dashboard.py +0 -604
- package/latticeai/api/setup.py +0 -319
- package/latticeai/api/static_routes.py +0 -354
- package/latticeai/api/ui_redirects.py +0 -26
- package/latticeai/api/workflow_designer.py +0 -394
- package/latticeai/api/workspace.py +0 -856
- package/latticeai/api/workspace_scope.py +0 -125
- package/latticeai/core/agent/__init__.py +0 -93
- package/latticeai/core/agent/_contract.py +0 -79
- package/latticeai/core/agent/context.py +0 -57
- package/latticeai/core/agent/deps.py +0 -125
- package/latticeai/core/agent/execution.py +0 -622
- package/latticeai/core/agent/planning.py +0 -145
- package/latticeai/core/agent/recovery.py +0 -157
- package/latticeai/core/agent/runtime.py +0 -210
- package/latticeai/core/agent/verification.py +0 -231
- package/latticeai/core/agent_eval.py +0 -739
- package/latticeai/core/agent_helpers.py +0 -493
- package/latticeai/core/agent_profiles.py +0 -110
- package/latticeai/core/agent_prompts.py +0 -171
- package/latticeai/core/agent_registry.py +0 -232
- package/latticeai/core/agent_state.py +0 -41
- package/latticeai/core/agent_trace.py +0 -104
- package/latticeai/core/artifact_ledger.py +0 -109
- package/latticeai/core/audit.py +0 -260
- package/latticeai/core/builtin_hooks.py +0 -105
- package/latticeai/core/context_builder.py +0 -394
- package/latticeai/core/document_generator.py +0 -103
- package/latticeai/core/enterprise.py +0 -154
- package/latticeai/core/enterprise_admin.py +0 -158
- package/latticeai/core/file_generation/__init__.py +0 -115
- package/latticeai/core/file_generation/bundles.py +0 -76
- package/latticeai/core/file_generation/extraction.py +0 -154
- package/latticeai/core/file_generation/inference.py +0 -235
- package/latticeai/core/file_generation/orchestration.py +0 -152
- package/latticeai/core/file_generation/prompting.py +0 -117
- package/latticeai/core/file_generation/repair.py +0 -114
- package/latticeai/core/file_generation/sanitize.py +0 -61
- package/latticeai/core/file_generation/validation.py +0 -201
- package/latticeai/core/invitations.py +0 -132
- package/latticeai/core/legacy_compatibility.py +0 -243
- package/latticeai/core/logging_safety.py +0 -46
- package/latticeai/core/marketplace.py +0 -293
- package/latticeai/core/mcp_catalog.py +0 -452
- package/latticeai/core/mcp_registry.py +0 -506
- package/latticeai/core/network_boundary.py +0 -168
- package/latticeai/core/oidc.py +0 -208
- package/latticeai/core/plugins.py +0 -432
- package/latticeai/core/product_hardening.py +0 -218
- package/latticeai/core/project_sessions.py +0 -337
- package/latticeai/core/realtime.py +0 -238
- package/latticeai/core/run_explain.py +0 -426
- package/latticeai/core/run_store.py +0 -252
- package/latticeai/core/timezones.py +0 -80
- package/latticeai/core/workspace_computer_memory.py +0 -84
- package/latticeai/core/workspace_graph_trace.py +0 -155
- package/latticeai/core/workspace_indexing.py +0 -102
- package/latticeai/core/workspace_memory.py +0 -77
- package/latticeai/core/workspace_onboarding.py +0 -104
- package/latticeai/core/workspace_os.py +0 -978
- package/latticeai/core/workspace_os_constants.py +0 -126
- package/latticeai/core/workspace_os_state.py +0 -180
- package/latticeai/core/workspace_os_utils.py +0 -103
- package/latticeai/core/workspace_permissions.py +0 -101
- package/latticeai/core/workspace_plugins.py +0 -97
- package/latticeai/core/workspace_relationships.py +0 -99
- package/latticeai/core/workspace_reorganization.py +0 -335
- package/latticeai/core/workspace_review_items.py +0 -112
- package/latticeai/core/workspace_runs.py +0 -726
- package/latticeai/core/workspace_skills.py +0 -109
- package/latticeai/core/workspace_snapshots.py +0 -198
- package/latticeai/core/workspace_timeline.py +0 -110
- package/latticeai/integrations/__init__.py +0 -0
- package/latticeai/integrations/telegram_bot/__init__.py +0 -123
- package/latticeai/integrations/telegram_bot/__main__.py +0 -17
- package/latticeai/integrations/telegram_bot/config.py +0 -86
- package/latticeai/integrations/telegram_bot/dispatch.py +0 -311
- package/latticeai/integrations/telegram_bot/flows.py +0 -478
- package/latticeai/integrations/telegram_bot/helpers.py +0 -322
- package/latticeai/integrations/telegram_bot/screens.py +0 -394
- package/latticeai/runtime/audit_runtime.py +0 -76
- package/latticeai/runtime/automation_runtime.py +0 -81
- package/latticeai/runtime/chat_wiring.py +0 -141
- package/latticeai/runtime/context_runtime.py +0 -66
- package/latticeai/runtime/feature_toggle_wiring.py +0 -157
- package/latticeai/runtime/history_runtime.py +0 -163
- package/latticeai/runtime/history_writer.py +0 -138
- package/latticeai/runtime/hooks_runtime.py +0 -77
- package/latticeai/runtime/model_wiring.py +0 -68
- package/latticeai/runtime/namespace_runtime.py +0 -163
- package/latticeai/runtime/network_boundary_wiring.py +0 -117
- package/latticeai/runtime/network_config_runtime.py +0 -56
- package/latticeai/runtime/permission_mode_wiring.py +0 -112
- package/latticeai/runtime/persistence_runtime.py +0 -159
- package/latticeai/runtime/platform_runtime_wiring.py +0 -89
- package/latticeai/runtime/review_wiring.py +0 -42
- package/latticeai/runtime/router_registration.py +0 -693
- package/latticeai/runtime/service_singletons.py +0 -55
- package/latticeai/runtime/sso_config_runtime.py +0 -128
- package/latticeai/runtime/user_key_runtime.py +0 -106
- package/latticeai/runtime/web_runtime.py +0 -92
- package/latticeai/server_app.py +0 -51
- package/latticeai/services/app_context.py +0 -130
- package/latticeai/services/automation_execution.py +0 -266
- package/latticeai/services/automation_intelligence.py +0 -614
- package/latticeai/services/brain_automation.py +0 -191
- package/latticeai/services/brain_intelligence/__init__.py +0 -58
- package/latticeai/services/brain_intelligence/_contract.py +0 -71
- package/latticeai/services/brain_intelligence/consistency.py +0 -193
- package/latticeai/services/brain_intelligence/constants.py +0 -47
- package/latticeai/services/brain_intelligence/digest.py +0 -258
- package/latticeai/services/brain_intelligence/health.py +0 -331
- package/latticeai/services/brain_intelligence/proposals.py +0 -259
- package/latticeai/services/brain_intelligence/sampling.py +0 -84
- package/latticeai/services/brain_intelligence/service.py +0 -48
- package/latticeai/services/change_proposals.py +0 -471
- package/latticeai/services/chat_service.py +0 -243
- package/latticeai/services/chronicle.py +0 -555
- package/latticeai/services/cloud_egress_audit.py +0 -85
- package/latticeai/services/cloud_extraction.py +0 -129
- package/latticeai/services/cloud_streaming.py +0 -268
- package/latticeai/services/cloud_token_guard.py +0 -84
- package/latticeai/services/command_center.py +0 -548
- package/latticeai/services/evidence_actions.py +0 -258
- package/latticeai/services/feature_toggles.py +0 -502
- package/latticeai/services/folder_watch.py +0 -520
- package/latticeai/services/funnel_metrics.py +0 -307
- package/latticeai/services/hybrid_chat.py +0 -316
- package/latticeai/services/hybrid_context.py +0 -228
- package/latticeai/services/hybrid_policy.py +0 -129
- package/latticeai/services/interop_bridges.py +0 -978
- package/latticeai/services/local_knowledge.py +0 -465
- package/latticeai/services/memory_service/__init__.py +0 -52
- package/latticeai/services/memory_service/_contract.py +0 -100
- package/latticeai/services/memory_service/brief.py +0 -431
- package/latticeai/services/memory_service/constants.py +0 -57
- package/latticeai/services/memory_service/maintenance.py +0 -138
- package/latticeai/services/memory_service/manager.py +0 -186
- package/latticeai/services/memory_service/proof.py +0 -136
- package/latticeai/services/memory_service/recall.py +0 -225
- package/latticeai/services/memory_service/service.py +0 -48
- package/latticeai/services/memory_service/stores.py +0 -110
- package/latticeai/services/mode_store.py +0 -132
- package/latticeai/services/model_recommendation.py +0 -224
- package/latticeai/services/model_runtime/cloud.py +0 -87
- package/latticeai/services/network_boundary_service.py +0 -117
- package/latticeai/services/obsidian_bridge.py +0 -609
- package/latticeai/services/openai_compatible_adapter.py +0 -101
- package/latticeai/services/permission_mode_service.py +0 -122
- package/latticeai/services/platform_runtime.py +0 -366
- package/latticeai/services/review_queue.py +0 -380
- package/latticeai/services/router_context.py +0 -59
- package/latticeai/services/run_executor.py +0 -387
- package/latticeai/services/self_model_service.py +0 -171
- package/latticeai/services/setup_detection.py +0 -147
- package/latticeai/services/triggers.py +0 -378
- package/latticeai/services/upload_service.py +0 -172
- package/latticeai/services/workspace_service.py +0 -165
- package/latticeai/setup/__init__.py +0 -25
- package/latticeai/setup/auto_setup.py +0 -846
- package/latticeai/setup/demo_corpus.py +0 -98
- package/latticeai/setup/wizard/__init__.py +0 -126
- package/latticeai/setup/wizard/catalog.py +0 -175
- package/latticeai/setup/wizard/detect.py +0 -302
- package/latticeai/setup/wizard/install.py +0 -348
- package/latticeai/setup/wizard/paths.py +0 -165
- package/latticeai/setup/wizard/plans.py +0 -74
- package/latticeai/setup/wizard/recommend.py +0 -320
- package/scripts/bench_agent_smoke.py +0 -409
- package/scripts/bench_models.py +0 -540
- package/scripts/bench_vector_index.py +0 -295
- package/scripts/funnel_soft_gate.py +0 -192
- package/scripts/generate_agent_loop_fixtures.py +0 -994
- package/scripts/generate_rust_parity_fixtures.py +0 -908
- package/scripts/migrate_brain_storage.py +0 -57
- package/scripts/parity_fixture_corpus_context.py +0 -162
- package/scripts/parity_fixture_corpus_docgen.py +0 -341
- package/scripts/profile_kg.py +0 -355
- package/server.py +0 -30
- package/static/app/assets/AdminConsole-cf4npybT.js +0 -1
- package/static/app/assets/Capture-DiQ219jW.js +0 -1
- package/static/app/assets/System-CieofHQa.js +0 -1
- package/static/app/assets/arrow-left-kfsrk0mv.js +0 -1
- package/static/app/assets/circle-check-qqLug9nU.js +0 -1
- package/static/app/assets/index-DxmOfNRi.css +0 -2
- package/static/app/assets/primitives-SNp0LRJz.js +0 -1
- package/static/app/assets/search-BcHqkjoy.js +0 -1
|
@@ -1,710 +0,0 @@
|
|
|
1
|
-
"""Logical export/import, and the opt-in Brain Network subgraph share.
|
|
2
|
-
|
|
3
|
-
The half of :class:`~lattice_brain.portability.KGPortabilityService` that moves
|
|
4
|
-
*knowledge* rather than *files*: a whole-graph JSON artifact, and — behind the
|
|
5
|
-
``LATTICEAI_BRAIN_NETWORK`` opt-in — a signed, encrypted slice of the graph that
|
|
6
|
-
arrives at the far end as review proposals, never as a merge.
|
|
7
|
-
|
|
8
|
-
The receiving X25519 key is loaded here (``load_recipient_identity``), so a test
|
|
9
|
-
standing in for it patches *this* module rather than the package.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
from __future__ import annotations
|
|
13
|
-
|
|
14
|
-
import base64
|
|
15
|
-
import json
|
|
16
|
-
import os
|
|
17
|
-
from pathlib import Path
|
|
18
|
-
from typing import Any, Dict, Iterable, List, Optional
|
|
19
|
-
|
|
20
|
-
from cryptography.exceptions import InvalidTag
|
|
21
|
-
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
22
|
-
|
|
23
|
-
from ..archive import KDF_ITERATIONS, _derive_key
|
|
24
|
-
from ..quiet import quiet
|
|
25
|
-
from ..sealed_box import (
|
|
26
|
-
SEALED_BOX_ALGORITHM,
|
|
27
|
-
load_recipient_identity,
|
|
28
|
-
public_key_fingerprint,
|
|
29
|
-
seal,
|
|
30
|
-
)
|
|
31
|
-
from ..utils import utc_now_iso
|
|
32
|
-
from ._contract import PortabilityCore as _Core
|
|
33
|
-
from .bundles import (
|
|
34
|
-
_canonical_digest,
|
|
35
|
-
_expand_neighbors,
|
|
36
|
-
_redact_node,
|
|
37
|
-
_redact_provenance_row,
|
|
38
|
-
_scope_node,
|
|
39
|
-
_select_node_ids,
|
|
40
|
-
_shared_node_summary,
|
|
41
|
-
)
|
|
42
|
-
from .constants import (
|
|
43
|
-
_OPEN_REVIEW_STATUSES,
|
|
44
|
-
_PAYLOAD_KEYS,
|
|
45
|
-
_REDACTED_FIELDS,
|
|
46
|
-
BRAIN_NETWORK_DISABLED_DETAIL,
|
|
47
|
-
BRAIN_NETWORK_ENV,
|
|
48
|
-
BRAIN_NETWORK_GATE,
|
|
49
|
-
ENCRYPTION_MODES,
|
|
50
|
-
FORMAT,
|
|
51
|
-
FORMAT_VERSION,
|
|
52
|
-
SUBGRAPH_ARCHIVE_FORMAT,
|
|
53
|
-
SUBGRAPH_FORMAT,
|
|
54
|
-
SUBGRAPH_FORMAT_VERSION,
|
|
55
|
-
SUBGRAPH_PROPOSAL_CAP,
|
|
56
|
-
SUBGRAPH_REVIEW_KIND,
|
|
57
|
-
SUBGRAPH_REVIEW_SOURCE,
|
|
58
|
-
_stamp,
|
|
59
|
-
brain_network_enabled,
|
|
60
|
-
require_brain_network,
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
class KGPortabilitySharingMixin(_Core):
|
|
65
|
-
"""Export / import / share. Mixed into ``KGPortabilityService``."""
|
|
66
|
-
|
|
67
|
-
def _recipient_identity(self) -> Any:
|
|
68
|
-
"""This Brain's X25519 receiving key, created on first use.
|
|
69
|
-
|
|
70
|
-
``None`` when the data directory cannot hold one — reported as a state
|
|
71
|
-
by :meth:`recipient_key`, never raised into a status read.
|
|
72
|
-
"""
|
|
73
|
-
if not self._recipient_loaded:
|
|
74
|
-
self._recipient = load_recipient_identity(self._data_dir)
|
|
75
|
-
self._recipient_loaded = True
|
|
76
|
-
return self._recipient
|
|
77
|
-
|
|
78
|
-
def recipient_key(self) -> Dict[str, Any]:
|
|
79
|
-
"""The public half to hand a sender, so they can seal a bundle to us.
|
|
80
|
-
|
|
81
|
-
Publishing this is safe by construction: a public key encrypts, it does
|
|
82
|
-
not decrypt, and it says nothing about what this Brain contains.
|
|
83
|
-
"""
|
|
84
|
-
require_brain_network()
|
|
85
|
-
identity = self._recipient_identity()
|
|
86
|
-
if identity is None:
|
|
87
|
-
return {
|
|
88
|
-
"available": False,
|
|
89
|
-
"detail": (
|
|
90
|
-
"a receiving key could not be created in the data directory; "
|
|
91
|
-
"sealed bundles cannot be opened on this machine"
|
|
92
|
-
),
|
|
93
|
-
}
|
|
94
|
-
return {"available": True, **identity.describe()}
|
|
95
|
-
|
|
96
|
-
# ── logical export / import ──────────────────────────────────────────────
|
|
97
|
-
def export(
|
|
98
|
-
self,
|
|
99
|
-
*,
|
|
100
|
-
workspace_id: Optional[str] = None,
|
|
101
|
-
include_legacy_global: bool = False,
|
|
102
|
-
) -> Dict[str, Any]:
|
|
103
|
-
self._require()
|
|
104
|
-
data = self._kg.export_graph_data(
|
|
105
|
-
workspace_id=workspace_id,
|
|
106
|
-
include_legacy_global=include_legacy_global,
|
|
107
|
-
)
|
|
108
|
-
header = {
|
|
109
|
-
"format": FORMAT,
|
|
110
|
-
"format_version": FORMAT_VERSION,
|
|
111
|
-
**self._kg.schema_versions(),
|
|
112
|
-
"exported_at": utc_now_iso(),
|
|
113
|
-
"workspace_id": workspace_id,
|
|
114
|
-
"include_legacy_global": bool(include_legacy_global),
|
|
115
|
-
"counts": data.get("counts"),
|
|
116
|
-
}
|
|
117
|
-
artifact = {"header": header, **data}
|
|
118
|
-
if self._identity is not None:
|
|
119
|
-
artifact["signature"] = self._identity.sign_manifest(header)
|
|
120
|
-
return artifact
|
|
121
|
-
|
|
122
|
-
def export_to_file(
|
|
123
|
-
self,
|
|
124
|
-
path=None,
|
|
125
|
-
*,
|
|
126
|
-
workspace_id: Optional[str] = None,
|
|
127
|
-
include_legacy_global: bool = False,
|
|
128
|
-
) -> Dict[str, Any]:
|
|
129
|
-
artifact = self.export(
|
|
130
|
-
workspace_id=workspace_id,
|
|
131
|
-
include_legacy_global=include_legacy_global,
|
|
132
|
-
)
|
|
133
|
-
self._exports_dir.mkdir(parents=True, exist_ok=True)
|
|
134
|
-
path = Path(path) if path else self._exports_dir / f"kg-export-{_stamp()}.json"
|
|
135
|
-
path.write_text(json.dumps(artifact, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
136
|
-
return {"path": str(path), "header": artifact["header"], "bytes": path.stat().st_size}
|
|
137
|
-
|
|
138
|
-
def import_data(self, artifact: Dict[str, Any], *, mode: str = "merge", dry_run: bool = False) -> Dict[str, Any]:
|
|
139
|
-
self._require()
|
|
140
|
-
if not isinstance(artifact, dict) or "nodes" not in artifact:
|
|
141
|
-
raise ValueError("Invalid Knowledge Graph export artifact.")
|
|
142
|
-
if mode not in ("merge", "replace"):
|
|
143
|
-
raise ValueError("mode must be 'merge' or 'replace'.")
|
|
144
|
-
origin = "unsigned-legacy"
|
|
145
|
-
signature = artifact.get("signature")
|
|
146
|
-
if signature:
|
|
147
|
-
from ..graph.identity import verify_manifest
|
|
148
|
-
|
|
149
|
-
if not verify_manifest(artifact.get("header") or {}, signature):
|
|
150
|
-
raise ValueError("Bundle signature verification failed — refusing to import.")
|
|
151
|
-
origin = f"device:{signature.get('fingerprint') or 'unknown'}"
|
|
152
|
-
result = self._kg.import_graph_data(artifact, mode=mode, dry_run=dry_run)
|
|
153
|
-
result["header"] = artifact.get("header")
|
|
154
|
-
result["origin"] = origin
|
|
155
|
-
result["signed"] = bool(signature)
|
|
156
|
-
if not dry_run:
|
|
157
|
-
try:
|
|
158
|
-
self._kg.record_provenance(
|
|
159
|
-
node_id="import:" + str((artifact.get("header") or {}).get("exported_at") or utc_now_iso()),
|
|
160
|
-
source_type="bundle_import",
|
|
161
|
-
pipeline="kg-portability",
|
|
162
|
-
owner=None,
|
|
163
|
-
metadata={"origin": origin, "mode": mode,
|
|
164
|
-
"counts": (artifact.get("header") or {}).get("counts")},
|
|
165
|
-
)
|
|
166
|
-
except Exception:
|
|
167
|
-
quiet()
|
|
168
|
-
return result
|
|
169
|
-
|
|
170
|
-
# ── selective subgraph share (opt-in prototype, off by default) ──────────
|
|
171
|
-
def share_status(self) -> Dict[str, Any]:
|
|
172
|
-
"""Honest read of the share surface. Safe to call while it is off.
|
|
173
|
-
|
|
174
|
-
A status route that refuses while disabled cannot tell a UI *why* the
|
|
175
|
-
feature is missing, so this one always answers — with ``enabled:
|
|
176
|
-
false`` and the flag that turns it on.
|
|
177
|
-
"""
|
|
178
|
-
enabled = brain_network_enabled()
|
|
179
|
-
return {
|
|
180
|
-
"enabled": enabled,
|
|
181
|
-
"flag": BRAIN_NETWORK_ENV,
|
|
182
|
-
"format": SUBGRAPH_FORMAT,
|
|
183
|
-
"format_version": SUBGRAPH_FORMAT_VERSION,
|
|
184
|
-
"graph_available": self.available(),
|
|
185
|
-
"signing": self._identity is not None,
|
|
186
|
-
"device": self._share_device(),
|
|
187
|
-
"proposal_cap": SUBGRAPH_PROPOSAL_CAP,
|
|
188
|
-
# Stated rather than implied: a bundle is always signed by this
|
|
189
|
-
# device, and encrypted either with a shared passphrase or — since
|
|
190
|
-
# 11.2.0 — to the recipient's own public key, so nothing secret has
|
|
191
|
-
# to travel ahead of it.
|
|
192
|
-
"encryption": list(ENCRYPTION_MODES),
|
|
193
|
-
"recipient_public_key_encryption": True,
|
|
194
|
-
"sealed_box_algorithm": SEALED_BOX_ALGORITHM,
|
|
195
|
-
"gate": BRAIN_NETWORK_GATE.describe(),
|
|
196
|
-
"detail": None if enabled else BRAIN_NETWORK_DISABLED_DETAIL,
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
def _share_device(self) -> Dict[str, Any]:
|
|
200
|
-
"""Signer identity for a bundle — key material only, no storage hints."""
|
|
201
|
-
if self._identity is None:
|
|
202
|
-
return {}
|
|
203
|
-
described = self._identity.describe()
|
|
204
|
-
return {
|
|
205
|
-
"fingerprint": described.get("fingerprint"),
|
|
206
|
-
"public_key": described.get("public_key"),
|
|
207
|
-
"algorithm": described.get("algorithm"),
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
def export_subgraph(
|
|
211
|
-
self,
|
|
212
|
-
*,
|
|
213
|
-
node_ids: Optional[Iterable[str]] = None,
|
|
214
|
-
node_types: Optional[Iterable[str]] = None,
|
|
215
|
-
source_types: Optional[Iterable[str]] = None,
|
|
216
|
-
workspace_id: Optional[str] = None,
|
|
217
|
-
include_legacy_global: bool = False,
|
|
218
|
-
include_neighbors: bool = False,
|
|
219
|
-
redact_provenance: bool = True,
|
|
220
|
-
) -> Dict[str, Any]:
|
|
221
|
-
"""A chosen slice of the graph, with provenance, signed by this device.
|
|
222
|
-
|
|
223
|
-
At least one selector is required. "Share everything" already exists as
|
|
224
|
-
:meth:`export`; a *selective* door that silently defaults to the whole
|
|
225
|
-
Brain is how an accident becomes a leak.
|
|
226
|
-
|
|
227
|
-
``include_neighbors`` widens the selection by one hop (both directions)
|
|
228
|
-
so "share this decision" can carry the decision's immediate relations.
|
|
229
|
-
``redact_provenance`` (default on) strips the sender's local identity —
|
|
230
|
-
owner, source URI, permissions — from both node metadata and the
|
|
231
|
-
provenance rows, and records that it did so in the header.
|
|
232
|
-
|
|
233
|
-
Knowledge-source rows (connected folder registrations, with their local
|
|
234
|
-
paths) are never included; they describe the sender's machine, not the
|
|
235
|
-
knowledge.
|
|
236
|
-
"""
|
|
237
|
-
require_brain_network()
|
|
238
|
-
self._require()
|
|
239
|
-
if self._identity is None:
|
|
240
|
-
raise RuntimeError(
|
|
241
|
-
"A device identity is required to share a subgraph; "
|
|
242
|
-
"an unsigned bundle cannot be attributed and would be refused."
|
|
243
|
-
)
|
|
244
|
-
wanted_ids = sorted({str(v) for v in (node_ids or []) if str(v).strip()})
|
|
245
|
-
wanted_types = sorted({str(v).strip().lower() for v in (node_types or []) if str(v).strip()})
|
|
246
|
-
wanted_sources = sorted({str(v).strip().lower() for v in (source_types or []) if str(v).strip()})
|
|
247
|
-
selection: Dict[str, Any] = {
|
|
248
|
-
"node_ids": wanted_ids,
|
|
249
|
-
"node_types": wanted_types,
|
|
250
|
-
"source_types": wanted_sources,
|
|
251
|
-
"include_neighbors": bool(include_neighbors),
|
|
252
|
-
"workspace_id": workspace_id,
|
|
253
|
-
}
|
|
254
|
-
if not (wanted_ids or wanted_types or wanted_sources):
|
|
255
|
-
raise ValueError(
|
|
256
|
-
"A subgraph share must name what to share "
|
|
257
|
-
"(node_ids, node_types, or source_types)."
|
|
258
|
-
)
|
|
259
|
-
data = self._kg.export_graph_data(
|
|
260
|
-
workspace_id=workspace_id, include_legacy_global=include_legacy_global,
|
|
261
|
-
)
|
|
262
|
-
all_nodes = data.get("nodes") or []
|
|
263
|
-
# Chunks are derived retrieval units, not shareable subjects: they are
|
|
264
|
-
# never selectable, but they travel with the node they belong to so the
|
|
265
|
-
# receiver can actually search the shared content rather than a summary.
|
|
266
|
-
nodes = [n for n in all_nodes if str(n.get("type")) != "Chunk"]
|
|
267
|
-
chunk_nodes_by_id = {
|
|
268
|
-
str(n.get("id")): n for n in all_nodes if str(n.get("type")) == "Chunk"
|
|
269
|
-
}
|
|
270
|
-
edges = list(data.get("edges") or [])
|
|
271
|
-
keep = _select_node_ids(nodes, selection)
|
|
272
|
-
present = {str(n.get("id")): str(n.get("type") or "") for n in nodes}
|
|
273
|
-
missing = [nid for nid in wanted_ids if nid not in present]
|
|
274
|
-
if include_neighbors:
|
|
275
|
-
keep = _expand_neighbors(keep, edges, present)
|
|
276
|
-
if not keep:
|
|
277
|
-
raise ValueError("The selection matched no nodes; nothing was exported.")
|
|
278
|
-
kept_nodes = [n for n in nodes if str(n.get("id")) in keep]
|
|
279
|
-
kept_chunks = [c for c in data.get("chunks") or [] if str(c.get("source_node")) in keep]
|
|
280
|
-
kept_chunk_nodes = [
|
|
281
|
-
chunk_nodes_by_id[str(c.get("id"))]
|
|
282
|
-
for c in kept_chunks
|
|
283
|
-
if str(c.get("id")) in chunk_nodes_by_id
|
|
284
|
-
]
|
|
285
|
-
carried = keep | {str(n.get("id")) for n in kept_chunk_nodes}
|
|
286
|
-
kept_edges = [
|
|
287
|
-
e for e in edges
|
|
288
|
-
if str(e.get("from_node")) in carried and str(e.get("to_node")) in carried
|
|
289
|
-
]
|
|
290
|
-
kept_prov = [p for p in data.get("provenance") or [] if str(p.get("node_id")) in keep]
|
|
291
|
-
if redact_provenance:
|
|
292
|
-
kept_nodes = [_redact_node(n) for n in kept_nodes]
|
|
293
|
-
kept_chunk_nodes = [_redact_node(n) for n in kept_chunk_nodes]
|
|
294
|
-
kept_prov = [_redact_provenance_row(p) for p in kept_prov]
|
|
295
|
-
payload = {
|
|
296
|
-
"nodes": kept_nodes,
|
|
297
|
-
"chunk_nodes": kept_chunk_nodes,
|
|
298
|
-
"edges": kept_edges,
|
|
299
|
-
"chunks": kept_chunks,
|
|
300
|
-
"knowledge_sources": [],
|
|
301
|
-
"provenance": kept_prov,
|
|
302
|
-
}
|
|
303
|
-
counts = {key: len(value) for key, value in payload.items()}
|
|
304
|
-
header = {
|
|
305
|
-
"format": SUBGRAPH_FORMAT,
|
|
306
|
-
"format_version": SUBGRAPH_FORMAT_VERSION,
|
|
307
|
-
**self._kg.schema_versions(),
|
|
308
|
-
"exported_at": utc_now_iso(),
|
|
309
|
-
"workspace_id": workspace_id,
|
|
310
|
-
"selection": selection,
|
|
311
|
-
"counts": counts,
|
|
312
|
-
"unmatched_node_ids": missing,
|
|
313
|
-
"redacted": sorted(_REDACTED_FIELDS) if redact_provenance else [],
|
|
314
|
-
"includes_knowledge_sources": False,
|
|
315
|
-
"device": self._share_device(),
|
|
316
|
-
# The signature covers the header; the header pins the payload.
|
|
317
|
-
"payload_sha256": _canonical_digest(payload),
|
|
318
|
-
}
|
|
319
|
-
return {
|
|
320
|
-
"header": header,
|
|
321
|
-
**payload,
|
|
322
|
-
"signature": self._identity.sign_manifest(header),
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
def export_subgraph_archive(
|
|
326
|
-
self,
|
|
327
|
-
dest_path=None,
|
|
328
|
-
*,
|
|
329
|
-
passphrase: Optional[str] = None,
|
|
330
|
-
recipient_public_key: Optional[str] = None,
|
|
331
|
-
**selection: Any,
|
|
332
|
-
) -> Dict[str, Any]:
|
|
333
|
-
"""Write the signed subgraph as an encrypted ``.latticebrain`` file.
|
|
334
|
-
|
|
335
|
-
Exactly one recipient mechanism, named explicitly:
|
|
336
|
-
|
|
337
|
-
* ``passphrase`` — the 11.1.0 mechanism, identical to the whole-Brain
|
|
338
|
-
archive (PBKDF2-SHA256 → AES-256-GCM). Simple, and it means a secret
|
|
339
|
-
has to reach the receiver through some other channel first.
|
|
340
|
-
* ``recipient_public_key`` — an X25519 sealed box (11.2.0). The
|
|
341
|
-
receiver publishes a public key; nothing secret travels at all.
|
|
342
|
-
|
|
343
|
-
Supplying both is refused rather than silently preferring one: which
|
|
344
|
-
key opens a bundle is not a detail to guess at. A different ``format``
|
|
345
|
-
from the whole-Brain archive keeps the two from ever being confused,
|
|
346
|
-
and the header and signature stay *outside* the ciphertext on purpose —
|
|
347
|
-
a recipient can see who sent a bundle, and how big it is, before
|
|
348
|
-
deciding to open it.
|
|
349
|
-
"""
|
|
350
|
-
mode = self._encryption_mode(passphrase, recipient_public_key)
|
|
351
|
-
artifact = self.export_subgraph(**selection)
|
|
352
|
-
self._exports_dir.mkdir(parents=True, exist_ok=True)
|
|
353
|
-
dest = Path(dest_path) if dest_path else self._exports_dir / f"subgraph-{_stamp()}.latticebrain"
|
|
354
|
-
if dest.suffix != ".latticebrain":
|
|
355
|
-
dest = dest.with_suffix(".latticebrain")
|
|
356
|
-
body = {key: value for key, value in artifact.items() if key not in {"header", "signature"}}
|
|
357
|
-
plaintext = json.dumps(body, ensure_ascii=False, sort_keys=True).encode("utf-8")
|
|
358
|
-
envelope: Dict[str, Any] = {
|
|
359
|
-
"format": SUBGRAPH_ARCHIVE_FORMAT,
|
|
360
|
-
"format_version": SUBGRAPH_FORMAT_VERSION,
|
|
361
|
-
"created_at": utc_now_iso(),
|
|
362
|
-
"encryption": mode,
|
|
363
|
-
"header": artifact["header"],
|
|
364
|
-
"signature": artifact["signature"],
|
|
365
|
-
}
|
|
366
|
-
recipient: Optional[str] = None
|
|
367
|
-
if mode == "recipient_public_key":
|
|
368
|
-
sealed = seal(plaintext, recipient_public_key=str(recipient_public_key))
|
|
369
|
-
recipient = str(sealed["recipient_fingerprint"])
|
|
370
|
-
envelope["sealed_box"] = sealed
|
|
371
|
-
else:
|
|
372
|
-
salt = os.urandom(16)
|
|
373
|
-
nonce = os.urandom(12)
|
|
374
|
-
ciphertext = AESGCM(_derive_key(str(passphrase), salt)).encrypt(nonce, plaintext, None)
|
|
375
|
-
envelope["kdf"] = {
|
|
376
|
-
"name": "PBKDF2HMAC-SHA256",
|
|
377
|
-
"iterations": KDF_ITERATIONS,
|
|
378
|
-
"salt": base64.b64encode(salt).decode("ascii"),
|
|
379
|
-
}
|
|
380
|
-
envelope["cipher"] = {
|
|
381
|
-
"name": "AES-256-GCM",
|
|
382
|
-
"nonce": base64.b64encode(nonce).decode("ascii"),
|
|
383
|
-
}
|
|
384
|
-
envelope["payload"] = base64.b64encode(ciphertext).decode("ascii")
|
|
385
|
-
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
386
|
-
dest.write_text(json.dumps(envelope, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
387
|
-
return {
|
|
388
|
-
"path": str(dest),
|
|
389
|
-
"bytes": dest.stat().st_size,
|
|
390
|
-
"encrypted": True,
|
|
391
|
-
"encryption": mode,
|
|
392
|
-
"recipient_fingerprint": recipient,
|
|
393
|
-
"format": SUBGRAPH_ARCHIVE_FORMAT,
|
|
394
|
-
"header": artifact["header"],
|
|
395
|
-
"counts": artifact["header"]["counts"],
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
@staticmethod
|
|
399
|
-
def _encryption_mode(
|
|
400
|
-
passphrase: Optional[str], recipient_public_key: Optional[str]
|
|
401
|
-
) -> str:
|
|
402
|
-
"""``passphrase`` | ``recipient_public_key`` — exactly one, validated."""
|
|
403
|
-
has_passphrase = bool(str(passphrase or "").strip())
|
|
404
|
-
wanted_key = str(recipient_public_key or "").strip()
|
|
405
|
-
if has_passphrase and wanted_key:
|
|
406
|
-
raise ValueError(
|
|
407
|
-
"Choose one: a passphrase, or a recipient public key. "
|
|
408
|
-
"A bundle sealed both ways would hide which key really opens it."
|
|
409
|
-
)
|
|
410
|
-
if wanted_key:
|
|
411
|
-
public_key_fingerprint(wanted_key) # validates; raises on garbage
|
|
412
|
-
return "recipient_public_key"
|
|
413
|
-
if has_passphrase:
|
|
414
|
-
return "passphrase"
|
|
415
|
-
raise ValueError(
|
|
416
|
-
"A subgraph bundle must be encrypted: supply a passphrase or the "
|
|
417
|
-
"recipient's public key."
|
|
418
|
-
)
|
|
419
|
-
|
|
420
|
-
def read_subgraph_archive(
|
|
421
|
-
self, path, *, passphrase: Optional[str] = None
|
|
422
|
-
) -> Dict[str, Any]:
|
|
423
|
-
"""Decrypt an encrypted subgraph bundle back into a signed artifact.
|
|
424
|
-
|
|
425
|
-
The envelope says how it was sealed, so the reader never has to guess:
|
|
426
|
-
a passphrase bundle needs the passphrase, a sealed-box bundle needs
|
|
427
|
-
this Brain's own receiving key and no passphrase at all.
|
|
428
|
-
"""
|
|
429
|
-
require_brain_network()
|
|
430
|
-
source = Path(path)
|
|
431
|
-
if not source.exists():
|
|
432
|
-
raise FileNotFoundError(f"Subgraph bundle not found: {source}")
|
|
433
|
-
try:
|
|
434
|
-
envelope = json.loads(source.read_text(encoding="utf-8"))
|
|
435
|
-
except json.JSONDecodeError as exc:
|
|
436
|
-
raise ValueError("Subgraph bundle is not valid JSON.") from exc
|
|
437
|
-
if envelope.get("format") != SUBGRAPH_ARCHIVE_FORMAT:
|
|
438
|
-
raise ValueError("Not a .latticebrain subgraph bundle.")
|
|
439
|
-
if envelope.get("encryption") == "recipient_public_key":
|
|
440
|
-
plaintext = self._unseal_bundle(envelope)
|
|
441
|
-
else:
|
|
442
|
-
plaintext = self._decrypt_with_passphrase(envelope, passphrase)
|
|
443
|
-
body = json.loads(plaintext.decode("utf-8"))
|
|
444
|
-
return {
|
|
445
|
-
"header": envelope.get("header") or {},
|
|
446
|
-
**body,
|
|
447
|
-
"signature": envelope.get("signature") or {},
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
def _unseal_bundle(self, envelope: Dict[str, Any]) -> bytes:
|
|
451
|
-
"""Open a sealed box with this Brain's receiving key, or say why not."""
|
|
452
|
-
identity = self._recipient_identity()
|
|
453
|
-
if identity is None:
|
|
454
|
-
raise ValueError(
|
|
455
|
-
"This bundle is sealed to a public key, but no receiving key "
|
|
456
|
-
"could be loaded on this machine."
|
|
457
|
-
)
|
|
458
|
-
block = envelope.get("sealed_box")
|
|
459
|
-
if not isinstance(block, dict):
|
|
460
|
-
raise ValueError("Subgraph bundle is missing encryption metadata.")
|
|
461
|
-
return bytes(identity.unseal(block))
|
|
462
|
-
|
|
463
|
-
@staticmethod
|
|
464
|
-
def _decrypt_with_passphrase(
|
|
465
|
-
envelope: Dict[str, Any], passphrase: Optional[str]
|
|
466
|
-
) -> bytes:
|
|
467
|
-
if not str(passphrase or "").strip():
|
|
468
|
-
raise ValueError("This bundle is passphrase-encrypted; a passphrase is required.")
|
|
469
|
-
try:
|
|
470
|
-
salt = base64.b64decode(envelope["kdf"]["salt"])
|
|
471
|
-
nonce = base64.b64decode(envelope["cipher"]["nonce"])
|
|
472
|
-
ciphertext = base64.b64decode(envelope["payload"])
|
|
473
|
-
except (KeyError, TypeError, ValueError) as exc:
|
|
474
|
-
raise ValueError("Subgraph bundle is missing encryption metadata.") from exc
|
|
475
|
-
try:
|
|
476
|
-
return bytes(
|
|
477
|
-
AESGCM(_derive_key(str(passphrase), salt)).decrypt(nonce, ciphertext, None)
|
|
478
|
-
)
|
|
479
|
-
except InvalidTag as exc:
|
|
480
|
-
raise ValueError(
|
|
481
|
-
"Subgraph decryption failed; the passphrase or the bundle is invalid."
|
|
482
|
-
) from exc
|
|
483
|
-
|
|
484
|
-
def verify_subgraph(self, artifact: Dict[str, Any]) -> Dict[str, Any]:
|
|
485
|
-
"""Signature + payload-digest verdict for a received bundle.
|
|
486
|
-
|
|
487
|
-
Fail-closed and specific: the caller learns *which* check failed, and
|
|
488
|
-
an unsigned bundle is a failure here (unlike a local file import, where
|
|
489
|
-
pre-v4 unsigned artifacts stay importable).
|
|
490
|
-
"""
|
|
491
|
-
errors: List[str] = []
|
|
492
|
-
header = artifact.get("header") if isinstance(artifact, dict) else None
|
|
493
|
-
header = header if isinstance(header, dict) else {}
|
|
494
|
-
signature = artifact.get("signature") if isinstance(artifact, dict) else None
|
|
495
|
-
signature = signature if isinstance(signature, dict) else {}
|
|
496
|
-
if header.get("format") != SUBGRAPH_FORMAT:
|
|
497
|
-
errors.append("not a Lattice subgraph bundle")
|
|
498
|
-
incoming_version = int(header.get("format_version") or 0)
|
|
499
|
-
if incoming_version > SUBGRAPH_FORMAT_VERSION:
|
|
500
|
-
errors.append(
|
|
501
|
-
f"bundle format version {incoming_version} is newer than this build "
|
|
502
|
-
f"({SUBGRAPH_FORMAT_VERSION})"
|
|
503
|
-
)
|
|
504
|
-
if not signature.get("signature"):
|
|
505
|
-
errors.append("bundle is unsigned")
|
|
506
|
-
else:
|
|
507
|
-
from ..graph.identity import verify_manifest
|
|
508
|
-
|
|
509
|
-
if not verify_manifest(header, signature):
|
|
510
|
-
errors.append("signature does not match the bundle header")
|
|
511
|
-
expected = header.get("payload_sha256")
|
|
512
|
-
if expected:
|
|
513
|
-
payload = {key: artifact.get(key) or [] for key in _PAYLOAD_KEYS}
|
|
514
|
-
if _canonical_digest(payload) != expected:
|
|
515
|
-
errors.append("bundle contents do not match the signed digest")
|
|
516
|
-
else:
|
|
517
|
-
errors.append("bundle header does not pin its payload digest")
|
|
518
|
-
fingerprint = str(signature.get("fingerprint") or "") or None
|
|
519
|
-
return {
|
|
520
|
-
"ok": not errors,
|
|
521
|
-
"errors": errors,
|
|
522
|
-
"fingerprint": fingerprint,
|
|
523
|
-
"origin": f"device:{fingerprint}" if fingerprint and not errors else None,
|
|
524
|
-
"exported_at": header.get("exported_at"),
|
|
525
|
-
"counts": header.get("counts") or {},
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
def import_subgraph_proposals(
|
|
529
|
-
self,
|
|
530
|
-
artifact: Dict[str, Any],
|
|
531
|
-
*,
|
|
532
|
-
review_sink: Any,
|
|
533
|
-
workspace_id: Optional[str] = None,
|
|
534
|
-
user_email: Optional[str] = None,
|
|
535
|
-
dry_run: bool = False,
|
|
536
|
-
cap: int = SUBGRAPH_PROPOSAL_CAP,
|
|
537
|
-
) -> Dict[str, Any]:
|
|
538
|
-
"""Receive a subgraph as *proposals*, never as a merge.
|
|
539
|
-
|
|
540
|
-
Every node becomes one review item carrying its edges, chunks, and
|
|
541
|
-
provenance, stamped with the sending device's fingerprint and the
|
|
542
|
-
signature verdict. Nothing reaches the graph until a human accepts an
|
|
543
|
-
item through :meth:`accept_subgraph_proposal`.
|
|
544
|
-
"""
|
|
545
|
-
require_brain_network()
|
|
546
|
-
self._require()
|
|
547
|
-
verdict = self.verify_subgraph(artifact)
|
|
548
|
-
if not verdict["ok"]:
|
|
549
|
-
raise ValueError(
|
|
550
|
-
"Refusing the bundle: " + "; ".join(verdict["errors"])
|
|
551
|
-
)
|
|
552
|
-
header = dict(artifact.get("header") or {})
|
|
553
|
-
nodes = [n for n in artifact.get("nodes") or [] if n.get("id")]
|
|
554
|
-
edges = list(artifact.get("edges") or [])
|
|
555
|
-
chunks = list(artifact.get("chunks") or [])
|
|
556
|
-
chunk_nodes = {str(n.get("id")): n for n in artifact.get("chunk_nodes") or []}
|
|
557
|
-
provenance = list(artifact.get("provenance") or [])
|
|
558
|
-
cap = max(1, int(cap))
|
|
559
|
-
selected = nodes[:cap]
|
|
560
|
-
summary: Dict[str, Any] = {
|
|
561
|
-
"status": "dry_run" if dry_run else "proposed",
|
|
562
|
-
"origin": verdict["origin"],
|
|
563
|
-
"fingerprint": verdict["fingerprint"],
|
|
564
|
-
"signed": True,
|
|
565
|
-
"signature_verified": True,
|
|
566
|
-
"nodes": len(nodes),
|
|
567
|
-
"edges": len(edges),
|
|
568
|
-
"chunks": len(chunks),
|
|
569
|
-
"proposed": len(selected),
|
|
570
|
-
"skipped": len(nodes) - len(selected),
|
|
571
|
-
"capped": len(nodes) > len(selected),
|
|
572
|
-
"exported_at": header.get("exported_at"),
|
|
573
|
-
"items": [],
|
|
574
|
-
}
|
|
575
|
-
if dry_run:
|
|
576
|
-
return summary
|
|
577
|
-
for node in selected:
|
|
578
|
-
node_id = str(node.get("id"))
|
|
579
|
-
node_chunks = [c for c in chunks if str(c.get("source_node")) == node_id]
|
|
580
|
-
carried = {node_id} | {str(c.get("id")) for c in node_chunks}
|
|
581
|
-
item = review_sink.create(
|
|
582
|
-
title=str(node.get("title") or node_id)[:200],
|
|
583
|
-
summary=_shared_node_summary(node, verdict),
|
|
584
|
-
source=SUBGRAPH_REVIEW_SOURCE,
|
|
585
|
-
kind=SUBGRAPH_REVIEW_KIND,
|
|
586
|
-
payload={
|
|
587
|
-
"kind": SUBGRAPH_REVIEW_KIND,
|
|
588
|
-
"node": node,
|
|
589
|
-
"chunk_nodes": [
|
|
590
|
-
chunk_nodes[str(c.get("id"))]
|
|
591
|
-
for c in node_chunks
|
|
592
|
-
if str(c.get("id")) in chunk_nodes
|
|
593
|
-
],
|
|
594
|
-
"edges": [
|
|
595
|
-
e for e in edges
|
|
596
|
-
if str(e.get("from_node")) in carried or str(e.get("to_node")) in carried
|
|
597
|
-
],
|
|
598
|
-
"chunks": node_chunks,
|
|
599
|
-
"provenance": [p for p in provenance if str(p.get("node_id")) == node_id],
|
|
600
|
-
"graph_schema_version": header.get("graph_schema_version"),
|
|
601
|
-
},
|
|
602
|
-
provenance={
|
|
603
|
-
"origin": verdict["origin"],
|
|
604
|
-
"fingerprint": verdict["fingerprint"],
|
|
605
|
-
"public_key": (artifact.get("signature") or {}).get("public_key"),
|
|
606
|
-
"signature_verified": True,
|
|
607
|
-
"exported_at": header.get("exported_at"),
|
|
608
|
-
"source_detail": "brain_network_subgraph",
|
|
609
|
-
"redacted": header.get("redacted") or [],
|
|
610
|
-
},
|
|
611
|
-
user_email=user_email,
|
|
612
|
-
workspace_id=workspace_id,
|
|
613
|
-
)
|
|
614
|
-
summary["items"].append(item.get("id"))
|
|
615
|
-
try:
|
|
616
|
-
self._kg.record_provenance(
|
|
617
|
-
node_id="subgraph-proposal:" + str(header.get("exported_at") or utc_now_iso()),
|
|
618
|
-
source_type="subgraph_share",
|
|
619
|
-
pipeline="kg-portability",
|
|
620
|
-
owner=user_email,
|
|
621
|
-
workspace_id=workspace_id,
|
|
622
|
-
metadata={
|
|
623
|
-
"origin": verdict["origin"],
|
|
624
|
-
"proposed": len(selected),
|
|
625
|
-
"counts": header.get("counts"),
|
|
626
|
-
},
|
|
627
|
-
)
|
|
628
|
-
except Exception: # noqa: BLE001 — the proposals already landed
|
|
629
|
-
quiet()
|
|
630
|
-
return summary
|
|
631
|
-
|
|
632
|
-
def accept_subgraph_proposal(
|
|
633
|
-
self,
|
|
634
|
-
item_id: str,
|
|
635
|
-
*,
|
|
636
|
-
review_sink: Any,
|
|
637
|
-
workspace_id: Optional[str] = None,
|
|
638
|
-
) -> Dict[str, Any]:
|
|
639
|
-
"""Merge one accepted proposal into this Brain and approve the item.
|
|
640
|
-
|
|
641
|
-
Only the node the reviewer looked at is written. An edge whose other
|
|
642
|
-
endpoint is not (yet) in this Brain is *deferred* and reported, because
|
|
643
|
-
writing a dangling relation would invent a connection the receiver
|
|
644
|
-
cannot see.
|
|
645
|
-
"""
|
|
646
|
-
require_brain_network()
|
|
647
|
-
self._require()
|
|
648
|
-
item = review_sink.get(item_id, workspace_id=workspace_id)
|
|
649
|
-
payload = dict((item or {}).get("payload") or {})
|
|
650
|
-
if payload.get("kind") != SUBGRAPH_REVIEW_KIND:
|
|
651
|
-
raise ValueError("That review item is not a shared-subgraph proposal.")
|
|
652
|
-
node = dict(payload.get("node") or {})
|
|
653
|
-
node_id = str(node.get("id") or "")
|
|
654
|
-
if not node_id:
|
|
655
|
-
raise ValueError("The proposal carries no node id.")
|
|
656
|
-
status = str((item or {}).get("status") or "pending")
|
|
657
|
-
if status not in _OPEN_REVIEW_STATUSES:
|
|
658
|
-
# Import first, approve second would re-write an already-decided
|
|
659
|
-
# item; refuse before touching the graph instead.
|
|
660
|
-
raise ValueError(f"That proposal is already {status}; nothing was imported.")
|
|
661
|
-
chunk_nodes = [dict(n) for n in payload.get("chunk_nodes") or []]
|
|
662
|
-
if workspace_id:
|
|
663
|
-
node = _scope_node(node, workspace_id)
|
|
664
|
-
chunk_nodes = [_scope_node(n, workspace_id) for n in chunk_nodes]
|
|
665
|
-
# The chunk rows land in the same write, so their nodes count as
|
|
666
|
-
# present even though this Brain has not seen them yet.
|
|
667
|
-
writing = {node_id} | {str(n.get("id")) for n in chunk_nodes}
|
|
668
|
-
kept: List[Dict[str, Any]] = []
|
|
669
|
-
deferred: List[Dict[str, Any]] = []
|
|
670
|
-
for edge in payload.get("edges") or []:
|
|
671
|
-
endpoints = {str(edge.get("from_node")), str(edge.get("to_node"))}
|
|
672
|
-
outside = endpoints - writing
|
|
673
|
-
if all(self._node_present(other) for other in outside):
|
|
674
|
-
kept.append(edge)
|
|
675
|
-
else:
|
|
676
|
-
deferred.append({
|
|
677
|
-
"from": edge.get("from_node"),
|
|
678
|
-
"to": edge.get("to_node"),
|
|
679
|
-
"type": edge.get("type"),
|
|
680
|
-
"reason": "the other node is not in this Brain yet",
|
|
681
|
-
})
|
|
682
|
-
result = self._kg.import_graph_data(
|
|
683
|
-
{
|
|
684
|
-
"header": {"graph_schema_version": payload.get("graph_schema_version")},
|
|
685
|
-
"nodes": [node, *chunk_nodes],
|
|
686
|
-
"edges": kept,
|
|
687
|
-
"chunks": list(payload.get("chunks") or []),
|
|
688
|
-
"knowledge_sources": [],
|
|
689
|
-
"provenance": list(payload.get("provenance") or []),
|
|
690
|
-
},
|
|
691
|
-
mode="merge",
|
|
692
|
-
)
|
|
693
|
-
approved = review_sink.approve(item_id, workspace_id=workspace_id)
|
|
694
|
-
return {
|
|
695
|
-
"status": "accepted",
|
|
696
|
-
"item_id": item_id,
|
|
697
|
-
"node_id": node_id,
|
|
698
|
-
"imported": result,
|
|
699
|
-
"edges_written": len(kept),
|
|
700
|
-
"edges_deferred": deferred,
|
|
701
|
-
"origin": (item.get("provenance") or {}).get("origin"),
|
|
702
|
-
"review_status": approved.get("status"),
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
def _node_present(self, node_id: str) -> bool:
|
|
706
|
-
try:
|
|
707
|
-
self._kg.get_node(node_id)
|
|
708
|
-
except Exception: # noqa: BLE001 — "not found" is the answer, not a failure
|
|
709
|
-
return False
|
|
710
|
-
return True
|