ltcai 11.5.2 → 11.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -148
- package/bin/ltcai.js +234 -24
- package/docs/CHANGELOG.md +119 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +8 -6
- package/docs/ENTERPRISE.md +2 -1
- package/docs/MULTI_AGENT_RUNTIME.md +12 -5
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +6 -3
- package/docs/REALTIME_COLLABORATION.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/WORKFLOW_DESIGNER.md +3 -2
- package/docs/kg-schema.md +1 -1
- package/docs/v11.6.0_ONE_DOOR_PLAN.md +170 -0
- package/lattice_brain/__init__.py +42 -79
- package/lattice_brain/graph/__init__.py +9 -24
- package/lattice_brain/graph/_kg_common/__init__.py +13 -24
- package/lattice_brain/ingestion/__init__.py +19 -58
- package/lattice_brain/ingestion/pipeline.py +20 -398
- package/lattice_brain/multimodal/__init__.py +7 -18
- package/lattice_brain/multimodal/images.py +6 -264
- package/lattice_brain/multimodal/video.py +8 -247
- package/lattice_brain/runtime/__init__.py +8 -79
- package/lattice_brain/runtime/hooks.py +22 -584
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agent_worker_seam.py +7 -80
- package/latticeai/api/health.py +6 -20
- package/latticeai/api/local_files.py +16 -613
- package/latticeai/api/models.py +12 -87
- package/latticeai/api/search.py +17 -255
- package/latticeai/api/tools.py +69 -750
- package/latticeai/api/voice_capture.py +8 -70
- package/latticeai/api/worker_compute.py +842 -0
- package/latticeai/api/worker_seams.py +216 -0
- package/latticeai/app_factory.py +23 -232
- package/latticeai/cli/entrypoint.py +36 -249
- package/latticeai/core/agent_permission.py +16 -91
- package/latticeai/core/messages.py +89 -531
- package/latticeai/runtime/access_runtime.py +0 -14
- package/latticeai/runtime/bootstrap.py +10 -21
- package/latticeai/runtime/brain_runtime.py +35 -43
- package/latticeai/runtime/build_phases/__init__.py +21 -37
- package/latticeai/runtime/build_phases/features.py +99 -389
- package/latticeai/runtime/build_phases/foundation.py +82 -392
- package/latticeai/runtime/build_phases/web.py +91 -408
- package/latticeai/runtime/build_phases/worker_profile.py +244 -0
- package/latticeai/runtime/lifespan_runtime.py +7 -16
- package/latticeai/runtime/platform_services_runtime.py +1 -15
- package/latticeai/runtime/runtime_context.py +13 -130
- package/latticeai/runtime/security_runtime.py +10 -103
- package/latticeai/services/architecture_readiness.py +82 -43
- package/latticeai/services/model_runtime/__init__.py +2 -11
- package/latticeai/services/model_runtime/service.py +4 -28
- package/latticeai/services/p_reinforce.py +10 -261
- package/latticeai/services/product_readiness.py +31 -38
- package/latticeai/services/search_service.py +37 -795
- package/latticeai/services/tool_dispatch.py +30 -386
- package/latticeai/services/voice_capture.py +13 -107
- package/latticeai/tools/__init__.py +22 -49
- package/latticeai/tools/commands.py +0 -163
- package/latticeai/tools/computer.py +0 -39
- package/latticeai/tools/documents.py +1 -134
- package/latticeai/tools/filesystem.py +1 -247
- package/latticeai/tools/knowledge.py +1 -52
- package/latticeai/tools/local_files.py +0 -20
- package/latticeai/worker_app.py +75 -0
- package/package.json +2 -3
- package/requirements.txt +0 -5
- package/scripts/agent_eval.py +16 -28
- package/scripts/brain_quality_eval.py +20 -183
- package/scripts/bump_version.py +5 -5
- package/scripts/check_current_release_docs.mjs +7 -4
- package/scripts/check_openapi_drift.mjs +10 -0
- package/scripts/check_server_i18n.mjs +2 -18
- package/scripts/compose_openapi.py +377 -0
- package/scripts/export_openapi.py +32 -4
- package/scripts/gen_messages_catalog_fixture.py +302 -0
- package/scripts/gen_openapi_fragments.py +365 -0
- package/scripts/gen_redact_fixture.py +196 -0
- package/scripts/gen_worker_allowlist_fixture.py +115 -0
- package/scripts/generate_agent_parity_fixtures.py +33 -14
- package/scripts/openapi_route_families.json +2161 -0
- package/scripts/release_screen_claims.json +54 -0
- package/scripts/run_integration_tests.mjs +134 -29
- package/scripts/run_sidecar_e2e.mjs +104 -11
- package/scripts/wheel_smoke.py +32 -22
- package/src-tauri/Cargo.lock +375 -9
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/backend.rs +151 -45
- package/src-tauri/src/main.rs +18 -18
- package/src-tauri/src/topology.rs +58 -88
- package/src-tauri/tauri.conf.json +1 -2
- package/static/app/asset-manifest.json +41 -41
- package/static/app/assets/{Act-DcQizkl1.js → Act-BPcVAbOL.js} +1 -1
- package/static/app/assets/AdminConsole-Bw1ATQL0.js +1 -0
- package/static/app/assets/{Brain-3VCSHFcn.js → Brain-CT92Kos0.js} +2 -2
- package/static/app/assets/{BrainHome-Qm8eaztx.js → BrainHome-CFBkt1K_.js} +1 -1
- package/static/app/assets/{BrainSignals-DS9BtKOW.js → BrainSignals-ReLWF2H8.js} +1 -1
- package/static/app/assets/Capture-BsTokYkk.js +1 -0
- package/static/app/assets/{Chronicle-BGvuAchH.js → Chronicle-B6f0T9id.js} +1 -1
- package/static/app/assets/{CommandPalette-Bqhm0Urn.js → CommandPalette-CuvjTv1u.js} +1 -1
- package/static/app/assets/{Library-BV6NnF0a.js → Library-BGJbG9Hd.js} +1 -1
- package/static/app/assets/{LivingBrain-GzenJchP.js → LivingBrain-DGYK_Jsa.js} +1 -1
- package/static/app/assets/{ProductFlow-DEP6-vML.js → ProductFlow-DXBC6brE.js} +1 -1
- package/static/app/assets/{ReviewCard-CNZ7XjWG.js → ReviewCard-HXRle3qq.js} +2 -2
- package/static/app/assets/System-CMHSO9qM.js +1 -0
- package/static/app/assets/arrow-left-BfmkskWx.js +1 -0
- package/static/app/assets/{bot-B_K1Tdmw.js → bot-Cn8bWRuq.js} +1 -1
- package/static/app/assets/{brain-DWyaV1L1.js → brain-CQJberbE.js} +1 -1
- package/static/app/assets/{button-aTn4s84A.js → button-Ct9f2_oT.js} +1 -1
- package/static/app/assets/circle-check-DruOxB-4.js +1 -0
- package/static/app/assets/{circle-pause-xKgeGXkT.js → circle-pause-CmzC_apg.js} +1 -1
- package/static/app/assets/{circle-play-DkT6tYPX.js → circle-play-D8mW2aQ7.js} +1 -1
- package/static/app/assets/{cpu-85xYObUC.js → cpu-DZcdd0PZ.js} +1 -1
- package/static/app/assets/{download-B5Fm7YXo.js → download-bv1KEPGQ.js} +1 -1
- package/static/app/assets/{folder-open-kk2Xa52u.js → folder-open-d-Pip5gr.js} +1 -1
- package/static/app/assets/{hard-drive-DkA3zBW_.js → hard-drive-D20iavUb.js} +1 -1
- package/static/app/assets/index-D9x-kSNy.css +2 -0
- package/static/app/assets/{index-BMPdTmlY.js → index-Do83hDzJ.js} +3 -3
- package/static/app/assets/{input-B0nRf2jO.js → input-BLXVNmj1.js} +1 -1
- package/static/app/assets/{link-2-Dwb4gnTc.js → link-2-BPJOFlAy.js} +1 -1
- package/static/app/assets/{permissionCopy-CQDUBrOZ.js → permissionCopy-ChdJd493.js} +1 -1
- package/static/app/assets/primitives-Cv5tbZBY.js +1 -0
- package/static/app/assets/search-CT9aho2j.js +1 -0
- package/static/app/assets/{share-2-BsrxFglO.js → share-2-YNX_NtMU.js} +1 -1
- package/static/app/assets/{shield-alert-5BStfp2_.js → shield-alert-DuQ3zrVL.js} +1 -1
- package/static/app/assets/{textarea-Cg8IUA-k.js → textarea-DqwLnli4.js} +1 -1
- package/static/app/assets/{useFocusTrap-CYKvE46M.js → useFocusTrap-ZVI98jaW.js} +1 -1
- package/static/app/assets/{useMutation-CSn9t1op.js → useMutation-CVC4qv_D.js} +1 -1
- package/static/app/assets/{useQuery-CY2OI2uy.js → useQuery-C7BeG4HU.js} +1 -1
- package/static/app/assets/{utils-Ddol2RWD.js → utils-CiFtIdZq.js} +1 -1
- package/static/app/assets/{workspace-BqDwOz_p.js → workspace-DQz9vIId.js} +1 -1
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/lattice_brain/archive.py +0 -522
- package/lattice_brain/context.py +0 -325
- package/lattice_brain/conversations.py +0 -382
- package/lattice_brain/core.py +0 -82
- package/lattice_brain/graph/_kg_contract.py +0 -249
- package/lattice_brain/graph/curator.py +0 -676
- package/lattice_brain/graph/discovery.py +0 -595
- package/lattice_brain/graph/discovery_index/__init__.py +0 -35
- package/lattice_brain/graph/discovery_index/cleanup.py +0 -182
- package/lattice_brain/graph/discovery_index/extract.py +0 -137
- package/lattice_brain/graph/discovery_index/scan.py +0 -411
- package/lattice_brain/graph/discovery_index/upsert.py +0 -495
- package/lattice_brain/graph/documents.py +0 -380
- package/lattice_brain/graph/fusion.py +0 -395
- package/lattice_brain/graph/identity.py +0 -175
- package/lattice_brain/graph/image_vectors.py +0 -230
- package/lattice_brain/graph/ingest.py +0 -829
- package/lattice_brain/graph/network.py +0 -205
- package/lattice_brain/graph/proactive.py +0 -724
- package/lattice_brain/graph/projection/__init__.py +0 -42
- package/lattice_brain/graph/projection/curation.py +0 -500
- package/lattice_brain/graph/projection/v2_schema.py +0 -518
- package/lattice_brain/graph/provenance.py +0 -524
- package/lattice_brain/graph/rerank.py +0 -163
- package/lattice_brain/graph/retrieval/__init__.py +0 -54
- package/lattice_brain/graph/retrieval/context.py +0 -197
- package/lattice_brain/graph/retrieval/graph_view.py +0 -319
- package/lattice_brain/graph/retrieval/hybrid.py +0 -488
- package/lattice_brain/graph/retrieval/maintenance.py +0 -121
- package/lattice_brain/graph/retrieval/signals.py +0 -95
- package/lattice_brain/graph/retrieval_docgen.py +0 -253
- package/lattice_brain/graph/retrieval_policy.py +0 -180
- package/lattice_brain/graph/retrieval_reads.py +0 -769
- package/lattice_brain/graph/retrieval_vector/__init__.py +0 -42
- package/lattice_brain/graph/retrieval_vector/fingerprint.py +0 -97
- package/lattice_brain/graph/retrieval_vector/indexing.py +0 -347
- package/lattice_brain/graph/retrieval_vector/search.py +0 -560
- package/lattice_brain/graph/retrieval_vector/status.py +0 -374
- package/lattice_brain/graph/schema.py +0 -792
- package/lattice_brain/graph/store.py +0 -268
- package/lattice_brain/graph/vector_index/__init__.py +0 -85
- package/lattice_brain/graph/vector_index/base.py +0 -167
- package/lattice_brain/graph/vector_index/brute_force.py +0 -110
- package/lattice_brain/graph/vector_index/hnsw.py +0 -290
- package/lattice_brain/graph/vector_index/jobs.py +0 -287
- package/lattice_brain/graph/vector_index/quantized.py +0 -148
- package/lattice_brain/graph/vector_index/selector.py +0 -161
- package/lattice_brain/graph/write_master.py +0 -308
- package/lattice_brain/ingestion/_contract.py +0 -90
- package/lattice_brain/ingestion/folder_scan.py +0 -57
- package/lattice_brain/ingestion/folders.py +0 -258
- package/lattice_brain/ingestion/jobs_api.py +0 -107
- package/lattice_brain/ingestion/routing.py +0 -295
- package/lattice_brain/ingestion_jobs.py +0 -380
- package/lattice_brain/memory.py +0 -75
- package/lattice_brain/portability/__init__.py +0 -90
- package/lattice_brain/portability/_contract.py +0 -42
- package/lattice_brain/portability/backups.py +0 -338
- package/lattice_brain/portability/bundles.py +0 -136
- package/lattice_brain/portability/constants.py +0 -93
- package/lattice_brain/portability/fsops.py +0 -138
- package/lattice_brain/portability/service.py +0 -41
- package/lattice_brain/portability/sharing.py +0 -710
- package/lattice_brain/quality.py +0 -543
- package/lattice_brain/retrieval_benchmark_fixtures.py +0 -92
- package/lattice_brain/runtime/agent_runtime.py +0 -859
- package/lattice_brain/runtime/contracts.py +0 -460
- package/lattice_brain/runtime/multi_agent.py +0 -942
- package/lattice_brain/runtime/statuses.py +0 -10
- package/lattice_brain/sealed_box.py +0 -240
- package/lattice_brain/self_model.py +0 -675
- package/lattice_brain/sensitivity.py +0 -94
- package/lattice_brain/storage/__init__.py +0 -22
- package/lattice_brain/storage/base.py +0 -100
- package/lattice_brain/storage/docker.py +0 -105
- package/lattice_brain/storage/factory.py +0 -31
- package/lattice_brain/storage/migration.py +0 -191
- package/lattice_brain/storage/postgres.py +0 -123
- package/lattice_brain/storage/sqlite.py +0 -143
- package/lattice_brain/synthesis.py +0 -824
- package/lattice_brain/workflow.py +0 -497
- package/latticeai/api/admin.py +0 -471
- package/latticeai/api/agent_registry.py +0 -105
- package/latticeai/api/agents.py +0 -228
- package/latticeai/api/auth.py +0 -383
- package/latticeai/api/automation_intelligence.py +0 -401
- package/latticeai/api/brain_intelligence.py +0 -199
- package/latticeai/api/browser.py +0 -493
- package/latticeai/api/change_proposals.py +0 -89
- package/latticeai/api/chat.py +0 -572
- package/latticeai/api/chat_agent_http.py +0 -892
- package/latticeai/api/chat_contracts.py +0 -73
- package/latticeai/api/chat_documents.py +0 -276
- package/latticeai/api/chat_helpers.py +0 -460
- package/latticeai/api/chat_history.py +0 -101
- package/latticeai/api/chat_hybrid.py +0 -113
- package/latticeai/api/chat_intents.py +0 -646
- package/latticeai/api/chat_stream.py +0 -216
- package/latticeai/api/chronicle.py +0 -63
- package/latticeai/api/command_center.py +0 -51
- package/latticeai/api/computer_use.py +0 -474
- package/latticeai/api/evidence_actions.py +0 -48
- package/latticeai/api/features.py +0 -70
- package/latticeai/api/funnel_metrics.py +0 -31
- package/latticeai/api/garden.py +0 -34
- package/latticeai/api/hooks.py +0 -165
- package/latticeai/api/index_jobs.py +0 -145
- package/latticeai/api/invitations.py +0 -100
- package/latticeai/api/knowledge_graph.py +0 -536
- package/latticeai/api/marketplace.py +0 -105
- package/latticeai/api/mcp.py +0 -482
- package/latticeai/api/memory.py +0 -270
- package/latticeai/api/network.py +0 -81
- package/latticeai/api/network_boundary.py +0 -225
- package/latticeai/api/permission_mode.py +0 -61
- package/latticeai/api/permissions.py +0 -436
- package/latticeai/api/plugins.py +0 -126
- package/latticeai/api/portability.py +0 -391
- package/latticeai/api/project_sessions.py +0 -114
- package/latticeai/api/realtime.py +0 -118
- package/latticeai/api/review_queue.py +0 -364
- package/latticeai/api/security_dashboard.py +0 -604
- package/latticeai/api/setup.py +0 -319
- package/latticeai/api/static_routes.py +0 -354
- package/latticeai/api/ui_redirects.py +0 -26
- package/latticeai/api/workflow_designer.py +0 -394
- package/latticeai/api/workspace.py +0 -856
- package/latticeai/api/workspace_scope.py +0 -125
- package/latticeai/core/agent/__init__.py +0 -93
- package/latticeai/core/agent/_contract.py +0 -79
- package/latticeai/core/agent/context.py +0 -57
- package/latticeai/core/agent/deps.py +0 -125
- package/latticeai/core/agent/execution.py +0 -622
- package/latticeai/core/agent/planning.py +0 -145
- package/latticeai/core/agent/recovery.py +0 -157
- package/latticeai/core/agent/runtime.py +0 -210
- package/latticeai/core/agent/verification.py +0 -231
- package/latticeai/core/agent_eval.py +0 -739
- package/latticeai/core/agent_helpers.py +0 -493
- package/latticeai/core/agent_profiles.py +0 -110
- package/latticeai/core/agent_prompts.py +0 -171
- package/latticeai/core/agent_registry.py +0 -232
- package/latticeai/core/agent_state.py +0 -41
- package/latticeai/core/agent_trace.py +0 -104
- package/latticeai/core/artifact_ledger.py +0 -109
- package/latticeai/core/audit.py +0 -260
- package/latticeai/core/builtin_hooks.py +0 -105
- package/latticeai/core/context_builder.py +0 -394
- package/latticeai/core/document_generator.py +0 -103
- package/latticeai/core/enterprise.py +0 -154
- package/latticeai/core/enterprise_admin.py +0 -158
- package/latticeai/core/file_generation/__init__.py +0 -115
- package/latticeai/core/file_generation/bundles.py +0 -76
- package/latticeai/core/file_generation/extraction.py +0 -154
- package/latticeai/core/file_generation/inference.py +0 -235
- package/latticeai/core/file_generation/orchestration.py +0 -152
- package/latticeai/core/file_generation/prompting.py +0 -117
- package/latticeai/core/file_generation/repair.py +0 -114
- package/latticeai/core/file_generation/sanitize.py +0 -61
- package/latticeai/core/file_generation/validation.py +0 -201
- package/latticeai/core/invitations.py +0 -132
- package/latticeai/core/legacy_compatibility.py +0 -243
- package/latticeai/core/logging_safety.py +0 -46
- package/latticeai/core/marketplace.py +0 -293
- package/latticeai/core/mcp_catalog.py +0 -452
- package/latticeai/core/mcp_registry.py +0 -506
- package/latticeai/core/network_boundary.py +0 -168
- package/latticeai/core/oidc.py +0 -208
- package/latticeai/core/plugins.py +0 -432
- package/latticeai/core/product_hardening.py +0 -218
- package/latticeai/core/project_sessions.py +0 -337
- package/latticeai/core/realtime.py +0 -238
- package/latticeai/core/run_explain.py +0 -426
- package/latticeai/core/run_store.py +0 -252
- package/latticeai/core/timezones.py +0 -80
- package/latticeai/core/workspace_computer_memory.py +0 -84
- package/latticeai/core/workspace_graph_trace.py +0 -155
- package/latticeai/core/workspace_indexing.py +0 -102
- package/latticeai/core/workspace_memory.py +0 -77
- package/latticeai/core/workspace_onboarding.py +0 -104
- package/latticeai/core/workspace_os.py +0 -978
- package/latticeai/core/workspace_os_constants.py +0 -126
- package/latticeai/core/workspace_os_state.py +0 -180
- package/latticeai/core/workspace_os_utils.py +0 -103
- package/latticeai/core/workspace_permissions.py +0 -101
- package/latticeai/core/workspace_plugins.py +0 -97
- package/latticeai/core/workspace_relationships.py +0 -99
- package/latticeai/core/workspace_reorganization.py +0 -335
- package/latticeai/core/workspace_review_items.py +0 -112
- package/latticeai/core/workspace_runs.py +0 -726
- package/latticeai/core/workspace_skills.py +0 -109
- package/latticeai/core/workspace_snapshots.py +0 -198
- package/latticeai/core/workspace_timeline.py +0 -110
- package/latticeai/integrations/__init__.py +0 -0
- package/latticeai/integrations/telegram_bot/__init__.py +0 -123
- package/latticeai/integrations/telegram_bot/__main__.py +0 -17
- package/latticeai/integrations/telegram_bot/config.py +0 -86
- package/latticeai/integrations/telegram_bot/dispatch.py +0 -311
- package/latticeai/integrations/telegram_bot/flows.py +0 -478
- package/latticeai/integrations/telegram_bot/helpers.py +0 -322
- package/latticeai/integrations/telegram_bot/screens.py +0 -394
- package/latticeai/runtime/audit_runtime.py +0 -76
- package/latticeai/runtime/automation_runtime.py +0 -81
- package/latticeai/runtime/chat_wiring.py +0 -141
- package/latticeai/runtime/context_runtime.py +0 -66
- package/latticeai/runtime/feature_toggle_wiring.py +0 -157
- package/latticeai/runtime/history_runtime.py +0 -163
- package/latticeai/runtime/history_writer.py +0 -138
- package/latticeai/runtime/hooks_runtime.py +0 -77
- package/latticeai/runtime/model_wiring.py +0 -68
- package/latticeai/runtime/namespace_runtime.py +0 -163
- package/latticeai/runtime/network_boundary_wiring.py +0 -117
- package/latticeai/runtime/network_config_runtime.py +0 -56
- package/latticeai/runtime/permission_mode_wiring.py +0 -112
- package/latticeai/runtime/persistence_runtime.py +0 -159
- package/latticeai/runtime/platform_runtime_wiring.py +0 -89
- package/latticeai/runtime/review_wiring.py +0 -42
- package/latticeai/runtime/router_registration.py +0 -693
- package/latticeai/runtime/service_singletons.py +0 -55
- package/latticeai/runtime/sso_config_runtime.py +0 -128
- package/latticeai/runtime/user_key_runtime.py +0 -106
- package/latticeai/runtime/web_runtime.py +0 -92
- package/latticeai/server_app.py +0 -51
- package/latticeai/services/app_context.py +0 -130
- package/latticeai/services/automation_execution.py +0 -266
- package/latticeai/services/automation_intelligence.py +0 -614
- package/latticeai/services/brain_automation.py +0 -191
- package/latticeai/services/brain_intelligence/__init__.py +0 -58
- package/latticeai/services/brain_intelligence/_contract.py +0 -71
- package/latticeai/services/brain_intelligence/consistency.py +0 -193
- package/latticeai/services/brain_intelligence/constants.py +0 -47
- package/latticeai/services/brain_intelligence/digest.py +0 -258
- package/latticeai/services/brain_intelligence/health.py +0 -331
- package/latticeai/services/brain_intelligence/proposals.py +0 -259
- package/latticeai/services/brain_intelligence/sampling.py +0 -84
- package/latticeai/services/brain_intelligence/service.py +0 -48
- package/latticeai/services/change_proposals.py +0 -471
- package/latticeai/services/chat_service.py +0 -243
- package/latticeai/services/chronicle.py +0 -555
- package/latticeai/services/cloud_egress_audit.py +0 -85
- package/latticeai/services/cloud_extraction.py +0 -129
- package/latticeai/services/cloud_streaming.py +0 -268
- package/latticeai/services/cloud_token_guard.py +0 -84
- package/latticeai/services/command_center.py +0 -548
- package/latticeai/services/evidence_actions.py +0 -258
- package/latticeai/services/feature_toggles.py +0 -502
- package/latticeai/services/folder_watch.py +0 -520
- package/latticeai/services/funnel_metrics.py +0 -307
- package/latticeai/services/hybrid_chat.py +0 -316
- package/latticeai/services/hybrid_context.py +0 -228
- package/latticeai/services/hybrid_policy.py +0 -129
- package/latticeai/services/interop_bridges.py +0 -978
- package/latticeai/services/local_knowledge.py +0 -465
- package/latticeai/services/memory_service/__init__.py +0 -52
- package/latticeai/services/memory_service/_contract.py +0 -100
- package/latticeai/services/memory_service/brief.py +0 -431
- package/latticeai/services/memory_service/constants.py +0 -57
- package/latticeai/services/memory_service/maintenance.py +0 -138
- package/latticeai/services/memory_service/manager.py +0 -186
- package/latticeai/services/memory_service/proof.py +0 -136
- package/latticeai/services/memory_service/recall.py +0 -225
- package/latticeai/services/memory_service/service.py +0 -48
- package/latticeai/services/memory_service/stores.py +0 -110
- package/latticeai/services/mode_store.py +0 -132
- package/latticeai/services/model_recommendation.py +0 -224
- package/latticeai/services/model_runtime/cloud.py +0 -87
- package/latticeai/services/network_boundary_service.py +0 -117
- package/latticeai/services/obsidian_bridge.py +0 -609
- package/latticeai/services/openai_compatible_adapter.py +0 -101
- package/latticeai/services/permission_mode_service.py +0 -122
- package/latticeai/services/platform_runtime.py +0 -366
- package/latticeai/services/review_queue.py +0 -380
- package/latticeai/services/router_context.py +0 -59
- package/latticeai/services/run_executor.py +0 -387
- package/latticeai/services/self_model_service.py +0 -171
- package/latticeai/services/setup_detection.py +0 -147
- package/latticeai/services/triggers.py +0 -378
- package/latticeai/services/upload_service.py +0 -172
- package/latticeai/services/workspace_service.py +0 -165
- package/latticeai/setup/__init__.py +0 -25
- package/latticeai/setup/auto_setup.py +0 -846
- package/latticeai/setup/demo_corpus.py +0 -98
- package/latticeai/setup/wizard/__init__.py +0 -126
- package/latticeai/setup/wizard/catalog.py +0 -175
- package/latticeai/setup/wizard/detect.py +0 -302
- package/latticeai/setup/wizard/install.py +0 -348
- package/latticeai/setup/wizard/paths.py +0 -165
- package/latticeai/setup/wizard/plans.py +0 -74
- package/latticeai/setup/wizard/recommend.py +0 -320
- package/scripts/bench_agent_smoke.py +0 -409
- package/scripts/bench_models.py +0 -540
- package/scripts/bench_vector_index.py +0 -295
- package/scripts/funnel_soft_gate.py +0 -192
- package/scripts/generate_agent_loop_fixtures.py +0 -994
- package/scripts/generate_rust_parity_fixtures.py +0 -908
- package/scripts/migrate_brain_storage.py +0 -57
- package/scripts/parity_fixture_corpus_context.py +0 -162
- package/scripts/parity_fixture_corpus_docgen.py +0 -341
- package/scripts/profile_kg.py +0 -355
- package/server.py +0 -30
- package/static/app/assets/AdminConsole-cf4npybT.js +0 -1
- package/static/app/assets/Capture-DiQ219jW.js +0 -1
- package/static/app/assets/System-CieofHQa.js +0 -1
- package/static/app/assets/arrow-left-kfsrk0mv.js +0 -1
- package/static/app/assets/circle-check-qqLug9nU.js +0 -1
- package/static/app/assets/index-DxmOfNRi.css +0 -2
- package/static/app/assets/primitives-SNp0LRJz.js +0 -1
- package/static/app/assets/search-BcHqkjoy.js +0 -1
|
@@ -1,69 +1,33 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Tool lifecycle dispatch — the one path every tool call takes.
|
|
2
2
|
|
|
3
3
|
(lattice_brain/runtime/hooks.py)
|
|
4
|
-
책임: HookContext/Result,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
user has registered (``user``). The registry never silently drops a hook: the
|
|
21
|
-
full set is always returned so the UI can show exactly what runs and in which
|
|
22
|
-
order.
|
|
4
|
+
책임: HookContext/Result, dispatch_tool (pre_tool → execute → post_tool).
|
|
5
|
+
상위: api/agent_worker_seam, api/tools.
|
|
6
|
+
|
|
7
|
+
v3.2.0 made lifecycle hooks first-class: a persisted registry under
|
|
8
|
+
``data_dir/hooks.json``, ordering, enable flags, built-in descriptions and a
|
|
9
|
+
subprocess runner for user hooks. All of that is platform state, and v11.6.0
|
|
10
|
+
moved platform state into ``lattice-host`` — the registry, its HTTP surface
|
|
11
|
+
(``/api/hooks/*``) and the built-in runners (which bound to the audit log and
|
|
12
|
+
the sensitivity classifier) went with it.
|
|
13
|
+
|
|
14
|
+
What a stateless compute worker still needs is the *shape* of a dispatch, so
|
|
15
|
+
the seam that runs a tool runs it through the same three steps the product path
|
|
16
|
+
does. :func:`dispatch_tool` with ``hooks=None`` — which is what the worker
|
|
17
|
+
passes, because it holds no registry — is a transparent pass-through, and
|
|
18
|
+
:class:`HookContext` / :class:`HookResult` remain the contract a bound runner
|
|
19
|
+
would be handed.
|
|
23
20
|
"""
|
|
24
21
|
|
|
25
22
|
from __future__ import annotations
|
|
26
23
|
|
|
27
|
-
import json
|
|
28
24
|
import logging
|
|
29
|
-
import os
|
|
30
|
-
import shlex
|
|
31
|
-
import subprocess
|
|
32
|
-
import tempfile
|
|
33
|
-
import threading
|
|
34
|
-
import time
|
|
35
|
-
from collections import deque
|
|
36
|
-
from pathlib import Path
|
|
37
25
|
from typing import Any, Callable, Dict, List, Optional
|
|
38
26
|
|
|
39
|
-
from ..quiet import quiet
|
|
40
27
|
from ..utils import now_iso as _now
|
|
41
28
|
|
|
42
29
|
LOGGER = logging.getLogger(__name__)
|
|
43
30
|
|
|
44
|
-
|
|
45
|
-
HOOK_KINDS = (
|
|
46
|
-
"pre_run",
|
|
47
|
-
"post_run",
|
|
48
|
-
"pre_tool",
|
|
49
|
-
"post_tool",
|
|
50
|
-
"pre_workflow",
|
|
51
|
-
"post_workflow",
|
|
52
|
-
"pre_upload",
|
|
53
|
-
"post_upload",
|
|
54
|
-
"pre_index",
|
|
55
|
-
"post_index",
|
|
56
|
-
"agent",
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
# Kinds retired in v3.4.1 in favour of the explicit pre_/post_ lifecycle pairs.
|
|
60
|
-
# Accepted on input and mapped forward so older callers / persisted custom hooks
|
|
61
|
-
# never break.
|
|
62
|
-
LEGACY_KIND_ALIASES = {
|
|
63
|
-
"workflow": "post_workflow",
|
|
64
|
-
"pipeline": "post_index",
|
|
65
|
-
}
|
|
66
|
-
|
|
67
31
|
# Hook statuses a dispatch can record.
|
|
68
32
|
HOOK_STATUSES = ("ok", "blocked", "error", "skipped", "advisory")
|
|
69
33
|
|
|
@@ -231,535 +195,9 @@ def dispatch_tool(
|
|
|
231
195
|
return result
|
|
232
196
|
|
|
233
197
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
BUILTIN_HOOKS: List[Dict[str, Any]] = [
|
|
240
|
-
{
|
|
241
|
-
"id": "builtin:redact-secrets",
|
|
242
|
-
"name": "Redact secrets",
|
|
243
|
-
"kind": "pre_run",
|
|
244
|
-
"order": 10,
|
|
245
|
-
"description": "Strip secret-like fields (token, password, api_key…) from agent context packets before a run.",
|
|
246
|
-
"binding": "lattice_brain.runtime.multi_agent._redact",
|
|
247
|
-
"managed": "platform",
|
|
248
|
-
},
|
|
249
|
-
{
|
|
250
|
-
"id": "builtin:research-memory-snapshot",
|
|
251
|
-
"name": "Research memory snapshot",
|
|
252
|
-
"kind": "agent",
|
|
253
|
-
"order": 20,
|
|
254
|
-
"description": "Capture a short-term memory snapshot after the researcher stage gathers context.",
|
|
255
|
-
"binding": "lattice_brain.runtime.multi_agent.default_role_runner",
|
|
256
|
-
"managed": "platform",
|
|
257
|
-
},
|
|
258
|
-
{
|
|
259
|
-
"id": "builtin:tool-permission-gate",
|
|
260
|
-
"name": "Tool permission gate",
|
|
261
|
-
"kind": "pre_tool",
|
|
262
|
-
"order": 10,
|
|
263
|
-
"description": "Require explicit approval for tools whose governance policy is not auto-approve.",
|
|
264
|
-
"binding": "latticeai.core.tool_registry.ToolRegistry.permission",
|
|
265
|
-
"managed": "platform",
|
|
266
|
-
},
|
|
267
|
-
{
|
|
268
|
-
"id": "builtin:sensitive-data-guard",
|
|
269
|
-
"name": "Sensitive-data guard",
|
|
270
|
-
"kind": "pre_tool",
|
|
271
|
-
"order": 20,
|
|
272
|
-
"description": "Classify outgoing content for sensitive data before tool execution.",
|
|
273
|
-
"binding": "server_app.classify_sensitive_message",
|
|
274
|
-
"managed": "platform",
|
|
275
|
-
},
|
|
276
|
-
{
|
|
277
|
-
"id": "builtin:audit-agent-run",
|
|
278
|
-
"name": "Audit agent run",
|
|
279
|
-
"kind": "post_run",
|
|
280
|
-
"order": 10,
|
|
281
|
-
"description": "Append every completed agent run to the workspace audit log.",
|
|
282
|
-
"binding": "lattice_brain.runtime.agent_runtime.AgentRuntime.start",
|
|
283
|
-
"managed": "platform",
|
|
284
|
-
},
|
|
285
|
-
{
|
|
286
|
-
"id": "builtin:workflow-replay-log",
|
|
287
|
-
"name": "Workflow replay log",
|
|
288
|
-
"kind": "workflow",
|
|
289
|
-
"order": 10,
|
|
290
|
-
"description": "Record each workflow run's timeline so it can be replayed step by step.",
|
|
291
|
-
"binding": "latticeai.api.workflow_designer",
|
|
292
|
-
"managed": "platform",
|
|
293
|
-
},
|
|
294
|
-
{
|
|
295
|
-
"id": "builtin:pipeline-index-status",
|
|
296
|
-
"name": "Pipeline index status",
|
|
297
|
-
"kind": "pipeline",
|
|
298
|
-
"order": 10,
|
|
299
|
-
"description": "Publish ingest / embed / graph-build pipeline state to the retrieval index status.",
|
|
300
|
-
"binding": "latticeai.api.search",
|
|
301
|
-
"managed": "platform",
|
|
302
|
-
},
|
|
198
|
+
__all__ = [
|
|
199
|
+
"HOOK_STATUSES",
|
|
200
|
+
"HookContext",
|
|
201
|
+
"HookResult",
|
|
202
|
+
"dispatch_tool",
|
|
303
203
|
]
|
|
304
|
-
|
|
305
|
-
# Built-in hooks now bucket onto the v3.4.1 lifecycle pairs.
|
|
306
|
-
for _hook in BUILTIN_HOOKS:
|
|
307
|
-
_hook["kind"] = LEGACY_KIND_ALIASES.get(_hook["kind"], _hook["kind"])
|
|
308
|
-
del _hook
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
class HooksRegistry:
|
|
312
|
-
"""Persisted registry of lifecycle hooks (built-in + user-registered)."""
|
|
313
|
-
|
|
314
|
-
def __init__(self, path: Path, *, command_timeout: float = 20.0, run_log_limit: int = 100):
|
|
315
|
-
self.path = Path(path)
|
|
316
|
-
self._state: Dict[str, Any] = self._load()
|
|
317
|
-
# Runtime dispatch state: in-process runners bound to hook ids, plus a
|
|
318
|
-
# bounded, persisted log of recent executions so the UI can show that
|
|
319
|
-
# hooks actually ran (not just that they are registered).
|
|
320
|
-
self._runtime_runners: Dict[str, Callable[[HookContext], Any]] = {}
|
|
321
|
-
self._command_timeout = float(command_timeout)
|
|
322
|
-
self._run_lock = threading.Lock()
|
|
323
|
-
self._runs_path = self.path.parent / "hooks_runs.json"
|
|
324
|
-
self._runs: deque = deque(self._load_runs(), maxlen=int(run_log_limit))
|
|
325
|
-
|
|
326
|
-
# ── persistence ───────────────────────────────────────────────────────
|
|
327
|
-
def _load(self) -> Dict[str, Any]:
|
|
328
|
-
if self.path.exists():
|
|
329
|
-
try:
|
|
330
|
-
with open(self.path, "r", encoding="utf-8") as fh:
|
|
331
|
-
data = json.load(fh)
|
|
332
|
-
if isinstance(data, dict):
|
|
333
|
-
data.setdefault("custom", [])
|
|
334
|
-
data.setdefault("overrides", {})
|
|
335
|
-
return data
|
|
336
|
-
except (OSError, json.JSONDecodeError, TypeError, ValueError) as exc:
|
|
337
|
-
LOGGER.warning("hook registry state is unreadable at %s: %s", self.path, exc)
|
|
338
|
-
return {"custom": [], "overrides": {}}
|
|
339
|
-
|
|
340
|
-
def _save(self) -> None:
|
|
341
|
-
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
342
|
-
fd, tmp = tempfile.mkstemp(dir=str(self.path.parent), suffix=".tmp")
|
|
343
|
-
try:
|
|
344
|
-
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
|
345
|
-
json.dump(self._state, fh, ensure_ascii=False, indent=2)
|
|
346
|
-
os.replace(tmp, self.path)
|
|
347
|
-
finally:
|
|
348
|
-
if os.path.exists(tmp):
|
|
349
|
-
os.unlink(tmp)
|
|
350
|
-
|
|
351
|
-
# ── views ─────────────────────────────────────────────────────────────
|
|
352
|
-
def _materialize(self) -> List[Dict[str, Any]]:
|
|
353
|
-
overrides: Dict[str, Any] = self._state.get("overrides", {})
|
|
354
|
-
hooks: List[Dict[str, Any]] = []
|
|
355
|
-
for base in BUILTIN_HOOKS:
|
|
356
|
-
ov = overrides.get(base["id"], {})
|
|
357
|
-
hook = dict(base)
|
|
358
|
-
hook["source"] = "builtin"
|
|
359
|
-
hook["enabled"] = bool(ov.get("enabled", True))
|
|
360
|
-
if "order" in ov:
|
|
361
|
-
hook["order"] = ov["order"]
|
|
362
|
-
hook["removable"] = False
|
|
363
|
-
hooks.append(hook)
|
|
364
|
-
for custom in self._state.get("custom", []):
|
|
365
|
-
hook = dict(custom)
|
|
366
|
-
hook["source"] = "user"
|
|
367
|
-
hook.setdefault("managed", "user")
|
|
368
|
-
hook.setdefault("binding", "advisory")
|
|
369
|
-
hook["enabled"] = bool(custom.get("enabled", True))
|
|
370
|
-
hook["removable"] = True
|
|
371
|
-
hooks.append(hook)
|
|
372
|
-
# Honest execution flag: a hook actually runs only if a runner is bound
|
|
373
|
-
# (built-ins) or it carries a command (user hooks); otherwise it is
|
|
374
|
-
# advisory (listed + ordered, but a no-op when fired).
|
|
375
|
-
for hook in hooks:
|
|
376
|
-
hook["executable"] = self.has_runner(hook["id"]) or bool(str(hook.get("command") or "").strip())
|
|
377
|
-
hook["advisory"] = not hook["executable"]
|
|
378
|
-
hooks.sort(key=lambda h: (HOOK_KINDS.index(h["kind"]) if h["kind"] in HOOK_KINDS else 99, h.get("order", 100), h["id"]))
|
|
379
|
-
return hooks
|
|
380
|
-
|
|
381
|
-
def list(self, kind: Optional[str] = None) -> Dict[str, Any]:
|
|
382
|
-
hooks = self._materialize()
|
|
383
|
-
if kind:
|
|
384
|
-
hooks = [h for h in hooks if h["kind"] == kind]
|
|
385
|
-
counts: Dict[str, Dict[str, int]] = {}
|
|
386
|
-
for h in self._materialize():
|
|
387
|
-
bucket = counts.setdefault(h["kind"], {"total": 0, "enabled": 0})
|
|
388
|
-
bucket["total"] += 1
|
|
389
|
-
if h["enabled"]:
|
|
390
|
-
bucket["enabled"] += 1
|
|
391
|
-
return {
|
|
392
|
-
"hooks": hooks,
|
|
393
|
-
"kinds": list(HOOK_KINDS),
|
|
394
|
-
"counts": counts,
|
|
395
|
-
"total": len(hooks),
|
|
396
|
-
"enabled": sum(1 for h in hooks if h["enabled"]),
|
|
397
|
-
"generated_at": _now(),
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
def get(self, hook_id: str) -> Optional[Dict[str, Any]]:
|
|
401
|
-
return next((h for h in self._materialize() if h["id"] == hook_id), None)
|
|
402
|
-
|
|
403
|
-
def inspect(self, hook_id: str) -> Dict[str, Any]:
|
|
404
|
-
hook = self.get(hook_id)
|
|
405
|
-
if hook is None:
|
|
406
|
-
raise KeyError(hook_id)
|
|
407
|
-
detail = dict(hook)
|
|
408
|
-
detail["advisory"] = hook.get("managed") != "platform"
|
|
409
|
-
detail["note"] = (
|
|
410
|
-
"Enforced by its owning subsystem; the registry controls visibility and ordering."
|
|
411
|
-
if hook.get("managed") == "platform"
|
|
412
|
-
else "User-registered hook: listed, ordered and inspectable; runs advisory in this build."
|
|
413
|
-
)
|
|
414
|
-
return detail
|
|
415
|
-
|
|
416
|
-
# ── mutations ─────────────────────────────────────────────────────────
|
|
417
|
-
def set_enabled(self, hook_id: str, enabled: bool) -> Dict[str, Any]:
|
|
418
|
-
if self.get(hook_id) is None:
|
|
419
|
-
raise KeyError(hook_id)
|
|
420
|
-
if hook_id.startswith("builtin:"):
|
|
421
|
-
self._state.setdefault("overrides", {}).setdefault(hook_id, {})["enabled"] = bool(enabled)
|
|
422
|
-
else:
|
|
423
|
-
for custom in self._state.get("custom", []):
|
|
424
|
-
if custom["id"] == hook_id:
|
|
425
|
-
custom["enabled"] = bool(enabled)
|
|
426
|
-
self._save()
|
|
427
|
-
return self.get(hook_id) # type: ignore[return-value]
|
|
428
|
-
|
|
429
|
-
def set_order(self, hook_id: str, order: int) -> Dict[str, Any]:
|
|
430
|
-
if self.get(hook_id) is None:
|
|
431
|
-
raise KeyError(hook_id)
|
|
432
|
-
order = int(order)
|
|
433
|
-
if hook_id.startswith("builtin:"):
|
|
434
|
-
self._state.setdefault("overrides", {}).setdefault(hook_id, {})["order"] = order
|
|
435
|
-
else:
|
|
436
|
-
for custom in self._state.get("custom", []):
|
|
437
|
-
if custom["id"] == hook_id:
|
|
438
|
-
custom["order"] = order
|
|
439
|
-
self._save()
|
|
440
|
-
return self.get(hook_id) # type: ignore[return-value]
|
|
441
|
-
|
|
442
|
-
def reorder(self, kind: str, ordered_ids: List[str]) -> Dict[str, Any]:
|
|
443
|
-
for idx, hook_id in enumerate(ordered_ids):
|
|
444
|
-
try:
|
|
445
|
-
self.set_order(hook_id, (idx + 1) * 10)
|
|
446
|
-
except KeyError:
|
|
447
|
-
quiet()
|
|
448
|
-
continue
|
|
449
|
-
return self.list(kind=kind)
|
|
450
|
-
|
|
451
|
-
def register(
|
|
452
|
-
self,
|
|
453
|
-
*,
|
|
454
|
-
name: str,
|
|
455
|
-
kind: str,
|
|
456
|
-
description: str = "",
|
|
457
|
-
command: str = "",
|
|
458
|
-
order: Optional[int] = None,
|
|
459
|
-
enabled: bool = True,
|
|
460
|
-
) -> Dict[str, Any]:
|
|
461
|
-
if not str(name).strip():
|
|
462
|
-
raise ValueError("name is required")
|
|
463
|
-
kind = LEGACY_KIND_ALIASES.get(kind, kind)
|
|
464
|
-
if kind not in HOOK_KINDS:
|
|
465
|
-
raise ValueError(f"kind must be one of {', '.join(HOOK_KINDS)}")
|
|
466
|
-
slug = str(name).strip().lower().replace(" ", "-")
|
|
467
|
-
hook_id = f"user:{slug}"
|
|
468
|
-
existing = {c["id"] for c in self._state.get("custom", [])}
|
|
469
|
-
if hook_id in existing:
|
|
470
|
-
hook_id = f"user:{slug}-{len(existing) + 1}"
|
|
471
|
-
entry = {
|
|
472
|
-
"id": hook_id,
|
|
473
|
-
"name": str(name).strip(),
|
|
474
|
-
"kind": kind,
|
|
475
|
-
"description": str(description or "").strip(),
|
|
476
|
-
"command": str(command or "").strip(),
|
|
477
|
-
"order": int(order) if order is not None else 100,
|
|
478
|
-
"enabled": bool(enabled),
|
|
479
|
-
"managed": "user",
|
|
480
|
-
"binding": "advisory",
|
|
481
|
-
"created_at": _now(),
|
|
482
|
-
}
|
|
483
|
-
self._state.setdefault("custom", []).append(entry)
|
|
484
|
-
self._save()
|
|
485
|
-
return entry
|
|
486
|
-
|
|
487
|
-
def remove(self, hook_id: str) -> Dict[str, Any]:
|
|
488
|
-
if hook_id.startswith("builtin:"):
|
|
489
|
-
raise ValueError("Built-in hooks cannot be removed; disable them instead.")
|
|
490
|
-
before = len(self._state.get("custom", []))
|
|
491
|
-
self._state["custom"] = [c for c in self._state.get("custom", []) if c["id"] != hook_id]
|
|
492
|
-
if len(self._state["custom"]) == before:
|
|
493
|
-
raise KeyError(hook_id)
|
|
494
|
-
self._save()
|
|
495
|
-
return {"removed": hook_id}
|
|
496
|
-
|
|
497
|
-
# ── execution engine ──────────────────────────────────────────────────
|
|
498
|
-
# The registry above owns *what* runs and in *what order*. The methods below
|
|
499
|
-
# own *running* it: a hook executes either via an in-process runner bound by
|
|
500
|
-
# its owning subsystem (built-ins) or, for user hooks, by running their
|
|
501
|
-
# ``command`` as a subprocess. ``pre_*`` hooks can block the pending action.
|
|
502
|
-
|
|
503
|
-
def register_hook(self, hook_id: str, runner: Callable[[HookContext], Any]) -> "HooksRegistry":
|
|
504
|
-
"""Bind a callable that *executes* when ``hook_id`` fires.
|
|
505
|
-
|
|
506
|
-
Built-in hooks bind their owning subsystem's real behaviour here at app
|
|
507
|
-
startup so dispatch performs actual work rather than a placeholder.
|
|
508
|
-
Returns ``self`` so registrations can be chained.
|
|
509
|
-
"""
|
|
510
|
-
if not callable(runner):
|
|
511
|
-
raise TypeError("runner must be callable")
|
|
512
|
-
self._runtime_runners[hook_id] = runner
|
|
513
|
-
return self
|
|
514
|
-
|
|
515
|
-
# Alias — descriptive name used by the wiring layer.
|
|
516
|
-
register_runtime_hook = register_hook
|
|
517
|
-
|
|
518
|
-
def has_runner(self, hook_id: str) -> bool:
|
|
519
|
-
return hook_id in self._runtime_runners
|
|
520
|
-
|
|
521
|
-
def run_hooks(
|
|
522
|
-
self,
|
|
523
|
-
kind: str,
|
|
524
|
-
context: Optional[HookContext] = None,
|
|
525
|
-
*,
|
|
526
|
-
event: Optional[str] = None,
|
|
527
|
-
payload: Optional[Dict[str, Any]] = None,
|
|
528
|
-
user_email: Optional[str] = None,
|
|
529
|
-
workspace_id: Optional[str] = None,
|
|
530
|
-
metadata: Optional[Dict[str, Any]] = None,
|
|
531
|
-
) -> Dict[str, Any]:
|
|
532
|
-
"""Run every enabled hook of ``kind``, in order, against one context.
|
|
533
|
-
|
|
534
|
-
Returns the dispatch record: which hooks ran, whether the pending action
|
|
535
|
-
was blocked, and a per-hook result list. A ``pre_*`` hook that blocks
|
|
536
|
-
short-circuits the remaining hooks (fail-closed gate semantics).
|
|
537
|
-
"""
|
|
538
|
-
kind = LEGACY_KIND_ALIASES.get(kind, kind)
|
|
539
|
-
if kind not in HOOK_KINDS:
|
|
540
|
-
raise ValueError(f"kind must be one of {', '.join(HOOK_KINDS)}")
|
|
541
|
-
if context is None:
|
|
542
|
-
context = HookContext(
|
|
543
|
-
kind, event=event or kind, payload=payload,
|
|
544
|
-
user_email=user_email, workspace_id=workspace_id, metadata=metadata,
|
|
545
|
-
)
|
|
546
|
-
hooks = [h for h in self._materialize() if h["kind"] == kind and h.get("enabled")]
|
|
547
|
-
results: List[HookResult] = []
|
|
548
|
-
for hook in hooks:
|
|
549
|
-
res = self._run_one(hook, context)
|
|
550
|
-
results.append(res)
|
|
551
|
-
self._record_run(res, context)
|
|
552
|
-
if res.blocked:
|
|
553
|
-
context.block(res.detail or f"{hook['id']} blocked {context.event}")
|
|
554
|
-
break
|
|
555
|
-
return {
|
|
556
|
-
"kind": kind,
|
|
557
|
-
"event": context.event,
|
|
558
|
-
"ran": len(results),
|
|
559
|
-
"blocked": context.blocked,
|
|
560
|
-
"block_reason": context.block_reason,
|
|
561
|
-
"results": [r.as_dict() for r in results],
|
|
562
|
-
"generated_at": _now(),
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
def run_hook(
|
|
566
|
-
self,
|
|
567
|
-
hook_id: str,
|
|
568
|
-
context: Optional[HookContext] = None,
|
|
569
|
-
*,
|
|
570
|
-
event: Optional[str] = None,
|
|
571
|
-
payload: Optional[Dict[str, Any]] = None,
|
|
572
|
-
user_email: Optional[str] = None,
|
|
573
|
-
workspace_id: Optional[str] = None,
|
|
574
|
-
metadata: Optional[Dict[str, Any]] = None,
|
|
575
|
-
) -> Dict[str, Any]:
|
|
576
|
-
"""Run a single hook by id (regardless of kind ordering)."""
|
|
577
|
-
hook = self.get(hook_id)
|
|
578
|
-
if hook is None:
|
|
579
|
-
raise KeyError(hook_id)
|
|
580
|
-
if context is None:
|
|
581
|
-
context = HookContext(
|
|
582
|
-
hook["kind"], event=event or hook_id, payload=payload,
|
|
583
|
-
user_email=user_email, workspace_id=workspace_id, metadata=metadata,
|
|
584
|
-
)
|
|
585
|
-
if not hook.get("enabled"):
|
|
586
|
-
res = HookResult(
|
|
587
|
-
hook_id=hook_id, name=hook.get("name", hook_id), kind=hook["kind"],
|
|
588
|
-
status="skipped", detail="hook disabled",
|
|
589
|
-
source=hook.get("source", ""), binding=hook.get("binding", ""),
|
|
590
|
-
)
|
|
591
|
-
else:
|
|
592
|
-
res = self._run_one(hook, context)
|
|
593
|
-
self._record_run(res, context)
|
|
594
|
-
return res.as_dict()
|
|
595
|
-
|
|
596
|
-
def fire_hook(
|
|
597
|
-
self,
|
|
598
|
-
kind: str,
|
|
599
|
-
event: str = "",
|
|
600
|
-
*,
|
|
601
|
-
payload: Optional[Dict[str, Any]] = None,
|
|
602
|
-
user_email: Optional[str] = None,
|
|
603
|
-
workspace_id: Optional[str] = None,
|
|
604
|
-
metadata: Optional[Dict[str, Any]] = None,
|
|
605
|
-
context: Optional[HookContext] = None,
|
|
606
|
-
) -> Dict[str, Any]:
|
|
607
|
-
"""Fire-and-forget convenience over :meth:`run_hooks`.
|
|
608
|
-
|
|
609
|
-
Never raises — a dispatch failure is captured in the returned record so
|
|
610
|
-
a hook misconfiguration can never crash the lifecycle point that fired
|
|
611
|
-
it. Callers that need to *gate* on a block should read ``["blocked"]``.
|
|
612
|
-
"""
|
|
613
|
-
try:
|
|
614
|
-
return self.run_hooks(
|
|
615
|
-
kind, context, event=event or kind, payload=payload,
|
|
616
|
-
user_email=user_email, workspace_id=workspace_id, metadata=metadata,
|
|
617
|
-
)
|
|
618
|
-
except Exception as exc: # pragma: no cover - defensive
|
|
619
|
-
LOGGER.exception("hook dispatch failed before execution: %s", kind)
|
|
620
|
-
return {"kind": kind, "event": event or kind, "ran": 0, "blocked": False,
|
|
621
|
-
"block_reason": "", "error": str(exc), "results": [], "generated_at": _now()}
|
|
622
|
-
|
|
623
|
-
# ── single-hook execution ─────────────────────────────────────────────
|
|
624
|
-
def _run_one(self, hook: Dict[str, Any], context: HookContext) -> HookResult:
|
|
625
|
-
hook_id = hook["id"]
|
|
626
|
-
start = time.perf_counter()
|
|
627
|
-
runner = self._runtime_runners.get(hook_id)
|
|
628
|
-
try:
|
|
629
|
-
if runner is not None:
|
|
630
|
-
out = runner(context)
|
|
631
|
-
status, detail, output, blocked = self._interpret_runner_output(out, context)
|
|
632
|
-
elif str(hook.get("command") or "").strip():
|
|
633
|
-
status, detail, output, blocked = self._run_command(hook, context)
|
|
634
|
-
else:
|
|
635
|
-
# No bound runner and no command → advisory (listed + ordered only).
|
|
636
|
-
status, detail, output, blocked = "advisory", "", "", False
|
|
637
|
-
except Exception as exc: # a misbehaving hook never breaks the dispatch
|
|
638
|
-
status, detail, output, blocked = "error", str(exc)[:500], "", False
|
|
639
|
-
duration_ms = int((time.perf_counter() - start) * 1000)
|
|
640
|
-
return HookResult(
|
|
641
|
-
hook_id=hook_id,
|
|
642
|
-
name=hook.get("name", hook_id),
|
|
643
|
-
kind=hook["kind"],
|
|
644
|
-
status=status,
|
|
645
|
-
detail=detail,
|
|
646
|
-
output=output,
|
|
647
|
-
duration_ms=duration_ms,
|
|
648
|
-
blocked=blocked,
|
|
649
|
-
source=hook.get("source", ""),
|
|
650
|
-
binding=hook.get("binding", ""),
|
|
651
|
-
)
|
|
652
|
-
|
|
653
|
-
@staticmethod
|
|
654
|
-
def _interpret_runner_output(out: Any, context: HookContext):
|
|
655
|
-
"""Normalize a runner's return value into (status, detail, output, blocked).
|
|
656
|
-
|
|
657
|
-
A runner may return ``None`` (ok), a ``str`` (ok + output text), or a
|
|
658
|
-
dict ``{status?, detail?, output?, block?}``. Calling ``context.block()``
|
|
659
|
-
also blocks, regardless of the return value.
|
|
660
|
-
"""
|
|
661
|
-
blocked = bool(getattr(context, "blocked", False))
|
|
662
|
-
if isinstance(out, dict):
|
|
663
|
-
status = str(out.get("status") or ("blocked" if (out.get("block") or blocked) else "ok"))
|
|
664
|
-
block = bool(out.get("block")) or blocked or status == "blocked"
|
|
665
|
-
detail = str(out.get("detail") or context.block_reason or "")[:500]
|
|
666
|
-
return status, detail, str(out.get("output") or "")[:4000], block
|
|
667
|
-
if isinstance(out, str):
|
|
668
|
-
return ("blocked" if blocked else "ok", context.block_reason if blocked else "", out[:4000], blocked)
|
|
669
|
-
# None or anything else → ok (or blocked if the runner called context.block()).
|
|
670
|
-
return ("blocked" if blocked else "ok", context.block_reason if blocked else "", "", blocked)
|
|
671
|
-
|
|
672
|
-
def _run_command(self, hook: Dict[str, Any], context: HookContext):
|
|
673
|
-
"""Run a user hook's ``command`` as a subprocess with the context on stdin.
|
|
674
|
-
|
|
675
|
-
The full context is provided both as the ``LATTICE_HOOK_CONTEXT`` env var
|
|
676
|
-
and on stdin (JSON). Exit code 0 = ok. A non-zero exit from a ``pre_*``
|
|
677
|
-
hook gates (blocks) the pending action; from any other kind it is
|
|
678
|
-
recorded as an error without blocking. A timeout fails closed for gates.
|
|
679
|
-
"""
|
|
680
|
-
cmd = str(hook.get("command") or "").strip()
|
|
681
|
-
try:
|
|
682
|
-
argv = shlex.split(cmd)
|
|
683
|
-
except ValueError as exc:
|
|
684
|
-
return "error", f"invalid command: {exc}", "", False
|
|
685
|
-
if not argv:
|
|
686
|
-
return "skipped", "empty command", "", False
|
|
687
|
-
ctx_json = json.dumps(context.as_dict(), ensure_ascii=False)
|
|
688
|
-
# Command hooks run with an intentionally small environment. In
|
|
689
|
-
# particular, provider keys, database credentials, session secrets,
|
|
690
|
-
# and arbitrary PYTHON*/NODE* injection variables from the server
|
|
691
|
-
# process must never be inherited by a child process. The retained
|
|
692
|
-
# entries are the minimum needed for executable lookup, home-relative
|
|
693
|
-
# tools, locale handling, temporary files, and Windows process startup.
|
|
694
|
-
allowed_env_keys = {
|
|
695
|
-
"PATH", "HOME", "LANG", "LC_ALL", "LC_CTYPE",
|
|
696
|
-
"TMPDIR", "TMP", "TEMP", "SYSTEMROOT", "WINDIR", "PATHEXT",
|
|
697
|
-
}
|
|
698
|
-
env = {
|
|
699
|
-
key: value
|
|
700
|
-
for key, value in os.environ.items()
|
|
701
|
-
if key.upper() in allowed_env_keys
|
|
702
|
-
}
|
|
703
|
-
env.update({
|
|
704
|
-
"LATTICE_HOOK_KIND": context.kind,
|
|
705
|
-
"LATTICE_HOOK_EVENT": context.event,
|
|
706
|
-
"LATTICE_HOOK_ID": str(hook.get("id", "")),
|
|
707
|
-
"LATTICE_HOOK_CONTEXT": ctx_json,
|
|
708
|
-
})
|
|
709
|
-
is_gate = context.kind.startswith("pre_")
|
|
710
|
-
try:
|
|
711
|
-
proc = subprocess.run(
|
|
712
|
-
argv, input=ctx_json, capture_output=True, text=True,
|
|
713
|
-
timeout=self._command_timeout, env=env,
|
|
714
|
-
)
|
|
715
|
-
except subprocess.TimeoutExpired:
|
|
716
|
-
return ("blocked" if is_gate else "error",
|
|
717
|
-
f"timed out after {self._command_timeout:.0f}s", "", is_gate)
|
|
718
|
-
except Exception as exc:
|
|
719
|
-
return "error", str(exc)[:500], "", False
|
|
720
|
-
output = (proc.stdout or "")[:4000]
|
|
721
|
-
if proc.returncode == 0:
|
|
722
|
-
return "ok", "", output, False
|
|
723
|
-
detail = (proc.stderr or proc.stdout or f"exit code {proc.returncode}").strip()[:500]
|
|
724
|
-
return ("blocked" if is_gate else "error", detail, output, is_gate)
|
|
725
|
-
|
|
726
|
-
# ── run log ────────────────────────────────────────────────────────────
|
|
727
|
-
def _load_runs(self) -> List[Dict[str, Any]]:
|
|
728
|
-
try:
|
|
729
|
-
if self._runs_path.exists():
|
|
730
|
-
with open(self._runs_path, "r", encoding="utf-8") as fh:
|
|
731
|
-
data = json.load(fh)
|
|
732
|
-
if isinstance(data, list):
|
|
733
|
-
return data
|
|
734
|
-
except (OSError, json.JSONDecodeError, TypeError, ValueError) as exc:
|
|
735
|
-
LOGGER.warning("hook run history is unreadable at %s: %s", self._runs_path, exc)
|
|
736
|
-
return []
|
|
737
|
-
|
|
738
|
-
def _save_runs(self) -> None:
|
|
739
|
-
try:
|
|
740
|
-
self._runs_path.parent.mkdir(parents=True, exist_ok=True)
|
|
741
|
-
fd, tmp = tempfile.mkstemp(dir=str(self._runs_path.parent), suffix=".tmp")
|
|
742
|
-
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
|
743
|
-
json.dump(list(self._runs), fh, ensure_ascii=False, indent=2)
|
|
744
|
-
os.replace(tmp, self._runs_path)
|
|
745
|
-
except OSError as exc: # pragma: no cover - the run log is best-effort
|
|
746
|
-
LOGGER.warning("hook run history write failed at %s: %s", self._runs_path, exc)
|
|
747
|
-
|
|
748
|
-
def _record_run(self, result: HookResult, context: HookContext) -> None:
|
|
749
|
-
entry = result.as_dict()
|
|
750
|
-
entry["target_event"] = context.event
|
|
751
|
-
entry["target_kind"] = context.kind
|
|
752
|
-
with self._run_lock:
|
|
753
|
-
self._runs.appendleft(entry)
|
|
754
|
-
self._save_runs()
|
|
755
|
-
|
|
756
|
-
def recent_runs(self, limit: int = 50, kind: Optional[str] = None) -> Dict[str, Any]:
|
|
757
|
-
with self._run_lock:
|
|
758
|
-
runs = list(self._runs)
|
|
759
|
-
if kind:
|
|
760
|
-
runs = [r for r in runs if r.get("target_kind") == kind or r.get("kind") == kind]
|
|
761
|
-
return {
|
|
762
|
-
"runs": runs[: max(0, int(limit))],
|
|
763
|
-
"total": len(runs),
|
|
764
|
-
"generated_at": _now(),
|
|
765
|
-
}
|
package/latticeai/__init__.py
CHANGED