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.
Files changed (182) hide show
  1. package/README.md +86 -47
  2. package/docs/CHANGELOG.md +76 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/HYBRID_CLOUD_KG_STREAMING.md +8 -4
  6. package/docs/MULTI_AGENT_RUNTIME.md +1 -1
  7. package/docs/ONBOARDING.md +1 -1
  8. package/docs/OPERATIONS.md +1 -1
  9. package/docs/TRUST_MODEL.md +1 -1
  10. package/docs/WHY_LATTICE.md +1 -1
  11. package/docs/WORKFLOW_DESIGNER.md +1 -1
  12. package/docs/architecture.md +4 -4
  13. package/docs/kg-schema.md +1 -1
  14. package/docs/v11.5.1_RUST_FULL_LOOP_PLAN.md +85 -0
  15. package/docs/v11.5.2_TIGHT_SHIP_PLAN.md +144 -0
  16. package/lattice_brain/__init__.py +1 -1
  17. package/lattice_brain/graph/proactive.py +1 -1
  18. package/lattice_brain/graph/projection/v2_schema.py +0 -28
  19. package/lattice_brain/graph/vector_index/base.py +0 -3
  20. package/lattice_brain/graph/vector_index/brute_force.py +0 -4
  21. package/lattice_brain/graph/vector_index/hnsw.py +0 -3
  22. package/lattice_brain/graph/vector_index/quantized.py +0 -3
  23. package/lattice_brain/ingestion_jobs.py +0 -3
  24. package/lattice_brain/memory.py +0 -27
  25. package/lattice_brain/portability/sharing.py +0 -4
  26. package/lattice_brain/quality.py +0 -49
  27. package/lattice_brain/runtime/agent_runtime.py +1 -1
  28. package/lattice_brain/runtime/hooks.py +0 -13
  29. package/lattice_brain/runtime/multi_agent.py +1 -1
  30. package/lattice_brain/sealed_box.py +0 -4
  31. package/latticeai/__init__.py +1 -1
  32. package/latticeai/api/admin.py +12 -12
  33. package/latticeai/api/agent_worker_seam.py +389 -0
  34. package/latticeai/api/auth.py +25 -2
  35. package/latticeai/api/chat.py +3 -3
  36. package/latticeai/api/chat_agent_http.py +3 -12
  37. package/latticeai/api/chat_helpers.py +10 -13
  38. package/latticeai/api/computer_use.py +31 -42
  39. package/latticeai/api/health.py +21 -1
  40. package/latticeai/api/local_files.py +14 -0
  41. package/latticeai/api/permissions.py +38 -12
  42. package/latticeai/api/search.py +24 -0
  43. package/latticeai/api/static_routes.py +4 -1
  44. package/latticeai/cli/runtime.py +7 -3
  45. package/latticeai/core/config.py +47 -3
  46. package/latticeai/core/csrf.py +28 -2
  47. package/latticeai/core/embedding_providers/__init__.py +3 -5
  48. package/latticeai/core/embedding_providers/base.py +1 -1
  49. package/latticeai/core/embedding_providers/text.py +1 -1
  50. package/latticeai/core/enterprise.py +1 -4
  51. package/latticeai/core/http_origin.py +146 -0
  52. package/latticeai/core/invitations.py +3 -2
  53. package/latticeai/core/io_utils.py +2 -11
  54. package/latticeai/core/legacy_compatibility.py +1 -1
  55. package/latticeai/core/marketplace.py +1 -1
  56. package/latticeai/core/messages.py +40 -0
  57. package/latticeai/core/model_compat.py +2 -8
  58. package/latticeai/core/module_probe.py +37 -0
  59. package/latticeai/core/run_explain.py +5 -2
  60. package/latticeai/core/run_store.py +2 -2
  61. package/latticeai/core/security.py +13 -0
  62. package/latticeai/core/sessions.py +3 -2
  63. package/latticeai/core/sse.py +25 -0
  64. package/latticeai/core/users.py +0 -9
  65. package/latticeai/core/workspace_graph_trace.py +2 -8
  66. package/latticeai/core/workspace_os.py +0 -10
  67. package/latticeai/core/workspace_os_constants.py +1 -1
  68. package/latticeai/core/workspace_os_utils.py +34 -0
  69. package/latticeai/core/workspace_review_items.py +2 -8
  70. package/latticeai/core/workspace_runs.py +2 -8
  71. package/latticeai/core/workspace_skills.py +2 -7
  72. package/latticeai/models/router/documents.py +0 -35
  73. package/latticeai/models/router/registry.py +0 -4
  74. package/latticeai/runtime/access_runtime.py +16 -4
  75. package/latticeai/runtime/build_phases/features.py +25 -2
  76. package/latticeai/runtime/build_phases/web.py +2 -0
  77. package/latticeai/runtime/feature_toggle_wiring.py +12 -18
  78. package/latticeai/runtime/network_boundary_wiring.py +8 -19
  79. package/latticeai/runtime/permission_mode_wiring.py +6 -13
  80. package/latticeai/runtime/router_registration.py +4 -0
  81. package/latticeai/runtime/runtime_context.py +1 -13
  82. package/latticeai/runtime/service_singletons.py +55 -0
  83. package/latticeai/services/architecture_readiness.py +1 -1
  84. package/latticeai/services/brain_intelligence/proposals.py +0 -5
  85. package/latticeai/services/change_proposals.py +2 -36
  86. package/latticeai/services/chronicle.py +4 -6
  87. package/latticeai/services/command_center.py +4 -4
  88. package/latticeai/services/evidence_actions.py +5 -2
  89. package/latticeai/services/hybrid_chat.py +2 -2
  90. package/latticeai/services/hybrid_policy.py +8 -57
  91. package/latticeai/services/mode_store.py +132 -0
  92. package/latticeai/services/model_capability_registry.py +0 -15
  93. package/latticeai/services/model_catalog.py +0 -7
  94. package/latticeai/services/model_loading.py +3 -2
  95. package/latticeai/services/model_runtime/__init__.py +0 -57
  96. package/latticeai/services/model_runtime/engines.py +27 -128
  97. package/latticeai/services/model_runtime/loading.py +2 -2
  98. package/latticeai/services/network_boundary_service.py +7 -44
  99. package/latticeai/services/permission_mode_service.py +8 -56
  100. package/latticeai/services/product_readiness.py +1 -1
  101. package/latticeai/services/setup_detection.py +67 -1
  102. package/latticeai/services/tool_dispatch.py +0 -4
  103. package/latticeai/services/upload_service.py +7 -20
  104. package/latticeai/services/workspace_service.py +0 -6
  105. package/latticeai/setup/auto_setup.py +15 -26
  106. package/latticeai/setup/wizard/catalog.py +5 -2
  107. package/latticeai/setup/wizard/detect.py +6 -27
  108. package/latticeai/setup/wizard/paths.py +2 -5
  109. package/latticeai/setup/wizard/plans.py +2 -2
  110. package/package.json +2 -4
  111. package/scripts/bump_version.py +14 -1
  112. package/scripts/check_current_release_docs.mjs +1 -1
  113. package/scripts/check_legacy_debt.mjs +1 -1
  114. package/scripts/check_server_i18n.mjs +1 -0
  115. package/scripts/generate_agent_loop_fixtures.py +994 -0
  116. package/scripts/generate_rust_parity_fixtures.py +72 -161
  117. package/scripts/parity_fixture_corpus_context.py +162 -0
  118. package/scripts/parity_fixture_corpus_docgen.py +341 -0
  119. package/scripts/release_screen_claims.json +20 -0
  120. package/src-tauri/Cargo.lock +13 -7
  121. package/src-tauri/Cargo.toml +1 -1
  122. package/src-tauri/src/backend.rs +76 -2
  123. package/src-tauri/src/main.rs +13 -3
  124. package/src-tauri/tauri.conf.json +2 -2
  125. package/static/app/asset-manifest.json +41 -41
  126. package/static/app/assets/{Act-CWnxSCgN.js → Act-DcQizkl1.js} +1 -1
  127. package/static/app/assets/{AdminConsole-BEQYU6kF.js → AdminConsole-cf4npybT.js} +1 -1
  128. package/static/app/assets/{Brain-DWu1BhFg.js → Brain-3VCSHFcn.js} +1 -1
  129. package/static/app/assets/{BrainHome-95Hilr9R.js → BrainHome-Qm8eaztx.js} +1 -1
  130. package/static/app/assets/{BrainSignals-QdeqCpAF.js → BrainSignals-DS9BtKOW.js} +1 -1
  131. package/static/app/assets/{Capture-BHpCxnzb.js → Capture-DiQ219jW.js} +1 -1
  132. package/static/app/assets/{Chronicle-B4xYKoed.js → Chronicle-BGvuAchH.js} +1 -1
  133. package/static/app/assets/{CommandPalette-BVXnttSz.js → CommandPalette-Bqhm0Urn.js} +1 -1
  134. package/static/app/assets/{Library-DgYcHome.js → Library-BV6NnF0a.js} +1 -1
  135. package/static/app/assets/{LivingBrain-CrJLDbf7.js → LivingBrain-GzenJchP.js} +1 -1
  136. package/static/app/assets/{ProductFlow-DFlScKoJ.js → ProductFlow-DEP6-vML.js} +1 -1
  137. package/static/app/assets/{ReviewCard-Cy5f48Pj.js → ReviewCard-CNZ7XjWG.js} +1 -1
  138. package/static/app/assets/{System-NF8IfhTa.js → System-CieofHQa.js} +1 -1
  139. package/static/app/assets/arrow-left-kfsrk0mv.js +1 -0
  140. package/static/app/assets/{bot-CucuhLhm.js → bot-B_K1Tdmw.js} +1 -1
  141. package/static/app/assets/{brain-BBnSryW_.js → brain-DWyaV1L1.js} +1 -1
  142. package/static/app/assets/{button-C2GUj2Ai.js → button-aTn4s84A.js} +1 -1
  143. package/static/app/assets/circle-check-qqLug9nU.js +1 -0
  144. package/static/app/assets/{circle-pause-CbkWzBmG.js → circle-pause-xKgeGXkT.js} +1 -1
  145. package/static/app/assets/{circle-play-7lEaqHdJ.js → circle-play-DkT6tYPX.js} +1 -1
  146. package/static/app/assets/{cpu-DAlCXlIy.js → cpu-85xYObUC.js} +1 -1
  147. package/static/app/assets/{download-RNhuuJwh.js → download-B5Fm7YXo.js} +1 -1
  148. package/static/app/assets/{folder-open-CLW4odzM.js → folder-open-kk2Xa52u.js} +1 -1
  149. package/static/app/assets/{hard-drive-NKEiDIAJ.js → hard-drive-DkA3zBW_.js} +1 -1
  150. package/static/app/assets/{index-DMurvUuR.js → index-BMPdTmlY.js} +3 -3
  151. package/static/app/assets/index-DxmOfNRi.css +2 -0
  152. package/static/app/assets/{input-D2UhPC1X.js → input-B0nRf2jO.js} +1 -1
  153. package/static/app/assets/{link-2-6amKbP_P.js → link-2-Dwb4gnTc.js} +1 -1
  154. package/static/app/assets/{permissionCopy-Cu9TZtdR.js → permissionCopy-CQDUBrOZ.js} +1 -1
  155. package/static/app/assets/{primitives-gPsccucr.js → primitives-SNp0LRJz.js} +1 -1
  156. package/static/app/assets/search-BcHqkjoy.js +1 -0
  157. package/static/app/assets/{share-2-Bau7KkPq.js → share-2-BsrxFglO.js} +1 -1
  158. package/static/app/assets/{shield-alert-BufNYypi.js → shield-alert-5BStfp2_.js} +1 -1
  159. package/static/app/assets/{textarea-BQnVWhYs.js → textarea-Cg8IUA-k.js} +1 -1
  160. package/static/app/assets/{useFocusTrap-B3_w60si.js → useFocusTrap-CYKvE46M.js} +1 -1
  161. package/static/app/assets/{useMutation-BHhCflT6.js → useMutation-CSn9t1op.js} +1 -1
  162. package/static/app/assets/{useQuery-rBWfI-5t.js → useQuery-CY2OI2uy.js} +1 -1
  163. package/static/app/assets/{utils-V_5-wxr5.js → utils-Ddol2RWD.js} +1 -1
  164. package/static/app/assets/{workspace-K1zjYUHj.js → workspace-BqDwOz_p.js} +1 -1
  165. package/static/app/index.html +4 -4
  166. package/static/sw.js +1 -1
  167. package/desktop/electron/README.md +0 -9
  168. package/desktop/electron/main.cjs +0 -58
  169. package/desktop/electron/preload.cjs +0 -5
  170. package/latticeai/core/graph_curator.py +0 -11
  171. package/latticeai/core/hooks.py +0 -11
  172. package/latticeai/core/local_embeddings.py +0 -104
  173. package/latticeai/core/multi_agent.py +0 -11
  174. package/latticeai/core/workflow_engine.py +0 -11
  175. package/latticeai/services/ingestion.py +0 -11
  176. package/latticeai/services/kg_portability.py +0 -11
  177. package/latticeai/services/multimodal_streaming.py +0 -129
  178. package/scripts/measure_brain_home_fill.mjs +0 -141
  179. package/static/app/assets/arrow-left-DwkSYrjR.js +0 -1
  180. package/static/app/assets/circle-check-CxOVPwYq.js +0 -1
  181. package/static/app/assets/index-BLPb5lmE.css +0 -2
  182. package/static/app/assets/search-Cj_TKk_2.js +0 -1
