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,471 +0,0 @@
|
|
|
1
|
-
"""Change proposals — 수정/삭제는 제안으로, 생성은 바로 (v9.6.0).
|
|
2
|
-
|
|
3
|
-
The consent model most users actually want is git's: creating something new
|
|
4
|
-
is cheap and reversible, but changing or deleting what already exists
|
|
5
|
-
deserves a review. This service implements that model for the agent
|
|
6
|
-
workspace:
|
|
7
|
-
|
|
8
|
-
* **additive** operations (new files, new notes) run with minimal friction;
|
|
9
|
-
* **mutation/destructive** operations (overwrite, in-place edit, delete of
|
|
10
|
-
an existing file) are *staged as proposals* instead of applied: a review
|
|
11
|
-
item (source ``change_proposal``) carrying the target path, a unified
|
|
12
|
-
diff, the exact resulting content, and a small/large tier. Nothing changes
|
|
13
|
-
on disk until the user approves; rejecting discards the staged change.
|
|
14
|
-
|
|
15
|
-
Approving applies the staged content exactly as reviewed (never recomputed),
|
|
16
|
-
then marks the review item approved — so the review timeline doubles as the
|
|
17
|
-
change audit log. The classification itself lives in
|
|
18
|
-
:mod:`latticeai.core.tool_governor` so every entry point shares one policy.
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
from __future__ import annotations
|
|
22
|
-
|
|
23
|
-
import difflib
|
|
24
|
-
import logging
|
|
25
|
-
import os
|
|
26
|
-
import tempfile
|
|
27
|
-
import threading
|
|
28
|
-
from pathlib import Path
|
|
29
|
-
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
30
|
-
|
|
31
|
-
from latticeai.core.quiet import quiet
|
|
32
|
-
from latticeai.core.security import sha256_hex
|
|
33
|
-
from latticeai.core.tool_governor import classify_tool_call
|
|
34
|
-
from latticeai.core.workspace_reorganization import (
|
|
35
|
-
REORG_KIND,
|
|
36
|
-
apply_reorganization,
|
|
37
|
-
propose_reorganization,
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
LOGGER = logging.getLogger(__name__)
|
|
41
|
-
|
|
42
|
-
_MAX_STAGED_BYTES = 400_000
|
|
43
|
-
_MAX_DIFF_LINES = 400
|
|
44
|
-
_SMALL_TIER_DIFF_LINES = 40
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class ProposalConflictError(ValueError):
|
|
48
|
-
"""The target file changed (or the proposal was already resolved) between
|
|
49
|
-
staging and approval — applying now would silently destroy user edits.
|
|
50
|
-
|
|
51
|
-
Subclasses :class:`ValueError` deliberately: API surfaces that only know
|
|
52
|
-
``except ValueError`` degrade to a 4xx instead of applying or crashing,
|
|
53
|
-
while conflict-aware surfaces catch this class first and answer **409**
|
|
54
|
-
with a rebase hint.
|
|
55
|
-
"""
|
|
56
|
-
|
|
57
|
-
def __init__(
|
|
58
|
-
self,
|
|
59
|
-
*,
|
|
60
|
-
reason: str,
|
|
61
|
-
path: str,
|
|
62
|
-
kind: str,
|
|
63
|
-
base_sha256: str = "",
|
|
64
|
-
current_sha256: str = "",
|
|
65
|
-
rebase_hint: str = "",
|
|
66
|
-
) -> None:
|
|
67
|
-
self.reason = reason
|
|
68
|
-
self.path = path
|
|
69
|
-
self.kind = kind
|
|
70
|
-
self.base_sha256 = base_sha256
|
|
71
|
-
self.current_sha256 = current_sha256
|
|
72
|
-
self.rebase_hint = rebase_hint or (
|
|
73
|
-
"제안 생성 이후 파일 상태가 바뀌었습니다. 이 제안을 거부하고 "
|
|
74
|
-
"현재 파일 내용을 기준으로 제안을 다시 생성하세요."
|
|
75
|
-
)
|
|
76
|
-
super().__init__(f"change proposal conflict ({reason}): {path}")
|
|
77
|
-
|
|
78
|
-
def to_detail(self) -> Dict[str, Any]:
|
|
79
|
-
"""409 response body — no staged content, just what the UI needs."""
|
|
80
|
-
return {
|
|
81
|
-
"error": "change_proposal_conflict",
|
|
82
|
-
"conflict": True,
|
|
83
|
-
"reason": self.reason,
|
|
84
|
-
"path": self.path,
|
|
85
|
-
"kind": self.kind,
|
|
86
|
-
"base_sha256": self.base_sha256,
|
|
87
|
-
"current_sha256": self.current_sha256,
|
|
88
|
-
"rebase_hint": self.rebase_hint,
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def _sha256_text(content: str) -> str:
|
|
93
|
-
return sha256_hex(content)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def _unified_diff(before: str, after: str, path: str) -> List[str]:
|
|
97
|
-
lines = list(
|
|
98
|
-
difflib.unified_diff(
|
|
99
|
-
before.splitlines(), after.splitlines(),
|
|
100
|
-
fromfile=f"a/{path}", tofile=f"b/{path}", lineterm="",
|
|
101
|
-
)
|
|
102
|
-
)
|
|
103
|
-
return lines[:_MAX_DIFF_LINES]
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
class ChangeProposalService:
|
|
107
|
-
"""Stages mutation/destructive file changes as reviewable proposals."""
|
|
108
|
-
|
|
109
|
-
#: Tools whose plan-time approval is delegated to per-call governance.
|
|
110
|
-
governed_tools = frozenset({"write_file", "edit_file"})
|
|
111
|
-
|
|
112
|
-
def __init__(
|
|
113
|
-
self,
|
|
114
|
-
*,
|
|
115
|
-
review_queue: Any,
|
|
116
|
-
resolve_path: Callable[[str], Path],
|
|
117
|
-
audit: Optional[Callable[..., None]] = None,
|
|
118
|
-
) -> None:
|
|
119
|
-
self._review_queue = review_queue
|
|
120
|
-
self._resolve_path = resolve_path
|
|
121
|
-
self._audit = audit or (lambda *a, **kw: None)
|
|
122
|
-
# One protected section for status-check → conflict-check → apply →
|
|
123
|
-
# transition. In-process serialization is the right weight here: the
|
|
124
|
-
# review queue lives in this process and the write itself is atomic
|
|
125
|
-
# (temp file + os.replace), so a cross-process file lock adds nothing.
|
|
126
|
-
self._apply_lock = threading.Lock()
|
|
127
|
-
|
|
128
|
-
# ── classification / staging (agent-loop governor port) ─────────────
|
|
129
|
-
|
|
130
|
-
def review(
|
|
131
|
-
self,
|
|
132
|
-
name: str,
|
|
133
|
-
args: Dict[str, Any],
|
|
134
|
-
*,
|
|
135
|
-
policy: Optional[Dict[str, Any]] = None,
|
|
136
|
-
user_email: Optional[str] = None,
|
|
137
|
-
workspace_id: Optional[str] = None,
|
|
138
|
-
conversation_id: Optional[str] = None,
|
|
139
|
-
) -> Optional[Dict[str, Any]]:
|
|
140
|
-
"""Governor port for the agent loop.
|
|
141
|
-
|
|
142
|
-
Returns ``None`` to fall through to the existing gates, or a verdict:
|
|
143
|
-
``{"decision": "allow_additive"}`` (execute without extra approval) /
|
|
144
|
-
``{"decision": "proposed", "proposal": {...}}`` (staged for review).
|
|
145
|
-
"""
|
|
146
|
-
if name not in {"write_file", "edit_file"}:
|
|
147
|
-
return None
|
|
148
|
-
verdict = classify_tool_call(
|
|
149
|
-
name, args, policy=policy, path_exists=self._path_exists
|
|
150
|
-
)
|
|
151
|
-
if verdict["change_class"] == "additive":
|
|
152
|
-
return {"decision": "allow_additive", "classification": verdict}
|
|
153
|
-
if not verdict["proposal_required"]:
|
|
154
|
-
return None
|
|
155
|
-
|
|
156
|
-
path = str(args.get("path") or "")
|
|
157
|
-
after = self._staged_content(name, args)
|
|
158
|
-
if after is None:
|
|
159
|
-
# The edit cannot be computed deterministically (e.g. old_string
|
|
160
|
-
# not found) — let the normal tool path surface the real error.
|
|
161
|
-
return None
|
|
162
|
-
try:
|
|
163
|
-
proposal = self.propose_file_update(
|
|
164
|
-
path=path,
|
|
165
|
-
new_content=after,
|
|
166
|
-
proposed_by="agent",
|
|
167
|
-
reason=verdict["reason"],
|
|
168
|
-
user_email=user_email,
|
|
169
|
-
workspace_id=workspace_id,
|
|
170
|
-
context={
|
|
171
|
-
# Full provenance for the Review Center: which tool asked,
|
|
172
|
-
# how the governor classified it, and the policy risk.
|
|
173
|
-
"tool": name,
|
|
174
|
-
"change_class": verdict.get("change_class"),
|
|
175
|
-
"risk": (policy or {}).get("risk"),
|
|
176
|
-
"conversation_id": conversation_id,
|
|
177
|
-
"source_detail": "agent change governor",
|
|
178
|
-
},
|
|
179
|
-
)
|
|
180
|
-
except Exception:
|
|
181
|
-
LOGGER.exception("change proposal staging failed")
|
|
182
|
-
return None
|
|
183
|
-
return {"decision": "proposed", "classification": verdict, "proposal": proposal}
|
|
184
|
-
|
|
185
|
-
def _path_exists(self, path: str) -> bool:
|
|
186
|
-
try:
|
|
187
|
-
return self._resolve_path(path).is_file()
|
|
188
|
-
except Exception:
|
|
189
|
-
return False
|
|
190
|
-
|
|
191
|
-
def _read_before(self, path: str) -> str:
|
|
192
|
-
return self._snapshot(path)[1]
|
|
193
|
-
|
|
194
|
-
def _snapshot(self, path: str) -> Tuple[bool, str]:
|
|
195
|
-
"""(exists, content) of the target *as the proposal pipeline sees it*.
|
|
196
|
-
|
|
197
|
-
Both staging and the approve-time conflict check go through this one
|
|
198
|
-
normalization (truncate → utf-8 with replacement), so an unchanged
|
|
199
|
-
file always hashes identically at both points in time.
|
|
200
|
-
"""
|
|
201
|
-
try:
|
|
202
|
-
target = self._resolve_path(path)
|
|
203
|
-
if not target.is_file():
|
|
204
|
-
return False, ""
|
|
205
|
-
raw = target.read_bytes()[:_MAX_STAGED_BYTES]
|
|
206
|
-
return True, raw.decode("utf-8", errors="replace")
|
|
207
|
-
except Exception:
|
|
208
|
-
LOGGER.exception("change proposal read failed")
|
|
209
|
-
return False, ""
|
|
210
|
-
|
|
211
|
-
def _staged_content(self, name: str, args: Dict[str, Any]) -> Optional[str]:
|
|
212
|
-
if name == "write_file":
|
|
213
|
-
return str(args.get("content") or "")[:_MAX_STAGED_BYTES]
|
|
214
|
-
if name == "edit_file":
|
|
215
|
-
before = self._read_before(str(args.get("path") or ""))
|
|
216
|
-
old = str(args.get("old_string") or "")
|
|
217
|
-
new = str(args.get("new_string") or "")
|
|
218
|
-
if not old or old not in before:
|
|
219
|
-
return None
|
|
220
|
-
if args.get("replace_all"):
|
|
221
|
-
return before.replace(old, new)[:_MAX_STAGED_BYTES]
|
|
222
|
-
if before.count(old) != 1:
|
|
223
|
-
return None
|
|
224
|
-
return before.replace(old, new, 1)[:_MAX_STAGED_BYTES]
|
|
225
|
-
return None
|
|
226
|
-
|
|
227
|
-
# ── proposal creation ────────────────────────────────────────────────
|
|
228
|
-
|
|
229
|
-
def propose_file_update(
|
|
230
|
-
self,
|
|
231
|
-
*,
|
|
232
|
-
path: str,
|
|
233
|
-
new_content: str,
|
|
234
|
-
proposed_by: str = "agent",
|
|
235
|
-
reason: str = "",
|
|
236
|
-
user_email: Optional[str] = None,
|
|
237
|
-
workspace_id: Optional[str] = None,
|
|
238
|
-
context: Optional[Dict[str, Any]] = None,
|
|
239
|
-
) -> Dict[str, Any]:
|
|
240
|
-
base_exists, before = self._snapshot(path)
|
|
241
|
-
diff = _unified_diff(before, new_content, path)
|
|
242
|
-
tier = "small" if len(diff) <= _SMALL_TIER_DIFF_LINES else "large"
|
|
243
|
-
provenance: Dict[str, Any] = {"proposed_by": proposed_by, "reason": reason}
|
|
244
|
-
for key, value in (context or {}).items():
|
|
245
|
-
if value is not None and key not in provenance:
|
|
246
|
-
provenance[key] = value
|
|
247
|
-
item = self._review_queue.create(
|
|
248
|
-
title=f"파일 수정 제안: {path}",
|
|
249
|
-
summary=reason or "기존 파일을 변경하는 작업이라 검토 후 적용됩니다.",
|
|
250
|
-
source="change_proposal",
|
|
251
|
-
kind="file_update",
|
|
252
|
-
payload={
|
|
253
|
-
"path": path,
|
|
254
|
-
"diff": diff,
|
|
255
|
-
"new_content": new_content[:_MAX_STAGED_BYTES],
|
|
256
|
-
"tier": tier,
|
|
257
|
-
"before_bytes": len(before.encode("utf-8")),
|
|
258
|
-
"after_bytes": len(new_content.encode("utf-8")),
|
|
259
|
-
# Base snapshot for the approve-time conflict check: an empty
|
|
260
|
-
# base_sha256 with base_exists=False means "proposed against a
|
|
261
|
-
# missing file", never "hash of the empty string".
|
|
262
|
-
"base_exists": base_exists,
|
|
263
|
-
"base_sha256": _sha256_text(before) if base_exists else "",
|
|
264
|
-
},
|
|
265
|
-
provenance=provenance,
|
|
266
|
-
user_email=user_email,
|
|
267
|
-
workspace_id=workspace_id,
|
|
268
|
-
)
|
|
269
|
-
self._audit(
|
|
270
|
-
"change_proposal_created", user_email=user_email,
|
|
271
|
-
proposal_id=item.get("id"), path=path, kind="file_update", tier=tier,
|
|
272
|
-
)
|
|
273
|
-
return item
|
|
274
|
-
|
|
275
|
-
def propose_reorganization(
|
|
276
|
-
self,
|
|
277
|
-
*,
|
|
278
|
-
root: str = "",
|
|
279
|
-
graph: Any = None,
|
|
280
|
-
user_email: Optional[str] = None,
|
|
281
|
-
workspace_id: Optional[str] = None,
|
|
282
|
-
max_moves: int = 20,
|
|
283
|
-
) -> Dict[str, Any]:
|
|
284
|
-
"""Stage a whole-folder reorganization as one reviewable proposal.
|
|
285
|
-
|
|
286
|
-
Moves only — see :mod:`latticeai.core.workspace_reorganization`. It
|
|
287
|
-
goes through this service (rather than the review queue directly) so
|
|
288
|
-
approving it from the Review Center applies it the same way every
|
|
289
|
-
other staged change is applied.
|
|
290
|
-
"""
|
|
291
|
-
return propose_reorganization(
|
|
292
|
-
root=root,
|
|
293
|
-
resolve_path=self._resolve_path,
|
|
294
|
-
review_queue=self._review_queue,
|
|
295
|
-
graph=graph,
|
|
296
|
-
user_email=user_email,
|
|
297
|
-
workspace_id=workspace_id,
|
|
298
|
-
max_moves=max_moves,
|
|
299
|
-
audit=self._audit,
|
|
300
|
-
)
|
|
301
|
-
|
|
302
|
-
# ── listing / apply / reject ─────────────────────────────────────────
|
|
303
|
-
|
|
304
|
-
def pending(
|
|
305
|
-
self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None
|
|
306
|
-
) -> Dict[str, Any]:
|
|
307
|
-
listed = self._review_queue.list(
|
|
308
|
-
workspace_id=workspace_id, user_email=user_email,
|
|
309
|
-
status="pending", source="change_proposal",
|
|
310
|
-
)
|
|
311
|
-
items = listed.get("items") or []
|
|
312
|
-
return {
|
|
313
|
-
"items": items,
|
|
314
|
-
"count": len(items),
|
|
315
|
-
"contract": {
|
|
316
|
-
"additive_writes": "auto",
|
|
317
|
-
"mutations": "proposal",
|
|
318
|
-
"deletions": "proposal",
|
|
319
|
-
"applied_content": "exactly_as_reviewed",
|
|
320
|
-
},
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
def counts(
|
|
324
|
-
self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None
|
|
325
|
-
) -> Dict[str, Any]:
|
|
326
|
-
"""Pending proposal count for the Review Center badge."""
|
|
327
|
-
listed = self._review_queue.list(
|
|
328
|
-
workspace_id=workspace_id, user_email=user_email,
|
|
329
|
-
status="pending", source="change_proposal",
|
|
330
|
-
)
|
|
331
|
-
return {"pending": len(listed.get("items") or [])}
|
|
332
|
-
|
|
333
|
-
def get_proposal(
|
|
334
|
-
self, item_id: str, *, workspace_id: Optional[str] = None
|
|
335
|
-
) -> Dict[str, Any]:
|
|
336
|
-
"""Full proposal detail (diff + staged content) for the preview UI.
|
|
337
|
-
|
|
338
|
-
Raises ``KeyError`` when the id exists but is not a change proposal,
|
|
339
|
-
so the API surface cannot leak arbitrary review items.
|
|
340
|
-
"""
|
|
341
|
-
item = self._review_queue.get(item_id, workspace_id=workspace_id)
|
|
342
|
-
if item.get("source") != "change_proposal":
|
|
343
|
-
raise KeyError(f"not a change proposal: {item_id}")
|
|
344
|
-
return item
|
|
345
|
-
|
|
346
|
-
def approve_and_apply(
|
|
347
|
-
self, item_id: str, *, user_email: Optional[str] = None,
|
|
348
|
-
workspace_id: Optional[str] = None,
|
|
349
|
-
) -> Dict[str, Any]:
|
|
350
|
-
"""Apply the staged change exactly as reviewed — but only if the file
|
|
351
|
-
on disk still matches the base snapshot the reviewer looked at.
|
|
352
|
-
|
|
353
|
-
Raises :class:`ProposalConflictError` (→ HTTP 409) when the target
|
|
354
|
-
drifted since staging or the proposal was already resolved; nothing
|
|
355
|
-
touches disk in that case, so out-of-band user edits are preserved.
|
|
356
|
-
"""
|
|
357
|
-
with self._apply_lock:
|
|
358
|
-
item = self._review_queue.get(item_id, workspace_id=workspace_id)
|
|
359
|
-
if item.get("source") != "change_proposal":
|
|
360
|
-
raise KeyError(f"not a change proposal: {item_id}")
|
|
361
|
-
payload = item.get("payload") or {}
|
|
362
|
-
kind = str(item.get("kind") or "")
|
|
363
|
-
path = str(payload.get("path") or payload.get("root") or "")
|
|
364
|
-
if kind not in ("file_update", "file_delete", REORG_KIND):
|
|
365
|
-
raise ValueError(f"unknown change proposal kind: {kind}")
|
|
366
|
-
|
|
367
|
-
# Duplicate/concurrent approval guard: a resolved proposal must
|
|
368
|
-
# never re-apply (the disk may have moved on since it was applied).
|
|
369
|
-
status = str(item.get("status") or "pending")
|
|
370
|
-
if status not in ("pending", "snoozed"):
|
|
371
|
-
raise ProposalConflictError(
|
|
372
|
-
reason=f"already_{status}", path=path, kind=kind,
|
|
373
|
-
rebase_hint="이미 처리된 제안입니다. 다시 적용할 수 없습니다.",
|
|
374
|
-
)
|
|
375
|
-
|
|
376
|
-
moves: Optional[Dict[str, Any]] = None
|
|
377
|
-
if kind == REORG_KIND:
|
|
378
|
-
# A reorganization has no single base file to hash: each move
|
|
379
|
-
# is re-checked at apply time and a drifted one is skipped,
|
|
380
|
-
# never forced (see workspace_reorganization.apply_*).
|
|
381
|
-
moves = apply_reorganization(payload, resolve_path=self._resolve_path)
|
|
382
|
-
else:
|
|
383
|
-
self._check_base_unchanged(payload, path=path, kind=kind)
|
|
384
|
-
target = self._resolve_path(path)
|
|
385
|
-
if kind == "file_update":
|
|
386
|
-
self._atomic_write(target, str(payload.get("new_content") or ""))
|
|
387
|
-
elif target.is_file(): # file_delete — re-verified inside the lock
|
|
388
|
-
target.unlink()
|
|
389
|
-
approved = self._review_queue.approve(item_id, workspace_id=workspace_id)
|
|
390
|
-
self._audit(
|
|
391
|
-
"change_proposal_applied", user_email=user_email,
|
|
392
|
-
proposal_id=item_id, path=path, kind=kind,
|
|
393
|
-
)
|
|
394
|
-
result = {"item": approved, "applied": True, "path": path, "kind": kind}
|
|
395
|
-
if moves is not None:
|
|
396
|
-
result["moves"] = moves
|
|
397
|
-
return result
|
|
398
|
-
|
|
399
|
-
def _check_base_unchanged(
|
|
400
|
-
self, payload: Dict[str, Any], *, path: str, kind: str
|
|
401
|
-
) -> None:
|
|
402
|
-
"""Compare the disk state *now* against the staged base snapshot."""
|
|
403
|
-
if "base_sha256" not in payload or "base_exists" not in payload:
|
|
404
|
-
# Legacy proposal staged before base snapshots existed — keep the
|
|
405
|
-
# historical apply-as-reviewed behavior rather than rejecting it.
|
|
406
|
-
return
|
|
407
|
-
base_exists = bool(payload.get("base_exists"))
|
|
408
|
-
base_sha256 = str(payload.get("base_sha256") or "")
|
|
409
|
-
current_exists, current_content = self._snapshot(path)
|
|
410
|
-
current_sha256 = _sha256_text(current_content) if current_exists else ""
|
|
411
|
-
if not base_exists:
|
|
412
|
-
if current_exists:
|
|
413
|
-
raise ProposalConflictError(
|
|
414
|
-
reason="file_created_since_proposal", path=path, kind=kind,
|
|
415
|
-
current_sha256=current_sha256,
|
|
416
|
-
)
|
|
417
|
-
return
|
|
418
|
-
if not current_exists:
|
|
419
|
-
raise ProposalConflictError(
|
|
420
|
-
reason="file_deleted_since_proposal", path=path, kind=kind,
|
|
421
|
-
base_sha256=base_sha256,
|
|
422
|
-
)
|
|
423
|
-
if current_sha256 != base_sha256:
|
|
424
|
-
raise ProposalConflictError(
|
|
425
|
-
reason="file_modified_since_proposal", path=path, kind=kind,
|
|
426
|
-
base_sha256=base_sha256, current_sha256=current_sha256,
|
|
427
|
-
)
|
|
428
|
-
|
|
429
|
-
@staticmethod
|
|
430
|
-
def _atomic_write(target: Path, content: str) -> None:
|
|
431
|
-
"""Write via a same-directory temp file + ``os.replace`` so readers
|
|
432
|
-
never observe a partially written proposal."""
|
|
433
|
-
target.parent.mkdir(parents=True, exist_ok=True)
|
|
434
|
-
fd, tmp_name = tempfile.mkstemp(
|
|
435
|
-
dir=str(target.parent), prefix=f".{target.name}.", suffix=".staged"
|
|
436
|
-
)
|
|
437
|
-
try:
|
|
438
|
-
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
439
|
-
handle.write(content)
|
|
440
|
-
os.replace(tmp_name, target)
|
|
441
|
-
except BaseException:
|
|
442
|
-
try:
|
|
443
|
-
os.unlink(tmp_name)
|
|
444
|
-
except OSError:
|
|
445
|
-
quiet()
|
|
446
|
-
raise
|
|
447
|
-
|
|
448
|
-
def reject(
|
|
449
|
-
self, item_id: str, *, user_email: Optional[str] = None,
|
|
450
|
-
workspace_id: Optional[str] = None, reason: str = "",
|
|
451
|
-
) -> Dict[str, Any]:
|
|
452
|
-
self.get_proposal(item_id, workspace_id=workspace_id) # source guard
|
|
453
|
-
reason = str(reason or "").strip()[:500]
|
|
454
|
-
if reason:
|
|
455
|
-
try:
|
|
456
|
-
dismissed = self._review_queue.dismiss(
|
|
457
|
-
item_id, workspace_id=workspace_id, reason=reason
|
|
458
|
-
)
|
|
459
|
-
except TypeError:
|
|
460
|
-
# Older/fake queues without reason support still dismiss.
|
|
461
|
-
dismissed = self._review_queue.dismiss(item_id, workspace_id=workspace_id)
|
|
462
|
-
else:
|
|
463
|
-
dismissed = self._review_queue.dismiss(item_id, workspace_id=workspace_id)
|
|
464
|
-
self._audit(
|
|
465
|
-
"change_proposal_rejected", user_email=user_email, proposal_id=item_id,
|
|
466
|
-
reason=reason or None,
|
|
467
|
-
)
|
|
468
|
-
return {"item": dismissed, "applied": False, "reason": reason}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
__all__ = ["ChangeProposalService", "ProposalConflictError"]
|