pawly 0.1.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.
Files changed (206) hide show
  1. pawly/__init__.py +154 -0
  2. pawly/__pycache__/__init__.cpython-311.pyc +0 -0
  3. pawly/__pycache__/__init__.cpython-313.pyc +0 -0
  4. pawly/__pycache__/action_selection.cpython-311.pyc +0 -0
  5. pawly/__pycache__/action_selection.cpython-313.pyc +0 -0
  6. pawly/__pycache__/api.cpython-311.pyc +0 -0
  7. pawly/__pycache__/api.cpython-313.pyc +0 -0
  8. pawly/__pycache__/contracts.cpython-311.pyc +0 -0
  9. pawly/__pycache__/contracts.cpython-313.pyc +0 -0
  10. pawly/__pycache__/decision_engine.cpython-311.pyc +0 -0
  11. pawly/__pycache__/decision_engine.cpython-313.pyc +0 -0
  12. pawly/__pycache__/pawprint_loader.cpython-311.pyc +0 -0
  13. pawly/__pycache__/pawprint_loader.cpython-313.pyc +0 -0
  14. pawly/__pycache__/protected_oss.cpython-311.pyc +0 -0
  15. pawly/__pycache__/protected_oss.cpython-313.pyc +0 -0
  16. pawly/__pycache__/runtime.cpython-311.pyc +0 -0
  17. pawly/__pycache__/runtime.cpython-313.pyc +0 -0
  18. pawly/__pycache__/runtime_bootstrap.cpython-311.pyc +0 -0
  19. pawly/__pycache__/runtime_bootstrap.cpython-313.pyc +0 -0
  20. pawly/__pycache__/runtime_decision.cpython-311.pyc +0 -0
  21. pawly/__pycache__/runtime_decision.cpython-313.pyc +0 -0
  22. pawly/__pycache__/runtime_execute.cpython-311.pyc +0 -0
  23. pawly/__pycache__/runtime_execute.cpython-313.pyc +0 -0
  24. pawly/__pycache__/runtime_flow.cpython-311.pyc +0 -0
  25. pawly/__pycache__/runtime_flow.cpython-313.pyc +0 -0
  26. pawly/__pycache__/runtime_ids.cpython-311.pyc +0 -0
  27. pawly/__pycache__/runtime_ids.cpython-313.pyc +0 -0
  28. pawly/__pycache__/runtime_report.cpython-311.pyc +0 -0
  29. pawly/__pycache__/runtime_report.cpython-313.pyc +0 -0
  30. pawly/__pycache__/runtime_request.cpython-311.pyc +0 -0
  31. pawly/__pycache__/runtime_request.cpython-313.pyc +0 -0
  32. pawly/__pycache__/runtime_result.cpython-311.pyc +0 -0
  33. pawly/__pycache__/runtime_result.cpython-313.pyc +0 -0
  34. pawly/__pycache__/runtime_scoring.cpython-311.pyc +0 -0
  35. pawly/__pycache__/runtime_scoring.cpython-313.pyc +0 -0
  36. pawly/__pycache__/shield.cpython-311.pyc +0 -0
  37. pawly/__pycache__/shield.cpython-313.pyc +0 -0
  38. pawly/__pycache__/skill_registry.cpython-311.pyc +0 -0
  39. pawly/__pycache__/skill_registry.cpython-313.pyc +0 -0
  40. pawly/__pycache__/task_request.cpython-311.pyc +0 -0
  41. pawly/__pycache__/task_request.cpython-313.pyc +0 -0
  42. pawly/__pycache__/types.cpython-311.pyc +0 -0
  43. pawly/__pycache__/types.cpython-313.pyc +0 -0
  44. pawly/action_selection.py +64 -0
  45. pawly/adapters/__init__.py +25 -0
  46. pawly/adapters/__pycache__/__init__.cpython-311.pyc +0 -0
  47. pawly/adapters/__pycache__/__init__.cpython-313.pyc +0 -0
  48. pawly/adapters/__pycache__/claude_skills.cpython-311.pyc +0 -0
  49. pawly/adapters/__pycache__/claude_skills.cpython-313.pyc +0 -0
  50. pawly/adapters/__pycache__/openai_agents.cpython-311.pyc +0 -0
  51. pawly/adapters/__pycache__/openai_agents.cpython-313.pyc +0 -0
  52. pawly/adapters/claude_skills.py +210 -0
  53. pawly/adapters/openai_agents.py +208 -0
  54. pawly/api.py +57 -0
  55. pawly/approval/__init__.py +18 -0
  56. pawly/approval/__pycache__/__init__.cpython-311.pyc +0 -0
  57. pawly/approval/__pycache__/__init__.cpython-313.pyc +0 -0
  58. pawly/approval/__pycache__/callback.cpython-311.pyc +0 -0
  59. pawly/approval/__pycache__/callback.cpython-313.pyc +0 -0
  60. pawly/approval/__pycache__/handler.cpython-311.pyc +0 -0
  61. pawly/approval/__pycache__/handler.cpython-313.pyc +0 -0
  62. pawly/approval/__pycache__/models.cpython-311.pyc +0 -0
  63. pawly/approval/__pycache__/models.cpython-313.pyc +0 -0
  64. pawly/approval/__pycache__/queue.cpython-311.pyc +0 -0
  65. pawly/approval/__pycache__/queue.cpython-313.pyc +0 -0
  66. pawly/approval/__pycache__/router.cpython-311.pyc +0 -0
  67. pawly/approval/__pycache__/router.cpython-313.pyc +0 -0
  68. pawly/approval/__pycache__/timeout.cpython-311.pyc +0 -0
  69. pawly/approval/__pycache__/timeout.cpython-313.pyc +0 -0
  70. pawly/approval/callback.py +16 -0
  71. pawly/approval/handler.py +34 -0
  72. pawly/approval/models.py +119 -0
  73. pawly/approval/queue.py +109 -0
  74. pawly/approval/router.py +66 -0
  75. pawly/approval/timeout.py +18 -0
  76. pawly/audit/__pycache__/diff.cpython-311.pyc +0 -0
  77. pawly/audit/__pycache__/diff.cpython-313.pyc +0 -0
  78. pawly/audit/__pycache__/events.cpython-311.pyc +0 -0
  79. pawly/audit/__pycache__/events.cpython-313.pyc +0 -0
  80. pawly/audit/__pycache__/ledger.cpython-311.pyc +0 -0
  81. pawly/audit/__pycache__/ledger.cpython-313.pyc +0 -0
  82. pawly/audit/__pycache__/replay.cpython-311.pyc +0 -0
  83. pawly/audit/__pycache__/replay.cpython-313.pyc +0 -0
  84. pawly/audit/diff.py +24 -0
  85. pawly/audit/events.py +156 -0
  86. pawly/audit/ledger.py +39 -0
  87. pawly/audit/replay.py +45 -0
  88. pawly/backends/__init__.py +39 -0
  89. pawly/backends/__pycache__/__init__.cpython-311.pyc +0 -0
  90. pawly/backends/__pycache__/__init__.cpython-313.pyc +0 -0
  91. pawly/backends/__pycache__/approval.cpython-311.pyc +0 -0
  92. pawly/backends/__pycache__/approval.cpython-313.pyc +0 -0
  93. pawly/backends/__pycache__/audit.cpython-311.pyc +0 -0
  94. pawly/backends/__pycache__/audit.cpython-313.pyc +0 -0
  95. pawly/backends/__pycache__/reviewer.cpython-311.pyc +0 -0
  96. pawly/backends/__pycache__/reviewer.cpython-313.pyc +0 -0
  97. pawly/backends/__pycache__/risk.cpython-311.pyc +0 -0
  98. pawly/backends/__pycache__/risk.cpython-313.pyc +0 -0
  99. pawly/backends/approval.py +85 -0
  100. pawly/backends/audit.py +189 -0
  101. pawly/backends/reviewer.py +127 -0
  102. pawly/backends/risk.py +71 -0
  103. pawly/budget/__pycache__/checker.cpython-311.pyc +0 -0
  104. pawly/budget/__pycache__/checker.cpython-313.pyc +0 -0
  105. pawly/budget/__pycache__/state.cpython-311.pyc +0 -0
  106. pawly/budget/__pycache__/state.cpython-313.pyc +0 -0
  107. pawly/budget/checker.py +37 -0
  108. pawly/budget/state.py +15 -0
  109. pawly/cli.py +44 -0
  110. pawly/contracts.py +153 -0
  111. pawly/decision_engine.py +6 -0
  112. pawly/escalation/__pycache__/patterns.cpython-311.pyc +0 -0
  113. pawly/escalation/__pycache__/patterns.cpython-313.pyc +0 -0
  114. pawly/escalation/patterns.py +33 -0
  115. pawly/gateway/__init__.py +4 -0
  116. pawly/gateway/__pycache__/__init__.cpython-311.pyc +0 -0
  117. pawly/gateway/__pycache__/__init__.cpython-313.pyc +0 -0
  118. pawly/gateway/__pycache__/adapter_support.cpython-311.pyc +0 -0
  119. pawly/gateway/__pycache__/adapter_support.cpython-313.pyc +0 -0
  120. pawly/gateway/__pycache__/protocol.cpython-311.pyc +0 -0
  121. pawly/gateway/__pycache__/protocol.cpython-313.pyc +0 -0
  122. pawly/gateway/__pycache__/result.cpython-311.pyc +0 -0
  123. pawly/gateway/__pycache__/result.cpython-313.pyc +0 -0
  124. pawly/gateway/__pycache__/wrapper.cpython-311.pyc +0 -0
  125. pawly/gateway/__pycache__/wrapper.cpython-313.pyc +0 -0
  126. pawly/gateway/adapter_support.py +56 -0
  127. pawly/gateway/protocol.py +24 -0
  128. pawly/gateway/result.py +67 -0
  129. pawly/gateway/wrapper.py +273 -0
  130. pawly/loader/__pycache__/schema_loader.cpython-311.pyc +0 -0
  131. pawly/loader/__pycache__/schema_loader.cpython-313.pyc +0 -0
  132. pawly/loader/__pycache__/yaml_loader.cpython-311.pyc +0 -0
  133. pawly/loader/__pycache__/yaml_loader.cpython-313.pyc +0 -0
  134. pawly/loader/schema_loader.py +51 -0
  135. pawly/loader/yaml_loader.py +164 -0
  136. pawly/memory/__pycache__/store.cpython-311.pyc +0 -0
  137. pawly/memory/__pycache__/store.cpython-313.pyc +0 -0
  138. pawly/memory/__pycache__/tiers.cpython-311.pyc +0 -0
  139. pawly/memory/__pycache__/tiers.cpython-313.pyc +0 -0
  140. pawly/memory/store.py +23 -0
  141. pawly/memory/tiers.py +9 -0
  142. pawly/middleware/__init__.py +1 -0
  143. pawly/middleware/__pycache__/__init__.cpython-311.pyc +0 -0
  144. pawly/middleware/__pycache__/__init__.cpython-313.pyc +0 -0
  145. pawly/middleware/__pycache__/hooks.cpython-311.pyc +0 -0
  146. pawly/middleware/__pycache__/hooks.cpython-313.pyc +0 -0
  147. pawly/middleware/hooks.py +20 -0
  148. pawly/pawprint_loader.py +301 -0
  149. pawly/performance/__pycache__/metrics.cpython-311.pyc +0 -0
  150. pawly/performance/__pycache__/metrics.cpython-313.pyc +0 -0
  151. pawly/performance/__pycache__/report.cpython-311.pyc +0 -0
  152. pawly/performance/__pycache__/report.cpython-313.pyc +0 -0
  153. pawly/performance/metrics.py +48 -0
  154. pawly/performance/report.py +44 -0
  155. pawly/policy/__init__.py +15 -0
  156. pawly/policy/__pycache__/__init__.cpython-311.pyc +0 -0
  157. pawly/policy/__pycache__/__init__.cpython-313.pyc +0 -0
  158. pawly/policy/__pycache__/base.cpython-311.pyc +0 -0
  159. pawly/policy/__pycache__/base.cpython-313.pyc +0 -0
  160. pawly/policy/__pycache__/heuristic.cpython-311.pyc +0 -0
  161. pawly/policy/__pycache__/heuristic.cpython-313.pyc +0 -0
  162. pawly/policy/__pycache__/resolve.cpython-311.pyc +0 -0
  163. pawly/policy/__pycache__/resolve.cpython-313.pyc +0 -0
  164. pawly/policy/base.py +84 -0
  165. pawly/policy/cloud.py +18 -0
  166. pawly/policy/heuristic.py +141 -0
  167. pawly/policy/resolve.py +60 -0
  168. pawly/policy_engine/__pycache__/decision.cpython-311.pyc +0 -0
  169. pawly/policy_engine/__pycache__/decision.cpython-313.pyc +0 -0
  170. pawly/policy_engine/__pycache__/engine.cpython-311.pyc +0 -0
  171. pawly/policy_engine/__pycache__/engine.cpython-313.pyc +0 -0
  172. pawly/policy_engine/__pycache__/models.cpython-311.pyc +0 -0
  173. pawly/policy_engine/__pycache__/models.cpython-313.pyc +0 -0
  174. pawly/policy_engine/__pycache__/runtime_overlay.cpython-311.pyc +0 -0
  175. pawly/policy_engine/__pycache__/runtime_overlay.cpython-313.pyc +0 -0
  176. pawly/policy_engine/decision.py +40 -0
  177. pawly/policy_engine/engine.py +152 -0
  178. pawly/policy_engine/models.py +62 -0
  179. pawly/policy_engine/risk.py +24 -0
  180. pawly/policy_engine/runtime_overlay.py +37 -0
  181. pawly/protected_oss.py +168 -0
  182. pawly/runtime.py +1004 -0
  183. pawly/runtime_bootstrap.py +104 -0
  184. pawly/runtime_decision.py +52 -0
  185. pawly/runtime_execute.py +59 -0
  186. pawly/runtime_flow.py +50 -0
  187. pawly/runtime_ids.py +19 -0
  188. pawly/runtime_report.py +18 -0
  189. pawly/runtime_request.py +23 -0
  190. pawly/runtime_result.py +142 -0
  191. pawly/runtime_scoring.py +25 -0
  192. pawly/schemas/decision.schema.json +38 -0
  193. pawly/schemas/intent.schema.json +33 -0
  194. pawly/schemas/report.schema.json +36 -0
  195. pawly/shield.py +436 -0
  196. pawly/skill_registry.py +31 -0
  197. pawly/task_request.py +27 -0
  198. pawly/types.py +15 -0
  199. pawly/validator/__pycache__/validator.cpython-311.pyc +0 -0
  200. pawly/validator/__pycache__/validator.cpython-313.pyc +0 -0
  201. pawly/validator/validator.py +177 -0
  202. pawly-0.1.0.dist-info/METADATA +7 -0
  203. pawly-0.1.0.dist-info/RECORD +206 -0
  204. pawly-0.1.0.dist-info/WHEEL +4 -0
  205. pawly-0.1.0.dist-info/entry_points.txt +2 -0
  206. pawly-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,127 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import importlib
