krons 0.1.1__py3-none-any.whl → 0.2.1__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.
- krons/__init__.py +49 -0
- krons/agent/__init__.py +144 -0
- krons/agent/mcps/__init__.py +14 -0
- krons/agent/mcps/loader.py +287 -0
- krons/agent/mcps/wrapper.py +799 -0
- krons/agent/message/__init__.py +20 -0
- krons/agent/message/action.py +69 -0
- krons/agent/message/assistant.py +52 -0
- krons/agent/message/common.py +49 -0
- krons/agent/message/instruction.py +130 -0
- krons/agent/message/prepare_msg.py +187 -0
- krons/agent/message/role.py +53 -0
- krons/agent/message/system.py +53 -0
- krons/agent/operations/__init__.py +82 -0
- krons/agent/operations/act.py +100 -0
- krons/agent/operations/generate.py +145 -0
- krons/agent/operations/llm_reparse.py +89 -0
- krons/agent/operations/operate.py +247 -0
- krons/agent/operations/parse.py +243 -0
- krons/agent/operations/react.py +286 -0
- krons/agent/operations/specs.py +235 -0
- krons/agent/operations/structure.py +151 -0
- krons/agent/operations/utils.py +79 -0
- krons/agent/providers/__init__.py +17 -0
- krons/agent/providers/anthropic_messages.py +146 -0
- krons/agent/providers/claude_code.py +276 -0
- krons/agent/providers/gemini.py +268 -0
- krons/agent/providers/match.py +75 -0
- krons/agent/providers/oai_chat.py +174 -0
- krons/agent/third_party/__init__.py +2 -0
- krons/agent/third_party/anthropic_models.py +154 -0
- krons/agent/third_party/claude_code.py +682 -0
- krons/agent/third_party/gemini_models.py +508 -0
- krons/agent/third_party/openai_models.py +295 -0
- krons/agent/tool.py +291 -0
- krons/core/__init__.py +56 -74
- krons/core/base/__init__.py +121 -0
- krons/core/{broadcaster.py → base/broadcaster.py} +7 -3
- krons/core/{element.py → base/element.py} +13 -5
- krons/core/{event.py → base/event.py} +39 -6
- krons/core/{eventbus.py → base/eventbus.py} +3 -1
- krons/core/{flow.py → base/flow.py} +11 -4
- krons/core/{graph.py → base/graph.py} +24 -8
- krons/core/{node.py → base/node.py} +44 -19
- krons/core/{pile.py → base/pile.py} +22 -8
- krons/core/{processor.py → base/processor.py} +21 -7
- krons/core/{progression.py → base/progression.py} +3 -1
- krons/{specs → core/specs}/__init__.py +0 -5
- krons/{specs → core/specs}/adapters/dataclass_field.py +16 -8
- krons/{specs → core/specs}/adapters/pydantic_adapter.py +11 -5
- krons/{specs → core/specs}/adapters/sql_ddl.py +14 -8
- krons/{specs → core/specs}/catalog/__init__.py +2 -2
- krons/{specs → core/specs}/catalog/_audit.py +2 -2
- krons/{specs → core/specs}/catalog/_common.py +2 -2
- krons/{specs → core/specs}/catalog/_content.py +4 -4
- krons/{specs → core/specs}/catalog/_enforcement.py +3 -3
- krons/{specs → core/specs}/factory.py +5 -5
- krons/{specs → core/specs}/operable.py +8 -2
- krons/{specs → core/specs}/protocol.py +4 -2
- krons/{specs → core/specs}/spec.py +23 -11
- krons/{types → core/types}/base.py +4 -2
- krons/{types → core/types}/db_types.py +2 -2
- krons/errors.py +13 -13
- krons/protocols.py +9 -4
- krons/resource/__init__.py +89 -0
- krons/{services → resource}/backend.py +48 -22
- krons/{services → resource}/endpoint.py +28 -14
- krons/{services → resource}/hook.py +20 -7
- krons/{services → resource}/imodel.py +46 -28
- krons/{services → resource}/registry.py +26 -24
- krons/{services → resource}/utilities/rate_limited_executor.py +7 -3
- krons/{services → resource}/utilities/rate_limiter.py +3 -1
- krons/{services → resource}/utilities/resilience.py +15 -5
- krons/resource/utilities/token_calculator.py +185 -0
- krons/session/__init__.py +12 -17
- krons/session/constraints.py +70 -0
- krons/session/exchange.py +11 -3
- krons/session/message.py +3 -1
- krons/session/registry.py +35 -0
- krons/session/session.py +165 -174
- krons/utils/__init__.py +45 -0
- krons/utils/_function_arg_parser.py +99 -0
- krons/utils/_pythonic_function_call.py +249 -0
- krons/utils/_to_list.py +9 -3
- krons/utils/_utils.py +6 -2
- krons/utils/concurrency/_async_call.py +4 -2
- krons/utils/concurrency/_errors.py +3 -1
- krons/utils/concurrency/_patterns.py +3 -1
- krons/utils/concurrency/_resource_tracker.py +6 -2
- krons/utils/display.py +257 -0
- krons/utils/fuzzy/__init__.py +6 -1
- krons/utils/fuzzy/_fuzzy_match.py +14 -8
- krons/utils/fuzzy/_string_similarity.py +3 -1
- krons/utils/fuzzy/_to_dict.py +3 -1
- krons/utils/schemas/__init__.py +26 -0
- krons/utils/schemas/_breakdown_pydantic_annotation.py +131 -0
- krons/utils/schemas/_formatter.py +72 -0
- krons/utils/schemas/_minimal_yaml.py +151 -0
- krons/utils/schemas/_typescript.py +153 -0
- krons/utils/validators/__init__.py +3 -0
- krons/utils/validators/_validate_image_url.py +56 -0
- krons/work/__init__.py +115 -0
- krons/work/engine.py +333 -0
- krons/work/form.py +242 -0
- krons/{operations → work/operations}/__init__.py +7 -4
- krons/{operations → work/operations}/builder.py +1 -1
- krons/{enforcement → work/operations}/context.py +36 -5
- krons/{operations → work/operations}/flow.py +13 -5
- krons/{operations → work/operations}/node.py +45 -43
- krons/work/operations/registry.py +103 -0
- krons/work/report.py +268 -0
- krons/work/rules/__init__.py +47 -0
- krons/{enforcement → work/rules}/common/boolean.py +3 -1
- krons/{enforcement → work/rules}/common/choice.py +9 -3
- krons/{enforcement → work/rules}/common/number.py +3 -1
- krons/{enforcement → work/rules}/common/string.py +9 -3
- krons/{enforcement → work/rules}/rule.py +1 -1
- krons/{enforcement → work/rules}/validator.py +20 -5
- krons/work/worker.py +266 -0
- {krons-0.1.1.dist-info → krons-0.2.1.dist-info}/METADATA +15 -1
- krons-0.2.1.dist-info/RECORD +151 -0
- krons/enforcement/__init__.py +0 -57
- krons/enforcement/policy.py +0 -80
- krons/enforcement/service.py +0 -370
- krons/operations/registry.py +0 -92
- krons/services/__init__.py +0 -81
- krons/specs/phrase.py +0 -405
- krons-0.1.1.dist-info/RECORD +0 -101
- /krons/{specs → core/specs}/adapters/__init__.py +0 -0
- /krons/{specs → core/specs}/adapters/_utils.py +0 -0
- /krons/{specs → core/specs}/adapters/factory.py +0 -0
- /krons/{types → core/types}/__init__.py +0 -0
- /krons/{types → core/types}/_sentinel.py +0 -0
- /krons/{types → core/types}/identity.py +0 -0
- /krons/{services → resource}/utilities/__init__.py +0 -0
- /krons/{services → resource}/utilities/header_factory.py +0 -0
- /krons/{enforcement → work/rules}/common/__init__.py +0 -0
- /krons/{enforcement → work/rules}/common/mapping.py +0 -0
- /krons/{enforcement → work/rules}/common/model.py +0 -0
- /krons/{enforcement → work/rules}/registry.py +0 -0
- {krons-0.1.1.dist-info → krons-0.2.1.dist-info}/WHEEL +0 -0
- {krons-0.1.1.dist-info → krons-0.2.1.dist-info}/licenses/LICENSE +0 -0
krons/enforcement/__init__.py
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
-
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
|
|
4
|
-
"""Enforcement module - Validation and policy protocols.
|
|
5
|
-
|
|
6
|
-
Provides:
|
|
7
|
-
- Rule: Base validation rule with auto-correction support
|
|
8
|
-
- Validator: Validates data against Spec/Operable using rules
|
|
9
|
-
- RuleRegistry: Maps types to validation rules
|
|
10
|
-
- Policy protocols: Abstract contracts for policy evaluation
|
|
11
|
-
|
|
12
|
-
Mental model:
|
|
13
|
-
Rule = validation (is data valid?) - with optional auto-fix
|
|
14
|
-
Policy = external evaluation protocol (implementations in domain libs)
|
|
15
|
-
|
|
16
|
-
Usage:
|
|
17
|
-
from krons.enforcement import Rule, Validator, RuleRegistry
|
|
18
|
-
|
|
19
|
-
# Register rules
|
|
20
|
-
registry = RuleRegistry()
|
|
21
|
-
registry.register(str, StringRule(min_length=1))
|
|
22
|
-
|
|
23
|
-
# Validate
|
|
24
|
-
validator = Validator(registry=registry)
|
|
25
|
-
result = await validator.validate_spec(spec, value)
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
from .context import QueryFn, RequestContext
|
|
29
|
-
from .policy import EnforcementLevel, PolicyEngine, PolicyResolver, ResolvedPolicy
|
|
30
|
-
from .registry import RuleRegistry, get_default_registry
|
|
31
|
-
from .rule import Rule, RuleParams, RuleQualifier, ValidationError
|
|
32
|
-
from .service import ActionMeta, KronConfig, KronService, action, get_action_meta
|
|
33
|
-
from .validator import Validator
|
|
34
|
-
|
|
35
|
-
__all__ = (
|
|
36
|
-
# Rule system
|
|
37
|
-
"Rule",
|
|
38
|
-
"RuleParams",
|
|
39
|
-
"RuleQualifier",
|
|
40
|
-
"RuleRegistry",
|
|
41
|
-
"ValidationError",
|
|
42
|
-
"Validator",
|
|
43
|
-
"get_default_registry",
|
|
44
|
-
# Policy protocols
|
|
45
|
-
"EnforcementLevel",
|
|
46
|
-
"PolicyEngine",
|
|
47
|
-
"PolicyResolver",
|
|
48
|
-
"ResolvedPolicy",
|
|
49
|
-
# Service
|
|
50
|
-
"ActionMeta",
|
|
51
|
-
"KronConfig",
|
|
52
|
-
"KronService",
|
|
53
|
-
"QueryFn",
|
|
54
|
-
"RequestContext",
|
|
55
|
-
"action",
|
|
56
|
-
"get_action_meta",
|
|
57
|
-
)
|
krons/enforcement/policy.py
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
-
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
|
|
4
|
-
"""Policy protocols and types.
|
|
5
|
-
|
|
6
|
-
Defines contracts for policy resolution and evaluation.
|
|
7
|
-
Implementations provided by domain libs (e.g., canon-core).
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
from collections.abc import Sequence
|
|
13
|
-
from dataclasses import dataclass, field
|
|
14
|
-
from typing import Any, Protocol, runtime_checkable
|
|
15
|
-
|
|
16
|
-
from krons.enforcement.context import RequestContext
|
|
17
|
-
from krons.specs.catalog._enforcement import EnforcementLevel
|
|
18
|
-
from krons.types.base import DataClass
|
|
19
|
-
|
|
20
|
-
__all__ = (
|
|
21
|
-
"EnforcementLevel",
|
|
22
|
-
"PolicyEngine",
|
|
23
|
-
"PolicyResolver",
|
|
24
|
-
"ResolvedPolicy",
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
@dataclass(slots=True)
|
|
29
|
-
class ResolvedPolicy(DataClass):
|
|
30
|
-
"""A policy resolved for evaluation.
|
|
31
|
-
|
|
32
|
-
Returned by PolicyResolver.resolve(). Contains policy ID and
|
|
33
|
-
any resolution metadata needed by the engine.
|
|
34
|
-
"""
|
|
35
|
-
|
|
36
|
-
policy_id: str
|
|
37
|
-
enforcement: str = EnforcementLevel.HARD_MANDATORY.value
|
|
38
|
-
metadata: dict[str, Any] = field(default_factory=dict)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@runtime_checkable
|
|
42
|
-
class PolicyEngine(Protocol):
|
|
43
|
-
"""Abstract policy evaluation engine.
|
|
44
|
-
|
|
45
|
-
kron defines the contract. Implementations:
|
|
46
|
-
- canon-core: OPAEngine (Rego/Regorus evaluation)
|
|
47
|
-
- Testing: MockPolicyEngine
|
|
48
|
-
"""
|
|
49
|
-
|
|
50
|
-
async def evaluate(
|
|
51
|
-
self,
|
|
52
|
-
policy_id: str,
|
|
53
|
-
input_data: dict[str, Any],
|
|
54
|
-
**options: Any,
|
|
55
|
-
) -> Any:
|
|
56
|
-
"""Evaluate a single policy against input."""
|
|
57
|
-
...
|
|
58
|
-
|
|
59
|
-
async def evaluate_batch(
|
|
60
|
-
self,
|
|
61
|
-
policy_ids: Sequence[str],
|
|
62
|
-
input_data: dict[str, Any],
|
|
63
|
-
**options: Any,
|
|
64
|
-
) -> list[Any]:
|
|
65
|
-
"""Evaluate multiple policies."""
|
|
66
|
-
...
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
@runtime_checkable
|
|
70
|
-
class PolicyResolver(Protocol):
|
|
71
|
-
"""Resolves which policies apply to a given context.
|
|
72
|
-
|
|
73
|
-
kron defines the contract. Implementations:
|
|
74
|
-
- canon-core: CharteredResolver (charter-based resolution)
|
|
75
|
-
- Testing: MockPolicyResolver, StaticPolicyResolver
|
|
76
|
-
"""
|
|
77
|
-
|
|
78
|
-
def resolve(self, ctx: RequestContext) -> Sequence[ResolvedPolicy]:
|
|
79
|
-
"""Determine applicable policies for context."""
|
|
80
|
-
...
|
krons/enforcement/service.py
DELETED
|
@@ -1,370 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
-
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
|
|
4
|
-
"""KronService - typed action handlers with policy evaluation."""
|
|
5
|
-
|
|
6
|
-
from __future__ import annotations
|
|
7
|
-
|
|
8
|
-
import logging
|
|
9
|
-
from collections.abc import Awaitable, Callable
|
|
10
|
-
from dataclasses import dataclass
|
|
11
|
-
from typing import Any
|
|
12
|
-
|
|
13
|
-
from pydantic import Field, PrivateAttr
|
|
14
|
-
|
|
15
|
-
from krons.services import ServiceBackend, ServiceConfig
|
|
16
|
-
|
|
17
|
-
from .context import RequestContext
|
|
18
|
-
from .policy import EnforcementLevel, PolicyEngine, PolicyResolver
|
|
19
|
-
|
|
20
|
-
logger = logging.getLogger(__name__)
|
|
21
|
-
|
|
22
|
-
__all__ = (
|
|
23
|
-
"ActionMeta",
|
|
24
|
-
"KronConfig",
|
|
25
|
-
"KronService",
|
|
26
|
-
"action",
|
|
27
|
-
"get_action_meta",
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class KronConfig(ServiceConfig):
|
|
32
|
-
"""Configuration for KronService.
|
|
33
|
-
|
|
34
|
-
Attributes:
|
|
35
|
-
operable: Canonical Operable containing all field specs for this service.
|
|
36
|
-
action_timeout: Timeout for action execution (None = no timeout).
|
|
37
|
-
use_policies: Enable policy evaluation.
|
|
38
|
-
policy_timeout: Timeout for policy evaluation.
|
|
39
|
-
fail_open_on_engine_error: Allow action if engine fails (DANGEROUS).
|
|
40
|
-
hooks: Available hooks {name: callable}.
|
|
41
|
-
"""
|
|
42
|
-
|
|
43
|
-
operable: Any = None
|
|
44
|
-
"""Canonical Operable for the service's field namespace."""
|
|
45
|
-
|
|
46
|
-
action_timeout: float | None = None
|
|
47
|
-
"""Timeout for action execution in seconds. None means no timeout."""
|
|
48
|
-
|
|
49
|
-
use_policies: bool = True
|
|
50
|
-
"""Enable policy evaluation."""
|
|
51
|
-
|
|
52
|
-
policy_timeout: float = 10.0
|
|
53
|
-
"""Timeout for policy evaluation in seconds."""
|
|
54
|
-
|
|
55
|
-
fail_open_on_engine_error: bool = False
|
|
56
|
-
"""If True, allow action when engine fails. DANGEROUS for production."""
|
|
57
|
-
|
|
58
|
-
hooks: dict[str, Callable[..., Awaitable[Any]]] = Field(default_factory=dict)
|
|
59
|
-
"""Available hooks {name: hook_function}."""
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
_ACTION_ATTR = "_kron_action"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
@dataclass(frozen=True, slots=True)
|
|
66
|
-
class ActionMeta:
|
|
67
|
-
"""Metadata for an action handler.
|
|
68
|
-
|
|
69
|
-
Attributes:
|
|
70
|
-
name: Action identifier (e.g., "consent.grant").
|
|
71
|
-
inputs: Field names from service operable used as inputs.
|
|
72
|
-
outputs: Field names from service operable used as outputs.
|
|
73
|
-
pre_hooks: Hook names to run before action.
|
|
74
|
-
post_hooks: Hook names to run after action.
|
|
75
|
-
"""
|
|
76
|
-
|
|
77
|
-
name: str
|
|
78
|
-
inputs: frozenset[str] = frozenset()
|
|
79
|
-
outputs: frozenset[str] = frozenset()
|
|
80
|
-
pre_hooks: tuple[str, ...] = ()
|
|
81
|
-
post_hooks: tuple[str, ...] = ()
|
|
82
|
-
|
|
83
|
-
# Lazily computed types (set by service at registration)
|
|
84
|
-
_options_type: Any = None
|
|
85
|
-
_result_type: Any = None
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def action(
|
|
89
|
-
name: str,
|
|
90
|
-
inputs: set[str] | None = None,
|
|
91
|
-
outputs: set[str] | None = None,
|
|
92
|
-
pre_hooks: list[str] | None = None,
|
|
93
|
-
post_hooks: list[str] | None = None,
|
|
94
|
-
) -> Callable[[Callable], Callable]:
|
|
95
|
-
"""Decorator to declare action handler metadata.
|
|
96
|
-
|
|
97
|
-
Args:
|
|
98
|
-
name: Action identifier (e.g., "consent.grant").
|
|
99
|
-
inputs: Field names from service operable used as inputs.
|
|
100
|
-
outputs: Field names from service operable used as outputs.
|
|
101
|
-
pre_hooks: Hook names to run before action.
|
|
102
|
-
post_hooks: Hook names to run after action.
|
|
103
|
-
|
|
104
|
-
Usage:
|
|
105
|
-
@action(
|
|
106
|
-
name="consent.grant",
|
|
107
|
-
inputs={"permissions", "subject_id"},
|
|
108
|
-
outputs={"consent_id", "granted_at"},
|
|
109
|
-
)
|
|
110
|
-
async def _handle_grant(self, options, ctx):
|
|
111
|
-
...
|
|
112
|
-
"""
|
|
113
|
-
|
|
114
|
-
def decorator(func: Callable) -> Callable:
|
|
115
|
-
meta = ActionMeta(
|
|
116
|
-
name=name,
|
|
117
|
-
inputs=frozenset(inputs or set()),
|
|
118
|
-
outputs=frozenset(outputs or set()),
|
|
119
|
-
pre_hooks=tuple(pre_hooks or []),
|
|
120
|
-
post_hooks=tuple(post_hooks or []),
|
|
121
|
-
)
|
|
122
|
-
setattr(func, _ACTION_ATTR, meta)
|
|
123
|
-
return func
|
|
124
|
-
|
|
125
|
-
return decorator
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def get_action_meta(handler: Callable) -> ActionMeta | None:
|
|
129
|
-
"""Get action metadata from a handler method."""
|
|
130
|
-
return getattr(handler, _ACTION_ATTR, None)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
# =============================================================================
|
|
134
|
-
# KronService
|
|
135
|
-
# =============================================================================
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
class KronService(ServiceBackend):
|
|
139
|
-
"""Service backend with typed actions.
|
|
140
|
-
|
|
141
|
-
Subclasses implement action handlers with @action decorator.
|
|
142
|
-
Actions derive typed I/O from service's canonical operable.
|
|
143
|
-
|
|
144
|
-
Example:
|
|
145
|
-
class ConsentService(KronService):
|
|
146
|
-
config = KronConfig(
|
|
147
|
-
name="consent",
|
|
148
|
-
operable=Operable([
|
|
149
|
-
Spec("permissions", list[str]),
|
|
150
|
-
Spec("consent_id", UUID),
|
|
151
|
-
Spec("granted_at", datetime),
|
|
152
|
-
Spec("subject_id", FK[Subject]),
|
|
153
|
-
]),
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
@action(
|
|
157
|
-
name="consent.grant",
|
|
158
|
-
inputs={"permissions", "subject_id"},
|
|
159
|
-
outputs={"consent_id", "granted_at"},
|
|
160
|
-
)
|
|
161
|
-
async def _handle_grant(self, options, ctx):
|
|
162
|
-
...
|
|
163
|
-
|
|
164
|
-
service = ConsentService()
|
|
165
|
-
result = await service.call("consent.grant", options, ctx)
|
|
166
|
-
"""
|
|
167
|
-
|
|
168
|
-
config: KronConfig = Field(default_factory=KronConfig)
|
|
169
|
-
_policy_engine: PolicyEngine | None = PrivateAttr(default=None)
|
|
170
|
-
_policy_resolver: PolicyResolver | None = PrivateAttr(default=None)
|
|
171
|
-
_action_registry: dict[str, tuple[Callable, ActionMeta]] = PrivateAttr(default_factory=dict)
|
|
172
|
-
|
|
173
|
-
def __init__(
|
|
174
|
-
self,
|
|
175
|
-
config: KronConfig | None = None,
|
|
176
|
-
policy_engine: PolicyEngine | None = None,
|
|
177
|
-
policy_resolver: PolicyResolver | None = None,
|
|
178
|
-
**kwargs: Any,
|
|
179
|
-
) -> None:
|
|
180
|
-
"""Initialize service with optional policy engine and resolver.
|
|
181
|
-
|
|
182
|
-
Args:
|
|
183
|
-
config: Service configuration.
|
|
184
|
-
policy_engine: PolicyEngine for policy evaluation.
|
|
185
|
-
policy_resolver: PolicyResolver for determining applicable policies.
|
|
186
|
-
"""
|
|
187
|
-
super().__init__(config=config, **kwargs)
|
|
188
|
-
self._policy_engine = policy_engine
|
|
189
|
-
self._policy_resolver = policy_resolver
|
|
190
|
-
self._action_registry = {}
|
|
191
|
-
self._register_actions()
|
|
192
|
-
|
|
193
|
-
def _register_actions(self) -> None:
|
|
194
|
-
"""Scan for @action decorated methods and register them."""
|
|
195
|
-
for name in dir(self):
|
|
196
|
-
if name.startswith("_"):
|
|
197
|
-
method = getattr(self, name, None)
|
|
198
|
-
if method and callable(method):
|
|
199
|
-
meta = get_action_meta(method)
|
|
200
|
-
if meta:
|
|
201
|
-
self._action_registry[meta.name] = (method, meta)
|
|
202
|
-
self._build_action_types(meta)
|
|
203
|
-
|
|
204
|
-
def _build_action_types(self, meta: ActionMeta) -> None:
|
|
205
|
-
"""Build options_type and result_type for an action from service operable."""
|
|
206
|
-
if not self.config.operable:
|
|
207
|
-
return
|
|
208
|
-
|
|
209
|
-
operable = self.config.operable
|
|
210
|
-
|
|
211
|
-
# Validate inputs/outputs exist in operable
|
|
212
|
-
allowed = operable.allowed()
|
|
213
|
-
invalid_inputs = meta.inputs - allowed
|
|
214
|
-
invalid_outputs = meta.outputs - allowed
|
|
215
|
-
|
|
216
|
-
if invalid_inputs:
|
|
217
|
-
logger.warning(
|
|
218
|
-
"Action '%s' has inputs not in operable: %s",
|
|
219
|
-
meta.name,
|
|
220
|
-
invalid_inputs,
|
|
221
|
-
)
|
|
222
|
-
if invalid_outputs:
|
|
223
|
-
logger.warning(
|
|
224
|
-
"Action '%s' has outputs not in operable: %s",
|
|
225
|
-
meta.name,
|
|
226
|
-
invalid_outputs,
|
|
227
|
-
)
|
|
228
|
-
|
|
229
|
-
# Build typed structures (frozen dataclasses)
|
|
230
|
-
if meta.inputs:
|
|
231
|
-
options_type = operable.compose_structure(
|
|
232
|
-
_to_pascal(meta.name) + "Options",
|
|
233
|
-
include=set(meta.inputs),
|
|
234
|
-
frozen=True,
|
|
235
|
-
)
|
|
236
|
-
object.__setattr__(meta, "_options_type", options_type)
|
|
237
|
-
|
|
238
|
-
if meta.outputs:
|
|
239
|
-
result_type = operable.compose_structure(
|
|
240
|
-
_to_pascal(meta.name) + "Result",
|
|
241
|
-
include=set(meta.outputs),
|
|
242
|
-
frozen=True,
|
|
243
|
-
)
|
|
244
|
-
object.__setattr__(meta, "_result_type", result_type)
|
|
245
|
-
|
|
246
|
-
@property
|
|
247
|
-
def has_engine(self) -> bool:
|
|
248
|
-
"""True if policy engine is configured."""
|
|
249
|
-
return self._policy_engine is not None
|
|
250
|
-
|
|
251
|
-
@property
|
|
252
|
-
def has_resolver(self) -> bool:
|
|
253
|
-
"""True if policy resolver is configured."""
|
|
254
|
-
return self._policy_resolver is not None
|
|
255
|
-
|
|
256
|
-
async def call(
|
|
257
|
-
self,
|
|
258
|
-
name: str,
|
|
259
|
-
options: Any,
|
|
260
|
-
ctx: RequestContext,
|
|
261
|
-
) -> Any:
|
|
262
|
-
"""Call an action by name.
|
|
263
|
-
|
|
264
|
-
Args:
|
|
265
|
-
name: Action name (e.g., "consent.grant").
|
|
266
|
-
options: Input data (dict or typed dataclass).
|
|
267
|
-
ctx: Request context.
|
|
268
|
-
|
|
269
|
-
Returns:
|
|
270
|
-
Action result.
|
|
271
|
-
|
|
272
|
-
Raises:
|
|
273
|
-
ValueError: If action not found.
|
|
274
|
-
PermissionError: If policy blocks action.
|
|
275
|
-
"""
|
|
276
|
-
handler, meta = self._fetch_handler(name)
|
|
277
|
-
|
|
278
|
-
# Update context
|
|
279
|
-
ctx.name = name
|
|
280
|
-
|
|
281
|
-
# Run pre-hooks
|
|
282
|
-
await self._run_hooks(meta.pre_hooks, options, ctx)
|
|
283
|
-
|
|
284
|
-
# Evaluate policies
|
|
285
|
-
if self.config.use_policies and self._policy_engine:
|
|
286
|
-
await self._evaluate_policies(ctx)
|
|
287
|
-
|
|
288
|
-
# Validate options if we have typed options_type
|
|
289
|
-
if meta._options_type and self.config.operable:
|
|
290
|
-
options = self.config.operable.validate_instance(meta._options_type, options)
|
|
291
|
-
|
|
292
|
-
# Execute handler
|
|
293
|
-
result = await handler(options, ctx)
|
|
294
|
-
|
|
295
|
-
# Run post-hooks
|
|
296
|
-
await self._run_hooks(meta.post_hooks, options, ctx, result=result)
|
|
297
|
-
|
|
298
|
-
return result
|
|
299
|
-
|
|
300
|
-
def _fetch_handler(self, name: str) -> tuple[Callable, ActionMeta]:
|
|
301
|
-
"""Fetch handler and metadata by action name.
|
|
302
|
-
|
|
303
|
-
Args:
|
|
304
|
-
name: Action name.
|
|
305
|
-
|
|
306
|
-
Returns:
|
|
307
|
-
Tuple of (handler, ActionMeta).
|
|
308
|
-
|
|
309
|
-
Raises:
|
|
310
|
-
ValueError: If action not found.
|
|
311
|
-
"""
|
|
312
|
-
if name not in self._action_registry:
|
|
313
|
-
raise ValueError(f"Unknown action: {name}")
|
|
314
|
-
return self._action_registry[name]
|
|
315
|
-
|
|
316
|
-
async def _run_hooks(
|
|
317
|
-
self,
|
|
318
|
-
hook_names: tuple[str, ...],
|
|
319
|
-
options: Any,
|
|
320
|
-
ctx: RequestContext,
|
|
321
|
-
result: Any = None,
|
|
322
|
-
) -> None:
|
|
323
|
-
"""Run named hooks from config.hooks."""
|
|
324
|
-
for hook_name in hook_names:
|
|
325
|
-
hook_fn = self.config.hooks.get(hook_name)
|
|
326
|
-
if hook_fn:
|
|
327
|
-
try:
|
|
328
|
-
await hook_fn(self, options, ctx, result)
|
|
329
|
-
except Exception as e:
|
|
330
|
-
logger.error("Hook '%s' failed: %s", hook_name, e)
|
|
331
|
-
else:
|
|
332
|
-
logger.warning("Hook '%s' not found in config.hooks", hook_name)
|
|
333
|
-
|
|
334
|
-
async def _evaluate_policies(self, ctx: RequestContext) -> None:
|
|
335
|
-
"""Evaluate policies via engine."""
|
|
336
|
-
if not self._policy_engine or not self._policy_resolver:
|
|
337
|
-
return
|
|
338
|
-
|
|
339
|
-
try:
|
|
340
|
-
resolved = self._policy_resolver.resolve(ctx)
|
|
341
|
-
|
|
342
|
-
if not resolved:
|
|
343
|
-
return
|
|
344
|
-
|
|
345
|
-
policy_ids = [p.policy_id for p in resolved]
|
|
346
|
-
input_data = ctx.to_dict()
|
|
347
|
-
|
|
348
|
-
results = await self._policy_engine.evaluate_batch(policy_ids, input_data)
|
|
349
|
-
|
|
350
|
-
for result in results:
|
|
351
|
-
if EnforcementLevel.is_blocking(result):
|
|
352
|
-
raise PermissionError(f"Policy {result.policy_id} blocked: {result.message}")
|
|
353
|
-
|
|
354
|
-
except PermissionError:
|
|
355
|
-
raise
|
|
356
|
-
except Exception as e:
|
|
357
|
-
logger.error("Policy evaluation failed: %s", e)
|
|
358
|
-
if not self.config.fail_open_on_engine_error:
|
|
359
|
-
raise PermissionError(f"Policy engine error: {e}")
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
def _to_pascal(name: str) -> str:
|
|
363
|
-
"""Convert action name to PascalCase.
|
|
364
|
-
|
|
365
|
-
consent.grant -> ConsentGrant
|
|
366
|
-
consent_grant -> ConsentGrant
|
|
367
|
-
"""
|
|
368
|
-
# Replace dots and underscores, capitalize each part
|
|
369
|
-
parts = name.replace(".", "_").split("_")
|
|
370
|
-
return "".join(part.capitalize() for part in parts)
|
krons/operations/registry.py
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
-
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
|
|
4
|
-
"""Per-session operation factory registry.
|
|
5
|
-
|
|
6
|
-
Maps operation names to async factory functions. Instantiated per-Session
|
|
7
|
-
for isolation, testability, and per-session customization.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
from collections.abc import Awaitable, Callable
|
|
13
|
-
from typing import Any
|
|
14
|
-
|
|
15
|
-
__all__ = ("OperationRegistry",)
|
|
16
|
-
|
|
17
|
-
OperationFactory = Callable[..., Awaitable[Any]]
|
|
18
|
-
"""Factory signature: async (session, branch, parameters) -> result"""
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class OperationRegistry:
|
|
22
|
-
"""Map operation names to async factory functions.
|
|
23
|
-
|
|
24
|
-
Per-session registry (not global) for isolation and testability.
|
|
25
|
-
|
|
26
|
-
Example:
|
|
27
|
-
registry = OperationRegistry()
|
|
28
|
-
registry.register("chat", chat_factory)
|
|
29
|
-
factory = registry.get("chat")
|
|
30
|
-
result = await factory(session, branch, params)
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
def __init__(self):
|
|
34
|
-
"""Initialize empty registry."""
|
|
35
|
-
self._factories: dict[str, OperationFactory] = {}
|
|
36
|
-
|
|
37
|
-
def register(
|
|
38
|
-
self,
|
|
39
|
-
operation_name: str,
|
|
40
|
-
factory: OperationFactory,
|
|
41
|
-
*,
|
|
42
|
-
override: bool = False,
|
|
43
|
-
) -> None:
|
|
44
|
-
"""Register factory for operation name.
|
|
45
|
-
|
|
46
|
-
Args:
|
|
47
|
-
operation_name: Lookup key.
|
|
48
|
-
factory: Async (session, branch, params) -> result.
|
|
49
|
-
override: Allow replacing existing. Default False.
|
|
50
|
-
|
|
51
|
-
Raises:
|
|
52
|
-
ValueError: If name exists and override=False.
|
|
53
|
-
"""
|
|
54
|
-
if operation_name in self._factories and not override:
|
|
55
|
-
raise ValueError(
|
|
56
|
-
f"Operation '{operation_name}' already registered. Use override=True to replace."
|
|
57
|
-
)
|
|
58
|
-
self._factories[operation_name] = factory
|
|
59
|
-
|
|
60
|
-
def get(self, operation_name: str) -> OperationFactory:
|
|
61
|
-
"""Get factory by name. Raises KeyError with available names if not found."""
|
|
62
|
-
if operation_name not in self._factories:
|
|
63
|
-
raise KeyError(
|
|
64
|
-
f"Operation '{operation_name}' not registered. Available: {self.list_names()}"
|
|
65
|
-
)
|
|
66
|
-
return self._factories[operation_name]
|
|
67
|
-
|
|
68
|
-
def has(self, operation_name: str) -> bool:
|
|
69
|
-
"""Check if name is registered."""
|
|
70
|
-
return operation_name in self._factories
|
|
71
|
-
|
|
72
|
-
def unregister(self, operation_name: str) -> bool:
|
|
73
|
-
"""Remove registration. Returns True if existed."""
|
|
74
|
-
if operation_name in self._factories:
|
|
75
|
-
del self._factories[operation_name]
|
|
76
|
-
return True
|
|
77
|
-
return False
|
|
78
|
-
|
|
79
|
-
def list_names(self) -> list[str]:
|
|
80
|
-
"""Return all registered operation names."""
|
|
81
|
-
return list(self._factories.keys())
|
|
82
|
-
|
|
83
|
-
def __contains__(self, operation_name: str) -> bool:
|
|
84
|
-
"""Support 'name in registry' syntax."""
|
|
85
|
-
return operation_name in self._factories
|
|
86
|
-
|
|
87
|
-
def __len__(self) -> int:
|
|
88
|
-
"""Count of registered operations."""
|
|
89
|
-
return len(self._factories)
|
|
90
|
-
|
|
91
|
-
def __repr__(self) -> str:
|
|
92
|
-
return f"OperationRegistry(operations={self.list_names()})"
|
krons/services/__init__.py
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
-
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
|
|
4
|
-
"""Services module: iModel, ServiceBackend, hooks, and registry.
|
|
5
|
-
|
|
6
|
-
Core exports:
|
|
7
|
-
- iModel: Unified service interface with rate limiting and hooks
|
|
8
|
-
- ServiceBackend/Endpoint: Backend abstractions for API calls
|
|
9
|
-
- HookRegistry/HookEvent/HookPhase: Lifecycle hook system
|
|
10
|
-
- ServiceRegistry: O(1) name-based service lookup
|
|
11
|
-
|
|
12
|
-
Uses lazy loading for fast import.
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
from __future__ import annotations
|
|
16
|
-
|
|
17
|
-
from typing import TYPE_CHECKING
|
|
18
|
-
|
|
19
|
-
# Lazy import mapping
|
|
20
|
-
_LAZY_IMPORTS: dict[str, tuple[str, str]] = {
|
|
21
|
-
"Calling": ("krons.services.backend", "Calling"),
|
|
22
|
-
"NormalizedResponse": ("krons.services.backend", "NormalizedResponse"),
|
|
23
|
-
"ServiceBackend": ("krons.services.backend", "ServiceBackend"),
|
|
24
|
-
"ServiceConfig": ("krons.services.backend", "ServiceConfig"),
|
|
25
|
-
"ServiceRegistry": ("krons.services.registry", "ServiceRegistry"),
|
|
26
|
-
"iModel": ("krons.services.imodel", "iModel"),
|
|
27
|
-
"Endpoint": ("krons.services.endpoint", "Endpoint"),
|
|
28
|
-
"EndpointConfig": ("krons.services.endpoint", "EndpointConfig"),
|
|
29
|
-
"APICalling": ("krons.services.endpoint", "APICalling"),
|
|
30
|
-
"HookRegistry": ("krons.services.hook", "HookRegistry"),
|
|
31
|
-
"HookEvent": ("krons.services.hook", "HookEvent"),
|
|
32
|
-
"HookPhase": ("krons.services.hook", "HookPhase"),
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
_LOADED: dict[str, object] = {}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def __getattr__(name: str) -> object:
|
|
39
|
-
"""Lazy import attributes on first access."""
|
|
40
|
-
if name in _LOADED:
|
|
41
|
-
return _LOADED[name]
|
|
42
|
-
|
|
43
|
-
if name in _LAZY_IMPORTS:
|
|
44
|
-
from importlib import import_module
|
|
45
|
-
|
|
46
|
-
module_name, attr_name = _LAZY_IMPORTS[name]
|
|
47
|
-
module = import_module(module_name)
|
|
48
|
-
value = getattr(module, attr_name)
|
|
49
|
-
_LOADED[name] = value
|
|
50
|
-
return value
|
|
51
|
-
|
|
52
|
-
raise AttributeError(f"module 'krons.services' has no attribute {name!r}")
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def __dir__() -> list[str]:
|
|
56
|
-
"""Return all available attributes for autocomplete."""
|
|
57
|
-
return list(__all__)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
# TYPE_CHECKING block for static analysis
|
|
61
|
-
if TYPE_CHECKING:
|
|
62
|
-
from .backend import Calling, NormalizedResponse, ServiceBackend, ServiceConfig
|
|
63
|
-
from .endpoint import APICalling, Endpoint, EndpointConfig
|
|
64
|
-
from .hook import HookEvent, HookPhase, HookRegistry
|
|
65
|
-
from .imodel import iModel
|
|
66
|
-
from .registry import ServiceRegistry
|
|
67
|
-
|
|
68
|
-
__all__ = (
|
|
69
|
-
"APICalling",
|
|
70
|
-
"Calling",
|
|
71
|
-
"Endpoint",
|
|
72
|
-
"EndpointConfig",
|
|
73
|
-
"HookEvent",
|
|
74
|
-
"HookPhase",
|
|
75
|
-
"HookRegistry",
|
|
76
|
-
"NormalizedResponse",
|
|
77
|
-
"ServiceBackend",
|
|
78
|
-
"ServiceConfig",
|
|
79
|
-
"ServiceRegistry",
|
|
80
|
-
"iModel",
|
|
81
|
-
)
|