@@ -644,7 +644,7 @@ def gate_ingest_candidate(
644
644
  """Score whether new content should be ingested into the graph.
645
645
 
646
646
  Pure function — no store access, no side effects. Intended adoption point
647
- for the ingestion pipeline (``services/ingestion.py`` /
647
+ for the ingestion pipeline (``lattice_brain.ingestion`` /
648
648
  ``lattice_brain.graph.ingest``), which passes a closure over its own
649
649
  scoped search, e.g.::
650
650
 
@@ -516,31 +516,3 @@ class KnowledgeGraphV2SchemaMixin(_Core):
516
516
  except Exception as ex:
517
517
  logging.debug("knowledge_graph: v2 edge delete mirror skipped: %s", ex)
518
518
 
519
- def _v2_sync_report(self) -> Dict[str, Any]:
520
- """Diagnose the dual-write invariant: legacy node/edge id sets must equal
521
- the v2 projection's. Returns counts + any drift (ids missing from / extra
522
- in v2). ``in_sync`` is True only when both id sets match exactly.
523
-
524
- All legacy writes go through _upsert_node/_upsert_edge (which dual-write)
525
- and every legacy delete is mirrored, so a non-empty drift signals a
526
- bypassed write path — this is the runtime guard for that invariant.
527
- """
528
- if KGStoreV2 is None:
529
- return {"available": False, "in_sync": True}
530
- with self._connect() as conn:
531
- legacy_nodes = {r[0] for r in conn.execute("SELECT id FROM nodes")}
532
- v2_nodes = {r[0] for r in conn.execute("SELECT id FROM nodes_v2")}
533
- legacy_edges = {r[0] for r in conn.execute("SELECT id FROM edges")}
534
- v2_edges = {r[0] for r in conn.execute("SELECT id FROM edges_v2")}
535
- return {
536
- "available": True,
537
- "in_sync": legacy_nodes == v2_nodes and legacy_edges == v2_edges,
538
- "nodes_legacy": len(legacy_nodes),
539
- "nodes_v2": len(v2_nodes),
540
- "edges_legacy": len(legacy_edges),
541
- "edges_v2": len(v2_edges),
542
- "nodes_missing_from_v2": sorted(legacy_nodes - v2_nodes),
543
- "nodes_extra_in_v2": sorted(v2_nodes - legacy_nodes),
544
- "edges_missing_from_v2": sorted(legacy_edges - v2_edges),
545
- "edges_extra_in_v2": sorted(v2_edges - legacy_edges),
546
- }
@@ -142,9 +142,6 @@ class VectorIndex(Protocol):
142
142
  def rebuild(self, items: Iterable[IndexItem]) -> None:
143
143
  raise NotImplementedError
144
144
 
145
- def metadata_for(self, id: str) -> Dict[str, Any]:
146
- raise NotImplementedError
147
-
148
145
  def search(
149
146
  self,
150
147
  query: Sequence[float],
@@ -80,10 +80,6 @@ class BruteForceIndex:
80
80
  for item_id, vector, metadata in items:
81
81
  self.add(item_id, vector, metadata)
82
82
 
83
- def metadata_for(self, id: str) -> Dict[str, Any]:
84
- """Metadata stored alongside ``id`` (empty dict when unknown)."""
85
- return dict(self._metadata.get(str(id), {}))
86
-
87
83
  def search(
88
84
  self,
89
85
  query: Sequence[float],
@@ -150,9 +150,6 @@ class HnswIndex:
150
150
  self.add(item_id, vector, metadata)
151
151
  self._dirty = True
152
152
 
153
- def metadata_for(self, id: str) -> Dict[str, Any]:
154
- return dict(self._metadata.get(str(id), {}))
155
-
156
153
  # ── graph construction ───────────────────────────────────────────────────
157
154
  def _new_graph(self, module: Any, capacity: int) -> Any:
158
155
  graph = module.Index(space=self._space, dim=self._dim)
@@ -109,9 +109,6 @@ class QuantizedIndex:
109
109
  for item_id, vector, metadata in items:
110
110
  self.add(item_id, vector, metadata)
111
111
 
112
- def metadata_for(self, id: str) -> Dict[str, Any]:
113
- return dict(self._metadata.get(str(id), {}))
114
-
115
112
  def search(
116
113
  self,
117
114
  query: Sequence[float],
@@ -351,9 +351,6 @@ class BackgroundIngestionQueue:
351
351
  def get(self, job_id: str) -> Optional[BackgroundIngestionJob]:
352
352
  return self._jobs.get(job_id)
353
353
 
354
- def list_pending(self) -> List[BackgroundIngestionJob]:
355
- return [j for j in self._jobs.values() if j.status == "queued"]
356
-
357
354
  def list_recent(self, limit: int = 20) -> List[BackgroundIngestionJob]:
358
355
  """Most recent jobs first (insertion order is schedule order)."""
359
356
  limit = max(1, int(limit))
@@ -27,33 +27,6 @@ class BrainMemory:
27
27
  def available(self) -> bool:
28
28
  return self._pipeline is not None and self._pipeline.available()
29
29
 
30
- def record_decision(
31
- self,
32
- title: str,
33
- detail: str = "",
34
- *,
35
- user_email: Optional[str] = None,
36
- workspace_id: Optional[str] = None,
37
- conversation_id: Optional[str] = None,
38
- decided_by: Optional[str] = None,
39
- metadata: Optional[Dict[str, Any]] = None,
40
- ) -> Dict[str, Any]:
41
- if not str(title or "").strip():
42
- raise ValueError("a decision needs a title")
43
- result = self._pipeline.ingest(
44
- IngestionItem(
45
- source_type="decision",
46
- title=title.strip(),
47
- text=detail,
48
- owner=user_email,
49
- workspace_id=workspace_id,
50
- conversation_id=conversation_id,
51
- metadata={"decided_by": decided_by or user_email, **(metadata or {})},
52
- ),
53
- user_email=user_email,
54
- )
55
- return result.as_dict()
56
-
57
30
  def record_experience(
58
31
  self,
59
32
  title: str,
@@ -167,10 +167,6 @@ class KGPortabilitySharingMixin(_Core):
167
167
  quiet()
168
168
  return result
169
169
 
170
- def import_from_file(self, path, *, mode: str = "merge", dry_run: bool = False) -> Dict[str, Any]:
171
- artifact = json.loads(Path(path).read_text(encoding="utf-8"))
172
- return self.import_data(artifact, mode=mode, dry_run=dry_run)
173
-
174
170
  # ── selective subgraph share (opt-in prototype, off by default) ──────────
175
171
  def share_status(self) -> Dict[str, Any]:
176
172
  """Honest read of the share surface. Safe to call while it is off.
