prometheusnn 0.2.1__tar.gz → 0.2.2__tar.gz

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 (77) hide show
  1. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/CHANGELOG.md +12 -0
  2. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/PKG-INFO +1 -1
  3. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/pyproject.toml +1 -1
  4. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/inference/novelty.py +17 -8
  5. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/ui_server.py +1 -1
  6. prometheusnn-0.2.2/tests/test_novelty.py +100 -0
  7. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/uv.lock +1 -1
  8. prometheusnn-0.2.1/tests/test_novelty.py +0 -50
  9. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/.github/workflows/ci.yml +0 -0
  10. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/.github/workflows/publish.yml +0 -0
  11. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/.gitignore +0 -0
  12. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/LICENSE +0 -0
  13. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/README.md +0 -0
  14. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/docs/beam-search-strategy.md +0 -0
  15. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/docs/roadmap-self-aware-server.md +0 -0
  16. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/scripts/parse_hts.py +0 -0
  17. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/scripts/parse_mesh.py +0 -0
  18. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/__init__.py +0 -0
  19. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/core/__init__.py +0 -0
  20. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/core/config.py +0 -0
  21. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/core/embedding.py +0 -0
  22. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/core/logging.py +0 -0
  23. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/core/lru.py +0 -0
  24. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/core/mcp_safety.py +0 -0
  25. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/core/responses.py +0 -0
  26. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/data/__init__.py +0 -0
  27. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/data/loader.py +0 -0
  28. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/data/splits.py +0 -0
  29. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/data/taxonomy.py +0 -0
  30. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/inference/__init__.py +0 -0
  31. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/inference/beam_search.py +0 -0
  32. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/inference/predictor.py +0 -0
  33. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/inference/router.py +0 -0
  34. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/inference/summary.py +0 -0
  35. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/insights.py +0 -0
  36. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/intake/__init__.py +0 -0
  37. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/intake/detector.py +0 -0
  38. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/intake/models.py +0 -0
  39. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/intake/profiler.py +0 -0
  40. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/intake/recommender.py +0 -0
  41. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/intake/scoper.py +0 -0
  42. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/py.typed +0 -0
  43. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/server/INSTRUCTIONS.md +0 -0
  44. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/server/__init__.py +0 -0
  45. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/server/tools/__init__.py +0 -0
  46. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/server/tools/evaluation.py +0 -0
  47. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/server/tools/inference.py +0 -0
  48. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/server/tools/intake.py +0 -0
  49. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/server/tools/management.py +0 -0
  50. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/server/tools/training.py +0 -0
  51. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/storage/__init__.py +0 -0
  52. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/storage/model_store.py +0 -0
  53. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/training/__init__.py +0 -0
  54. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/training/calibration.py +0 -0
  55. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/training/cascade.py +0 -0
  56. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/training/classifier.py +0 -0
  57. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/training/noise.py +0 -0
  58. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/training/trainer.py +0 -0
  59. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/__init__.py +0 -0
  60. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/conftest.py +0 -0
  61. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/fixtures/naics_test.csv +0 -0
  62. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_beam_search.py +0 -0
  63. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_calibration.py +0 -0
  64. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_classifier.py +0 -0
  65. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_insights.py +0 -0
  66. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_intake.py +0 -0
  67. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_integration.py +0 -0
  68. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_noise.py +0 -0
  69. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_predictor.py +0 -0
  70. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_router.py +0 -0
  71. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_server.py +0 -0
  72. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_splits.py +0 -0
  73. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_storage.py +0 -0
  74. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_summary.py +0 -0
  75. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_taxonomy.py +0 -0
  76. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_trainer.py +0 -0
  77. {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_ui_server.py +0 -0
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.2] - 2026-10-30
9
+
10
+ ### Security
11
+ - **Novelty data is loaded without pickle.** `novelty_centroids.npz` was loaded
12
+ with `allow_pickle=True`, so a crafted model directory could run code in the
13
+ server when any tool loaded that model. In 0.2.0 and earlier this was
14
+ reachable through the `model_name` path traversal fixed in 0.2.1; in 0.2.1 it
15
+ still applied to a malicious model placed in the models directory. Both
16
+ novelty files now load with `allow_pickle=False`. Models saved by
17
+ PrometheusNN contain only numeric and string arrays and load unchanged; a
18
+ file holding pickled objects is refused with an error asking for a retrain.
19
+
8
20
  ## [0.2.1] - 2026-10-30
9
21
 
10
22
  ### Security
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: prometheusnn
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Frictionless hierarchical text classification — an MCP server that guides you from CSV to trained classifier
5
5
  Project-URL: Homepage, https://github.com/mfbaig35r/prometheusnn
