ski-model-deploy 3.1.0a2__tar.gz → 3.2.0__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 (18) hide show
  1. {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/PKG-INFO +5 -2
  2. {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/pyproject.toml +5 -2
  3. {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/src/ski_model_deploy/__init__.py +1 -1
  4. {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/src/ski_model_deploy/cli.py +31 -3
  5. {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/src/ski_model_deploy/deployer.py +53 -6
  6. {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/src/ski_model_deploy/utils.py +8 -7
  7. {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/src/ski_model_deploy.egg-info/PKG-INFO +5 -2
  8. {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/src/ski_model_deploy.egg-info/requires.txt +2 -1
  9. ski_model_deploy-3.2.0/tests/test_deployer.py +183 -0
  10. ski_model_deploy-3.1.0a2/tests/test_deployer.py +0 -43
  11. {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/README.md +0 -0
  12. {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/setup.cfg +0 -0
  13. {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/src/ski_model_deploy/models.py +0 -0
  14. {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/src/ski_model_deploy/py.typed +0 -0
  15. {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/src/ski_model_deploy.egg-info/SOURCES.txt +0 -0
  16. {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/src/ski_model_deploy.egg-info/dependency_links.txt +0 -0
  17. {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/src/ski_model_deploy.egg-info/entry_points.txt +0 -0
  18. {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/src/ski_model_deploy.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ski-model-deploy
3
- Version: 3.1.0a2
3
+ Version: 3.2.0
4
4
  Summary: Deploy and verify signed Knowledge Graphs against a SKI Model runtime.
5
5
  Author-email: KpiFinity <hello@kpifinity.com>
6
6
  License-Expression: Apache-2.0
@@ -15,16 +15,19 @@ Classifier: Programming Language :: Python :: 3
15
15
  Classifier: Programming Language :: Python :: 3.10
16
16
  Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
18
20
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
21
  Requires-Python: >=3.10
20
22
  Description-Content-Type: text/markdown
23
+ Requires-Dist: ski-schemas<4,>=3.2.0
21
24
  Requires-Dist: click<9,>=8.1.7
22
25
  Requires-Dist: pydantic<3,>=2.13.4
23
26
  Requires-Dist: python-dotenv<2,>=1.2.2
24
27
  Requires-Dist: pyyaml<7,>=6.0.1
25
28
  Requires-Dist: jinja2<4,>=3.1.6
26
29
  Requires-Dist: httpx<0.29,>=0.27
27
- Requires-Dist: cryptography<48,>=46
30
+ Requires-Dist: cryptography<52,>=46
28
31
  Requires-Dist: docker<8,>=7
29
32
  Requires-Dist: psycopg[binary]<4,>=3.1.18
30
33
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ski-model-deploy"
7
- version = "3.1.0a2"
7
+ version = "3.2.0"
8
8
  description = "Deploy and verify signed Knowledge Graphs against a SKI Model runtime."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -19,16 +19,19 @@ classifiers = [
19
19
  "Programming Language :: Python :: 3.10",
20
20
  "Programming Language :: Python :: 3.11",
21
21
  "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
22
24
  "Topic :: Software Development :: Libraries :: Python Modules",
23
25
  ]
24
26
  dependencies = [
27
+ "ski-schemas>=3.2.0,<4",
25
28
  "click>=8.1.7,<9",
26
29
  "pydantic>=2.13.4,<3",
27
30
  "python-dotenv>=1.2.2,<2",
28
31
  "pyyaml>=6.0.1,<7",
29
32
  "jinja2>=3.1.6,<4",
30
33
  "httpx>=0.27,<0.29",
31
- "cryptography>=46,<48",
34
+ "cryptography>=46,<52",
32
35
  "docker>=7,<8",
33
36
  "psycopg[binary]>=3.1.18,<4",
34
37
  ]
@@ -3,7 +3,7 @@
3
3
  from .deployer import Deployer, UnsignedKGError
4
4
  from .models import DeploymentConfig, DeploymentStatus
5
5
 
6
- __version__ = "3.1.0a2"
6
+ __version__ = "3.2.0"
7
7
  __author__ = "KpiFinity"
8
8
 
9
9
  __all__ = ["Deployer", "DeploymentConfig", "DeploymentStatus", "UnsignedKGError"]
@@ -3,10 +3,12 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
+ from pathlib import Path
7
+ from typing import Optional
6
8
 
7
9
  import click
8
10
 
9
- from .deployer import Deployer, UnsignedKGError
11
+ from .deployer import Deployer, UnsignedKGError, sign_kg
10
12
  from .utils import load_config, save_config
11
13
 
12
14
 
@@ -37,10 +39,36 @@ def init(name: str, sector: str, mode: str, output: str) -> None:
37
39
  click.echo(f" 3. Start the stack: ski-model-deploy start --config {output}")
38
40
 
39
41
 
42
+ @main.command()
43
+ @click.option("--kg", "-k", required=True, help="Knowledge Graph JSON file to sign")
44
+ @click.option("--key", required=True, help="Ed25519 private key (PEM, unencrypted) of the KG signer")
45
+ @click.option("--output", "-o", required=True, help="Where to write the signed KG")
46
+ def sign(kg: str, key: str, output: str) -> None:
47
+ """Sign a Knowledge Graph with an Ed25519 key (spec §3.5).
48
+
49
+ The runtime loads the result only if this key's public half is one of
50
+ its trust anchors (SKI_KG_TRUST_ANCHORS).
51
+ """
52
+ try:
53
+ kg_data = json.loads(Path(kg).read_text(encoding="utf-8"))
54
+ signed = sign_kg(kg_data, Path(key).read_bytes())
55
+ Path(output).write_text(json.dumps(signed, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
56
+ click.echo(f"✓ Signed {kg} -> {output}")
57
+ except Exception as exc:
58
+ click.echo(f"Error: {exc}", err=True)
59
+ raise SystemExit(1)
60
+
61
+
40
62
  @main.command(name="load-kg")
41
63
  @click.option("--config", "-c", required=True, help="Deployment configuration file")
42
64
  @click.option("--kg", "-k", required=True, help="Knowledge Graph JSON file (MUST be signed)")
43
- def load_kg(config: str, kg: str) -> None:
65
+ @click.option(
66
+ "--trust-anchors",
67
+ envvar="SKI_KG_TRUST_ANCHORS",
68
+ default=None,
69
+ help="PEM bundle of Ed25519 public keys allowed to sign the KG (default: $SKI_KG_TRUST_ANCHORS).",
70
+ )
71
+ def load_kg(config: str, kg: str, trust_anchors: Optional[str]) -> None:
44
72
  """Verify a signed Knowledge Graph and register it with a deployment.
45
73
 
46
74
  Signature verification is MANDATORY. There is no override flag here.
@@ -52,7 +80,7 @@ def load_kg(config: str, kg: str) -> None:
52
80
  deployer = Deployer(config)
53
81
  if not deployer.config:
54
82
  deployer.config = load_config(config)
55
- deployer.load_knowledge_graph(kg_path=kg)
83
+ deployer.load_knowledge_graph(kg_path=kg, trust_anchors=trust_anchors)
56
84
  save_config(deployer.config, config)
57
85
  click.echo("✓ Knowledge Graph signature verified and registered.")
58
86
  click.echo(f" Version: {deployer.config.knowledge_graph.version}")
@@ -17,7 +17,10 @@ import os
17
17
  import subprocess
18
18
  import time
19
19
  from pathlib import Path
20
- from typing import Dict, Optional
20
+ from typing import Any, Dict, Optional
21
+
22
+ from cryptography.hazmat.primitives import serialization
23
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
21
24
 
22
25
  from .models import DeploymentConfig, DeploymentMode, DeploymentStatus, HealthCheck
23
26
  from .utils import generate_docker_compose, load_config, save_config
@@ -29,17 +32,51 @@ try:
29
32
 
30
33
  sys.path.insert(
31
34
  0,
32
- str(Path(__file__).resolve().parents[5] / "reference-implementation" / "src"),
35
+ str(Path(__file__).resolve().parents[4] / "reference-implementation" / "src"),
36
+ )
37
+ from ski_model.kg_loader import ( # type: ignore
38
+ KGTrustPolicy,
39
+ KnowledgeGraph,
40
+ load_trust_anchors,
41
+ signing_bytes,
33
42
  )
34
- from ski_model.kg_loader import KnowledgeGraph # type: ignore
35
43
  except Exception: # pragma: no cover — fall back when run as installed package
36
44
  KnowledgeGraph = None # type: ignore
45
+ KGTrustPolicy = None # type: ignore
46
+ load_trust_anchors = None # type: ignore
47
+ signing_bytes = None # type: ignore
37
48
 
38
49
 
39
50
  class UnsignedKGError(RuntimeError):
40
51
  """Raised if signature verification fails or is missing."""
41
52
 
42
53
 
54
+ def sign_kg(kg_data: Dict[str, Any], private_key_pem: bytes) -> Dict[str, Any]:
55
+ """Return ``kg_data`` with an Ed25519 ``signature`` block over its signing bytes.
56
+
57
+ Any existing signature is replaced. The bytes are the runtime's own
58
+ :func:`ski_model.kg_loader.signing_bytes` (spec §3.5), so what this signs
59
+ is exactly what the runtime verifies.
60
+ """
61
+ if signing_bytes is None:
62
+ raise RuntimeError("The runtime KG loader is unavailable; cannot compute the KG signing bytes.")
63
+ key = serialization.load_pem_private_key(private_key_pem, password=None)
64
+ if not isinstance(key, Ed25519PrivateKey):
65
+ raise ValueError("The signing key must be an Ed25519 private key (PEM, unencrypted).")
66
+ unsigned = {k: v for k, v in kg_data.items() if k != "signature"}
67
+ public_pem = key.public_key().public_bytes(
68
+ serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo
69
+ )
70
+ return {
71
+ **unsigned,
72
+ "signature": {
73
+ "algorithm": "ed25519",
74
+ "public_key_pem": public_pem.decode("ascii"),
75
+ "value_hex": key.sign(signing_bytes(unsigned)).hex(),
76
+ },
77
+ }
78
+
79
+
43
80
  class Deployer:
44
81
  """Deploy and manage the SKI Model runtime."""
45
82
 
@@ -68,11 +105,18 @@ class Deployer:
68
105
  # KG loading — signature verification is MANDATORY.
69
106
  # ------------------------------------------------------------------
70
107
 
71
- def load_knowledge_graph(self, kg_path: str) -> bool:
108
+ def load_knowledge_graph(self, kg_path: str, trust_anchors: Optional[str] = None) -> bool:
72
109
  """Verify a Knowledge Graph signature and register it with the deployment.
73
110
 
74
111
  Raises UnsignedKGError if the KG has no signature or the signature
75
112
  does not verify. There is no flag to bypass this.
113
+
114
+ Framework v3.2.0: the signature must verify against an operator
115
+ trust anchor, exactly as the runtime checks it. ``trust_anchors`` is
116
+ the path of a PEM bundle of Ed25519 public keys; when omitted the
117
+ runtime's own configuration applies (``SKI_KG_TRUST_ANCHORS``, or
118
+ the dev-only ``SKI_KG_ALLOW_UNANCHORED=true``), and with neither the
119
+ KG is refused.
76
120
  """
77
121
  if not os.path.exists(kg_path):
78
122
  raise FileNotFoundError(f"Knowledge Graph not found: {kg_path}")
@@ -96,7 +140,8 @@ class Deployer:
96
140
  )
97
141
 
98
142
  try:
99
- kg = KnowledgeGraph.from_dict(kg_data, require_signature=True)
143
+ trust = KGTrustPolicy(anchors=load_trust_anchors(Path(trust_anchors))) if trust_anchors else None
144
+ kg = KnowledgeGraph.from_dict(kg_data, require_signature=True, trust=trust)
100
145
  except Exception as exc:
101
146
  raise UnsignedKGError(f"Signature verification failed for {kg_path}: {exc}") from exc
102
147
 
@@ -106,7 +151,9 @@ class Deployer:
106
151
  self.config.knowledge_graph = KnowledgeGraphConfig(
107
152
  path=kg_path,
108
153
  version=kg.version,
109
- signature_verified=True,
154
+ # True only when verified against a trust anchor; an
155
+ # unanchored (SKI_KG_ALLOW_UNANCHORED) load is not "verified".
156
+ signature_verified=kg.signature_verified,
110
157
  )
111
158
  return True
112
159
 
@@ -25,7 +25,7 @@ def load_config(config_path: str) -> DeploymentConfig:
25
25
 
26
26
 
27
27
  def save_config(config: DeploymentConfig, output_path: str) -> None:
28
- data = config.model_dump()
28
+ data = config.model_dump(mode="json")
29
29
  if output_path.endswith((".yaml", ".yml")):
30
30
  with open(output_path, "w") as f:
31
31
  yaml.dump(data, f, default_flow_style=False, sort_keys=True)
@@ -38,16 +38,17 @@ def save_config(config: DeploymentConfig, output_path: str) -> None:
38
38
  # Docker compose template
39
39
  # ---------------------------------------------------------------------------
40
40
  #
41
- # Sovereign-by-default: no ANTHROPIC_API_KEY anywhere. Ollama is the
42
- # inference backend. Operators who want the non-conformant demo backend
43
- # can override SKI_INFERENCE_BACKEND=anthropic and supply the key out of
44
- # band, with the caveat that doing so disqualifies the deployment.
41
+ # Sovereign-by-default: no cloud API key anywhere. Ollama is the
42
+ # inference backend; the runtime has no cloud backend. SKI_V3_LLM_BACKEND is
43
+ # the variable the runtime reads (v3.2.0; SKI_INFERENCE_BACKEND is only an
44
+ # alias). Ollama >= 0.5 is required for the JSON-schema `format` the runtime
45
+ # sends; keep the tag in step with reference-implementation/docker-compose.yml.
45
46
 
46
47
  DOCKER_COMPOSE_TEMPLATE = """version: '3.8'
47
48
 
48
49
  services:
49
50
  ollama:
50
- image: ollama/ollama:0.3.12
51
+ image: ollama/ollama:0.40.5@sha256:5d3a2f25f9825628daba94dc0b1d421c7d57d6c03ae5faa9ba050e058bab8241
51
52
  container_name: ollama-{{ name }}
52
53
  volumes:
53
54
  - ollama-models:/root/.ollama
@@ -61,7 +62,7 @@ services:
61
62
  ports:
62
63
  - "8000:8000"
63
64
  environment:
64
- SKI_INFERENCE_BACKEND: {{ ski_model.backend }}
65
+ SKI_V3_LLM_BACKEND: {{ ski_model.backend }}
65
66
  SKI_MODEL_NAME: {{ ski_model.model }}
66
67
  SKI_MODEL_TEMPERATURE: "{{ ski_model.temperature }}"
67
68
  SKI_MODEL_SEED: "{{ ski_model.seed }}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ski-model-deploy
3
- Version: 3.1.0a2
3
+ Version: 3.2.0
4
4
  Summary: Deploy and verify signed Knowledge Graphs against a SKI Model runtime.
5
5
  Author-email: KpiFinity <hello@kpifinity.com>
6
6
  License-Expression: Apache-2.0
@@ -15,16 +15,19 @@ Classifier: Programming Language :: Python :: 3
15
15
  Classifier: Programming Language :: Python :: 3.10
16
16
  Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
18
20
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
21
  Requires-Python: >=3.10
20
22
  Description-Content-Type: text/markdown
23
+ Requires-Dist: ski-schemas<4,>=3.2.0
21
24
  Requires-Dist: click<9,>=8.1.7
22
25
  Requires-Dist: pydantic<3,>=2.13.4
23
26
  Requires-Dist: python-dotenv<2,>=1.2.2
24
27
  Requires-Dist: pyyaml<7,>=6.0.1
25
28
  Requires-Dist: jinja2<4,>=3.1.6
26
29
  Requires-Dist: httpx<0.29,>=0.27
27
- Requires-Dist: cryptography<48,>=46
30
+ Requires-Dist: cryptography<52,>=46
28
31
  Requires-Dist: docker<8,>=7
29
32
  Requires-Dist: psycopg[binary]<4,>=3.1.18
30
33
 
@@ -1,9 +1,10 @@
1
+ ski-schemas<4,>=3.2.0
1
2
  click<9,>=8.1.7
2
3
  pydantic<3,>=2.13.4
3
4
  python-dotenv<2,>=1.2.2
4
5
  pyyaml<7,>=6.0.1
5
6
  jinja2<4,>=3.1.6
6
7
  httpx<0.29,>=0.27
7
- cryptography<48,>=46
8
+ cryptography<52,>=46
8
9
  docker<8,>=7
9
10
  psycopg[binary]<4,>=3.1.18
@@ -0,0 +1,183 @@
1
+ """Tests for the ski-model-deploy deployer."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Tuple
6
+
7
+ import pytest
8
+ from cryptography.hazmat.primitives import serialization
9
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
10
+ from ski_model_deploy import Deployer
11
+ from ski_model_deploy.deployer import UnsignedKGError
12
+ from ski_model_deploy.models import DeploymentConfig, DeploymentMode
13
+
14
+
15
+ class TestDeployer:
16
+ def test_initialize_deployment(self) -> None:
17
+ deployer = Deployer()
18
+ config = deployer.initialize(name="test-system", sector="energy", mode="docker")
19
+ assert config.name == "test-system"
20
+ assert config.sector == "energy"
21
+ assert config.mode == DeploymentMode.DOCKER
22
+
23
+ def test_deployer_config_persistence(self) -> None:
24
+ deployer = Deployer()
25
+ deployer.initialize(name="test-system", sector="finance")
26
+ assert deployer.config is not None
27
+ assert deployer.config.name == "test-system"
28
+
29
+
30
+ class TestDeploymentConfig:
31
+ def test_config_defaults_are_sovereign(self) -> None:
32
+ config = DeploymentConfig(name="test", sector="energy")
33
+ # Sovereign-by-default: local Ollama backend, temperature=0, mandatory TLS + API key.
34
+ assert config.ski_model.backend == "ollama"
35
+ assert config.ski_model.temperature == 0.0
36
+ assert config.ski_model.model == "qwen2.5:7b-instruct"
37
+ assert config.security.tls_enabled is True
38
+ assert config.security.api_key_required is True
39
+ assert config.monitoring.enabled is True
40
+ assert config.ledger.backend == "postgresql"
41
+
42
+ def test_config_sidecar_defaults_to_file_source(self) -> None:
43
+ config = DeploymentConfig(name="test", sector="energy")
44
+ assert config.sidecar.enabled is True
45
+ # Default telemetry source is "file" — Kafka is opt-in via compose profile.
46
+ assert config.sidecar.telemetry_source == "file"
47
+
48
+
49
+ def _write_signed_kg(tmp_path: Path, key: Ed25519PrivateKey) -> Tuple[Path, str]:
50
+ metadata = {"version": "deploy-test-1"}
51
+ rules = [{"id": "r1", "metric": "so2_ppm", "predicate": "must_not_exceed", "value": 100}]
52
+ tags = {"emissions": "r1"}
53
+ canonical = json.dumps(
54
+ {"metadata": metadata, "rules": rules, "tag_registry": tags},
55
+ sort_keys=True,
56
+ separators=(",", ":"),
57
+ ensure_ascii=False,
58
+ ).encode("utf-8")
59
+ pem = (
60
+ key.public_key()
61
+ .public_bytes(serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo)
62
+ .decode("ascii")
63
+ )
64
+ kg_path = tmp_path / "kg.json"
65
+ kg_path.write_text(
66
+ json.dumps(
67
+ {
68
+ "metadata": metadata,
69
+ "rules": rules,
70
+ "tag_registry": tags,
71
+ "signature": {
72
+ "algorithm": "ed25519",
73
+ "public_key_pem": pem,
74
+ "value_hex": key.sign(canonical).hex(),
75
+ },
76
+ }
77
+ )
78
+ )
79
+ return kg_path, pem
80
+
81
+
82
+ class TestLoadKnowledgeGraphTrustAnchors:
83
+ """Framework v3.2.0: the deployer verifies against trust anchors like the runtime."""
84
+
85
+ @pytest.fixture(autouse=True)
86
+ def _no_env_trust(self, monkeypatch: pytest.MonkeyPatch) -> None:
87
+ monkeypatch.delenv("SKI_KG_TRUST_ANCHORS", raising=False)
88
+ monkeypatch.delenv("SKI_KG_ALLOW_UNANCHORED", raising=False)
89
+
90
+ def test_anchored_kg_is_registered_as_verified(self, tmp_path: Path) -> None:
91
+ key = Ed25519PrivateKey.generate()
92
+ kg_path, pem = _write_signed_kg(tmp_path, key)
93
+ anchors = tmp_path / "anchors.pem"
94
+ anchors.write_text(pem)
95
+ deployer = Deployer()
96
+ deployer.initialize(name="t", sector="energy")
97
+ assert deployer.load_knowledge_graph(str(kg_path), trust_anchors=str(anchors))
98
+ assert deployer.config is not None and deployer.config.knowledge_graph is not None
99
+ assert deployer.config.knowledge_graph.signature_verified is True
100
+
101
+ def test_kg_signed_by_non_anchor_is_refused(self, tmp_path: Path) -> None:
102
+ kg_path, _ = _write_signed_kg(tmp_path, Ed25519PrivateKey.generate())
103
+ other = Ed25519PrivateKey.generate().public_key()
104
+ anchors = tmp_path / "anchors.pem"
105
+ anchors.write_bytes(
106
+ other.public_bytes(serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo)
107
+ )
108
+ deployer = Deployer()
109
+ deployer.initialize(name="t", sector="energy")
110
+ with pytest.raises(UnsignedKGError, match="not a configured trust anchor"):
111
+ deployer.load_knowledge_graph(str(kg_path), trust_anchors=str(anchors))
112
+
113
+ def test_no_anchors_configured_is_refused(self, tmp_path: Path) -> None:
114
+ kg_path, _ = _write_signed_kg(tmp_path, Ed25519PrivateKey.generate())
115
+ deployer = Deployer()
116
+ deployer.initialize(name="t", sector="energy")
117
+ with pytest.raises(UnsignedKGError, match="No KG trust anchors configured"):
118
+ deployer.load_knowledge_graph(str(kg_path))
119
+
120
+
121
+ class TestSignKnowledgeGraph:
122
+ """`ski-model-deploy sign` produces a KG the runtime and `load-kg` accept."""
123
+
124
+ @pytest.fixture(autouse=True)
125
+ def _no_env_trust(self, monkeypatch: pytest.MonkeyPatch) -> None:
126
+ monkeypatch.delenv("SKI_KG_TRUST_ANCHORS", raising=False)
127
+ monkeypatch.delenv("SKI_KG_ALLOW_UNANCHORED", raising=False)
128
+
129
+ def test_signed_demo_graph_round_trips_through_load_kg(self, tmp_path: Path) -> None:
130
+ from click.testing import CliRunner
131
+ from ski_model_deploy.cli import main
132
+
133
+ key = Ed25519PrivateKey.generate()
134
+ key_path = tmp_path / "kg.key"
135
+ key_path.write_bytes(
136
+ key.private_bytes(
137
+ serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption()
138
+ )
139
+ )
140
+ anchors = tmp_path / "anchors.pem"
141
+ anchors.write_bytes(
142
+ key.public_key().public_bytes(
143
+ serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo
144
+ )
145
+ )
146
+ demo = Path(__file__).resolve().parents[3] / "examples/energy/knowledge-graphs/kg-energy-v3-demo.json"
147
+ signed = tmp_path / "signed.json"
148
+ config = tmp_path / "deployment.yaml"
149
+ runner = CliRunner()
150
+
151
+ result = runner.invoke(main, ["sign", "--kg", str(demo), "--key", str(key_path), "-o", str(signed)])
152
+ assert result.exit_code == 0, result.output
153
+ assert runner.invoke(main, ["init", "-n", "t", "-s", "energy", "-o", str(config)]).exit_code == 0
154
+ result = runner.invoke(
155
+ main, ["load-kg", "-c", str(config), "-k", str(signed), "--trust-anchors", str(anchors)]
156
+ )
157
+ assert result.exit_code == 0, result.output
158
+ assert "signature verified" in result.output
159
+
160
+ def test_sign_refuses_a_non_ed25519_key(self, tmp_path: Path) -> None:
161
+ from cryptography.hazmat.primitives.asymmetric import ec
162
+ from ski_model_deploy.deployer import sign_kg
163
+
164
+ other = ec.generate_private_key(ec.SECP256R1()).private_bytes(
165
+ serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption()
166
+ )
167
+ with pytest.raises(ValueError, match="Ed25519"):
168
+ sign_kg({"metadata": {}, "nodes": {}, "edges": []}, other)
169
+
170
+ def test_deployer_finds_the_runtime_loader_without_help(self) -> None:
171
+ # The deployer resolves reference-implementation/src itself (the CLI
172
+ # has no pytest path setup); a wrong path left KnowledgeGraph = None.
173
+ import subprocess
174
+ import sys
175
+
176
+ code = (
177
+ "import ski_model_deploy.deployer as d; assert d.KnowledgeGraph is not None and d.signing_bytes"
178
+ )
179
+ subprocess.run([sys.executable, "-I", "-c", code], check=True)
180
+
181
+
182
+ if __name__ == "__main__": # pragma: no cover
183
+ pytest.main([__file__])
@@ -1,43 +0,0 @@
1
- """Tests for the ski-model-deploy deployer."""
2
-
3
- import pytest
4
- from ski_model_deploy import Deployer
5
- from ski_model_deploy.models import DeploymentConfig, DeploymentMode
6
-
7
-
8
- class TestDeployer:
9
- def test_initialize_deployment(self) -> None:
10
- deployer = Deployer()
11
- config = deployer.initialize(name="test-system", sector="energy", mode="docker")
12
- assert config.name == "test-system"
13
- assert config.sector == "energy"
14
- assert config.mode == DeploymentMode.DOCKER
15
-
16
- def test_deployer_config_persistence(self) -> None:
17
- deployer = Deployer()
18
- deployer.initialize(name="test-system", sector="finance")
19
- assert deployer.config is not None
20
- assert deployer.config.name == "test-system"
21
-
22
-
23
- class TestDeploymentConfig:
24
- def test_config_defaults_are_sovereign(self) -> None:
25
- config = DeploymentConfig(name="test", sector="energy")
26
- # Sovereign-by-default: local Ollama backend, temperature=0, mandatory TLS + API key.
27
- assert config.ski_model.backend == "ollama"
28
- assert config.ski_model.temperature == 0.0
29
- assert config.ski_model.model == "qwen2.5:7b-instruct"
30
- assert config.security.tls_enabled is True
31
- assert config.security.api_key_required is True
32
- assert config.monitoring.enabled is True
33
- assert config.ledger.backend == "postgresql"
34
-
35
- def test_config_sidecar_defaults_to_file_source(self) -> None:
36
- config = DeploymentConfig(name="test", sector="energy")
37
- assert config.sidecar.enabled is True
38
- # Default telemetry source is "file" — Kafka is opt-in via compose profile.
39
- assert config.sidecar.telemetry_source == "file"
40
-
41
-
42
- if __name__ == "__main__": # pragma: no cover
43
- pytest.main([__file__])