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
package/latticeai/api/setup.py
DELETED
|
@@ -1,319 +0,0 @@
|
|
|
1
|
-
"""Setup wizard and OS permission routes."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import Any, Dict, List, Optional
|
|
6
|
-
|
|
7
|
-
from fastapi import APIRouter, HTTPException, Request
|
|
8
|
-
from fastapi.responses import StreamingResponse
|
|
9
|
-
from pydantic import BaseModel
|
|
10
|
-
|
|
11
|
-
from lattice_brain.ingestion import IngestionItem
|
|
12
|
-
from latticeai.core.messages import http_error, resolve_language
|
|
13
|
-
from latticeai.models.router import parse_model_ref
|
|
14
|
-
from latticeai.services.process_audit import command_plan
|
|
15
|
-
from latticeai.setup.auto_setup import (
|
|
16
|
-
plan as auto_setup_plan,
|
|
17
|
-
)
|
|
18
|
-
from latticeai.setup.auto_setup import (
|
|
19
|
-
preset as auto_setup_preset,
|
|
20
|
-
)
|
|
21
|
-
from latticeai.setup.auto_setup import (
|
|
22
|
-
probe as auto_setup_probe,
|
|
23
|
-
)
|
|
24
|
-
from latticeai.setup.auto_setup import (
|
|
25
|
-
recommend as auto_setup_recommend,
|
|
26
|
-
)
|
|
27
|
-
from latticeai.setup.auto_setup import (
|
|
28
|
-
verify as auto_setup_verify,
|
|
29
|
-
)
|
|
30
|
-
from latticeai.setup.demo_corpus import (
|
|
31
|
-
DEMO_DOCUMENTS,
|
|
32
|
-
DEMO_METADATA_FLAG,
|
|
33
|
-
DEMO_URI_PREFIX,
|
|
34
|
-
demo_source_uri,
|
|
35
|
-
suggested_questions,
|
|
36
|
-
)
|
|
37
|
-
from latticeai.setup.wizard import (
|
|
38
|
-
get_recommendations,
|
|
39
|
-
install_stream,
|
|
40
|
-
open_url,
|
|
41
|
-
scan_environment,
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
class SetupInstallRequest(BaseModel):
|
|
46
|
-
items: List[Dict]
|
|
47
|
-
confirmation_token: Optional[str] = None
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class DemoCorpusRequest(BaseModel):
|
|
51
|
-
workspace_id: Optional[str] = None
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def create_setup_router(
|
|
55
|
-
*,
|
|
56
|
-
model_router,
|
|
57
|
-
require_user,
|
|
58
|
-
ingestion_pipeline=None,
|
|
59
|
-
knowledge_graph=None,
|
|
60
|
-
workspace_service=None,
|
|
61
|
-
) -> APIRouter:
|
|
62
|
-
api_router = APIRouter()
|
|
63
|
-
router = model_router
|
|
64
|
-
|
|
65
|
-
# ── Setup Wizard ─────────────────────────────────────────────────────────────
|
|
66
|
-
|
|
67
|
-
def setup_auto_state() -> Dict[str, Any]:
|
|
68
|
-
"""Return the PPT-aligned zero-config setup state used by setup UI/API."""
|
|
69
|
-
profile = auto_setup_probe()
|
|
70
|
-
recommendation = auto_setup_recommend(profile)
|
|
71
|
-
install_plan = auto_setup_plan(profile, recommendation)
|
|
72
|
-
return {
|
|
73
|
-
"probe": profile.to_json(),
|
|
74
|
-
"recommend": recommendation.to_json(),
|
|
75
|
-
"plan": install_plan.to_json(),
|
|
76
|
-
"verify": auto_setup_verify(profile, recommendation),
|
|
77
|
-
"preset": auto_setup_preset(profile, recommendation),
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def primary_setup_model(recs: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
82
|
-
models = recs.get("models") if isinstance(recs, dict) else None
|
|
83
|
-
if not isinstance(models, list):
|
|
84
|
-
return None
|
|
85
|
-
candidates = [
|
|
86
|
-
item for item in models
|
|
87
|
-
if isinstance(item, dict) and not item.get("disabled") and (item.get("model_id") or (item.get("action") or {}).get("model_id"))
|
|
88
|
-
]
|
|
89
|
-
if not candidates:
|
|
90
|
-
return None
|
|
91
|
-
return next((item for item in candidates if item.get("checked")), candidates[0])
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
@api_router.get("/setup/scan")
|
|
95
|
-
async def setup_scan(request: Request):
|
|
96
|
-
"""환경 감지 및 맞춤 추천 반환."""
|
|
97
|
-
require_user(request)
|
|
98
|
-
env = scan_environment()
|
|
99
|
-
recs = get_recommendations(env)
|
|
100
|
-
zero_config = setup_auto_state()
|
|
101
|
-
primary_model = primary_setup_model(recs)
|
|
102
|
-
if primary_model:
|
|
103
|
-
model_id = primary_model.get("model_id") or (primary_model.get("action") or {}).get("model_id")
|
|
104
|
-
model_provider, provider_model = parse_model_ref(str(model_id))
|
|
105
|
-
primary_runtime = "mlx" if model_provider == "local_mlx" else model_provider
|
|
106
|
-
zero_config.setdefault("recommend", {})["model_id"] = model_id
|
|
107
|
-
zero_config["recommend"]["runtime"] = primary_runtime
|
|
108
|
-
rationale = [
|
|
109
|
-
item for item in zero_config["recommend"].get("rationale", [])
|
|
110
|
-
if not (isinstance(item, str) and item.startswith("RAM ") and "→" in item)
|
|
111
|
-
]
|
|
112
|
-
rationale.append(f"실제 다운로드 및 로드 가능한 {primary_runtime} 모델 → {model_id}")
|
|
113
|
-
zero_config["recommend"]["rationale"] = rationale
|
|
114
|
-
if isinstance(zero_config.get("plan"), dict):
|
|
115
|
-
if model_provider == "ollama":
|
|
116
|
-
command = ["ollama", "pull", provider_model]
|
|
117
|
-
elif model_provider in {"vllm", "lmstudio", "llamacpp"}:
|
|
118
|
-
command = ["lattice-ai", "models", "load", str(model_id)]
|
|
119
|
-
else:
|
|
120
|
-
command = ["huggingface-cli", "download", str(model_id), "--quiet"]
|
|
121
|
-
step_plan = command_plan(
|
|
122
|
-
command,
|
|
123
|
-
name=f"weights:{model_id}",
|
|
124
|
-
purpose="auto_setup_install",
|
|
125
|
-
metadata={"model_id": str(model_id)},
|
|
126
|
-
)
|
|
127
|
-
zero_config["plan"]["steps"] = [{
|
|
128
|
-
"name": f"weights:{model_id}",
|
|
129
|
-
"why": "추론에 사용할 모델 가중치",
|
|
130
|
-
"command": command,
|
|
131
|
-
"requires_admin": False,
|
|
132
|
-
"command_plan": step_plan,
|
|
133
|
-
"confirmation_token": step_plan["confirmation_token"],
|
|
134
|
-
}]
|
|
135
|
-
if isinstance(zero_config.get("preset"), dict):
|
|
136
|
-
zero_config["preset"].setdefault("model", {})["id"] = model_id
|
|
137
|
-
zero_config["preset"]["model"]["runtime"] = primary_runtime
|
|
138
|
-
env["zero_config"] = zero_config
|
|
139
|
-
recs.setdefault("summary", {})["zero_config"] = zero_config["recommend"]
|
|
140
|
-
recs["install_plan"] = zero_config["plan"]
|
|
141
|
-
recs["preset"] = zero_config["preset"]
|
|
142
|
-
return {"environment": env, "recommendations": recs, "zero_config": zero_config}
|
|
143
|
-
|
|
144
|
-
@api_router.get("/setup/auto")
|
|
145
|
-
async def setup_auto(request: Request):
|
|
146
|
-
"""PPT-aligned zero-config setup pipeline: probe → recommend → plan → verify → preset."""
|
|
147
|
-
require_user(request)
|
|
148
|
-
return setup_auto_state()
|
|
149
|
-
|
|
150
|
-
@api_router.post("/setup/install")
|
|
151
|
-
async def setup_install(req: SetupInstallRequest, request: Request):
|
|
152
|
-
"""선택된 항목을 순서대로 설치 · 로드하는 SSE 스트림."""
|
|
153
|
-
user_email = require_user(request)
|
|
154
|
-
async def _gen():
|
|
155
|
-
async for chunk in install_stream(
|
|
156
|
-
req.items,
|
|
157
|
-
router,
|
|
158
|
-
confirmation_token=req.confirmation_token,
|
|
159
|
-
user_email=user_email,
|
|
160
|
-
):
|
|
161
|
-
yield chunk
|
|
162
|
-
return StreamingResponse(_gen(), media_type="text/event-stream",
|
|
163
|
-
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
|
|
164
|
-
|
|
165
|
-
@api_router.post("/setup/open-auth/{mcp_id}")
|
|
166
|
-
async def setup_open_auth(mcp_id: str, request: Request):
|
|
167
|
-
require_user(request)
|
|
168
|
-
"""MCP 인증 페이지를 브라우저에서 자동으로 엽니다."""
|
|
169
|
-
auth_urls: Dict[str, str] = {
|
|
170
|
-
"github": "https://github.com/apps",
|
|
171
|
-
"google-drive": "https://chatgpt.com/connectors",
|
|
172
|
-
"slack": "https://chatgpt.com/connectors",
|
|
173
|
-
"chrome": "https://chatgpt.com/connectors",
|
|
174
|
-
"computer-use": "https://chatgpt.com/connectors",
|
|
175
|
-
"figma": "https://chatgpt.com/connectors",
|
|
176
|
-
"notion": "https://chatgpt.com/connectors",
|
|
177
|
-
"linear": "https://chatgpt.com/connectors",
|
|
178
|
-
"gmail": "https://chatgpt.com/connectors",
|
|
179
|
-
"google-calendar": "https://chatgpt.com/connectors",
|
|
180
|
-
"outlook-email": "https://chatgpt.com/connectors",
|
|
181
|
-
"outlook-calendar": "https://chatgpt.com/connectors",
|
|
182
|
-
"teams": "https://chatgpt.com/connectors",
|
|
183
|
-
"sharepoint": "https://chatgpt.com/connectors",
|
|
184
|
-
"canva": "https://chatgpt.com/connectors",
|
|
185
|
-
}
|
|
186
|
-
url = auth_urls.get(mcp_id)
|
|
187
|
-
if not url:
|
|
188
|
-
raise http_error(404, "mcp.unknown_id", resolve_language(request), mcp_id=mcp_id)
|
|
189
|
-
open_url(url)
|
|
190
|
-
return {"status": "ok", "opened": url, "mcp_id": mcp_id}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
# ── First Value Loop demo corpus (backlog #3, review §3.3 P0) ────────────
|
|
194
|
-
|
|
195
|
-
def _require_demo_pipeline(request: Request):
|
|
196
|
-
if ingestion_pipeline is None or not ingestion_pipeline.available():
|
|
197
|
-
raise http_error(503, "capture.ingestion_disabled", resolve_language(request))
|
|
198
|
-
if knowledge_graph is None:
|
|
199
|
-
raise http_error(503, "common.graph_disabled", resolve_language(request))
|
|
200
|
-
|
|
201
|
-
def _demo_workspace(request: Request, body_workspace: Optional[str], user: str) -> Optional[str]:
|
|
202
|
-
header = request.headers.get("X-Workspace-Id")
|
|
203
|
-
header = header.strip() if header and header.strip() else None
|
|
204
|
-
supplied = [value for value in (body_workspace, header) if value]
|
|
205
|
-
if len(set(supplied)) > 1:
|
|
206
|
-
raise http_error(403, "common.workspace_mismatch", resolve_language(request))
|
|
207
|
-
requested = supplied[0] if supplied else None
|
|
208
|
-
if workspace_service is None:
|
|
209
|
-
return requested
|
|
210
|
-
try:
|
|
211
|
-
return workspace_service.resolve_write_scope(requested, user or None)
|
|
212
|
-
except PermissionError as exc:
|
|
213
|
-
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
|
214
|
-
|
|
215
|
-
@api_router.get("/api/setup/demo-corpus")
|
|
216
|
-
async def demo_corpus_status(request: Request):
|
|
217
|
-
"""Whether the demo corpus is installed + the suggestion chips."""
|
|
218
|
-
require_user(request)
|
|
219
|
-
_require_demo_pipeline(request)
|
|
220
|
-
installed = knowledge_graph.find_documents_by_uri_prefix(DEMO_URI_PREFIX)
|
|
221
|
-
return {
|
|
222
|
-
"installed": bool(installed),
|
|
223
|
-
"documents": installed,
|
|
224
|
-
"document_count": len(installed),
|
|
225
|
-
"suggested_questions": suggested_questions(),
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
@api_router.post("/api/setup/demo-corpus")
|
|
229
|
-
async def demo_corpus_install(request: Request, req: Optional[DemoCorpusRequest] = None):
|
|
230
|
-
"""One-click ingest of the 3 built-in demo documents (idempotent).
|
|
231
|
-
|
|
232
|
-
Every document goes through the normal IngestionPipeline door with
|
|
233
|
-
``demo://`` provenance so it is honest (real nodes, real chunks, real
|
|
234
|
-
provenance) and removable via DELETE. Re-POSTing reports duplicates
|
|
235
|
-
instead of duplicating.
|
|
236
|
-
"""
|
|
237
|
-
user = require_user(request)
|
|
238
|
-
_require_demo_pipeline(request)
|
|
239
|
-
workspace_id = _demo_workspace(request, req.workspace_id if req else None, user)
|
|
240
|
-
results = []
|
|
241
|
-
ingested = 0
|
|
242
|
-
duplicates = 0
|
|
243
|
-
failed = 0
|
|
244
|
-
for doc in DEMO_DOCUMENTS:
|
|
245
|
-
result = ingestion_pipeline.ingest(
|
|
246
|
-
IngestionItem(
|
|
247
|
-
source_type="note",
|
|
248
|
-
title=doc["title"],
|
|
249
|
-
text=doc["text"],
|
|
250
|
-
source_uri=demo_source_uri(doc["id"]),
|
|
251
|
-
owner=user or None,
|
|
252
|
-
workspace_id=workspace_id,
|
|
253
|
-
metadata={DEMO_METADATA_FLAG: True, "demo_id": doc["id"]},
|
|
254
|
-
),
|
|
255
|
-
user_email=user or None,
|
|
256
|
-
)
|
|
257
|
-
if result.status != "ok":
|
|
258
|
-
failed += 1
|
|
259
|
-
elif result.duplicate:
|
|
260
|
-
duplicates += 1
|
|
261
|
-
else:
|
|
262
|
-
ingested += 1
|
|
263
|
-
results.append({
|
|
264
|
-
"demo_id": doc["id"],
|
|
265
|
-
"title": doc["title"],
|
|
266
|
-
"source_uri": demo_source_uri(doc["id"]),
|
|
267
|
-
"status": result.status,
|
|
268
|
-
"node_id": result.node_id,
|
|
269
|
-
"duplicate": result.duplicate,
|
|
270
|
-
"chunk_count": result.chunk_count,
|
|
271
|
-
"detail": result.detail,
|
|
272
|
-
})
|
|
273
|
-
status = "ok" if failed == 0 else ("partial" if (ingested + duplicates) else "failed")
|
|
274
|
-
return {
|
|
275
|
-
"status": status,
|
|
276
|
-
"ingested": ingested,
|
|
277
|
-
"duplicates": duplicates,
|
|
278
|
-
"failed": failed,
|
|
279
|
-
"documents": results,
|
|
280
|
-
"suggested_questions": suggested_questions(),
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
@api_router.delete("/api/setup/demo-corpus")
|
|
284
|
-
async def demo_corpus_remove(request: Request):
|
|
285
|
-
"""Remove every demo document (node + chunks + edges + orphan source)."""
|
|
286
|
-
require_user(request)
|
|
287
|
-
_require_demo_pipeline(request)
|
|
288
|
-
installed = knowledge_graph.find_documents_by_uri_prefix(DEMO_URI_PREFIX)
|
|
289
|
-
removed = []
|
|
290
|
-
for doc in installed:
|
|
291
|
-
outcome = knowledge_graph.delete_document_tree(doc["id"])
|
|
292
|
-
removed.append({
|
|
293
|
-
"node_id": doc["id"],
|
|
294
|
-
"title": doc.get("title"),
|
|
295
|
-
"source_uri": doc.get("source_uri"),
|
|
296
|
-
"status": outcome.get("status"),
|
|
297
|
-
"removed_nodes": outcome.get("removed_nodes", 0),
|
|
298
|
-
})
|
|
299
|
-
return {
|
|
300
|
-
"status": "ok",
|
|
301
|
-
"removed_count": len(removed),
|
|
302
|
-
"removed": removed,
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
@api_router.post("/permissions/open/{permission_id}")
|
|
306
|
-
async def open_permission_settings(permission_id: str, request: Request):
|
|
307
|
-
require_user(request)
|
|
308
|
-
"""macOS 권한 설정 화면을 엽니다."""
|
|
309
|
-
urls = {
|
|
310
|
-
"accessibility": "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility",
|
|
311
|
-
"automation": "x-apple.systempreferences:com.apple.preference.security?Privacy_Automation",
|
|
312
|
-
"screen": "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture",
|
|
313
|
-
}
|
|
314
|
-
url = urls.get(permission_id)
|
|
315
|
-
if not url:
|
|
316
|
-
raise http_error(404, "setup.unknown_permission", resolve_language(request))
|
|
317
|
-
open_url(url)
|
|
318
|
-
return {"status": "ok", "opened": url, "permission": permission_id}
|
|
319
|
-
return api_router
|
|
@@ -1,354 +0,0 @@
|
|
|
1
|
-
"""Static UI and lightweight status routes."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import asyncio
|
|
6
|
-
import hashlib
|
|
7
|
-
import hmac
|
|
8
|
-
import re
|
|
9
|
-
import secrets
|
|
10
|
-
import subprocess
|
|
11
|
-
import time
|
|
12
|
-
from dataclasses import dataclass
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
from typing import Any, Callable, Dict, Optional
|
|
15
|
-
|
|
16
|
-
from fastapi import APIRouter, Cookie, HTTPException, Request
|
|
17
|
-
from fastapi.responses import FileResponse, HTMLResponse
|
|
18
|
-
|
|
19
|
-
from latticeai.api.ui_redirects import app_redirect
|
|
20
|
-
from latticeai.core.quiet import quiet
|
|
21
|
-
|
|
22
|
-
PRODUCTION_CSP = (
|
|
23
|
-
"default-src 'self'; "
|
|
24
|
-
"script-src 'self'; "
|
|
25
|
-
"style-src 'self' 'unsafe-inline'; "
|
|
26
|
-
"img-src 'self' data: blob: http://127.0.0.1:*; "
|
|
27
|
-
"font-src 'self' data:; "
|
|
28
|
-
# No `ws:`: this product has no WebSocket endpoint anywhere. The live
|
|
29
|
-
# surfaces (chat, agent steps, index progress) are server-sent events, read
|
|
30
|
-
# with fetch/ReadableStream, which `connect-src` already covers over http.
|
|
31
|
-
"connect-src 'self' http://127.0.0.1:*; "
|
|
32
|
-
"frame-src 'none'; "
|
|
33
|
-
"object-src 'none'; "
|
|
34
|
-
"base-uri 'none'; "
|
|
35
|
-
"form-action 'self'; "
|
|
36
|
-
"frame-ancestors 'none'"
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
INVITE_COOKIE_NAME = "lattice_invite"
|
|
40
|
-
INVITE_COOKIE_TTL_SECONDS = 60 * 60 * 24 * 7
|
|
41
|
-
|
|
42
|
-
# Host-capacity plain-language judgment for System basic mode (layout rebuild).
|
|
43
|
-
# Thresholds live here so the frontend never re-derives copy from raw percents.
|
|
44
|
-
SYSINFO_READINESS_ROOMY_MAX = 55.0
|
|
45
|
-
SYSINFO_READINESS_TIGHT_MAX = 80.0
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def _probe_host_capacity() -> Dict[str, Any]:
|
|
49
|
-
"""Sample CPU / RAM / GPU on this machine. Blocking — call off the loop.
|
|
50
|
-
|
|
51
|
-
Extracted from the route so the subprocess sampling can be handed to a
|
|
52
|
-
worker thread, and so the parsing is testable without an HTTP request.
|
|
53
|
-
"""
|
|
54
|
-
result: Dict[str, Any] = {
|
|
55
|
-
"cpu_pct": 0.0,
|
|
56
|
-
"ram_pct": 0.0,
|
|
57
|
-
"gpu_mem_pct": 0.0,
|
|
58
|
-
"gpu_mem_gb": 0.0,
|
|
59
|
-
"readiness": "roomy",
|
|
60
|
-
}
|
|
61
|
-
try:
|
|
62
|
-
# CPU
|
|
63
|
-
top_out = subprocess.run(["top", "-l", "1", "-n", "0"], capture_output=True, text=True, timeout=4).stdout
|
|
64
|
-
for line in top_out.splitlines():
|
|
65
|
-
if "CPU usage" in line:
|
|
66
|
-
m = re.search(r"([\d.]+)% user.*?([\d.]+)% sys", line)
|
|
67
|
-
if m:
|
|
68
|
-
result["cpu_pct"] = round(float(m.group(1)) + float(m.group(2)), 1)
|
|
69
|
-
# RAM
|
|
70
|
-
vm_out = subprocess.run(["vm_stat"], capture_output=True, text=True, timeout=4).stdout
|
|
71
|
-
pages: dict = {}
|
|
72
|
-
for line in vm_out.splitlines():
|
|
73
|
-
for key in ["Pages free", "Pages active", "Pages inactive", "Pages wired down", "Pages occupied by compressor"]:
|
|
74
|
-
if line.startswith(key):
|
|
75
|
-
m = re.search(r"(\d+)", line)
|
|
76
|
-
if m:
|
|
77
|
-
pages[key] = int(m.group(1))
|
|
78
|
-
total = sum(pages.values())
|
|
79
|
-
used = total - pages.get("Pages free", 0)
|
|
80
|
-
result["ram_pct"] = round(used / total * 100, 1) if total else 0.0
|
|
81
|
-
# GPU (MLX / Apple Silicon unified memory)
|
|
82
|
-
try:
|
|
83
|
-
import mlx.core as _mx
|
|
84
|
-
hw_out = subprocess.run(["sysctl", "-n", "hw.memsize"], capture_output=True, text=True, timeout=2).stdout
|
|
85
|
-
total_bytes = int(hw_out.strip())
|
|
86
|
-
gpu_bytes = _mx.get_active_memory() + _mx.get_cache_memory()
|
|
87
|
-
result["gpu_mem_gb"] = round(gpu_bytes / (1024 ** 3), 2)
|
|
88
|
-
result["gpu_mem_pct"] = round(gpu_bytes / total_bytes * 100, 1) if total_bytes else 0.0
|
|
89
|
-
except Exception:
|
|
90
|
-
quiet()
|
|
91
|
-
except Exception as e:
|
|
92
|
-
result["error"] = str(e)
|
|
93
|
-
result["readiness"] = host_capacity_readiness(
|
|
94
|
-
cpu_pct=float(result.get("cpu_pct") or 0.0),
|
|
95
|
-
ram_pct=float(result.get("ram_pct") or 0.0),
|
|
96
|
-
gpu_mem_pct=float(result.get("gpu_mem_pct") or 0.0),
|
|
97
|
-
)
|
|
98
|
-
return result
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def host_capacity_readiness(
|
|
102
|
-
*,
|
|
103
|
-
cpu_pct: float = 0.0,
|
|
104
|
-
ram_pct: float = 0.0,
|
|
105
|
-
gpu_mem_pct: float = 0.0,
|
|
106
|
-
) -> str:
|
|
107
|
-
"""Map host telemetry to a single plain-language readiness bucket.
|
|
108
|
-
|
|
109
|
-
Returns one of ``roomy`` / ``tight`` / ``low``. The product uses this so
|
|
110
|
-
basic-mode System copy and advanced-mode numbers share one judgment.
|
|
111
|
-
"""
|
|
112
|
-
load = max(float(cpu_pct or 0.0), float(ram_pct or 0.0), float(gpu_mem_pct or 0.0))
|
|
113
|
-
if load <= SYSINFO_READINESS_ROOMY_MAX:
|
|
114
|
-
return "roomy"
|
|
115
|
-
if load <= SYSINFO_READINESS_TIGHT_MAX:
|
|
116
|
-
return "tight"
|
|
117
|
-
return "low"
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def _sign_invite_cookie(secret: str, *, now: Optional[int] = None) -> str:
|
|
121
|
-
"""Create an expiring, nonce-bearing invite-gate capability cookie."""
|
|
122
|
-
|
|
123
|
-
issued_at = int(time.time() if now is None else now)
|
|
124
|
-
expires_at = issued_at + INVITE_COOKIE_TTL_SECONDS
|
|
125
|
-
nonce = secrets.token_urlsafe(24)
|
|
126
|
-
payload = f"{expires_at}.{nonce}"
|
|
127
|
-
signature = hmac.new(
|
|
128
|
-
str(secret).encode("utf-8"),
|
|
129
|
-
payload.encode("utf-8"),
|
|
130
|
-
hashlib.sha256,
|
|
131
|
-
).hexdigest()
|
|
132
|
-
return f"v1.{payload}.{signature}"
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def _verify_invite_cookie(
|
|
136
|
-
value: Optional[str],
|
|
137
|
-
secret: str,
|
|
138
|
-
*,
|
|
139
|
-
now: Optional[int] = None,
|
|
140
|
-
) -> bool:
|
|
141
|
-
"""Verify version, expiry and HMAC without trusting client claims."""
|
|
142
|
-
|
|
143
|
-
if not value or not secret:
|
|
144
|
-
return False
|
|
145
|
-
try:
|
|
146
|
-
version, raw_expiry, nonce, supplied_signature = value.split(".", 3)
|
|
147
|
-
expires_at = int(raw_expiry)
|
|
148
|
-
except (TypeError, ValueError):
|
|
149
|
-
return False
|
|
150
|
-
if version != "v1" or not nonce or expires_at <= int(time.time() if now is None else now):
|
|
151
|
-
return False
|
|
152
|
-
payload = f"{expires_at}.{nonce}"
|
|
153
|
-
expected_signature = hmac.new(
|
|
154
|
-
str(secret).encode("utf-8"),
|
|
155
|
-
payload.encode("utf-8"),
|
|
156
|
-
hashlib.sha256,
|
|
157
|
-
).hexdigest()
|
|
158
|
-
return hmac.compare_digest(supplied_signature, expected_signature)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
def _invite_denied_response() -> HTMLResponse:
|
|
162
|
-
return HTMLResponse(
|
|
163
|
-
content="""
|
|
164
|
-
<body style="background:#0f1115; color:white; display:flex; flex-direction:column; align-items:center; justify-content:center; height:100vh; font-family:sans-serif;">
|
|
165
|
-
<div style="background:#16191f; padding:40px; border-radius:24px; border:1px solid rgba(255,255,255,0.1); text-align:center; box-shadow: 0 20px 40px rgba(0,0,0,0.5);">
|
|
166
|
-
<div style="font-size:48px; margin-bottom:20px;">🔒</div>
|
|
167
|
-
<h1 style="color:#378ADD; margin:0; font-size:24px;">Invitation Required</h1>
|
|
168
|
-
<p style="color:#94a3b8; margin:20px 0; line-height:1.6;">이 서비스는 비공개로 운영되고 있습니다.<br>선생님께 받은 <b>초대용 전용 링크</b>를 통해 접속해 주세요.</p>
|
|
169
|
-
<div style="margin-top:30px; padding-top:20px; border-top:1px solid rgba(255,255,255,0.05); font-size:11px; color:rgba(255,255,255,0.2); letter-spacing:1px;">LATTICE AI</div>
|
|
170
|
-
</div>
|
|
171
|
-
</body>
|
|
172
|
-
""",
|
|
173
|
-
status_code=403,
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def ui_file_response(path: Path) -> FileResponse:
|
|
178
|
-
response = FileResponse(path)
|
|
179
|
-
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
|
180
|
-
response.headers["Pragma"] = "no-cache"
|
|
181
|
-
response.headers["Expires"] = "0"
|
|
182
|
-
response.headers["Content-Security-Policy"] = PRODUCTION_CSP
|
|
183
|
-
return response
|
|
184
|
-
|
|
185
|
-
@dataclass(frozen=True)
|
|
186
|
-
class StaticRoutesBundle:
|
|
187
|
-
router: APIRouter
|
|
188
|
-
ui_file_response: Callable[[Path], FileResponse]
|
|
189
|
-
local_sysinfo: Callable[[Request], object]
|
|
190
|
-
invite_authorized: Callable[[Request], bool]
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
def create_static_routes_router(
|
|
194
|
-
*,
|
|
195
|
-
static_dir: Path,
|
|
196
|
-
invite_gate_enabled: bool,
|
|
197
|
-
invite_code: str,
|
|
198
|
-
app_mode: str,
|
|
199
|
-
model_router,
|
|
200
|
-
require_user,
|
|
201
|
-
invite_cookie_secret: str = "",
|
|
202
|
-
secure_cookies: bool = False,
|
|
203
|
-
) -> StaticRoutesBundle:
|
|
204
|
-
api_router = APIRouter()
|
|
205
|
-
STATIC_DIR = static_dir
|
|
206
|
-
INVITE_GATE_ENABLED = invite_gate_enabled
|
|
207
|
-
INVITE_CODE = invite_code
|
|
208
|
-
INVITE_COOKIE_SECRET = invite_cookie_secret or secrets.token_urlsafe(48)
|
|
209
|
-
SECURE_COOKIES = bool(secure_cookies)
|
|
210
|
-
APP_MODE = app_mode
|
|
211
|
-
router = model_router
|
|
212
|
-
|
|
213
|
-
def invite_authorized(request: Request) -> bool:
|
|
214
|
-
"""Verify the request's signed invite claim at every gated boundary."""
|
|
215
|
-
|
|
216
|
-
if not INVITE_GATE_ENABLED:
|
|
217
|
-
return True
|
|
218
|
-
return _verify_invite_cookie(
|
|
219
|
-
request.cookies.get(INVITE_COOKIE_NAME),
|
|
220
|
-
INVITE_COOKIE_SECRET,
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
@api_router.get("/")
|
|
224
|
-
async def root(
|
|
225
|
-
request: Request,
|
|
226
|
-
code: Optional[str] = None,
|
|
227
|
-
invite_cookie: Optional[str] = Cookie(None, alias=INVITE_COOKIE_NAME),
|
|
228
|
-
):
|
|
229
|
-
"""로그인/회원가입 페이지. 초대 게이트 활성화 시 코드 검증 후 진입."""
|
|
230
|
-
if not INVITE_GATE_ENABLED:
|
|
231
|
-
return app_redirect("account", request)
|
|
232
|
-
|
|
233
|
-
# 1. 유효한 서버 서명 쿠키가 있는 경우
|
|
234
|
-
if invite_authorized(request):
|
|
235
|
-
return app_redirect("account")
|
|
236
|
-
|
|
237
|
-
# 2. 초대 코드가 일치하는 경우 (최초 진입)
|
|
238
|
-
if INVITE_CODE and code and secrets.compare_digest(code, INVITE_CODE):
|
|
239
|
-
# Do not retain the invitation code in the redirect URL/history.
|
|
240
|
-
response = app_redirect("account")
|
|
241
|
-
response.set_cookie(
|
|
242
|
-
key=INVITE_COOKIE_NAME,
|
|
243
|
-
value=_sign_invite_cookie(INVITE_COOKIE_SECRET),
|
|
244
|
-
httponly=True,
|
|
245
|
-
secure=SECURE_COOKIES,
|
|
246
|
-
samesite="lax",
|
|
247
|
-
max_age=INVITE_COOKIE_TTL_SECONDS,
|
|
248
|
-
path="/",
|
|
249
|
-
)
|
|
250
|
-
return response
|
|
251
|
-
|
|
252
|
-
# 3. 인증 실패 시 차단 화면
|
|
253
|
-
return _invite_denied_response()
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
@api_router.get("/account")
|
|
257
|
-
async def account_page(
|
|
258
|
-
request: Request,
|
|
259
|
-
invite_cookie: Optional[str] = Cookie(None, alias=INVITE_COOKIE_NAME),
|
|
260
|
-
):
|
|
261
|
-
"""Direct login/register page route used by logout and manual navigation."""
|
|
262
|
-
if INVITE_GATE_ENABLED and not invite_authorized(request):
|
|
263
|
-
return _invite_denied_response()
|
|
264
|
-
return app_redirect("account")
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
@api_router.get("/manifest.json")
|
|
268
|
-
async def manifest():
|
|
269
|
-
p = STATIC_DIR / "manifest.json"
|
|
270
|
-
if not p.exists():
|
|
271
|
-
raise HTTPException(status_code=404)
|
|
272
|
-
return FileResponse(str(p), media_type="application/manifest+json")
|
|
273
|
-
|
|
274
|
-
@api_router.api_route("/favicon.ico", methods=["GET", "HEAD"], include_in_schema=False)
|
|
275
|
-
async def favicon():
|
|
276
|
-
ico = STATIC_DIR / "favicon.ico"
|
|
277
|
-
png = STATIC_DIR / "icons" / "favicon-32.png"
|
|
278
|
-
if ico.exists():
|
|
279
|
-
return FileResponse(str(ico), media_type="image/x-icon")
|
|
280
|
-
if png.exists():
|
|
281
|
-
return FileResponse(str(png), media_type="image/png")
|
|
282
|
-
raise HTTPException(status_code=404)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
@api_router.get("/sw.js")
|
|
286
|
-
async def service_worker():
|
|
287
|
-
p = STATIC_DIR / "sw.js"
|
|
288
|
-
if not p.exists():
|
|
289
|
-
raise HTTPException(status_code=404)
|
|
290
|
-
resp = FileResponse(str(p), media_type="application/javascript")
|
|
291
|
-
resp.headers["Service-Worker-Allowed"] = "/"
|
|
292
|
-
return resp
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
@api_router.get("/chat")
|
|
296
|
-
async def chat_page(request: Request):
|
|
297
|
-
return app_redirect("chat", request)
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
@api_router.get("/app")
|
|
301
|
-
async def app_shell(
|
|
302
|
-
request: Request,
|
|
303
|
-
invite_cookie: Optional[str] = Cookie(None, alias=INVITE_COOKIE_NAME),
|
|
304
|
-
):
|
|
305
|
-
"""React desktop single-page workspace shell."""
|
|
306
|
-
if INVITE_GATE_ENABLED and not invite_authorized(request):
|
|
307
|
-
return _invite_denied_response()
|
|
308
|
-
page = STATIC_DIR / "app" / "index.html"
|
|
309
|
-
if not page.exists():
|
|
310
|
-
raise HTTPException(status_code=404, detail="React shell not found.")
|
|
311
|
-
return ui_file_response(page)
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
@api_router.get("/admin")
|
|
315
|
-
async def admin_page(request: Request):
|
|
316
|
-
return app_redirect("admin/users", request)
|
|
317
|
-
|
|
318
|
-
# /workspace and /onboarding UI pages are served by the workspace router
|
|
319
|
-
# (latticeai.api.workspace), included below after its dependencies are defined.
|
|
320
|
-
|
|
321
|
-
@api_router.get("/status")
|
|
322
|
-
async def status(request: Request):
|
|
323
|
-
"""서버 상태 및 현재 로드된 모델 정보를 반환합니다."""
|
|
324
|
-
require_user(request)
|
|
325
|
-
return {
|
|
326
|
-
"message": "🧠 Lattice AI MLX Server is running!",
|
|
327
|
-
"status": "online",
|
|
328
|
-
"mode": APP_MODE,
|
|
329
|
-
"loaded_model": router._current or "None"
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
@api_router.get("/local/sysinfo")
|
|
334
|
-
async def local_sysinfo(request: Request):
|
|
335
|
-
"""CPU / RAM / GPU(MLX) usage plus a plain-language readiness bucket.
|
|
336
|
-
|
|
337
|
-
``readiness`` is ``roomy`` | ``tight`` | ``low`` so basic System copy
|
|
338
|
-
does not re-interpret raw percents on the client.
|
|
339
|
-
|
|
340
|
-
The probe itself is three subprocesses — ``top -l 1`` alone samples for
|
|
341
|
-
about a second — so it runs on a worker thread. Run inline it held the
|
|
342
|
-
event loop for the whole sample, and this route is hit on the System
|
|
343
|
-
screen *and* during first-run analysis, i.e. exactly while an answer is
|
|
344
|
-
streaming: one status poll used to stall every open stream.
|
|
345
|
-
"""
|
|
346
|
-
require_user(request)
|
|
347
|
-
return await asyncio.to_thread(_probe_host_capacity)
|
|
348
|
-
|
|
349
|
-
return StaticRoutesBundle(
|
|
350
|
-
api_router,
|
|
351
|
-
ui_file_response,
|
|
352
|
-
local_sysinfo,
|
|
353
|
-
invite_authorized,
|
|
354
|
-
)
|