ltcai 4.3.3 → 4.5.1

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 (138) hide show
  1. package/README.md +53 -20
  2. package/docs/CHANGELOG.md +122 -0
  3. package/docs/V4_4_0_EXTRACTION_REPORT.md +239 -0
  4. package/docs/V4_5_0_GEMMA_RUNTIME_COMPATIBILITY_REPORT.md +49 -0
  5. package/docs/V4_5_0_GRAPH_UX_REPORT.md +34 -0
  6. package/docs/V4_5_0_MODEL_RUNTIME_UX_REPORT.md +40 -0
  7. package/docs/V4_5_0_ONBOARDING_REPORT.md +31 -0
  8. package/docs/V4_5_0_PRODUCT_EXPERIENCE_RECOVERY_REPORT.md +49 -0
  9. package/docs/V4_5_0_VALIDATION_REPORT.md +60 -0
  10. package/docs/V4_5_1_GRAPH_EXPERIENCE_REPORT.md +33 -0
  11. package/docs/V4_5_1_MODEL_EXPERIENCE_REPORT.md +37 -0
  12. package/docs/V4_5_1_NAVIGATION_REPORT.md +37 -0
  13. package/docs/V4_5_1_ONBOARDING_REPORT.md +29 -0
  14. package/docs/V4_5_1_PRODUCT_REIMAGINING_REPORT.md +61 -0
  15. package/docs/V4_5_1_RC_ARTIFACTS.md +44 -0
  16. package/docs/V4_5_1_UX_REPORT.md +45 -0
  17. package/docs/V4_5_1_VALIDATION_REPORT.md +54 -0
  18. package/docs/V4_5_1_VISUAL_DESIGN_REPORT.md +30 -0
  19. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +16 -16
  20. package/docs/architecture.md +8 -4
  21. package/frontend/src/App.tsx +152 -91
  22. package/frontend/src/api/client.ts +83 -1
  23. package/frontend/src/components/FirstRunGuide.tsx +99 -0
  24. package/frontend/src/components/primitives.tsx +131 -25
  25. package/frontend/src/components/ui/badge.tsx +2 -2
  26. package/frontend/src/components/ui/button.tsx +7 -7
  27. package/frontend/src/components/ui/card.tsx +5 -5
  28. package/frontend/src/components/ui/input.tsx +1 -1
  29. package/frontend/src/components/ui/textarea.tsx +1 -1
  30. package/frontend/src/pages/Act.tsx +58 -28
  31. package/frontend/src/pages/Ask.tsx +51 -19
  32. package/frontend/src/pages/Brain.tsx +60 -42
  33. package/frontend/src/pages/Capture.tsx +24 -24
  34. package/frontend/src/pages/Library.tsx +222 -32
  35. package/frontend/src/pages/System.tsx +56 -34
  36. package/frontend/src/routes.ts +15 -13
  37. package/frontend/src/store/appStore.ts +8 -1
  38. package/frontend/src/styles.css +666 -36
  39. package/lattice_brain/__init__.py +38 -23
  40. package/lattice_brain/_kg_common.py +11 -1
  41. package/lattice_brain/context.py +212 -2
  42. package/lattice_brain/conversations.py +234 -1
  43. package/lattice_brain/discovery.py +11 -1
  44. package/lattice_brain/documents.py +11 -1
  45. package/lattice_brain/graph/__init__.py +28 -0
  46. package/lattice_brain/graph/_kg_common.py +1123 -0
  47. package/lattice_brain/graph/curator.py +473 -0
  48. package/lattice_brain/graph/discovery.py +1455 -0
  49. package/lattice_brain/graph/documents.py +218 -0
  50. package/lattice_brain/graph/identity.py +175 -0
  51. package/lattice_brain/graph/ingest.py +644 -0
  52. package/lattice_brain/graph/network.py +205 -0
  53. package/lattice_brain/graph/projection.py +571 -0
  54. package/lattice_brain/graph/provenance.py +401 -0
  55. package/lattice_brain/graph/retrieval.py +1341 -0
  56. package/lattice_brain/graph/schema.py +640 -0
  57. package/lattice_brain/graph/store.py +237 -0
  58. package/lattice_brain/graph/write_master.py +225 -0
  59. package/lattice_brain/identity.py +11 -13
  60. package/lattice_brain/ingest.py +11 -1
  61. package/lattice_brain/ingestion.py +318 -0
  62. package/lattice_brain/memory.py +100 -1
  63. package/lattice_brain/network.py +11 -1
  64. package/lattice_brain/portability.py +431 -0
  65. package/lattice_brain/projection.py +11 -1
  66. package/lattice_brain/provenance.py +11 -1
  67. package/lattice_brain/retrieval.py +11 -1
  68. package/lattice_brain/runtime/__init__.py +32 -0
  69. package/lattice_brain/runtime/agent_runtime.py +569 -0
  70. package/lattice_brain/runtime/hooks.py +754 -0
  71. package/lattice_brain/runtime/multi_agent.py +795 -0
  72. package/lattice_brain/schema.py +11 -1
  73. package/lattice_brain/store.py +10 -2
  74. package/lattice_brain/workflow.py +461 -0
  75. package/lattice_brain/write_master.py +11 -1
  76. package/latticeai/__init__.py +1 -1
  77. package/latticeai/api/agents.py +2 -2
  78. package/latticeai/api/browser.py +1 -1
  79. package/latticeai/api/chat.py +1 -1
  80. package/latticeai/api/computer_use.py +1 -1
  81. package/latticeai/api/hooks.py +2 -2
  82. package/latticeai/api/mcp.py +1 -1
  83. package/latticeai/api/models.py +107 -18
  84. package/latticeai/api/tools.py +1 -1
  85. package/latticeai/api/workflow_designer.py +2 -2
  86. package/latticeai/app_factory.py +4 -4
  87. package/latticeai/brain/__init__.py +24 -6
  88. package/latticeai/brain/_kg_common.py +11 -1117
  89. package/latticeai/brain/context.py +12 -208
  90. package/latticeai/brain/conversations.py +12 -231
  91. package/latticeai/brain/discovery.py +13 -1451
  92. package/latticeai/brain/documents.py +13 -214
  93. package/latticeai/brain/identity.py +11 -169
  94. package/latticeai/brain/ingest.py +13 -640
  95. package/latticeai/brain/memory.py +12 -97
  96. package/latticeai/brain/network.py +12 -200
  97. package/latticeai/brain/projection.py +13 -567
  98. package/latticeai/brain/provenance.py +13 -397
  99. package/latticeai/brain/retrieval.py +13 -1337
  100. package/latticeai/brain/schema.py +12 -635
  101. package/latticeai/brain/store.py +13 -233
  102. package/latticeai/brain/write_master.py +13 -221
  103. package/latticeai/core/agent.py +1 -1
  104. package/latticeai/core/agent_registry.py +2 -2
  105. package/latticeai/core/builtin_hooks.py +2 -2
  106. package/latticeai/core/graph_curator.py +6 -468
  107. package/latticeai/core/hooks.py +6 -749
  108. package/latticeai/core/marketplace.py +1 -1
  109. package/latticeai/core/model_compat.py +250 -0
  110. package/latticeai/core/multi_agent.py +6 -790
  111. package/latticeai/core/workflow_engine.py +6 -456
  112. package/latticeai/core/workspace_os.py +1 -1
  113. package/latticeai/models/router.py +136 -32
  114. package/latticeai/services/agent_runtime.py +6 -564
  115. package/latticeai/services/ingestion.py +6 -313
  116. package/latticeai/services/kg_portability.py +6 -426
  117. package/latticeai/services/model_catalog.py +2 -2
  118. package/latticeai/services/model_recommendation.py +8 -1
  119. package/latticeai/services/model_runtime.py +18 -3
  120. package/latticeai/services/platform_runtime.py +3 -3
  121. package/latticeai/services/run_executor.py +1 -1
  122. package/latticeai/services/upload_service.py +1 -1
  123. package/p_reinforce.py +1 -1
  124. package/package.json +1 -1
  125. package/scripts/build_frontend_assets.mjs +12 -1
  126. package/scripts/bump_version.py +1 -1
  127. package/scripts/wheel_smoke.py +7 -0
  128. package/src-tauri/Cargo.lock +1 -1
  129. package/src-tauri/Cargo.toml +1 -1
  130. package/src-tauri/tauri.conf.json +1 -1
  131. package/static/app/asset-manifest.json +5 -5
  132. package/static/app/assets/index-3G8qcrIS.js +336 -0
  133. package/static/app/assets/index-3G8qcrIS.js.map +1 -0
  134. package/static/app/assets/index-C0wYZp7k.css +2 -0
  135. package/static/app/index.html +2 -2
  136. package/static/app/assets/index-CHHal8Zl.css +0 -2
  137. package/static/app/assets/index-pdzil9ac.js +0 -333
  138. package/static/app/assets/index-pdzil9ac.js.map +0 -1