@@ -375,15 +375,6 @@ class GraphEdgeQualityManager:
375
375
  seen[key] = e.get("id")
376
376
  return dups
377
377
 
378
- def merge_duplicate_edges(self, edges: List[Dict]) -> List[Dict]:
379
- # keep highest confidence
380
- best: Dict[Any, Dict[str, Any]] = {}
381
- for e in edges:
382
- key = (e.get("source"), e.get("target"), e.get("type"))
383
- if key not in best or e.get("confidence", 0) > best[key].get("confidence", 0):
384
- best[key] = e
385
- return list(best.values())
386
-
387
378
  def compute_quality_metrics(self, edges: List[Dict]) -> Dict[str, float]:
388
379
  if not edges:
389
380
  return {"avg_conf": 0.0, "avg_evidence": 0.0, "dup_rate": 0.0}
@@ -532,46 +523,6 @@ class LatticeBrainQuality:
532
523
  self.context_assembler = StructuredContextAssembler()
533
524
  self.benchmark = RetrievalBenchmarkRunner()
534
525
 
535
- def full_quality_pass(self, payload: Dict[str, Any]) -> Dict[str, Any]:
536
- """End-to-end quality pipeline (non-destructive)"""
537
- result = {"status": "ok", "timestamp": time.time()}
538
-
539
- # embedding
540
- if "embeddings" in payload:
541
- labels = [self.embed_labeller.label(e["id"], e.get("vector")) for e in payload["embeddings"]]
542
- result["embedding_labels"] = [label.__dict__ for label in labels]
543
- result["reindex_plan"] = self.embed_labeller.generate_reindex_plan()
544
-
545
- # hybrid retrieval
546
- if "retrieval" in payload:
547
- fused = self.hybrid.fuse(payload["retrieval"]["query"], payload["retrieval"]["candidates"])
548
- reranked = self.reranker.rerank(payload["retrieval"]["query"], fused)
549
- result["retrieval"] = {"fused": fused, "reranked": reranked}
550
-
551
- # memory
552
- if "memories" in payload:
553
- cands = self.memory_mgr.extract_candidates(payload["memories"])
554
- cands = self.memory_mgr.score_candidates(cands, payload.get("query", ""))
555
- cands = self.memory_mgr.dedupe(cands)
556
- cands = self.memory_mgr.merge(cands)
557
- cands = self.memory_mgr.detect_conflicts(cands)
558
- cands = self.memory_mgr.apply_retention(cands)
559
- result["memory_candidates"] = [c.__dict__ for c in cands]
560
-
561
- # graph
562
- if "graph_edges" in payload:
563
- eqs = [self.graph_mgr.validate_edge(e) for e in payload["graph_edges"]]
564
- result["graph_quality"] = [q.__dict__ for q in eqs]
565
- result["graph_metrics"] = self.graph_mgr.compute_quality_metrics(payload["graph_edges"])
566
-
567
- # context
568
- if "context_items" in payload:
569
- ctx = self.context_assembler.assemble(payload["context_items"])
570
- ctx = self.context_assembler.apply_guardrails(ctx)
571
- result["structured_context"] = ctx
572
-
573
- return result
574
-
575
526
 
