msdev 0.9.0__py3-none-any.whl

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.
@@ -0,0 +1,1390 @@
1
+ """Node-local model artifact and replica inventory."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import os
8
+ import re
9
+ import socket
10
+ import sqlite3
11
+ import uuid
12
+ from contextlib import contextmanager
13
+ from datetime import datetime, timezone
14
+ from pathlib import Path
15
+ from typing import Any, Iterable, Iterator
16
+
17
+ from .limits import (
18
+ MAX_INVENTORY_ALIASES_PER_ARTIFACT,
19
+ MAX_INVENTORY_ARTIFACTS,
20
+ MAX_INVENTORY_CONTENT_BYTES,
21
+ MAX_INVENTORY_REPLICAS_PER_ARTIFACT,
22
+ MAX_INVENTORY_TAGS_PER_ARTIFACT,
23
+ MAX_INVENTORY_TOTAL_ALIASES,
24
+ MAX_INVENTORY_TOTAL_REPLICAS,
25
+ MAX_INVENTORY_TOTAL_TAGS,
26
+ MAX_ROOT_MAP_PATH_BYTES,
27
+ MAX_ROOT_MAPS,
28
+ )
29
+
30
+
31
+ SCHEMA_VERSION = 1
32
+ _DISCOVERY_SKIP_DIRS = {
33
+ ".git",
34
+ "__pycache__",
35
+ "node_modules",
36
+ "site-packages",
37
+ "venv",
38
+ ".venv",
39
+ }
40
+
41
+
42
+ def _utc_now() -> str:
43
+ return datetime.now(timezone.utc).isoformat()
44
+
45
+
46
+ class Inventory:
47
+ """SQLite inventory owned by one user-level daemon."""
48
+
49
+ def __init__(self, data_dir: Path):
50
+ self.data_dir = data_dir.expanduser()
51
+ self.data_dir.mkdir(parents=True, exist_ok=True)
52
+ self.db_path = self.data_dir / "inventory.db"
53
+ self.node_id = self._load_node_id()
54
+ self._initialize()
55
+
56
+ def _load_node_id(self) -> str:
57
+ path = self.data_dir / "node-id"
58
+ if path.exists():
59
+ value = path.read_text(encoding="utf-8").strip()
60
+ if value:
61
+ return value
62
+ value = f"{socket.gethostname()}-{uuid.uuid4().hex[:12]}"
63
+ path.write_text(value + "\n", encoding="utf-8")
64
+ os.chmod(path, 0o600)
65
+ return value
66
+
67
+ def _connect(self) -> sqlite3.Connection:
68
+ conn = sqlite3.connect(self.db_path)
69
+ conn.row_factory = sqlite3.Row
70
+ conn.execute("PRAGMA foreign_keys = ON")
71
+ conn.execute("PRAGMA journal_mode = WAL")
72
+ return conn
73
+
74
+ @contextmanager
75
+ def _connection(self) -> Iterator[sqlite3.Connection]:
76
+ conn = self._connect()
77
+ try:
78
+ with conn:
79
+ yield conn
80
+ finally:
81
+ conn.close()
82
+
83
+ def _initialize(self) -> None:
84
+ with self._connection() as conn:
85
+ conn.executescript(
86
+ """
87
+ CREATE TABLE IF NOT EXISTS artifacts (
88
+ id INTEGER PRIMARY KEY,
89
+ ref TEXT NOT NULL UNIQUE,
90
+ revision TEXT,
91
+ variant TEXT,
92
+ format TEXT,
93
+ manifest_digest TEXT,
94
+ metadata_json TEXT NOT NULL DEFAULT '{}',
95
+ created_at TEXT NOT NULL,
96
+ updated_at TEXT NOT NULL
97
+ );
98
+
99
+ CREATE TABLE IF NOT EXISTS replicas (
100
+ id INTEGER PRIMARY KEY,
101
+ artifact_id INTEGER NOT NULL REFERENCES artifacts(id) ON DELETE CASCADE,
102
+ node_id TEXT NOT NULL,
103
+ path TEXT NOT NULL,
104
+ scope TEXT NOT NULL,
105
+ management TEXT NOT NULL,
106
+ state TEXT NOT NULL,
107
+ verification_level TEXT NOT NULL,
108
+ size_bytes INTEGER NOT NULL DEFAULT 0,
109
+ file_count INTEGER NOT NULL DEFAULT 0,
110
+ last_seen TEXT NOT NULL,
111
+ UNIQUE(node_id, path)
112
+ );
113
+ """
114
+ )
115
+ artifact_columns = {
116
+ row["name"]
117
+ for row in conn.execute("PRAGMA table_info(artifacts)").fetchall()
118
+ }
119
+ if "description" not in artifact_columns:
120
+ conn.execute("ALTER TABLE artifacts ADD COLUMN description TEXT")
121
+ if "deleted_at" not in artifact_columns:
122
+ conn.execute("ALTER TABLE artifacts ADD COLUMN deleted_at TEXT")
123
+ conn.executescript(
124
+ """
125
+ CREATE TABLE IF NOT EXISTS artifact_aliases (
126
+ artifact_id INTEGER NOT NULL REFERENCES artifacts(id) ON DELETE CASCADE,
127
+ alias TEXT NOT NULL UNIQUE,
128
+ PRIMARY KEY (artifact_id, alias)
129
+ );
130
+
131
+ CREATE TABLE IF NOT EXISTS artifact_tags (
132
+ artifact_id INTEGER NOT NULL REFERENCES artifacts(id) ON DELETE CASCADE,
133
+ tag TEXT NOT NULL,
134
+ PRIMARY KEY (artifact_id, tag)
135
+ );
136
+
137
+ CREATE TABLE IF NOT EXISTS audit_log (
138
+ id INTEGER PRIMARY KEY,
139
+ action TEXT NOT NULL,
140
+ ref TEXT,
141
+ details_json TEXT NOT NULL DEFAULT '{}',
142
+ created_at TEXT NOT NULL
143
+ );
144
+ """
145
+ )
146
+
147
+ @staticmethod
148
+ def _audit(
149
+ conn: sqlite3.Connection,
150
+ action: str,
151
+ ref: str | None,
152
+ details: dict[str, Any] | None = None,
153
+ ) -> None:
154
+ conn.execute(
155
+ """
156
+ INSERT INTO audit_log (action, ref, details_json, created_at)
157
+ VALUES (?, ?, ?, ?)
158
+ """,
159
+ (
160
+ action,
161
+ ref,
162
+ json.dumps(details or {}, ensure_ascii=False, sort_keys=True),
163
+ _utc_now(),
164
+ ),
165
+ )
166
+
167
+ @staticmethod
168
+ def _artifact_row(
169
+ conn: sqlite3.Connection,
170
+ ref_or_alias: str,
171
+ ) -> sqlite3.Row:
172
+ row = conn.execute(
173
+ "SELECT * FROM artifacts WHERE ref = ?",
174
+ (ref_or_alias,),
175
+ ).fetchone()
176
+ if row is None:
177
+ row = conn.execute(
178
+ """
179
+ SELECT a.*
180
+ FROM artifacts a
181
+ JOIN artifact_aliases x ON x.artifact_id = a.id
182
+ WHERE x.alias = ?
183
+ """,
184
+ (ref_or_alias,),
185
+ ).fetchone()
186
+ if row is None:
187
+ raise KeyError(f"unknown model: {ref_or_alias}")
188
+ return row
189
+
190
+ @staticmethod
191
+ def _file_digest(path: Path, hasher: Any) -> None:
192
+ with path.open("rb") as handle:
193
+ while chunk := handle.read(8 * 1024 * 1024):
194
+ hasher.update(chunk)
195
+
196
+ @classmethod
197
+ def scan_path(cls, path: Path, full_checksum: bool = False) -> dict[str, Any]:
198
+ resolved = path.expanduser().resolve(strict=True)
199
+ files: list[tuple[str, Path]] = []
200
+ if resolved.is_file():
201
+ files.append((resolved.name, resolved))
202
+ elif resolved.is_dir():
203
+ for child in resolved.rglob("*"):
204
+ if child.is_file():
205
+ files.append((child.relative_to(resolved).as_posix(), child))
206
+ else:
207
+ raise ValueError(f"model path must be a file or directory: {resolved}")
208
+
209
+ files.sort(key=lambda item: item[0])
210
+ metadata_hasher = hashlib.sha256()
211
+ content_hasher = hashlib.sha256() if full_checksum else None
212
+ total_size = 0
213
+ for relative, file_path in files:
214
+ stat = file_path.stat()
215
+ total_size += stat.st_size
216
+ metadata_hasher.update(relative.encode("utf-8"))
217
+ metadata_hasher.update(b"\0")
218
+ metadata_hasher.update(str(stat.st_size).encode("ascii"))
219
+ metadata_hasher.update(b"\0")
220
+ metadata_hasher.update(str(stat.st_mtime_ns).encode("ascii"))
221
+ metadata_hasher.update(b"\n")
222
+ if content_hasher is not None:
223
+ content_hasher.update(relative.encode("utf-8"))
224
+ content_hasher.update(b"\0")
225
+ cls._file_digest(file_path, content_hasher)
226
+
227
+ return {
228
+ "path": str(resolved),
229
+ "size_bytes": total_size,
230
+ "file_count": len(files),
231
+ "metadata_digest": f"sha256:{metadata_hasher.hexdigest()}",
232
+ "content_digest": f"sha256:{content_hasher.hexdigest()}" if content_hasher else None,
233
+ }
234
+
235
+ @staticmethod
236
+ def _model_metadata(path: Path) -> tuple[str, dict[str, Any]] | None:
237
+ try:
238
+ files = [item for item in path.iterdir() if item.is_file()]
239
+ except (OSError, PermissionError):
240
+ return None
241
+ names = {item.name for item in files}
242
+ suffixes = {item.suffix.lower() for item in files}
243
+ has_config = "config.json" in names
244
+ has_adapter = "adapter_config.json" in names
245
+ has_safetensors = ".safetensors" in suffixes
246
+ has_bin = ".bin" in suffixes
247
+ has_gguf = ".gguf" in suffixes
248
+ has_index = bool(
249
+ {
250
+ "model.safetensors.index.json",
251
+ "pytorch_model.bin.index.json",
252
+ }
253
+ & names
254
+ )
255
+ if not (
256
+ (has_config and (has_safetensors or has_bin or has_gguf or has_index))
257
+ or (has_adapter and (has_safetensors or has_bin))
258
+ or has_gguf
259
+ ):
260
+ return None
261
+
262
+ if has_adapter:
263
+ format_name = "adapter-safetensors" if has_safetensors else "adapter-bin"
264
+ elif has_gguf:
265
+ format_name = "gguf"
266
+ elif has_safetensors or "model.safetensors.index.json" in names:
267
+ format_name = "huggingface-safetensors"
268
+ else:
269
+ format_name = "pytorch-bin"
270
+
271
+ config_name = "adapter_config.json" if has_adapter else "config.json"
272
+ config: dict[str, Any] = {}
273
+ config_path = path / config_name
274
+ if config_path.is_file() and config_path.stat().st_size <= 4 * 1024 * 1024:
275
+ try:
276
+ value = json.loads(config_path.read_text(encoding="utf-8"))
277
+ if isinstance(value, dict):
278
+ config = value
279
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError):
280
+ pass
281
+ return format_name, config
282
+
283
+ @staticmethod
284
+ def _portable_structure(path: Path) -> dict[str, Any]:
285
+ hasher = hashlib.sha256()
286
+ total_size = 0
287
+ file_count = 0
288
+ for child in sorted(
289
+ (item for item in path.rglob("*") if item.is_file()),
290
+ key=lambda item: item.relative_to(path).as_posix(),
291
+ ):
292
+ try:
293
+ stat = child.stat()
294
+ except (OSError, FileNotFoundError):
295
+ continue
296
+ relative = child.relative_to(path).as_posix()
297
+ total_size += stat.st_size
298
+ file_count += 1
299
+ hasher.update(relative.encode("utf-8"))
300
+ hasher.update(b"\0")
301
+ hasher.update(str(stat.st_size).encode("ascii"))
302
+ hasher.update(b"\n")
303
+ return {
304
+ "structure_digest": f"sha256:{hasher.hexdigest()}",
305
+ "size_bytes": total_size,
306
+ "file_count": file_count,
307
+ }
308
+
309
+ @staticmethod
310
+ def _discovered_ref(
311
+ root: Path,
312
+ path: Path,
313
+ config: dict[str, Any],
314
+ structure_digest: str,
315
+ namespace: str,
316
+ ) -> str:
317
+ configured = config.get("_name_or_path")
318
+ if isinstance(configured, str) and configured and not Path(configured).is_absolute():
319
+ name = configured.removeprefix("model://")
320
+ else:
321
+ try:
322
+ name = path.relative_to(root).as_posix()
323
+ except ValueError:
324
+ name = path.name
325
+ components = []
326
+ for component in name.split("/"):
327
+ normalized = re.sub(r"[^A-Za-z0-9._-]+", "-", component).strip("-")
328
+ if normalized:
329
+ components.append(normalized)
330
+ safe_name = "/".join(components) or "model"
331
+ digest = structure_digest.removeprefix("sha256:")[:12]
332
+ return f"model://{namespace}/{safe_name}@{digest}"
333
+
334
+ def discover_models(
335
+ self,
336
+ *,
337
+ root: str,
338
+ max_depth: int = 5,
339
+ register: bool = False,
340
+ namespace: str = "discovered",
341
+ ) -> dict[str, Any]:
342
+ root_path = Path(root).expanduser().resolve(strict=True)
343
+ if not root_path.is_dir():
344
+ raise ValueError(f"discovery root must be a directory: {root_path}")
345
+ if not 0 <= max_depth <= 20:
346
+ raise ValueError("max_depth must be between 0 and 20")
347
+ if not re.fullmatch(r"[A-Za-z0-9._-]+", namespace):
348
+ raise ValueError("namespace may only contain letters, numbers, dot, underscore, and dash")
349
+
350
+ found: list[dict[str, Any]] = []
351
+ for current, dirs, _files in os.walk(root_path, followlinks=False):
352
+ current_path = Path(current)
353
+ depth = len(current_path.relative_to(root_path).parts)
354
+ dirs[:] = [
355
+ name
356
+ for name in dirs
357
+ if name not in _DISCOVERY_SKIP_DIRS
358
+ and not (current_path / name).is_symlink()
359
+ ]
360
+ if depth > max_depth:
361
+ dirs[:] = []
362
+ continue
363
+
364
+ detected = self._model_metadata(current_path)
365
+ if detected is None:
366
+ if depth == max_depth:
367
+ dirs[:] = []
368
+ continue
369
+
370
+ format_name, config = detected
371
+ structure = self._portable_structure(current_path)
372
+ ref = self._discovered_ref(
373
+ root_path,
374
+ current_path,
375
+ config,
376
+ structure["structure_digest"],
377
+ namespace,
378
+ )
379
+ item = {
380
+ "ref": ref,
381
+ "path": str(current_path),
382
+ "format": format_name,
383
+ "size_bytes": structure["size_bytes"],
384
+ "file_count": structure["file_count"],
385
+ "structure_digest": structure["structure_digest"],
386
+ "model_type": config.get("model_type"),
387
+ "architectures": config.get("architectures"),
388
+ "registered": False,
389
+ }
390
+ if register:
391
+ registered = self.register_model(
392
+ ref=ref,
393
+ path=str(current_path),
394
+ format_name=format_name,
395
+ extra_metadata={
396
+ "model_type": config.get("model_type"),
397
+ "architectures": config.get("architectures"),
398
+ },
399
+ )
400
+ item["registered"] = True
401
+ item["state"] = registered["replicas"][0]["state"]
402
+ found.append(item)
403
+ dirs[:] = []
404
+
405
+ return {
406
+ "root": str(root_path),
407
+ "max_depth": max_depth,
408
+ "registered": register,
409
+ "models": found,
410
+ }
411
+
412
+ def _upsert_artifact(
413
+ self,
414
+ conn: sqlite3.Connection,
415
+ *,
416
+ ref: str,
417
+ manifest_digest: str | None,
418
+ format_name: str | None = None,
419
+ revision: str | None = None,
420
+ variant: str | None = None,
421
+ metadata: dict[str, Any] | None = None,
422
+ ) -> int:
423
+ now = _utc_now()
424
+ conn.execute(
425
+ """
426
+ INSERT INTO artifacts (
427
+ ref, revision, variant, format, manifest_digest,
428
+ metadata_json, created_at, updated_at
429
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
430
+ ON CONFLICT(ref) DO UPDATE SET
431
+ revision = COALESCE(excluded.revision, artifacts.revision),
432
+ variant = COALESCE(excluded.variant, artifacts.variant),
433
+ format = COALESCE(excluded.format, artifacts.format),
434
+ manifest_digest = COALESCE(excluded.manifest_digest, artifacts.manifest_digest),
435
+ metadata_json = excluded.metadata_json,
436
+ deleted_at = NULL,
437
+ updated_at = excluded.updated_at
438
+ """,
439
+ (
440
+ ref,
441
+ revision,
442
+ variant,
443
+ format_name,
444
+ manifest_digest,
445
+ json.dumps(metadata or {}, ensure_ascii=False, sort_keys=True),
446
+ now,
447
+ now,
448
+ ),
449
+ )
450
+ row = conn.execute("SELECT id FROM artifacts WHERE ref = ?", (ref,)).fetchone()
451
+ assert row is not None
452
+ return int(row["id"])
453
+
454
+ def register_model(
455
+ self,
456
+ *,
457
+ ref: str,
458
+ path: str,
459
+ scope: str = "user",
460
+ format_name: str | None = None,
461
+ full_checksum: bool = False,
462
+ extra_metadata: dict[str, Any] | None = None,
463
+ ) -> dict[str, Any]:
464
+ if not ref.strip():
465
+ raise ValueError("model ref must not be empty")
466
+ if scope not in {"user", "team", "system"}:
467
+ raise ValueError(f"unsupported scope: {scope}")
468
+ with self._connection() as conn:
469
+ existing = conn.execute(
470
+ "SELECT manifest_digest, metadata_json FROM artifacts WHERE ref = ?",
471
+ (ref,),
472
+ ).fetchone()
473
+ existing_metadata = json.loads(existing["metadata_json"]) if existing else {}
474
+ require_content = bool(existing_metadata.get("content_digest"))
475
+ scanned = self.scan_path(Path(path), full_checksum=full_checksum or require_content)
476
+ with self._connection() as conn:
477
+ self._register_scanned(
478
+ conn,
479
+ ref=ref,
480
+ scanned=scanned,
481
+ scope=scope,
482
+ format_name=format_name,
483
+ extra_metadata=extra_metadata,
484
+ )
485
+ return self.inspect_model(ref)
486
+
487
+ def _register_scanned(
488
+ self,
489
+ conn: sqlite3.Connection,
490
+ *,
491
+ ref: str,
492
+ scanned: dict[str, Any],
493
+ scope: str,
494
+ format_name: str | None,
495
+ extra_metadata: dict[str, Any] | None = None,
496
+ ) -> None:
497
+ existing = conn.execute(
498
+ "SELECT manifest_digest, metadata_json FROM artifacts WHERE ref = ?",
499
+ (ref,),
500
+ ).fetchone()
501
+ existing_metadata = json.loads(existing["metadata_json"]) if existing else {}
502
+ require_content = bool(existing_metadata.get("content_digest"))
503
+ actual_identity = (
504
+ scanned["content_digest"]
505
+ if require_content
506
+ else scanned["metadata_digest"]
507
+ )
508
+ expected_identity = (
509
+ existing_metadata.get("content_digest")
510
+ or existing_metadata.get("metadata_digest")
511
+ or (existing["manifest_digest"] if existing else None)
512
+ )
513
+ state = "ready" if expected_identity in {None, actual_identity} else "dirty"
514
+ manifest = existing["manifest_digest"] if existing else (
515
+ scanned["content_digest"] or scanned["metadata_digest"]
516
+ )
517
+ verification = (
518
+ "checksum_verified"
519
+ if scanned["content_digest"] is not None and state == "ready"
520
+ else "metadata_verified"
521
+ )
522
+ now = _utc_now()
523
+ artifact_metadata = dict(existing_metadata)
524
+ if existing is None:
525
+ artifact_metadata.update(
526
+ {
527
+ "metadata_digest": scanned["metadata_digest"],
528
+ "content_digest": scanned["content_digest"],
529
+ }
530
+ )
531
+ artifact_metadata.update(extra_metadata or {})
532
+ artifact_id = self._upsert_artifact(
533
+ conn,
534
+ ref=ref,
535
+ manifest_digest=manifest,
536
+ format_name=format_name,
537
+ metadata=artifact_metadata,
538
+ )
539
+ conn.execute(
540
+ """
541
+ INSERT INTO replicas (
542
+ artifact_id, node_id, path, scope, management, state,
543
+ verification_level, size_bytes, file_count, last_seen
544
+ ) VALUES (?, ?, ?, ?, 'external', ?, ?, ?, ?, ?)
545
+ ON CONFLICT(node_id, path) DO UPDATE SET
546
+ artifact_id = excluded.artifact_id,
547
+ scope = excluded.scope,
548
+ state = excluded.state,
549
+ verification_level = excluded.verification_level,
550
+ size_bytes = excluded.size_bytes,
551
+ file_count = excluded.file_count,
552
+ last_seen = excluded.last_seen
553
+ """,
554
+ (
555
+ artifact_id,
556
+ self.node_id,
557
+ scanned["path"],
558
+ scope,
559
+ state,
560
+ verification,
561
+ scanned["size_bytes"],
562
+ scanned["file_count"],
563
+ now,
564
+ ),
565
+ )
566
+ self._audit(
567
+ conn,
568
+ "model.register",
569
+ ref,
570
+ {
571
+ "path": scanned["path"],
572
+ "state": state,
573
+ "verification_level": verification,
574
+ },
575
+ )
576
+
577
+ def list_models(
578
+ self,
579
+ *,
580
+ format_name: str | None = None,
581
+ model_type: str | None = None,
582
+ state: str | None = None,
583
+ tag: str | None = None,
584
+ name: str | None = None,
585
+ include_deleted: bool = False,
586
+ ) -> list[dict[str, Any]]:
587
+ with self._connection() as conn:
588
+ query = "SELECT * FROM artifacts"
589
+ if not include_deleted:
590
+ query += " WHERE deleted_at IS NULL"
591
+ query += " ORDER BY ref"
592
+ artifacts = conn.execute(query).fetchall()
593
+ results: list[dict[str, Any]] = []
594
+ for artifact in artifacts:
595
+ metadata = json.loads(artifact["metadata_json"])
596
+ aliases = [
597
+ row["alias"]
598
+ for row in conn.execute(
599
+ "SELECT alias FROM artifact_aliases WHERE artifact_id = ? ORDER BY alias",
600
+ (artifact["id"],),
601
+ ).fetchall()
602
+ ]
603
+ tags = [
604
+ row["tag"]
605
+ for row in conn.execute(
606
+ "SELECT tag FROM artifact_tags WHERE artifact_id = ? ORDER BY tag",
607
+ (artifact["id"],),
608
+ ).fetchall()
609
+ ]
610
+ if format_name and artifact["format"] != format_name:
611
+ continue
612
+ if model_type and metadata.get("model_type") != model_type:
613
+ continue
614
+ if tag and tag not in tags:
615
+ continue
616
+ replicas = conn.execute(
617
+ "SELECT * FROM replicas WHERE artifact_id = ? ORDER BY node_id, path",
618
+ (artifact["id"],),
619
+ ).fetchall()
620
+ replica_values: list[sqlite3.Row | None] = list(replicas) or [None]
621
+ for replica in replica_values:
622
+ if state and (replica is None or replica["state"] != state):
623
+ continue
624
+ value = {
625
+ "ref": artifact["ref"],
626
+ "revision": artifact["revision"],
627
+ "variant": artifact["variant"],
628
+ "format": artifact["format"],
629
+ "manifest_digest": artifact["manifest_digest"],
630
+ "description": artifact["description"],
631
+ "deleted_at": artifact["deleted_at"],
632
+ "aliases": aliases,
633
+ "tags": tags,
634
+ "model_type": metadata.get("model_type"),
635
+ }
636
+ if replica is not None:
637
+ value.update(dict(replica))
638
+ value.pop("id", None)
639
+ value.pop("artifact_id", None)
640
+ else:
641
+ value.update(
642
+ {
643
+ "node_id": None,
644
+ "path": None,
645
+ "scope": None,
646
+ "management": None,
647
+ "state": None,
648
+ "verification_level": None,
649
+ "size_bytes": 0,
650
+ "file_count": 0,
651
+ "last_seen": None,
652
+ }
653
+ )
654
+ searchable = " ".join(
655
+ [
656
+ artifact["ref"],
657
+ *aliases,
658
+ value.get("path") or "",
659
+ artifact["description"] or "",
660
+ ]
661
+ ).lower()
662
+ if name and name.lower() not in searchable:
663
+ continue
664
+ results.append(value)
665
+ return results
666
+
667
+ def inspect_model(self, ref: str) -> dict[str, Any]:
668
+ with self._connection() as conn:
669
+ artifact = self._artifact_row(conn, ref)
670
+ replicas = conn.execute(
671
+ "SELECT * FROM replicas WHERE artifact_id = ? ORDER BY node_id, path",
672
+ (artifact["id"],),
673
+ ).fetchall()
674
+ aliases = [
675
+ row["alias"]
676
+ for row in conn.execute(
677
+ "SELECT alias FROM artifact_aliases WHERE artifact_id = ? ORDER BY alias",
678
+ (artifact["id"],),
679
+ ).fetchall()
680
+ ]
681
+ tags = [
682
+ row["tag"]
683
+ for row in conn.execute(
684
+ "SELECT tag FROM artifact_tags WHERE artifact_id = ? ORDER BY tag",
685
+ (artifact["id"],),
686
+ ).fetchall()
687
+ ]
688
+ value = dict(artifact)
689
+ value["metadata"] = json.loads(value.pop("metadata_json"))
690
+ value["replicas"] = [dict(row) for row in replicas]
691
+ value["aliases"] = aliases
692
+ value["tags"] = tags
693
+ return value
694
+
695
+ def update_model(
696
+ self,
697
+ ref: str,
698
+ *,
699
+ add_aliases: Iterable[str] = (),
700
+ remove_aliases: Iterable[str] = (),
701
+ add_tags: Iterable[str] = (),
702
+ remove_tags: Iterable[str] = (),
703
+ description: str | None = None,
704
+ clear_description: bool = False,
705
+ ) -> dict[str, Any]:
706
+ additions_alias = sorted({item.strip() for item in add_aliases if item.strip()})
707
+ removals_alias = sorted({item.strip() for item in remove_aliases if item.strip()})
708
+ additions_tag = sorted({item.strip() for item in add_tags if item.strip()})
709
+ removals_tag = sorted({item.strip() for item in remove_tags if item.strip()})
710
+ with self._connection() as conn:
711
+ artifact = self._artifact_row(conn, ref)
712
+ try:
713
+ for alias in additions_alias:
714
+ existing_alias = conn.execute(
715
+ "SELECT artifact_id FROM artifact_aliases WHERE alias = ?",
716
+ (alias,),
717
+ ).fetchone()
718
+ if existing_alias is None:
719
+ conn.execute(
720
+ "INSERT INTO artifact_aliases (artifact_id, alias) VALUES (?, ?)",
721
+ (artifact["id"], alias),
722
+ )
723
+ elif existing_alias["artifact_id"] != artifact["id"]:
724
+ raise ValueError(
725
+ f"alias already belongs to another model: {alias}"
726
+ )
727
+ except sqlite3.IntegrityError as exc:
728
+ raise ValueError(f"alias already belongs to another model: {alias}") from exc
729
+ for alias in removals_alias:
730
+ conn.execute(
731
+ "DELETE FROM artifact_aliases WHERE artifact_id = ? AND alias = ?",
732
+ (artifact["id"], alias),
733
+ )
734
+ for tag in additions_tag:
735
+ conn.execute(
736
+ "INSERT OR IGNORE INTO artifact_tags (artifact_id, tag) VALUES (?, ?)",
737
+ (artifact["id"], tag),
738
+ )
739
+ for tag in removals_tag:
740
+ conn.execute(
741
+ "DELETE FROM artifact_tags WHERE artifact_id = ? AND tag = ?",
742
+ (artifact["id"], tag),
743
+ )
744
+ if description is not None or clear_description:
745
+ conn.execute(
746
+ "UPDATE artifacts SET description = ?, updated_at = ? WHERE id = ?",
747
+ (None if clear_description else description, _utc_now(), artifact["id"]),
748
+ )
749
+ self._audit(
750
+ conn,
751
+ "model.update",
752
+ artifact["ref"],
753
+ {
754
+ "add_aliases": additions_alias,
755
+ "remove_aliases": removals_alias,
756
+ "add_tags": additions_tag,
757
+ "remove_tags": removals_tag,
758
+ "description_changed": description is not None or clear_description,
759
+ },
760
+ )
761
+ return self.inspect_model(artifact["ref"])
762
+
763
+ def delete_model(self, ref: str) -> dict[str, Any]:
764
+ with self._connection() as conn:
765
+ artifact = self._artifact_row(conn, ref)
766
+ deleted_at = _utc_now()
767
+ conn.execute(
768
+ "UPDATE artifacts SET deleted_at = ?, updated_at = ? WHERE id = ?",
769
+ (deleted_at, deleted_at, artifact["id"]),
770
+ )
771
+ self._audit(conn, "model.delete", artifact["ref"])
772
+ return {"ref": artifact["ref"], "deleted_at": deleted_at}
773
+
774
+ def restore_model(self, ref: str) -> dict[str, Any]:
775
+ with self._connection() as conn:
776
+ artifact = self._artifact_row(conn, ref)
777
+ conn.execute(
778
+ "UPDATE artifacts SET deleted_at = NULL, updated_at = ? WHERE id = ?",
779
+ (_utc_now(), artifact["id"]),
780
+ )
781
+ self._audit(conn, "model.restore", artifact["ref"])
782
+ return self.inspect_model(artifact["ref"])
783
+
784
+ def list_replicas(self, ref: str) -> dict[str, Any]:
785
+ model = self.inspect_model(ref)
786
+ return {
787
+ "ref": model["ref"],
788
+ "deleted_at": model["deleted_at"],
789
+ "replicas": model["replicas"],
790
+ }
791
+
792
+ def remove_replica(self, ref: str, path: str) -> dict[str, Any]:
793
+ resolved = str(Path(path).expanduser().resolve())
794
+ with self._connection() as conn:
795
+ artifact = self._artifact_row(conn, ref)
796
+ removed = conn.execute(
797
+ """
798
+ DELETE FROM replicas
799
+ WHERE artifact_id = ? AND node_id = ? AND path = ?
800
+ """,
801
+ (artifact["id"], self.node_id, resolved),
802
+ ).rowcount
803
+ self._audit(
804
+ conn,
805
+ "replica.remove",
806
+ artifact["ref"],
807
+ {"path": resolved, "removed": int(removed)},
808
+ )
809
+ return {"ref": artifact["ref"], "path": resolved, "removed": int(removed)}
810
+
811
+ def refresh_model(
812
+ self,
813
+ ref: str,
814
+ *,
815
+ path: str | None = None,
816
+ full_checksum: bool = False,
817
+ ) -> dict[str, Any]:
818
+ model = self.inspect_model(ref)
819
+ replicas = [
820
+ item
821
+ for item in model["replicas"]
822
+ if item["node_id"] == self.node_id
823
+ and (path is None or item["path"] == str(Path(path).expanduser().resolve()))
824
+ ]
825
+ if not replicas:
826
+ raise KeyError(f"model has no matching replica on this node: {ref}")
827
+ scans: list[tuple[dict[str, Any], dict[str, Any] | None]] = []
828
+ for replica in replicas:
829
+ replica_path = Path(replica["path"])
830
+ scan = (
831
+ self.scan_path(replica_path, full_checksum=full_checksum)
832
+ if replica_path.exists()
833
+ else None
834
+ )
835
+ scans.append((replica, scan))
836
+ baseline = next((scan for _replica, scan in scans if scan is not None), None)
837
+ if baseline is None:
838
+ raise FileNotFoundError("no matching replica paths exist")
839
+ identity_key = "content_digest" if full_checksum else "metadata_digest"
840
+ metadata = dict(model["metadata"])
841
+ metadata["metadata_digest"] = baseline["metadata_digest"]
842
+ metadata["content_digest"] = baseline["content_digest"] if full_checksum else None
843
+ now = _utc_now()
844
+ with self._connection() as conn:
845
+ conn.execute(
846
+ """
847
+ UPDATE artifacts
848
+ SET manifest_digest = ?, metadata_json = ?, deleted_at = NULL, updated_at = ?
849
+ WHERE id = ?
850
+ """,
851
+ (
852
+ baseline[identity_key],
853
+ json.dumps(metadata, ensure_ascii=False, sort_keys=True),
854
+ now,
855
+ model["id"],
856
+ ),
857
+ )
858
+ results = []
859
+ for replica, scan in scans:
860
+ if scan is None:
861
+ state = "missing"
862
+ verification = "metadata_verified"
863
+ else:
864
+ state = (
865
+ "ready"
866
+ if scan[identity_key] == baseline[identity_key]
867
+ else "dirty"
868
+ )
869
+ verification = (
870
+ "checksum_verified" if full_checksum and state == "ready"
871
+ else "metadata_verified"
872
+ )
873
+ conn.execute(
874
+ """
875
+ UPDATE replicas
876
+ SET state = ?, verification_level = ?,
877
+ size_bytes = ?, file_count = ?, last_seen = ?
878
+ WHERE id = ?
879
+ """,
880
+ (
881
+ state,
882
+ verification,
883
+ scan["size_bytes"] if scan else 0,
884
+ scan["file_count"] if scan else 0,
885
+ now,
886
+ replica["id"],
887
+ ),
888
+ )
889
+ results.append({"path": replica["path"], "state": state, "scan": scan})
890
+ self._audit(
891
+ conn,
892
+ "model.refresh",
893
+ model["ref"],
894
+ {"paths": [item["path"] for item, _scan in scans], "full_checksum": full_checksum},
895
+ )
896
+ return {"ref": model["ref"], "results": results}
897
+
898
+ def validate_model(self, ref: str, path: str | None = None) -> dict[str, Any]:
899
+ model = self.inspect_model(ref)
900
+ replicas = [
901
+ item
902
+ for item in model["replicas"]
903
+ if item["node_id"] == self.node_id
904
+ and (path is None or item["path"] == str(Path(path).expanduser().resolve()))
905
+ ]
906
+ if not replicas:
907
+ raise KeyError(f"model has no matching replica on this node: {ref}")
908
+ results = []
909
+ for replica in replicas:
910
+ root = Path(replica["path"])
911
+ missing: list[str] = []
912
+ index_files = [
913
+ root / "model.safetensors.index.json",
914
+ root / "pytorch_model.bin.index.json",
915
+ ]
916
+ index_path = next((item for item in index_files if item.is_file()), None)
917
+ referenced: list[str] = []
918
+ parse_error = None
919
+ if index_path is not None:
920
+ try:
921
+ index = json.loads(index_path.read_text(encoding="utf-8"))
922
+ weight_map = index.get("weight_map") or {}
923
+ referenced = sorted(
924
+ {str(item) for item in weight_map.values()}
925
+ )
926
+ missing = [
927
+ item for item in referenced if not (root / item).is_file()
928
+ ]
929
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError, AttributeError) as exc:
930
+ parse_error = str(exc)
931
+ weights = sorted(
932
+ item.name
933
+ for item in root.iterdir()
934
+ if item.is_file()
935
+ and item.suffix.lower() in {".safetensors", ".bin", ".gguf"}
936
+ ) if root.is_dir() else []
937
+ has_config = (root / "config.json").is_file() or (
938
+ root / "adapter_config.json"
939
+ ).is_file()
940
+ valid = bool(root.is_dir() and weights and not missing and not parse_error)
941
+ if not any(item.endswith(".gguf") for item in weights):
942
+ valid = valid and has_config
943
+ results.append(
944
+ {
945
+ "path": replica["path"],
946
+ "valid": valid,
947
+ "index": str(index_path) if index_path else None,
948
+ "referenced_shards": len(referenced),
949
+ "present_weight_files": len(weights),
950
+ "missing_files": missing,
951
+ "parse_error": parse_error,
952
+ }
953
+ )
954
+ return {"ref": model["ref"], "valid": all(item["valid"] for item in results), "results": results}
955
+
956
+ def audit_records(self, ref: str | None = None, limit: int = 100) -> list[dict[str, Any]]:
957
+ if not 1 <= limit <= 1000:
958
+ raise ValueError("audit limit must be between 1 and 1000")
959
+ with self._connection() as conn:
960
+ if ref:
961
+ artifact = self._artifact_row(conn, ref)
962
+ rows = conn.execute(
963
+ "SELECT * FROM audit_log WHERE ref = ? ORDER BY id DESC LIMIT ?",
964
+ (artifact["ref"], limit),
965
+ ).fetchall()
966
+ else:
967
+ rows = conn.execute(
968
+ "SELECT * FROM audit_log ORDER BY id DESC LIMIT ?",
969
+ (limit,),
970
+ ).fetchall()
971
+ results = []
972
+ for row in rows:
973
+ value = dict(row)
974
+ value["details"] = json.loads(value.pop("details_json"))
975
+ results.append(value)
976
+ return results
977
+
978
+ def unregister_model(self, ref: str, path: str | None = None) -> dict[str, Any]:
979
+ with self._connection() as conn:
980
+ artifact = self._artifact_row(conn, ref)
981
+ if path is None:
982
+ removed = conn.execute(
983
+ "DELETE FROM replicas WHERE artifact_id = ? AND node_id = ?",
984
+ (artifact["id"], self.node_id),
985
+ ).rowcount
986
+ else:
987
+ resolved = str(Path(path).expanduser().resolve())
988
+ removed = conn.execute(
989
+ "DELETE FROM replicas WHERE artifact_id = ? AND node_id = ? AND path = ?",
990
+ (artifact["id"], self.node_id, resolved),
991
+ ).rowcount
992
+ remaining = conn.execute(
993
+ "SELECT COUNT(*) AS count FROM replicas WHERE artifact_id = ?",
994
+ (artifact["id"],),
995
+ ).fetchone()["count"]
996
+ if remaining == 0:
997
+ conn.execute(
998
+ "UPDATE artifacts SET deleted_at = ?, updated_at = ? WHERE id = ?",
999
+ (_utc_now(), _utc_now(), artifact["id"]),
1000
+ )
1001
+ self._audit(
1002
+ conn,
1003
+ "model.unregister",
1004
+ artifact["ref"],
1005
+ {
1006
+ "path": path,
1007
+ "removed_replicas": int(removed),
1008
+ "soft_deleted": remaining == 0,
1009
+ },
1010
+ )
1011
+ return {
1012
+ "removed_replicas": int(removed),
1013
+ "soft_deleted": remaining == 0,
1014
+ }
1015
+
1016
+ def verify_model(self, ref: str, full_checksum: bool = False) -> dict[str, Any]:
1017
+ model = self.inspect_model(ref)
1018
+ local = [item for item in model["replicas"] if item["node_id"] == self.node_id]
1019
+ if not local:
1020
+ raise KeyError(f"model has no replica on this node: {ref}")
1021
+ results = []
1022
+ with self._connection() as conn:
1023
+ for replica in local:
1024
+ path = Path(replica["path"])
1025
+ if not path.exists():
1026
+ state = "missing"
1027
+ scanned = None
1028
+ else:
1029
+ require_content = full_checksum or bool(model["metadata"].get("content_digest"))
1030
+ scanned = self.scan_path(path, full_checksum=require_content)
1031
+ actual = (
1032
+ scanned["content_digest"]
1033
+ if require_content
1034
+ else scanned["metadata_digest"]
1035
+ )
1036
+ comparable_expected = (
1037
+ model["metadata"].get("content_digest")
1038
+ if require_content
1039
+ else model["metadata"].get("metadata_digest")
1040
+ )
1041
+ state = "ready" if comparable_expected in {None, actual} else "dirty"
1042
+ verification = (
1043
+ "checksum_verified"
1044
+ if scanned and scanned["content_digest"] and state == "ready"
1045
+ else "metadata_verified"
1046
+ )
1047
+ if (
1048
+ scanned
1049
+ and full_checksum
1050
+ and state == "ready"
1051
+ and not model["metadata"].get("content_digest")
1052
+ ):
1053
+ metadata = dict(model["metadata"])
1054
+ metadata["content_digest"] = scanned["content_digest"]
1055
+ conn.execute(
1056
+ """
1057
+ UPDATE artifacts
1058
+ SET manifest_digest = ?, metadata_json = ?, updated_at = ?
1059
+ WHERE id = ?
1060
+ """,
1061
+ (
1062
+ scanned["content_digest"],
1063
+ json.dumps(metadata, ensure_ascii=False, sort_keys=True),
1064
+ _utc_now(),
1065
+ model["id"],
1066
+ ),
1067
+ )
1068
+ conn.execute(
1069
+ """
1070
+ UPDATE replicas
1071
+ SET state = ?, verification_level = ?, last_seen = ?
1072
+ WHERE id = ?
1073
+ """,
1074
+ (state, verification, _utc_now(), replica["id"]),
1075
+ )
1076
+ results.append({"path": replica["path"], "state": state, "scan": scanned})
1077
+ return {"ref": ref, "results": results}
1078
+
1079
+ def export_records(self) -> dict[str, Any]:
1080
+ exported_at = _utc_now()
1081
+ header = {
1082
+ "type": "meta",
1083
+ "schema_version": SCHEMA_VERSION,
1084
+ "exported_at": exported_at,
1085
+ "source_node": self.node_id,
1086
+ }
1087
+
1088
+ def json_size(value: Any) -> int:
1089
+ try:
1090
+ text = json.dumps(
1091
+ value,
1092
+ ensure_ascii=False,
1093
+ sort_keys=True,
1094
+ allow_nan=False,
1095
+ )
1096
+ return len(text.encode("utf-8"))
1097
+ except (TypeError, ValueError, UnicodeEncodeError) as exc:
1098
+ raise ValueError(
1099
+ f"inventory export contains invalid JSON data: {exc}"
1100
+ ) from exc
1101
+
1102
+ serialized_bytes = json_size(header) + 1
1103
+ if serialized_bytes > MAX_INVENTORY_CONTENT_BYTES:
1104
+ raise ValueError("inventory export content exceeds byte limit")
1105
+ artifacts: list[dict[str, Any]] = []
1106
+ total_aliases = 0
1107
+ total_tags = 0
1108
+ total_replicas = 0
1109
+ with self._connection() as conn:
1110
+ rows = conn.execute("SELECT * FROM artifacts ORDER BY ref")
1111
+ for artifact_index, row in enumerate(rows):
1112
+ if artifact_index >= MAX_INVENTORY_ARTIFACTS:
1113
+ raise ValueError("inventory contains too many artifacts")
1114
+ artifact = dict(row)
1115
+ artifact["metadata"] = json.loads(artifact.pop("metadata_json"))
1116
+ artifact["aliases"] = []
1117
+ artifact["tags"] = []
1118
+ artifact["replicas"] = []
1119
+ artifact_id = artifact["id"]
1120
+ artifact.pop("id", None)
1121
+
1122
+ artifact_size = json_size(
1123
+ {"type": "artifact", "value": artifact}
1124
+ )
1125
+ if (
1126
+ serialized_bytes + artifact_size + 1
1127
+ > MAX_INVENTORY_CONTENT_BYTES
1128
+ ):
1129
+ raise ValueError("inventory export content exceeds byte limit")
1130
+
1131
+ def append_item(
1132
+ field: str,
1133
+ item: Any,
1134
+ *,
1135
+ per_artifact_limit: int,
1136
+ ) -> None:
1137
+ nonlocal artifact_size
1138
+ values = artifact[field]
1139
+ if len(values) >= per_artifact_limit:
1140
+ raise ValueError(
1141
+ f"inventory artifact has too many {field}"
1142
+ )
1143
+ added_size = json_size(item) + (2 if values else 0)
1144
+ if (
1145
+ serialized_bytes + artifact_size + added_size + 1
1146
+ > MAX_INVENTORY_CONTENT_BYTES
1147
+ ):
1148
+ raise ValueError(
1149
+ "inventory export content exceeds byte limit"
1150
+ )
1151
+ values.append(item)
1152
+ artifact_size += added_size
1153
+
1154
+ for value in conn.execute(
1155
+ """
1156
+ SELECT alias FROM artifact_aliases
1157
+ WHERE artifact_id = ? ORDER BY alias
1158
+ """,
1159
+ (artifact_id,),
1160
+ ):
1161
+ total_aliases += 1
1162
+ if total_aliases > MAX_INVENTORY_TOTAL_ALIASES:
1163
+ raise ValueError("inventory has too many total aliases")
1164
+ append_item(
1165
+ "aliases",
1166
+ value["alias"],
1167
+ per_artifact_limit=MAX_INVENTORY_ALIASES_PER_ARTIFACT,
1168
+ )
1169
+ for value in conn.execute(
1170
+ """
1171
+ SELECT tag FROM artifact_tags
1172
+ WHERE artifact_id = ? ORDER BY tag
1173
+ """,
1174
+ (artifact_id,),
1175
+ ):
1176
+ total_tags += 1
1177
+ if total_tags > MAX_INVENTORY_TOTAL_TAGS:
1178
+ raise ValueError("inventory has too many total tags")
1179
+ append_item(
1180
+ "tags",
1181
+ value["tag"],
1182
+ per_artifact_limit=MAX_INVENTORY_TAGS_PER_ARTIFACT,
1183
+ )
1184
+ for value in conn.execute(
1185
+ """
1186
+ SELECT * FROM replicas
1187
+ WHERE artifact_id = ? ORDER BY node_id, path
1188
+ """,
1189
+ (artifact_id,),
1190
+ ):
1191
+ total_replicas += 1
1192
+ if total_replicas > MAX_INVENTORY_TOTAL_REPLICAS:
1193
+ raise ValueError("inventory has too many total replicas")
1194
+ replica = dict(value)
1195
+ replica.pop("id", None)
1196
+ replica.pop("artifact_id", None)
1197
+ append_item(
1198
+ "replicas",
1199
+ replica,
1200
+ per_artifact_limit=MAX_INVENTORY_REPLICAS_PER_ARTIFACT,
1201
+ )
1202
+
1203
+ exact_size = json_size(
1204
+ {"type": "artifact", "value": artifact}
1205
+ )
1206
+ if (
1207
+ serialized_bytes + exact_size + 1
1208
+ > MAX_INVENTORY_CONTENT_BYTES
1209
+ ):
1210
+ raise ValueError("inventory export content exceeds byte limit")
1211
+ artifacts.append(artifact)
1212
+ serialized_bytes += exact_size + 1
1213
+ return {
1214
+ "schema_version": SCHEMA_VERSION,
1215
+ "exported_at": exported_at,
1216
+ "source_node": self.node_id,
1217
+ "artifacts": artifacts,
1218
+ }
1219
+
1220
+ @staticmethod
1221
+ def _normalize_root_maps(
1222
+ root_maps: Iterable[tuple[str, str]],
1223
+ ) -> tuple[tuple[Path, Path], ...]:
1224
+ result: list[tuple[Path, Path]] = []
1225
+ for index, item in enumerate(root_maps):
1226
+ if index >= MAX_ROOT_MAPS:
1227
+ raise ValueError(
1228
+ f"root maps must contain at most {MAX_ROOT_MAPS} pairs"
1229
+ )
1230
+ if (
1231
+ not isinstance(item, (list, tuple))
1232
+ or isinstance(item, (str, bytes))
1233
+ or len(item) != 2
1234
+ ):
1235
+ raise ValueError(
1236
+ "root maps must contain old/new path pairs"
1237
+ )
1238
+ old, new = item
1239
+ old_text = str(old)
1240
+ new_text = str(new)
1241
+ for value, name in ((old_text, "old"), (new_text, "new")):
1242
+ if not value:
1243
+ raise ValueError(f"root mapping {name} must not be empty")
1244
+ try:
1245
+ path_bytes = len(value.encode("utf-8"))
1246
+ except UnicodeEncodeError as exc:
1247
+ raise ValueError(
1248
+ f"root mapping {name} is not valid UTF-8"
1249
+ ) from exc
1250
+ if path_bytes > MAX_ROOT_MAP_PATH_BYTES:
1251
+ raise ValueError(
1252
+ f"root mapping {name} exceeds "
1253
+ f"{MAX_ROOT_MAP_PATH_BYTES} UTF-8 bytes"
1254
+ )
1255
+ result.append(
1256
+ (Path(old_text).expanduser(), Path(new_text).expanduser())
1257
+ )
1258
+ return tuple(result)
1259
+
1260
+ @staticmethod
1261
+ def _mapped_path(
1262
+ path: str,
1263
+ root_maps: Iterable[tuple[Path, Path]],
1264
+ ) -> str | None:
1265
+ source = Path(path)
1266
+ for old, new in root_maps:
1267
+ try:
1268
+ relative = source.relative_to(old)
1269
+ except ValueError:
1270
+ continue
1271
+ return str(new / relative)
1272
+ return None
1273
+
1274
+ def import_records(
1275
+ self,
1276
+ payload: dict[str, Any],
1277
+ root_maps: Iterable[tuple[str, str]] = (),
1278
+ ) -> dict[str, int]:
1279
+ if payload.get("schema_version") != SCHEMA_VERSION:
1280
+ raise ValueError(f"unsupported inventory schema: {payload.get('schema_version')}")
1281
+ maps = self._normalize_root_maps(root_maps)
1282
+ artifacts = payload.get("artifacts", [])
1283
+ if not isinstance(artifacts, list):
1284
+ raise ValueError("inventory artifacts must be an array")
1285
+ if len(artifacts) > MAX_INVENTORY_ARTIFACTS:
1286
+ raise ValueError("inventory contains too many artifacts")
1287
+ total_aliases = 0
1288
+ total_tags = 0
1289
+ total_replicas = 0
1290
+ for artifact in artifacts:
1291
+ if not isinstance(artifact, dict):
1292
+ raise ValueError("each inventory artifact must be an object")
1293
+ aliases = artifact.get("aliases", [])
1294
+ tags = artifact.get("tags", [])
1295
+ replicas = artifact.get("replicas", [])
1296
+ for name, values in (
1297
+ ("aliases", aliases),
1298
+ ("tags", tags),
1299
+ ("replicas", replicas),
1300
+ ):
1301
+ if not isinstance(values, list):
1302
+ raise ValueError(
1303
+ f"inventory artifact {name} must be an array"
1304
+ )
1305
+ total_aliases += len(aliases)
1306
+ total_tags += len(tags)
1307
+ total_replicas += len(replicas)
1308
+ if total_aliases > MAX_INVENTORY_TOTAL_ALIASES:
1309
+ raise ValueError("inventory has too many total aliases")
1310
+ if total_tags > MAX_INVENTORY_TOTAL_TAGS:
1311
+ raise ValueError("inventory has too many total tags")
1312
+ if total_replicas > MAX_INVENTORY_TOTAL_REPLICAS:
1313
+ raise ValueError("inventory has too many total replicas")
1314
+
1315
+ prepared: list[
1316
+ tuple[dict[str, Any], str, tuple[dict[str, Any], str] | None]
1317
+ ] = []
1318
+ for artifact in artifacts:
1319
+ ref = artifact.get("ref")
1320
+ if not ref:
1321
+ continue
1322
+ registration = None
1323
+ metadata = artifact.get("metadata") or {}
1324
+ require_content = bool(metadata.get("content_digest"))
1325
+ for replica in artifact.get("replicas", []):
1326
+ mapped = self._mapped_path(str(replica.get("path", "")), maps)
1327
+ if mapped and Path(mapped).expanduser().exists():
1328
+ scope = replica.get("scope", "user")
1329
+ if scope not in {"user", "team", "system"}:
1330
+ raise ValueError(f"unsupported scope: {scope}")
1331
+ registration = (
1332
+ self.scan_path(
1333
+ Path(mapped),
1334
+ full_checksum=require_content,
1335
+ ),
1336
+ scope,
1337
+ )
1338
+ break
1339
+ prepared.append((artifact, ref, registration))
1340
+
1341
+ rebound_replicas = 0
1342
+ with self._connection() as conn:
1343
+ for artifact, ref, registration in prepared:
1344
+ artifact_id = self._upsert_artifact(
1345
+ conn,
1346
+ ref=ref,
1347
+ manifest_digest=artifact.get("manifest_digest"),
1348
+ format_name=artifact.get("format"),
1349
+ revision=artifact.get("revision"),
1350
+ variant=artifact.get("variant"),
1351
+ metadata=artifact.get("metadata") or {},
1352
+ )
1353
+ conn.execute(
1354
+ """
1355
+ UPDATE artifacts
1356
+ SET description = ?, deleted_at = ?, updated_at = ?
1357
+ WHERE id = ?
1358
+ """,
1359
+ (
1360
+ artifact.get("description"),
1361
+ artifact.get("deleted_at"),
1362
+ _utc_now(),
1363
+ artifact_id,
1364
+ ),
1365
+ )
1366
+ for alias in artifact.get("aliases") or []:
1367
+ conn.execute(
1368
+ "INSERT OR IGNORE INTO artifact_aliases (artifact_id, alias) VALUES (?, ?)",
1369
+ (artifact_id, str(alias)),
1370
+ )
1371
+ for tag in artifact.get("tags") or []:
1372
+ conn.execute(
1373
+ "INSERT OR IGNORE INTO artifact_tags (artifact_id, tag) VALUES (?, ?)",
1374
+ (artifact_id, str(tag)),
1375
+ )
1376
+ self._audit(conn, "model.import", ref)
1377
+ if registration is not None:
1378
+ scanned, scope = registration
1379
+ self._register_scanned(
1380
+ conn,
1381
+ ref=ref,
1382
+ scanned=scanned,
1383
+ scope=scope,
1384
+ format_name=artifact.get("format"),
1385
+ )
1386
+ rebound_replicas += 1
1387
+ return {
1388
+ "imported_artifacts": len(prepared),
1389
+ "rebound_replicas": rebound_replicas,
1390
+ }