@@ -1,218 +1,17 @@
1
- from __future__ import annotations
1
+ """Deprecated shim: physically moved to lattice_brain.graph.documents.
2
2
 
3
- # ruff: noqa: F403,F405
3
+ Kept only for the compatibility window. The module aliases itself to the
4
+ physical module so identity, singletons, and monkeypatching are preserved.
5
+ """
4
6
 
5
- from ._kg_common import * # noqa: F403,F401
7
+ import sys
8
+ import warnings
6
9
 
10
+ import lattice_brain.graph.documents as _impl
7
11
 
8
- class KnowledgeGraphDocumentsMixin:
9
- def _ingest_structure_nodes(
10
- self,
11
- conn: sqlite3.Connection,
12
- file_id: str,
13
- filename: str,
14
- structure: Dict[str, Any],
15
- ) -> None:
16
- for slide in structure.get("slides") or []:
17
- index = slide.get("index")
18
- slide_id = f"slide:{_sha256_text(f'{file_id}:slide:{index}')[:24]}"
19
- title = f"{filename} slide {index}"
20
- summary = "\n".join(slide.get("texts") or [])[:800]
21
- self._upsert_node(
22
- conn, slide_id, "Slide", title, summary=summary, metadata=slide
23
- )
24
- self._upsert_edge(conn, file_id, slide_id, "has_slide")
25
- for text in slide.get("texts") or []:
26
- for topic in _topic_candidates(text, limit=4):
27
- topic_id = f"topic:{_slug(topic)}"
28
- self._upsert_node(
29
- conn,
30
- topic_id,
31
- "Topic",
32
- topic,
33
- metadata={"auto_extracted": True},
34
- )
35
- self._upsert_edge(conn, slide_id, topic_id, "discusses", weight=0.6)
36
-
37
- for page in structure.get("pages") or []:
38
- index = page.get("index")
39
- page_id = f"page:{_sha256_text(f'{file_id}:page:{index}')[:24]}"
40
- title = f"{filename} page {index}"
41
- self._upsert_node(
42
- conn,
43
- page_id,
44
- "Page",
45
- title,
46
- summary=page.get("preview") or "",
47
- metadata=page,
48
- )
49
- self._upsert_edge(conn, file_id, page_id, "has_page")
50
- for topic in _topic_candidates(page.get("preview") or "", limit=4):
51
- topic_id = f"topic:{_slug(topic)}"
52
- self._upsert_node(
53
- conn, topic_id, "Topic", topic, metadata={"auto_extracted": True}
54
- )
55
- self._upsert_edge(conn, page_id, topic_id, "discusses", weight=0.6)
56
-
57
- for sheet in structure.get("sheets") or []:
58
- sheet_title = sheet.get("title")
59
- sheet_id = f"sheet:{_sha256_text(f'{file_id}:sheet:{sheet_title}')[:24]}"
60
- self._upsert_node(
61
- conn, sheet_id, "Sheet", f"{filename} / {sheet_title}", metadata=sheet
62
- )
63
- self._upsert_edge(conn, file_id, sheet_id, "has_sheet")
64
-
65
- for image in structure.get("images") or []:
66
- image_key = image.get("sha256") or _sha256_text(
67
- json.dumps(image, ensure_ascii=False, sort_keys=True)
68
- )
69
- image_id = f"image:{str(image_key)[:24]}"
70
- title_parts = [filename, "image"]
71
- if image.get("page"):
72
- title_parts.append(f"page {image.get('page')}")
73
- if image.get("name"):
74
- title_parts.append(str(image.get("name")).split("/")[-1])
75
- self._upsert_node(
76
- conn, image_id, "Image", " / ".join(title_parts), metadata=image
77
- )
78
- self._upsert_edge(conn, file_id, image_id, "contains_image")
79
-
80
- def _document_structure(self, path: Path, ext: str) -> Dict[str, Any]:
81
- try:
82
- if ext == ".pptx":
83
- return self._pptx_structure(path)
84
- if ext == ".pdf":
85
- return self._pdf_structure(path)
86
- if ext == ".docx":
87
- return self._docx_structure(path)
88
- if ext == ".xlsx":
89
- return self._xlsx_structure(path)
90
- except Exception as exc:
91
- return {"error": str(exc)}
92
- return {}
93
-
94
- def _pptx_structure(self, path: Path) -> Dict[str, Any]:
95
- result: Dict[str, Any] = {"slides": [], "images": []}
96
- try:
97
- from PIL import Image
98
- from pptx import Presentation
99
-
100
- prs = Presentation(str(path))
101
- for slide_index, slide in enumerate(prs.slides, start=1):
102
- slide_info = {"index": slide_index, "shapes": [], "texts": []}
103
- for shape_index, shape in enumerate(slide.shapes, start=1):
104
- shape_info = {
105
- "index": shape_index,
106
- "name": getattr(shape, "name", ""),
107
- "shape_type": str(getattr(shape, "shape_type", "")),
108
- "bbox": {
109
- "left": int(getattr(shape, "left", 0) or 0),
110
- "top": int(getattr(shape, "top", 0) or 0),
111
- "width": int(getattr(shape, "width", 0) or 0),
112
- "height": int(getattr(shape, "height", 0) or 0),
113
- },
114
- }
115
- if getattr(shape, "has_text_frame", False):
116
- text = shape.text_frame.text.strip()
117
- if text:
118
- shape_info["text"] = text[:1000]
119
- slide_info["texts"].append(text)
120
- slide_info["shapes"].append(shape_info)
121
- result["slides"].append(slide_info)
122
- with zipfile.ZipFile(path) as zf:
123
- for name in zf.namelist():
124
- if not name.startswith("ppt/media/"):
125
- continue
126
- data = zf.read(name)
127
- image_info: Dict[str, Any] = {
128
- "name": name,
129
- "bytes": len(data),
130
- "sha256": _sha256_bytes(data),
131
- }
132
- try:
133
- from io import BytesIO
134
-
135
- with Image.open(BytesIO(data)) as img:
136
- image_info.update(
137
- {
138
- "width": img.width,
139
- "height": img.height,
140
- "format": img.format,
141
- }
142
- )
143
- except Exception:
144
- pass
145
- result["images"].append(image_info)
146
- except Exception as exc:
147
- result["error"] = str(exc)
148
- return result
149
-
150
- def _pdf_structure(self, path: Path) -> Dict[str, Any]:
151
- result: Dict[str, Any] = {"pages": [], "images": []}
152
- try:
153
- import pdfplumber
154
-
155
- with pdfplumber.open(str(path)) as pdf:
156
- metadata = dict(pdf.metadata or {})
157
- result["metadata"] = {str(k): str(v) for k, v in metadata.items()}
158
- for page_index, page in enumerate(pdf.pages, start=1):
159
- text = page.extract_text() or ""
160
- page_info = {
161
- "index": page_index,
162
- "width": float(page.width or 0),
163
- "height": float(page.height or 0),
164
- "chars": len(text),
165
- "preview": _clean_text(text)[:500],
166
- "image_count": len(page.images or []),
167
- }
168
- result["pages"].append(page_info)
169
- for image_index, image in enumerate(page.images or [], start=1):
170
- result["images"].append(
171
- {
172
- "page": page_index,
173
- "index": image_index,
174
- "name": image.get("name"),
175
- "width": image.get("width"),
176
- "height": image.get("height"),
177
- "bbox": {
178
- "x0": image.get("x0"),
179
- "top": image.get("top"),
180
- "x1": image.get("x1"),
181
- "bottom": image.get("bottom"),
182
- },
183
- }
184
- )
185
- except Exception as exc:
186
- result["error"] = str(exc)
187
- return result
188
-
189
- def _docx_structure(self, path: Path) -> Dict[str, Any]:
190
- from docx import Document
191
-
192
- doc = Document(str(path))
193
- headings = []
194
- paragraphs = 0
195
- for p in doc.paragraphs:
196
- text = p.text.strip()
197
- if not text:
198
- continue
199
- paragraphs += 1
200
- style = getattr(p.style, "name", "")
201
- if style.lower().startswith("heading"):
202
- headings.append({"style": style, "text": text[:240]})
203
- return {
204
- "paragraphs": paragraphs,
205
- "headings": headings[:80],
206
- "tables": len(doc.tables),
207
- }
208
-
209
- def _xlsx_structure(self, path: Path) -> Dict[str, Any]:
210
- from openpyxl import load_workbook
211
-
212
- wb = load_workbook(str(path), read_only=True, data_only=True)
213
- sheets = []
214
- for ws in wb.worksheets:
215
- sheets.append(
216
- {"title": ws.title, "max_row": ws.max_row, "max_column": ws.max_column}
217
- )
218
- return {"sheets": sheets}
12
+ warnings.warn(
13
+ "latticeai.brain.documents is deprecated; import lattice_brain.graph.documents instead",
14
+ DeprecationWarning,
15
+ stacklevel=2,
16
+ )
17
+ sys.modules[__name__] = _impl
@@ -1,175 +1,17 @@
1
- """Device identity the sovereignty primitive.
1
+ """Deprecated shim: physically moved to lattice_brain.graph.identity.
2
2
 
