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.
Files changed (439) hide show
  1. package/README.md +91 -148
  2. package/bin/ltcai.js +234 -24
  3. package/docs/CHANGELOG.md +75 -0
  4. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  5. package/docs/DEVELOPMENT.md +8 -6
  6. package/docs/ENTERPRISE.md +2 -1
  7. package/docs/MULTI_AGENT_RUNTIME.md +12 -5
  8. package/docs/ONBOARDING.md +1 -1
  9. package/docs/OPERATIONS.md +6 -3
  10. package/docs/REALTIME_COLLABORATION.md +1 -1
  11. package/docs/TRUST_MODEL.md +1 -1
  12. package/docs/WHY_LATTICE.md +1 -1
  13. package/docs/WORKFLOW_DESIGNER.md +3 -2
  14. package/docs/kg-schema.md +1 -1
  15. package/docs/v11.6.0_ONE_DOOR_PLAN.md +170 -0
  16. package/lattice_brain/__init__.py +42 -79
  17. package/lattice_brain/graph/__init__.py +9 -24
  18. package/lattice_brain/graph/_kg_common/__init__.py +13 -24
  19. package/lattice_brain/ingestion/__init__.py +19 -58
  20. package/lattice_brain/ingestion/pipeline.py +20 -398
  21. package/lattice_brain/multimodal/__init__.py +7 -18
  22. package/lattice_brain/multimodal/images.py +6 -264
  23. package/lattice_brain/multimodal/video.py +8 -247
  24. package/lattice_brain/runtime/__init__.py +8 -79
  25. package/lattice_brain/runtime/hooks.py +22 -584
  26. package/latticeai/__init__.py +1 -1
  27. package/latticeai/api/agent_worker_seam.py +7 -80
  28. package/latticeai/api/health.py +6 -20
  29. package/latticeai/api/local_files.py +16 -613
  30. package/latticeai/api/models.py +12 -87
  31. package/latticeai/api/search.py +17 -255
  32. package/latticeai/api/tools.py +69 -750
  33. package/latticeai/api/voice_capture.py +8 -70
  34. package/latticeai/api/worker_compute.py +842 -0
  35. package/latticeai/api/worker_seams.py +216 -0
  36. package/latticeai/app_factory.py +23 -232
  37. package/latticeai/cli/entrypoint.py +36 -249
  38. package/latticeai/core/agent_permission.py +16 -91
  39. package/latticeai/core/messages.py +89 -531
  40. package/latticeai/runtime/access_runtime.py +0 -14
  41. package/latticeai/runtime/bootstrap.py +10 -21
  42. package/latticeai/runtime/brain_runtime.py +35 -43
  43. package/latticeai/runtime/build_phases/__init__.py +21 -37
  44. package/latticeai/runtime/build_phases/features.py +99 -389
  45. package/latticeai/runtime/build_phases/foundation.py +82 -392
  46. package/latticeai/runtime/build_phases/web.py +91 -408
  47. package/latticeai/runtime/build_phases/worker_profile.py +244 -0
  48. package/latticeai/runtime/lifespan_runtime.py +7 -16
  49. package/latticeai/runtime/platform_services_runtime.py +1 -15
  50. package/latticeai/runtime/runtime_context.py +13 -130
  51. package/latticeai/runtime/security_runtime.py +10 -103
  52. package/latticeai/services/architecture_readiness.py +82 -43
  53. package/latticeai/services/model_runtime/__init__.py +2 -11
  54. package/latticeai/services/model_runtime/service.py +4 -28
  55. package/latticeai/services/p_reinforce.py +10 -261
  56. package/latticeai/services/product_readiness.py +31 -38
  57. package/latticeai/services/search_service.py +37 -795
  58. package/latticeai/services/tool_dispatch.py +30 -386
  59. package/latticeai/services/voice_capture.py +13 -107
  60. package/latticeai/tools/__init__.py +22 -49
  61. package/latticeai/tools/commands.py +0 -163
  62. package/latticeai/tools/computer.py +0 -39
  63. package/latticeai/tools/documents.py +1 -134
  64. package/latticeai/tools/filesystem.py +1 -247
  65. package/latticeai/tools/knowledge.py +1 -52
  66. package/latticeai/tools/local_files.py +0 -20
  67. package/latticeai/worker_app.py +75 -0
  68. package/package.json +2 -3
  69. package/requirements.txt +0 -5
  70. package/scripts/agent_eval.py +16 -28
  71. package/scripts/brain_quality_eval.py +20 -183
  72. package/scripts/bump_version.py +5 -5
  73. package/scripts/check_current_release_docs.mjs +7 -4
  74. package/scripts/check_openapi_drift.mjs +10 -0
  75. package/scripts/check_server_i18n.mjs +2 -18
  76. package/scripts/compose_openapi.py +377 -0
  77. package/scripts/export_openapi.py +32 -4
  78. package/scripts/gen_messages_catalog_fixture.py +302 -0
  79. package/scripts/gen_openapi_fragments.py +365 -0
  80. package/scripts/gen_redact_fixture.py +196 -0
  81. package/scripts/gen_worker_allowlist_fixture.py +115 -0
  82. package/scripts/generate_agent_parity_fixtures.py +33 -14
  83. package/scripts/openapi_route_families.json +2161 -0
  84. package/scripts/release_screen_claims.json +21 -0
  85. package/scripts/run_integration_tests.mjs +134 -29
  86. package/scripts/run_sidecar_e2e.mjs +104 -11
  87. package/scripts/wheel_smoke.py +32 -22
  88. package/src-tauri/Cargo.lock +375 -9
  89. package/src-tauri/Cargo.toml +1 -1
  90. package/src-tauri/src/backend.rs +151 -45
  91. package/src-tauri/src/main.rs +18 -18
  92. package/src-tauri/src/topology.rs +58 -88
  93. package/src-tauri/tauri.conf.json +1 -2
  94. package/static/app/asset-manifest.json +41 -41
  95. package/static/app/assets/{Act-DcQizkl1.js → Act-CToZnOHz.js} +1 -1
  96. package/static/app/assets/{AdminConsole-cf4npybT.js → AdminConsole-D5kbrBu8.js} +1 -1
  97. package/static/app/assets/{Brain-3VCSHFcn.js → Brain-C6zCdv4S.js} +1 -1
  98. package/static/app/assets/{BrainHome-Qm8eaztx.js → BrainHome-D4n3EDUt.js} +1 -1
  99. package/static/app/assets/{BrainSignals-DS9BtKOW.js → BrainSignals-CzeZ-vQ9.js} +1 -1
  100. package/static/app/assets/{Capture-DiQ219jW.js → Capture-DPcyTczq.js} +1 -1
  101. package/static/app/assets/{Chronicle-BGvuAchH.js → Chronicle-D3Nx-oWS.js} +1 -1
  102. package/static/app/assets/{CommandPalette-Bqhm0Urn.js → CommandPalette-DPPvpp28.js} +1 -1
  103. package/static/app/assets/{Library-BV6NnF0a.js → Library-Co2qTu3I.js} +1 -1
  104. package/static/app/assets/{LivingBrain-GzenJchP.js → LivingBrain-Bsmzwy8_.js} +1 -1
  105. package/static/app/assets/{ProductFlow-DEP6-vML.js → ProductFlow-CBLdmesT.js} +1 -1
  106. package/static/app/assets/{ReviewCard-CNZ7XjWG.js → ReviewCard-CfR6DLs7.js} +1 -1
  107. package/static/app/assets/{System-CieofHQa.js → System-Ce-P2YL6.js} +1 -1
  108. package/static/app/assets/arrow-left-DD5jFGHV.js +1 -0
  109. package/static/app/assets/{bot-B_K1Tdmw.js → bot-C5-XIww2.js} +1 -1
  110. package/static/app/assets/{brain-DWyaV1L1.js → brain-CFRJUvdX.js} +1 -1
  111. package/static/app/assets/{button-aTn4s84A.js → button-DI1KcQP-.js} +1 -1
  112. package/static/app/assets/circle-check-CY20XMCh.js +1 -0
  113. package/static/app/assets/{circle-pause-xKgeGXkT.js → circle-pause-C8HHyood.js} +1 -1
  114. package/static/app/assets/{circle-play-DkT6tYPX.js → circle-play-Cufc235E.js} +1 -1
  115. package/static/app/assets/{cpu-85xYObUC.js → cpu-DW2_JVsv.js} +1 -1
  116. package/static/app/assets/{download-B5Fm7YXo.js → download-gbdKTBb8.js} +1 -1
  117. package/static/app/assets/{folder-open-kk2Xa52u.js → folder-open-C9ydyX33.js} +1 -1
  118. package/static/app/assets/{hard-drive-DkA3zBW_.js → hard-drive-BxNMzBM3.js} +1 -1
  119. package/static/app/assets/index-BovSWRmQ.css +2 -0
  120. package/static/app/assets/{index-BMPdTmlY.js → index-q9XyDY7A.js} +3 -3
  121. package/static/app/assets/{input-B0nRf2jO.js → input-GLD94Y_K.js} +1 -1
  122. package/static/app/assets/{link-2-Dwb4gnTc.js → link-2-B4Y0gz1S.js} +1 -1
  123. package/static/app/assets/{permissionCopy-CQDUBrOZ.js → permissionCopy-CxrV0fvU.js} +1 -1
  124. package/static/app/assets/{primitives-SNp0LRJz.js → primitives-2ANdAsu2.js} +1 -1
  125. package/static/app/assets/search-DTxtjQ07.js +1 -0
  126. package/static/app/assets/{share-2-BsrxFglO.js → share-2-DxArfiuI.js} +1 -1
  127. package/static/app/assets/{shield-alert-5BStfp2_.js → shield-alert-CgftuFFs.js} +1 -1
  128. package/static/app/assets/{textarea-Cg8IUA-k.js → textarea-BHVBAoCr.js} +1 -1
  129. package/static/app/assets/{useFocusTrap-CYKvE46M.js → useFocusTrap-Dzq2j_OG.js} +1 -1
  130. package/static/app/assets/{useMutation-CSn9t1op.js → useMutation-BcqfMLNq.js} +1 -1
  131. package/static/app/assets/{useQuery-CY2OI2uy.js → useQuery-DBzLKCOv.js} +1 -1
  132. package/static/app/assets/{utils-Ddol2RWD.js → utils-B-Aah1bO.js} +1 -1
  133. package/static/app/assets/{workspace-BqDwOz_p.js → workspace-B6fFhoko.js} +1 -1
  134. package/static/app/index.html +4 -4
  135. package/static/sw.js +1 -1
  136. package/lattice_brain/archive.py +0 -522
  137. package/lattice_brain/context.py +0 -325
  138. package/lattice_brain/conversations.py +0 -382
  139. package/lattice_brain/core.py +0 -82
  140. package/lattice_brain/graph/_kg_contract.py +0 -249
  141. package/lattice_brain/graph/curator.py +0 -676
  142. package/lattice_brain/graph/discovery.py +0 -595
  143. package/lattice_brain/graph/discovery_index/__init__.py +0 -35
  144. package/lattice_brain/graph/discovery_index/cleanup.py +0 -182
  145. package/lattice_brain/graph/discovery_index/extract.py +0 -137
  146. package/lattice_brain/graph/discovery_index/scan.py +0 -411
  147. package/lattice_brain/graph/discovery_index/upsert.py +0 -495
  148. package/lattice_brain/graph/documents.py +0 -380
  149. package/lattice_brain/graph/fusion.py +0 -395
  150. package/lattice_brain/graph/identity.py +0 -175
  151. package/lattice_brain/graph/image_vectors.py +0 -230
  152. package/lattice_brain/graph/ingest.py +0 -829
  153. package/lattice_brain/graph/network.py +0 -205
  154. package/lattice_brain/graph/proactive.py +0 -724
  155. package/lattice_brain/graph/projection/__init__.py +0 -42
  156. package/lattice_brain/graph/projection/curation.py +0 -500
  157. package/lattice_brain/graph/projection/v2_schema.py +0 -518
  158. package/lattice_brain/graph/provenance.py +0 -524
  159. package/lattice_brain/graph/rerank.py +0 -163
  160. package/lattice_brain/graph/retrieval/__init__.py +0 -54
  161. package/lattice_brain/graph/retrieval/context.py +0 -197
  162. package/lattice_brain/graph/retrieval/graph_view.py +0 -319
  163. package/lattice_brain/graph/retrieval/hybrid.py +0 -488
  164. package/lattice_brain/graph/retrieval/maintenance.py +0 -121
  165. package/lattice_brain/graph/retrieval/signals.py +0 -95
  166. package/lattice_brain/graph/retrieval_docgen.py +0 -253
  167. package/lattice_brain/graph/retrieval_policy.py +0 -180
  168. package/lattice_brain/graph/retrieval_reads.py +0 -769
  169. package/lattice_brain/graph/retrieval_vector/__init__.py +0 -42
  170. package/lattice_brain/graph/retrieval_vector/fingerprint.py +0 -97
  171. package/lattice_brain/graph/retrieval_vector/indexing.py +0 -347
  172. package/lattice_brain/graph/retrieval_vector/search.py +0 -560
  173. package/lattice_brain/graph/retrieval_vector/status.py +0 -374
  174. package/lattice_brain/graph/schema.py +0 -792
  175. package/lattice_brain/graph/store.py +0 -268
  176. package/lattice_brain/graph/vector_index/__init__.py +0 -85
  177. package/lattice_brain/graph/vector_index/base.py +0 -167
  178. package/lattice_brain/graph/vector_index/brute_force.py +0 -110
  179. package/lattice_brain/graph/vector_index/hnsw.py +0 -290
  180. package/lattice_brain/graph/vector_index/jobs.py +0 -287
  181. package/lattice_brain/graph/vector_index/quantized.py +0 -148
  182. package/lattice_brain/graph/vector_index/selector.py +0 -161
  183. package/lattice_brain/graph/write_master.py +0 -308
  184. package/lattice_brain/ingestion/_contract.py +0 -90
  185. package/lattice_brain/ingestion/folder_scan.py +0 -57
  186. package/lattice_brain/ingestion/folders.py +0 -258
  187. package/lattice_brain/ingestion/jobs_api.py +0 -107
  188. package/lattice_brain/ingestion/routing.py +0 -295
  189. package/lattice_brain/ingestion_jobs.py +0 -380
  190. package/lattice_brain/memory.py +0 -75
  191. package/lattice_brain/portability/__init__.py +0 -90
  192. package/lattice_brain/portability/_contract.py +0 -42
  193. package/lattice_brain/portability/backups.py +0 -338
  194. package/lattice_brain/portability/bundles.py +0 -136
  195. package/lattice_brain/portability/constants.py +0 -93
  196. package/lattice_brain/portability/fsops.py +0 -138
  197. package/lattice_brain/portability/service.py +0 -41
  198. package/lattice_brain/portability/sharing.py +0 -710
  199. package/lattice_brain/quality.py +0 -543
  200. package/lattice_brain/retrieval_benchmark_fixtures.py +0 -92
  201. package/lattice_brain/runtime/agent_runtime.py +0 -859
  202. package/lattice_brain/runtime/contracts.py +0 -460
  203. package/lattice_brain/runtime/multi_agent.py +0 -942
  204. package/lattice_brain/runtime/statuses.py +0 -10
  205. package/lattice_brain/sealed_box.py +0 -240
  206. package/lattice_brain/self_model.py +0 -675
  207. package/lattice_brain/sensitivity.py +0 -94
  208. package/lattice_brain/storage/__init__.py +0 -22
  209. package/lattice_brain/storage/base.py +0 -100
  210. package/lattice_brain/storage/docker.py +0 -105
  211. package/lattice_brain/storage/factory.py +0 -31
  212. package/lattice_brain/storage/migration.py +0 -191
  213. package/lattice_brain/storage/postgres.py +0 -123
  214. package/lattice_brain/storage/sqlite.py +0 -143
  215. package/lattice_brain/synthesis.py +0 -824
  216. package/lattice_brain/workflow.py +0 -497
  217. package/latticeai/api/admin.py +0 -471
  218. package/latticeai/api/agent_registry.py +0 -105
  219. package/latticeai/api/agents.py +0 -228
  220. package/latticeai/api/auth.py +0 -383
  221. package/latticeai/api/automation_intelligence.py +0 -401
  222. package/latticeai/api/brain_intelligence.py +0 -199
  223. package/latticeai/api/browser.py +0 -493
  224. package/latticeai/api/change_proposals.py +0 -89
  225. package/latticeai/api/chat.py +0 -572
  226. package/latticeai/api/chat_agent_http.py +0 -892
  227. package/latticeai/api/chat_contracts.py +0 -73
  228. package/latticeai/api/chat_documents.py +0 -276
  229. package/latticeai/api/chat_helpers.py +0 -460
  230. package/latticeai/api/chat_history.py +0 -101
  231. package/latticeai/api/chat_hybrid.py +0 -113
  232. package/latticeai/api/chat_intents.py +0 -646
  233. package/latticeai/api/chat_stream.py +0 -216
  234. package/latticeai/api/chronicle.py +0 -63
  235. package/latticeai/api/command_center.py +0 -51
  236. package/latticeai/api/computer_use.py +0 -474
  237. package/latticeai/api/evidence_actions.py +0 -48
  238. package/latticeai/api/features.py +0 -70
  239. package/latticeai/api/funnel_metrics.py +0 -31
  240. package/latticeai/api/garden.py +0 -34
  241. package/latticeai/api/hooks.py +0 -165
  242. package/latticeai/api/index_jobs.py +0 -145
  243. package/latticeai/api/invitations.py +0 -100
  244. package/latticeai/api/knowledge_graph.py +0 -536
  245. package/latticeai/api/marketplace.py +0 -105
  246. package/latticeai/api/mcp.py +0 -482
  247. package/latticeai/api/memory.py +0 -270
  248. package/latticeai/api/network.py +0 -81
  249. package/latticeai/api/network_boundary.py +0 -225
  250. package/latticeai/api/permission_mode.py +0 -61
  251. package/latticeai/api/permissions.py +0 -436
  252. package/latticeai/api/plugins.py +0 -126
  253. package/latticeai/api/portability.py +0 -391
  254. package/latticeai/api/project_sessions.py +0 -114
  255. package/latticeai/api/realtime.py +0 -118
  256. package/latticeai/api/review_queue.py +0 -364
  257. package/latticeai/api/security_dashboard.py +0 -604
  258. package/latticeai/api/setup.py +0 -319
  259. package/latticeai/api/static_routes.py +0 -354
  260. package/latticeai/api/ui_redirects.py +0 -26
  261. package/latticeai/api/workflow_designer.py +0 -394
  262. package/latticeai/api/workspace.py +0 -856
  263. package/latticeai/api/workspace_scope.py +0 -125
  264. package/latticeai/core/agent/__init__.py +0 -93
  265. package/latticeai/core/agent/_contract.py +0 -79
  266. package/latticeai/core/agent/context.py +0 -57
  267. package/latticeai/core/agent/deps.py +0 -125
  268. package/latticeai/core/agent/execution.py +0 -622
  269. package/latticeai/core/agent/planning.py +0 -145
  270. package/latticeai/core/agent/recovery.py +0 -157
  271. package/latticeai/core/agent/runtime.py +0 -210
  272. package/latticeai/core/agent/verification.py +0 -231
  273. package/latticeai/core/agent_eval.py +0 -739
  274. package/latticeai/core/agent_helpers.py +0 -493
  275. package/latticeai/core/agent_profiles.py +0 -110
  276. package/latticeai/core/agent_prompts.py +0 -171
  277. package/latticeai/core/agent_registry.py +0 -232
  278. package/latticeai/core/agent_state.py +0 -41
  279. package/latticeai/core/agent_trace.py +0 -104
  280. package/latticeai/core/artifact_ledger.py +0 -109
  281. package/latticeai/core/audit.py +0 -260
  282. package/latticeai/core/builtin_hooks.py +0 -105
  283. package/latticeai/core/context_builder.py +0 -394
  284. package/latticeai/core/document_generator.py +0 -103
  285. package/latticeai/core/enterprise.py +0 -154
  286. package/latticeai/core/enterprise_admin.py +0 -158
  287. package/latticeai/core/file_generation/__init__.py +0 -115
  288. package/latticeai/core/file_generation/bundles.py +0 -76
  289. package/latticeai/core/file_generation/extraction.py +0 -154
  290. package/latticeai/core/file_generation/inference.py +0 -235
  291. package/latticeai/core/file_generation/orchestration.py +0 -152
  292. package/latticeai/core/file_generation/prompting.py +0 -117
  293. package/latticeai/core/file_generation/repair.py +0 -114
  294. package/latticeai/core/file_generation/sanitize.py +0 -61
  295. package/latticeai/core/file_generation/validation.py +0 -201
  296. package/latticeai/core/invitations.py +0 -132
  297. package/latticeai/core/legacy_compatibility.py +0 -243
  298. package/latticeai/core/logging_safety.py +0 -46
  299. package/latticeai/core/marketplace.py +0 -293
  300. package/latticeai/core/mcp_catalog.py +0 -452
  301. package/latticeai/core/mcp_registry.py +0 -506
  302. package/latticeai/core/network_boundary.py +0 -168
  303. package/latticeai/core/oidc.py +0 -208
  304. package/latticeai/core/plugins.py +0 -432
  305. package/latticeai/core/product_hardening.py +0 -218
  306. package/latticeai/core/project_sessions.py +0 -337
  307. package/latticeai/core/realtime.py +0 -238
  308. package/latticeai/core/run_explain.py +0 -426
  309. package/latticeai/core/run_store.py +0 -252
  310. package/latticeai/core/timezones.py +0 -80
  311. package/latticeai/core/workspace_computer_memory.py +0 -84
  312. package/latticeai/core/workspace_graph_trace.py +0 -155
  313. package/latticeai/core/workspace_indexing.py +0 -102
  314. package/latticeai/core/workspace_memory.py +0 -77
  315. package/latticeai/core/workspace_onboarding.py +0 -104
  316. package/latticeai/core/workspace_os.py +0 -978
  317. package/latticeai/core/workspace_os_constants.py +0 -126
  318. package/latticeai/core/workspace_os_state.py +0 -180
  319. package/latticeai/core/workspace_os_utils.py +0 -103
  320. package/latticeai/core/workspace_permissions.py +0 -101
  321. package/latticeai/core/workspace_plugins.py +0 -97
  322. package/latticeai/core/workspace_relationships.py +0 -99
  323. package/latticeai/core/workspace_reorganization.py +0 -335
  324. package/latticeai/core/workspace_review_items.py +0 -112
  325. package/latticeai/core/workspace_runs.py +0 -726
  326. package/latticeai/core/workspace_skills.py +0 -109
  327. package/latticeai/core/workspace_snapshots.py +0 -198
  328. package/latticeai/core/workspace_timeline.py +0 -110
  329. package/latticeai/integrations/__init__.py +0 -0
  330. package/latticeai/integrations/telegram_bot/__init__.py +0 -123
  331. package/latticeai/integrations/telegram_bot/__main__.py +0 -17
  332. package/latticeai/integrations/telegram_bot/config.py +0 -86
  333. package/latticeai/integrations/telegram_bot/dispatch.py +0 -311
  334. package/latticeai/integrations/telegram_bot/flows.py +0 -478
  335. package/latticeai/integrations/telegram_bot/helpers.py +0 -322
  336. package/latticeai/integrations/telegram_bot/screens.py +0 -394
  337. package/latticeai/runtime/audit_runtime.py +0 -76
  338. package/latticeai/runtime/automation_runtime.py +0 -81
  339. package/latticeai/runtime/chat_wiring.py +0 -141
  340. package/latticeai/runtime/context_runtime.py +0 -66
  341. package/latticeai/runtime/feature_toggle_wiring.py +0 -157
  342. package/latticeai/runtime/history_runtime.py +0 -163
  343. package/latticeai/runtime/history_writer.py +0 -138
  344. package/latticeai/runtime/hooks_runtime.py +0 -77
  345. package/latticeai/runtime/model_wiring.py +0 -68
  346. package/latticeai/runtime/namespace_runtime.py +0 -163
  347. package/latticeai/runtime/network_boundary_wiring.py +0 -117
  348. package/latticeai/runtime/network_config_runtime.py +0 -56
  349. package/latticeai/runtime/permission_mode_wiring.py +0 -112
  350. package/latticeai/runtime/persistence_runtime.py +0 -159
  351. package/latticeai/runtime/platform_runtime_wiring.py +0 -89
  352. package/latticeai/runtime/review_wiring.py +0 -42
  353. package/latticeai/runtime/router_registration.py +0 -693
  354. package/latticeai/runtime/service_singletons.py +0 -55
  355. package/latticeai/runtime/sso_config_runtime.py +0 -128
  356. package/latticeai/runtime/user_key_runtime.py +0 -106
  357. package/latticeai/runtime/web_runtime.py +0 -92
  358. package/latticeai/server_app.py +0 -51
  359. package/latticeai/services/app_context.py +0 -130
  360. package/latticeai/services/automation_execution.py +0 -266
  361. package/latticeai/services/automation_intelligence.py +0 -614
  362. package/latticeai/services/brain_automation.py +0 -191
  363. package/latticeai/services/brain_intelligence/__init__.py +0 -58
  364. package/latticeai/services/brain_intelligence/_contract.py +0 -71
  365. package/latticeai/services/brain_intelligence/consistency.py +0 -193
  366. package/latticeai/services/brain_intelligence/constants.py +0 -47
  367. package/latticeai/services/brain_intelligence/digest.py +0 -258
  368. package/latticeai/services/brain_intelligence/health.py +0 -331
  369. package/latticeai/services/brain_intelligence/proposals.py +0 -259
  370. package/latticeai/services/brain_intelligence/sampling.py +0 -84
  371. package/latticeai/services/brain_intelligence/service.py +0 -48
  372. package/latticeai/services/change_proposals.py +0 -471
  373. package/latticeai/services/chat_service.py +0 -243
  374. package/latticeai/services/chronicle.py +0 -555
  375. package/latticeai/services/cloud_egress_audit.py +0 -85
  376. package/latticeai/services/cloud_extraction.py +0 -129
  377. package/latticeai/services/cloud_streaming.py +0 -268
  378. package/latticeai/services/cloud_token_guard.py +0 -84
  379. package/latticeai/services/command_center.py +0 -548
  380. package/latticeai/services/evidence_actions.py +0 -258
  381. package/latticeai/services/feature_toggles.py +0 -502
  382. package/latticeai/services/folder_watch.py +0 -520
  383. package/latticeai/services/funnel_metrics.py +0 -307
  384. package/latticeai/services/hybrid_chat.py +0 -316
  385. package/latticeai/services/hybrid_context.py +0 -228
  386. package/latticeai/services/hybrid_policy.py +0 -129
  387. package/latticeai/services/interop_bridges.py +0 -978
  388. package/latticeai/services/local_knowledge.py +0 -465
  389. package/latticeai/services/memory_service/__init__.py +0 -52
  390. package/latticeai/services/memory_service/_contract.py +0 -100
  391. package/latticeai/services/memory_service/brief.py +0 -431
  392. package/latticeai/services/memory_service/constants.py +0 -57
  393. package/latticeai/services/memory_service/maintenance.py +0 -138
  394. package/latticeai/services/memory_service/manager.py +0 -186
  395. package/latticeai/services/memory_service/proof.py +0 -136
  396. package/latticeai/services/memory_service/recall.py +0 -225
  397. package/latticeai/services/memory_service/service.py +0 -48
  398. package/latticeai/services/memory_service/stores.py +0 -110
  399. package/latticeai/services/mode_store.py +0 -132
  400. package/latticeai/services/model_recommendation.py +0 -224
  401. package/latticeai/services/model_runtime/cloud.py +0 -87
  402. package/latticeai/services/network_boundary_service.py +0 -117
  403. package/latticeai/services/obsidian_bridge.py +0 -609
  404. package/latticeai/services/openai_compatible_adapter.py +0 -101
  405. package/latticeai/services/permission_mode_service.py +0 -122
  406. package/latticeai/services/platform_runtime.py +0 -366
  407. package/latticeai/services/review_queue.py +0 -380
  408. package/latticeai/services/router_context.py +0 -59
  409. package/latticeai/services/run_executor.py +0 -387
  410. package/latticeai/services/self_model_service.py +0 -171
  411. package/latticeai/services/setup_detection.py +0 -147
  412. package/latticeai/services/triggers.py +0 -378
  413. package/latticeai/services/upload_service.py +0 -172
  414. package/latticeai/services/workspace_service.py +0 -165
  415. package/latticeai/setup/__init__.py +0 -25
  416. package/latticeai/setup/auto_setup.py +0 -846
  417. package/latticeai/setup/demo_corpus.py +0 -98
  418. package/latticeai/setup/wizard/__init__.py +0 -126
  419. package/latticeai/setup/wizard/catalog.py +0 -175
  420. package/latticeai/setup/wizard/detect.py +0 -302
  421. package/latticeai/setup/wizard/install.py +0 -348
  422. package/latticeai/setup/wizard/paths.py +0 -165
  423. package/latticeai/setup/wizard/plans.py +0 -74
  424. package/latticeai/setup/wizard/recommend.py +0 -320
  425. package/scripts/bench_agent_smoke.py +0 -409
  426. package/scripts/bench_models.py +0 -540
  427. package/scripts/bench_vector_index.py +0 -295
  428. package/scripts/funnel_soft_gate.py +0 -192
  429. package/scripts/generate_agent_loop_fixtures.py +0 -994
  430. package/scripts/generate_rust_parity_fixtures.py +0 -908
  431. package/scripts/migrate_brain_storage.py +0 -57
  432. package/scripts/parity_fixture_corpus_context.py +0 -162
  433. package/scripts/parity_fixture_corpus_docgen.py +0 -341
  434. package/scripts/profile_kg.py +0 -355
  435. package/server.py +0 -30
  436. package/static/app/assets/arrow-left-kfsrk0mv.js +0 -1
  437. package/static/app/assets/circle-check-qqLug9nU.js +0 -1
  438. package/static/app/assets/index-DxmOfNRi.css +0 -2
  439. package/static/app/assets/search-BcHqkjoy.js +0 -1
@@ -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)))