576
527
  __all__ = [
577
528
  "BM25Scorer",
@@ -8,7 +8,7 @@
8
8
  진입점: app_factory.py:AGENT_RUNTIME (wiring root), api/agents.py, RunExecutor.
9
9
 
10
10
  Before this module the agent concern was spread across three places: the
11
- :class:`~latticeai.core.multi_agent.MultiAgentOrchestrator` (role pipeline),
11
+ :class:`~lattice_brain.runtime.multi_agent.MultiAgentOrchestrator` (role pipeline),
12
12
  the :class:`~latticeai.services.platform_runtime.PlatformRuntime` (cross-system
13
13
  wiring + an ad-hoc ``run_agent``), and ``api/agents.py`` (HTTP transport that
14
14
  also owned orchestration + persistence + audit). The frontend reached past all
@@ -181,16 +181,6 @@ class HookResult:
181
181
  }
182
182
 
183
183
 
184
- def hook_context(kind: str, event: str = "", **kwargs: Any) -> HookContext:
185
- """Factory for a :class:`HookContext` (matches the public hook vocabulary)."""
186
- return HookContext(kind, event, **kwargs)
187
-
188
-
189
- def hook_result(**kwargs: Any) -> HookResult:
190
- """Factory for a :class:`HookResult`."""
191
- return HookResult(**kwargs)
192
-
193
-
194
184
  def dispatch_tool(
195
185
  hooks: Any,
196
186
  tool_name: str,
@@ -525,9 +515,6 @@ class HooksRegistry:
525
515
  # Alias — descriptive name used by the wiring layer.
526
516
  register_runtime_hook = register_hook
527
517
 
528
- def unregister_hook(self, hook_id: str) -> None:
529
- self._runtime_runners.pop(hook_id, None)
530
-
531
518
  def has_runner(self, hook_id: str) -> bool:
532
519
  return hook_id in self._runtime_runners
533
520
 
@@ -49,7 +49,7 @@ from typing import Any, Callable, Dict, List, Optional
49
49
  from ..utils import now_iso as _now
50
50
  from .contracts import multi_agent_contract
51
51
 
52
- MULTI_AGENT_VERSION = "11.5.0"
52
+ MULTI_AGENT_VERSION = "11.5.2"
53
53
 
54
54
  AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
55
55
  CORE_PIPELINE = ("planner", "executor", "reviewer")
@@ -212,10 +212,6 @@ class RecipientIdentity:
212
212
  "path": str(self._key_file),
213
213
  }
