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
pawly/audit/diff.py ADDED
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ def diff_actions(original: dict[str, Any] | None, executed: dict[str, Any] | None) -> dict[str, Any]:
7
+ original = original or {}
8
+ executed = executed or {}
9
+ changed_fields: list[dict[str, Any]] = []
10
+ for key in sorted(set(original) | set(executed)):
11
+ if original.get(key) != executed.get(key):
12
+ changed_fields.append(
13
+ {
14
+ "field": key,
15
+ "original": original.get(key),
16
+ "executed": executed.get(key),
17
+ }
18
+ )
19
+ return {
20
+ "changed": bool(changed_fields),
21
+ "changed_fields": changed_fields,
22
+ "original_action": original,
23
+ "executed_action": executed,
24
+ }
pawly/audit/events.py ADDED
@@ -0,0 +1,156 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, asdict
4
+ from datetime import datetime, timezone
5
+ from typing import Any
6
+
7
+
8
+ @dataclass(slots=True)
9
+ class AuditEvent:
10
+ event_id: str
11
+ event_type: str
12
+ timestamp: str
13
+ agent_id: str
14
+ pawprint_version: str
15
+ decision_id: str
16
+ outcome: str
17
+ action: dict[str, Any]
18
+ original_intent: dict[str, Any]
19
+ normalized_intent: dict[str, Any]
20
+ policy_evaluation: dict[str, Any]
21
+ runtime_overlays: dict[str, Any]
22
+ policy_references: list[str]
23
+ matched_policy_rules: list[str]
24
+ final_decision: dict[str, Any]
25
+ reason_codes: list[str]
26
+ approval: dict[str, Any] | None = None
27
+ executed_action: dict[str, Any] | None = None
28
+ action_diff: dict[str, Any] | None = None
29
+ execution: dict[str, Any] | None = None
30
+ execution_result_ref: str | None = None
31
+ risk_score: float | None = None
32
+ request_id: str | None = None
33
+ escalated_to: str | None = None
34
+ tenant_id: str | None = None
35
+ user_id: str | None = None
36
+ protection_level: str | None = None
37
+ protection_handling: str | None = None
38
+ protection_assets: list[str] | None = None
39
+ action_argument_summary: dict[str, Any] | None = None
40
+ output_summary: Any | None = None
41
+ error_summary: str | None = None
42
+ redactions_applied: list[str] | None = None
43
+
44
+ @classmethod
45
+ def from_decision(
46
+ cls,
47
+ *,
48
+ event_id: str,
49
+ decision_id: str,
50
+ agent_id: str,
51
+ pawprint_version: str,
52
+ outcome: str,
53
+ original_intent: dict[str, Any],
54
+ normalized_intent: dict[str, Any],
55
+ action_name: str,
56
+ policy_evaluation: dict[str, Any],
57
+ runtime_overlays: dict[str, Any],
58
+ policy_references: list[str],
59
+ final_decision: dict[str, Any],
60
+ reason_codes: list[str],
61
+ risk_score: float | None,
62
+ escalated_to: str | None,
63
+ ) -> "AuditEvent":
64
+ return cls(
65
+ event_id=event_id,
66
+ event_type="action-proposed",
67
+ timestamp=datetime.now(timezone.utc).isoformat(),
68
+ agent_id=agent_id,
69
+ pawprint_version=pawprint_version,
70
+ decision_id=decision_id,
71
+ outcome=outcome,
72
+ action={"name": action_name, "approved": outcome == "allow"},
73
+ original_intent=original_intent,
74
+ normalized_intent=normalized_intent,
75
+ policy_evaluation=policy_evaluation,
76
+ runtime_overlays=runtime_overlays,
77
+ policy_references=policy_references,
78
+ matched_policy_rules=list(policy_references),
79
+ final_decision=final_decision,
80
+ reason_codes=reason_codes,
81
+ risk_score=risk_score,
82
+ escalated_to=escalated_to,
83
+ redactions_applied=[],
84
+ )
85
+
86
+ @classmethod
87
+ def from_governed_execution(
88
+ cls,
89
+ *,
90
+ event_id: str,
91
+ decision_id: str,
92
+ agent_id: str,
93
+ pawprint_version: str,
94
+ outcome: str,
95
+ original_intent: dict[str, Any],
96
+ normalized_intent: dict[str, Any],
97
+ action: dict[str, Any],
98
+ policy_evaluation: dict[str, Any],
99
+ runtime_overlays: dict[str, Any],
100
+ policy_references: list[str],
101
+ final_decision: dict[str, Any],
102
+ reason_codes: list[str],
103
+ approval: dict[str, Any] | None,
104
+ executed_action: dict[str, Any] | None,
105
+ action_diff: dict[str, Any] | None,
106
+ execution: dict[str, Any],
107
+ execution_result_ref: str | None,
108
+ risk_score: float | None,
109
+ escalated_to: str | None,
110
+ tenant_id: str | None = None,
111
+ user_id: str | None = None,
112
+ protection_level: str | None = None,
113
+ protection_handling: str | None = None,
114
+ protection_assets: list[str] | None = None,
115
+ action_argument_summary: dict[str, Any] | None = None,
116
+ output_summary: Any | None = None,
117
+ error_summary: str | None = None,
118
+ ) -> "AuditEvent":
119
+ return cls(
120
+ event_id=event_id,
121
+ event_type="governed-execution",
122
+ timestamp=datetime.now(timezone.utc).isoformat(),
123
+ agent_id=agent_id,
124
+ pawprint_version=pawprint_version,
125
+ decision_id=decision_id,
126
+ outcome=outcome,
127
+ action=action,
128
+ original_intent=original_intent,
129
+ normalized_intent=normalized_intent,
130
+ policy_evaluation=policy_evaluation,
131
+ runtime_overlays=runtime_overlays,
132
+ policy_references=policy_references,
133
+ matched_policy_rules=list(policy_references),
134
+ final_decision=final_decision,
135
+ reason_codes=reason_codes,
136
+ approval=approval,
137
+ executed_action=executed_action,
138
+ action_diff=action_diff,
139
+ execution=execution,
140
+ execution_result_ref=execution_result_ref,
141
+ risk_score=risk_score,
142
+ escalated_to=escalated_to,
143
+ tenant_id=tenant_id,
144
+ user_id=user_id,
145
+ protection_level=protection_level,
146
+ protection_handling=protection_handling,
147
+ protection_assets=protection_assets,
148
+ action_argument_summary=action_argument_summary,
149
+ output_summary=output_summary,
150
+ error_summary=error_summary,
151
+ redactions_applied=[],
152
+ )
153
+
154
+ def to_dict(self) -> dict[str, Any]:
155
+ payload = asdict(self)
156
+ return {key: value for key, value in payload.items() if value is not None}
pawly/audit/ledger.py ADDED
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from pawly.audit.events import AuditEvent
7
+
8
+
9
+ class AuditLedger:
10
+ def __init__(self, path: str | Path | None = None) -> None:
11
+ self.path = Path(path) if path else None
12
+ self.events: list[dict] = []
13
+
14
+ def append(self, event: AuditEvent) -> dict:
15
+ payload = event.to_dict()
16
+ self.events.append(payload)
17
+ if self.path:
18
+ self.path.parent.mkdir(parents=True, exist_ok=True)
19
+ with self.path.open("a", encoding="utf-8") as handle:
20
+ handle.write(json.dumps(payload, sort_keys=True) + "\n")
21
+ return payload
22
+
23
+ def load_events(self) -> list[dict]:
24
+ if self.path and self.path.exists():
25
+ lines = self.path.read_text(encoding="utf-8").splitlines()
26
+ return [json.loads(line) for line in lines if line.strip()]
27
+ return list(self.events)
28
+
29
+ def find_event(self, *, event_id: str | None = None, decision_id: str | None = None, event_type: str | None = None) -> dict | None:
30
+ events = self.load_events()
31
+ for event in reversed(events):
32
+ if event_id is not None and event.get("event_id") != event_id:
33
+ continue
34
+ if decision_id is not None and event.get("decision_id") != decision_id:
35
+ continue
36
+ if event_type is not None and event.get("event_type") != event_type:
37
+ continue
38
+ return event
39
+ return None
pawly/audit/replay.py ADDED
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from pawly.audit.diff import diff_actions
7
+ from pawly.audit.ledger import AuditLedger
8
+
9
+
10
+ def load_audit_record(
11
+ path: str | Path,
12
+ *,
13
+ event_id: str | None = None,
14
+ decision_id: str | None = None,
15
+ event_type: str = "governed-execution",
16
+ ) -> dict[str, Any]:
17
+ ledger = AuditLedger(path)
18
+ record = ledger.find_event(event_id=event_id, decision_id=decision_id, event_type=event_type)
19
+ if record is None:
20
+ raise KeyError(event_id or decision_id or event_type)
21
+ return record
22
+
23
+
24
+ def replay_governed_path(record: dict[str, Any]) -> dict[str, Any]:
25
+ original_action = record.get("action", {})
26
+ executed_action = record.get("executed_action")
27
+ action_diff = record.get("action_diff") or diff_actions(original_action, executed_action)
28
+ return {
29
+ "event_id": record.get("event_id"),
30
+ "decision_id": record.get("decision_id"),
31
+ "agent_id": record.get("agent_id"),
32
+ "original_intent": record.get("original_intent"),
33
+ "normalized_intent": record.get("normalized_intent"),
34
+ "policy": {
35
+ "matched_rules": record.get("matched_policy_rules", record.get("policy_references", [])),
36
+ "risk_score": record.get("risk_score"),
37
+ "policy_evaluation": record.get("policy_evaluation"),
38
+ "final_decision": record.get("final_decision"),
39
+ },
40
+ "approval": record.get("approval"),
41
+ "execution": record.get("execution"),
42
+ "original_action": original_action,
43
+ "executed_action": executed_action,
44
+ "action_diff": action_diff,
45
+ }
@@ -0,0 +1,39 @@
1
+ from .approval import ApprovalBackend, CloudApprovalBackendStub, LocalApprovalBackend
2
+ from .audit import AuditSink, CloudAuditSinkStub, CompositeAuditSink, HostedActionSyncAuditSink, LocalAuditSink, build_default_audit_sink
3
+ from .reviewer import (
4
+ CloudReviewer,
5
+ CloudReviewerStub,
6
+ ReviewerPolicy,
7
+ ReviewerBackend,
8
+ RulePolicy,
9
+ RuleReviewer,
10
+ evaluate_reviewer_policy,
11
+ resolve_policy,
12
+ resolve_reviewer_backend,
13
+ )
14
+ from .risk import AdvancedRiskProviderStub, LocalRiskProvider, RiskContext, RiskProvider
15
+
16
+ __all__ = [
17
+ "AdvancedRiskProviderStub",
18
+ "ApprovalBackend",
19
+ "AuditSink",
20
+ "CloudReviewer",
21
+ "CloudApprovalBackendStub",
22
+ "CloudAuditSinkStub",
23
+ "CloudReviewerStub",
24
+ "CompositeAuditSink",
25
+ "evaluate_reviewer_policy",
26
+ "HostedActionSyncAuditSink",
27
+ "LocalApprovalBackend",
28
+ "LocalAuditSink",
29
+ "LocalRiskProvider",
30
+ "ReviewerPolicy",
31
+ "ReviewerBackend",
32
+ "RiskContext",
33
+ "RiskProvider",
34
+ "RulePolicy",
35
+ "RuleReviewer",
36
+ "resolve_policy",
37
+ "resolve_reviewer_backend",
38
+ "build_default_audit_sink",
39
+ ]
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Protocol
4
+
5
+ from pawly.contracts import Decision
6
+ from pawly.approval.handler import ApprovalHandler
7
+ from pawly.approval.models import ApprovalRecord, ApprovalResponse
8
+ from pawly.approval.queue import ApprovalQueue, InMemoryApprovalQueue
9
+ from pawly.approval.router import ApprovalRouter
10
+ from pawly.types import Intent
11
+
12
+
13
+ class ApprovalBackend(Protocol):
14
+ name: str
15
+
16
+ def submit(self, intent: Intent, decision: Decision) -> ApprovalRecord:
17
+ ...
18
+
19
+ def get(self, record_id: str) -> ApprovalRecord:
20
+ ...
21
+
22
+ def apply_response(self, record_id: str, response: ApprovalResponse) -> ApprovalRecord:
23
+ ...
24
+
25
+ def expire_due(self) -> list[ApprovalRecord]:
26
+ ...
27
+
28
+ def status_payload(self, record: ApprovalRecord) -> dict[str, Any]:
29
+ ...
30
+
31
+
32
+ class LocalApprovalBackend:
33
+ name = "local-approval"
34
+
35
+ def __init__(
36
+ self,
37
+ *,
38
+ queue: ApprovalQueue | None = None,
39
+ handler: ApprovalHandler | None = None,
40
+ timeout_seconds: int | None = 300,
41
+ router: ApprovalRouter | None = None,
42
+ ) -> None:
43
+ self.router = router or ApprovalRouter(
44
+ queue=queue or InMemoryApprovalQueue(),
45
+ handler=handler,
46
+ timeout_seconds=timeout_seconds,
47
+ )
48
+
49
+ def submit(self, intent: Intent, decision: Decision) -> ApprovalRecord:
50
+ return self.router.submit(intent, decision)
51
+
52
+ def get(self, record_id: str) -> ApprovalRecord:
53
+ return self.router.get(record_id)
54
+
55
+ def apply_response(self, record_id: str, response: ApprovalResponse) -> ApprovalRecord:
56
+ return self.router.apply_response(record_id, response)
57
+
58
+ def expire_due(self) -> list[ApprovalRecord]:
59
+ return self.router.expire_due()
60
+
61
+ def status_payload(self, record: ApprovalRecord) -> dict[str, Any]:
62
+ return self.router.status_payload(record)
63
+
64
+
65
+ class CloudApprovalBackendStub:
66
+ name = "cloud-approval"
67
+
68
+ def submit(self, intent: Intent, decision: Decision) -> ApprovalRecord:
69
+ del intent, decision
70
+ raise NotImplementedError("Cloud approval backends are optional extensions and are not part of the OSS runtime path.")
71
+
72
+ def get(self, record_id: str) -> ApprovalRecord:
73
+ del record_id
74
+ raise NotImplementedError("Cloud approval backends are optional extensions and are not part of the OSS runtime path.")
75
+
76
+ def apply_response(self, record_id: str, response: ApprovalResponse) -> ApprovalRecord:
77
+ del record_id, response
78
+ raise NotImplementedError("Cloud approval backends are optional extensions and are not part of the OSS runtime path.")
79
+
80
+ def expire_due(self) -> list[ApprovalRecord]:
81
+ raise NotImplementedError("Cloud approval backends are optional extensions and are not part of the OSS runtime path.")
82
+
83
+ def status_payload(self, record: ApprovalRecord) -> dict[str, Any]:
84
+ del record
85
+ raise NotImplementedError("Cloud approval backends are optional extensions and are not part of the OSS runtime path.")
@@ -0,0 +1,189 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Any, Protocol
7
+ from urllib import error, request
8
+
9
+ from pawly.audit.events import AuditEvent
10
+ from pawly.audit.ledger import AuditLedger
11
+ from pawly.loader.yaml_loader import load_yaml_file
12
+
13
+ DEFAULT_PAWLY_CLOUD_BASE_URL = "https://api.pawly.dev"
14
+ PAWLY_CLOUD_BASE_URL_ENV = "PAWLY_CLOUD_BASE_URL"
15
+ PAWLY_AUTH_PATH_ENV = "PAWLY_AUTH_PATH"
16
+
17
+
18
+ class AuditSink(Protocol):
19
+ name: str
20
+
21
+ def append(self, event: AuditEvent) -> dict[str, Any]:
22
+ ...
23
+
24
+ def load_events(self) -> list[dict[str, Any]]:
25
+ ...
26
+
27
+ def find_event(self, *, event_id: str | None = None, decision_id: str | None = None, event_type: str | None = None) -> dict[str, Any] | None:
28
+ ...
29
+
30
+
31
+ class LocalAuditSink:
32
+ name = "local-jsonl"
33
+
34
+ def __init__(self, path: str | Path | None = None) -> None:
35
+ self.ledger = AuditLedger(path)
36
+
37
+ def append(self, event: AuditEvent) -> dict[str, Any]:
38
+ return self.ledger.append(event)
39
+
40
+ def load_events(self) -> list[dict[str, Any]]:
41
+ return self.ledger.load_events()
42
+
43
+ def find_event(self, *, event_id: str | None = None, decision_id: str | None = None, event_type: str | None = None) -> dict[str, Any] | None:
44
+ return self.ledger.find_event(event_id=event_id, decision_id=decision_id, event_type=event_type)
45
+
46
+
47
+ class CompositeAuditSink:
48
+ name = "composite-audit"
49
+
50
+ def __init__(self, sinks: list[AuditSink]) -> None:
51
+ if not sinks:
52
+ raise ValueError("CompositeAuditSink requires at least one sink")
53
+ self.sinks = sinks
54
+ self.ledger = getattr(sinks[0], "ledger", sinks[0])
55
+
56
+ def append(self, event: AuditEvent) -> dict[str, Any]:
57
+ payload: dict[str, Any] | None = None
58
+ for index, sink in enumerate(self.sinks):
59
+ result = sink.append(event)
60
+ if index == 0:
61
+ payload = result
62
+ return payload or event.to_dict()
63
+
64
+ def load_events(self) -> list[dict[str, Any]]:
65
+ return self.sinks[0].load_events()
66
+
67
+ def find_event(self, *, event_id: str | None = None, decision_id: str | None = None, event_type: str | None = None) -> dict[str, Any] | None:
68
+ return self.sinks[0].find_event(event_id=event_id, decision_id=decision_id, event_type=event_type)
69
+
70
+
71
+ class HostedActionSyncAuditSink:
72
+ name = "cloud-action-sync"
73
+
74
+ def __init__(
75
+ self,
76
+ *,
77
+ base_url: str,
78
+ api_key: str,
79
+ timeout: float = 5.0,
80
+ ) -> None:
81
+ self.base_url = base_url.rstrip("/")
82
+ self.api_key = api_key
83
+ self.timeout = timeout
84
+
85
+ def append(self, event: AuditEvent) -> dict[str, Any]:
86
+ payload = event.to_dict()
87
+ body = json.dumps(_action_ingest_payload(payload)).encode("utf-8")
88
+ req = request.Request(
89
+ f"{self.base_url}/v1/actions:ingest",
90
+ data=body,
91
+ headers={
92
+ "Authorization": f"Bearer {self.api_key}",
93
+ "Content-Type": "application/json",
94
+ },
95
+ method="POST",
96
+ )
97
+ try:
98
+ with request.urlopen(req, timeout=self.timeout) as response:
99
+ response.read()
100
+ except (OSError, error.URLError, error.HTTPError) as exc:
101
+ raise RuntimeError(f"cloud action sync failed: {exc}") from exc
102
+ return payload
103
+
104
+ def load_events(self) -> list[dict[str, Any]]:
105
+ raise NotImplementedError("Hosted action sync is write-only from the OSS runtime path.")
106
+
107
+ def find_event(self, *, event_id: str | None = None, decision_id: str | None = None, event_type: str | None = None) -> dict[str, Any] | None:
108
+ del event_id, decision_id, event_type
109
+ raise NotImplementedError("Hosted action sync is write-only from the OSS runtime path.")
110
+
111
+
112
+ class CloudAuditSinkStub:
113
+ name = "cloud-audit"
114
+
115
+ def append(self, event: AuditEvent) -> dict[str, Any]:
116
+ del event
117
+ raise NotImplementedError("Cloud audit sinks are optional extensions and are not part of the OSS runtime path.")
118
+
119
+ def load_events(self) -> list[dict[str, Any]]:
120
+ raise NotImplementedError("Cloud audit sinks are optional extensions and are not part of the OSS runtime path.")
121
+
122
+ def find_event(self, *, event_id: str | None = None, decision_id: str | None = None, event_type: str | None = None) -> dict[str, Any] | None:
123
+ del event_id, decision_id, event_type
124
+ raise NotImplementedError("Cloud audit sinks are optional extensions and are not part of the OSS runtime path.")
125
+
126
+
127
+ def build_default_audit_sink(audit_path: str | Path | None = None) -> AuditSink:
128
+ local = LocalAuditSink(audit_path)
129
+ hosted = _build_hosted_action_sync()
130
+ if hosted is None:
131
+ return local
132
+ return CompositeAuditSink([local, hosted])
133
+
134
+
135
+ def _build_hosted_action_sync() -> HostedActionSyncAuditSink | None:
136
+ auth = _load_pawly_auth()
137
+ if auth is None:
138
+ return None
139
+ api_key = str(auth.get("api_key", "")).strip()
140
+ if not api_key:
141
+ return None
142
+ return HostedActionSyncAuditSink(
143
+ base_url=_resolve_pawly_cloud_base_url(),
144
+ api_key=api_key,
145
+ )
146
+
147
+
148
+ def _action_ingest_payload(event: dict[str, Any]) -> dict[str, Any]:
149
+ return {
150
+ "event_id": event.get("event_id"),
151
+ "decision_id": event.get("decision_id"),
152
+ "timestamp": event.get("timestamp"),
153
+ "event_type": event.get("event_type"),
154
+ "outcome": event.get("outcome"),
155
+ "agent_id": event.get("agent_id"),
156
+ "action": dict(event.get("executed_action") or event.get("action") or {}),
157
+ "executed_action": event.get("executed_action"),
158
+ "risk_score": event.get("risk_score"),
159
+ "request_id": event.get("request_id"),
160
+ "metadata": {
161
+ "reason_codes": event.get("reason_codes") or [],
162
+ "policy_references": event.get("policy_references") or [],
163
+ "execution_result_ref": event.get("execution_result_ref"),
164
+ },
165
+ }
166
+
167
+
168
+ def _load_pawly_auth() -> dict[str, Any] | None:
169
+ path = _pawly_auth_path()
170
+ if not path.exists() or not path.is_file():
171
+ return None
172
+ if path.suffix.lower() == ".json":
173
+ payload = json.loads(path.read_text(encoding="utf-8"))
174
+ else:
175
+ payload = load_yaml_file(path)
176
+ if isinstance(payload, dict):
177
+ return payload
178
+ return None
179
+
180
+
181
+ def _pawly_auth_path() -> Path:
182
+ explicit = os.environ.get(PAWLY_AUTH_PATH_ENV, "").strip()
183
+ if explicit:
184
+ return Path(explicit)
185
+ return Path.home() / ".pawly" / "pawly_auth.yaml"
186
+
187
+
188
+ def _resolve_pawly_cloud_base_url() -> str:
189
+ return os.environ.get(PAWLY_CLOUD_BASE_URL_ENV, "").strip() or DEFAULT_PAWLY_CLOUD_BASE_URL