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,67 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+ from uuid import uuid4
5
+
6
+ from pawly.contracts import Decision, Intent
7
+ from pawly.audit.diff import diff_actions
8
+ from pawly.audit.events import AuditEvent
9
+ from pawly.loader.schema_loader import load_pawprint_version
10
+ from pawly.runtime_result import RuntimeDecisionResult
11
+
12
+
13
+ def build_gateway_payload(
14
+ decision_result: RuntimeDecisionResult,
15
+ *,
16
+ execution: dict[str, Any],
17
+ approval: dict[str, Any] | None = None,
18
+ ) -> dict[str, Any]:
19
+ payload = {
20
+ **decision_result.to_dict(),
21
+ "execution": execution,
22
+ }
23
+ if approval is not None:
24
+ payload["approval"] = approval
25
+ return payload
26
+
27
+
28
+ def build_governed_execution_event(
29
+ *,
30
+ event_id: str | None,
31
+ original_intent: Intent,
32
+ decision_result: RuntimeDecisionResult,
33
+ payload: dict[str, Any],
34
+ ) -> AuditEvent:
35
+ executed_action = payload.get("execution", {}).get("used_action")
36
+ action = original_intent.action.to_dict()
37
+ return AuditEvent.from_governed_execution(
38
+ event_id=event_id or f"event-{uuid4().hex}",
39
+ decision_id=decision_result.decision_id,
40
+ agent_id=decision_result.agent_id,
41
+ pawprint_version=decision_result.pawprint_version or load_pawprint_version(),
42
+ outcome=decision_result.decision.type.value,
43
+ original_intent=original_intent.to_dict(),
44
+ normalized_intent=decision_result.intent.to_dict(),
45
+ action=action,
46
+ policy_evaluation=decision_result.policy_evaluation.to_dict(),
47
+ runtime_overlays=decision_result.runtime_overlays,
48
+ policy_references=decision_result.decision.matched_rules,
49
+ final_decision=decision_result.decision.to_dict(),
50
+ reason_codes=decision_result.decision.reason_codes,
51
+ approval=payload.get("approval"),
52
+ executed_action=executed_action,
53
+ action_diff=diff_actions(action, executed_action),
54
+ execution=payload.get("execution"),
55
+ execution_result_ref=_execution_result_ref(payload.get("execution", {}).get("result")),
56
+ risk_score=decision_result.decision.risk_score,
57
+ escalated_to=decision_result.policy_evaluation.handoff_target,
58
+ )
59
+
60
+
61
+ def _execution_result_ref(result: Any) -> str | None:
62
+ if isinstance(result, dict):
63
+ for key in ("result_ref", "artifact_ref", "reference", "uri", "path"):
64
+ value = result.get(key)
65
+ if isinstance(value, str) and value:
66
+ return value
67
+ return None
@@ -0,0 +1,273 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any, Callable
5
+
6
+ from pawly.approval.handler import ApprovalHandler
7
+ from pawly.approval.models import ApprovalStatus
8
+ from pawly.approval.router import ApprovalRouter
9
+ from pawly.backends.approval import ApprovalBackend, LocalApprovalBackend
10
+ from pawly.backends.audit import AuditSink
11
+ from pawly.backends.reviewer import ReviewerPolicy
12
+ from pawly.backends.risk import RiskProvider
13
+ from pawly.decision_engine import DecisionEngine
14
+ from pawly.gateway.protocol import ExecutorFunc
15
+ from pawly.gateway.result import build_gateway_payload, build_governed_execution_event
16
+ from pawly.policy import resolve_reviewer_selection
17
+ from pawly.runtime_request import build_task_request_intent
18
+ from pawly.runtime_result import RuntimeDecisionResult
19
+ from pawly.types import Intent
20
+
21
+ ExecuteFn = Callable[[str, str, float, dict[str, Any] | None], Any]
22
+
23
+
24
+ class ExecutionGateway:
25
+ def __init__(
26
+ self,
27
+ decision_engine: DecisionEngine,
28
+ approval_handler: ApprovalHandler | None = None,
29
+ policy: str = "rules",
30
+ approval_router: ApprovalRouter | None = None,
31
+ approval_backend: ApprovalBackend | None = None,
32
+ runtime: DecisionEngine | None = None,
33
+ reviewer: str | None = None,
34
+ ) -> None:
35
+ self.runtime = runtime or decision_engine
36
+ self.policy = reviewer or policy
37
+ self.reviewer = self.policy
38
+ self.approval_backend = approval_backend or LocalApprovalBackend(
39
+ handler=approval_handler,
40
+ router=approval_router,
41
+ )
42
+
43
+ def execute(
44
+ self,
45
+ *,
46
+ task: str,
47
+ action: str,
48
+ confidence: float,
49
+ executor: ExecutorFunc,
50
+ metadata: dict[str, Any] | None = None,
51
+ ) -> dict[str, Any]:
52
+ return self.execute_intent(
53
+ build_task_request_intent(
54
+ task=task,
55
+ action=action,
56
+ confidence=confidence,
57
+ metadata=metadata,
58
+ ),
59
+ executor,
60
+ )
61
+
62
+ def execute_intent(self, intent: Intent, executor: ExecutorFunc) -> dict[str, Any]:
63
+ decision_result = (
64
+ self.runtime.evaluate_intent_result(intent)
65
+ if hasattr(self.runtime, "evaluate_intent_result")
66
+ else None
67
+ )
68
+ if decision_result is None:
69
+ from pawly.runtime_result import RuntimeDecisionResult
70
+
71
+ decision_result = RuntimeDecisionResult.from_dict(self.runtime.evaluate_intent(intent))
72
+ if decision_result.decision.type.value == "require_approval":
73
+ record = self.approval_backend.submit(intent, decision_result.decision)
74
+ approval = self.approval_backend.status_payload(record)
75
+ if record.status == ApprovalStatus.APPROVED:
76
+ approved_intent = record.approved_intent()
77
+ result = executor(approved_intent)
78
+ payload = build_gateway_payload(
79
+ decision_result,
80
+ approval=approval,
81
+ execution={
82
+ "attempted": True,
83
+ "executed": True,
84
+ "blocked_by": None,
85
+ "result": result,
86
+ "used_action": approved_intent.action.to_dict(),
87
+ },
88
+ )
89
+ self._append_governed_execution_event(intent, decision_result, payload)
90
+ return payload
91
+ blocked_by = "expired" if record.status == ApprovalStatus.EXPIRED else "require_approval"
92
+ payload = build_gateway_payload(
93
+ decision_result,
94
+ approval=approval,
95
+ execution={
96
+ "attempted": False,
97
+ "executed": False,
98
+ "blocked_by": blocked_by,
99
+ "reviewer": self.policy,
100
+ },
101
+ )
102
+ self._append_governed_execution_event(intent, decision_result, payload)
103
+ return payload
104
+ if decision_result.decision.type.value == "simulate":
105
+ payload = build_gateway_payload(
106
+ decision_result,
107
+ execution={
108
+ "attempted": False,
109
+ "executed": False,
110
+ "blocked_by": "simulate",
111
+ "reviewer": self.policy,
112
+ },
113
+ )
114
+ self._append_governed_execution_event(intent, decision_result, payload)
115
+ return payload
116
+ if decision_result.decision.type.value != "allow":
117
+ payload = build_gateway_payload(
118
+ decision_result,
119
+ execution={
120
+ "attempted": False,
121
+ "executed": False,
122
+ "blocked_by": decision_result.decision.type.value,
123
+ "reviewer": self.policy,
124
+ },
125
+ )
126
+ self._append_governed_execution_event(intent, decision_result, payload)
127
+ return payload
128
+
129
+ result = executor(intent)
130
+ payload = build_gateway_payload(
131
+ decision_result,
132
+ execution={
133
+ "attempted": True,
134
+ "executed": True,
135
+ "blocked_by": None,
136
+ "reviewer": self.policy,
137
+ "result": result,
138
+ "used_action": intent.action.to_dict(),
139
+ },
140
+ )
141
+ self._append_governed_execution_event(intent, decision_result, payload)
142
+ return payload
143
+
144
+ def _append_governed_execution_event(
145
+ self,
146
+ original_intent: Intent,
147
+ decision_result: RuntimeDecisionResult,
148
+ payload: dict[str, Any],
149
+ ) -> None:
150
+ event = build_governed_execution_event(
151
+ event_id=self.runtime.next_event_id() if hasattr(self.runtime, "next_event_id") else None,
152
+ original_intent=original_intent,
153
+ decision_result=decision_result,
154
+ payload=payload,
155
+ )
156
+ if hasattr(self.runtime, "audit_sink"):
157
+ self.runtime.audit_sink.append(event)
158
+
159
+
160
+ def wrap_executor(
161
+ executor: ExecutorFunc,
162
+ pawprint: str | Path,
163
+ policy: str = "rules",
164
+ *,
165
+ approval_handler: ApprovalHandler | None = None,
166
+ approval_router: ApprovalRouter | None = None,
167
+ approval_backend: ApprovalBackend | None = None,
168
+ policy_impl: ReviewerPolicy | None = None,
169
+ risk_provider: RiskProvider | None = None,
170
+ audit_sink: AuditSink | None = None,
171
+ audit_path: str | Path | None = None,
172
+ reviewer: str | None = None,
173
+ reviewer_backend: ReviewerPolicy | None = None,
174
+ ) -> ExecutorFunc:
175
+ selected_policy, selected_policy_impl = resolve_reviewer_selection(
176
+ policy=policy,
177
+ policy_impl=policy_impl,
178
+ reviewer=reviewer,
179
+ reviewer_backend=reviewer_backend,
180
+ )
181
+ decision_engine = DecisionEngine(
182
+ pawprint,
183
+ audit_path=audit_path,
184
+ policy=selected_policy,
185
+ policy_impl=selected_policy_impl,
186
+ risk_provider=risk_provider,
187
+ audit_sink=audit_sink,
188
+ )
189
+ gateway = ExecutionGateway(
190
+ decision_engine,
191
+ approval_handler=approval_handler,
192
+ policy=selected_policy,
193
+ approval_router=approval_router,
194
+ approval_backend=approval_backend,
195
+ )
196
+
197
+ def wrapped(intent: Intent) -> dict[str, Any]:
198
+ return gateway.execute_intent(intent, executor)
199
+
200
+ return wrapped
201
+
202
+
203
+ def wrap_execute_fn(
204
+ fn: ExecuteFn,
205
+ pawprint: str | Path,
206
+ policy: str = "rules",
207
+ *,
208
+ approval_handler: ApprovalHandler | None = None,
209
+ approval_router: ApprovalRouter | None = None,
210
+ approval_backend: ApprovalBackend | None = None,
211
+ policy_impl: ReviewerPolicy | None = None,
212
+ risk_provider: RiskProvider | None = None,
213
+ audit_sink: AuditSink | None = None,
214
+ audit_path: str | Path | None = None,
215
+ reviewer: str | None = None,
216
+ reviewer_backend: ReviewerPolicy | None = None,
217
+ ) -> ExecuteFn:
218
+ selected_policy, selected_policy_impl = resolve_reviewer_selection(
219
+ policy=policy,
220
+ policy_impl=policy_impl,
221
+ reviewer=reviewer,
222
+ reviewer_backend=reviewer_backend,
223
+ )
224
+ decision_engine = DecisionEngine(
225
+ pawprint,
226
+ audit_path=audit_path,
227
+ policy=selected_policy,
228
+ policy_impl=selected_policy_impl,
229
+ risk_provider=risk_provider,
230
+ audit_sink=audit_sink,
231
+ )
232
+ gateway = ExecutionGateway(
233
+ decision_engine,
234
+ approval_handler=approval_handler,
235
+ policy=selected_policy,
236
+ approval_router=approval_router,
237
+ approval_backend=approval_backend,
238
+ )
239
+
240
+ def wrapped(task: str, action: str, confidence: float, metadata: dict[str, Any] | None = None) -> dict[str, Any]:
241
+ return gateway.execute(
242
+ task=task,
243
+ action=action,
244
+ confidence=confidence,
245
+ metadata=metadata,
246
+ executor=lambda intent: _call_execute_fn_from_intent(fn, intent),
247
+ )
248
+
249
+ return wrapped
250
+
251
+
252
+ def wrap_framework_adapter(
253
+ decision_engine: DecisionEngine,
254
+ framework: str = "generic",
255
+ *,
256
+ approval_handler: ApprovalHandler | None = None,
257
+ approval_router: ApprovalRouter | None = None,
258
+ approval_backend: ApprovalBackend | None = None,
259
+ ) -> ExecutionGateway:
260
+ del framework
261
+ return ExecutionGateway(
262
+ decision_engine,
263
+ approval_handler=approval_handler,
264
+ policy="rules",
265
+ approval_router=approval_router,
266
+ approval_backend=approval_backend,
267
+ )
268
+ def _call_execute_fn_from_intent(fn: ExecuteFn, intent: Intent) -> Any:
269
+ metadata = dict(intent.metadata)
270
+ for key, value in intent.action.arguments.items():
271
+ if key != "task":
272
+ metadata[key] = value
273
+ return fn(intent.summary, intent.action.name, intent.confidence, metadata)
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ from importlib import resources
4
+ import json
5
+ from functools import lru_cache
6
+ from pathlib import Path
7
+
8
+
9
+ class PawprintDependencyError(RuntimeError):
10
+ """Raised when the runtime cannot load schema assets."""
11
+
12
+
13
+ PAWLY_SCHEMA_DIR = Path(__file__).resolve().parents[1] / "schemas"
14
+
15
+
16
+ def _schema_path(name: str) -> Path:
17
+ if name == "pawprint.schema.json":
18
+ schema_path = _pawprint_resource_path("schemas", name)
19
+ else:
20
+ schema_path = PAWLY_SCHEMA_DIR / name
21
+ if not schema_path.exists():
22
+ raise PawprintDependencyError(f"schema not found: {schema_path}")
23
+ return schema_path
24
+
25
+
26
+ @lru_cache(maxsize=None)
27
+ def load_schema(name: str) -> dict:
28
+ schema_path = _schema_path(name)
29
+ return json.loads(schema_path.read_text(encoding="utf-8"))
30
+
31
+
32
+ @lru_cache(maxsize=1)
33
+ def load_pawprint_version() -> str:
34
+ version_path = _pawprint_resource_path("VERSION")
35
+ if not version_path.exists():
36
+ raise PawprintDependencyError(f"pawprint version file not found: {version_path}")
37
+ return version_path.read_text(encoding="utf-8").strip()
38
+
39
+
40
+ def resolve_schema_path(name: str) -> Path:
41
+ return _schema_path(name)
42
+
43
+
44
+ def _pawprint_resource_path(*parts: str) -> Path:
45
+ try:
46
+ resource = resources.files("pawprint").joinpath(*parts)
47
+ except ModuleNotFoundError as exc:
48
+ raise PawprintDependencyError(
49
+ "pawprint package is not installed. Install 'pawprint' before using pawly."
50
+ ) from exc
51
+ return Path(str(resource))
@@ -0,0 +1,164 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from pathlib import Path
5
+
6
+
7
+ class YAMLParseError(ValueError):
8
+ """Raised when the lightweight YAML parser cannot parse a document."""
9
+
10
+
11
+ def load_yaml_file(path: str | Path) -> dict:
12
+ text = Path(path).read_text(encoding="utf-8")
13
+ return loads(text)
14
+
15
+
16
+ def loads(text: str):
17
+ lines = []
18
+ for number, raw in enumerate(text.splitlines(), start=1):
19
+ if not raw.strip() or raw.lstrip().startswith("#"):
20
+ continue
21
+ indent = len(raw) - len(raw.lstrip(" "))
22
+ lines.append((number, indent, raw.strip()))
23
+ if not lines:
24
+ return {}
25
+ value, index = _parse_block(lines, 0, lines[0][1])
26
+ if index != len(lines):
27
+ raise YAMLParseError(f"Unexpected trailing content near line {lines[index][0]}")
28
+ return value
29
+
30
+
31
+ def _parse_block(lines, index: int, indent: int):
32
+ if index >= len(lines):
33
+ return {}, index
34
+ if lines[index][1] != indent:
35
+ raise YAMLParseError(f"Unexpected indentation near line {lines[index][0]}")
36
+ if lines[index][2].startswith("- "):
37
+ return _parse_sequence(lines, index, indent)
38
+ return _parse_mapping(lines, index, indent)
39
+
40
+
41
+ def _parse_mapping(lines, index: int, indent: int):
42
+ mapping = {}
43
+ while index < len(lines):
44
+ line_no, line_indent, text = lines[index]
45
+ if line_indent < indent:
46
+ break
47
+ if line_indent > indent:
48
+ raise YAMLParseError(f"Unexpected indentation near line {line_no}")
49
+ if text.startswith("- "):
50
+ break
51
+ key, has_value, remainder = _split_key_value(text, line_no)
52
+ index += 1
53
+ if has_value:
54
+ mapping[key] = _parse_scalar(remainder)
55
+ continue
56
+ if index >= len(lines) or lines[index][1] <= indent:
57
+ mapping[key] = {}
58
+ continue
59
+ child, index = _parse_block(lines, index, lines[index][1])
60
+ mapping[key] = child
61
+ return mapping, index
62
+
63
+
64
+ def _parse_sequence(lines, index: int, indent: int):
65
+ items = []
66
+ while index < len(lines):
67
+ line_no, line_indent, text = lines[index]
68
+ if line_indent < indent:
69
+ break
70
+ if line_indent != indent or not text.startswith("- "):
71
+ break
72
+ content = text[2:].strip()
73
+ index += 1
74
+ if not content:
75
+ if index >= len(lines) or lines[index][1] <= indent:
76
+ items.append(None)
77
+ continue
78
+ child, index = _parse_block(lines, index, lines[index][1])
79
+ items.append(child)
80
+ continue
81
+ if _looks_like_mapping(content):
82
+ key, has_value, remainder = _split_key_value(content, line_no)
83
+ item = {}
84
+ if has_value:
85
+ item[key] = _parse_scalar(remainder)
86
+ else:
87
+ item[key] = {}
88
+ if index < len(lines) and lines[index][1] > indent:
89
+ child, index = _parse_block(lines, index, lines[index][1])
90
+ if not isinstance(child, dict):
91
+ raise YAMLParseError(f"List item near line {line_no} must contain a mapping")
92
+ item.update(child)
93
+ items.append(item)
94
+ continue
95
+ items.append(_parse_scalar(content))
96
+ return items, index
97
+
98
+
99
+ def _split_key_value(text: str, line_no: int):
100
+ if ":" not in text:
101
+ raise YAMLParseError(f"Expected ':' in mapping near line {line_no}")
102
+ key, remainder = text.split(":", 1)
103
+ key = key.strip()
104
+ if not key:
105
+ raise YAMLParseError(f"Empty key near line {line_no}")
106
+ remainder = remainder.strip()
107
+ return key, bool(remainder), remainder
108
+
109
+
110
+ def _looks_like_mapping(text: str) -> bool:
111
+ if text.startswith("[") or text.startswith("{"):
112
+ return False
113
+ return ":" in text
114
+
115
+
116
+ def _parse_scalar(value: str):
117
+ if value.startswith("[") and value.endswith("]"):
118
+ inner = value[1:-1].strip()
119
+ if not inner:
120
+ return []
121
+ return [_parse_scalar(part.strip()) for part in _split_flow_items(inner)]
122
+ if value.startswith(('"', "'")) and value.endswith(('"', "'")):
123
+ return value[1:-1]
124
+ lowered = value.lower()
125
+ if lowered == "true":
126
+ return True
127
+ if lowered == "false":
128
+ return False
129
+ if lowered in {"null", "none"}:
130
+ return None
131
+ if re.fullmatch(r"-?[0-9]+", value):
132
+ return int(value)
133
+ if re.fullmatch(r"-?[0-9]+\.[0-9]+", value):
134
+ return float(value)
135
+ return value
136
+
137
+
138
+ def _split_flow_items(text: str) -> list[str]:
139
+ items = []
140
+ current = []
141
+ quote = None
142
+ depth = 0
143
+ for char in text:
144
+ if quote:
145
+ current.append(char)
146
+ if char == quote:
147
+ quote = None
148
+ continue
149
+ if char in {'"', "'"}:
150
+ quote = char
151
+ current.append(char)
152
+ continue
153
+ if char in "[{":
154
+ depth += 1
155
+ elif char in "]}":
156
+ depth -= 1
157
+ if char == "," and depth == 0:
158
+ items.append("".join(current).strip())
159
+ current = []
160
+ continue
161
+ current.append(char)
162
+ if current:
163
+ items.append("".join(current).strip())
164
+ return items
pawly/memory/store.py ADDED
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+ from typing import Any
5
+
6
+ from pawly.memory.tiers import MemoryTier
7
+
8
+
9
+ class MemoryStore:
10
+ def __init__(self, mode: str = MemoryTier.NONE.value) -> None:
11
+ self.mode = MemoryTier(mode)
12
+ self._store: dict[str, list[dict[str, Any]]] = defaultdict(list)
13
+
14
+ def remember(self, category: str, payload: dict[str, Any]) -> None:
15
+ if self.mode == MemoryTier.NONE:
16
+ return
17
+ self._store[category].append(payload)
18
+
19
+ def recall(self, category: str) -> list[dict[str, Any]]:
20
+ return list(self._store.get(category, []))
21
+
22
+ def clear(self) -> None:
23
+ self._store.clear()
pawly/memory/tiers.py ADDED
@@ -0,0 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class MemoryTier(str, Enum):
7
+ NONE = "none"
8
+ SESSION_ONLY = "session-only"
9
+ BOUNDED_PERSISTENT = "bounded-persistent"
@@ -0,0 +1 @@
1
+ """Middleware helpers for the Pawly execution-boundary controller."""
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable
4
+
5
+
6
+ Hook = Callable[[dict], None]
7
+
8
+
9
+ class HookRegistry:
10
+ def __init__(self) -> None:
11
+ self.before_decision: list[Hook] = []
12
+ self.after_decision: list[Hook] = []
13
+
14
+ def run_before(self, payload: dict) -> None:
15
+ for hook in self.before_decision:
16
+ hook(payload)
17
+
18
+ def run_after(self, payload: dict) -> None:
19
+ for hook in self.after_decision:
20
+ hook(payload)