ltcai 11.5.2 → 11.6.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 +91 -148
- package/bin/ltcai.js +234 -24
- package/docs/CHANGELOG.md +75 -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 +21 -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-CToZnOHz.js} +1 -1
- package/static/app/assets/{AdminConsole-cf4npybT.js → AdminConsole-D5kbrBu8.js} +1 -1
- package/static/app/assets/{Brain-3VCSHFcn.js → Brain-C6zCdv4S.js} +1 -1
- package/static/app/assets/{BrainHome-Qm8eaztx.js → BrainHome-D4n3EDUt.js} +1 -1
- package/static/app/assets/{BrainSignals-DS9BtKOW.js → BrainSignals-CzeZ-vQ9.js} +1 -1
- package/static/app/assets/{Capture-DiQ219jW.js → Capture-DPcyTczq.js} +1 -1
- package/static/app/assets/{Chronicle-BGvuAchH.js → Chronicle-D3Nx-oWS.js} +1 -1
- package/static/app/assets/{CommandPalette-Bqhm0Urn.js → CommandPalette-DPPvpp28.js} +1 -1
- package/static/app/assets/{Library-BV6NnF0a.js → Library-Co2qTu3I.js} +1 -1
- package/static/app/assets/{LivingBrain-GzenJchP.js → LivingBrain-Bsmzwy8_.js} +1 -1
- package/static/app/assets/{ProductFlow-DEP6-vML.js → ProductFlow-CBLdmesT.js} +1 -1
- package/static/app/assets/{ReviewCard-CNZ7XjWG.js → ReviewCard-CfR6DLs7.js} +1 -1
- package/static/app/assets/{System-CieofHQa.js → System-Ce-P2YL6.js} +1 -1
- package/static/app/assets/arrow-left-DD5jFGHV.js +1 -0
- package/static/app/assets/{bot-B_K1Tdmw.js → bot-C5-XIww2.js} +1 -1
- package/static/app/assets/{brain-DWyaV1L1.js → brain-CFRJUvdX.js} +1 -1
- package/static/app/assets/{button-aTn4s84A.js → button-DI1KcQP-.js} +1 -1
- package/static/app/assets/circle-check-CY20XMCh.js +1 -0
- package/static/app/assets/{circle-pause-xKgeGXkT.js → circle-pause-C8HHyood.js} +1 -1
- package/static/app/assets/{circle-play-DkT6tYPX.js → circle-play-Cufc235E.js} +1 -1
- package/static/app/assets/{cpu-85xYObUC.js → cpu-DW2_JVsv.js} +1 -1
- package/static/app/assets/{download-B5Fm7YXo.js → download-gbdKTBb8.js} +1 -1
- package/static/app/assets/{folder-open-kk2Xa52u.js → folder-open-C9ydyX33.js} +1 -1
- package/static/app/assets/{hard-drive-DkA3zBW_.js → hard-drive-BxNMzBM3.js} +1 -1
- package/static/app/assets/index-BovSWRmQ.css +2 -0
- package/static/app/assets/{index-BMPdTmlY.js → index-q9XyDY7A.js} +3 -3
- package/static/app/assets/{input-B0nRf2jO.js → input-GLD94Y_K.js} +1 -1
- package/static/app/assets/{link-2-Dwb4gnTc.js → link-2-B4Y0gz1S.js} +1 -1
- package/static/app/assets/{permissionCopy-CQDUBrOZ.js → permissionCopy-CxrV0fvU.js} +1 -1
- package/static/app/assets/{primitives-SNp0LRJz.js → primitives-2ANdAsu2.js} +1 -1
- package/static/app/assets/search-DTxtjQ07.js +1 -0
- package/static/app/assets/{share-2-BsrxFglO.js → share-2-DxArfiuI.js} +1 -1
- package/static/app/assets/{shield-alert-5BStfp2_.js → shield-alert-CgftuFFs.js} +1 -1
- package/static/app/assets/{textarea-Cg8IUA-k.js → textarea-BHVBAoCr.js} +1 -1
- package/static/app/assets/{useFocusTrap-CYKvE46M.js → useFocusTrap-Dzq2j_OG.js} +1 -1
- package/static/app/assets/{useMutation-CSn9t1op.js → useMutation-BcqfMLNq.js} +1 -1
- package/static/app/assets/{useQuery-CY2OI2uy.js → useQuery-DBzLKCOv.js} +1 -1
- package/static/app/assets/{utils-Ddol2RWD.js → utils-B-Aah1bO.js} +1 -1
- package/static/app/assets/{workspace-BqDwOz_p.js → workspace-B6fFhoko.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/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/search-BcHqkjoy.js +0 -1
|
@@ -1,506 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Lattice AI — MCP Registry data & pure helper functions.
|
|
3
|
-
|
|
4
|
-
Extracted from server.py to reduce module size.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import asyncio
|
|
8
|
-
import json
|
|
9
|
-
import logging
|
|
10
|
-
import os
|
|
11
|
-
import subprocess
|
|
12
|
-
import sys
|
|
13
|
-
from datetime import datetime, timedelta
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
from typing import Callable, Dict, List, Optional
|
|
16
|
-
|
|
17
|
-
import httpx
|
|
18
|
-
from fastapi import HTTPException
|
|
19
|
-
|
|
20
|
-
# ── MCP Registry (built-in tool definitions) ─────────────────────────────────
|
|
21
|
-
# Built-in MCP server catalog data → .mcp_catalog (re-exported).
|
|
22
|
-
from .mcp_catalog import MCP_REGISTRY # noqa: F401
|
|
23
|
-
|
|
24
|
-
# ── Remote MCP Registry (registry.modelcontextprotocol.io) ───────────────────
|
|
25
|
-
_REMOTE_REGISTRY_CACHE: List[Dict] = []
|
|
26
|
-
_REMOTE_REGISTRY_FETCHED_AT: Optional[datetime] = None
|
|
27
|
-
_REMOTE_REGISTRY_TTL = timedelta(hours=1)
|
|
28
|
-
_REMOTE_REGISTRY_URL = "https://registry.modelcontextprotocol.io/v0/servers"
|
|
29
|
-
_LOCAL_IDS = {e["id"] for e in MCP_REGISTRY}
|
|
30
|
-
|
|
31
|
-
async def _fetch_remote_mcp_registry() -> List[Dict]:
|
|
32
|
-
global _REMOTE_REGISTRY_CACHE, _REMOTE_REGISTRY_FETCHED_AT
|
|
33
|
-
now = datetime.now()
|
|
34
|
-
if _REMOTE_REGISTRY_FETCHED_AT and (now - _REMOTE_REGISTRY_FETCHED_AT) < _REMOTE_REGISTRY_TTL:
|
|
35
|
-
return _REMOTE_REGISTRY_CACHE
|
|
36
|
-
try:
|
|
37
|
-
result: List[Dict] = []
|
|
38
|
-
cursor = None
|
|
39
|
-
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
40
|
-
while True:
|
|
41
|
-
params: Dict = {"limit": 100}
|
|
42
|
-
if cursor:
|
|
43
|
-
params["cursor"] = cursor
|
|
44
|
-
resp = await client.get(_REMOTE_REGISTRY_URL, params=params)
|
|
45
|
-
resp.raise_for_status()
|
|
46
|
-
data = resp.json()
|
|
47
|
-
for s in data.get("servers", []):
|
|
48
|
-
srv = s["server"]
|
|
49
|
-
meta = s.get("_meta", {}).get("io.modelcontextprotocol.registry/official", {})
|
|
50
|
-
if not meta.get("isLatest", True):
|
|
51
|
-
continue
|
|
52
|
-
pkg = next(
|
|
53
|
-
(p for p in srv.get("packages", [])
|
|
54
|
-
if p.get("transport", {}).get("type") == "stdio"
|
|
55
|
-
and p.get("registryType") in ("npm", "pypi")),
|
|
56
|
-
None,
|
|
57
|
-
)
|
|
58
|
-
if not pkg:
|
|
59
|
-
continue
|
|
60
|
-
entry_id = srv["name"].replace("/", "-").replace(".", "-")
|
|
61
|
-
if entry_id in _LOCAL_IDS:
|
|
62
|
-
continue
|
|
63
|
-
result.append({
|
|
64
|
-
"id": entry_id,
|
|
65
|
-
"name": srv.get("title") or srv["name"],
|
|
66
|
-
"category": "MCP Registry",
|
|
67
|
-
"install_mode": pkg["registryType"],
|
|
68
|
-
"package": pkg["identifier"],
|
|
69
|
-
"package_version": pkg.get("version"),
|
|
70
|
-
"description": srv.get("description", ""),
|
|
71
|
-
"keywords": [],
|
|
72
|
-
"capabilities": [],
|
|
73
|
-
"source": "registry",
|
|
74
|
-
"homepage": (srv.get("repository") or {}).get("url"),
|
|
75
|
-
})
|
|
76
|
-
cursor = data.get("nextCursor")
|
|
77
|
-
if not cursor:
|
|
78
|
-
break
|
|
79
|
-
_REMOTE_REGISTRY_CACHE = result
|
|
80
|
-
_REMOTE_REGISTRY_FETCHED_AT = now
|
|
81
|
-
logging.info("Fetched %d stdio MCP servers from remote registry", len(result))
|
|
82
|
-
except Exception as e:
|
|
83
|
-
logging.warning("Failed to fetch remote MCP registry: %s", e)
|
|
84
|
-
return _REMOTE_REGISTRY_CACHE
|
|
85
|
-
|
|
86
|
-
async def _get_combined_registry() -> List[Dict]:
|
|
87
|
-
remote = await _fetch_remote_mcp_registry()
|
|
88
|
-
return MCP_REGISTRY + remote
|
|
89
|
-
|
|
90
|
-
# ── Anthropic Skills Marketplace (Apache 2.0) ─────────────────────────────────
|
|
91
|
-
_MARKETPLACE_RAW = "https://raw.githubusercontent.com/anthropics/claude-plugins-official/main"
|
|
92
|
-
_MARKETPLACE_API = "https://api.github.com/repos/anthropics/claude-plugins-official/contents"
|
|
93
|
-
|
|
94
|
-
# 검증된 서드파티 skills 소스 (Apache-2.0 / MIT)
|
|
95
|
-
_THIRD_PARTY_SKILL_SOURCES: List[Dict] = [
|
|
96
|
-
{
|
|
97
|
-
"plugin": "adobe-for-creativity", "author": "Adobe", "license": "Apache-2.0",
|
|
98
|
-
"repo": "adobe/skills", "branch": "main",
|
|
99
|
-
"plugin_path": "plugins/creative-cloud/adobe-for-creativity",
|
|
100
|
-
"category": "design",
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
"plugin": "airtable", "author": "Airtable", "license": "MIT",
|
|
104
|
-
"repo": "Airtable/skills", "branch": "main",
|
|
105
|
-
"plugin_path": "plugins/airtable",
|
|
106
|
-
"category": "productivity",
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
"plugin": "auth0", "author": "Auth0", "license": "Apache-2.0",
|
|
110
|
-
"repo": "auth0/agent-skills", "branch": "main",
|
|
111
|
-
"plugin_path": "plugins/auth0",
|
|
112
|
-
"category": "security",
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
"plugin": "expo", "author": "Expo", "license": "MIT",
|
|
116
|
-
"repo": "expo/skills", "branch": "main",
|
|
117
|
-
"plugin_path": "plugins/expo",
|
|
118
|
-
"category": "development",
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
"plugin": "logfire", "author": "Pydantic", "license": "MIT",
|
|
122
|
-
"repo": "pydantic/skills", "branch": "main",
|
|
123
|
-
"plugin_path": "plugins/logfire",
|
|
124
|
-
"category": "monitoring",
|
|
125
|
-
},
|
|
126
|
-
]
|
|
127
|
-
|
|
128
|
-
# 검증된 레포 라이선스 맵 (GitHub API 없이 빠르게 조회)
|
|
129
|
-
_KNOWN_REPO_LICENSES: Dict[str, str] = {
|
|
130
|
-
# Apache-2.0
|
|
131
|
-
"adobe/skills": "Apache-2.0", "awslabs/agent-plugins": "Apache-2.0",
|
|
132
|
-
"auth0/agent-skills": "Apache-2.0", "aws/agent-toolkit-for-aws": "Apache-2.0",
|
|
133
|
-
"carta/plugins": "Apache-2.0", "circlefin/skills": "Apache-2.0",
|
|
134
|
-
"clickhouse/clickhouse-docs": "Apache-2.0", "cloudflare/agents": "Apache-2.0",
|
|
135
|
-
"cockroachdb/claude-code": "Apache-2.0", "codspeed-hq/codspeed-claude": "Apache-2.0",
|
|
136
|
-
"DataDog/datadog-claude-code": "Apache-2.0", "datahub-project/datahub-skills": "Apache-2.0",
|
|
137
|
-
"neondatabase/agent-skills": "Apache-2.0", "PagerDuty/pd-ai-agents-plugins": "Apache-2.0",
|
|
138
|
-
"getpostman/postman-mcp-server": "Apache-2.0", "qdrant/qdrant-skills": "Apache-2.0",
|
|
139
|
-
"rootlyhq/rootly-plugins": "Apache-2.0", "snowflake-labs/snowflake-claude": "Apache-2.0",
|
|
140
|
-
"sumup/sumup-claude": "Apache-2.0", "zilliz-labs/zilliz-skills": "Apache-2.0",
|
|
141
|
-
"mercadopago/mercadopago-claude-marketplace": "Apache-2.0",
|
|
142
|
-
# MIT
|
|
143
|
-
"Airtable/skills": "MIT", "endorlabs/ai-plugins": "MIT",
|
|
144
|
-
"apollographql/apollo-claude-skills": "MIT", "appwrite/skills": "MIT",
|
|
145
|
-
"atlan-inc/claude-code-skills": "MIT", "boxer/boxerbox": "MIT",
|
|
146
|
-
"buildkite/claude-code": "MIT", "coderabbitai/coderabbit-skills": "MIT",
|
|
147
|
-
"CrowdStrike/crowdstrike-skills": "MIT", "microsoft/Dataverse-skills": "MIT",
|
|
148
|
-
"duckdb/duckdb-skills": "MIT", "expo/skills": "MIT",
|
|
149
|
-
"intercom/intercom-skills": "MIT", "pydantic/skills": "MIT",
|
|
150
|
-
"mapbox/mapbox-skills": "MIT", "mintlify/mintlify-skills": "MIT",
|
|
151
|
-
"miroapp/miro-ai": "MIT", "netlify/netlify-skills": "MIT",
|
|
152
|
-
"pinecone-io/pinecone-skills": "MIT", "railwayapp/railway-skills": "MIT",
|
|
153
|
-
"resend/resend-skills": "MIT", "sanity-io/sanity-skills": "MIT",
|
|
154
|
-
"getsentry/sentry-ai-skills": "MIT", "Shopify/liquid-skills": "MIT",
|
|
155
|
-
"slackapi/slack-skills": "MIT", "stripe/stripe-skills": "MIT",
|
|
156
|
-
"twilio-labs/twilio-skills": "MIT", "workos/workos-skills": "MIT",
|
|
157
|
-
"zoom/zoom-skills": "MIT", "aws-samples/sample-claude-code-plugins-for-startups": "MIT-0",
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
_SKILLS_MARKETPLACE_CACHE: List[Dict] = []
|
|
161
|
-
_SKILLS_MARKETPLACE_FETCHED_AT: Optional[datetime] = None
|
|
162
|
-
_SKILLS_MARKETPLACE_TTL = timedelta(hours=1)
|
|
163
|
-
|
|
164
|
-
def _extract_skill_desc(skill_md: str, fallback: str) -> str:
|
|
165
|
-
for line in skill_md.splitlines():
|
|
166
|
-
if line.startswith("description:"):
|
|
167
|
-
return line.split(":", 1)[1].strip()
|
|
168
|
-
return fallback
|
|
169
|
-
|
|
170
|
-
async def _fetch_plugin_skills(client: httpx.AsyncClient, source: Dict) -> List[Dict]:
|
|
171
|
-
"""단일 소스에서 skill 목록을 fetch해 반환"""
|
|
172
|
-
repo, branch, plugin_path = source["repo"], source["branch"], source["plugin_path"]
|
|
173
|
-
raw_base = f"https://raw.githubusercontent.com/{repo}/{branch}"
|
|
174
|
-
api_base = f"https://api.github.com/repos/{repo}/contents"
|
|
175
|
-
homepage_base = f"https://github.com/{repo}/tree/{branch}"
|
|
176
|
-
|
|
177
|
-
dir_resp = await client.get(f"{api_base}/{plugin_path}/skills")
|
|
178
|
-
if dir_resp.status_code != 200:
|
|
179
|
-
return []
|
|
180
|
-
skill_dirs = [f["name"] for f in dir_resp.json() if f["type"] == "dir"]
|
|
181
|
-
|
|
182
|
-
skills: List[Dict] = []
|
|
183
|
-
for skill_name in skill_dirs:
|
|
184
|
-
skill_md_url = f"{raw_base}/{plugin_path}/skills/{skill_name}/SKILL.md"
|
|
185
|
-
sm_resp = await client.get(skill_md_url)
|
|
186
|
-
if sm_resp.status_code != 200:
|
|
187
|
-
continue
|
|
188
|
-
skills.append({
|
|
189
|
-
"plugin": source["plugin"],
|
|
190
|
-
"skill": skill_name,
|
|
191
|
-
"category": source.get("category", "development"),
|
|
192
|
-
"description": _extract_skill_desc(sm_resp.text, source.get("description", "")),
|
|
193
|
-
"skill_md_url": skill_md_url,
|
|
194
|
-
"homepage": f"{homepage_base}/{plugin_path}/skills/{skill_name}",
|
|
195
|
-
"license": source["license"],
|
|
196
|
-
"author": source["author"],
|
|
197
|
-
})
|
|
198
|
-
return skills
|
|
199
|
-
|
|
200
|
-
async def _fetch_skills_marketplace() -> List[Dict]:
|
|
201
|
-
global _SKILLS_MARKETPLACE_CACHE, _SKILLS_MARKETPLACE_FETCHED_AT
|
|
202
|
-
now = datetime.now()
|
|
203
|
-
if _SKILLS_MARKETPLACE_FETCHED_AT and (now - _SKILLS_MARKETPLACE_FETCHED_AT) < _SKILLS_MARKETPLACE_TTL:
|
|
204
|
-
return _SKILLS_MARKETPLACE_CACHE
|
|
205
|
-
try:
|
|
206
|
-
result: List[Dict] = []
|
|
207
|
-
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
208
|
-
# ── Anthropic 공식 skills (Apache-2.0) ──────────────────────────
|
|
209
|
-
mp_resp = await client.get(f"{_MARKETPLACE_RAW}/.claude-plugin/marketplace.json")
|
|
210
|
-
mp_resp.raise_for_status()
|
|
211
|
-
marketplace_json = mp_resp.json()
|
|
212
|
-
anthropic_plugins = [
|
|
213
|
-
p for p in marketplace_json.get("plugins", [])
|
|
214
|
-
if (p.get("author") or {}).get("name") == "Anthropic"
|
|
215
|
-
and isinstance(p.get("source"), str)
|
|
216
|
-
and p["source"].startswith("./")
|
|
217
|
-
]
|
|
218
|
-
for plugin in anthropic_plugins:
|
|
219
|
-
plugin_path = plugin["source"].lstrip("./")
|
|
220
|
-
result.extend(await _fetch_plugin_skills(client, {
|
|
221
|
-
"plugin": plugin["name"],
|
|
222
|
-
"author": "Anthropic",
|
|
223
|
-
"license": "Apache-2.0",
|
|
224
|
-
"repo": "anthropics/claude-plugins-official",
|
|
225
|
-
"branch": "main",
|
|
226
|
-
"plugin_path": plugin_path,
|
|
227
|
-
"category": plugin.get("category", "development"),
|
|
228
|
-
"description": plugin.get("description", ""),
|
|
229
|
-
}))
|
|
230
|
-
# ── 검증된 서드파티 skills ────────────────────────────────────────
|
|
231
|
-
for source in _THIRD_PARTY_SKILL_SOURCES:
|
|
232
|
-
result.extend(await _fetch_plugin_skills(client, source))
|
|
233
|
-
|
|
234
|
-
_SKILLS_MARKETPLACE_CACHE = result
|
|
235
|
-
_SKILLS_MARKETPLACE_FETCHED_AT = now
|
|
236
|
-
logging.info("Fetched %d skills from marketplace (%d sources)",
|
|
237
|
-
len(result), len(anthropic_plugins) + len(_THIRD_PARTY_SKILL_SOURCES))
|
|
238
|
-
except Exception as e:
|
|
239
|
-
logging.warning("Failed to fetch skills marketplace: %s", e)
|
|
240
|
-
return _SKILLS_MARKETPLACE_CACHE
|
|
241
|
-
|
|
242
|
-
# ── Plugin Directory ──────────────────────────────────────────────────────────
|
|
243
|
-
_PLUGIN_DIRECTORY_CACHE: List[Dict] = []
|
|
244
|
-
_PLUGIN_DIRECTORY_FETCHED_AT: Optional[datetime] = None
|
|
245
|
-
_PLUGIN_DIRECTORY_TTL = timedelta(hours=1)
|
|
246
|
-
_OPEN_LICENSES = {"Apache-2.0", "MIT", "MIT-0", "CC-BY-4.0"}
|
|
247
|
-
_REPO_LICENSE_CACHE: Dict[str, str] = {}
|
|
248
|
-
|
|
249
|
-
async def _get_repo_license(client: httpx.AsyncClient, repo: str) -> str:
|
|
250
|
-
if repo in _REPO_LICENSE_CACHE:
|
|
251
|
-
return _REPO_LICENSE_CACHE[repo]
|
|
252
|
-
if repo in _KNOWN_REPO_LICENSES:
|
|
253
|
-
_REPO_LICENSE_CACHE[repo] = _KNOWN_REPO_LICENSES[repo]
|
|
254
|
-
return _KNOWN_REPO_LICENSES[repo]
|
|
255
|
-
try:
|
|
256
|
-
r = await client.get(f"https://api.github.com/repos/{repo}", timeout=5.0)
|
|
257
|
-
lic = (r.json().get("license") or {}).get("spdx_id", "") if r.status_code == 200 else ""
|
|
258
|
-
except Exception:
|
|
259
|
-
lic = ""
|
|
260
|
-
_REPO_LICENSE_CACHE[repo] = lic
|
|
261
|
-
return lic
|
|
262
|
-
|
|
263
|
-
async def _fetch_plugin_directory() -> List[Dict]:
|
|
264
|
-
global _PLUGIN_DIRECTORY_CACHE, _PLUGIN_DIRECTORY_FETCHED_AT
|
|
265
|
-
now = datetime.now()
|
|
266
|
-
if _PLUGIN_DIRECTORY_FETCHED_AT and (now - _PLUGIN_DIRECTORY_FETCHED_AT) < _PLUGIN_DIRECTORY_TTL:
|
|
267
|
-
return _PLUGIN_DIRECTORY_CACHE
|
|
268
|
-
try:
|
|
269
|
-
result: List[Dict] = []
|
|
270
|
-
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
271
|
-
mp_resp = await client.get(f"{_MARKETPLACE_RAW}/.claude-plugin/marketplace.json")
|
|
272
|
-
mp_resp.raise_for_status()
|
|
273
|
-
plugins = mp_resp.json().get("plugins", [])
|
|
274
|
-
|
|
275
|
-
for p in plugins:
|
|
276
|
-
author = (p.get("author") or {}).get("name", "")
|
|
277
|
-
src = p.get("source", {})
|
|
278
|
-
|
|
279
|
-
# Anthropic 같은 레포 플러그인 → Apache-2.0 확인됨
|
|
280
|
-
if isinstance(src, str) and src.startswith("./") and author == "Anthropic":
|
|
281
|
-
plugin_path = src.lstrip("./")
|
|
282
|
-
result.append({
|
|
283
|
-
"name": p["name"],
|
|
284
|
-
"description": p.get("description", ""),
|
|
285
|
-
"category": p.get("category", ""),
|
|
286
|
-
"author": author,
|
|
287
|
-
"license": "Apache-2.0",
|
|
288
|
-
"homepage": p.get("homepage") or f"https://github.com/anthropics/claude-plugins-official/tree/main/{plugin_path}",
|
|
289
|
-
"source_type": "anthropic",
|
|
290
|
-
})
|
|
291
|
-
continue
|
|
292
|
-
|
|
293
|
-
# 외부 레포 플러그인 → 라이선스 확인
|
|
294
|
-
if not isinstance(src, dict):
|
|
295
|
-
continue
|
|
296
|
-
repo_url = src.get("url", "").replace("https://github.com/", "").replace(".git", "").split("/tree/")[0]
|
|
297
|
-
if not repo_url:
|
|
298
|
-
continue
|
|
299
|
-
license_id = await _get_repo_license(client, repo_url)
|
|
300
|
-
if license_id not in _OPEN_LICENSES:
|
|
301
|
-
continue
|
|
302
|
-
result.append({
|
|
303
|
-
"name": p["name"],
|
|
304
|
-
"description": p.get("description", ""),
|
|
305
|
-
"category": p.get("category", ""),
|
|
306
|
-
"author": author or repo_url.split("/")[0],
|
|
307
|
-
"license": license_id,
|
|
308
|
-
"homepage": p.get("homepage") or f"https://github.com/{repo_url}",
|
|
309
|
-
"source_type": "third-party",
|
|
310
|
-
})
|
|
311
|
-
|
|
312
|
-
_PLUGIN_DIRECTORY_CACHE = result
|
|
313
|
-
_PLUGIN_DIRECTORY_FETCHED_AT = now
|
|
314
|
-
logging.info("Fetched plugin directory: %d open-source plugins", len(result))
|
|
315
|
-
except Exception as e:
|
|
316
|
-
logging.warning("Failed to fetch plugin directory: %s", e)
|
|
317
|
-
return _PLUGIN_DIRECTORY_CACHE
|
|
318
|
-
|
|
319
|
-
# ─────────────────────────────────────────────────────────────────────────────
|
|
320
|
-
|
|
321
|
-
SKILLS_DIR = Path(__file__).resolve().parent / "skills"
|
|
322
|
-
|
|
323
|
-
async def install_skill(plugin: str, skill: str) -> Dict:
|
|
324
|
-
marketplace = await _fetch_skills_marketplace()
|
|
325
|
-
entry = next((s for s in marketplace if s["plugin"] == plugin and s["skill"] == skill), None)
|
|
326
|
-
if not entry:
|
|
327
|
-
raise HTTPException(status_code=404, detail=f"Skill '{plugin}/{skill}' not found in marketplace")
|
|
328
|
-
skill_dir = SKILLS_DIR / skill
|
|
329
|
-
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
330
|
-
skill_md_path = skill_dir / "SKILL.md"
|
|
331
|
-
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
332
|
-
resp = await client.get(entry["skill_md_url"])
|
|
333
|
-
resp.raise_for_status()
|
|
334
|
-
content = resp.text
|
|
335
|
-
# 출처 표기 (Apache-2.0 / MIT 공통)
|
|
336
|
-
repo_hint = entry.get("homepage", "")
|
|
337
|
-
attribution = f"<!-- Source: {repo_hint}, {entry['license']} -->\n"
|
|
338
|
-
if not content.startswith("<!--"):
|
|
339
|
-
content = attribution + content
|
|
340
|
-
skill_md_path.write_text(content, encoding="utf-8")
|
|
341
|
-
risk_path = skill_dir / "risk.json"
|
|
342
|
-
if not risk_path.exists():
|
|
343
|
-
risk_path.write_text(json.dumps({
|
|
344
|
-
"risk": "read", "destructive": False,
|
|
345
|
-
"shell": False, "network": False,
|
|
346
|
-
"auto_approve": True, "sandbox": "workspace", "rollback": "none"
|
|
347
|
-
}, indent=2), encoding="utf-8")
|
|
348
|
-
return {
|
|
349
|
-
"status": "installed",
|
|
350
|
-
"plugin": plugin,
|
|
351
|
-
"skill": skill,
|
|
352
|
-
"path": str(skill_dir),
|
|
353
|
-
"license": entry["license"],
|
|
354
|
-
"author": entry["author"],
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
def _run_installer(command: List[str], timeout: int) -> "subprocess.CompletedProcess[str]":
|
|
359
|
-
"""Run one package-installer command. Blocking — call off the event loop."""
|
|
360
|
-
return subprocess.run(
|
|
361
|
-
command, capture_output=True, text=True, timeout=timeout, check=False
|
|
362
|
-
)
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
def create_mcp_install_state(data_dir: Path) -> Dict[str, Callable]:
|
|
366
|
-
"""Return bound MCP state helpers for given data_dir. No global side effects."""
|
|
367
|
-
MCP_FILE = Path(data_dir) / "mcp_installs.json"
|
|
368
|
-
|
|
369
|
-
def load_mcp_installs() -> Dict:
|
|
370
|
-
if not os.path.exists(MCP_FILE):
|
|
371
|
-
return {"installed": {}, "updated_at": None}
|
|
372
|
-
try:
|
|
373
|
-
with open(MCP_FILE, "r", encoding="utf-8") as f:
|
|
374
|
-
data = json.load(f)
|
|
375
|
-
if "installed" not in data:
|
|
376
|
-
data["installed"] = {}
|
|
377
|
-
return data
|
|
378
|
-
except Exception as e:
|
|
379
|
-
logging.warning("load_mcp_installs failed: %s", e)
|
|
380
|
-
return {"installed": {}, "updated_at": None}
|
|
381
|
-
|
|
382
|
-
def save_mcp_installs(data: Dict) -> None:
|
|
383
|
-
data["updated_at"] = datetime.now().isoformat()
|
|
384
|
-
with open(MCP_FILE, "w", encoding="utf-8") as f:
|
|
385
|
-
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
386
|
-
|
|
387
|
-
def mcp_public_item(item: Dict, installed_state: Dict) -> Dict:
|
|
388
|
-
state = installed_state.get(item.get("id"), {}) or {}
|
|
389
|
-
installed = item.get("install_mode") in {"builtin", "bundled"} or bool(state.get("installed"))
|
|
390
|
-
connector_pending = item.get("install_mode") == "connector" and not state.get("authenticated")
|
|
391
|
-
authenticated = item.get("install_mode") != "connector" or bool(state.get("authenticated"))
|
|
392
|
-
return {
|
|
393
|
-
"id": item.get("id"),
|
|
394
|
-
"name": item.get("name"),
|
|
395
|
-
"category": item.get("category", ""),
|
|
396
|
-
"install_mode": item.get("install_mode"),
|
|
397
|
-
"description": item.get("description", ""),
|
|
398
|
-
"capabilities": item.get("capabilities", []),
|
|
399
|
-
"connector_url": item.get("connector_url"),
|
|
400
|
-
"external_url": item.get("external_url"),
|
|
401
|
-
"package": item.get("package"),
|
|
402
|
-
"homepage": item.get("homepage"),
|
|
403
|
-
"source": item.get("source", "local"),
|
|
404
|
-
"installed": installed,
|
|
405
|
-
"status": state.get("status") or ("active" if installed and not connector_pending else "needs_auth" if connector_pending else "available"),
|
|
406
|
-
"authenticated": authenticated,
|
|
407
|
-
"updated_at": state.get("updated_at"),
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
async def recommend_mcps(query: str, limit: int = 5) -> List[Dict]:
|
|
411
|
-
text = (query or "").lower()
|
|
412
|
-
installed = load_mcp_installs().get("installed", {})
|
|
413
|
-
registry = await _get_combined_registry()
|
|
414
|
-
scored: List[Dict] = []
|
|
415
|
-
for item in registry:
|
|
416
|
-
score = 0
|
|
417
|
-
hits: List[str] = []
|
|
418
|
-
for keyword in item.get("keywords", []):
|
|
419
|
-
if keyword.lower() in text:
|
|
420
|
-
score += 3 if len(keyword) > 2 else 1
|
|
421
|
-
hits.append(keyword)
|
|
422
|
-
if not hits and text:
|
|
423
|
-
desc_words = item.get("description", "").lower().split()
|
|
424
|
-
for word in text.split():
|
|
425
|
-
if len(word) > 2 and word in desc_words:
|
|
426
|
-
score += 1
|
|
427
|
-
hits.append(word)
|
|
428
|
-
if item.get("id") == "filesystem" and any(w in text for w in ["만들", "구현", "build", "deploy", "코드", "앱"]):
|
|
429
|
-
score += 2
|
|
430
|
-
if score:
|
|
431
|
-
public = mcp_public_item(item, installed)
|
|
432
|
-
public["score"] = score
|
|
433
|
-
public["matched_keywords"] = hits[:6]
|
|
434
|
-
scored.append(public)
|
|
435
|
-
if not scored:
|
|
436
|
-
fallback_ids = ["filesystem", "browser", "documents"]
|
|
437
|
-
scored = [
|
|
438
|
-
{**mcp_public_item(item, installed), "score": 1, "matched_keywords": []}
|
|
439
|
-
for item in registry if item.get("id") in fallback_ids
|
|
440
|
-
]
|
|
441
|
-
return sorted(scored, key=lambda x: x["score"], reverse=True)[:max(1, min(limit, 24))]
|
|
442
|
-
|
|
443
|
-
async def install_mcp(mcp_id: str) -> Dict:
|
|
444
|
-
registry = await _get_combined_registry()
|
|
445
|
-
item = next((entry for entry in registry if entry.get("id") == mcp_id), None)
|
|
446
|
-
if not item:
|
|
447
|
-
raise HTTPException(status_code=404, detail="MCP를 찾을 수 없습니다.")
|
|
448
|
-
data = load_mcp_installs()
|
|
449
|
-
state = data.setdefault("installed", {})
|
|
450
|
-
status = "active"
|
|
451
|
-
message = "MCP가 활성화되었습니다."
|
|
452
|
-
if item.get("install_mode") == "connector":
|
|
453
|
-
status = "needs_auth"
|
|
454
|
-
message = "커넥터 인증이 필요합니다. Codex 앱의 connector 설정에서 계정을 연결하면 바로 사용할 수 있습니다."
|
|
455
|
-
elif item.get("install_mode") == "pip":
|
|
456
|
-
# Package installers are minutes of network I/O. On the event loop
|
|
457
|
-
# they froze every other request for the whole install; each one now
|
|
458
|
-
# runs on a worker thread instead.
|
|
459
|
-
packages = item.get("pip_packages") or []
|
|
460
|
-
for pkg in packages:
|
|
461
|
-
completed = await asyncio.to_thread(
|
|
462
|
-
_run_installer,
|
|
463
|
-
[sys.executable, "-m", "pip", "install", "--upgrade", pkg],
|
|
464
|
-
900,
|
|
465
|
-
)
|
|
466
|
-
if completed.returncode != 0:
|
|
467
|
-
raise HTTPException(status_code=500, detail=(completed.stderr or "")[-2000:] or f"{pkg} 설치 실패")
|
|
468
|
-
message = f"필수 패키지 설치 완료: {', '.join(packages)}"
|
|
469
|
-
elif item.get("install_mode") == "pypi":
|
|
470
|
-
pkg = item.get("package", "")
|
|
471
|
-
version = item.get("package_version")
|
|
472
|
-
pkg_str = f"{pkg}=={version}" if version else pkg
|
|
473
|
-
completed = await asyncio.to_thread(
|
|
474
|
-
_run_installer, [sys.executable, "-m", "pip", "install", pkg_str], 300
|
|
475
|
-
)
|
|
476
|
-
if completed.returncode != 0:
|
|
477
|
-
raise HTTPException(status_code=500, detail=(completed.stderr or "")[-2000:] or f"{pkg} 설치 실패")
|
|
478
|
-
message = f"pip 패키지 설치 완료: {pkg_str}"
|
|
479
|
-
elif item.get("install_mode") == "npm":
|
|
480
|
-
pkg = item.get("package", "")
|
|
481
|
-
version = item.get("package_version")
|
|
482
|
-
pkg_str = f"{pkg}@{version}" if version else pkg
|
|
483
|
-
completed = await asyncio.to_thread(
|
|
484
|
-
_run_installer, ["npm", "install", "-g", pkg_str], 300
|
|
485
|
-
)
|
|
486
|
-
if completed.returncode != 0:
|
|
487
|
-
raise HTTPException(status_code=500, detail=(completed.stderr or "")[-2000:] or f"{pkg} 설치 실패")
|
|
488
|
-
message = f"npm 패키지 설치 완료: {pkg_str}"
|
|
489
|
-
state[mcp_id] = {
|
|
490
|
-
"installed": True,
|
|
491
|
-
"status": status,
|
|
492
|
-
"authenticated": item.get("install_mode") != "connector",
|
|
493
|
-
"updated_at": datetime.now().isoformat(),
|
|
494
|
-
}
|
|
495
|
-
save_mcp_installs(data)
|
|
496
|
-
public = mcp_public_item(item, state)
|
|
497
|
-
public["message"] = message
|
|
498
|
-
return public
|
|
499
|
-
|
|
500
|
-
return {
|
|
501
|
-
"load_mcp_installs": load_mcp_installs,
|
|
502
|
-
"save_mcp_installs": save_mcp_installs,
|
|
503
|
-
"mcp_public_item": mcp_public_item,
|
|
504
|
-
"recommend_mcps": recommend_mcps,
|
|
505
|
-
"install_mcp": install_mcp,
|
|
506
|
-
}
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
"""Network boundary modes for hybrid local + cloud LLM usage.
|
|
2
|
-
|
|
3
|
-
Local-first principle: the Knowledge Graph and all durable memory stay on the
|
|
4
|
-
machine. Cloud LLMs are an explicit, opt-in worker. This module defines the
|
|
5
|
-
dial that controls whether any knowledge is allowed to leave the host.
|
|
6
|
-
|
|
7
|
-
Modes
|
|
8
|
-
-----
|
|
9
|
-
* ``local_only`` — default. No chat context is ever sent to a cloud provider.
|
|
10
|
-
* ``cloud_allowed`` — user has explicitly opted in. Only the *minimal* related
|
|
11
|
-
nodes selected by the hybrid context extractor may be sent. Sensitive types
|
|
12
|
-
and hard filters still apply (mode-invariant).
|
|
13
|
-
|
|
14
|
-
The mode is orthogonal to PermissionMode (agent autonomy). A session can be
|
|
15
|
-
``cloud_allowed`` + ``strict`` at the same time.
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
from __future__ import annotations
|
|
19
|
-
|
|
20
|
-
from enum import Enum
|
|
21
|
-
from typing import Any, Dict, FrozenSet, Mapping, Optional
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class NetworkBoundaryMode(str, Enum):
|
|
25
|
-
LOCAL_ONLY = "local_only"
|
|
26
|
-
CLOUD_ALLOWED = "cloud_allowed"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
DEFAULT_NETWORK_MODE = NetworkBoundaryMode.LOCAL_ONLY
|
|
30
|
-
|
|
31
|
-
# Node types that must never be included in a cloud payload, regardless of mode.
|
|
32
|
-
# Extend carefully; this is a hard circuit breaker.
|
|
33
|
-
#
|
|
34
|
-
# This was empty through 10.1.x ("keep empty for v1 scaffolding"), which meant
|
|
35
|
-
# the type arm of the filter could never fire. Combined with nothing being able
|
|
36
|
-
# to *set* the metadata flags below, the whole guard was unreachable — correct
|
|
37
|
-
# code protecting nothing.
|
|
38
|
-
HARD_BLOCK_NODE_TYPES: FrozenSet[str] = frozenset({
|
|
39
|
-
"Credential",
|
|
40
|
-
"Secret",
|
|
41
|
-
"ApiKey",
|
|
42
|
-
"Token",
|
|
43
|
-
"Password",
|
|
44
|
-
"PrivateKey",
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
# Metadata keys that, when present and truthy, block a node from leaving.
|
|
48
|
-
HARD_BLOCK_METADATA_FLAGS: FrozenSet[str] = frozenset({
|
|
49
|
-
"sensitive",
|
|
50
|
-
"private",
|
|
51
|
-
"do_not_share",
|
|
52
|
-
"local_only",
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
# Path-based never-leaves rules live in Brain Core: they describe the data, not
|
|
56
|
-
# the transport, and Brain Core cannot import from this package. Re-exported
|
|
57
|
-
# here so the boundary module stays the single place the app layer reads.
|
|
58
|
-
from lattice_brain.sensitivity import ( # noqa: E402
|
|
59
|
-
SENSITIVE_FILENAMES,
|
|
60
|
-
SENSITIVE_PATH_FRAGMENTS,
|
|
61
|
-
sensitive_reason_for_path,
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def normalize_network_mode(value: Any) -> NetworkBoundaryMode:
|
|
66
|
-
"""Parse user/API/env input into a NetworkBoundaryMode; unknown → local_only."""
|
|
67
|
-
if isinstance(value, NetworkBoundaryMode):
|
|
68
|
-
return value
|
|
69
|
-
text = str(value or "").strip().lower()
|
|
70
|
-
aliases = {
|
|
71
|
-
"local_only": NetworkBoundaryMode.LOCAL_ONLY,
|
|
72
|
-
"local": NetworkBoundaryMode.LOCAL_ONLY,
|
|
73
|
-
"local-only": NetworkBoundaryMode.LOCAL_ONLY,
|
|
74
|
-
"offline": NetworkBoundaryMode.LOCAL_ONLY,
|
|
75
|
-
"cloud_allowed": NetworkBoundaryMode.CLOUD_ALLOWED,
|
|
76
|
-
"cloud": NetworkBoundaryMode.CLOUD_ALLOWED,
|
|
77
|
-
"cloud-allowed": NetworkBoundaryMode.CLOUD_ALLOWED,
|
|
78
|
-
"hybrid": NetworkBoundaryMode.CLOUD_ALLOWED,
|
|
79
|
-
"online": NetworkBoundaryMode.CLOUD_ALLOWED,
|
|
80
|
-
}
|
|
81
|
-
return aliases.get(text, DEFAULT_NETWORK_MODE)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def network_mode_catalog() -> list[Dict[str, Any]]:
|
|
85
|
-
"""UI/API catalog for the network boundary selector."""
|
|
86
|
-
return [
|
|
87
|
-
{
|
|
88
|
-
"id": NetworkBoundaryMode.LOCAL_ONLY.value,
|
|
89
|
-
"label": "Local only",
|
|
90
|
-
"label_ko": "로컬만",
|
|
91
|
-
"summary": "Nothing leaves this machine. Answers use local models and the local Brain only.",
|
|
92
|
-
"summary_ko": "이 컴퓨터를 벗어나지 않습니다. 로컬 모델과 로컬 Brain만 사용합니다.",
|
|
93
|
-
"risk": "low",
|
|
94
|
-
"requires_ack": False,
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
"id": NetworkBoundaryMode.CLOUD_ALLOWED.value,
|
|
98
|
-
"label": "Cloud streaming allowed",
|
|
99
|
-
"label_ko": "클라우드 스트리밍 허용",
|
|
100
|
-
"summary": (
|
|
101
|
-
"Minimal related Knowledge Graph nodes may be sent to a cloud LLM. "
|
|
102
|
-
"The streamed answer is written back into the local Brain with provenance."
|
|
103
|
-
),
|
|
104
|
-
"summary_ko": (
|
|
105
|
-
"관련된 최소 Knowledge Graph 노드만 클라우드 LLM으로 전송될 수 있습니다. "
|
|
106
|
-
"스트리밍 답변은 provenance와 함께 로컬 Brain에 다시 기록됩니다."
|
|
107
|
-
),
|
|
108
|
-
"risk": "medium",
|
|
109
|
-
"requires_ack": True,
|
|
110
|
-
"warning": (
|
|
111
|
-
"Cloud mode sends a compact summary of selected local nodes to an external provider. "
|
|
112
|
-
"Sensitive nodes remain blocked. You can switch back to Local only at any time."
|
|
113
|
-
),
|
|
114
|
-
"warning_ko": (
|
|
115
|
-
"클라우드 모드는 선택된 로컬 노드의 압축 요약을 외부 제공자에게 전송합니다. "
|
|
116
|
-
"민감 노드는 계속 차단됩니다. 언제든지 로컬만으로 되돌릴 수 있습니다."
|
|
117
|
-
),
|
|
118
|
-
},
|
|
119
|
-
]
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def is_node_blocked_for_cloud(node: Mapping[str, Any]) -> Optional[str]:
|
|
123
|
-
"""Return a reason string when this node must never leave the host."""
|
|
124
|
-
node_type = str(node.get("type") or "")
|
|
125
|
-
if node_type in HARD_BLOCK_NODE_TYPES:
|
|
126
|
-
return f"node type {node_type!r} is blocked from cloud payloads"
|
|
127
|
-
|
|
128
|
-
meta = node.get("metadata") or {}
|
|
129
|
-
if not isinstance(meta, Mapping):
|
|
130
|
-
return None
|
|
131
|
-
for flag in HARD_BLOCK_METADATA_FLAGS:
|
|
132
|
-
if meta.get(flag):
|
|
133
|
-
return f"node flagged {flag!r} is blocked from cloud payloads"
|
|
134
|
-
return None
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def network_mode_contract(mode: NetworkBoundaryMode | str) -> Dict[str, Any]:
|
|
138
|
-
"""Serializable contract for API / agent / UI."""
|
|
139
|
-
mode = normalize_network_mode(mode)
|
|
140
|
-
entry = next(
|
|
141
|
-
(m for m in network_mode_catalog() if m["id"] == mode.value),
|
|
142
|
-
network_mode_catalog()[0],
|
|
143
|
-
)
|
|
144
|
-
return {
|
|
145
|
-
"mode": mode.value,
|
|
146
|
-
"label": entry["label"],
|
|
147
|
-
"label_ko": entry["label_ko"],
|
|
148
|
-
"risk": entry["risk"],
|
|
149
|
-
"requires_ack": entry["requires_ack"],
|
|
150
|
-
"allows_cloud": mode == NetworkBoundaryMode.CLOUD_ALLOWED,
|
|
151
|
-
"hard_block_node_types": sorted(HARD_BLOCK_NODE_TYPES),
|
|
152
|
-
"hard_block_metadata_flags": sorted(HARD_BLOCK_METADATA_FLAGS),
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
__all__ = [
|
|
157
|
-
"NetworkBoundaryMode",
|
|
158
|
-
"DEFAULT_NETWORK_MODE",
|
|
159
|
-
"HARD_BLOCK_NODE_TYPES",
|
|
160
|
-
"HARD_BLOCK_METADATA_FLAGS",
|
|
161
|
-
"normalize_network_mode",
|
|
162
|
-
"network_mode_catalog",
|
|
163
|
-
"is_node_blocked_for_cloud",
|
|
164
|
-
"sensitive_reason_for_path",
|
|
165
|
-
"SENSITIVE_PATH_FRAGMENTS",
|
|
166
|
-
"SENSITIVE_FILENAMES",
|
|
167
|
-
"network_mode_contract",
|
|
168
|
-
]
|