5
+ import logging
6
+ from typing import Any
7
+ from typing import Protocol
8
+
9
+ from pawly.backends.risk import LocalRiskProvider, RiskProvider
10
+ from pawly.pawprint_loader import PawprintConfig
11
+ from pawly.policy_engine.engine import evaluate_pawprint
12
+ from pawly.types import Intent, PolicyEvaluation
13
+
14
+
15
+ LOGGER = logging.getLogger(__name__)
16
+ _CONFIG_WARNING = "Pawly Cloud not configured. Falling back to rule-based decision."
17
+
18
+
19
+ class ReviewerPolicy(Protocol):
20
+ name: str
21
+
22
+ def evaluate(self, intent: Intent, pawprint: PawprintConfig, context: dict[str, Any] | None = None) -> PolicyEvaluation:
23
+ ...
24
+
25
+
26
+ class RulePolicy:
27
+ name = "rules"
28
+ reviewer_mode = "rule"
29
+
30
+ def __init__(self, risk_provider: RiskProvider | None = None) -> None:
31
+ self.risk_provider = risk_provider or LocalRiskProvider()
32
+
33
+ def evaluate(self, intent: Intent, pawprint: PawprintConfig, context: dict[str, Any] | None = None) -> PolicyEvaluation:
34
+ del context
35
+ return evaluate_pawprint(
36
+ intent,
37
+ pawprint,
38
+ risk_provider=self.risk_provider,
39
+ )
40
+
41
+ def review(self, intent: Intent, pawprint: PawprintConfig, context: dict[str, Any] | None = None) -> PolicyEvaluation:
42
+ return self.evaluate(intent, pawprint, context)
43
+
44
+
45
+ class CloudReviewerStub:
46
+ name = "cloud-review"
47
+
48
+ def evaluate(self, intent: Intent, pawprint: PawprintConfig, context: dict[str, Any] | None = None) -> PolicyEvaluation:
49
+ del context
50
+ del intent, pawprint
51
+ raise NotImplementedError("Cloud reviewer backends are optional extensions and are not part of the OSS runtime path.")
52
+
53
+ def review(self, intent: Intent, pawprint: PawprintConfig, context: dict[str, Any] | None = None) -> PolicyEvaluation:
54
+ return self.evaluate(intent, pawprint, context)
55
+
56
+
57
+ class CloudReviewer(CloudReviewerStub):
58
+ @classmethod
59
+ def from_env(cls) -> "CloudReviewer | None":
60
+ cloud_reviewer_cls = _load_external_cloud_reviewer()
61
+ if cloud_reviewer_cls is None:
62
+ return None
63
+ return cloud_reviewer_cls.from_env()
64
+
65
+
66
+ def resolve_policy(
67
+ policy: str = "rules",
68
+ *,
69
+ policy_impl: ReviewerPolicy | None = None,
70
+ risk_provider: RiskProvider | None = None,
71
+ ) -> ReviewerPolicy:
72
+ if policy_impl is not None:
73
+ return policy_impl
74
+ if policy == "rules":
75
+ return RulePolicy(risk_provider=risk_provider)
76
+ if policy == "cloud":
77
+ cloud_reviewer = CloudReviewer.from_env()
78
+ if cloud_reviewer is not None:
79
+ return cloud_reviewer
80
+ LOGGER.warning(_CONFIG_WARNING)
81
+ return RulePolicy(risk_provider=risk_provider)
82
+ raise ValueError(f"unsupported policy: {policy}")
83
+
84
+
85
+ def evaluate_reviewer_policy(
86
+ policy: ReviewerPolicy,
87
+ intent: Intent,
88
+ pawprint: PawprintConfig,
89
+ context: dict[str, Any] | None = None,
90
+ ) -> PolicyEvaluation:
91
+ evaluate = getattr(policy, "evaluate", None)
92
+ if callable(evaluate):
93
+ return evaluate(intent, pawprint, context)
94
+
95
+ review = getattr(policy, "review", None)
96
+ if not callable(review):
97
+ raise TypeError("reviewer policy must provide evaluate(...) or review(...)")
98
+
99
+ parameters = inspect.signature(review).parameters
100
+ if "context" in parameters or len(parameters) >= 3:
101
+ return review(intent, pawprint, context)
102
+ return review(intent, pawprint)
103
+
104
+
105
+ ReviewerBackend = ReviewerPolicy
106
+ RuleReviewer = RulePolicy
107
+
108
+
109
+ def resolve_reviewer_backend(
110
+ reviewer: str = "rules",
111
+ *,
112
+ reviewer_backend: ReviewerBackend | None = None,
113
+ risk_provider: RiskProvider | None = None,
114
+ ) -> ReviewerBackend:
115
+ return resolve_policy(
116
+ policy=reviewer,
117
+ policy_impl=reviewer_backend,
118
+ risk_provider=risk_provider,
119
+ )
120
+
121
+
122
+ def _load_external_cloud_reviewer():
123
+ try:
124
+ module = importlib.import_module("pawly_cloud")
125
+ except ModuleNotFoundError:
126
+ return None
127
+ return getattr(module, "CloudReviewer", None)
pawly/backends/risk.py ADDED
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Protocol
5
+
6
+ from pawly.escalation.patterns import text_matches_rule
7
+ from pawly.types import Intent
8
+
9
+
10
+ EXTERNAL_SIDE_EFFECT_RULES = (
11
+ "issue refund",
12
+ "change account details",
13
+ "delete customer records",
14
+ "approve exception",
15
+ "send external message",
16
+ "publish update",
17
+ "modify billing",
18
+ "legal advice",
19
+ )
20
+
21
+
22
+ @dataclass(slots=True)
23
+ class RiskContext:
24
+ intent: Intent
25
+ ask_first_matches: list[str]
26
+ never_matches: list[str]
27
+ capability_matched: bool
28
+ handoff_matches: list[str]
29
+
30
+
31
+ class RiskProvider(Protocol):
32
+ name: str
33
+
34
+ def score(self, context: RiskContext) -> float:
35
+ ...
36
+
37
+
38
+ class LocalRiskProvider:
39
+ name = "local-rules"
40
+
41
+ def score(self, context: RiskContext) -> float:
42
+ if context.never_matches:
43
+ return 0.98
44
+
45
+ score = 0.1
46
+ if context.ask_first_matches:
47
+ score += 0.35
48
+ if context.handoff_matches:
49
+ score += 0.2
50
+ if not context.capability_matched:
51
+ score += 0.35
52
+ if _has_external_side_effect(context.intent):
53
+ score += 0.15
54
+ if context.intent.confidence < 0.7:
55
+ score += 0.15
56
+ return round(min(score, 0.95), 2)
57
+
58
+
59
+ class AdvancedRiskProviderStub:
60
+ name = "cloud-risk"
61
+
62
+ def score(self, context: RiskContext) -> float:
63
+ del context
64
+ raise NotImplementedError("Cloud risk providers are optional extensions and are not part of the OSS runtime path.")
65
+
66
+
67
+ def _has_external_side_effect(intent: Intent) -> bool:
68
+ text = " ".join(
69
+ part for part in [intent.summary, intent.action.name, intent.action.target or ""] if part
70
+ )
71
+ return any(text_matches_rule(text, rule) for rule in EXTERNAL_SIDE_EFFECT_RULES)
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+ from pawly.budget.state import BudgetState
6
+ from pawly.types import Intent
7
+
8
+
9
+ @dataclass(slots=True)
10
+ class BudgetResult:
11
+ exhausted: bool
12
+ warnings: list[str] = field(default_factory=list)
13
+ consumed: dict[str, float] = field(default_factory=dict)
14
+
15
+
16
+ def check_budget(agent_config: dict, state: BudgetState, intent: Intent) -> BudgetResult:
17
+ warnings: list[str] = []
18
+ consumed: dict[str, float] = {}
19
+ exhausted = False
20
+ for limit in agent_config.get("budget", {}).get("limits", []):
21
+ name = limit.get("name", "unnamed-limit")
22
+ amount = float(limit.get("amount", 0))
23
+ unit = limit.get("unit", "count")
24
+ increment = float(intent.metadata.get(name, 1 if unit == "count" else 0))
25
+ current = state.consume(name, increment)
26
+ consumed[name] = current
27
+ if amount and current >= amount:
28
+ if limit.get("enforcement") == "hard":
29
+ exhausted = True
30
+ warnings.append(f"budget limit reached for {name}: {current:g}/{amount:g}")
31
+ else:
32
+ for threshold in agent_config.get("budget", {}).get("warning_thresholds", []):
33
+ if threshold.get("limit") == name:
34
+ percent = float(threshold.get("percent", 0))
35
+ if amount and current >= amount * (percent / 100.0):
36
+ warnings.append(f"budget warning for {name}: {current:g}/{amount:g}")
37
+ return BudgetResult(exhausted=exhausted, warnings=list(dict.fromkeys(warnings)), consumed=consumed)
pawly/budget/state.py ADDED
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+
5
+
6
+ class BudgetState:
7
+ def __init__(self) -> None:
8
+ self._usage = defaultdict(float)
9
+
10
+ def consume(self, name: str, amount: float) -> float:
11
+ self._usage[name] += amount
12
+ return self._usage[name]
13
+
14
+ def snapshot(self) -> dict[str, float]:
15
+ return dict(self._usage)
pawly/cli.py ADDED
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ from pathlib import Path
6
+ import sys
7
+
8
+ from pawly.decision_engine import DecisionEngine
9
+ from pawly.validator.validator import SchemaValidationError
10
+
11
+ DEFAULT_AUDIT_LOG = "pawly.audit.jsonl"
12
+
13
+
14
+ def build_parser() -> argparse.ArgumentParser:
15
+ parser = argparse.ArgumentParser(prog="pawly", description="Aploy Pawly execution-boundary controller CLI")
16
+ parser.add_argument("--agent", required=True, help="Path to a Pawprint worker card")
17
+ parser.add_argument("--task", required=True, help="Task description to evaluate")
18
+ parser.add_argument("--action", required=True, help="Action name to evaluate")
19
+ parser.add_argument("--confidence", required=True, type=float, help="Model confidence in the proposed action")
20
+ parser.add_argument("--audit-log", default=DEFAULT_AUDIT_LOG, help="JSONL audit log path (defaults to ./pawly.audit.jsonl)")
21
+ parser.add_argument("--report", action="store_true", help="Print a report after evaluation")
22
+ return parser
23
+
24
+
25
+ def main(argv: list[str] | None = None) -> int:
26
+ parser = build_parser()
27
+ args = parser.parse_args(argv)
28
+ try:
29
+ runtime = DecisionEngine(args.agent, audit_path=args.audit_log)
30
+ decision = runtime.evaluate(task=args.task, action=args.action, confidence=args.confidence)
31
+ decision["audit_log"] = str(Path(args.audit_log).resolve())
32
+ print(json.dumps(decision, indent=2, sort_keys=True))
33
+ if args.report:
34
+ print(json.dumps(runtime.build_report(), indent=2, sort_keys=True))
35
+ return 0
36
+ except SchemaValidationError as exc:
37
+ parser.error(str(exc))
38
+ except Exception as exc: # pragma: no cover - CLI safety
39
+ print(json.dumps({"error": str(exc)}), file=sys.stderr)
40
+ return 1
41
+
42
+
43
+ if __name__ == "__main__": # pragma: no cover
44
+ raise SystemExit(main())
pawly/contracts.py ADDED
@@ -0,0 +1,153 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from typing import Any
6
+
7
+
8
+ class DecisionState(str, Enum):
9
+ ALLOW = "allow"
10
+ DENY = "deny"
11
+ REQUIRE_APPROVAL = "require_approval"
12
+ SIMULATE = "simulate"
13
+
14
+
15
+ class DecisionSource(str, Enum):
16
+ RULE = "rule"
17
+ CLOUD = "cloud"
18
+
19
+
20
+ class IntentSource(str, Enum):
21
+ TOOL_CALL = "tool_call"
22
+ PLANNER_OUTPUT = "planner_output"
23
+ EXECUTION_REQUEST = "execution_request"
24
+
25
+
26
+ @dataclass(slots=True)
27
+ class Action:
28
+ name: str
29
+ arguments: dict[str, Any] = field(default_factory=dict)
30
+ target: str | None = None
31
+
32
+ @classmethod
33
+ def from_dict(cls, payload: dict[str, Any]) -> "Action":
34
+ return cls(
35
+ name=str(payload["name"]),
36
+ arguments=dict(payload.get("arguments", {})),
37
+ target=None if payload.get("target") is None else str(payload["target"]),
38
+ )
39
+
40
+ def to_dict(self) -> dict[str, Any]:
41
+ payload = {"name": self.name, "arguments": self.arguments}
42
+ if self.target is not None:
43
+ payload["target"] = self.target
44
+ return payload
45
+
46
+
47
+ @dataclass(slots=True)
48
+ class Intent:
49
+ intent_id: str
50
+ source: IntentSource
51
+ action: Action
52
+ summary: str = ""
53
+ confidence: float = 0.0
54
+ metadata: dict[str, Any] = field(default_factory=dict)
55
+
56
+ @classmethod
57
+ def from_dict(cls, payload: dict[str, Any]) -> "Intent":
58
+ context = payload.get("context", {})
59
+ return cls(
60
+ intent_id=str(payload["intent_id"]),
61
+ source=IntentSource(payload["source"]),
62
+ action=Action.from_dict(payload["action"]),
63
+ summary=str(context.get("summary", "")),
64
+ confidence=float(context.get("confidence", 0.0)),
65
+ metadata=dict(payload.get("metadata", {})),
66
+ )
67
+
68
+ def text(self) -> str:
69
+ return f"{self.summary} {self.action.name}".strip()
70
+
71
+ def to_dict(self) -> dict[str, Any]:
72
+ return {
73
+ "intent_id": self.intent_id,
74
+ "source": self.source.value,
75
+ "action": self.action.to_dict(),
76
+ "context": {
77
+ "summary": self.summary,
78
+ "confidence": self.confidence,
79
+ },
80
+ "metadata": self.metadata,
81
+ }
82
+
83
+
84
+ @dataclass(slots=True)
85
+ class PolicyScore:
86
+ risk_score: float | None = None
87
+ reason_codes: list[str] = field(default_factory=list)
88
+ matched_rules: list[str] = field(default_factory=list)
89
+ audit_tags: list[str] = field(default_factory=list)
90
+ uncertainty: float | None = None
91
+
92
+ def to_dict(self) -> dict[str, Any]:
93
+ payload: dict[str, Any] = {}
94
+ if self.risk_score is not None:
95
+ payload["risk_score"] = self.risk_score
96
+ if self.reason_codes:
97
+ payload["reason_codes"] = list(self.reason_codes)
98
+ if self.matched_rules:
99
+ payload["matched_rules"] = list(self.matched_rules)
100
+ if self.audit_tags:
101
+ payload["audit_tags"] = list(self.audit_tags)
102
+ if self.uncertainty is not None:
103
+ payload["uncertainty"] = self.uncertainty
104
+ return payload
105
+
106
+
107
+ @dataclass(slots=True)
108
+ class Decision:
109
+ type: DecisionState
110
+ reason: str
111
+ source: str | None = None
112
+ reason_codes: list[str] = field(default_factory=list)
113
+ matched_rules: list[str] = field(default_factory=list)
114
+ risk_score: float | None = None
115
+ rewritten_action: Action | None = None
116
+ audit_tags: list[str] = field(default_factory=list)
117
+ worker_id: str | None = None
118
+
119
+ @classmethod
120
+ def from_dict(cls, payload: dict[str, Any]) -> "Decision":
121
+ rewritten_action = payload.get("rewritten_action")
122
+ return cls(
123
+ type=DecisionState(payload["type"]),
124
+ reason=str(payload["reason"]),
125
+ source=None if payload.get("source") is None else str(payload["source"]),
126
+ reason_codes=[str(item) for item in payload.get("reason_codes", [])],
127
+ matched_rules=[str(item) for item in payload.get("matched_rules", [])],
128
+ risk_score=None if payload.get("risk_score") is None else float(payload["risk_score"]),
129
+ rewritten_action=Action.from_dict(rewritten_action) if isinstance(rewritten_action, dict) else None,
130
+ audit_tags=[str(item) for item in payload.get("audit_tags", [])],
131
+ worker_id=None if payload.get("worker_id") is None else str(payload["worker_id"]),
132
+ )
133
+
134
+ def to_dict(self) -> dict[str, Any]:
135
+ payload: dict[str, Any] = {
136
+ "type": self.type.value,
137
+ "reason": self.reason,
138
+ }
139
+ if self.source is not None:
140
+ payload["source"] = self.source
141
+ if self.reason_codes:
142
+ payload["reason_codes"] = list(self.reason_codes)
143
+ if self.matched_rules:
144
+ payload["matched_rules"] = list(self.matched_rules)
145
+ if self.risk_score is not None:
146
+ payload["risk_score"] = self.risk_score
147
+ if self.rewritten_action is not None:
148
+ payload["rewritten_action"] = self.rewritten_action.to_dict()
149
+ if self.audit_tags:
150
+ payload["audit_tags"] = list(self.audit_tags)
151
+ if self.worker_id is not None:
152
+ payload["worker_id"] = self.worker_id
153
+ return payload
@@ -0,0 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+ from pawly.action_selection import ActionCandidate, ActionDecision
4
+ from pawly.runtime import DecisionEngine, PawlyRuntime
5
+
6
+ __all__ = ["ActionCandidate", "ActionDecision", "DecisionEngine", "PawlyRuntime"]
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ KEYWORD_ALIASES = {
6
+ "pricing": {"pricing", "price", "discount", "quote"},
7
+ "contract": {"contract", "agreement", "terms"},
8
+ "refund": {"refund", "credit", "chargeback"},
9
+ "security": {"security", "breach", "fraud", "compromise"},
10
+ "legal": {"legal", "lawsuit", "attorney"},
11
+ "fairness": {"fairness", "bias", "discrimination", "accommodation"},
12
+ }
13
+
14
+
15
+ def normalize_tokens(text: str) -> set[str]:
16
+ tokens = set(re.findall(r"[a-z0-9]+", text.lower()))
17
+ expanded = set(tokens)
18
+ for canonical, aliases in KEYWORD_ALIASES.items():
19
+ if tokens & aliases:
20
+ expanded.add(canonical)
21
+ expanded.update(aliases)
22
+ return expanded
23
+
24
+
25
+ def text_matches_rule(text: str, rule_text: str) -> bool:
26
+ text_tokens = normalize_tokens(text)
27
+ rule_tokens = normalize_tokens(rule_text)
28
+ if not rule_tokens:
29
+ return False
30
+ if rule_text.lower() in text.lower():
31
+ return True
32
+ overlap = text_tokens & rule_tokens
33
+ return len(overlap) >= max(1, min(2, len(rule_tokens) // 2 or 1))
@@ -0,0 +1,4 @@
1
+ from .protocol import GatewayProtocol
2
+ from .wrapper import ExecutionGateway, wrap_execute_fn, wrap_executor, wrap_framework_adapter
3
+
4
+ __all__ = ["ExecutionGateway", "GatewayProtocol", "wrap_execute_fn", "wrap_executor", "wrap_framework_adapter"]
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, TypeVar
4
+
5
+ from pawly.gateway import ExecutionGateway, GatewayProtocol
6
+ from pawly.runtime import PawlyRuntime
7
+
8
+
9
+ T = TypeVar("T")
10
+
11
+ AdapterExecutor = Callable[[T], Any]
12
+ AdapterRemap = Callable[[T, Any], T]
13
+ AuditHook = Callable[[dict[str, Any]], None]
14
+ AuditPayloadBuilder = Callable[[dict[str, Any]], dict[str, Any]]
15
+
16
+
17
+ def build_gateway(
18
+ runtime: PawlyRuntime,
19
+ *,
20
+ approval_handler: object | None = None,
21
+ gateway: GatewayProtocol | None = None,
22
+ ) -> GatewayProtocol:
23
+ return gateway or ExecutionGateway(runtime, approval_handler=approval_handler)
24
+
25
+
26
+ def metadata_from_intent(intent: Any) -> dict[str, Any]:
27
+ metadata = dict(intent.metadata)
28
+ for key, value in intent.action.arguments.items():
29
+ if key != "task":
30
+ metadata[key] = value
31
+ return metadata
32
+
33
+
34
+ def execute_adapter_action(
35
+ *,
36
+ gateway: GatewayProtocol,
37
+ item: T,
38
+ task: str,
39
+ action: str,
40
+ confidence: float,
41
+ metadata: dict[str, Any],
42
+ executor: AdapterExecutor[T],
43
+ remap: AdapterRemap[T],
44
+ audit_hook: AuditHook | None = None,
45
+ audit_payload: AuditPayloadBuilder | None = None,
46
+ ) -> dict[str, Any]:
47
+ outcome = gateway.execute(
48
+ task=task,
49
+ action=action,
50
+ confidence=confidence,
51
+ metadata=metadata,
52
+ executor=lambda intent: executor(remap(item, intent)),
53
+ )
54
+ if audit_hook is not None and audit_payload is not None:
55
+ audit_hook(audit_payload(outcome))
56
+ return outcome
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Protocol
4
+
5
+ from pawly.contracts import Intent
6
+
7
+
8
+ ExecutorFunc = Callable[[Intent], Any]
9
+
10
+
11
+ class GatewayProtocol(Protocol):
12
+ def execute(
13
+ self,
14
+ *,
15
+ task: str,
16
+ action: str,
17
+ confidence: float,
18
+ executor: ExecutorFunc,
19
+ metadata: dict[str, Any] | None = None,
20
+ ) -> dict[str, Any]:
21
+ ...
22
+
23
+ def execute_intent(self, intent: Intent, executor: ExecutorFunc) -> dict[str, Any]:
24
+ ...