214
214
 
215
- def seal_to_self(self, plaintext: bytes) -> Dict[str, Any]:
216
- """Seal to this Brain's own key — the honest way to test a round trip."""
217
- return seal(plaintext, recipient_public_key=self.public_key_b64)
218
-
219
215
  def unseal(self, block: Dict[str, Any]) -> bytes:
220
216
  return unseal(block, self._private)
221
217
 
@@ -1,3 +1,3 @@
1
1
  """Lattice AI - modular server package."""
2
2
 
3
- __version__ = "11.5.0"
3
+ __version__ = "11.5.2"
@@ -8,6 +8,8 @@ from typing import Any, Callable, Dict, List, Optional
8
8
  from fastapi import APIRouter, Query, Request
9
9
  from pydantic import BaseModel
10
10
 
11
+ from latticeai.api.workspace_scope import requested_workspace
12
+ from latticeai.core.http_origin import request_external_origin
11
13
  from latticeai.core.messages import http_error, resolve_language
12
14
  from latticeai.core.workspace_os import DEFAULT_WORKSPACE_ID
13
15
 
@@ -65,13 +67,6 @@ def create_admin_router(
65
67
  ) -> APIRouter:
66
68
  router = APIRouter()
67
69
 
68
- def _workspace_scope(request: Request) -> Optional[str]:
69
- header = request.headers.get("X-Workspace-Id")
70
- if header and header.strip():
71
- return header.strip()
72
- query = request.query_params.get("workspace_id")
73
- return query.strip() if query and query.strip() else None
74
-
75
70
  def _matches_scope(item: Dict[str, object], workspace_id: Optional[str]) -> bool:
