ltcai 11.5.1 → 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 +85 -53
- package/docs/CHANGELOG.md +54 -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.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/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 +5 -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 +2 -0
- 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/generate_agent_loop_fixtures.py +87 -0
- package/scripts/generate_rust_parity_fixtures.py +3 -95
- package/scripts/parity_fixture_corpus_context.py +162 -0
- package/scripts/release_screen_claims.json +10 -0
- package/src-tauri/Cargo.lock +7 -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 +40 -40
- package/static/app/assets/{Act-Drs_jd-O.js → Act-DcQizkl1.js} +1 -1
- package/static/app/assets/{AdminConsole-BqNx5oF6.js → AdminConsole-cf4npybT.js} +1 -1
- package/static/app/assets/{Brain-C7bdNmz-.js → Brain-3VCSHFcn.js} +1 -1
- package/static/app/assets/{BrainHome-DcTnINcI.js → BrainHome-Qm8eaztx.js} +1 -1
- package/static/app/assets/{BrainSignals-Df1MRIPD.js → BrainSignals-DS9BtKOW.js} +1 -1
- package/static/app/assets/{Capture-Dgsxvnbx.js → Capture-DiQ219jW.js} +1 -1
- package/static/app/assets/{Chronicle-BrkTxJK1.js → Chronicle-BGvuAchH.js} +1 -1
- package/static/app/assets/{CommandPalette-DDziw7Oj.js → CommandPalette-Bqhm0Urn.js} +1 -1
- package/static/app/assets/{Library-CYxeuraA.js → Library-BV6NnF0a.js} +1 -1
- package/static/app/assets/{LivingBrain-CNp6mvCm.js → LivingBrain-GzenJchP.js} +1 -1
- package/static/app/assets/{ProductFlow-BVsowu3Z.js → ProductFlow-DEP6-vML.js} +1 -1
- package/static/app/assets/{ReviewCard-D1LbOfJS.js → ReviewCard-CNZ7XjWG.js} +1 -1
- package/static/app/assets/{System-DcRAq7wj.js → System-CieofHQa.js} +1 -1
- package/static/app/assets/arrow-left-kfsrk0mv.js +1 -0
- package/static/app/assets/{bot-BK2xQ9mN.js → bot-B_K1Tdmw.js} +1 -1
- package/static/app/assets/{brain-BZNztFBb.js → brain-DWyaV1L1.js} +1 -1
- package/static/app/assets/{button-CjueubVZ.js → button-aTn4s84A.js} +1 -1
- package/static/app/assets/circle-check-qqLug9nU.js +1 -0
- package/static/app/assets/{circle-pause-HRjSmpyY.js → circle-pause-xKgeGXkT.js} +1 -1
- package/static/app/assets/{circle-play-CMcKDVwu.js → circle-play-DkT6tYPX.js} +1 -1
- package/static/app/assets/{cpu-B_MyTfYr.js → cpu-85xYObUC.js} +1 -1
- package/static/app/assets/{download-CTy0EByV.js → download-B5Fm7YXo.js} +1 -1
- package/static/app/assets/{folder-open-DF6bLhTg.js → folder-open-kk2Xa52u.js} +1 -1
- package/static/app/assets/{hard-drive-CpWHG77C.js → hard-drive-DkA3zBW_.js} +1 -1
- package/static/app/assets/{index-Dd6abJHX.js → index-BMPdTmlY.js} +3 -3
- package/static/app/assets/{input-bVgIRN3s.js → input-B0nRf2jO.js} +1 -1
- package/static/app/assets/{link-2-bg6CG5Vk.js → link-2-Dwb4gnTc.js} +1 -1
- package/static/app/assets/{permissionCopy-DFTf1HDZ.js → permissionCopy-CQDUBrOZ.js} +1 -1
- package/static/app/assets/{primitives-D7D-sQ3T.js → primitives-SNp0LRJz.js} +1 -1
- package/static/app/assets/search-BcHqkjoy.js +1 -0
- package/static/app/assets/{share-2-BLzo7U4L.js → share-2-BsrxFglO.js} +1 -1
- package/static/app/assets/{shield-alert-BmngqnsJ.js → shield-alert-5BStfp2_.js} +1 -1
- package/static/app/assets/{textarea-DOSlAQQ3.js → textarea-Cg8IUA-k.js} +1 -1
- package/static/app/assets/{useFocusTrap-pZhgeee4.js → useFocusTrap-CYKvE46M.js} +1 -1
- package/static/app/assets/{useMutation-BMDwNk4I.js → useMutation-CSn9t1op.js} +1 -1
- package/static/app/assets/{useQuery-CSjKttRo.js → useQuery-CY2OI2uy.js} +1 -1
- package/static/app/assets/{utils-D3u_yv7B.js → utils-Ddol2RWD.js} +1 -1
- package/static/app/assets/{workspace-H3bMjYXC.js → workspace-BqDwOz_p.js} +1 -1
- package/static/app/index.html +3 -3
- 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-BHYmOTWU.js +0 -1
- package/static/app/assets/circle-check-CLeWJr67.js +0 -1
- package/static/app/assets/search-BgSVX6NG.js +0 -1
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
|
-
import importlib.util
|
|
16
15
|
import json
|
|
17
16
|
import logging
|
|
18
17
|
import re
|
|
@@ -22,6 +21,8 @@ from dataclasses import asdict, dataclass
|
|
|
22
21
|
from pathlib import Path
|
|
23
22
|
from typing import Any, Dict, List, Optional, Tuple
|
|
24
23
|
|
|
24
|
+
from latticeai.core.module_probe import module_available as _module_available
|
|
25
|
+
|
|
25
26
|
logger = logging.getLogger(__name__)
|
|
26
27
|
|
|
27
28
|
|
|
@@ -198,13 +199,6 @@ GEMMA4_MLX_LM_MODULES = ("mlx_lm.models.gemma4", "mlx_lm.models.gemma4_text")
|
|
|
198
199
|
GEMMA4_UNIFIED_ID_PATTERN = re.compile(r"gemma[-_/ ]?4[-_/ ]?12b", re.I)
|
|
199
200
|
|
|
200
201
|
|
|
201
|
-
def _module_available(module: str) -> bool:
|
|
202
|
-
try:
|
|
203
|
-
return importlib.util.find_spec(module) is not None
|
|
204
|
-
except (ImportError, ModuleNotFoundError, AttributeError, ValueError):
|
|
205
|
-
return False
|
|
206
|
-
|
|
207
|
-
|
|
208
202
|
def _is_gemma4(model_id: str) -> bool:
|
|
209
203
|
raw = str(model_id or "").lower()
|
|
210
204
|
return bool(re.search(r"gemma[-_/ ]?4", raw))
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""One answer to "is this module importable?".
|
|
2
|
+
|
|
3
|
+
Four modules asked it four different ways: two consulted ``find_spec``, one
|
|
4
|
+
imported for real, and they disagreed about which failures mean "no" and which
|
|
5
|
+
should escape. The disagreement mattered — the setup probe's copy imports for
|
|
6
|
+
real *on purpose*, because a native wheel like ``mlx`` can have a perfectly
|
|
7
|
+
findable spec on the wrong architecture and still raise when it loads. A copy
|
|
8
|
+
that only looked for the spec would report the runtime as present and let the
|
|
9
|
+
recommendation engine plan an install around a module that cannot run.
|
|
10
|
+
|
|
11
|
+
So the two behaviours stay, named: ``strict=False`` asks whether Python can
|
|
12
|
+
*find* the module (cheap, no side effects, no import cost); ``strict=True``
|
|
13
|
+
actually imports it and answers whether it *works*.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import importlib
|
|
19
|
+
import importlib.util
|
|
20
|
+
|
|
21
|
+
__all__ = ["module_available"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def module_available(name: str, *, strict: bool = False) -> bool:
|
|
25
|
+
"""Report whether ``name`` can be imported, never raising.
|
|
26
|
+
|
|
27
|
+
A probe that raises is a probe that takes the caller down for asking, and
|
|
28
|
+
every caller here is deciding what to *offer* the user — "I could not tell"
|
|
29
|
+
and "it is not there" lead to the same honest answer.
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
if strict:
|
|
33
|
+
importlib.import_module(name)
|
|
34
|
+
return True
|
|
35
|
+
return importlib.util.find_spec(name) is not None
|
|
36
|
+
except Exception:
|
|
37
|
+
return False
|
|
@@ -30,6 +30,8 @@ from __future__ import annotations
|
|
|
30
30
|
|
|
31
31
|
from typing import Any, Dict, List, Mapping, Optional, Sequence
|
|
32
32
|
|
|
33
|
+
from latticeai.core.messages import bilingual as _bilingual
|
|
34
|
+
|
|
33
35
|
__all__ = ["explain_run", "STRAIN_LEVELS"]
|
|
34
36
|
|
|
35
37
|
|
|
@@ -53,8 +55,9 @@ _REPAIR_GROUPS: Dict[str, tuple] = {
|
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
|
|
59
|
+
#: Historical module-local name for the shared phrase-pair helper.
|
|
60
|
+
_phrase = _bilingual
|
|
58
61
|
|
|
59
62
|
|
|
60
63
|
def _int(value: Any) -> int:
|
|
@@ -21,7 +21,6 @@ Design constraints (deliberate, matching :mod:`latticeai.services.funnel_metrics
|
|
|
21
21
|
|
|
22
22
|
from __future__ import annotations
|
|
23
23
|
|
|
24
|
-
import hashlib
|
|
25
24
|
import json
|
|
26
25
|
import logging
|
|
27
26
|
import os
|
|
@@ -34,6 +33,7 @@ from typing import Any, Dict, List, Optional
|
|
|
34
33
|
from latticeai.core.agent import AgentRunContext, AgentState
|
|
35
34
|
from latticeai.core.agent_trace import LoopTrace
|
|
36
35
|
from latticeai.core.quiet import quiet
|
|
36
|
+
from latticeai.core.security import sha256_hex
|
|
37
37
|
|
|
38
38
|
LOGGER = logging.getLogger(__name__)
|
|
39
39
|
|
|
@@ -44,7 +44,7 @@ _RUN_ID_RE = re.compile(r"^[A-Za-z0-9_-]{8,64}$")
|
|
|
44
44
|
|
|
45
45
|
def hash_approval_token(token: str) -> str:
|
|
46
46
|
"""Stable digest used both at save and at resume comparison time."""
|
|
47
|
-
return
|
|
47
|
+
return sha256_hex(str(token or ""))
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
def serialize_run_context(ctx: AgentRunContext) -> Dict[str, Any]:
|
|
@@ -13,6 +13,19 @@ from fastapi import HTTPException
|
|
|
13
13
|
from latticeai.core.quiet import quiet
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
def sha256_hex(text: str) -> str:
|
|
17
|
+
"""The one UTF-8 SHA-256 hex digest.
|
|
18
|
+
|
|
19
|
+
Six modules each carried this three-token expression, and they did not
|
|
20
|
+
agree on what to do with ``None``: two coerced it away, one raised. The
|
|
21
|
+
helper stays **strict** on purpose — coercing here would silently give
|
|
22
|
+
every caller the digest of the empty string, which is a valid-looking
|
|
23
|
+
token hash for "no token at all". Each call site keeps the contract it
|
|
24
|
+
already had, stated in its own body where a reader can see it.
|
|
25
|
+
"""
|
|
26
|
+
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
|
27
|
+
|
|
28
|
+
|
|
16
29
|
def hash_password(password: str) -> str:
|
|
17
30
|
salt = secrets.token_hex(16)
|
|
18
31
|
key = hashlib.scrypt(password.encode(), salt=salt.encode(), n=16384, r=8, p=1)
|
|
@@ -6,7 +6,6 @@ holding raw tokens are migrated transparently on load (sessions survive the
|
|
|
6
6
|
upgrade; the raw token never touches disk again).
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
import hashlib
|
|
10
9
|
import json
|
|
11
10
|
import logging
|
|
12
11
|
import secrets
|
|
@@ -16,6 +15,7 @@ from pathlib import Path
|
|
|
16
15
|
from typing import Dict, Optional
|
|
17
16
|
|
|
18
17
|
from latticeai.core.io_utils import atomic_write_json
|
|
18
|
+
from latticeai.core.security import sha256_hex
|
|
19
19
|
|
|
20
20
|
SESSION_TTL = 60 * 60 * 24 # 24 hours
|
|
21
21
|
SESSION_REFRESH_THRESHOLD = 60 * 15 # only persist if >15 min since last bump
|
|
@@ -25,7 +25,8 @@ _HEX64 = frozenset("0123456789abcdef")
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def _hash_token(token: str) -> str:
|
|
28
|
-
|
|
28
|
+
"""Session keys are hashed at rest; a missing token hashes the empty string."""
|
|
29
|
+
return sha256_hex(token or "")
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
def _looks_hashed(key: str) -> bool:
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""The one Server-Sent Events frame builder.
|
|
2
|
+
|
|
3
|
+
Five modules each formatted their own frame — three in the
|
|
4
|
+
``event:``/``data:`` shape and two in the bare ``data:`` shape — with the same
|
|
5
|
+
``json.dumps(..., ensure_ascii=False)`` payload. They agreed by accident, and a
|
|
6
|
+
frame that disagrees by one byte is a stream the browser silently stops
|
|
7
|
+
parsing, so the agreement is now structural rather than coincidental.
|
|
8
|
+
|
|
9
|
+
``event=None`` produces the bare shape; a named event produces the prefixed
|
|
10
|
+
one. Both are byte-identical to the copies they replaced.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
from typing import Any, Optional
|
|
17
|
+
|
|
18
|
+
__all__ = ["sse_frame"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def sse_frame(event: Optional[str], data: Any) -> str:
|
|
22
|
+
"""Render one SSE frame, terminated by the blank line that ends it."""
|
|
23
|
+
payload = json.dumps(data, ensure_ascii=False)
|
|
24
|
+
prefix = f"event: {event}\n" if event else ""
|
|
25
|
+
return f"{prefix}data: {payload}\n\n"
|
package/latticeai/core/users.py
CHANGED
|
@@ -100,15 +100,6 @@ def user_id_for_email(users: Dict[str, Any], email: Optional[str]) -> Optional[s
|
|
|
100
100
|
return stable_user_id(normalized)
|
|
101
101
|
|
|
102
102
|
|
|
103
|
-
def email_for_user_id(users: Dict[str, Any], user_id: Optional[str]) -> Optional[str]:
|
|
104
|
-
if not user_id:
|
|
105
|
-
return None
|
|
106
|
-
for email, user in (users or {}).items():
|
|
107
|
-
if isinstance(user, dict) and user.get("id") == user_id:
|
|
108
|
-
return email
|
|
109
|
-
return None
|
|
110
|
-
|
|
111
|
-
|
|
112
103
|
def migrate_knowledge_graph_identity(db_path: Path, email_to_id: Dict[str, str]) -> int:
|
|
113
104
|
"""Rewrite KG owner/creator identity columns from email to stable UUIDs."""
|
|
114
105
|
if not db_path.exists() or not email_to_id:
|
|
@@ -6,16 +6,10 @@ from typing import Any, Dict, List, Optional
|
|
|
6
6
|
from latticeai.core.quiet import quiet
|
|
7
7
|
|
|
8
8
|
from .timeutil import now_iso as _now
|
|
9
|
-
from .workspace_os_utils import _json_hash, _listify
|
|
9
|
+
from .workspace_os_utils import WorkspaceStoreMixin, _json_hash, _listify
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
class WorkspaceGraphTrace:
|
|
13
|
-
def __init__(self, store: Any):
|
|
14
|
-
self._store = store
|
|
15
|
-
|
|
16
|
-
def __getattr__(self, name: str) -> Any:
|
|
17
|
-
return getattr(self._store, name)
|
|
18
|
-
|
|
12
|
+
class WorkspaceGraphTrace(WorkspaceStoreMixin):
|
|
19
13
|
def build_graph_trace(
|
|
20
14
|
self,
|
|
21
15
|
question: str,
|
|
@@ -53,7 +53,6 @@ from .workspace_permissions import ( # type: ignore
|
|
|
53
53
|
from .workspace_plugins import WorkspacePluginManager
|
|
54
54
|
from .workspace_relationships import WorkspaceRelationships
|
|
55
55
|
from .workspace_relationships import shortest_path as _shortest_path
|
|
56
|
-
from .workspace_reorganization import plan_reorganization
|
|
57
56
|
from .workspace_review_items import WorkspaceReviewItems
|
|
58
57
|
from .workspace_runs import WorkspaceRuns
|
|
59
58
|
from .workspace_skills import WorkspaceSkills
|
|
@@ -844,15 +843,6 @@ class WorkspaceOSStore:
|
|
|
844
843
|
# Agent-native workspace: folder reorganization (v11.1.0)
|
|
845
844
|
# ------------------------------------------------------------------
|
|
846
845
|
|
|
847
|
-
def preview_reorganization(
|
|
848
|
-
self, *, root: str = "", resolve_path: Callable[[str], Any], graph: Any = None,
|
|
849
|
-
max_moves: int = 20,
|
|
850
|
-
) -> Dict[str, Any]:
|
|
851
|
-
"""What a reorganization would do — read-only, nothing is staged."""
|
|
852
|
-
return plan_reorganization(
|
|
853
|
-
root=root, resolve_path=resolve_path, graph=graph, max_moves=max_moves
|
|
854
|
-
)
|
|
855
|
-
|
|
856
846
|
def propose_reorganization(
|
|
857
847
|
self, *, root: str = "", change_proposals: Any, graph: Any = None,
|
|
858
848
|
user_email: Optional[str] = None, workspace_id: Optional[str] = None,
|
|
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
from typing import Dict
|
|
12
12
|
|
|
13
|
-
WORKSPACE_OS_VERSION = "11.5.
|
|
13
|
+
WORKSPACE_OS_VERSION = "11.5.2"
|
|
14
14
|
|
|
15
15
|
# Workspace types separate single-user Personal workspaces from shared
|
|
16
16
|
# Organization workspaces. Both keep the same local-first JSON store; the type
|
|
@@ -17,6 +17,40 @@ from .io_utils import (
|
|
|
17
17
|
from .io_utils import parse_iso as _parse_iso # noqa: F401 - legacy helper re-export
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
class WorkspaceStoreMixin:
|
|
21
|
+
"""A collaborator that reads and writes through ``WorkspaceOSStore``.
|
|
22
|
+
|
|
23
|
+
The store was decomposed into managers that each own one branch of the state
|
|
24
|
+
document, and every one of them needs the same two things: a reference to the
|
|
25
|
+
store, and for anything it does not implement itself (``load_state``,
|
|
26
|
+
``_scoped``, ``record_timeline_event``) to reach the store instead of raising
|
|
27
|
+
``AttributeError``. Four managers wrote that out identically; the pair lives
|
|
28
|
+
here so a fifth cannot invent a third spelling of it.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, store: Any) -> None:
|
|
32
|
+
self._store = store
|
|
33
|
+
|
|
34
|
+
def __getattr__(self, name: str) -> Any:
|
|
35
|
+
# Only reached for attributes this manager does not define, so the
|
|
36
|
+
# store is the answer for every state-document operation.
|
|
37
|
+
return getattr(self._store, name)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def graph_scope_kwargs(workspace_id: Any) -> Dict[str, Any]:
|
|
41
|
+
"""The scoped-read kwargs the graph store expects (house pattern).
|
|
42
|
+
|
|
43
|
+
``workspace_id is None`` means "unscoped caller": it reads everything it is
|
|
44
|
+
allowed to, legacy rows that predate workspaces included. A named workspace
|
|
45
|
+
reads only that workspace and never the legacy pool — which is the whole
|
|
46
|
+
isolation guarantee, so it is stated once.
|
|
47
|
+
"""
|
|
48
|
+
return {
|
|
49
|
+
"allowed_workspaces": {workspace_id} if workspace_id is not None else None,
|
|
50
|
+
"include_legacy_global": workspace_id is None,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
20
54
|
def _safe_slug(raw: str) -> str:
|
|
21
55
|
value = "".join(ch if ch.isalnum() or ch in "-_." else "-" for ch in str(raw or "").strip())
|
|
22
56
|
value = "-".join(part for part in value.split("-") if part)
|
|
@@ -14,16 +14,10 @@ from __future__ import annotations
|
|
|
14
14
|
from typing import Any, Dict, List, Optional
|
|
15
15
|
|
|
16
16
|
from .timeutil import now_iso as _now
|
|
17
|
-
from .workspace_os_utils import _json_hash, _listify
|
|
17
|
+
from .workspace_os_utils import WorkspaceStoreMixin, _json_hash, _listify
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
class WorkspaceReviewItems:
|
|
21
|
-
def __init__(self, store: Any):
|
|
22
|
-
self._store = store
|
|
23
|
-
|
|
24
|
-
def __getattr__(self, name: str) -> Any:
|
|
25
|
-
return getattr(self._store, name)
|
|
26
|
-
|
|
20
|
+
class WorkspaceReviewItems(WorkspaceStoreMixin):
|
|
27
21
|
def create_review_item(
|
|
28
22
|
self,
|
|
29
23
|
*,
|
|
@@ -8,16 +8,10 @@ from lattice_brain.runtime.contracts import run_record_contract, workflow_run_co
|
|
|
8
8
|
from lattice_brain.runtime.statuses import RUN_ACTIVE_STATUSES, RUN_TERMINAL_STATUSES
|
|
9
9
|
|
|
10
10
|
from .timeutil import now_iso as _now
|
|
11
|
-
from .workspace_os_utils import _json_hash, _listify
|
|
11
|
+
from .workspace_os_utils import WorkspaceStoreMixin, _json_hash, _listify
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
class WorkspaceRuns:
|
|
15
|
-
def __init__(self, store: Any):
|
|
16
|
-
self._store = store
|
|
17
|
-
|
|
18
|
-
def __getattr__(self, name: str) -> Any:
|
|
19
|
-
return getattr(self._store, name)
|
|
20
|
-
|
|
14
|
+
class WorkspaceRuns(WorkspaceStoreMixin):
|
|
21
15
|
def list_agents(self, workspace_id: Optional[str] = None) -> Dict[str, Any]:
|
|
22
16
|
state = self.load_state()
|
|
23
17
|
runs = self._scoped(_listify(state.get("agent_runs")), workspace_id)
|
|
@@ -6,15 +6,10 @@ from pathlib import Path
|
|
|
6
6
|
from typing import Any, Dict, List, Optional
|
|
7
7
|
|
|
8
8
|
from .timeutil import now_iso as _now
|
|
9
|
+
from .workspace_os_utils import WorkspaceStoreMixin
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
class WorkspaceSkills:
|
|
12
|
-
def __init__(self, store: Any):
|
|
13
|
-
self._store = store
|
|
14
|
-
|
|
15
|
-
def __getattr__(self, name: str) -> Any:
|
|
16
|
-
return getattr(self._store, name)
|
|
17
|
-
|
|
12
|
+
class WorkspaceSkills(WorkspaceStoreMixin):
|
|
18
13
|
def list_skill_registry(self, skills_dir: Path, marketplace: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]:
|
|
19
14
|
state = self.load_state()
|
|
20
15
|
registry = state.setdefault("skill_registry", {})
|
|
@@ -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
|
|
|
@@ -305,6 +305,7 @@ def phase_interaction(ctx: RuntimeContext) -> None:
|
|
|
305
305
|
from latticeai.core.model_compat import (
|
|
306
306
|
list_cached_profiles as _list_compat_profiles,
|
|
307
307
|
)
|
|
308
|
+
from latticeai.runtime.access_runtime import is_externally_reachable
|
|
308
309
|
from latticeai.runtime.chat_wiring import build_interaction_contexts
|
|
309
310
|
from latticeai.runtime.model_wiring import register_model_runtime_routers
|
|
310
311
|
from latticeai.runtime.platform_services_runtime import build_brain_network
|
|
@@ -340,6 +341,7 @@ def phase_interaction(ctx: RuntimeContext) -> None:
|
|
|
340
341
|
engine_status=service.engine_status,
|
|
341
342
|
get_current_user=ctx.get_current_user,
|
|
342
343
|
require_auth=ctx.REQUIRE_AUTH,
|
|
344
|
+
externally_reachable=is_externally_reachable(ctx.CONFIG),
|
|
343
345
|
app_version=ctx.APP_VERSION,
|
|
344
346
|
app_mode=ctx.APP_MODE,
|
|
345
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(
|