6
6
  Project-URL: Repository, https://github.com/mfbaig35r/prometheusnn
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "prometheusnn"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "Frictionless hierarchical text classification — an MCP server that guides you from CSV to trained classifier"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -116,17 +116,26 @@ class NoveltyDetector:
116
116
 
117
117
  def load(self, path: Path):
118
118
  path = Path(path)
119
- data = np.load(path / "novelty_centroids.npz", allow_pickle=True)
120
- class_names = data["class_names"]
121
- mean_dists = data["mean_dists"]
122
- std_dists = data["std_dists"]
123
- p95_dists = data["p95_dists"]
124
- counts = data["counts"]
119
+ # Never unpickle: a model directory is data, and a crafted .npz with object arrays would
120
+ # run code on load. save() writes only numeric and string arrays, which need no pickle.
121
+ data = np.load(path / "novelty_centroids.npz", allow_pickle=False)
122
+ try:
123
+ class_names = data["class_names"]
124
+ mean_dists = data["mean_dists"]
125
+ std_dists = data["std_dists"]
126
+ p95_dists = data["p95_dists"]
127
+ counts = data["counts"]
128
+ centroids = [data[f"centroid_{i}"] for i in range(len(class_names))]
129
+ except ValueError as exc:
130
+ raise ValueError(
131
+ f"{path / 'novelty_centroids.npz'} contains pickled objects; refusing to load it. "
132
+ "Retrain the model to regenerate it."
133
+ ) from exc
125
134
 
126
135
  self.centroids = {}
127
136
  for i, name in enumerate(class_names):