3
- Every Lattice installation owns an Ed25519 keypair. Exports are signed by
4
- it, peers pair against its public key, and imported knowledge records which
5
- device it came from. The private key never leaves the machine: it lives in
6
- the OS keyring when one is available, otherwise in a 0600 file under the
7
- data directory (the storage backend is reported honestly).
3
+ Kept only for the compatibility window. The module aliases itself to the
4
+ physical module so identity, singletons, and monkeypatching are preserved.
8
5
  """
9
6
 
10
- from __future__ import annotations
7
+ import sys
8
+ import warnings
11
9
 
12
- import base64
13
- import hashlib
14
- import json
15
- import logging
16
- import os
17
- from pathlib import Path
18
- from typing import Any, Dict, Optional
10
+ import lattice_brain.graph.identity as _impl
19
11
 
20
- from cryptography.hazmat.primitives import serialization
21
- from cryptography.hazmat.primitives.asymmetric.ed25519 import (
22
- Ed25519PrivateKey,
23
- Ed25519PublicKey,
12
+ warnings.warn(
13
+ "latticeai.brain.identity is deprecated; import lattice_brain.graph.identity instead",
14
+ DeprecationWarning,
15
+ stacklevel=2,
24
16
  )
25
-
26
- _KEYRING_SERVICE = "lattice-ai-device-identity"
27
- _KEYRING_ENTRY = "ed25519-private-key"
28
-
29
-
30
- def _b64(data: bytes) -> str:
31
- return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=")
32
-
33
-
34
- def _unb64(text: str) -> bytes:
35
- padded = text + "=" * (-len(text) % 4)
36
- return base64.urlsafe_b64decode(padded.encode("ascii"))
37
-
38
-
39
- def _keyring_opt_in() -> bool:
40
- """Keyring storage is opt-in (LATTICEAI_DEVICE_KEY_KEYRING=1).
41
-
42
- OS keychain access can block or prompt during startup/tests; the default
43
- is a 0600 file under the data dir, and ``describe()`` reports which
44
- backend holds the key — no silent security theater either way.
45
- """
46
- return os.getenv("LATTICEAI_DEVICE_KEY_KEYRING", "").strip() in {"1", "true", "yes"}
47
-
48
-
49
- class DeviceIdentity:
50
- """Loads-or-creates the installation's Ed25519 keypair."""
51
-
52
- def __init__(self, data_dir: Path, *, use_keyring: Optional[bool] = None):
53
- if use_keyring is None:
54
- use_keyring = _keyring_opt_in()
55
- self._data_dir = Path(data_dir)
56
- self._key_file = self._data_dir / "device_identity.key"
57
- self._private: Ed25519PrivateKey
58
- self.storage: str # "keyring" | "file"
59
- self._load_or_create(use_keyring)
60
-
61
- # ── key material ───────────────────────────────────────────────────────
62
- def _load_or_create(self, use_keyring: bool) -> None:
63
- raw: Optional[bytes] = None
64
- backend = "file"
65
- if use_keyring:
66
- try:
67
- import keyring
68
-
69
- stored = keyring.get_password(_KEYRING_SERVICE, _KEYRING_ENTRY)
70
- if stored:
71
- raw = _unb64(stored)
72
- backend = "keyring"
73
- except Exception as exc:
74
- logging.debug("device identity: keyring unavailable (%s)", exc)
75
- if raw is None and self._key_file.exists():
76
- raw = _unb64(self._key_file.read_text().strip())
77
- backend = "file"
78
- if raw is None:
79
- key = Ed25519PrivateKey.generate()
80
- raw = key.private_bytes(
81
- serialization.Encoding.Raw,
82
- serialization.PrivateFormat.Raw,
83
- serialization.NoEncryption(),
84
- )
85
- backend = self._persist_new(raw, use_keyring)
86
- self._private = Ed25519PrivateKey.from_private_bytes(raw)
87
- self.storage = backend
88
-
89
- def _persist_new(self, raw: bytes, use_keyring: bool) -> str:
90
- if use_keyring:
91
- try:
92
- import keyring
93
-
94
- keyring.set_password(_KEYRING_SERVICE, _KEYRING_ENTRY, _b64(raw))
95
- return "keyring"
96
- except Exception as exc:
97
- logging.debug("device identity: keyring store failed (%s); using file", exc)
98
- self._data_dir.mkdir(parents=True, exist_ok=True)
99
- self._key_file.write_text(_b64(raw))
100
- os.chmod(self._key_file, 0o600)
101
- return "file"
102
-
103
- # ── public surface ─────────────────────────────────────────────────────
104
- @property
105
- def public_key_b64(self) -> str:
106
- return _b64(
107
- self._private.public_key().public_bytes(
108
- serialization.Encoding.Raw, serialization.PublicFormat.Raw
109
- )
110
- )
111
-
112
- @property
113
- def fingerprint(self) -> str:
114
- """Short human-comparable id: sha256 of the raw public key."""
115
- raw = self._private.public_key().public_bytes(
116
- serialization.Encoding.Raw, serialization.PublicFormat.Raw
117
- )
118
- digest = hashlib.sha256(raw).hexdigest()
119
- return ":".join(digest[i : i + 4] for i in range(0, 16, 4))
120
-
121
- def describe(self) -> Dict[str, Any]:
122
- return {
123
- "fingerprint": self.fingerprint,
124
- "public_key": self.public_key_b64,
125
- "algorithm": "ed25519",
126
- "storage": self.storage,
127
- }
128
-
129
- # ── signing ────────────────────────────────────────────────────────────
130
- def sign(self, payload: bytes) -> str:
131
- return _b64(self._private.sign(payload))
132
-
133
- def sign_manifest(self, manifest: Dict[str, Any]) -> Dict[str, Any]:
134
- """Detached signature over the canonical JSON of a manifest."""
135
- canonical = json.dumps(manifest, sort_keys=True, ensure_ascii=False).encode("utf-8")
136
- return {
137
- "algorithm": "ed25519",
138
- "public_key": self.public_key_b64,
139
- "fingerprint": self.fingerprint,
140
- "signature": self.sign(canonical),
141
- }
142
-
143
-
144
- def fingerprint_of(public_key_b64: str) -> str:
145
- """Human-comparable fingerprint of an Ed25519 public key.
146
-
147
- Raises ValueError when the input is not a valid key — the pairing flow
148
- uses this as its validation gate.
149
- """
150
- raw = _unb64(public_key_b64)
151
- Ed25519PublicKey.from_public_bytes(raw) # validates; raises on garbage
152
- digest = hashlib.sha256(raw).hexdigest()
153
- return ":".join(digest[i : i + 4] for i in range(0, 16, 4))
154
-
155
-
156
- def verify_signature(public_key_b64: str, payload: bytes, signature_b64: str) -> bool:
157
- """True iff ``signature`` is valid for ``payload`` under the given key."""
158
- try:
159
- key = Ed25519PublicKey.from_public_bytes(_unb64(public_key_b64))
160
- key.verify(_unb64(signature_b64), payload)
161
- return True
162
- except Exception:
163
- return False
164
-
165
-
166
- def verify_manifest(manifest: Dict[str, Any], signature_block: Dict[str, Any]) -> bool:
167
- canonical = json.dumps(manifest, sort_keys=True, ensure_ascii=False).encode("utf-8")
168
- return verify_signature(
169
- str(signature_block.get("public_key") or ""),
170
- canonical,
171
- str(signature_block.get("signature") or ""),
172
- )
173
-
174
-
175
- __all__ = ["DeviceIdentity", "verify_signature", "verify_manifest"]
17
+ sys.modules[__name__] = _impl