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.
- {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/PKG-INFO +5 -2
- {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/pyproject.toml +5 -2
- {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/src/ski_model_deploy/__init__.py +1 -1
- {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/src/ski_model_deploy/cli.py +31 -3
- {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/src/ski_model_deploy/deployer.py +53 -6
- {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/src/ski_model_deploy/utils.py +8 -7
- {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/src/ski_model_deploy.egg-info/PKG-INFO +5 -2
- {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/src/ski_model_deploy.egg-info/requires.txt +2 -1
- ski_model_deploy-3.2.0/tests/test_deployer.py +183 -0
- ski_model_deploy-3.1.0a2/tests/test_deployer.py +0 -43
- {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/README.md +0 -0
- {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/setup.cfg +0 -0
- {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/src/ski_model_deploy/models.py +0 -0
- {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/src/ski_model_deploy/py.typed +0 -0
- {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/src/ski_model_deploy.egg-info/SOURCES.txt +0 -0
- {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/src/ski_model_deploy.egg-info/dependency_links.txt +0 -0
- {ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/src/ski_model_deploy.egg-info/entry_points.txt +0 -0
- {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.
|
|
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<
|
|
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.
|
|
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,<
|
|
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.
|
|
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
|
-
|
|
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[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
42
|
-
# inference backend
|
|
43
|
-
#
|
|
44
|
-
#
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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<
|
|
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
|
|
|
@@ -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__])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/src/ski_model_deploy.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/src/ski_model_deploy.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{ski_model_deploy-3.1.0a2 → ski_model_deploy-3.2.0}/src/ski_model_deploy.egg-info/top_level.txt
RENAMED
|
File without changes
|