kyvvu-engine 0.3.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,58 @@
1
+ # SPDX-License-Identifier: BUSL-1.1
2
+ # Copyright 2026 Kyvvu B.V.
3
+
4
+ """kyvvu-engine — standalone policy evaluation engine for AI governance.
5
+
6
+ Public API
7
+ ----------
8
+ Core evaluation (zero-I/O)::
9
+
10
+ from kyvvu_engine import PolicyEngine
11
+
12
+ I/O wrapper (policy fetch + batch logging)::
13
+
14
+ from kyvvu_engine import KyvvuRunner, KyvvuSettings
15
+
16
+ Exceptions::
17
+
18
+ from kyvvu_engine import KyvvuBlockedError, KyvvuConfigError
19
+
20
+ Everything else (:mod:`~kyvvu_engine.schemas`, :mod:`~kyvvu_engine.rules`,
21
+ etc.) is importable for advanced use but not part of the stable public interface.
22
+
23
+ Note
24
+ ----
25
+ Template mapping (framework events → v0.05 behaviors) has moved to the
26
+ ``kyvvu`` SDK package::
27
+
28
+ from kyvvu.templates import load_builtin, load_template, BehaviorTemplate
29
+ """
30
+
31
+ from kyvvu_engine.__version__ import __version__
32
+ from kyvvu_engine.aggregation import (
33
+ aggregate_max,
34
+ aggregate_mean,
35
+ aggregate_weighted_sum,
36
+ )
37
+ from kyvvu_engine.engine import PolicyEngine
38
+ from kyvvu_engine.io.exceptions import KyvvuBlockedError, KyvvuConfigError
39
+ from kyvvu_engine.io.runner import KyvvuRunner
40
+ from kyvvu_engine.io.settings import KyvvuSettings
41
+ from kyvvu_engine.logging import setup_logging
42
+ from kyvvu_engine.rules._registry import PolicyRule
43
+ from kyvvu_engine.schemas import PolicyStatus
44
+
45
+ __all__ = [
46
+ "PolicyEngine",
47
+ "KyvvuRunner",
48
+ "KyvvuSettings",
49
+ "KyvvuBlockedError",
50
+ "KyvvuConfigError",
51
+ "PolicyRule",
52
+ "PolicyStatus",
53
+ "aggregate_max",
54
+ "aggregate_mean",
55
+ "aggregate_weighted_sum",
56
+ "setup_logging",
57
+ "__version__",
58
+ ]
@@ -0,0 +1,4 @@
1
+ # SPDX-License-Identifier: BUSL-1.1
2
+ # Copyright 2026 Kyvvu B.V.
3
+
4
+ __version__ = "0.3.0"
kyvvu_engine/_cli.py ADDED
@@ -0,0 +1,58 @@
1
+ # SPDX-License-Identifier: BUSL-1.1
2
+ # Copyright 2026 Kyvvu B.V.
3
+
4
+ """Entry point for the ``kyvvu-serve`` CLI.
5
+
6
+ Starts a local HTTP server wrapping :class:`~kyvvu_engine.engine.PolicyEngine`
7
+ so non-Python harnesses can evaluate policies via REST.
8
+
9
+ Requires the ``[serve]`` extra::
10
+
11
+ pip install "kyvvu-engine[serve]"
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import sys
17
+
18
+
19
+ def main() -> None:
20
+ """Parse CLI args and start the kyvvu-serve uvicorn server.
21
+
22
+ Reads configuration from CLI flags (``--agent-key``, ``--api-url``,
23
+ ``--api-key``) and falls back to ``KV_*`` environment variables for
24
+ unset values. Requires the ``[serve]`` extra for uvicorn/fastapi.
25
+ """
26
+ parser = argparse.ArgumentParser(
27
+ prog="kyvvu-serve",
28
+ description="Local policy evaluation server for kyvvu-engine",
29
+ )
30
+ parser.add_argument("--host", default="127.0.0.1", help="Bind address (default: 127.0.0.1)")
31
+ parser.add_argument("--port", type=int, default=8090, help="Bind port (default: 8090)")
32
+ parser.add_argument("--agent-key", default=None, help="Agent key for policy fetch")
33
+ parser.add_argument("--api-url", default=None, help="Kyvvu platform API URL")
34
+ parser.add_argument("--api-key", default=None, help="Bearer API key")
35
+ args = parser.parse_args()
36
+
37
+ try:
38
+ import uvicorn
39
+ except ImportError:
40
+ print(
41
+ "kyvvu-serve requires the [serve] extra.\n"
42
+ 'Install with: pip install "kyvvu-engine[serve]"',
43
+ file=sys.stderr,
44
+ )
45
+ sys.exit(1)
46
+
47
+ from kyvvu_engine.serve.app import create_app
48
+
49
+ app = create_app(
50
+ agent_key=args.agent_key,
51
+ api_url=args.api_url,
52
+ api_key=args.api_key,
53
+ )
54
+ uvicorn.run(app, host=args.host, port=args.port)
55
+
56
+
57
+ if __name__ == "__main__":
58
+ main()
@@ -0,0 +1,103 @@
1
+ # SPDX-License-Identifier: BUSL-1.1
2
+ # Copyright 2026 Kyvvu B.V.
3
+
4
+ """Score aggregation strategies for kyvvu-engine.
5
+
6
+ Given a list of per-policy outcomes, these functions compute an aggregate
7
+ risk score and derive the recommended :class:`~kyvvu_engine.schemas.Action`.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+
14
+ from kyvvu_engine.schemas import Action, EvalResult, PolicyResult
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Normalised weight per severity level. ``critical`` maps to the maximum
19
+ # score of 1.0 so that a single critical violation always triggers ``block``.
20
+ SEVERITY_WEIGHTS: dict[str, float] = {
21
+ "low": 0.25,
22
+ "medium": 0.50,
23
+ "high": 0.75,
24
+ "critical": 1.0,
25
+ }
26
+
27
+
28
+ def _score_to_action(score: float) -> Action:
29
+ """Map a normalised risk score to a recommended :class:`Action`.
30
+
31
+ Args:
32
+ score: Risk score in ``[0.0, 1.0]``.
33
+
34
+ Returns:
35
+ :attr:`Action.allow` when score is ``0.0``,
36
+ :attr:`Action.block` when score is ``>= 1.0``,
37
+ :attr:`Action.warn` for any value in between.
38
+ """
39
+ if score == 0.0:
40
+ return Action.allow
41
+ if score >= 1.0:
42
+ return Action.block
43
+ return Action.warn
44
+
45
+
46
+ def aggregate_max(results: list[PolicyResult]) -> EvalResult:
47
+ """Aggregate by taking the maximum severity among all violations.
48
+
49
+ Action thresholds:
50
+ - ``risk_score == 0.0`` → :attr:`Action.allow`
51
+ - ``0.0 < risk_score < 1.0`` → :attr:`Action.warn`
52
+ - ``risk_score == 1.0`` → :attr:`Action.block`
53
+
54
+ Args:
55
+ results: Per-policy evaluation outcomes.
56
+
57
+ Returns:
58
+ :class:`EvalResult` with ``risk_score`` and ``action`` from worst violation.
59
+ """
60
+ violations = [r for r in results if r.violated]
61
+ if not violations:
62
+ return EvalResult(risk_score=0.0, action=Action.allow, policies=results)
63
+ max_score = max(SEVERITY_WEIGHTS.get(v.severity, 0.0) for v in violations)
64
+ return EvalResult(risk_score=max_score, action=_score_to_action(max_score), policies=results)
65
+
66
+
67
+ def aggregate_mean(results: list[PolicyResult]) -> EvalResult:
68
+ """Aggregate by computing the mean severity across all violations.
69
+
70
+ Args:
71
+ results: Per-policy evaluation outcomes.
72
+
73
+ Returns:
74
+ :class:`EvalResult` with mean risk score.
75
+ """
76
+ violations = [r for r in results if r.violated]
77
+ if not violations:
78
+ return EvalResult(risk_score=0.0, action=Action.allow, policies=results)
79
+ mean_score = sum(SEVERITY_WEIGHTS.get(v.severity, 0.0) for v in violations) / len(violations)
80
+ return EvalResult(risk_score=mean_score, action=_score_to_action(mean_score), policies=results)
81
+
82
+
83
+ def aggregate_weighted_sum(
84
+ results: list[PolicyResult], weights: dict[int | None, float]
85
+ ) -> EvalResult:
86
+ """Aggregate using caller-supplied per-policy weights.
87
+
88
+ Args:
89
+ results: Per-policy evaluation outcomes.
90
+ weights: Dict mapping policy_id to a numeric weight.
91
+
92
+ Returns:
93
+ :class:`EvalResult` with weighted sum capped at 1.0.
94
+ """
95
+ violations = [r for r in results if r.violated]
96
+ if not violations:
97
+ return EvalResult(risk_score=0.0, action=Action.allow, policies=results)
98
+ total: float = sum(
99
+ weights.get(v.policy_id, SEVERITY_WEIGHTS.get(v.severity, 0.0))
100
+ for v in violations
101
+ )
102
+ score = min(total, 1.0)
103
+ return EvalResult(risk_score=score, action=_score_to_action(score), policies=results)