128
137
  self.centroids[str(name)] = ClassCentroid(
129
- centroid=data[f"centroid_{i}"],
138
+ centroid=centroids[i],
130
139
  mean_dist=float(mean_dists[i]),
131
140
  std_dist=max(float(std_dists[i]), 0.1),
132
141
  p95_dist=float(p95_dists[i]),
@@ -135,7 +144,7 @@ class NoveltyDetector:
135
144
 
136
145
  emb_path = path / "novelty_embeddings.npy"
137
146
  if emb_path.exists():
138
- self.training_embeddings = np.load(emb_path)
147
+ self.training_embeddings = np.load(emb_path, allow_pickle=False)
139
148
 
140
149
  def get_centroids_data(self) -> dict:
141
150
  """Return data dict suitable for np.savez."""
@@ -236,7 +236,7 @@ async def _handle_message(ws, msg: dict[str, Any]) -> None:
236
236
  try:
237
237
  from fastapi import FastAPI, WebSocket, WebSocketDisconnect
238
238
 
239
- app: FastAPI | None = FastAPI(title="prometheusnn-ui", version="0.2.1")
239
+ app: FastAPI | None = FastAPI(title="prometheusnn-ui", version="0.2.2")
240
240
 
241
241
  @app.get("/health")
242
242
  def _health() -> dict:
@@ -0,0 +1,100 @@
1
+ """Tests for novelty detection."""
2
+
3
+ import tempfile
4
+ from pathlib import Path
5
+
6
+ import numpy as np
7
+ import pytest
8
+
9
+ from prometheus.inference.novelty import NoveltyDetector
10
+
11
+
12
+ def test_fit_and_score():
13
+ rng = np.random.default_rng(42)
14
+ n = 50
15
+ embs = rng.standard_normal((n, 16)).astype(np.float32)
16
+ labels = np.array([0] * 25 + [1] * 25)
17
+ classes = ["classA", "classB"]
18
+
19
+ det = NoveltyDetector(z_score_threshold=2.5)
20
+ det.fit(embs, labels, classes)
21
+
22
+ assert len(det.centroids) == 2
23
+
24
+ # Score an in-distribution sample
25
+ score = det.score(embs[0], "classA", confidence=0.9)
26
+ assert not score.is_novel
27
+
28
+ # Score a far-out sample
29
+ outlier = np.ones(16, dtype=np.float32) * 100
30
+ score = det.score(outlier, "classA", confidence=0.3)
31
+ assert score.centroid_z_score > 2.5
32
+
33
+
34
+ def test_save_load_roundtrip():
35
+ rng = np.random.default_rng(42)
36
+ embs = rng.standard_normal((30, 8)).astype(np.float32)
37
+ labels = np.array([0] * 15 + [1] * 15)
38
+ classes = ["X", "Y"]
39
+
40
+ det = NoveltyDetector()
41
+ det.fit(embs, labels, classes)
42
+
43
+ with tempfile.TemporaryDirectory() as tmpdir:
44
+ det.save(Path(tmpdir))
45
+
46
+ det2 = NoveltyDetector()
47
+ det2.load(Path(tmpdir))
48
+
49
+ assert set(det2.centroids.keys()) == {"X", "Y"}
50
+ assert det2.training_embeddings is not None
51
+ assert det2.training_embeddings.shape == (30, 8)
52
+
53
+
54
+ class _Payload:
55
+ """Unpickling this creates a file: proof that loading ran attacker code."""
56
+
57
+ def __init__(self, marker: Path):
58
+ self.marker = str(marker)
59
+
60
+ def __reduce__(self):
61
+ return (open, (self.marker, "w"))
62
+
63
+
64
+ def test_load_never_unpickles_centroids(tmp_path):
65
+ marker = tmp_path / "executed"
66
+ np.savez(
67
+ tmp_path / "novelty_centroids.npz",
68
+ centroid_0=np.zeros(4),
69
+ class_names=np.array([_Payload(marker)], dtype=object),
70
+ mean_dists=[1.0],
71
+ std_dists=[1.0],
72
+ p95_dists=[1.0],
73
+ counts=[1],
74
+ )
75
+ with pytest.raises(ValueError, match="pickled objects"):
76
+ NoveltyDetector().load(tmp_path)
77
+ assert not marker.exists()
78
+
79
+
80
+ def test_load_never_unpickles_embeddings(tmp_path):
81
+ marker = tmp_path / "executed"
82
+ det = NoveltyDetector()
83
+ det.fit(np.eye(4, dtype=np.float32), np.array([0, 0, 1, 1]), ["a", "b"])
84
+ det.save(tmp_path)
85
+ np.save(tmp_path / "novelty_embeddings.npy", np.array([_Payload(marker)], dtype=object))
86
+ with pytest.raises(ValueError):
87
+ NoveltyDetector().load(tmp_path)
88
+ assert not marker.exists()
89
+
90
+
91
+ def test_saved_files_need_no_pickle(tmp_path):
92
+ rng = np.random.default_rng(0)
93
+ det = NoveltyDetector()
94
+ det.fit(rng.standard_normal((20, 4)), np.array([0] * 10 + [1] * 10), ["Café", "naïve/class"])
95
+ det.save(tmp_path)
96
+ with np.load(tmp_path / "novelty_centroids.npz", allow_pickle=False) as data:
97
+ assert all(data[k].dtype != object for k in data.files)
98
+ det2 = NoveltyDetector()
99
+ det2.load(tmp_path)
100
+ assert set(det2.centroids) == {"Café", "naïve/class"}
@@ -907,7 +907,7 @@ wheels = [
907
907
 
908
908
  [[package]]
909
909
  name = "prometheusnn"
910
- version = "0.2.1"
910
+ version = "0.2.2"
911
911
  source = { editable = "." }
912
912
  dependencies = [
913
913
  { name = "mcp", extra = ["cli"] },
@@ -1,50 +0,0 @@
1
- """Tests for novelty detection."""
2
-
3
- import tempfile
4
- from pathlib import Path
5
-
6
- import numpy as np
7
-
8
- from prometheus.inference.novelty import NoveltyDetector
9
-
10
-
11
- def test_fit_and_score():
12
- rng = np.random.default_rng(42)
13
- n = 50
14
- embs = rng.standard_normal((n, 16)).astype(np.float32)
15
- labels = np.array([0] * 25 + [1] * 25)
16
- classes = ["classA", "classB"]
17
-
18
- det = NoveltyDetector(z_score_threshold=2.5)
19
- det.fit(embs, labels, classes)
20
-
21
- assert len(det.centroids) == 2
22
-
23
- # Score an in-distribution sample
24
- score = det.score(embs[0], "classA", confidence=0.9)
25
- assert not score.is_novel
26
-
27
- # Score a far-out sample
28
- outlier = np.ones(16, dtype=np.float32) * 100
29
- score = det.score(outlier, "classA", confidence=0.3)
30
- assert score.centroid_z_score > 2.5
31
-
32
-
33
- def test_save_load_roundtrip():
34
- rng = np.random.default_rng(42)
35
- embs = rng.standard_normal((30, 8)).astype(np.float32)
36
- labels = np.array([0] * 15 + [1] * 15)
37
- classes = ["X", "Y"]
38
-
39
- det = NoveltyDetector()
40
- det.fit(embs, labels, classes)
41
-
42
- with tempfile.TemporaryDirectory() as tmpdir:
43
- det.save(Path(tmpdir))
44
-
45
- det2 = NoveltyDetector()
46
- det2.load(Path(tmpdir))
47
-
48
- assert set(det2.centroids.keys()) == {"X", "Y"}
49
- assert det2.training_embeddings is not None
50
- assert det2.training_embeddings.shape == (30, 8)
File without changes
File without changes
File without changes