76
71
  if not workspace_id:
77
72
  return True
@@ -81,11 +76,11 @@ def create_admin_router(
81
76
  return str(item_scope or "") == str(workspace_id)
82
77
 
83
78
  def _scoped_history(request: Request) -> List[Dict]:
84
- scope = _workspace_scope(request)
79
+ scope = requested_workspace(request)
85
80
  return [item for item in get_history() if _matches_scope(item, scope)]
86
81
 
87
82
  def _scoped_audit_log(request: Request) -> List[Dict]:
88
- scope = _workspace_scope(request)
83
+ scope = requested_workspace(request)
89
84
  return [item for item in get_audit_log() if _matches_scope(item, scope)]
90
85
 
91
86
  def _filter_audit_log(
@@ -427,9 +422,14 @@ def create_admin_router(
427
422
  @router.get("/admin/invite-link")
428
423
  async def admin_invite_link(request: Request):
429
424
  require_admin(request)
430
- host = request.headers.get("host", f"localhost:{default_port}")
431
- scheme = "https" if request.headers.get("x-forwarded-proto") == "https" else "http"
432
- url = f"{scheme}://{host}/?code={invite_code}" if invite_gate_enabled else f"{scheme}://{host}/"
425
+ # The link is *mailed to a person*, so it has to name the door they can
426
+ # actually knock on. Behind the desktop gateway ``Host`` is the internal
427
+ # worker authority, which meant every invite emitted through the front
428
+ # door pointed at a port only the gateway can reach.
429
+ origin = request_external_origin(
430
+ request, fallback=f"http://localhost:{default_port}",
431
+ )
432
+ url = f"{origin}/?code={invite_code}" if invite_gate_enabled else f"{origin}/"
433
433
  return {"invite_url": url, "invite_code": invite_code, "gate_enabled": invite_gate_enabled}
434
434
 
435
435
  @router.get("/admin/sso")