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
package/latticeai/core/oidc.py
DELETED
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
"""Self-contained OIDC ID-token validation.
|
|
2
|
-
|
|
3
|
-
A JWT is ``base64url(header).base64url(claims).base64url(signature)``. The login
|
|
4
|
-
flow must *never* trust a decoded payload on its own — without verifying the
|
|
5
|
-
signature an attacker can forge any ``email``/``sub`` claim. This module verifies
|
|
6
|
-
the RSA signature against the provider's published JWKS and then validates the
|
|
7
|
-
standard registered claims plus the login ``nonce``.
|
|
8
|
-
|
|
9
|
-
Design goals:
|
|
10
|
-
|
|
11
|
-
* No third-party JWT dependency — only the standard library plus ``cryptography``
|
|
12
|
-
(already a transitive dependency, and pinned explicitly in ``pyproject.toml``).
|
|
13
|
-
* **Fail-closed**: any anomaly raises :class:`OIDCValidationError`; the caller
|
|
14
|
-
must reject the login. There is no "best effort accept".
|
|
15
|
-
* Asymmetric algorithms only (``RS256``/``RS384``/``RS512``). ``alg: none`` and
|
|
16
|
-
symmetric ``HS*`` tokens are rejected outright — the classic OIDC bypasses.
|
|
17
|
-
* Pure and injectable: :func:`verify_id_token` takes the JWKS and clock as
|
|
18
|
-
arguments so every rejection path is unit-testable offline.
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
from __future__ import annotations
|
|
22
|
-
|
|
23
|
-
import base64
|
|
24
|
-
import json
|
|
25
|
-
import time
|
|
26
|
-
from typing import Any, Dict, List, Optional
|
|
27
|
-
|
|
28
|
-
from latticeai.core.quiet import quiet
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class OIDCValidationError(Exception):
|
|
32
|
-
"""Raised when an OIDC ID token fails any validation step (fail-closed)."""
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
# Asymmetric RSA algorithms only. Excluding HS*/none is deliberate: an attacker
|
|
36
|
-
# who can set ``alg`` must not be able to downgrade to a symmetric or unsigned
|
|
37
|
-
# token. Maps JWT alg name → cryptography hash name.
|
|
38
|
-
_ALLOWED_ALGS: Dict[str, str] = {"RS256": "sha256", "RS384": "sha384", "RS512": "sha512"}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def _b64url_decode(segment: str) -> bytes:
|
|
42
|
-
if not isinstance(segment, str) or not segment:
|
|
43
|
-
raise OIDCValidationError("empty JWT segment")
|
|
44
|
-
padding = "=" * (-len(segment) % 4)
|
|
45
|
-
try:
|
|
46
|
-
return base64.urlsafe_b64decode(segment + padding)
|
|
47
|
-
except Exception as exc: # malformed base64
|
|
48
|
-
raise OIDCValidationError(f"invalid base64url segment: {exc}")
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def _b64url_uint(value: str) -> int:
|
|
52
|
-
return int.from_bytes(_b64url_decode(value), "big")
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def _split(token: str) -> List[str]:
|
|
56
|
-
parts = str(token or "").split(".")
|
|
57
|
-
if len(parts) != 3 or not all(parts):
|
|
58
|
-
raise OIDCValidationError("malformed JWT: expected three non-empty segments")
|
|
59
|
-
return parts
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def decode_unverified_header(token: str) -> Dict[str, Any]:
|
|
63
|
-
"""Decode the JWT header WITHOUT verifying it (used only to pick the key)."""
|
|
64
|
-
header_b64 = _split(token)[0]
|
|
65
|
-
try:
|
|
66
|
-
header = json.loads(_b64url_decode(header_b64))
|
|
67
|
-
except Exception as exc:
|
|
68
|
-
raise OIDCValidationError(f"invalid JWT header: {exc}")
|
|
69
|
-
if not isinstance(header, dict):
|
|
70
|
-
raise OIDCValidationError("JWT header is not an object")
|
|
71
|
-
return header
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def _public_key_from_jwk(jwk: Dict[str, Any]):
|
|
75
|
-
if not isinstance(jwk, dict) or jwk.get("kty") != "RSA":
|
|
76
|
-
raise OIDCValidationError("unsupported JWK key type (RSA required)")
|
|
77
|
-
if not jwk.get("n") or not jwk.get("e"):
|
|
78
|
-
raise OIDCValidationError("JWK missing modulus/exponent")
|
|
79
|
-
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
|
|
80
|
-
|
|
81
|
-
numbers = RSAPublicNumbers(_b64url_uint(jwk["e"]), _b64url_uint(jwk["n"]))
|
|
82
|
-
return numbers.public_key()
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def _candidate_keys(jwks: Any, kid: Optional[str]) -> List[Dict[str, Any]]:
|
|
86
|
-
keys = jwks.get("keys") if isinstance(jwks, dict) else jwks
|
|
87
|
-
if not isinstance(keys, list) or not keys:
|
|
88
|
-
raise OIDCValidationError("JWKS contains no keys")
|
|
89
|
-
rsa_keys = [k for k in keys if isinstance(k, dict) and k.get("kty") == "RSA"]
|
|
90
|
-
if kid:
|
|
91
|
-
matched = [k for k in rsa_keys if k.get("kid") == kid]
|
|
92
|
-
if not matched:
|
|
93
|
-
raise OIDCValidationError("no JWKS key matches the token 'kid'")
|
|
94
|
-
return matched
|
|
95
|
-
return rsa_keys
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def _verify_signature(token: str, jwks: Any) -> Dict[str, Any]:
|
|
99
|
-
header_b64, payload_b64, sig_b64 = _split(token)
|
|
100
|
-
header = decode_unverified_header(token)
|
|
101
|
-
alg = header.get("alg")
|
|
102
|
-
if alg not in _ALLOWED_ALGS:
|
|
103
|
-
# Rejects 'none' and symmetric HS* — the canonical signature-bypass attacks.
|
|
104
|
-
raise OIDCValidationError(f"unsupported or unsafe JWT alg: {alg!r}")
|
|
105
|
-
|
|
106
|
-
from cryptography.exceptions import InvalidSignature
|
|
107
|
-
from cryptography.hazmat.primitives import hashes
|
|
108
|
-
from cryptography.hazmat.primitives.asymmetric import padding
|
|
109
|
-
|
|
110
|
-
hash_obj = {"sha256": hashes.SHA256(), "sha384": hashes.SHA384(), "sha512": hashes.SHA512()}[
|
|
111
|
-
_ALLOWED_ALGS[alg]
|
|
112
|
-
]
|
|
113
|
-
signature = _b64url_decode(sig_b64)
|
|
114
|
-
signing_input = f"{header_b64}.{payload_b64}".encode("ascii")
|
|
115
|
-
|
|
116
|
-
for jwk in _candidate_keys(jwks, header.get("kid")):
|
|
117
|
-
try:
|
|
118
|
-
public_key = _public_key_from_jwk(jwk)
|
|
119
|
-
public_key.verify(signature, signing_input, padding.PKCS1v15(), hash_obj)
|
|
120
|
-
except (InvalidSignature, OIDCValidationError):
|
|
121
|
-
quiet()
|
|
122
|
-
continue
|
|
123
|
-
# Signature verified — now it is safe to parse the claims.
|
|
124
|
-
try:
|
|
125
|
-
claims = json.loads(_b64url_decode(payload_b64))
|
|
126
|
-
except Exception as exc:
|
|
127
|
-
raise OIDCValidationError(f"invalid JWT claims JSON: {exc}")
|
|
128
|
-
if not isinstance(claims, dict):
|
|
129
|
-
raise OIDCValidationError("JWT claims are not an object")
|
|
130
|
-
return claims
|
|
131
|
-
|
|
132
|
-
raise OIDCValidationError("signature verification failed against all JWKS keys")
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def verify_id_token(
|
|
136
|
-
id_token: str,
|
|
137
|
-
*,
|
|
138
|
-
jwks: Any,
|
|
139
|
-
issuer: str,
|
|
140
|
-
audience: str,
|
|
141
|
-
nonce: Optional[str] = None,
|
|
142
|
-
now: Optional[float] = None,
|
|
143
|
-
leeway: int = 60,
|
|
144
|
-
) -> Dict[str, Any]:
|
|
145
|
-
"""Verify an OIDC ID token and return its claims, or raise ``OIDCValidationError``.
|
|
146
|
-
|
|
147
|
-
Validates, in order: signature (against ``jwks``, RSA only), ``iss``, ``aud``
|
|
148
|
-
(and ``azp`` when multi-audience), ``exp``, ``iat``/``nbf``, and ``nonce``.
|
|
149
|
-
All checks are fail-closed.
|
|
150
|
-
"""
|
|
151
|
-
if not id_token:
|
|
152
|
-
raise OIDCValidationError("missing id_token")
|
|
153
|
-
if not issuer:
|
|
154
|
-
raise OIDCValidationError("issuer not configured")
|
|
155
|
-
if not audience:
|
|
156
|
-
raise OIDCValidationError("audience (client_id) not configured")
|
|
157
|
-
|
|
158
|
-
claims = _verify_signature(id_token, jwks)
|
|
159
|
-
current = int(now if now is not None else time.time())
|
|
160
|
-
|
|
161
|
-
if claims.get("iss") != issuer:
|
|
162
|
-
raise OIDCValidationError("issuer mismatch")
|
|
163
|
-
|
|
164
|
-
aud = claims.get("aud")
|
|
165
|
-
audiences = aud if isinstance(aud, list) else [aud]
|
|
166
|
-
if audience not in audiences:
|
|
167
|
-
raise OIDCValidationError("audience mismatch")
|
|
168
|
-
if isinstance(aud, list) and len(aud) > 1 and claims.get("azp") not in (None, audience):
|
|
169
|
-
raise OIDCValidationError("azp (authorized party) mismatch")
|
|
170
|
-
|
|
171
|
-
exp = claims.get("exp")
|
|
172
|
-
if exp is None:
|
|
173
|
-
raise OIDCValidationError("token missing 'exp'")
|
|
174
|
-
if current > int(exp) + leeway:
|
|
175
|
-
raise OIDCValidationError("token expired")
|
|
176
|
-
|
|
177
|
-
iat = claims.get("iat")
|
|
178
|
-
if iat is not None and int(iat) - leeway > current:
|
|
179
|
-
raise OIDCValidationError("token 'iat' is in the future")
|
|
180
|
-
|
|
181
|
-
nbf = claims.get("nbf")
|
|
182
|
-
if nbf is not None and int(nbf) - leeway > current:
|
|
183
|
-
raise OIDCValidationError("token not yet valid ('nbf')")
|
|
184
|
-
|
|
185
|
-
if nonce is not None and claims.get("nonce") != nonce:
|
|
186
|
-
raise OIDCValidationError("nonce mismatch")
|
|
187
|
-
|
|
188
|
-
return claims
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
async def fetch_jwks(jwks_uri: str, *, timeout: float = 15.0) -> Dict[str, Any]:
|
|
192
|
-
"""Fetch a provider JWKS document. Network-only; injectable in tests."""
|
|
193
|
-
if not jwks_uri:
|
|
194
|
-
raise OIDCValidationError("discovery document has no 'jwks_uri'")
|
|
195
|
-
import httpx
|
|
196
|
-
|
|
197
|
-
async with httpx.AsyncClient() as client:
|
|
198
|
-
resp = await client.get(jwks_uri, timeout=timeout)
|
|
199
|
-
resp.raise_for_status()
|
|
200
|
-
return resp.json()
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
__all__ = [
|
|
204
|
-
"OIDCValidationError",
|
|
205
|
-
"verify_id_token",
|
|
206
|
-
"fetch_jwks",
|
|
207
|
-
"decode_unverified_header",
|
|
208
|
-
]
|
|
@@ -1,432 +0,0 @@
|
|
|
1
|
-
"""Plugin SDK — manifest, registry, lifecycle, permissions, validation, and a
|
|
2
|
-
safe execution boundary.
|
|
3
|
-
|
|
4
|
-
The Plugin SDK is the v2 extension layer. It is intentionally additive:
|
|
5
|
-
a plugin is a directory under the configured ``plugins`` root that ships a
|
|
6
|
-
``plugin.json`` manifest and *extends* the existing Skill / Tool / Workflow
|
|
7
|
-
surfaces rather than replacing them. Installed standalone skills keep working
|
|
8
|
-
untouched; a plugin can simply *bundle* skills (and declare tools / workflow
|
|
9
|
-
templates) under one versioned, permissioned unit.
|
|
10
|
-
|
|
11
|
-
Design rules (mirrors :mod:`latticeai.core.tool_registry` and the workspace
|
|
12
|
-
skill registry):
|
|
13
|
-
|
|
14
|
-
* **No import-time I/O.** The registry only touches the filesystem when asked
|
|
15
|
-
to ``discover``.
|
|
16
|
-
* **No FastAPI, no globals.** Lifecycle state lives in the Workspace OS store
|
|
17
|
-
(passed in), so personal/organization scoping and the local-first JSON store
|
|
18
|
-
are reused, not duplicated.
|
|
19
|
-
* **Permissions are an allow-list.** A manifest may only request permissions in
|
|
20
|
-
:data:`PLUGIN_PERMISSIONS`; the execution boundary refuses anything a plugin
|
|
21
|
-
did not declare *and* was not granted.
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
from __future__ import annotations
|
|
25
|
-
|
|
26
|
-
import json
|
|
27
|
-
import re
|
|
28
|
-
from dataclasses import dataclass, field
|
|
29
|
-
from pathlib import Path
|
|
30
|
-
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
31
|
-
|
|
32
|
-
from latticeai.core.quiet import quiet
|
|
33
|
-
|
|
34
|
-
PLUGIN_SDK_VERSION = "2.2.0"
|
|
35
|
-
|
|
36
|
-
# Capability-style permissions a plugin can request. Kept deliberately small so
|
|
37
|
-
# the Enterprise seam can layer finer-grained policy on top without changing the
|
|
38
|
-
# community contract. Every permission maps to a concrete thing the execution
|
|
39
|
-
# boundary will or will not allow.
|
|
40
|
-
PLUGIN_PERMISSIONS = (
|
|
41
|
-
"read_workspace",
|
|
42
|
-
"write_workspace",
|
|
43
|
-
"read_graph",
|
|
44
|
-
"write_graph",
|
|
45
|
-
"run_tools",
|
|
46
|
-
"run_skills",
|
|
47
|
-
"run_workflows",
|
|
48
|
-
"run_agents",
|
|
49
|
-
"network",
|
|
50
|
-
"manage_memory",
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
# What a plugin may contribute to the platform. These extend existing systems.
|
|
54
|
-
PLUGIN_PROVIDES = ("skills", "tools", "workflows", "actions")
|
|
55
|
-
|
|
56
|
-
_SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+([.-][0-9A-Za-z.]+)?$")
|
|
57
|
-
_ID_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{1,63}$")
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
class PluginError(Exception):
|
|
61
|
-
"""Raised for plugin validation / lifecycle / execution failures."""
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _version_tuple(version: str) -> Tuple[int, int, int]:
|
|
65
|
-
parts = re.split(r"[.-]", str(version or "0"))
|
|
66
|
-
nums: List[int] = []
|
|
67
|
-
for part in parts[:3]:
|
|
68
|
-
try:
|
|
69
|
-
nums.append(int(part))
|
|
70
|
-
except ValueError:
|
|
71
|
-
nums.append(0)
|
|
72
|
-
while len(nums) < 3:
|
|
73
|
-
nums.append(0)
|
|
74
|
-
return nums[0], nums[1], nums[2]
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def is_compatible(required: str, current: str = PLUGIN_SDK_VERSION) -> bool:
|
|
78
|
-
"""True if a plugin requiring ``required`` runs on ``current`` host version.
|
|
79
|
-
|
|
80
|
-
``required`` is a minimum (major must match, host must be >= required).
|
|
81
|
-
Empty / missing requirement is treated as "any host".
|
|
82
|
-
"""
|
|
83
|
-
required = str(required or "").strip().lstrip(">=").strip()
|
|
84
|
-
if not required:
|
|
85
|
-
return True
|
|
86
|
-
req = _version_tuple(required)
|
|
87
|
-
cur = _version_tuple(current)
|
|
88
|
-
if req[0] != cur[0]:
|
|
89
|
-
return False
|
|
90
|
-
return cur >= req
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
@dataclass(frozen=True)
|
|
94
|
-
class PluginManifest:
|
|
95
|
-
"""A parsed, validated ``plugin.json``."""
|
|
96
|
-
|
|
97
|
-
id: str
|
|
98
|
-
name: str
|
|
99
|
-
version: str
|
|
100
|
-
description: str = ""
|
|
101
|
-
author: str = ""
|
|
102
|
-
lattice_version: str = ""
|
|
103
|
-
permissions: Tuple[str, ...] = ()
|
|
104
|
-
provides: Dict[str, List[str]] = field(default_factory=dict)
|
|
105
|
-
entrypoint: str = ""
|
|
106
|
-
homepage: str = ""
|
|
107
|
-
path: str = ""
|
|
108
|
-
|
|
109
|
-
def public(self) -> Dict[str, Any]:
|
|
110
|
-
return {
|
|
111
|
-
"id": self.id,
|
|
112
|
-
"name": self.name,
|
|
113
|
-
"version": self.version,
|
|
114
|
-
"description": self.description,
|
|
115
|
-
"author": self.author,
|
|
116
|
-
"lattice_version": self.lattice_version,
|
|
117
|
-
"permissions": list(self.permissions),
|
|
118
|
-
"provides": {key: list(value) for key, value in self.provides.items()},
|
|
119
|
-
"entrypoint": self.entrypoint,
|
|
120
|
-
"homepage": self.homepage,
|
|
121
|
-
"path": self.path,
|
|
122
|
-
"compatible": is_compatible(self.lattice_version),
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def validate_manifest(data: Any, *, path: str = "") -> Tuple[Optional[PluginManifest], List[str]]:
|
|
127
|
-
"""Validate a manifest dict. Returns ``(manifest_or_None, errors)``.
|
|
128
|
-
|
|
129
|
-
A manifest with errors still returns ``None`` so callers never accidentally
|
|
130
|
-
treat an invalid plugin as runnable.
|
|
131
|
-
"""
|
|
132
|
-
errors: List[str] = []
|
|
133
|
-
if not isinstance(data, dict):
|
|
134
|
-
return None, ["manifest is not a JSON object"]
|
|
135
|
-
|
|
136
|
-
plugin_id = str(data.get("id") or "").strip()
|
|
137
|
-
if not plugin_id:
|
|
138
|
-
errors.append("missing required field: id")
|
|
139
|
-
elif not _ID_RE.match(plugin_id):
|
|
140
|
-
errors.append("id must be lowercase alphanumeric with - or _ (2-64 chars)")
|
|
141
|
-
|
|
142
|
-
name = str(data.get("name") or plugin_id or "").strip()
|
|
143
|
-
if not name:
|
|
144
|
-
errors.append("missing required field: name")
|
|
145
|
-
|
|
146
|
-
version = str(data.get("version") or "").strip()
|
|
147
|
-
if not version:
|
|
148
|
-
errors.append("missing required field: version")
|
|
149
|
-
elif not _SEMVER_RE.match(version):
|
|
150
|
-
errors.append(f"version '{version}' is not a valid semantic version")
|
|
151
|
-
|
|
152
|
-
raw_perms = data.get("permissions") or []
|
|
153
|
-
if not isinstance(raw_perms, list):
|
|
154
|
-
errors.append("permissions must be a list")
|
|
155
|
-
raw_perms = []
|
|
156
|
-
perms: List[str] = []
|
|
157
|
-
for perm in raw_perms:
|
|
158
|
-
if perm not in PLUGIN_PERMISSIONS:
|
|
159
|
-
errors.append(f"unknown permission: {perm}")
|
|
160
|
-
else:
|
|
161
|
-
perms.append(perm)
|
|
162
|
-
|
|
163
|
-
raw_provides = data.get("provides") or {}
|
|
164
|
-
provides: Dict[str, List[str]] = {}
|
|
165
|
-
if not isinstance(raw_provides, dict):
|
|
166
|
-
errors.append("provides must be an object")
|
|
167
|
-
else:
|
|
168
|
-
for key, value in raw_provides.items():
|
|
169
|
-
if key not in PLUGIN_PROVIDES:
|
|
170
|
-
errors.append(f"unknown provides key: {key}")
|
|
171
|
-
continue
|
|
172
|
-
if not isinstance(value, list):
|
|
173
|
-
errors.append(f"provides.{key} must be a list")
|
|
174
|
-
continue
|
|
175
|
-
provides[key] = [str(item) for item in value]
|
|
176
|
-
|
|
177
|
-
lattice_version = str(data.get("lattice_version") or "").strip()
|
|
178
|
-
if lattice_version and not is_compatible(lattice_version):
|
|
179
|
-
errors.append(
|
|
180
|
-
f"requires Lattice {lattice_version} but host is {PLUGIN_SDK_VERSION}"
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
if errors:
|
|
184
|
-
return None, errors
|
|
185
|
-
|
|
186
|
-
manifest = PluginManifest(
|
|
187
|
-
id=plugin_id,
|
|
188
|
-
name=name,
|
|
189
|
-
version=version,
|
|
190
|
-
description=str(data.get("description") or ""),
|
|
191
|
-
author=str(data.get("author") or ""),
|
|
192
|
-
lattice_version=lattice_version,
|
|
193
|
-
permissions=tuple(perms),
|
|
194
|
-
provides=provides,
|
|
195
|
-
entrypoint=str(data.get("entrypoint") or ""),
|
|
196
|
-
homepage=str(data.get("homepage") or ""),
|
|
197
|
-
path=path,
|
|
198
|
-
)
|
|
199
|
-
return manifest, []
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
@dataclass
|
|
203
|
-
class PluginExecutionResult:
|
|
204
|
-
plugin_id: str
|
|
205
|
-
action: str
|
|
206
|
-
status: str # "ok" | "blocked" | "error" | "skipped"
|
|
207
|
-
output: Any = None
|
|
208
|
-
reason: str = ""
|
|
209
|
-
|
|
210
|
-
def as_dict(self) -> Dict[str, Any]:
|
|
211
|
-
return {
|
|
212
|
-
"plugin_id": self.plugin_id,
|
|
213
|
-
"action": self.action,
|
|
214
|
-
"status": self.status,
|
|
215
|
-
"output": self.output,
|
|
216
|
-
"reason": self.reason,
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
class PluginRegistry:
|
|
221
|
-
"""Discovery + validation + a permissioned execution boundary for plugins.
|
|
222
|
-
|
|
223
|
-
Lifecycle *state* (installed / enabled / validation status) is delegated to
|
|
224
|
-
the Workspace OS store via the small ``store`` port so plugins reuse the same
|
|
225
|
-
local-first JSON persistence, workspace scoping, and timeline events as
|
|
226
|
-
skills. The registry itself owns only manifest parsing and the execution
|
|
227
|
-
boundary.
|
|
228
|
-
"""
|
|
229
|
-
|
|
230
|
-
def __init__(self, plugins_dir: Path | str, *, store: Any = None):
|
|
231
|
-
self.plugins_dir = Path(plugins_dir).expanduser()
|
|
232
|
-
self.store = store
|
|
233
|
-
|
|
234
|
-
# ── discovery / validation ───────────────────────────────────────────
|
|
235
|
-
|
|
236
|
-
def discover(self) -> Dict[str, Any]:
|
|
237
|
-
"""Scan ``plugins_dir`` for ``<id>/plugin.json`` manifests."""
|
|
238
|
-
valid: List[PluginManifest] = []
|
|
239
|
-
invalid: List[Dict[str, Any]] = []
|
|
240
|
-
if self.plugins_dir.exists():
|
|
241
|
-
for entry in sorted(self.plugins_dir.iterdir()):
|
|
242
|
-
if not entry.is_dir():
|
|
243
|
-
continue
|
|
244
|
-
manifest_path = entry / "plugin.json"
|
|
245
|
-
if not manifest_path.exists():
|
|
246
|
-
continue
|
|
247
|
-
try:
|
|
248
|
-
data = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
249
|
-
except Exception as exc:
|
|
250
|
-
invalid.append({"path": str(entry), "errors": [f"invalid JSON: {exc}"]})
|
|
251
|
-
continue
|
|
252
|
-
manifest, errors = validate_manifest(data, path=str(entry))
|
|
253
|
-
if manifest is None:
|
|
254
|
-
invalid.append({"path": str(entry), "id": data.get("id"), "errors": errors})
|
|
255
|
-
else:
|
|
256
|
-
valid.append(manifest)
|
|
257
|
-
return {"valid": valid, "invalid": invalid}
|
|
258
|
-
|
|
259
|
-
def get_manifest(self, plugin_id: str) -> Optional[PluginManifest]:
|
|
260
|
-
for manifest in self.discover()["valid"]:
|
|
261
|
-
if manifest.id == plugin_id:
|
|
262
|
-
return manifest
|
|
263
|
-
return None
|
|
264
|
-
|
|
265
|
-
def catalog(self) -> Dict[str, Any]:
|
|
266
|
-
"""Merge discovered manifests with persisted lifecycle state for the UI."""
|
|
267
|
-
discovered = self.discover()
|
|
268
|
-
registry_state = self.store.list_plugin_registry() if self.store else {}
|
|
269
|
-
plugins = []
|
|
270
|
-
for manifest in discovered["valid"]:
|
|
271
|
-
state = registry_state.get(manifest.id, {})
|
|
272
|
-
public = manifest.public()
|
|
273
|
-
public.update({
|
|
274
|
-
"installed": bool(state.get("installed")),
|
|
275
|
-
"enabled": bool(state.get("enabled", state.get("installed"))),
|
|
276
|
-
"install_status": state.get("install_status") or ("ready" if state.get("installed") else "available"),
|
|
277
|
-
"validation_status": "valid",
|
|
278
|
-
"updated_at": state.get("updated_at"),
|
|
279
|
-
})
|
|
280
|
-
plugins.append(public)
|
|
281
|
-
return {
|
|
282
|
-
"sdk_version": PLUGIN_SDK_VERSION,
|
|
283
|
-
"permissions": list(PLUGIN_PERMISSIONS),
|
|
284
|
-
"provides": list(PLUGIN_PROVIDES),
|
|
285
|
-
"plugins": plugins,
|
|
286
|
-
"invalid": discovered["invalid"],
|
|
287
|
-
"plugins_dir": str(self.plugins_dir),
|
|
288
|
-
"total": len(plugins),
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
# ── lifecycle ─────────────────────────────────────────────────────────
|
|
292
|
-
|
|
293
|
-
def install(self, plugin_id: str, *, register_skill: Optional[Callable[[str, str], Any]] = None) -> Dict[str, Any]:
|
|
294
|
-
"""Install (activate) a discovered plugin and register the skills it bundles.
|
|
295
|
-
|
|
296
|
-
``register_skill(skill_name, plugin_id)`` is injected so plugins *extend*
|
|
297
|
-
the existing skill registry instead of owning a parallel one.
|
|
298
|
-
"""
|
|
299
|
-
manifest = self.get_manifest(plugin_id)
|
|
300
|
-
if manifest is None:
|
|
301
|
-
raise PluginError(f"plugin not found or invalid: {plugin_id}")
|
|
302
|
-
if not is_compatible(manifest.lattice_version):
|
|
303
|
-
raise PluginError(
|
|
304
|
-
f"plugin '{plugin_id}' requires Lattice {manifest.lattice_version}, host is {PLUGIN_SDK_VERSION}"
|
|
305
|
-
)
|
|
306
|
-
registered_skills = []
|
|
307
|
-
if register_skill is not None:
|
|
308
|
-
for skill_name in manifest.provides.get("skills", []):
|
|
309
|
-
try:
|
|
310
|
-
register_skill(skill_name, plugin_id)
|
|
311
|
-
registered_skills.append(skill_name)
|
|
312
|
-
except Exception: # pragma: no cover - skill registration is best-effort
|
|
313
|
-
quiet()
|
|
314
|
-
entry = {}
|
|
315
|
-
if self.store is not None:
|
|
316
|
-
entry = self.store.mark_plugin_installed(
|
|
317
|
-
plugin_id,
|
|
318
|
-
version=manifest.version,
|
|
319
|
-
metadata={
|
|
320
|
-
"permissions": list(manifest.permissions),
|
|
321
|
-
"provides": {k: list(v) for k, v in manifest.provides.items()},
|
|
322
|
-
"registered_skills": registered_skills,
|
|
323
|
-
},
|
|
324
|
-
)
|
|
325
|
-
return {"plugin": manifest.public(), "registry": entry, "registered_skills": registered_skills}
|
|
326
|
-
|
|
327
|
-
def uninstall(self, plugin_id: str) -> Dict[str, Any]:
|
|
328
|
-
if self.store is None:
|
|
329
|
-
return {"status": "ok", "plugin_id": plugin_id}
|
|
330
|
-
return self.store.mark_plugin_uninstalled(plugin_id)
|
|
331
|
-
|
|
332
|
-
def set_enabled(self, plugin_id: str, enabled: bool) -> Dict[str, Any]:
|
|
333
|
-
if self.store is None:
|
|
334
|
-
return {"plugin_id": plugin_id, "enabled": enabled}
|
|
335
|
-
return self.store.set_plugin_enabled(plugin_id, enabled)
|
|
336
|
-
|
|
337
|
-
# ── execution boundary ────────────────────────────────────────────────
|
|
338
|
-
|
|
339
|
-
def _granted_permissions(self, plugin_id: str) -> List[str]:
|
|
340
|
-
if self.store is None:
|
|
341
|
-
return []
|
|
342
|
-
state = self.store.list_plugin_registry().get(plugin_id, {})
|
|
343
|
-
return list((state.get("metadata") or {}).get("permissions") or [])
|
|
344
|
-
|
|
345
|
-
def execute_action(
|
|
346
|
-
self,
|
|
347
|
-
plugin_id: str,
|
|
348
|
-
action: str,
|
|
349
|
-
args: Optional[Dict[str, Any]] = None,
|
|
350
|
-
*,
|
|
351
|
-
runners: Optional[Dict[str, Callable[..., Any]]] = None,
|
|
352
|
-
workspace_id: Optional[str] = None,
|
|
353
|
-
) -> PluginExecutionResult:
|
|
354
|
-
"""Run a plugin-provided action through the permission boundary.
|
|
355
|
-
|
|
356
|
-
``runners`` maps a capability ("tools" / "skills" / "workflows" /
|
|
357
|
-
"agents") to a callable the host injects. The boundary refuses any
|
|
358
|
-
capability the plugin did not *declare in its manifest*; without a
|
|
359
|
-
matching runner the action is reported ``skipped`` (never crashes the
|
|
360
|
-
caller). This keeps plugins safe-by-default.
|
|
361
|
-
"""
|
|
362
|
-
args = args or {}
|
|
363
|
-
runners = runners or {}
|
|
364
|
-
|
|
365
|
-
def emit(event_type: str, payload: Dict[str, Any]) -> None:
|
|
366
|
-
if self.store is not None and hasattr(self.store, "record_timeline_event"):
|
|
367
|
-
try:
|
|
368
|
-
self.store.record_timeline_event("plugins", event_type, payload, workspace_id=workspace_id)
|
|
369
|
-
except Exception:
|
|
370
|
-
quiet()
|
|
371
|
-
|
|
372
|
-
emit("plugin_started", {"plugin_id": plugin_id, "action": action})
|
|
373
|
-
|
|
374
|
-
def finish(result: PluginExecutionResult) -> PluginExecutionResult:
|
|
375
|
-
event_type = "plugin_completed" if result.status in {"ok", "skipped"} else "execution_failed"
|
|
376
|
-
emit(event_type, {"plugin_id": plugin_id, "action": action, "status": result.status, "reason": result.reason})
|
|
377
|
-
return result
|
|
378
|
-
|
|
379
|
-
manifest = self.get_manifest(plugin_id)
|
|
380
|
-
if manifest is None:
|
|
381
|
-
return finish(PluginExecutionResult(plugin_id, action, "error", reason="plugin not found or invalid"))
|
|
382
|
-
|
|
383
|
-
registry_state = self.store.list_plugin_registry().get(plugin_id, {}) if self.store else {}
|
|
384
|
-
if self.store is not None and not registry_state.get("enabled", registry_state.get("installed")):
|
|
385
|
-
return finish(PluginExecutionResult(plugin_id, action, "blocked", reason="plugin is not enabled"))
|
|
386
|
-
|
|
387
|
-
# Map an action to the capability + permission it needs.
|
|
388
|
-
capability_for: Dict[str, Tuple[str, str]] = {
|
|
389
|
-
"run_tool": ("tools", "run_tools"),
|
|
390
|
-
"run_skill": ("skills", "run_skills"),
|
|
391
|
-
"run_workflow": ("workflows", "run_workflows"),
|
|
392
|
-
"run_agent": ("agents", "run_agents"),
|
|
393
|
-
}
|
|
394
|
-
capability, permission = capability_for.get(action, ("actions", ""))
|
|
395
|
-
|
|
396
|
-
if permission and permission not in manifest.permissions:
|
|
397
|
-
return finish(PluginExecutionResult(
|
|
398
|
-
plugin_id, action, "blocked",
|
|
399
|
-
reason=f"plugin did not declare required permission '{permission}'",
|
|
400
|
-
))
|
|
401
|
-
if permission and self.store is not None and permission not in self._granted_permissions(plugin_id):
|
|
402
|
-
return finish(PluginExecutionResult(
|
|
403
|
-
plugin_id, action, "blocked",
|
|
404
|
-
reason=f"permission '{permission}' not granted at install time",
|
|
405
|
-
))
|
|
406
|
-
|
|
407
|
-
if capability == "tools":
|
|
408
|
-
tool_name = str(args.get("tool") or "").strip()
|
|
409
|
-
declared_tools = {
|
|
410
|
-
str(item).strip()
|
|
411
|
-
for item in (manifest.provides.get("tools") or [])
|
|
412
|
-
if str(item).strip()
|
|
413
|
-
}
|
|
414
|
-
if not tool_name or tool_name not in declared_tools:
|
|
415
|
-
return finish(PluginExecutionResult(
|
|
416
|
-
plugin_id,
|
|
417
|
-
action,
|
|
418
|
-
"blocked",
|
|
419
|
-
reason=f"tool '{tool_name or '<missing>'}' is not declared in provides.tools",
|
|
420
|
-
))
|
|
421
|
-
|
|
422
|
-
runner = runners.get(capability)
|
|
423
|
-
if runner is None:
|
|
424
|
-
return finish(PluginExecutionResult(
|
|
425
|
-
plugin_id, action, "skipped",
|
|
426
|
-
reason=f"no host runner for capability '{capability}'",
|
|
427
|
-
))
|
|
428
|
-
try:
|
|
429
|
-
output = runner(plugin_id=plugin_id, action=action, args=args, manifest=manifest)
|
|
430
|
-
return finish(PluginExecutionResult(plugin_id, action, "ok", output=output))
|
|
431
|
-
except Exception as exc:
|
|
432
|
-
return finish(PluginExecutionResult(plugin_id, action, "error", reason=str(exc)))
|