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
package/latticeai/api/agents.py
DELETED
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
"""Multi-Agent Runtime API router (v2).
|
|
2
|
-
|
|
3
|
-
Exposes the built-in agent roles and an orchestrated run endpoint that connects
|
|
4
|
-
to Workspace, Memory, Knowledge Graph, Workflow runs, and the Timeline. Paths
|
|
5
|
-
are namespaced under ``/agents`` (plural) so they never collide with the
|
|
6
|
-
existing single-agent ``/agent`` endpoints.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
from typing import Any, Callable, Dict, List, Optional
|
|
13
|
-
|
|
14
|
-
from fastapi import APIRouter, HTTPException, Request
|
|
15
|
-
from pydantic import BaseModel
|
|
16
|
-
|
|
17
|
-
from latticeai.api.ui_redirects import app_redirect
|
|
18
|
-
|
|
19
|
-
_CORE_EXECUTION_ROLES = ["planner", "executor", "reviewer"]
|
|
20
|
-
_MEMORY_GROUNDED_ROLES = ["researcher", *_CORE_EXECUTION_ROLES]
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def _memory_grounded_roles(roles: List[str]) -> Optional[List[str]]:
|
|
24
|
-
"""Ground standard user-initiated agent runs in Brain recall first.
|
|
25
|
-
|
|
26
|
-
Explicit specialist/custom pipelines keep their requested shape. The
|
|
27
|
-
desktop Brain and Work surfaces both send the historical three-role core
|
|
28
|
-
sequence, so normalizing it at the API boundary upgrades existing clients
|
|
29
|
-
without a frontend-only compatibility branch.
|
|
30
|
-
"""
|
|
31
|
-
requested = list(roles or _CORE_EXECUTION_ROLES)
|
|
32
|
-
if requested == _CORE_EXECUTION_ROLES:
|
|
33
|
-
return list(_MEMORY_GROUNDED_ROLES)
|
|
34
|
-
return requested or None
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
class AgentRunRequest(BaseModel):
|
|
38
|
-
goal: str
|
|
39
|
-
roles: List[str] = []
|
|
40
|
-
inputs: Dict[str, Any] = {}
|
|
41
|
-
max_retries: int = 2
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class MemorySnapshotRequest(BaseModel):
|
|
45
|
-
label: str = "agent memory snapshot"
|
|
46
|
-
reason: str = ""
|
|
47
|
-
memory_ids: List[str] = []
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def create_agents_router(
|
|
51
|
-
*,
|
|
52
|
-
store,
|
|
53
|
-
orchestrator_factory: Callable[[Optional[str], Optional[str]], Any],
|
|
54
|
-
require_user: Callable[[Request], str],
|
|
55
|
-
get_current_user: Callable[[Request], Optional[str]],
|
|
56
|
-
gate_read: Callable[[Request], Optional[str]],
|
|
57
|
-
gate_write: Callable[[Request], Optional[str]],
|
|
58
|
-
workspace_graph: Callable[[], Any],
|
|
59
|
-
append_audit_event: Callable[..., None],
|
|
60
|
-
ui_file_response: Optional[Callable[[Path], Any]] = None,
|
|
61
|
-
static_dir: Optional[Path] = None,
|
|
62
|
-
agent_runtime: Any = None,
|
|
63
|
-
run_executor: Any = None,
|
|
64
|
-
) -> APIRouter:
|
|
65
|
-
from lattice_brain.runtime.agent_runtime import (
|
|
66
|
-
AgentRuntime,
|
|
67
|
-
AgentRuntimeUnavailable,
|
|
68
|
-
)
|
|
69
|
-
from lattice_brain.runtime.multi_agent import AGENT_ROLES, ROLE_AGENT_IDS
|
|
70
|
-
|
|
71
|
-
# Single AgentRuntime boundary: the router (and via it, the frontend) talks
|
|
72
|
-
# to this façade instead of reaching into the orchestrator/store directly.
|
|
73
|
-
runtime = agent_runtime or AgentRuntime(
|
|
74
|
-
store=store,
|
|
75
|
-
orchestrator_factory=orchestrator_factory,
|
|
76
|
-
workspace_graph=workspace_graph,
|
|
77
|
-
append_audit_event=append_audit_event,
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
router = APIRouter()
|
|
81
|
-
|
|
82
|
-
# ── AgentRuntime boundary endpoints ───────────────────────────────────
|
|
83
|
-
@router.get("/agents/api/runtime/status")
|
|
84
|
-
async def agent_runtime_status(request: Request):
|
|
85
|
-
require_user(request)
|
|
86
|
-
scope = gate_read(request)
|
|
87
|
-
return runtime.status(scope=scope)
|
|
88
|
-
|
|
89
|
-
@router.get("/agents/api/runtime/health")
|
|
90
|
-
async def agent_runtime_health(request: Request):
|
|
91
|
-
require_user(request)
|
|
92
|
-
return runtime.health()
|
|
93
|
-
|
|
94
|
-
@router.get("/agents/api/runtime/config")
|
|
95
|
-
async def agent_runtime_config(request: Request):
|
|
96
|
-
require_user(request)
|
|
97
|
-
return runtime.config()
|
|
98
|
-
|
|
99
|
-
@router.get("/agents/api/runs/{run_id}/events")
|
|
100
|
-
async def agent_run_events(run_id: str, request: Request):
|
|
101
|
-
require_user(request)
|
|
102
|
-
scope = gate_read(request)
|
|
103
|
-
try:
|
|
104
|
-
return runtime.events(run_id, scope=scope)
|
|
105
|
-
except FileNotFoundError as exc:
|
|
106
|
-
raise HTTPException(status_code=404, detail=f"Agent run not found: {run_id}") from exc
|
|
107
|
-
|
|
108
|
-
@router.post("/agents/api/runs/{run_id}/stop")
|
|
109
|
-
async def agent_run_stop(run_id: str, request: Request):
|
|
110
|
-
require_user(request)
|
|
111
|
-
scope = gate_write(request)
|
|
112
|
-
return runtime.stop(run_id, scope=scope)
|
|
113
|
-
|
|
114
|
-
@router.get("/agents")
|
|
115
|
-
async def agents_page(request: Request):
|
|
116
|
-
require_user(request)
|
|
117
|
-
return app_redirect("agents", request)
|
|
118
|
-
|
|
119
|
-
@router.get("/agents/api/roles")
|
|
120
|
-
async def agent_roles(request: Request):
|
|
121
|
-
require_user(request)
|
|
122
|
-
return {
|
|
123
|
-
"roles": [
|
|
124
|
-
{"role": role, "agent_id": ROLE_AGENT_IDS.get(role, f"agent:{role}")}
|
|
125
|
-
for role in AGENT_ROLES
|
|
126
|
-
],
|
|
127
|
-
"default_pipeline": ["planner", "executor", "reviewer"],
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
@router.get("/agents/api/runs")
|
|
131
|
-
async def agent_runs(request: Request):
|
|
132
|
-
require_user(request)
|
|
133
|
-
scope = gate_read(request)
|
|
134
|
-
return runtime.list_runs(scope=scope)
|
|
135
|
-
|
|
136
|
-
@router.get("/agents/api/handoffs")
|
|
137
|
-
async def agent_handoffs(request: Request, run_id: str = ""):
|
|
138
|
-
require_user(request)
|
|
139
|
-
scope = gate_read(request)
|
|
140
|
-
return store.list_handoffs(workspace_id=scope, run_id=run_id or None)
|
|
141
|
-
|
|
142
|
-
@router.get("/agents/api/runs/{run_id}")
|
|
143
|
-
async def agent_run_detail(run_id: str, request: Request):
|
|
144
|
-
require_user(request)
|
|
145
|
-
scope = gate_read(request)
|
|
146
|
-
try:
|
|
147
|
-
return runtime.get_run(run_id, scope=scope)
|
|
148
|
-
except FileNotFoundError as exc:
|
|
149
|
-
raise HTTPException(status_code=404, detail=f"Agent run not found: {run_id}") from exc
|
|
150
|
-
|
|
151
|
-
@router.get("/agents/api/runs/{run_id}/replay")
|
|
152
|
-
async def agent_run_replay(run_id: str, request: Request):
|
|
153
|
-
require_user(request)
|
|
154
|
-
scope = gate_read(request)
|
|
155
|
-
try:
|
|
156
|
-
return runtime.replay(run_id, scope=scope)
|
|
157
|
-
except FileNotFoundError as exc:
|
|
158
|
-
raise HTTPException(status_code=404, detail=f"Agent run not found: {run_id}") from exc
|
|
159
|
-
|
|
160
|
-
@router.get("/agents/api/memory/snapshots")
|
|
161
|
-
async def agent_memory_snapshots(request: Request, limit: int = 50):
|
|
162
|
-
require_user(request)
|
|
163
|
-
scope = gate_read(request)
|
|
164
|
-
return store.list_memory_snapshots(workspace_id=scope, limit=limit)
|
|
165
|
-
|
|
166
|
-
@router.post("/agents/api/memory/snapshots")
|
|
167
|
-
async def agent_memory_snapshot(req: MemorySnapshotRequest, request: Request):
|
|
168
|
-
current_user = require_user(request)
|
|
169
|
-
scope = gate_write(request)
|
|
170
|
-
snapshot = store.create_memory_snapshot(
|
|
171
|
-
label=req.label,
|
|
172
|
-
reason=req.reason,
|
|
173
|
-
memory_ids=req.memory_ids or None,
|
|
174
|
-
user_email=current_user or None,
|
|
175
|
-
workspace_id=scope,
|
|
176
|
-
)
|
|
177
|
-
return {"snapshot": snapshot}
|
|
178
|
-
|
|
179
|
-
@router.post("/agents/api/run")
|
|
180
|
-
async def agent_run(req: AgentRunRequest, request: Request):
|
|
181
|
-
current_user = require_user(request)
|
|
182
|
-
scope = gate_write(request)
|
|
183
|
-
grounded_roles = _memory_grounded_roles(req.roles)
|
|
184
|
-
try:
|
|
185
|
-
if run_executor is not None:
|
|
186
|
-
return await run_executor.start_agent(
|
|
187
|
-
req.goal,
|
|
188
|
-
user_email=current_user or None,
|
|
189
|
-
scope=scope,
|
|
190
|
-
roles=grounded_roles,
|
|
191
|
-
inputs=req.inputs,
|
|
192
|
-
max_retries=req.max_retries,
|
|
193
|
-
)
|
|
194
|
-
# Worker thread: an LLM-backed run blocks on model generation and
|
|
195
|
-
# must not stall the event loop (the sync model bridge also
|
|
196
|
-
# requires a loop-free thread).
|
|
197
|
-
import asyncio
|
|
198
|
-
|
|
199
|
-
return await asyncio.to_thread(
|
|
200
|
-
runtime.start,
|
|
201
|
-
req.goal,
|
|
202
|
-
user_email=current_user or None,
|
|
203
|
-
scope=scope,
|
|
204
|
-
roles=grounded_roles,
|
|
205
|
-
inputs=req.inputs,
|
|
206
|
-
max_retries=req.max_retries,
|
|
207
|
-
)
|
|
208
|
-
except ValueError as exc:
|
|
209
|
-
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
210
|
-
except AgentRuntimeUnavailable as exc:
|
|
211
|
-
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
|
212
|
-
except PermissionError as exc:
|
|
213
|
-
# A pre_run hook gated this run (e.g. a policy/permission hook).
|
|
214
|
-
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
|
215
|
-
|
|
216
|
-
@router.post("/agents/api/run/preview")
|
|
217
|
-
async def agent_run_preview(req: AgentRunRequest, request: Request):
|
|
218
|
-
require_user(request)
|
|
219
|
-
scope = gate_read(request)
|
|
220
|
-
return runtime.preview(
|
|
221
|
-
req.goal,
|
|
222
|
-
scope=scope,
|
|
223
|
-
roles=_memory_grounded_roles(req.roles),
|
|
224
|
-
inputs=req.inputs,
|
|
225
|
-
max_retries=req.max_retries,
|
|
226
|
-
)
|
|
227
|
-
|
|
228
|
-
return router
|
package/latticeai/api/auth.py
DELETED
|
@@ -1,383 +0,0 @@
|
|
|
1
|
-
"""Authentication API router: register, login, logout, SSO, profile."""
|
|
2
|
-
|
|
3
|
-
import base64
|
|
4
|
-
import hashlib
|
|
5
|
-
import logging
|
|
6
|
-
import secrets
|
|
7
|
-
import time
|
|
8
|
-
from dataclasses import dataclass
|
|
9
|
-
from typing import Any, Awaitable, Callable, Dict, Optional
|
|
10
|
-
from urllib.parse import urlencode
|
|
11
|
-
|
|
12
|
-
from fastapi import APIRouter, Request
|
|
13
|
-
from fastapi.responses import JSONResponse, RedirectResponse
|
|
14
|
-
from pydantic import BaseModel
|
|
15
|
-
|
|
16
|
-
from latticeai.core.config import SSO_CALLBACK_PATH
|
|
17
|
-
from latticeai.core.http_origin import request_external_origin
|
|
18
|
-
from latticeai.core.messages import DEFAULT_LANGUAGE, http_error, resolve_language
|
|
19
|
-
from latticeai.core.oidc import (
|
|
20
|
-
OIDCValidationError,
|
|
21
|
-
)
|
|
22
|
-
from latticeai.core.oidc import (
|
|
23
|
-
fetch_jwks as _default_fetch_jwks,
|
|
24
|
-
)
|
|
25
|
-
from latticeai.core.oidc import (
|
|
26
|
-
verify_id_token as _default_verify_id_token,
|
|
27
|
-
)
|
|
28
|
-
from latticeai.core.users import normalize_email
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class UserRegister(BaseModel):
|
|
32
|
-
email: str
|
|
33
|
-
password: str
|
|
34
|
-
name: str
|
|
35
|
-
nickname: str
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
class UserLogin(BaseModel):
|
|
39
|
-
email: str
|
|
40
|
-
password: str
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class ChangePasswordRequest(BaseModel):
|
|
44
|
-
current_password: str
|
|
45
|
-
new_password: str
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
class UpdateProfileRequest(BaseModel):
|
|
49
|
-
name: Optional[str] = None
|
|
50
|
-
nickname: Optional[str] = None
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
@dataclass(frozen=True)
|
|
54
|
-
class _SSOLoginState:
|
|
55
|
-
"""Server-side SSO transaction state consumed exactly once."""
|
|
56
|
-
|
|
57
|
-
issued_at: float
|
|
58
|
-
nonce: str
|
|
59
|
-
code_verifier: str
|
|
60
|
-
invite_authorized: bool
|
|
61
|
-
#: Exactly what was sent to ``authorize``. The token exchange must repeat
|
|
62
|
-
#: it byte for byte or the provider rejects the code, so it is carried
|
|
63
|
-
#: rather than recomputed from a second request that may differ.
|
|
64
|
-
redirect_uri: str = ""
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
# The nonce and PKCE verifier bind the callback/token to this login attempt.
|
|
68
|
-
# The signed invite cookie is verified once at login and reduced to a
|
|
69
|
-
# server-side claim, so the callback never trusts a client-supplied invite bit.
|
|
70
|
-
_sso_states: Dict[str, _SSOLoginState] = {}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def create_auth_router(
|
|
74
|
-
*,
|
|
75
|
-
load_users: Callable[[], Dict],
|
|
76
|
-
save_users: Callable[[Dict], None],
|
|
77
|
-
hash_password: Callable[[str], str],
|
|
78
|
-
verify_and_migrate: Callable[[str, str, str, Dict], bool],
|
|
79
|
-
create_session: Callable[[str], str],
|
|
80
|
-
get_session_email: Callable[[str], Optional[str]],
|
|
81
|
-
invalidate_session: Callable[[str], None],
|
|
82
|
-
extract_bearer_token: Callable[[Request], Optional[str]],
|
|
83
|
-
get_user_role: Callable[[str, Optional[Dict]], str],
|
|
84
|
-
require_user: Callable[[Request], str],
|
|
85
|
-
check_ip_rate_limit: Callable[..., None],
|
|
86
|
-
client_ip: Callable[[Request], str],
|
|
87
|
-
get_sso_settings: Callable[[], Dict],
|
|
88
|
-
get_sso_discovery: Callable[[], Any],
|
|
89
|
-
public_sso_config: Callable[..., Dict],
|
|
90
|
-
open_registration: bool,
|
|
91
|
-
session_ttl: int,
|
|
92
|
-
require_auth: bool = True,
|
|
93
|
-
secure_cookies: bool = False,
|
|
94
|
-
ensure_identity: Optional[Callable[[str, Dict], None]] = None,
|
|
95
|
-
invite_gate_enabled: bool = False,
|
|
96
|
-
invite_authorized: Optional[Callable[[Request], bool]] = None,
|
|
97
|
-
default_redirect_uri: Optional[str] = None,
|
|
98
|
-
verify_id_token: Callable[..., Dict] = _default_verify_id_token,
|
|
99
|
-
fetch_jwks: Callable[[str], Awaitable[Dict]] = _default_fetch_jwks,
|
|
100
|
-
) -> APIRouter:
|
|
101
|
-
router = APIRouter()
|
|
102
|
-
|
|
103
|
-
def _enforce_password_policy(password: str, language: str = DEFAULT_LANGUAGE) -> None:
|
|
104
|
-
# Real policy (v4): length >= 8 with letters AND digits. A 4-char
|
|
105
|
-
# minimum was not a policy.
|
|
106
|
-
pw = str(password or "")
|
|
107
|
-
if len(pw) < 8 or not any(c.isalpha() for c in pw) or not any(c.isdigit() for c in pw):
|
|
108
|
-
raise http_error(400, "auth.password_too_weak", language)
|
|
109
|
-
|
|
110
|
-
@router.post("/register")
|
|
111
|
-
async def register(req: UserRegister, request: Request):
|
|
112
|
-
lang = resolve_language(request)
|
|
113
|
-
check_ip_rate_limit(client_ip(request), "register", max_calls=5, window_secs=3600)
|
|
114
|
-
invite_claim = bool(
|
|
115
|
-
invite_gate_enabled
|
|
116
|
-
and invite_authorized is not None
|
|
117
|
-
and invite_authorized(request)
|
|
118
|
-
)
|
|
119
|
-
if invite_gate_enabled and not invite_claim:
|
|
120
|
-
raise http_error(403, "auth.invitation_required", lang)
|
|
121
|
-
# Closed public registration stays closed unless the operator enabled
|
|
122
|
-
# the invite gate and this exact request carries a valid signed claim.
|
|
123
|
-
if not open_registration and not invite_claim:
|
|
124
|
-
raise http_error(403, "auth.registration_disabled", lang)
|
|
125
|
-
_enforce_password_policy(req.password, lang)
|
|
126
|
-
email = normalize_email(req.email)
|
|
127
|
-
users = load_users()
|
|
128
|
-
if email in users:
|
|
129
|
-
raise http_error(400, "auth.email_taken", lang)
|
|
130
|
-
role = "admin" if not users else "user"
|
|
131
|
-
users[email] = {
|
|
132
|
-
"password": hash_password(req.password),
|
|
133
|
-
"name": req.name,
|
|
134
|
-
"nickname": req.nickname,
|
|
135
|
-
"role": role,
|
|
136
|
-
"disabled": False,
|
|
137
|
-
}
|
|
138
|
-
if ensure_identity is not None:
|
|
139
|
-
ensure_identity(email, users[email])
|
|
140
|
-
save_users(users)
|
|
141
|
-
msg = "회원가입 성공! 첫 번째 사용자로 관리자 권한이 부여되었습니다." if role == "admin" else "회원가입 성공!"
|
|
142
|
-
return {"status": "ok", "message": msg, "role": role}
|
|
143
|
-
|
|
144
|
-
@router.post("/login")
|
|
145
|
-
async def login(req: UserLogin, request: Request):
|
|
146
|
-
lang = resolve_language(request)
|
|
147
|
-
check_ip_rate_limit(client_ip(request), "login", max_calls=10, window_secs=300)
|
|
148
|
-
email = normalize_email(req.email)
|
|
149
|
-
users = load_users()
|
|
150
|
-
user = users.get(email)
|
|
151
|
-
if not user or not verify_and_migrate(email, req.password, user.get("password", ""), users):
|
|
152
|
-
raise http_error(401, "auth.bad_credentials", lang)
|
|
153
|
-
if user.get("disabled"):
|
|
154
|
-
raise http_error(403, "auth.account_disabled", lang)
|
|
155
|
-
role = get_user_role(email, users)
|
|
156
|
-
token = create_session(email)
|
|
157
|
-
response = JSONResponse(content={
|
|
158
|
-
"status": "ok",
|
|
159
|
-
"nickname": user["nickname"],
|
|
160
|
-
"name": user["name"],
|
|
161
|
-
"email": email,
|
|
162
|
-
"role": role,
|
|
163
|
-
"is_admin": role == "admin",
|
|
164
|
-
})
|
|
165
|
-
response.set_cookie(
|
|
166
|
-
key="session_token",
|
|
167
|
-
value=token,
|
|
168
|
-
httponly=True,
|
|
169
|
-
secure=secure_cookies,
|
|
170
|
-
samesite="lax",
|
|
171
|
-
max_age=session_ttl,
|
|
172
|
-
)
|
|
173
|
-
return response
|
|
174
|
-
|
|
175
|
-
@router.get("/auth/sso/config")
|
|
176
|
-
async def sso_config_endpoint():
|
|
177
|
-
return public_sso_config()
|
|
178
|
-
|
|
179
|
-
def _resolve_redirect_uri(request: Request, settings: Dict) -> str:
|
|
180
|
-
"""Where the provider should send the browser back to.
|
|
181
|
-
|
|
182
|
-
A configured URI is registered with the provider and is used verbatim.
|
|
183
|
-
Only the *built-in default* — which names the worker's own port, and is
|
|
184
|
-
therefore unreachable whenever a gateway fronts it — is replaced by the
|
|
185
|
-
front door this login actually arrived through.
|
|
186
|
-
"""
|
|
187
|
-
configured = str(settings.get("redirect_uri") or "")
|
|
188
|
-
if not default_redirect_uri or configured != default_redirect_uri:
|
|
189
|
-
return configured
|
|
190
|
-
origin = request_external_origin(request)
|
|
191
|
-
return f"{origin}{SSO_CALLBACK_PATH}" if origin else configured
|
|
192
|
-
|
|
193
|
-
@router.get("/auth/sso/login")
|
|
194
|
-
async def sso_login(request: Request):
|
|
195
|
-
lang = resolve_language(request)
|
|
196
|
-
settings = get_sso_settings()
|
|
197
|
-
discovery = await get_sso_discovery()
|
|
198
|
-
if not settings.get("enabled") or not discovery:
|
|
199
|
-
raise http_error(503, "sso.not_configured", lang)
|
|
200
|
-
state = secrets.token_urlsafe(16)
|
|
201
|
-
nonce = secrets.token_urlsafe(16)
|
|
202
|
-
# PKCE (S256): bind the token exchange to this login, so an
|
|
203
|
-
# intercepted authorization code is useless without the verifier.
|
|
204
|
-
code_verifier = secrets.token_urlsafe(48)
|
|
205
|
-
code_challenge = (
|
|
206
|
-
base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode("ascii")).digest())
|
|
207
|
-
.rstrip(b"=")
|
|
208
|
-
.decode("ascii")
|
|
209
|
-
)
|
|
210
|
-
invite_claim = bool(
|
|
211
|
-
not invite_gate_enabled
|
|
212
|
-
or (
|
|
213
|
-
invite_authorized is not None
|
|
214
|
-
and invite_authorized(request)
|
|
215
|
-
)
|
|
216
|
-
)
|
|
217
|
-
redirect_uri = _resolve_redirect_uri(request, settings)
|
|
218
|
-
_sso_states[state] = _SSOLoginState(
|
|
219
|
-
issued_at=time.time(),
|
|
220
|
-
nonce=nonce,
|
|
221
|
-
code_verifier=code_verifier,
|
|
222
|
-
invite_authorized=invite_claim,
|
|
223
|
-
redirect_uri=redirect_uri,
|
|
224
|
-
)
|
|
225
|
-
params = urlencode({
|
|
226
|
-
"client_id": settings["client_id"],
|
|
227
|
-
"response_type": "code",
|
|
228
|
-
"redirect_uri": redirect_uri,
|
|
229
|
-
"scope": settings.get("scopes") or "openid email profile",
|
|
230
|
-
"state": state,
|
|
231
|
-
"nonce": nonce,
|
|
232
|
-
"code_challenge": code_challenge,
|
|
233
|
-
"code_challenge_method": "S256",
|
|
234
|
-
})
|
|
235
|
-
return RedirectResponse(f"{discovery['authorization_endpoint']}?{params}")
|
|
236
|
-
|
|
237
|
-
@router.get("/auth/sso/callback")
|
|
238
|
-
async def sso_callback(request: Request, code: str = "", state: str = "", error: str = ""):
|
|
239
|
-
lang = resolve_language(request)
|
|
240
|
-
entry = _sso_states.pop(state, None)
|
|
241
|
-
if entry is None or time.time() - entry.issued_at > 300:
|
|
242
|
-
raise http_error(400, "sso.invalid_state", lang)
|
|
243
|
-
if error:
|
|
244
|
-
return RedirectResponse(f"/?{urlencode({'sso_error': error})}")
|
|
245
|
-
settings = get_sso_settings()
|
|
246
|
-
discovery = await get_sso_discovery()
|
|
247
|
-
if not settings.get("enabled") or not discovery:
|
|
248
|
-
raise http_error(503, "sso.config_error", lang)
|
|
249
|
-
import httpx as _httpx
|
|
250
|
-
async with _httpx.AsyncClient() as c:
|
|
251
|
-
r = await c.post(discovery["token_endpoint"], data={
|
|
252
|
-
"grant_type": "authorization_code",
|
|
253
|
-
"code": code,
|
|
254
|
-
"redirect_uri": entry.redirect_uri or settings["redirect_uri"],
|
|
255
|
-
"client_id": settings["client_id"],
|
|
256
|
-
"client_secret": settings["client_secret"],
|
|
257
|
-
"code_verifier": entry.code_verifier,
|
|
258
|
-
}, headers={"Accept": "application/json"}, timeout=15)
|
|
259
|
-
tokens = r.json()
|
|
260
|
-
id_token = tokens.get("id_token")
|
|
261
|
-
if not id_token:
|
|
262
|
-
raise http_error(400, "sso.no_id_token", lang)
|
|
263
|
-
# Never trust a decoded JWT payload: verify signature (against the
|
|
264
|
-
# provider JWKS), issuer, audience, expiry and the login nonce before
|
|
265
|
-
# using any claim. Any failure is fail-closed (401).
|
|
266
|
-
issuer = discovery.get("issuer") or ""
|
|
267
|
-
try:
|
|
268
|
-
jwks = await fetch_jwks(discovery.get("jwks_uri", ""))
|
|
269
|
-
payload = verify_id_token(
|
|
270
|
-
id_token,
|
|
271
|
-
jwks=jwks,
|
|
272
|
-
issuer=issuer,
|
|
273
|
-
audience=settings["client_id"],
|
|
274
|
-
nonce=entry.nonce,
|
|
275
|
-
)
|
|
276
|
-
except OIDCValidationError as exc:
|
|
277
|
-
logging.warning("SSO ID token rejected: %s", exc)
|
|
278
|
-
raise http_error(401, "sso.token_verification_failed", lang)
|
|
279
|
-
except Exception as exc: # discovery/JWKS fetch failure → fail closed
|
|
280
|
-
logging.warning("SSO token validation error: %s", exc)
|
|
281
|
-
raise http_error(502, "sso.provider_verification_failed", lang)
|
|
282
|
-
email = normalize_email(payload.get("email") or payload.get("preferred_username") or payload.get("upn") or "")
|
|
283
|
-
if not email:
|
|
284
|
-
raise http_error(400, "sso.email_unavailable", lang)
|
|
285
|
-
users = load_users()
|
|
286
|
-
if email not in users:
|
|
287
|
-
if invite_gate_enabled and not entry.invite_authorized:
|
|
288
|
-
raise http_error(403, "sso.invitation_required", lang)
|
|
289
|
-
is_first = len(users) == 0
|
|
290
|
-
users[email] = {
|
|
291
|
-
"password": "",
|
|
292
|
-
"name": payload.get("name", email.split("@")[0]),
|
|
293
|
-
"nickname": payload.get("given_name", email.split("@")[0]),
|
|
294
|
-
"role": "admin" if is_first else "user",
|
|
295
|
-
"disabled": False,
|
|
296
|
-
"sso": True,
|
|
297
|
-
}
|
|
298
|
-
if ensure_identity is not None:
|
|
299
|
-
ensure_identity(email, users[email])
|
|
300
|
-
save_users(users)
|
|
301
|
-
if users[email].get("disabled"):
|
|
302
|
-
raise http_error(403, "auth.account_disabled", lang)
|
|
303
|
-
token = create_session(email)
|
|
304
|
-
resp = RedirectResponse("/app", status_code=302)
|
|
305
|
-
resp.set_cookie(
|
|
306
|
-
"session_token",
|
|
307
|
-
token,
|
|
308
|
-
httponly=True,
|
|
309
|
-
secure=secure_cookies,
|
|
310
|
-
samesite="lax",
|
|
311
|
-
max_age=session_ttl,
|
|
312
|
-
)
|
|
313
|
-
return resp
|
|
314
|
-
|
|
315
|
-
@router.post("/logout")
|
|
316
|
-
async def logout(request: Request):
|
|
317
|
-
token = extract_bearer_token(request)
|
|
318
|
-
if token:
|
|
319
|
-
invalidate_session(token)
|
|
320
|
-
response = JSONResponse(content={"status": "ok"})
|
|
321
|
-
response.delete_cookie(
|
|
322
|
-
"session_token",
|
|
323
|
-
secure=secure_cookies,
|
|
324
|
-
httponly=True,
|
|
325
|
-
samesite="lax",
|
|
326
|
-
)
|
|
327
|
-
return response
|
|
328
|
-
|
|
329
|
-
@router.post("/account/change-password")
|
|
330
|
-
async def change_password(req: ChangePasswordRequest, request: Request):
|
|
331
|
-
lang = resolve_language(request)
|
|
332
|
-
email = normalize_email(require_user(request))
|
|
333
|
-
if not email:
|
|
334
|
-
raise http_error(401, "auth.login_required", lang)
|
|
335
|
-
_enforce_password_policy(req.new_password, lang)
|
|
336
|
-
users = load_users()
|
|
337
|
-
user = users.get(email)
|
|
338
|
-
if not user:
|
|
339
|
-
raise http_error(404, "auth.user_not_found", lang)
|
|
340
|
-
if not verify_and_migrate(email, req.current_password, user.get("password", ""), users):
|
|
341
|
-
raise http_error(401, "auth.current_password_wrong", lang)
|
|
342
|
-
users[email]["password"] = hash_password(req.new_password)
|
|
343
|
-
save_users(users)
|
|
344
|
-
return {"status": "ok", "message": "비밀번호가 변경되었습니다."}
|
|
345
|
-
|
|
346
|
-
@router.patch("/account/profile")
|
|
347
|
-
async def update_profile(req: UpdateProfileRequest, request: Request):
|
|
348
|
-
lang = resolve_language(request)
|
|
349
|
-
email = normalize_email(require_user(request))
|
|
350
|
-
if not email:
|
|
351
|
-
raise http_error(401, "auth.login_required", lang)
|
|
352
|
-
if req.name is not None and not req.name.strip():
|
|
353
|
-
raise http_error(400, "auth.name_required", lang)
|
|
354
|
-
if req.nickname is not None and not req.nickname.strip():
|
|
355
|
-
raise http_error(400, "auth.nickname_required", lang)
|
|
356
|
-
users = load_users()
|
|
357
|
-
user = users.get(email)
|
|
358
|
-
if not user:
|
|
359
|
-
raise http_error(404, "auth.user_not_found", lang)
|
|
360
|
-
if req.name is not None:
|
|
361
|
-
users[email]["name"] = req.name.strip()
|
|
362
|
-
if req.nickname is not None:
|
|
363
|
-
users[email]["nickname"] = req.nickname.strip()
|
|
364
|
-
save_users(users)
|
|
365
|
-
return {"status": "ok", "name": users[email]["name"], "nickname": users[email]["nickname"]}
|
|
366
|
-
|
|
367
|
-
@router.get("/account/profile")
|
|
368
|
-
async def get_profile(request: Request):
|
|
369
|
-
lang = resolve_language(request)
|
|
370
|
-
email = normalize_email(require_user(request))
|
|
371
|
-
if not email:
|
|
372
|
-
if require_auth:
|
|
373
|
-
raise http_error(401, "auth.login_required", lang)
|
|
374
|
-
return {"email": "", "name": "Local User", "nickname": "You", "role": "admin", "is_admin": True}
|
|
375
|
-
users = load_users()
|
|
376
|
-
user = users.get(email)
|
|
377
|
-
if not user:
|
|
378
|
-
raise http_error(404, "auth.user_not_found", lang)
|
|
379
|
-
role = get_user_role(email, users)
|
|
380
|
-
return {"email": email, "name": user.get("name", ""), "nickname": user.get("nickname", ""),
|
|
381
|
-
"role": role, "is_admin": role == "admin"}
|
|
382
|
-
|
|
383
|
-
return router
|