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,436 +0,0 @@
|
|
|
1
|
-
"""Local permission request and approval routes."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
import logging
|
|
7
|
-
import os
|
|
8
|
-
import secrets
|
|
9
|
-
import threading
|
|
10
|
-
import time
|
|
11
|
-
import urllib.request
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
from typing import Any, Dict, Optional, Tuple
|
|
14
|
-
|
|
15
|
-
from fastapi import APIRouter, HTTPException, Request
|
|
16
|
-
|
|
17
|
-
from latticeai.core.http_origin import request_external_origin
|
|
18
|
-
from latticeai.core.io_utils import atomic_write_json
|
|
19
|
-
from latticeai.core.security import sha256_hex
|
|
20
|
-
|
|
21
|
-
_PERMISSION_ACTION_LABELS = {
|
|
22
|
-
"list": "폴더 목록 보기",
|
|
23
|
-
"read": "파일 읽기",
|
|
24
|
-
"write": "파일 쓰기",
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class PermissionGateway:
|
|
29
|
-
"""Shared permission state used by local-file and knowledge routers."""
|
|
30
|
-
|
|
31
|
-
def __init__(self, *, config, data_dir: Path, require_admin, get_current_user) -> None:
|
|
32
|
-
self.require_admin = require_admin
|
|
33
|
-
self.get_current_user = get_current_user
|
|
34
|
-
self.local_approval_ttl_seconds = 5 * 60
|
|
35
|
-
self.local_approval_lock = threading.Lock()
|
|
36
|
-
self.perm_queue_lock = threading.Lock()
|
|
37
|
-
self.local_approvals: Dict[str, Dict[str, Any]] = {}
|
|
38
|
-
self.discord_permission_webhook_url = config.discord_permission_webhook
|
|
39
|
-
self.discord_bot_token = config.discord_bot_token
|
|
40
|
-
self.discord_permission_channel = config.discord_permission_channel
|
|
41
|
-
self.permission_monitor_secret = config.permission_monitor_secret
|
|
42
|
-
self.perm_queue_file = data_dir / "permission_queue.json"
|
|
43
|
-
self._explicit_ui_url = os.getenv("LATTICEAI_PERMISSION_UI_URL", "").strip()
|
|
44
|
-
self._public_url = os.getenv("LATTICEAI_PUBLIC_URL", "").strip().rstrip("/")
|
|
45
|
-
# The worker's own port. Behind the desktop gateway this is the
|
|
46
|
-
# *internal* one, which is why a notification built from it sends the
|
|
47
|
-
# approver to a door only the gateway can open — hence
|
|
48
|
-
# ``_front_door``, learned from the request that asked for approval.
|
|
49
|
-
self._loopback_ui_origin = f"http://127.0.0.1:{getattr(config, 'port', 4825)}"
|
|
50
|
-
self._front_door: Optional[str] = None
|
|
51
|
-
|
|
52
|
-
@property
|
|
53
|
-
def permission_ui_url(self) -> str:
|
|
54
|
-
"""Where to send the person who has to approve this.
|
|
55
|
-
|
|
56
|
-
Operator configuration wins — an explicit URL or a public URL is a
|
|
57
|
-
deliberate statement about how this install is reached. Only when
|
|
58
|
-
nothing is configured does the front door observed on the request that
|
|
59
|
-
triggered the approval beat the worker's own loopback address.
|
|
60
|
-
"""
|
|
61
|
-
if self._explicit_ui_url:
|
|
62
|
-
return self._explicit_ui_url
|
|
63
|
-
base = self._public_url or self._front_door or self._loopback_ui_origin
|
|
64
|
-
return f"{base.rstrip('/')}/app#/admin/permissions"
|
|
65
|
-
|
|
66
|
-
@staticmethod
|
|
67
|
-
def token_hash(token: str) -> str:
|
|
68
|
-
return sha256_hex(str(token or ""))
|
|
69
|
-
|
|
70
|
-
@staticmethod
|
|
71
|
-
def token_hint(token: str) -> str:
|
|
72
|
-
value = str(token or "")
|
|
73
|
-
return value[:8] if value else ""
|
|
74
|
-
|
|
75
|
-
def _read_queue(self) -> Dict:
|
|
76
|
-
if not self.perm_queue_file.exists():
|
|
77
|
-
return {}
|
|
78
|
-
try:
|
|
79
|
-
return json.loads(self.perm_queue_file.read_text(encoding="utf-8"))
|
|
80
|
-
except Exception:
|
|
81
|
-
return {}
|
|
82
|
-
|
|
83
|
-
def _write_queue(self, queue: Dict) -> None:
|
|
84
|
-
# Approval metadata contains local paths and identities. Keep writes
|
|
85
|
-
# atomic and private even when the caller's umask is permissive.
|
|
86
|
-
atomic_write_json(self.perm_queue_file, queue)
|
|
87
|
-
|
|
88
|
-
def _perm_queue_write(self, token: str, record: Dict[str, object]) -> None:
|
|
89
|
-
try:
|
|
90
|
-
key = self.token_hash(token)
|
|
91
|
-
with self.perm_queue_lock:
|
|
92
|
-
queue = self._read_queue()
|
|
93
|
-
queue[key] = {**record, "token_hint": self.token_hint(token), "notified": False}
|
|
94
|
-
self._write_queue(queue)
|
|
95
|
-
except Exception as exc:
|
|
96
|
-
logging.warning("perm_queue_write failed: %s", exc)
|
|
97
|
-
|
|
98
|
-
def _perm_queue_remove_key(self, key: str) -> None:
|
|
99
|
-
try:
|
|
100
|
-
with self.perm_queue_lock:
|
|
101
|
-
queue = self._read_queue()
|
|
102
|
-
queue.pop(key, None)
|
|
103
|
-
self._write_queue(queue)
|
|
104
|
-
except Exception as exc:
|
|
105
|
-
logging.warning("perm_queue_remove failed: %s", exc)
|
|
106
|
-
|
|
107
|
-
def resolve_approval_key(self, token_or_hint: str) -> str:
|
|
108
|
-
"""Resolve either the private token or its UI-safe eight-char hint.
|
|
109
|
-
|
|
110
|
-
Only an authenticated administrator/monitor reaches approval routes.
|
|
111
|
-
Supporting a unique hint lets the admin UI act on pending requests
|
|
112
|
-
without ever receiving the bearer-grade full token.
|
|
113
|
-
"""
|
|
114
|
-
value = str(token_or_hint or "")
|
|
115
|
-
direct_key = self.token_hash(value)
|
|
116
|
-
with self.local_approval_lock:
|
|
117
|
-
if direct_key in self.local_approvals:
|
|
118
|
-
return direct_key
|
|
119
|
-
matches = [
|
|
120
|
-
key
|
|
121
|
-
for key, record in self.local_approvals.items()
|
|
122
|
-
if len(value) == 8 and record.get("token_hint") == value
|
|
123
|
-
]
|
|
124
|
-
if len(matches) > 1:
|
|
125
|
-
raise HTTPException(status_code=409, detail="요청 ID가 중복되었습니다. 관리자 목록을 새로고침하세요.")
|
|
126
|
-
return matches[0] if matches else direct_key
|
|
127
|
-
|
|
128
|
-
@staticmethod
|
|
129
|
-
def normalize_local_path_for_approval(path: str) -> str:
|
|
130
|
-
return str(Path(path).expanduser().resolve())
|
|
131
|
-
|
|
132
|
-
@staticmethod
|
|
133
|
-
def content_fingerprint(content: str = "") -> str:
|
|
134
|
-
return sha256_hex(content)
|
|
135
|
-
|
|
136
|
-
def _notify_discord_permission_sync(self, token: str, path: str, action: str, user_email: str) -> None:
|
|
137
|
-
sent = False
|
|
138
|
-
token_hint = self.token_hint(token)
|
|
139
|
-
if self.discord_bot_token and self.discord_permission_channel:
|
|
140
|
-
action_label = _PERMISSION_ACTION_LABELS.get(action, action)
|
|
141
|
-
expires_at_iso = time.strftime(
|
|
142
|
-
"%Y-%m-%d %H:%M:%S UTC",
|
|
143
|
-
time.gmtime(time.time() + self.local_approval_ttl_seconds),
|
|
144
|
-
)
|
|
145
|
-
msg = (
|
|
146
|
-
f"🔐 **파일 접근 권한 요청**\n"
|
|
147
|
-
f"**경로:** `{path}`\n"
|
|
148
|
-
f"**작업:** {action_label}\n"
|
|
149
|
-
f"**요청자:** {user_email}\n"
|
|
150
|
-
f"**요청 ID:** `{token_hint}`\n"
|
|
151
|
-
f"**만료:** {expires_at_iso}\n\n"
|
|
152
|
-
f"승인/거부: {self.permission_ui_url}"
|
|
153
|
-
)
|
|
154
|
-
payload = json.dumps({"content": msg}, ensure_ascii=False).encode("utf-8")
|
|
155
|
-
try:
|
|
156
|
-
req = urllib.request.Request(
|
|
157
|
-
f"https://discord.com/api/v10/channels/{self.discord_permission_channel}/messages",
|
|
158
|
-
data=payload,
|
|
159
|
-
headers={
|
|
160
|
-
"Content-Type": "application/json",
|
|
161
|
-
"Authorization": f"Bot {self.discord_bot_token}",
|
|
162
|
-
},
|
|
163
|
-
method="POST",
|
|
164
|
-
)
|
|
165
|
-
with urllib.request.urlopen(req, timeout=5):
|
|
166
|
-
pass
|
|
167
|
-
sent = True
|
|
168
|
-
except Exception as exc:
|
|
169
|
-
logging.warning("Discord bot permission notify failed: %s", exc)
|
|
170
|
-
|
|
171
|
-
if not sent and self.discord_permission_webhook_url:
|
|
172
|
-
action_label = _PERMISSION_ACTION_LABELS.get(action, action)
|
|
173
|
-
expires_at_iso = time.strftime(
|
|
174
|
-
"%Y-%m-%d %H:%M:%S UTC",
|
|
175
|
-
time.gmtime(time.time() + self.local_approval_ttl_seconds),
|
|
176
|
-
)
|
|
177
|
-
payload = json.dumps(
|
|
178
|
-
{
|
|
179
|
-
"embeds": [
|
|
180
|
-
{
|
|
181
|
-
"title": "🔐 파일 접근 권한 요청",
|
|
182
|
-
"color": 0xFF9900,
|
|
183
|
-
"fields": [
|
|
184
|
-
{"name": "경로", "value": f"`{path}`", "inline": False},
|
|
185
|
-
{"name": "작업", "value": action_label, "inline": True},
|
|
186
|
-
{"name": "요청자", "value": user_email, "inline": True},
|
|
187
|
-
{"name": "요청 ID", "value": f"`{token_hint}`", "inline": False},
|
|
188
|
-
{"name": "만료", "value": expires_at_iso, "inline": True},
|
|
189
|
-
],
|
|
190
|
-
"footer": {
|
|
191
|
-
"text": f"승인/거부 UI: {self.permission_ui_url}"
|
|
192
|
-
},
|
|
193
|
-
}
|
|
194
|
-
]
|
|
195
|
-
},
|
|
196
|
-
ensure_ascii=False,
|
|
197
|
-
).encode("utf-8")
|
|
198
|
-
try:
|
|
199
|
-
req = urllib.request.Request(
|
|
200
|
-
self.discord_permission_webhook_url,
|
|
201
|
-
data=payload,
|
|
202
|
-
headers={"Content-Type": "application/json"},
|
|
203
|
-
method="POST",
|
|
204
|
-
)
|
|
205
|
-
with urllib.request.urlopen(req, timeout=5):
|
|
206
|
-
pass
|
|
207
|
-
except Exception as exc:
|
|
208
|
-
logging.warning("Discord permission webhook failed: %s", exc)
|
|
209
|
-
|
|
210
|
-
def local_permission_response(self, path: str, action: str, user_email: str, content: str = "") -> dict:
|
|
211
|
-
normalized = self.normalize_local_path_for_approval(path)
|
|
212
|
-
token = secrets.token_urlsafe(24)
|
|
213
|
-
record: Dict[str, object] = {
|
|
214
|
-
"path": normalized,
|
|
215
|
-
"action": action,
|
|
216
|
-
"user_email": user_email,
|
|
217
|
-
"expires_at": time.time() + self.local_approval_ttl_seconds,
|
|
218
|
-
"approved": False,
|
|
219
|
-
}
|
|
220
|
-
if action == "write":
|
|
221
|
-
self.ensure_path_allowed(path, action="write")
|
|
222
|
-
record["content_hash"] = self.content_fingerprint(content)
|
|
223
|
-
with self.local_approval_lock:
|
|
224
|
-
self.local_approvals[self.token_hash(token)] = {**record, "token_hint": self.token_hint(token)}
|
|
225
|
-
self._perm_queue_write(token, record)
|
|
226
|
-
action_label = _PERMISSION_ACTION_LABELS.get(action, action)
|
|
227
|
-
return {
|
|
228
|
-
"permission_required": True,
|
|
229
|
-
"path": path,
|
|
230
|
-
"action": action,
|
|
231
|
-
"action_label": action_label,
|
|
232
|
-
"approval_token": token,
|
|
233
|
-
"expires_in": self.local_approval_ttl_seconds,
|
|
234
|
-
"message": f"AI가 '{path}' 에 대한 {action_label} 권한을 요청합니다.",
|
|
235
|
-
"check_status_url": f"/permissions/status/{token}",
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
def require_local_user(self, request: Request) -> str:
|
|
239
|
-
email = self.get_current_user(request)
|
|
240
|
-
if not email:
|
|
241
|
-
raise HTTPException(status_code=401, detail="로컬 파일 접근은 로그인 세션이 필요합니다.")
|
|
242
|
-
# Every local-file approval passes through here immediately before the
|
|
243
|
-
# permission record (and its notification) is created, so this is the
|
|
244
|
-
# request whose front door the approver should be sent back to. The
|
|
245
|
-
# forwarded authority is only believed from a loopback or configured
|
|
246
|
-
# proxy peer — the same rule CSRF and the invite link use.
|
|
247
|
-
self.remember_front_door(request)
|
|
248
|
-
return email
|
|
249
|
-
|
|
250
|
-
def remember_front_door(self, request: Request) -> None:
|
|
251
|
-
"""Record the external origin this request came through, if any."""
|
|
252
|
-
origin = request_external_origin(request)
|
|
253
|
-
if origin:
|
|
254
|
-
self._front_door = origin
|
|
255
|
-
|
|
256
|
-
def require_local_approval(
|
|
257
|
-
self,
|
|
258
|
-
*,
|
|
259
|
-
token: Optional[str],
|
|
260
|
-
path: str,
|
|
261
|
-
action: str,
|
|
262
|
-
user_email: str,
|
|
263
|
-
content: str = "",
|
|
264
|
-
) -> None:
|
|
265
|
-
if not token:
|
|
266
|
-
raise HTTPException(status_code=403, detail="파일 접근 승인 토큰이 필요합니다.")
|
|
267
|
-
normalized = self.normalize_local_path_for_approval(path)
|
|
268
|
-
self.ensure_path_allowed(normalized, action=action)
|
|
269
|
-
now = time.time()
|
|
270
|
-
key = self.token_hash(token)
|
|
271
|
-
with self.local_approval_lock:
|
|
272
|
-
expired = [approval_key for approval_key, value in self.local_approvals.items() if float(value.get("expires_at", 0)) < now]
|
|
273
|
-
for expired_key in expired:
|
|
274
|
-
self.local_approvals.pop(expired_key, None)
|
|
275
|
-
record = self.local_approvals.get(key)
|
|
276
|
-
if not record:
|
|
277
|
-
raise HTTPException(status_code=403, detail="파일 접근 승인이 만료되었거나 유효하지 않습니다.")
|
|
278
|
-
if not record.get("approved"):
|
|
279
|
-
raise HTTPException(status_code=403, detail="파일 접근이 아직 승인되지 않았습니다. Discord 또는 UI에서 승인해주세요.")
|
|
280
|
-
if record.get("user_email") != user_email:
|
|
281
|
-
raise HTTPException(status_code=403, detail="다른 사용자의 파일 접근 승인은 사용할 수 없습니다.")
|
|
282
|
-
if record.get("path") != normalized or record.get("action") != action:
|
|
283
|
-
raise HTTPException(status_code=403, detail="파일 접근 승인 범위가 일치하지 않습니다.")
|
|
284
|
-
if action == "write" and record.get("content_hash") != self.content_fingerprint(content):
|
|
285
|
-
raise HTTPException(status_code=403, detail="승인된 파일 내용과 요청 내용이 다릅니다.")
|
|
286
|
-
|
|
287
|
-
def check_permission_auth(self, request: Request, token: Optional[str] = None) -> None:
|
|
288
|
-
"""Authorize an approval decision.
|
|
289
|
-
|
|
290
|
-
Possession of the approval token is deliberately *not* sufficient:
|
|
291
|
-
the requester receives that token in order to poll status, so allowing
|
|
292
|
-
token ownership here would let them approve their own request.
|
|
293
|
-
"""
|
|
294
|
-
_ = token # retained for compatibility with existing call sites
|
|
295
|
-
if self.permission_monitor_secret:
|
|
296
|
-
auth_header = request.headers.get("Authorization", "")
|
|
297
|
-
expected = f"Bearer {self.permission_monitor_secret}"
|
|
298
|
-
if secrets.compare_digest(auth_header, expected):
|
|
299
|
-
return
|
|
300
|
-
self.require_admin(request)
|
|
301
|
-
|
|
302
|
-
def require_permission_status_owner(self, *, token: str, user_email: str) -> None:
|
|
303
|
-
"""Allow approval-status polling only to the original requester."""
|
|
304
|
-
key = self.token_hash(token)
|
|
305
|
-
with self.local_approval_lock:
|
|
306
|
-
record = self.local_approvals.get(key)
|
|
307
|
-
# Missing/expired tokens intentionally retain the generic status
|
|
308
|
-
# response. For a live request, however, a different account must not
|
|
309
|
-
# learn or poll its state even if it obtains the URL.
|
|
310
|
-
if record and record.get("user_email") != user_email:
|
|
311
|
-
raise HTTPException(status_code=403, detail="다른 사용자의 승인 상태는 조회할 수 없습니다.")
|
|
312
|
-
|
|
313
|
-
def ensure_path_allowed(self, path: str, *, action: str) -> None:
|
|
314
|
-
if action != "write":
|
|
315
|
-
return
|
|
316
|
-
normalized = self.normalize_local_path_for_approval(path)
|
|
317
|
-
from latticeai.services.tool_dispatch import LOCAL_WRITE_BLOCKED_PREFIXES
|
|
318
|
-
|
|
319
|
-
for prefix in LOCAL_WRITE_BLOCKED_PREFIXES:
|
|
320
|
-
normalized_prefix = str(Path(prefix).expanduser()).rstrip("/")
|
|
321
|
-
if normalized == normalized_prefix or normalized.startswith(normalized_prefix + "/"):
|
|
322
|
-
raise HTTPException(status_code=403, detail=f"쓰기 금지 경로입니다: {prefix}")
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
def create_permissions_router(
|
|
326
|
-
*,
|
|
327
|
-
config,
|
|
328
|
-
data_dir: Path,
|
|
329
|
-
require_user,
|
|
330
|
-
require_admin,
|
|
331
|
-
get_current_user,
|
|
332
|
-
) -> Tuple[APIRouter, PermissionGateway]:
|
|
333
|
-
router = APIRouter()
|
|
334
|
-
gateway = PermissionGateway(
|
|
335
|
-
config=config,
|
|
336
|
-
data_dir=data_dir,
|
|
337
|
-
require_admin=require_admin,
|
|
338
|
-
get_current_user=get_current_user,
|
|
339
|
-
)
|
|
340
|
-
|
|
341
|
-
@router.get("/permissions/pending")
|
|
342
|
-
async def permissions_pending(request: Request):
|
|
343
|
-
require_admin(request)
|
|
344
|
-
now = time.time()
|
|
345
|
-
with gateway.local_approval_lock:
|
|
346
|
-
result = {}
|
|
347
|
-
for token_hash, rec in list(gateway.local_approvals.items()):
|
|
348
|
-
expires_at = float(rec.get("expires_at", 0))
|
|
349
|
-
if expires_at < now:
|
|
350
|
-
continue
|
|
351
|
-
result[str(rec.get("token_hint") or token_hash[:8])] = {
|
|
352
|
-
"path": rec.get("path"),
|
|
353
|
-
"action": rec.get("action"),
|
|
354
|
-
"action_label": _PERMISSION_ACTION_LABELS.get(str(rec.get("action", "")), str(rec.get("action", ""))),
|
|
355
|
-
"user_email": rec.get("user_email"),
|
|
356
|
-
"approved": bool(rec.get("approved")),
|
|
357
|
-
"expires_in": round(expires_at - now),
|
|
358
|
-
}
|
|
359
|
-
return {"pending": result, "count": len(result)}
|
|
360
|
-
|
|
361
|
-
@router.post("/permissions/approve/{token}")
|
|
362
|
-
async def permissions_approve(token: str, request: Request):
|
|
363
|
-
gateway.check_permission_auth(request, token)
|
|
364
|
-
key = gateway.resolve_approval_key(token)
|
|
365
|
-
with gateway.local_approval_lock:
|
|
366
|
-
record = gateway.local_approvals.get(key)
|
|
367
|
-
if not record:
|
|
368
|
-
raise HTTPException(status_code=404, detail="토큰이 없거나 만료되었습니다.")
|
|
369
|
-
if float(record.get("expires_at", 0)) < time.time():
|
|
370
|
-
gateway.local_approvals.pop(key, None)
|
|
371
|
-
raise HTTPException(status_code=410, detail="토큰이 만료되었습니다.")
|
|
372
|
-
record["approved"] = True
|
|
373
|
-
gateway._perm_queue_remove_key(key)
|
|
374
|
-
logging.info(
|
|
375
|
-
"Permission approved: token=%s path=%s action=%s user=%s",
|
|
376
|
-
gateway.token_hint(token),
|
|
377
|
-
record.get("path"),
|
|
378
|
-
record.get("action"),
|
|
379
|
-
record.get("user_email"),
|
|
380
|
-
)
|
|
381
|
-
return {
|
|
382
|
-
"ok": True,
|
|
383
|
-
"token_hint": gateway.token_hint(token),
|
|
384
|
-
"path": record.get("path"),
|
|
385
|
-
"action": record.get("action"),
|
|
386
|
-
"user_email": record.get("user_email"),
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
@router.post("/permissions/deny/{token}")
|
|
390
|
-
async def permissions_deny(token: str, request: Request):
|
|
391
|
-
gateway.check_permission_auth(request, token)
|
|
392
|
-
key = gateway.resolve_approval_key(token)
|
|
393
|
-
with gateway.local_approval_lock:
|
|
394
|
-
record = gateway.local_approvals.pop(key, None)
|
|
395
|
-
gateway._perm_queue_remove_key(key)
|
|
396
|
-
if not record:
|
|
397
|
-
raise HTTPException(status_code=404, detail="토큰이 없거나 이미 처리되었습니다.")
|
|
398
|
-
logging.info(
|
|
399
|
-
"Permission denied: token=%s path=%s action=%s user=%s",
|
|
400
|
-
gateway.token_hint(token),
|
|
401
|
-
record.get("path"),
|
|
402
|
-
record.get("action"),
|
|
403
|
-
record.get("user_email"),
|
|
404
|
-
)
|
|
405
|
-
return {
|
|
406
|
-
"ok": True,
|
|
407
|
-
"denied": True,
|
|
408
|
-
"token_hint": gateway.token_hint(token),
|
|
409
|
-
"path": record.get("path"),
|
|
410
|
-
"action": record.get("action"),
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
@router.get("/permissions/status/{token}")
|
|
414
|
-
async def permissions_status(token: str, request: Request):
|
|
415
|
-
current_user = require_user(request)
|
|
416
|
-
gateway.require_permission_status_owner(token=token, user_email=current_user)
|
|
417
|
-
now = time.time()
|
|
418
|
-
key = gateway.token_hash(token)
|
|
419
|
-
with gateway.local_approval_lock:
|
|
420
|
-
record = gateway.local_approvals.get(key)
|
|
421
|
-
if not record:
|
|
422
|
-
return {"status": "denied_or_expired", "token_hint": gateway.token_hint(token)}
|
|
423
|
-
if float(record.get("expires_at", 0)) < now:
|
|
424
|
-
return {"status": "expired", "token_hint": gateway.token_hint(token)}
|
|
425
|
-
if record.get("approved"):
|
|
426
|
-
return {"status": "approved", "token_hint": gateway.token_hint(token)}
|
|
427
|
-
return {
|
|
428
|
-
"status": "pending",
|
|
429
|
-
"token_hint": gateway.token_hint(token),
|
|
430
|
-
"expires_in": round(float(record.get("expires_at", 0)) - now),
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
return router, gateway
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
__all__ = ["PermissionGateway", "create_permissions_router"]
|
package/latticeai/api/plugins.py
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
"""Plugin SDK API router (v2).
|
|
2
|
-
|
|
3
|
-
Surfaces the :class:`latticeai.core.plugins.PluginRegistry` over HTTP using the
|
|
4
|
-
same router-factory convention as the rest of ``latticeai.api`` (server_app
|
|
5
|
-
constructs the dependencies and passes them in; this module never imports the
|
|
6
|
-
app). New paths are namespaced under ``/plugins/registry`` and friends so they
|
|
7
|
-
do not collide with the pre-existing ``/plugins/directory`` marketplace routes.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
from typing import Any, Callable, Dict, Optional
|
|
14
|
-
|
|
15
|
-
from fastapi import APIRouter, HTTPException, Request
|
|
16
|
-
from pydantic import BaseModel
|
|
17
|
-
|
|
18
|
-
from latticeai.api.ui_redirects import app_redirect
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class PluginActionRequest(BaseModel):
|
|
22
|
-
plugin_id: str
|
|
23
|
-
enabled: Optional[bool] = None
|
|
24
|
-
version: Optional[str] = None
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class PluginValidateRequest(BaseModel):
|
|
28
|
-
manifest: Dict[str, Any] = {}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class PluginExecuteRequest(BaseModel):
|
|
32
|
-
plugin_id: str
|
|
33
|
-
action: str
|
|
34
|
-
args: Dict[str, Any] = {}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def create_plugins_router(
|
|
38
|
-
*,
|
|
39
|
-
registry,
|
|
40
|
-
require_user: Callable[[Request], str],
|
|
41
|
-
require_admin: Callable[[Request], Any],
|
|
42
|
-
append_audit_event: Callable[..., None],
|
|
43
|
-
gate_write: Optional[Callable[[Request], Optional[str]]] = None,
|
|
44
|
-
register_skill: Optional[Callable[[str, str], Any]] = None,
|
|
45
|
-
plugin_runners_factory: Optional[
|
|
46
|
-
Callable[[str, Optional[str]], Dict[str, Callable[..., Any]]]
|
|
47
|
-
] = None,
|
|
48
|
-
ui_file_response: Optional[Callable[[Path], Any]] = None,
|
|
49
|
-
static_dir: Optional[Path] = None,
|
|
50
|
-
) -> APIRouter:
|
|
51
|
-
from latticeai.core.plugins import validate_manifest
|
|
52
|
-
|
|
53
|
-
router = APIRouter()
|
|
54
|
-
|
|
55
|
-
@router.get("/plugins/sdk")
|
|
56
|
-
async def plugins_sdk_page(request: Request):
|
|
57
|
-
require_user(request)
|
|
58
|
-
return app_redirect("marketplace", request)
|
|
59
|
-
|
|
60
|
-
@router.get("/plugins/registry")
|
|
61
|
-
async def plugins_registry(request: Request):
|
|
62
|
-
require_user(request)
|
|
63
|
-
return registry.catalog()
|
|
64
|
-
|
|
65
|
-
@router.get("/plugins/registry/{plugin_id}")
|
|
66
|
-
async def plugin_detail(plugin_id: str, request: Request):
|
|
67
|
-
require_user(request)
|
|
68
|
-
manifest = registry.get_manifest(plugin_id)
|
|
69
|
-
if manifest is None:
|
|
70
|
-
raise HTTPException(status_code=404, detail=f"Plugin not found: {plugin_id}")
|
|
71
|
-
state = registry.store.list_plugin_registry().get(plugin_id, {}) if registry.store else {}
|
|
72
|
-
return {"plugin": manifest.public(), "registry": state}
|
|
73
|
-
|
|
74
|
-
@router.post("/plugins/validate")
|
|
75
|
-
async def plugin_validate(req: PluginValidateRequest, request: Request):
|
|
76
|
-
require_user(request)
|
|
77
|
-
manifest, errors = validate_manifest(req.manifest)
|
|
78
|
-
return {"ok": not errors, "errors": errors, "manifest": manifest.public() if manifest else None}
|
|
79
|
-
|
|
80
|
-
@router.post("/plugins/install")
|
|
81
|
-
async def plugin_install(req: PluginActionRequest, request: Request):
|
|
82
|
-
admin_email, _ = require_admin(request)
|
|
83
|
-
try:
|
|
84
|
-
result = registry.install(req.plugin_id, register_skill=register_skill)
|
|
85
|
-
except Exception as exc:
|
|
86
|
-
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
87
|
-
append_audit_event("plugin_install", user_email=admin_email, plugin=req.plugin_id)
|
|
88
|
-
return result
|
|
89
|
-
|
|
90
|
-
@router.post("/plugins/uninstall")
|
|
91
|
-
async def plugin_uninstall(req: PluginActionRequest, request: Request):
|
|
92
|
-
admin_email, _ = require_admin(request)
|
|
93
|
-
result = registry.uninstall(req.plugin_id)
|
|
94
|
-
append_audit_event("plugin_uninstall", user_email=admin_email, plugin=req.plugin_id)
|
|
95
|
-
return result
|
|
96
|
-
|
|
97
|
-
@router.post("/plugins/enable")
|
|
98
|
-
async def plugin_enable(req: PluginActionRequest, request: Request):
|
|
99
|
-
admin_email, _ = require_admin(request)
|
|
100
|
-
plugin = registry.set_enabled(req.plugin_id, True)
|
|
101
|
-
append_audit_event("plugin_enable", user_email=admin_email, plugin=req.plugin_id)
|
|
102
|
-
return {"plugin": plugin}
|
|
103
|
-
|
|
104
|
-
@router.post("/plugins/disable")
|
|
105
|
-
async def plugin_disable(req: PluginActionRequest, request: Request):
|
|
106
|
-
admin_email, _ = require_admin(request)
|
|
107
|
-
plugin = registry.set_enabled(req.plugin_id, False)
|
|
108
|
-
append_audit_event("plugin_disable", user_email=admin_email, plugin=req.plugin_id)
|
|
109
|
-
return {"plugin": plugin}
|
|
110
|
-
|
|
111
|
-
@router.post("/plugins/execute")
|
|
112
|
-
async def plugin_execute(req: PluginExecuteRequest, request: Request):
|
|
113
|
-
current_user = require_user(request)
|
|
114
|
-
scope = gate_write(request) if gate_write is not None else None
|
|
115
|
-
runners = plugin_runners_factory(current_user, scope) if plugin_runners_factory else {}
|
|
116
|
-
result = registry.execute_action(
|
|
117
|
-
req.plugin_id,
|
|
118
|
-
req.action,
|
|
119
|
-
req.args,
|
|
120
|
-
runners=runners,
|
|
121
|
-
workspace_id=scope,
|
|
122
|
-
)
|
|
123
|
-
append_audit_event("plugin_execute", user_email=current_user, plugin=req.plugin_id, action=req.action, status=result.status)
|
|
124
|
-
return result.as_dict()
|
|
125
|
-
|
|
126
|
-
return router
|