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
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Which host a request *really* arrived at, when something proxied it.
|
|
2
|
+
|
|
3
|
+
The desktop front door is a reverse proxy: the browser talks to the gateway on
|
|
4
|
+
one port and the gateway forwards to the worker on another. ``Host`` is a
|
|
5
|
+
hop-by-hop header, so it is replaced on the way out and the worker sees only its
|
|
6
|
+
own internal authority — which is why every cookie-authenticated write through
|
|
7
|
+
an adopted worker was rejected as ``csrf_origin_rejected``. The gateway states
|
|
8
|
+
the fact it destroyed in ``X-Forwarded-Host`` / ``X-Forwarded-Proto``; this
|
|
9
|
+
module decides when to believe it.
|
|
10
|
+
|
|
11
|
+
Threat model, so the rule is checkable rather than felt:
|
|
12
|
+
|
|
13
|
+
* **A browser cannot set these headers.** ``X-Forwarded-*`` are not on the
|
|
14
|
+
fetch-safelist and a page cannot add them cross-origin without a preflight
|
|
15
|
+
the CORS allowlist rejects. So no *web page* can reach this code path.
|
|
16
|
+
* **A local non-browser caller could already forge ``Origin``.** curl on this
|
|
17
|
+
machine can send any ``Origin`` it likes, and the loopback exemption in
|
|
18
|
+
:mod:`latticeai.core.csrf` already trusts it. Honouring a forwarded host from
|
|
19
|
+
the same caller widens nothing that was not already open.
|
|
20
|
+
* **Anyone else is not trusted.** Off-loopback the header is honoured only from
|
|
21
|
+
a peer the operator listed in ``LATTICEAI_TRUSTED_PROXIES`` — the same
|
|
22
|
+
allowlist ``latticeai.core.security.client_ip`` uses for the same reason, and
|
|
23
|
+
empty by default.
|
|
24
|
+
|
|
25
|
+
The functions are pure: the caller supplies the headers and the peer address, so
|
|
26
|
+
the policy is testable without an ASGI app or a socket.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import ipaddress
|
|
32
|
+
from typing import Any, Optional
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"FORWARDED_HOST_HEADER",
|
|
36
|
+
"FORWARDED_PROTO_HEADER",
|
|
37
|
+
"effective_host",
|
|
38
|
+
"effective_origin",
|
|
39
|
+
"peer_may_forward",
|
|
40
|
+
"request_external_origin",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
#: The authority the client asked for, as stated by the proxy in front of us.
|
|
44
|
+
FORWARDED_HOST_HEADER = "x-forwarded-host"
|
|
45
|
+
#: The scheme the client used to reach that proxy.
|
|
46
|
+
FORWARDED_PROTO_HEADER = "x-forwarded-proto"
|
|
47
|
+
|
|
48
|
+
# Only these two are ever taken from a forwarded scheme; anything else is a
|
|
49
|
+
# value we would have to reason about, and the fallback is already correct.
|
|
50
|
+
_KNOWN_SCHEMES = ("https", "http")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def peer_may_forward(peer: Optional[str]) -> bool:
|
|
54
|
+
"""Whether ``X-Forwarded-*`` from this direct peer may be believed.
|
|
55
|
+
|
|
56
|
+
Loopback, or a member of the configured trusted-proxy allowlist. An
|
|
57
|
+
unparseable or absent peer is *not* trusted: a request whose origin we
|
|
58
|
+
cannot establish is exactly the one whose claims we must not take.
|
|
59
|
+
"""
|
|
60
|
+
if not peer:
|
|
61
|
+
return False
|
|
62
|
+
try:
|
|
63
|
+
address = ipaddress.ip_address(str(peer).strip())
|
|
64
|
+
except ValueError:
|
|
65
|
+
return False
|
|
66
|
+
if address.is_loopback:
|
|
67
|
+
return True
|
|
68
|
+
from latticeai.core.security import _peer_is_trusted_proxy
|
|
69
|
+
|
|
70
|
+
return _peer_is_trusted_proxy(str(peer).strip())
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _first(value: Optional[str]) -> str:
|
|
74
|
+
"""The first entry of a possibly comma-joined forwarded header."""
|
|
75
|
+
return (value or "").split(",")[0].strip()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def effective_host(
|
|
79
|
+
*,
|
|
80
|
+
host: Optional[str],
|
|
81
|
+
forwarded_host: Optional[str] = None,
|
|
82
|
+
peer: Optional[str] = None,
|
|
83
|
+
) -> Optional[str]:
|
|
84
|
+
"""The authority the client aimed at: the forwarded one when believable.
|
|
85
|
+
|
|
86
|
+
Falls back to the request's own ``Host`` in every other case — no proxy, a
|
|
87
|
+
peer we do not trust, or a forwarded header that is empty. ``None`` only
|
|
88
|
+
when there is nothing to report at all (an HTTP/1.0 request with no ``Host``
|
|
89
|
+
and no proxy), which callers already have to handle.
|
|
90
|
+
"""
|
|
91
|
+
if peer_may_forward(peer):
|
|
92
|
+
claimed = _first(forwarded_host)
|
|
93
|
+
if claimed:
|
|
94
|
+
return claimed
|
|
95
|
+
own = (host or "").strip()
|
|
96
|
+
return own or None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def effective_origin(
|
|
100
|
+
*,
|
|
101
|
+
host: Optional[str],
|
|
102
|
+
scheme: str = "http",
|
|
103
|
+
forwarded_host: Optional[str] = None,
|
|
104
|
+
forwarded_proto: Optional[str] = None,
|
|
105
|
+
peer: Optional[str] = None,
|
|
106
|
+
) -> Optional[str]:
|
|
107
|
+
"""``scheme://authority`` for the front door this request came through.
|
|
108
|
+
|
|
109
|
+
``scheme`` is what *this* server is speaking; the forwarded scheme wins when
|
|
110
|
+
the peer is trusted and states one we recognise, because a TLS-terminating
|
|
111
|
+
proxy speaks plain HTTP to us while the browser is on ``https`` and a link
|
|
112
|
+
built with the wrong scheme is a broken link.
|
|
113
|
+
"""
|
|
114
|
+
authority = effective_host(host=host, forwarded_host=forwarded_host, peer=peer)
|
|
115
|
+
if not authority:
|
|
116
|
+
return None
|
|
117
|
+
resolved = (scheme or "http").strip().lower() or "http"
|
|
118
|
+
if peer_may_forward(peer):
|
|
119
|
+
claimed = _first(forwarded_proto).lower()
|
|
120
|
+
if claimed in _KNOWN_SCHEMES:
|
|
121
|
+
resolved = claimed
|
|
122
|
+
return f"{resolved}://{authority}"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def request_external_origin(request: Any, *, fallback: Optional[str] = None) -> Optional[str]:
|
|
126
|
+
""":func:`effective_origin` for a live request — the *link-building* seam.
|
|
127
|
+
|
|
128
|
+
CSRF asks "is this same-origin?"; this asks the other question: "what URL
|
|
129
|
+
should I hand a person so they can come back here?" Both must read the same
|
|
130
|
+
front door, or an invite link mails out the internal worker port that only
|
|
131
|
+
the gateway can reach.
|
|
132
|
+
|
|
133
|
+
``fallback`` is returned when the request states no authority at all, so a
|
|
134
|
+
caller with a configured public URL keeps it.
|
|
135
|
+
"""
|
|
136
|
+
headers = getattr(request, "headers", None) or {}
|
|
137
|
+
client = getattr(request, "client", None)
|
|
138
|
+
url = getattr(request, "url", None)
|
|
139
|
+
origin = effective_origin(
|
|
140
|
+
host=headers.get("host"),
|
|
141
|
+
scheme=getattr(url, "scheme", "http") or "http",
|
|
142
|
+
forwarded_host=headers.get(FORWARDED_HOST_HEADER),
|
|
143
|
+
forwarded_proto=headers.get(FORWARDED_PROTO_HEADER),
|
|
144
|
+
peer=getattr(client, "host", None) if client else None,
|
|
145
|
+
)
|
|
146
|
+
return origin or fallback
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import hashlib
|
|
6
5
|
import json
|
|
7
6
|
import secrets
|
|
8
7
|
from datetime import datetime, timedelta
|
|
@@ -10,6 +9,7 @@ from pathlib import Path
|
|
|
10
9
|
from typing import Any, Dict, List, Optional
|
|
11
10
|
|
|
12
11
|
from latticeai.core.quiet import quiet
|
|
12
|
+
from latticeai.core.security import sha256_hex
|
|
13
13
|
|
|
14
14
|
from .timeutil import local_now as _now
|
|
15
15
|
|
|
@@ -19,7 +19,8 @@ def _iso(dt: datetime) -> str:
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def _hash_token(token: str) -> str:
|
|
22
|
-
|
|
22
|
+
"""Strict by design: an invite code is always a string by the time it lands here."""
|
|
23
|
+
return sha256_hex(token)
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
def _atomic_write(path: Path, data: Dict[str, Any]) -> None:
|
|
@@ -5,10 +5,10 @@ from __future__ import annotations
|
|
|
5
5
|
import hashlib
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
|
-
from datetime import datetime
|
|
9
8
|
from pathlib import Path
|
|
10
|
-
from typing import Any, Dict
|
|
9
|
+
from typing import Any, Dict
|
|
11
10
|
|
|
11
|
+
from lattice_brain.utils import parse_iso as parse_iso
|
|
12
12
|
from latticeai.core.quiet import quiet
|
|
13
13
|
|
|
14
14
|
|
|
@@ -27,15 +27,6 @@ def atomic_write_json(path: Path, payload: Dict[str, Any]) -> None:
|
|
|
27
27
|
quiet()
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
def parse_iso(value: Optional[str]) -> Optional[datetime]:
|
|
31
|
-
if not value:
|
|
32
|
-
return None
|
|
33
|
-
try:
|
|
34
|
-
return datetime.fromisoformat(str(value))
|
|
35
|
-
except (TypeError, ValueError):
|
|
36
|
-
return None
|
|
37
|
-
|
|
38
|
-
|
|
39
30
|
def sha256_file(path: Path) -> str:
|
|
40
31
|
digest = hashlib.sha256()
|
|
41
32
|
with open(path, "rb") as fh:
|
|
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
from copy import deepcopy
|
|
11
11
|
from typing import Any, Dict, List, Optional
|
|
12
12
|
|
|
13
|
-
MARKETPLACE_VERSION = "11.5.
|
|
13
|
+
MARKETPLACE_VERSION = "11.5.2"
|
|
14
14
|
TEMPLATE_KINDS = ("plugin", "workflow", "agent", "ingestion_bridge")
|
|
15
15
|
|
|
16
16
|
|
|
@@ -536,6 +536,41 @@ MESSAGES: Dict[str, Dict[str, str]] = {
|
|
|
536
536
|
"ko": "한 번에 처리할 개수는 {min}에서 {max} 사이여야 합니다.",
|
|
537
537
|
"en": "Ask for between {min} and {max} items in one pass.",
|
|
538
538
|
},
|
|
539
|
+
# ── AI-Worker seam (the Rust loop's three calls back into Python) ────
|
|
540
|
+
"agent_seam.disabled": {
|
|
541
|
+
"ko": "워커 시임이 꺼져 있습니다. 호스트가 직접 띄운 워커에서만 열립니다.",
|
|
542
|
+
"en": "The worker seam is off. It opens only in a worker the host started itself.",
|
|
543
|
+
},
|
|
544
|
+
"agent_seam.message_required": {
|
|
545
|
+
"ko": "모델에 보낼 내용을 적어주세요.",
|
|
546
|
+
"en": "Write the message to send to the model.",
|
|
547
|
+
},
|
|
548
|
+
"agent_seam.max_tokens_out_of_range": {
|
|
549
|
+
"ko": "한 번에 만들 길이는 {min}에서 {max} 사이여야 합니다.",
|
|
550
|
+
"en": "Ask for between {min} and {max} tokens in one completion.",
|
|
551
|
+
},
|
|
552
|
+
"agent_seam.temperature_out_of_range": {
|
|
553
|
+
"ko": "온도는 {min}에서 {max} 사이여야 합니다.",
|
|
554
|
+
"en": "Temperature has to be between {min} and {max}.",
|
|
555
|
+
},
|
|
556
|
+
"agent_seam.tool_required": {
|
|
557
|
+
"ko": "실행할 도구 이름이 필요합니다.",
|
|
558
|
+
"en": "A tool name to run is required.",
|
|
559
|
+
},
|
|
560
|
+
"agent_seam.tool_blocked": {
|
|
561
|
+
"ko": "'{tool}' 도구는 어떤 모드에서도 차단됩니다 ({reason}).",
|
|
562
|
+
"en": "'{tool}' is blocked in every mode ({reason}).",
|
|
563
|
+
},
|
|
564
|
+
"agent_seam.tool_fail_closed": {
|
|
565
|
+
"ko": "'{tool}' 도구는 기존 내용을 바꾸지만 검토할 수 있는 제안으로 "
|
|
566
|
+
"만들 수 없어 차단했습니다 ({reason}).",
|
|
567
|
+
"en": "'{tool}' would change existing content but cannot be staged as a "
|
|
568
|
+
"reviewable proposal, so it is blocked ({reason}).",
|
|
569
|
+
},
|
|
570
|
+
"agent_seam.proposals_unavailable": {
|
|
571
|
+
"ko": "변경 제안 서비스가 연결되어 있지 않습니다.",
|
|
572
|
+
"en": "The change proposal service is not connected.",
|
|
573
|
+
},
|
|
539
574
|
# ── MCP / setup ─────────────────────────────────────────────────────
|
|
540
575
|
"mcp.connector_not_found": {
|
|
541
576
|
"ko": "커넥터를 찾을 수 없습니다.",
|
|
@@ -588,6 +623,11 @@ MESSAGES: Dict[str, Dict[str, str]] = {
|
|
|
588
623
|
}
|
|
589
624
|
|
|
590
625
|
|
|
626
|
+
def bilingual(ko: str, en: str) -> Dict[str, str]:
|
|
627
|
+
"""One Korean/English phrase pair, in the shape every caller renders."""
|
|
628
|
+
return {"ko": ko, "en": en}
|
|
629
|
+
|
|
630
|
+
|
|
591
631
|
def resolve_language(request: Any, default: str = DEFAULT_LANGUAGE) -> str:
|
|
592
632
|
"""Language for this request: the product's choice, then the browser's.
|
|
593
633
|
|
|
@@ -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", {})
|