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.
Files changed (142) hide show
  1. krons/__init__.py +49 -0
  2. krons/agent/__init__.py +144 -0
  3. krons/agent/mcps/__init__.py +14 -0
  4. krons/agent/mcps/loader.py +287 -0
  5. krons/agent/mcps/wrapper.py +799 -0
  6. krons/agent/message/__init__.py +20 -0
  7. krons/agent/message/action.py +69 -0
  8. krons/agent/message/assistant.py +52 -0
  9. krons/agent/message/common.py +49 -0
  10. krons/agent/message/instruction.py +130 -0
  11. krons/agent/message/prepare_msg.py +187 -0
  12. krons/agent/message/role.py +53 -0
  13. krons/agent/message/system.py +53 -0
  14. krons/agent/operations/__init__.py +82 -0
  15. krons/agent/operations/act.py +100 -0
  16. krons/agent/operations/generate.py +145 -0
  17. krons/agent/operations/llm_reparse.py +89 -0
  18. krons/agent/operations/operate.py +247 -0
  19. krons/agent/operations/parse.py +243 -0
  20. krons/agent/operations/react.py +286 -0
  21. krons/agent/operations/specs.py +235 -0
  22. krons/agent/operations/structure.py +151 -0
  23. krons/agent/operations/utils.py +79 -0
  24. krons/agent/providers/__init__.py +17 -0
  25. krons/agent/providers/anthropic_messages.py +146 -0
  26. krons/agent/providers/claude_code.py +276 -0
  27. krons/agent/providers/gemini.py +268 -0
  28. krons/agent/providers/match.py +75 -0
  29. krons/agent/providers/oai_chat.py +174 -0
  30. krons/agent/third_party/__init__.py +2 -0
  31. krons/agent/third_party/anthropic_models.py +154 -0
  32. krons/agent/third_party/claude_code.py +682 -0
  33. krons/agent/third_party/gemini_models.py +508 -0
  34. krons/agent/third_party/openai_models.py +295 -0
  35. krons/agent/tool.py +291 -0
  36. krons/core/__init__.py +56 -74
  37. krons/core/base/__init__.py +121 -0
  38. krons/core/{broadcaster.py → base/broadcaster.py} +7 -3
  39. krons/core/{element.py → base/element.py} +13 -5
  40. krons/core/{event.py → base/event.py} +39 -6
  41. krons/core/{eventbus.py → base/eventbus.py} +3 -1
  42. krons/core/{flow.py → base/flow.py} +11 -4
  43. krons/core/{graph.py → base/graph.py} +24 -8
  44. krons/core/{node.py → base/node.py} +44 -19
  45. krons/core/{pile.py → base/pile.py} +22 -8
  46. krons/core/{processor.py → base/processor.py} +21 -7
  47. krons/core/{progression.py → base/progression.py} +3 -1
  48. krons/{specs → core/specs}/__init__.py +0 -5
  49. krons/{specs → core/specs}/adapters/dataclass_field.py +16 -8
  50. krons/{specs → core/specs}/adapters/pydantic_adapter.py +11 -5
  51. krons/{specs → core/specs}/adapters/sql_ddl.py +14 -8
  52. krons/{specs → core/specs}/catalog/__init__.py +2 -2
  53. krons/{specs → core/specs}/catalog/_audit.py +2 -2
  54. krons/{specs → core/specs}/catalog/_common.py +2 -2
  55. krons/{specs → core/specs}/catalog/_content.py +4 -4
  56. krons/{specs → core/specs}/catalog/_enforcement.py +3 -3
  57. krons/{specs → core/specs}/factory.py +5 -5
  58. krons/{specs → core/specs}/operable.py +8 -2
  59. krons/{specs → core/specs}/protocol.py +4 -2
  60. krons/{specs → core/specs}/spec.py +23 -11
  61. krons/{types → core/types}/base.py +4 -2
  62. krons/{types → core/types}/db_types.py +2 -2
  63. krons/errors.py +13 -13
  64. krons/protocols.py +9 -4
  65. krons/resource/__init__.py +89 -0
  66. krons/{services → resource}/backend.py +48 -22
  67. krons/{services → resource}/endpoint.py +28 -14
  68. krons/{services → resource}/hook.py +20 -7
  69. krons/{services → resource}/imodel.py +46 -28
  70. krons/{services → resource}/registry.py +26 -24
  71. krons/{services → resource}/utilities/rate_limited_executor.py +7 -3
  72. krons/{services → resource}/utilities/rate_limiter.py +3 -1
  73. krons/{services → resource}/utilities/resilience.py +15 -5
  74. krons/resource/utilities/token_calculator.py +185 -0
  75. krons/session/__init__.py +12 -17
  76. krons/session/constraints.py +70 -0
  77. krons/session/exchange.py +11 -3
  78. krons/session/message.py +3 -1
  79. krons/session/registry.py +35 -0
  80. krons/session/session.py +165 -174
  81. krons/utils/__init__.py +45 -0
  82. krons/utils/_function_arg_parser.py +99 -0
  83. krons/utils/_pythonic_function_call.py +249 -0
  84. krons/utils/_to_list.py +9 -3
  85. krons/utils/_utils.py +6 -2
  86. krons/utils/concurrency/_async_call.py +4 -2
  87. krons/utils/concurrency/_errors.py +3 -1
  88. krons/utils/concurrency/_patterns.py +3 -1
  89. krons/utils/concurrency/_resource_tracker.py +6 -2
  90. krons/utils/display.py +257 -0
  91. krons/utils/fuzzy/__init__.py +6 -1
  92. krons/utils/fuzzy/_fuzzy_match.py +14 -8
  93. krons/utils/fuzzy/_string_similarity.py +3 -1
  94. krons/utils/fuzzy/_to_dict.py +3 -1
  95. krons/utils/schemas/__init__.py +26 -0
  96. krons/utils/schemas/_breakdown_pydantic_annotation.py +131 -0
  97. krons/utils/schemas/_formatter.py +72 -0
  98. krons/utils/schemas/_minimal_yaml.py +151 -0
  99. krons/utils/schemas/_typescript.py +153 -0
  100. krons/utils/validators/__init__.py +3 -0
  101. krons/utils/validators/_validate_image_url.py +56 -0
  102. krons/work/__init__.py +115 -0
  103. krons/work/engine.py +333 -0
  104. krons/work/form.py +242 -0
  105. krons/{operations → work/operations}/__init__.py +7 -4
  106. krons/{operations → work/operations}/builder.py +1 -1
  107. krons/{enforcement → work/operations}/context.py +36 -5
  108. krons/{operations → work/operations}/flow.py +13 -5
  109. krons/{operations → work/operations}/node.py +45 -43
  110. krons/work/operations/registry.py +103 -0
  111. krons/work/report.py +268 -0
  112. krons/work/rules/__init__.py +47 -0
  113. krons/{enforcement → work/rules}/common/boolean.py +3 -1
  114. krons/{enforcement → work/rules}/common/choice.py +9 -3
  115. krons/{enforcement → work/rules}/common/number.py +3 -1
  116. krons/{enforcement → work/rules}/common/string.py +9 -3
  117. krons/{enforcement → work/rules}/rule.py +1 -1
  118. krons/{enforcement → work/rules}/validator.py +20 -5
  119. krons/work/worker.py +266 -0
  120. {krons-0.1.1.dist-info → krons-0.2.1.dist-info}/METADATA +15 -1
  121. krons-0.2.1.dist-info/RECORD +151 -0
  122. krons/enforcement/__init__.py +0 -57
  123. krons/enforcement/policy.py +0 -80
  124. krons/enforcement/service.py +0 -370
  125. krons/operations/registry.py +0 -92
  126. krons/services/__init__.py +0 -81
  127. krons/specs/phrase.py +0 -405
  128. krons-0.1.1.dist-info/RECORD +0 -101
  129. /krons/{specs → core/specs}/adapters/__init__.py +0 -0
  130. /krons/{specs → core/specs}/adapters/_utils.py +0 -0
  131. /krons/{specs → core/specs}/adapters/factory.py +0 -0
  132. /krons/{types → core/types}/__init__.py +0 -0
  133. /krons/{types → core/types}/_sentinel.py +0 -0
  134. /krons/{types → core/types}/identity.py +0 -0
  135. /krons/{services → resource}/utilities/__init__.py +0 -0
  136. /krons/{services → resource}/utilities/header_factory.py +0 -0
  137. /krons/{enforcement → work/rules}/common/__init__.py +0 -0
  138. /krons/{enforcement → work/rules}/common/mapping.py +0 -0
  139. /krons/{enforcement → work/rules}/common/model.py +0 -0
  140. /krons/{enforcement → work/rules}/registry.py +0 -0
  141. {krons-0.1.1.dist-info → krons-0.2.1.dist-info}/WHEEL +0 -0
  142. {krons-0.1.1.dist-info → krons-0.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -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
- )
@@ -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
- ...
@@ -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)
@@ -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()})"
@@ -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
- )