ltcai 11.5.0 → 11.5.2
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 +86 -47
- package/docs/CHANGELOG.md +76 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/HYBRID_CLOUD_KG_STREAMING.md +8 -4
- package/docs/MULTI_AGENT_RUNTIME.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/WORKFLOW_DESIGNER.md +1 -1
- package/docs/architecture.md +4 -4
- package/docs/kg-schema.md +1 -1
- package/docs/v11.5.1_RUST_FULL_LOOP_PLAN.md +85 -0
- package/docs/v11.5.2_TIGHT_SHIP_PLAN.md +144 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/graph/proactive.py +1 -1
- package/lattice_brain/graph/projection/v2_schema.py +0 -28
- package/lattice_brain/graph/vector_index/base.py +0 -3
- package/lattice_brain/graph/vector_index/brute_force.py +0 -4
- package/lattice_brain/graph/vector_index/hnsw.py +0 -3
- package/lattice_brain/graph/vector_index/quantized.py +0 -3
- package/lattice_brain/ingestion_jobs.py +0 -3
- package/lattice_brain/memory.py +0 -27
- package/lattice_brain/portability/sharing.py +0 -4
- package/lattice_brain/quality.py +0 -49
- package/lattice_brain/runtime/agent_runtime.py +1 -1
- package/lattice_brain/runtime/hooks.py +0 -13
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/lattice_brain/sealed_box.py +0 -4
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +12 -12
- package/latticeai/api/agent_worker_seam.py +389 -0
- package/latticeai/api/auth.py +25 -2
- package/latticeai/api/chat.py +3 -3
- package/latticeai/api/chat_agent_http.py +3 -12
- package/latticeai/api/chat_helpers.py +10 -13
- package/latticeai/api/computer_use.py +31 -42
- package/latticeai/api/health.py +21 -1
- package/latticeai/api/local_files.py +14 -0
- package/latticeai/api/permissions.py +38 -12
- package/latticeai/api/search.py +24 -0
- package/latticeai/api/static_routes.py +4 -1
- package/latticeai/cli/runtime.py +7 -3
- package/latticeai/core/config.py +47 -3
- package/latticeai/core/csrf.py +28 -2
- package/latticeai/core/embedding_providers/__init__.py +3 -5
- package/latticeai/core/embedding_providers/base.py +1 -1
- package/latticeai/core/embedding_providers/text.py +1 -1
- package/latticeai/core/enterprise.py +1 -4
- package/latticeai/core/http_origin.py +146 -0
- package/latticeai/core/invitations.py +3 -2
- package/latticeai/core/io_utils.py +2 -11
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/messages.py +40 -0
- package/latticeai/core/model_compat.py +2 -8
- package/latticeai/core/module_probe.py +37 -0
- package/latticeai/core/run_explain.py +5 -2
- package/latticeai/core/run_store.py +2 -2
- package/latticeai/core/security.py +13 -0
- package/latticeai/core/sessions.py +3 -2
- package/latticeai/core/sse.py +25 -0
- package/latticeai/core/users.py +0 -9
- package/latticeai/core/workspace_graph_trace.py +2 -8
- package/latticeai/core/workspace_os.py +0 -10
- package/latticeai/core/workspace_os_constants.py +1 -1
- package/latticeai/core/workspace_os_utils.py +34 -0
- package/latticeai/core/workspace_review_items.py +2 -8
- package/latticeai/core/workspace_runs.py +2 -8
- package/latticeai/core/workspace_skills.py +2 -7
- package/latticeai/models/router/documents.py +0 -35
- package/latticeai/models/router/registry.py +0 -4
- package/latticeai/runtime/access_runtime.py +16 -4
- package/latticeai/runtime/build_phases/features.py +25 -2
- package/latticeai/runtime/build_phases/web.py +2 -0
- package/latticeai/runtime/feature_toggle_wiring.py +12 -18
- package/latticeai/runtime/network_boundary_wiring.py +8 -19
- package/latticeai/runtime/permission_mode_wiring.py +6 -13
- package/latticeai/runtime/router_registration.py +4 -0
- package/latticeai/runtime/runtime_context.py +1 -13
- package/latticeai/runtime/service_singletons.py +55 -0
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/brain_intelligence/proposals.py +0 -5
- package/latticeai/services/change_proposals.py +2 -36
- package/latticeai/services/chronicle.py +4 -6
- package/latticeai/services/command_center.py +4 -4
- package/latticeai/services/evidence_actions.py +5 -2
- package/latticeai/services/hybrid_chat.py +2 -2
- package/latticeai/services/hybrid_policy.py +8 -57
- package/latticeai/services/mode_store.py +132 -0
- package/latticeai/services/model_capability_registry.py +0 -15
- package/latticeai/services/model_catalog.py +0 -7
- package/latticeai/services/model_loading.py +3 -2
- package/latticeai/services/model_runtime/__init__.py +0 -57
- package/latticeai/services/model_runtime/engines.py +27 -128
- package/latticeai/services/model_runtime/loading.py +2 -2
- package/latticeai/services/network_boundary_service.py +7 -44
- package/latticeai/services/permission_mode_service.py +8 -56
- package/latticeai/services/product_readiness.py +1 -1
- package/latticeai/services/setup_detection.py +67 -1
- package/latticeai/services/tool_dispatch.py +0 -4
- package/latticeai/services/upload_service.py +7 -20
- package/latticeai/services/workspace_service.py +0 -6
- package/latticeai/setup/auto_setup.py +15 -26
- package/latticeai/setup/wizard/catalog.py +5 -2
- package/latticeai/setup/wizard/detect.py +6 -27
- package/latticeai/setup/wizard/paths.py +2 -5
- package/latticeai/setup/wizard/plans.py +2 -2
- package/package.json +2 -4
- package/scripts/bump_version.py +14 -1
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/check_legacy_debt.mjs +1 -1
- package/scripts/check_server_i18n.mjs +1 -0
- package/scripts/generate_agent_loop_fixtures.py +994 -0
- package/scripts/generate_rust_parity_fixtures.py +72 -161
- package/scripts/parity_fixture_corpus_context.py +162 -0
- package/scripts/parity_fixture_corpus_docgen.py +341 -0
- package/scripts/release_screen_claims.json +20 -0
- package/src-tauri/Cargo.lock +13 -7
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/backend.rs +76 -2
- package/src-tauri/src/main.rs +13 -3
- package/src-tauri/tauri.conf.json +2 -2
- package/static/app/asset-manifest.json +41 -41
- package/static/app/assets/{Act-CWnxSCgN.js → Act-DcQizkl1.js} +1 -1
- package/static/app/assets/{AdminConsole-BEQYU6kF.js → AdminConsole-cf4npybT.js} +1 -1
- package/static/app/assets/{Brain-DWu1BhFg.js → Brain-3VCSHFcn.js} +1 -1
- package/static/app/assets/{BrainHome-95Hilr9R.js → BrainHome-Qm8eaztx.js} +1 -1
- package/static/app/assets/{BrainSignals-QdeqCpAF.js → BrainSignals-DS9BtKOW.js} +1 -1
- package/static/app/assets/{Capture-BHpCxnzb.js → Capture-DiQ219jW.js} +1 -1
- package/static/app/assets/{Chronicle-B4xYKoed.js → Chronicle-BGvuAchH.js} +1 -1
- package/static/app/assets/{CommandPalette-BVXnttSz.js → CommandPalette-Bqhm0Urn.js} +1 -1
- package/static/app/assets/{Library-DgYcHome.js → Library-BV6NnF0a.js} +1 -1
- package/static/app/assets/{LivingBrain-CrJLDbf7.js → LivingBrain-GzenJchP.js} +1 -1
- package/static/app/assets/{ProductFlow-DFlScKoJ.js → ProductFlow-DEP6-vML.js} +1 -1
- package/static/app/assets/{ReviewCard-Cy5f48Pj.js → ReviewCard-CNZ7XjWG.js} +1 -1
- package/static/app/assets/{System-NF8IfhTa.js → System-CieofHQa.js} +1 -1
- package/static/app/assets/arrow-left-kfsrk0mv.js +1 -0
- package/static/app/assets/{bot-CucuhLhm.js → bot-B_K1Tdmw.js} +1 -1
- package/static/app/assets/{brain-BBnSryW_.js → brain-DWyaV1L1.js} +1 -1
- package/static/app/assets/{button-C2GUj2Ai.js → button-aTn4s84A.js} +1 -1
- package/static/app/assets/circle-check-qqLug9nU.js +1 -0
- package/static/app/assets/{circle-pause-CbkWzBmG.js → circle-pause-xKgeGXkT.js} +1 -1
- package/static/app/assets/{circle-play-7lEaqHdJ.js → circle-play-DkT6tYPX.js} +1 -1
- package/static/app/assets/{cpu-DAlCXlIy.js → cpu-85xYObUC.js} +1 -1
- package/static/app/assets/{download-RNhuuJwh.js → download-B5Fm7YXo.js} +1 -1
- package/static/app/assets/{folder-open-CLW4odzM.js → folder-open-kk2Xa52u.js} +1 -1
- package/static/app/assets/{hard-drive-NKEiDIAJ.js → hard-drive-DkA3zBW_.js} +1 -1
- package/static/app/assets/{index-DMurvUuR.js → index-BMPdTmlY.js} +3 -3
- package/static/app/assets/index-DxmOfNRi.css +2 -0
- package/static/app/assets/{input-D2UhPC1X.js → input-B0nRf2jO.js} +1 -1
- package/static/app/assets/{link-2-6amKbP_P.js → link-2-Dwb4gnTc.js} +1 -1
- package/static/app/assets/{permissionCopy-Cu9TZtdR.js → permissionCopy-CQDUBrOZ.js} +1 -1
- package/static/app/assets/{primitives-gPsccucr.js → primitives-SNp0LRJz.js} +1 -1
- package/static/app/assets/search-BcHqkjoy.js +1 -0
- package/static/app/assets/{share-2-Bau7KkPq.js → share-2-BsrxFglO.js} +1 -1
- package/static/app/assets/{shield-alert-BufNYypi.js → shield-alert-5BStfp2_.js} +1 -1
- package/static/app/assets/{textarea-BQnVWhYs.js → textarea-Cg8IUA-k.js} +1 -1
- package/static/app/assets/{useFocusTrap-B3_w60si.js → useFocusTrap-CYKvE46M.js} +1 -1
- package/static/app/assets/{useMutation-BHhCflT6.js → useMutation-CSn9t1op.js} +1 -1
- package/static/app/assets/{useQuery-rBWfI-5t.js → useQuery-CY2OI2uy.js} +1 -1
- package/static/app/assets/{utils-V_5-wxr5.js → utils-Ddol2RWD.js} +1 -1
- package/static/app/assets/{workspace-K1zjYUHj.js → workspace-BqDwOz_p.js} +1 -1
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/desktop/electron/README.md +0 -9
- package/desktop/electron/main.cjs +0 -58
- package/desktop/electron/preload.cjs +0 -5
- package/latticeai/core/graph_curator.py +0 -11
- package/latticeai/core/hooks.py +0 -11
- package/latticeai/core/local_embeddings.py +0 -104
- package/latticeai/core/multi_agent.py +0 -11
- package/latticeai/core/workflow_engine.py +0 -11
- package/latticeai/services/ingestion.py +0 -11
- package/latticeai/services/kg_portability.py +0 -11
- package/latticeai/services/multimodal_streaming.py +0 -129
- package/scripts/measure_brain_home_fill.mjs +0 -141
- package/static/app/assets/arrow-left-DwkSYrjR.js +0 -1
- package/static/app/assets/circle-check-CxOVPwYq.js +0 -1
- package/static/app/assets/index-BLPb5lmE.css +0 -2
- package/static/app/assets/search-Cj_TKk_2.js +0 -1
|
@@ -21,23 +21,6 @@ class _DocumentMixin(_Core):
|
|
|
21
21
|
|
|
22
22
|
# ── Document Generation Pipeline ──────────────────────────────────────
|
|
23
23
|
|
|
24
|
-
async def generate_document(
|
|
25
|
-
self,
|
|
26
|
-
message: str,
|
|
27
|
-
system_prompt: str,
|
|
28
|
-
*,
|
|
29
|
-
max_tokens: int = 8192,
|
|
30
|
-
temperature: float = 0.3,
|
|
31
|
-
) -> str:
|
|
32
|
-
"""Generate a document using a specialized system prompt with graph context."""
|
|
33
|
-
return await self.generate_document_as(
|
|
34
|
-
None,
|
|
35
|
-
message,
|
|
36
|
-
system_prompt,
|
|
37
|
-
max_tokens=max_tokens,
|
|
38
|
-
temperature=temperature,
|
|
39
|
-
)
|
|
40
|
-
|
|
41
24
|
async def generate_document_as(
|
|
42
25
|
self,
|
|
43
26
|
model_id: str | None,
|
|
@@ -98,24 +81,6 @@ class _DocumentMixin(_Core):
|
|
|
98
81
|
raise RuntimeError(self._local_server_error_hint(cloud, e)) from e
|
|
99
82
|
return normalize_branding(response.choices[0].message.content or "")
|
|
100
83
|
|
|
101
|
-
async def stream_generate_document(
|
|
102
|
-
self,
|
|
103
|
-
message: str,
|
|
104
|
-
system_prompt: str,
|
|
105
|
-
*,
|
|
106
|
-
max_tokens: int = 8192,
|
|
107
|
-
temperature: float = 0.3,
|
|
108
|
-
) -> AsyncIterator[str]:
|
|
109
|
-
"""Stream document generation with specialized system prompt."""
|
|
110
|
-
async for chunk in self.stream_generate_document_as(
|
|
111
|
-
None,
|
|
112
|
-
message,
|
|
113
|
-
system_prompt,
|
|
114
|
-
max_tokens=max_tokens,
|
|
115
|
-
temperature=temperature,
|
|
116
|
-
):
|
|
117
|
-
yield chunk
|
|
118
|
-
|
|
119
84
|
async def stream_generate_document_as(
|
|
120
85
|
self,
|
|
121
86
|
model_id: str | None,
|
|
@@ -107,10 +107,6 @@ class _RegistryMixin(_Core):
|
|
|
107
107
|
self.unload_model(victim)
|
|
108
108
|
local_ids = [model_id for model_id in self._cache if self._is_local_model(model_id)]
|
|
109
109
|
|
|
110
|
-
def _is_cloud_current(self) -> bool:
|
|
111
|
-
with self._lock:
|
|
112
|
-
return bool(self._current and isinstance(self._cache.get(self._current), CloudModel))
|
|
113
|
-
|
|
114
110
|
def _local_server_error_hint(self, cloud: CloudModel, error: Exception) -> str:
|
|
115
111
|
raw = str(error)
|
|
116
112
|
if cloud.provider == "lmstudio":
|
|
@@ -7,6 +7,21 @@ from typing import Any, Callable, Dict, Optional
|
|
|
7
7
|
from latticeai.core.quiet import quiet
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
def is_externally_reachable(config: Any) -> bool:
|
|
11
|
+
"""Whether anything but this machine can open a socket to this worker.
|
|
12
|
+
|
|
13
|
+
The single definition of the second half of ``trusted_local_owner``. It is
|
|
14
|
+
read here (to decide who the anonymous caller is) and by the ``/health``
|
|
15
|
+
router (to *state* the posture, so the Rust front door can mirror it on its
|
|
16
|
+
own native lanes instead of guessing). Two derivations of one fact is how
|
|
17
|
+
a gateway ends up serving a graph the worker would have refused.
|
|
18
|
+
"""
|
|
19
|
+
return bool(
|
|
20
|
+
getattr(config, "is_public", False)
|
|
21
|
+
or getattr(config, "network_exposed", False)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
10
25
|
def build_access_runtime(
|
|
11
26
|
*,
|
|
12
27
|
config: Any,
|
|
@@ -29,10 +44,7 @@ def build_access_runtime(
|
|
|
29
44
|
# but project the identity as an owner at authorization boundaries. Never
|
|
30
45
|
# extend this trust to public or non-loopback bindings, even if an invalid
|
|
31
46
|
# caller constructs this runtime with ``require_auth=False`` directly.
|
|
32
|
-
externally_reachable =
|
|
33
|
-
getattr(config, "is_public", False)
|
|
34
|
-
or getattr(config, "network_exposed", False)
|
|
35
|
-
)
|
|
47
|
+
externally_reachable = is_externally_reachable(config)
|
|
36
48
|
trusted_local_owner = not require_auth and not externally_reachable
|
|
37
49
|
effective_require_auth = bool(require_auth or externally_reachable)
|
|
38
50
|
|
|
@@ -16,6 +16,7 @@ def phase_platform_features(ctx: RuntimeContext) -> None:
|
|
|
16
16
|
"""Workspace platform, automation, review queue, command centre, proposals."""
|
|
17
17
|
ctx.enter("platform_features")
|
|
18
18
|
|
|
19
|
+
from latticeai.api.agent_worker_seam import create_agent_worker_seam_router
|
|
19
20
|
from latticeai.api.agents import create_agents_router
|
|
20
21
|
from latticeai.api.automation_intelligence import (
|
|
21
22
|
create_automation_intelligence_router,
|
|
@@ -48,9 +49,12 @@ def phase_platform_features(ctx: RuntimeContext) -> None:
|
|
|
48
49
|
from latticeai.services.chronicle import ChronicleService
|
|
49
50
|
from latticeai.services.command_center import CommandCenterService
|
|
50
51
|
from latticeai.services.evidence_actions import EvidenceActionService
|
|
51
|
-
from latticeai.services.tool_dispatch import
|
|
52
|
+
from latticeai.services.tool_dispatch import (
|
|
53
|
+
DEFAULT_TOOL_DISPATCH_SERVICE,
|
|
54
|
+
get_tool_permission,
|
|
55
|
+
)
|
|
52
56
|
from latticeai.services.voice_capture import VoiceCaptureService
|
|
53
|
-
from latticeai.tools import resolve_workspace_path
|
|
57
|
+
from latticeai.tools import execute_tool, resolve_workspace_path
|
|
54
58
|
|
|
55
59
|
# v2 Agentic Workspace Platform: cross-system wiring.
|
|
56
60
|
platform_automation_runtime = build_platform_automation_runtime(
|
|
@@ -210,6 +214,23 @@ def phase_platform_features(ctx: RuntimeContext) -> None:
|
|
|
210
214
|
)
|
|
211
215
|
)
|
|
212
216
|
|
|
217
|
+
# AI-Worker seam (v11.5.1, plan §Y1): the three calls the Rust agent loop
|
|
218
|
+
# makes back into Python once orchestration lives in ``lattice-agent``.
|
|
219
|
+
# Registered here because it needs the change governor above; the two
|
|
220
|
+
# side-effecting routes stay behind ``LATTICEAI_AGENT_TOOL_SEAM=1``, which
|
|
221
|
+
# only lattice-host injects into a worker it started.
|
|
222
|
+
ctx.app.include_router(
|
|
223
|
+
create_agent_worker_seam_router(
|
|
224
|
+
model_router=ctx.model_router,
|
|
225
|
+
dispatch_service=DEFAULT_TOOL_DISPATCH_SERVICE,
|
|
226
|
+
execute_tool=execute_tool,
|
|
227
|
+
hooks=ctx.HOOKS_REGISTRY,
|
|
228
|
+
change_proposals=ctx.CHANGE_PROPOSALS,
|
|
229
|
+
require_user=ctx.require_user,
|
|
230
|
+
enforce_rate_limit=ctx.enforce_rate_limit,
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
|
|
213
234
|
# Evidence → action (v9.9.6): an answer's citations become ready-to-send,
|
|
214
235
|
# evidence-scoped follow-ups. Deterministic composition only — execution
|
|
215
236
|
# stays on the chat/file-generation path.
|
|
@@ -284,6 +305,7 @@ def phase_interaction(ctx: RuntimeContext) -> None:
|
|
|
284
305
|
from latticeai.core.model_compat import (
|
|
285
306
|
list_cached_profiles as _list_compat_profiles,
|
|
286
307
|
)
|
|
308
|
+
from latticeai.runtime.access_runtime import is_externally_reachable
|
|
287
309
|
from latticeai.runtime.chat_wiring import build_interaction_contexts
|
|
288
310
|
from latticeai.runtime.model_wiring import register_model_runtime_routers
|
|
289
311
|
from latticeai.runtime.platform_services_runtime import build_brain_network
|
|
@@ -319,6 +341,7 @@ def phase_interaction(ctx: RuntimeContext) -> None:
|
|
|
319
341
|
engine_status=service.engine_status,
|
|
320
342
|
get_current_user=ctx.get_current_user,
|
|
321
343
|
require_auth=ctx.REQUIRE_AUTH,
|
|
344
|
+
externally_reachable=is_externally_reachable(ctx.CONFIG),
|
|
322
345
|
app_version=ctx.APP_VERSION,
|
|
323
346
|
app_mode=ctx.APP_MODE,
|
|
324
347
|
require_user=ctx.require_user,
|
|
@@ -310,6 +310,7 @@ def phase_web(ctx: RuntimeContext) -> None:
|
|
|
310
310
|
create_security_router as _create_security_router,
|
|
311
311
|
)
|
|
312
312
|
from latticeai.api.static_routes import create_static_routes_router
|
|
313
|
+
from latticeai.core.config import default_sso_redirect_uri
|
|
313
314
|
from latticeai.core.policy import policy_matrix
|
|
314
315
|
from latticeai.core.product_hardening import build_product_hardening_status
|
|
315
316
|
from latticeai.core.security import hash_password
|
|
@@ -408,6 +409,7 @@ def phase_web(ctx: RuntimeContext) -> None:
|
|
|
408
409
|
|
|
409
410
|
foundation_router_bundle = build_auth_admin_security_router_bundle(
|
|
410
411
|
create_auth_router=create_auth_router,
|
|
412
|
+
sso_default_redirect_uri=default_sso_redirect_uri(ctx.DEFAULT_PORT),
|
|
411
413
|
load_users=ctx.load_users,
|
|
412
414
|
save_users=ctx.save_users,
|
|
413
415
|
hash_password=hash_password,
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"""Wire the feature switchboard into the running app (v11.2.0).
|
|
2
2
|
|
|
3
|
-
Same shape as ``permission_mode_wiring``: one process-wide service
|
|
4
|
-
idempotent router mount guarded on ``app.state
|
|
5
|
-
*rebind* an already-created service rather than
|
|
6
|
-
caller
|
|
3
|
+
Same shape as ``permission_mode_wiring``: one process-wide service and an
|
|
4
|
+
idempotent router mount guarded on ``app.state``. The rebinding rule that shape
|
|
5
|
+
turns on — explicit arguments *rebind* an already-created service rather than
|
|
6
|
+
being dropped, so a lazy first caller cannot pin the store to a fallback data
|
|
7
|
+
dir with no audit sink — is shared, in
|
|
8
|
+
:mod:`latticeai.runtime.service_singletons`.
|
|
7
9
|
|
|
8
10
|
The one job unique to this module is :func:`bind_feature_gates`: it points each
|
|
9
11
|
opt-in gate's resolver at the service, which is the step that turns a persisted
|
|
@@ -18,11 +20,14 @@ them back, and "the environment answers again" has to be reachable.
|
|
|
18
20
|
|
|
19
21
|
from __future__ import annotations
|
|
20
22
|
|
|
21
|
-
import os
|
|
22
23
|
import threading
|
|
23
24
|
from pathlib import Path
|
|
24
25
|
from typing import Any, Callable, List, Optional, Tuple
|
|
25
26
|
|
|
27
|
+
from latticeai.runtime.service_singletons import (
|
|
28
|
+
rebind_singleton,
|
|
29
|
+
singleton_data_dir,
|
|
30
|
+
)
|
|
26
31
|
from latticeai.services.feature_toggles import FeatureToggleService
|
|
27
32
|
|
|
28
33
|
_LOCK = threading.Lock()
|
|
@@ -47,13 +52,6 @@ GATE_BINDINGS: Tuple[Tuple[str, str, str], ...] = (
|
|
|
47
52
|
CHOICE_FEATURE = "vector_backend"
|
|
48
53
|
|
|
49
54
|
|
|
50
|
-
def _default_data_dir() -> Path:
|
|
51
|
-
raw = os.environ.get("LATTICEAI_DATA_DIR", "").strip()
|
|
52
|
-
if raw:
|
|
53
|
-
return Path(raw)
|
|
54
|
-
return Path.home() / ".ltcai"
|
|
55
|
-
|
|
56
|
-
|
|
57
55
|
def _boolean_gates() -> List[Tuple[str, Any]]:
|
|
58
56
|
"""``[(feature id, gate)]`` — imports are here, not at module import."""
|
|
59
57
|
from importlib import import_module
|
|
@@ -80,15 +78,11 @@ def get_feature_toggle_service(
|
|
|
80
78
|
with _LOCK:
|
|
81
79
|
if _SHARED is None:
|
|
82
80
|
_SHARED = FeatureToggleService(
|
|
83
|
-
data_dir=
|
|
81
|
+
data_dir=singleton_data_dir(data_dir),
|
|
84
82
|
audit=audit,
|
|
85
83
|
)
|
|
86
84
|
return _SHARED
|
|
87
|
-
|
|
88
|
-
_SHARED.rebind_data_dir(Path(data_dir))
|
|
89
|
-
if audit is not None:
|
|
90
|
-
_SHARED.rebind_audit(audit)
|
|
91
|
-
return _SHARED
|
|
85
|
+
return rebind_singleton(_SHARED, data_dir=data_dir, audit=audit)
|
|
92
86
|
|
|
93
87
|
|
|
94
88
|
def bind_feature_gates(service: Optional[FeatureToggleService] = None) -> None:
|
|
@@ -11,6 +11,10 @@ from latticeai.core.network_boundary import (
|
|
|
11
11
|
DEFAULT_NETWORK_MODE,
|
|
12
12
|
normalize_network_mode,
|
|
13
13
|
)
|
|
14
|
+
from latticeai.runtime.service_singletons import (
|
|
15
|
+
rebind_singleton,
|
|
16
|
+
singleton_data_dir,
|
|
17
|
+
)
|
|
14
18
|
from latticeai.services.hybrid_policy import HybridPolicyService
|
|
15
19
|
from latticeai.services.network_boundary_service import NetworkBoundaryService
|
|
16
20
|
|
|
@@ -19,13 +23,6 @@ _SHARED: Optional[NetworkBoundaryService] = None
|
|
|
19
23
|
_POLICY: Optional[HybridPolicyService] = None
|
|
20
24
|
|
|
21
25
|
|
|
22
|
-
def _default_data_dir() -> Path:
|
|
23
|
-
raw = os.environ.get("LATTICEAI_DATA_DIR", "").strip()
|
|
24
|
-
if raw:
|
|
25
|
-
return Path(raw)
|
|
26
|
-
return Path.home() / ".ltcai"
|
|
27
|
-
|
|
28
|
-
|
|
29
26
|
def _env_default_mode() -> Any:
|
|
30
27
|
return normalize_network_mode(
|
|
31
28
|
os.environ.get("LATTICEAI_NETWORK_MODE", DEFAULT_NETWORK_MODE.value)
|
|
@@ -41,16 +38,12 @@ def get_network_boundary_service(
|
|
|
41
38
|
with _LOCK:
|
|
42
39
|
if _SHARED is None:
|
|
43
40
|
_SHARED = NetworkBoundaryService(
|
|
44
|
-
data_dir=
|
|
41
|
+
data_dir=singleton_data_dir(data_dir),
|
|
45
42
|
default_mode=_env_default_mode(),
|
|
46
43
|
audit=audit,
|
|
47
44
|
)
|
|
48
45
|
return _SHARED
|
|
49
|
-
|
|
50
|
-
_SHARED.rebind_data_dir(Path(data_dir))
|
|
51
|
-
if audit is not None:
|
|
52
|
-
_SHARED.rebind_audit(audit)
|
|
53
|
-
return _SHARED
|
|
46
|
+
return rebind_singleton(_SHARED, data_dir=data_dir, audit=audit)
|
|
54
47
|
|
|
55
48
|
|
|
56
49
|
def get_hybrid_policy_service(
|
|
@@ -62,15 +55,11 @@ def get_hybrid_policy_service(
|
|
|
62
55
|
with _LOCK:
|
|
63
56
|
if _POLICY is None:
|
|
64
57
|
_POLICY = HybridPolicyService(
|
|
65
|
-
data_dir=
|
|
58
|
+
data_dir=singleton_data_dir(data_dir),
|
|
66
59
|
audit=audit,
|
|
67
60
|
)
|
|
68
61
|
return _POLICY
|
|
69
|
-
|
|
70
|
-
_POLICY.rebind_data_dir(Path(data_dir))
|
|
71
|
-
if audit is not None:
|
|
72
|
-
_POLICY.rebind_audit(audit)
|
|
73
|
-
return _POLICY
|
|
62
|
+
return rebind_singleton(_POLICY, data_dir=data_dir, audit=audit)
|
|
74
63
|
|
|
75
64
|
|
|
76
65
|
def resolve_active_network_mode(
|
|
@@ -15,19 +15,16 @@ from pathlib import Path
|
|
|
15
15
|
from typing import Any, Callable, Optional
|
|
16
16
|
|
|
17
17
|
from latticeai.core.permission_mode import DEFAULT_MODE, normalize_mode
|
|
18
|
+
from latticeai.runtime.service_singletons import (
|
|
19
|
+
rebind_singleton,
|
|
20
|
+
singleton_data_dir,
|
|
21
|
+
)
|
|
18
22
|
from latticeai.services.permission_mode_service import PermissionModeService
|
|
19
23
|
|
|
20
24
|
_LOCK = threading.Lock()
|
|
21
25
|
_SHARED: Optional[PermissionModeService] = None
|
|
22
26
|
|
|
23
27
|
|
|
24
|
-
def _default_data_dir() -> Path:
|
|
25
|
-
raw = os.environ.get("LATTICEAI_DATA_DIR", "").strip()
|
|
26
|
-
if raw:
|
|
27
|
-
return Path(raw)
|
|
28
|
-
return Path.home() / ".ltcai"
|
|
29
|
-
|
|
30
|
-
|
|
31
28
|
def _env_default_mode() -> Any:
|
|
32
29
|
return normalize_mode(os.environ.get("LATTICEAI_PERMISSION_MODE", DEFAULT_MODE.value))
|
|
33
30
|
|
|
@@ -48,16 +45,12 @@ def get_permission_mode_service(
|
|
|
48
45
|
with _LOCK:
|
|
49
46
|
if _SHARED is None:
|
|
50
47
|
_SHARED = PermissionModeService(
|
|
51
|
-
data_dir=
|
|
48
|
+
data_dir=singleton_data_dir(data_dir),
|
|
52
49
|
default_mode=_env_default_mode(),
|
|
53
50
|
audit=audit,
|
|
54
51
|
)
|
|
55
52
|
return _SHARED
|
|
56
|
-
|
|
57
|
-
_SHARED.rebind_data_dir(Path(data_dir))
|
|
58
|
-
if audit is not None:
|
|
59
|
-
_SHARED.rebind_audit(audit)
|
|
60
|
-
return _SHARED
|
|
53
|
+
return rebind_singleton(_SHARED, data_dir=data_dir, audit=audit)
|
|
61
54
|
|
|
62
55
|
|
|
63
56
|
def resolve_active_permission_mode(
|
|
@@ -83,6 +83,7 @@ def build_auth_admin_security_router_bundle(
|
|
|
83
83
|
secure_cookies: bool,
|
|
84
84
|
invite_authorized: Any,
|
|
85
85
|
ensure_identity: Any,
|
|
86
|
+
sso_default_redirect_uri: str,
|
|
86
87
|
create_admin_router: Any,
|
|
87
88
|
require_admin: Any,
|
|
88
89
|
get_history: Any,
|
|
@@ -138,6 +139,7 @@ def build_auth_admin_security_router_bundle(
|
|
|
138
139
|
invite_gate_enabled=invite_gate_enabled,
|
|
139
140
|
invite_authorized=invite_authorized,
|
|
140
141
|
ensure_identity=ensure_identity,
|
|
142
|
+
default_redirect_uri=sso_default_redirect_uri,
|
|
141
143
|
)
|
|
142
144
|
|
|
143
145
|
def graph_stats_safe() -> dict[str, Any]:
|
|
@@ -376,6 +378,7 @@ def register_health_and_model_routers(
|
|
|
376
378
|
engine_status: Any,
|
|
377
379
|
get_current_user: Any,
|
|
378
380
|
require_auth: bool,
|
|
381
|
+
externally_reachable: bool,
|
|
379
382
|
app_version: str,
|
|
380
383
|
app_mode: str,
|
|
381
384
|
create_models_router: Any,
|
|
@@ -411,6 +414,7 @@ def register_health_and_model_routers(
|
|
|
411
414
|
engine_status=engine_status,
|
|
412
415
|
get_current_user=get_current_user,
|
|
413
416
|
require_auth=require_auth,
|
|
417
|
+
externally_reachable=externally_reachable,
|
|
414
418
|
app_version=app_version,
|
|
415
419
|
app_mode=app_mode,
|
|
416
420
|
),
|
|
@@ -25,7 +25,7 @@ Two properties keep it honest:
|
|
|
25
25
|
|
|
26
26
|
from __future__ import annotations
|
|
27
27
|
|
|
28
|
-
from typing import Any, Dict,
|
|
28
|
+
from typing import Any, Dict, Set
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
class RuntimeContext:
|
|
@@ -257,21 +257,9 @@ class RuntimeContext:
|
|
|
257
257
|
f"RuntimeContext.{name} was read before it was built{hint}"
|
|
258
258
|
) from None
|
|
259
259
|
|
|
260
|
-
@property
|
|
261
|
-
def produced_by(self) -> Dict[str, str]:
|
|
262
|
-
"""Attribute name → the phase that published it, for the order test."""
|
|
263
|
-
return dict(self._produced)
|
|
264
|
-
|
|
265
260
|
def names(self) -> Set[str]:
|
|
266
261
|
return set(self._produced)
|
|
267
262
|
|
|
268
|
-
def phases_run(self) -> List[str]:
|
|
269
|
-
seen: List[str] = []
|
|
270
|
-
for phase in self._produced.values():
|
|
271
|
-
if phase not in seen:
|
|
272
|
-
seen.append(phase)
|
|
273
|
-
return seen
|
|
274
|
-
|
|
275
263
|
def __repr__(self) -> str: # pragma: no cover - debugging aid
|
|
276
264
|
return f"<RuntimeContext phase={self._phase!r} names={len(self._produced)}>"
|
|
277
265
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""The rebinding half of the process-wide service singletons.
|
|
2
|
+
|
|
3
|
+
Four wiring modules create one service each — the permission dial, the network
|
|
4
|
+
boundary, the hybrid policy, the feature switchboard — and all four had the
|
|
5
|
+
same problem and the same four-line answer to it: a lazy first caller (a tool
|
|
6
|
+
dispatch, a feature gate) can ask for the service *before* routers know the
|
|
7
|
+
configured data dir or the audit sink, and if the later explicit call were
|
|
8
|
+
allowed to be a no-op the service would stay pinned to the fallback path with
|
|
9
|
+
its audit events going nowhere.
|
|
10
|
+
|
|
11
|
+
So explicit arguments rebind rather than being dropped. That rule was written
|
|
12
|
+
out four times; it lives here once, because "the second caller silently loses"
|
|
13
|
+
is precisely the kind of divergence that produces a second, invisible copy of
|
|
14
|
+
the user's data and no failing test.
|
|
15
|
+
|
|
16
|
+
The singleton *slot* deliberately stays a module global in each wiring module:
|
|
17
|
+
the modules are the unit of process state, and the suite resets them by
|
|
18
|
+
assigning ``_SHARED = None``.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Callable, Optional, TypeVar
|
|
25
|
+
|
|
26
|
+
from latticeai.core.config import default_data_dir
|
|
27
|
+
|
|
28
|
+
ServiceT = TypeVar("ServiceT")
|
|
29
|
+
|
|
30
|
+
__all__ = ["singleton_data_dir", "rebind_singleton"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def singleton_data_dir(data_dir: Optional[Path]) -> Path:
|
|
34
|
+
"""Where a *first* lazy caller should put the store."""
|
|
35
|
+
return Path(data_dir) if data_dir is not None else default_data_dir()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def rebind_singleton(
|
|
39
|
+
service: ServiceT,
|
|
40
|
+
*,
|
|
41
|
+
data_dir: Optional[Path] = None,
|
|
42
|
+
audit: Optional[Callable[..., None]] = None,
|
|
43
|
+
) -> ServiceT:
|
|
44
|
+
"""Apply explicit arguments to an already-created singleton, then return it.
|
|
45
|
+
|
|
46
|
+
``None`` means "the caller did not say", never "unset it" — a router that
|
|
47
|
+
mounts without an audit sink must not silence a sink an earlier caller
|
|
48
|
+
already supplied.
|
|
49
|
+
"""
|
|
50
|
+
rebindable: Any = service
|
|
51
|
+
if data_dir is not None:
|
|
52
|
+
rebindable.rebind_data_dir(Path(data_dir))
|
|
53
|
+
if audit is not None:
|
|
54
|
+
rebindable.rebind_audit(audit)
|
|
55
|
+
return service
|
|
@@ -31,11 +31,6 @@ class BrainProposalsMixin(_Core):
|
|
|
31
31
|
# is absent they report ``available: False`` rather than falling back to a
|
|
32
32
|
# direct write.
|
|
33
33
|
|
|
34
|
-
def attach_review_queue(self, review_queue: Any) -> None:
|
|
35
|
-
"""Bind the review queue proposals are written to (composition root)."""
|
|
36
|
-
self._review_queue_service = review_queue
|
|
37
|
-
self._synthesizer = None
|
|
38
|
-
|
|
39
34
|
def _review_queue(self) -> Any:
|
|
40
35
|
"""The queue to propose into, or ``None`` when there is none.
|
|
41
36
|
|
|
@@ -21,7 +21,6 @@ change audit log. The classification itself lives in
|
|
|
21
21
|
from __future__ import annotations
|
|
22
22
|
|
|
23
23
|
import difflib
|
|
24
|
-
import hashlib
|
|
25
24
|
import logging
|
|
26
25
|
import os
|
|
27
26
|
import tempfile
|
|
@@ -30,6 +29,7 @@ from pathlib import Path
|
|
|
30
29
|
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
31
30
|
|
|
32
31
|
from latticeai.core.quiet import quiet
|
|
32
|
+
from latticeai.core.security import sha256_hex
|
|
33
33
|
from latticeai.core.tool_governor import classify_tool_call
|
|
34
34
|
from latticeai.core.workspace_reorganization import (
|
|
35
35
|
REORG_KIND,
|
|
@@ -90,7 +90,7 @@ class ProposalConflictError(ValueError):
|
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
def _sha256_text(content: str) -> str:
|
|
93
|
-
return
|
|
93
|
+
return sha256_hex(content)
|
|
94
94
|
|
|
95
95
|
|
|
96
96
|
def _unified_diff(before: str, after: str, path: str) -> List[str]:
|
|
@@ -272,40 +272,6 @@ class ChangeProposalService:
|
|
|
272
272
|
)
|
|
273
273
|
return item
|
|
274
274
|
|
|
275
|
-
def propose_file_delete(
|
|
276
|
-
self,
|
|
277
|
-
*,
|
|
278
|
-
path: str,
|
|
279
|
-
proposed_by: str = "agent",
|
|
280
|
-
reason: str = "",
|
|
281
|
-
user_email: Optional[str] = None,
|
|
282
|
-
workspace_id: Optional[str] = None,
|
|
283
|
-
) -> Dict[str, Any]:
|
|
284
|
-
base_exists, before = self._snapshot(path)
|
|
285
|
-
item = self._review_queue.create(
|
|
286
|
-
title=f"파일 삭제 제안: {path}",
|
|
287
|
-
summary=reason or "기존 파일을 삭제하는 작업이라 검토 후 적용됩니다.",
|
|
288
|
-
source="change_proposal",
|
|
289
|
-
kind="file_delete",
|
|
290
|
-
payload={
|
|
291
|
-
"path": path,
|
|
292
|
-
"diff": _unified_diff(before, "", path),
|
|
293
|
-
"tier": "large",
|
|
294
|
-
"before_bytes": len(before.encode("utf-8")),
|
|
295
|
-
"after_bytes": 0,
|
|
296
|
-
"base_exists": base_exists,
|
|
297
|
-
"base_sha256": _sha256_text(before) if base_exists else "",
|
|
298
|
-
},
|
|
299
|
-
provenance={"proposed_by": proposed_by, "reason": reason},
|
|
300
|
-
user_email=user_email,
|
|
301
|
-
workspace_id=workspace_id,
|
|
302
|
-
)
|
|
303
|
-
self._audit(
|
|
304
|
-
"change_proposal_created", user_email=user_email,
|
|
305
|
-
proposal_id=item.get("id"), path=path, kind="file_delete", tier="large",
|
|
306
|
-
)
|
|
307
|
-
return item
|
|
308
|
-
|
|
309
275
|
def propose_reorganization(
|
|
310
276
|
self,
|
|
311
277
|
*,
|
|
@@ -48,6 +48,7 @@ from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple
|
|
|
48
48
|
|
|
49
49
|
from latticeai.core.timeutil import parse_iso
|
|
50
50
|
from latticeai.core.timezones import get_timezone
|
|
51
|
+
from latticeai.core.workspace_os_utils import graph_scope_kwargs
|
|
51
52
|
|
|
52
53
|
LOGGER = logging.getLogger(__name__)
|
|
53
54
|
|
|
@@ -249,12 +250,9 @@ def _empty_lanes() -> Dict[str, int]:
|
|
|
249
250
|
return {lane: 0 for lane in _LANES}
|
|
250
251
|
|
|
251
252
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
"allowed_workspaces": {workspace_id} if workspace_id is not None else None,
|
|
256
|
-
"include_legacy_global": workspace_id is None,
|
|
257
|
-
}
|
|
253
|
+
|
|
254
|
+
#: Historical module-local name for the shared rule.
|
|
255
|
+
_scope_kwargs = graph_scope_kwargs
|
|
258
256
|
|
|
259
257
|
|
|
260
258
|
def _on_day(rows: Sequence[Any], day: str) -> List[Any]:
|
|
@@ -28,6 +28,7 @@ from typing import Any, Dict, List, Optional
|
|
|
28
28
|
|
|
29
29
|
from latticeai.core.timeutil import local_now, parse_iso
|
|
30
30
|
from latticeai.core.timeutil import now_iso as _now
|
|
31
|
+
from latticeai.core.workspace_os_utils import graph_scope_kwargs
|
|
31
32
|
|
|
32
33
|
LOGGER = logging.getLogger(__name__)
|
|
33
34
|
|
|
@@ -48,6 +49,8 @@ def _clip(text: Any, limit: int = 160) -> str:
|
|
|
48
49
|
return value[:limit]
|
|
49
50
|
|
|
50
51
|
|
|
52
|
+
|
|
53
|
+
|
|
51
54
|
class CommandCenterService:
|
|
52
55
|
def __init__(
|
|
53
56
|
self,
|
|
@@ -73,10 +76,7 @@ class CommandCenterService:
|
|
|
73
76
|
# ── scoped reads ─────────────────────────────────────────────────────
|
|
74
77
|
|
|
75
78
|
def _scope_kwargs(self, workspace_id: Optional[str]) -> Dict[str, Any]:
|
|
76
|
-
return
|
|
77
|
-
"allowed_workspaces": {workspace_id} if workspace_id is not None else None,
|
|
78
|
-
"include_legacy_global": workspace_id is None,
|
|
79
|
-
}
|
|
79
|
+
return graph_scope_kwargs(workspace_id)
|
|
80
80
|
|
|
81
81
|
def _history(
|
|
82
82
|
self, *, user_email: Optional[str], workspace_id: Optional[str], limit: int
|
|
@@ -28,6 +28,8 @@ import logging
|
|
|
28
28
|
import re
|
|
29
29
|
from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence
|
|
30
30
|
|
|
31
|
+
from latticeai.core.messages import bilingual
|
|
32
|
+
|
|
31
33
|
LOGGER = logging.getLogger(__name__)
|
|
32
34
|
|
|
33
35
|
__all__ = ["EvidenceActionService", "EVIDENCE_ACTIONS"]
|
|
@@ -38,8 +40,9 @@ _EXCERPT_CHARS = 600
|
|
|
38
40
|
_MAX_SOURCES = 8
|
|
39
41
|
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
|
|
44
|
+
#: Historical module-local name for the shared phrase-pair helper.
|
|
45
|
+
_phrase = bilingual
|
|
43
46
|
|
|
44
47
|
|
|
45
48
|
# The action catalog is closed and deterministic — the UI renders exactly
|
|
@@ -13,7 +13,6 @@ auto-commit — so a headless caller stages nothing rather than writing.
|
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
-
import json
|
|
17
16
|
import logging
|
|
18
17
|
from typing import Any, AsyncIterator, Dict, Optional
|
|
19
18
|
|
|
@@ -21,6 +20,7 @@ from latticeai.core.network_boundary import (
|
|
|
21
20
|
NetworkBoundaryMode,
|
|
22
21
|
normalize_network_mode,
|
|
23
22
|
)
|
|
23
|
+
from latticeai.core.sse import sse_frame
|
|
24
24
|
from latticeai.services.cloud_egress_audit import record_cloud_egress
|
|
25
25
|
from latticeai.services.cloud_extraction import plan_kg_expansion_rich
|
|
26
26
|
from latticeai.services.cloud_streaming import (
|
|
@@ -36,7 +36,7 @@ logger = logging.getLogger(__name__)
|
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def _sse(data: Dict[str, Any]) -> str:
|
|
39
|
-
return
|
|
39
|
+
return sse_frame(None, data)
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
def _scope_key(user_email: Optional[str], workspace_id: Optional[str]) -> str:
|