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/shield.py ADDED
@@ -0,0 +1,436 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field, replace
4
+ import re
5
+ from typing import Any
6
+
7
+ from pawly.action_selection import ActionCandidate
8
+ from pawly.contracts import Action, PolicyScore
9
+ from pawly.pawprint_loader import PawprintConfig
10
+
11
+ _EXTERNAL_WRITE_TERMS = frozenset({"delete", "email", "message", "post", "purchase", "send", "transfer", "update"})
12
+ _PROMPT_INJECTION_PATTERNS = (
13
+ r"ignore previous instructions",
14
+ r"reveal system prompt",
15
+ r"show hidden instructions",
16
+ r"dump tool schema",
17
+ r"bypass policy",
18
+ r"print hidden instructions",
19
+ r"show retrieved context",
20
+ )
21
+ _SECRET_PATTERNS = (
22
+ r"sk-[a-zA-Z0-9]{16,}",
23
+ r"api[_-]?key[\s:=]+[a-zA-Z0-9_\-]{8,}",
24
+ r"token[\s:=]+[a-zA-Z0-9_\-]{8,}",
25
+ r"-----BEGIN [A-Z ]+PRIVATE KEY-----",
26
+ )
27
+ _EMAIL_PATTERN = r"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b"
28
+ _PHONE_PATTERN = r"\b(?:\+?\d[\d .-]{7,}\d)\b"
29
+ _SYSTEM_LEAK_PATTERNS = (
30
+ r"system prompt",
31
+ r"developer instructions",
32
+ r"hidden instructions",
33
+ r"tool schema",
34
+ r"retrieved context",
35
+ )
36
+
37
+
38
+ @dataclass(slots=True)
39
+ class ShieldInputPolicy:
40
+ inspection: str
41
+ pii: str
42
+
43
+
44
+ @dataclass(slots=True)
45
+ class ShieldExecutionPolicy:
46
+ approval: str
47
+ rate_limit: str
48
+
49
+
50
+ @dataclass(slots=True)
51
+ class ShieldOutputPolicy:
52
+ inspection: str
53
+ pii: str
54
+ secret_handling: str
55
+ failure_mode: str
56
+
57
+
58
+ @dataclass(slots=True)
59
+ class ShieldTracePolicy:
60
+ input_storage: str
61
+ output_storage: str
62
+ cot_storage: str = "none"
63
+
64
+
65
+ @dataclass(slots=True)
66
+ class ShieldAuditPolicy:
67
+ mode: str
68
+
69
+
70
+ @dataclass(slots=True)
71
+ class ShieldEnvelope:
72
+ mode: str
73
+ level: str
74
+ handling: str
75
+ assets: list[str] = field(default_factory=list)
76
+ input: ShieldInputPolicy = field(default_factory=lambda: ShieldInputPolicy("basic", "off"))
77
+ execution: ShieldExecutionPolicy = field(default_factory=lambda: ShieldExecutionPolicy("high_risk", "default"))
78
+ output: ShieldOutputPolicy = field(default_factory=lambda: ShieldOutputPolicy("basic", "off", "redact", "redact"))
79
+ trace: ShieldTracePolicy = field(default_factory=lambda: ShieldTracePolicy("redacted", "redacted"))
80
+ audit: ShieldAuditPolicy = field(default_factory=lambda: ShieldAuditPolicy("standard"))
81
+
82
+ def to_dict(self) -> dict[str, Any]:
83
+ return {
84
+ "mode": self.mode,
85
+ "level": self.level,
86
+ "handling": self.handling,
87
+ "assets": list(self.assets),
88
+ "input": {"inspection": self.input.inspection, "pii": self.input.pii},
89
+ "execution": {"approval": self.execution.approval, "rate_limit": self.execution.rate_limit},
90
+ "output": {
91
+ "inspection": self.output.inspection,
92
+ "pii": self.output.pii,
93
+ "secret_handling": self.output.secret_handling,
94
+ "failure_mode": self.output.failure_mode,
95
+ },
96
+ "trace": {
97
+ "input_storage": self.trace.input_storage,
98
+ "output_storage": self.trace.output_storage,
99
+ "cot_storage": self.trace.cot_storage,
100
+ },
101
+ "audit": {"mode": self.audit.mode},
102
+ }
103
+
104
+
105
+ @dataclass(slots=True)
106
+ class OutputProtectionResult:
107
+ status: str
108
+ result: Any
109
+ reasons: list[str] = field(default_factory=list)
110
+ redactions: list[str] = field(default_factory=list)
111
+
112
+
113
+ class ShieldPolicy:
114
+ def envelope_for(self, pawprint: PawprintConfig) -> ShieldEnvelope:
115
+ config = pawprint.resolved_protection()
116
+ assets = list(config.assets)
117
+ envelope = _base_envelope(config.level, config.handling, assets)
118
+ envelope = _apply_handling_adjustments(envelope)
119
+ return envelope
120
+
121
+ def apply_to_candidate(
122
+ self,
123
+ candidate: ActionCandidate,
124
+ *,
125
+ pawprint: PawprintConfig,
126
+ state: dict[str, Any] | None = None,
127
+ ) -> tuple[str, ActionCandidate, list[str]]:
128
+ envelope = self.envelope_for(pawprint)
129
+ reasons: list[str] = []
130
+ action = candidate.action
131
+ risk_score = 1.0 if candidate.score.risk_score is None else float(candidate.score.risk_score)
132
+ action_name = action.name.strip().lower()
133
+ action_text = _flatten_value(action.arguments).lower()
134
+ actor_context = _actor_context(state)
135
+
136
+ requires_review = candidate.requires_review
137
+ boundary_type = candidate.boundary_type
138
+
139
+ is_external_write = "external_write" in envelope.assets and _looks_like_external_write(action_name)
140
+ if is_external_write and envelope.execution.approval in {"external_write", "always"}:
141
+ requires_review = True
142
+ boundary_type = "review"
143
+ reasons.append("external_write_requires_review")
144
+
145
+ if _contains_secret(action_text):
146
+ risk_score = max(risk_score, 0.92)
147
+ reasons.append("secret_like_argument_detected")
148
+ if envelope.level == "confidential":
149
+ return "block", _replace_candidate(candidate, boundary_type="blocked", requires_review=True, risk_score=risk_score, reasons=reasons), reasons
150
+ requires_review = True
151
+ boundary_type = "review"
152
+
153
+ if _contains_prompt_injection(action_text):
154
+ risk_score = max(risk_score, 0.8)
155
+ reasons.append("prompt_injection_pattern_detected")
156
+ if envelope.level in {"protected", "confidential"} or envelope.handling == "strict":
157
+ requires_review = True
158
+ boundary_type = "review"
159
+
160
+ if envelope.execution.approval == "high_risk" and risk_score >= 0.8:
161
+ requires_review = True
162
+ boundary_type = "review"
163
+ reasons.append("high_risk_requires_review")
164
+
165
+ if envelope.execution.approval == "always":
166
+ requires_review = True
167
+ boundary_type = "review"
168
+ reasons.append("always_requires_review")
169
+
170
+ if is_external_write and envelope.level in {"protected", "confidential"} and not actor_context["has_identity"]:
171
+ requires_review = True
172
+ boundary_type = "review"
173
+ reasons.append("missing_actor_context_requires_review")
174
+
175
+ if envelope.level == "confidential" and risk_score >= 0.9:
176
+ reasons.append("confidential_high_risk_requires_review")
177
+ requires_review = True
178
+ boundary_type = "review"
179
+
180
+ updated = _replace_candidate(candidate, boundary_type=boundary_type, requires_review=requires_review, risk_score=risk_score, reasons=reasons)
181
+ return ("review" if requires_review else "allow"), updated, reasons
182
+
183
+ def sanitize_action(self, action: Action, envelope: ShieldEnvelope) -> tuple[Action, list[str]]:
184
+ redactions: list[str] = []
185
+ updated_arguments = _sanitize_value(
186
+ action.arguments,
187
+ pii_mode=envelope.input.pii,
188
+ inspection_mode=envelope.input.inspection,
189
+ redactions=redactions,
190
+ )
191
+ if updated_arguments == action.arguments:
192
+ return action, redactions
193
+ return Action(name=action.name, arguments=updated_arguments, target=action.target), redactions
194
+
195
+ def protect_output(self, result: Any, envelope: ShieldEnvelope) -> OutputProtectionResult:
196
+ inspected = result
197
+ redactions: list[str] = []
198
+ reasons: list[str] = []
199
+
200
+ if envelope.output.pii == "redact":
201
+ inspected = _redact_pii_value(inspected, redactions)
202
+
203
+ detected_issue = False
204
+ if envelope.output.inspection != "off":
205
+ flattened = _flatten_value(inspected).lower()
206
+ if _contains_secret(flattened):
207
+ detected_issue = True
208
+ reasons.append("output_secret_detected")
209
+ if envelope.output.inspection == "strict" and _contains_system_leak(flattened):
210
+ detected_issue = True
211
+ reasons.append("output_system_leak_detected")
212
+
213
+ if detected_issue and envelope.output.secret_handling == "redact":
214
+ inspected = _redact_secret_value(inspected, redactions)
215
+ if detected_issue and envelope.output.secret_handling == "review":
216
+ return OutputProtectionResult(status="needs_review", result=inspected, reasons=reasons, redactions=redactions)
217
+ if detected_issue and envelope.output.secret_handling == "block":
218
+ return OutputProtectionResult(status="blocked", result=None, reasons=reasons, redactions=redactions)
219
+
220
+ if detected_issue and envelope.output.failure_mode == "review":
221
+ return OutputProtectionResult(status="needs_review", result=inspected, reasons=reasons, redactions=redactions)
222
+ if detected_issue and envelope.output.failure_mode == "block":
223
+ return OutputProtectionResult(status="blocked", result=None, reasons=reasons, redactions=redactions)
224
+
225
+ return OutputProtectionResult(status="completed", result=inspected, reasons=reasons, redactions=redactions)
226
+
227
+
228
+ def _base_envelope(level: str, handling: str, assets: list[str]) -> ShieldEnvelope:
229
+ pii_mode = "redact" if "customer_data" in assets else "off"
230
+ if level == "open":
231
+ envelope = ShieldEnvelope(
232
+ mode="opaque",
233
+ level="open",
234
+ handling=handling,
235
+ assets=assets,
236
+ input=ShieldInputPolicy("off", "off"),
237
+ execution=ShieldExecutionPolicy("none", "default"),
238
+ output=ShieldOutputPolicy("basic", "off", "redact", "redact"),
239
+ trace=ShieldTracePolicy("redacted", "redacted"),
240
+ audit=ShieldAuditPolicy("minimal"),
241
+ )
242
+ elif level == "protected":
243
+ envelope = ShieldEnvelope(
244
+ mode="opaque",
245
+ level="protected",
246
+ handling=handling,
247
+ assets=assets,
248
+ input=ShieldInputPolicy("basic", pii_mode),
249
+ execution=ShieldExecutionPolicy("external_write" if "external_write" in assets else "high_risk", "strict" if {"paid_api", "internal_workflow"} & set(assets) else "default"),
250
+ output=ShieldOutputPolicy("strict", pii_mode, "review", "review" if "external_write" in assets else "redact"),
251
+ trace=ShieldTracePolicy("summary" if {"customer_data", "private_knowledge"} & set(assets) else "redacted", "summary" if {"customer_data", "private_knowledge"} & set(assets) else "redacted"),
252
+ audit=ShieldAuditPolicy("detailed"),
253
+ )
254
+ elif level == "confidential":
255
+ envelope = ShieldEnvelope(
256
+ mode="opaque",
257
+ level="confidential",
258
+ handling=handling,
259
+ assets=assets,
260
+ input=ShieldInputPolicy("strict", "redact"),
261
+ execution=ShieldExecutionPolicy("external_write" if "external_write" in assets else "high_risk", "strict"),
262
+ output=ShieldOutputPolicy("strict", "redact", "block", "review"),
263
+ trace=ShieldTracePolicy("summary", "summary"),
264
+ audit=ShieldAuditPolicy("detailed"),
265
+ )
266
+ else:
267
+ envelope = ShieldEnvelope(
268
+ mode="opaque",
269
+ level="standard",
270
+ handling=handling,
271
+ assets=assets,
272
+ input=ShieldInputPolicy("basic", pii_mode),
273
+ execution=ShieldExecutionPolicy("high_risk", "default"),
274
+ output=ShieldOutputPolicy("basic", pii_mode, "redact", "redact"),
275
+ trace=ShieldTracePolicy("redacted", "redacted"),
276
+ audit=ShieldAuditPolicy("standard"),
277
+ )
278
+ return envelope
279
+
280
+
281
+ def _apply_handling_adjustments(envelope: ShieldEnvelope) -> ShieldEnvelope:
282
+ assets = set(envelope.assets)
283
+ trace_input = envelope.trace.input_storage
284
+ trace_output = envelope.trace.output_storage
285
+ output_failure = envelope.output.failure_mode
286
+ execution_approval = envelope.execution.approval
287
+ audit_mode = envelope.audit.mode
288
+ input_inspection = envelope.input.inspection
289
+ output_inspection = envelope.output.inspection
290
+
291
+ if envelope.handling == "cautious":
292
+ trace_input = "summary" if trace_input == "raw" else trace_input
293
+ trace_output = "summary" if trace_output == "raw" else trace_output
294
+ if output_failure == "allow":
295
+ output_failure = "redact"
296
+ if output_failure == "redact" and "external_write" in assets:
297
+ output_failure = "review"
298
+ if "external_write" in assets:
299
+ execution_approval = "external_write"
300
+ elif envelope.handling == "strict":
301
+ input_inspection = "strict"
302
+ output_inspection = "strict"
303
+ trace_input = "summary" if trace_input not in {"summary", "none"} else trace_input
304
+ trace_output = "summary" if trace_output not in {"summary", "none"} else trace_output
305
+ if envelope.level == "confidential" and "external_write" in assets:
306
+ execution_approval = "always"
307
+ if envelope.level != "open":
308
+ audit_mode = "detailed"
309
+
310
+ return ShieldEnvelope(
311
+ mode=envelope.mode,
312
+ level=envelope.level,
313
+ handling=envelope.handling,
314
+ assets=list(envelope.assets),
315
+ input=ShieldInputPolicy(input_inspection, envelope.input.pii),
316
+ execution=ShieldExecutionPolicy(execution_approval, envelope.execution.rate_limit),
317
+ output=ShieldOutputPolicy(output_inspection, envelope.output.pii, envelope.output.secret_handling, output_failure),
318
+ trace=ShieldTracePolicy(trace_input, trace_output, envelope.trace.cot_storage),
319
+ audit=ShieldAuditPolicy(audit_mode),
320
+ )
321
+
322
+
323
+ def _replace_candidate(candidate: ActionCandidate, *, boundary_type: str, requires_review: bool, risk_score: float, reasons: list[str]) -> ActionCandidate:
324
+ score = PolicyScore(
325
+ risk_score=round(min(1.0, max(0.0, risk_score)), 4),
326
+ reason_codes=list(dict.fromkeys([*candidate.score.reason_codes, *reasons])),
327
+ matched_rules=list(candidate.score.matched_rules),
328
+ audit_tags=list(dict.fromkeys([*candidate.score.audit_tags, *[f"shield:{reason}" for reason in reasons]])),
329
+ uncertainty=candidate.score.uncertainty,
330
+ )
331
+ reason = reasons[0] if reasons else candidate.reason
332
+ return replace(candidate, boundary_type=boundary_type, requires_review=requires_review, score=score, reason=reason)
333
+
334
+
335
+ def _looks_like_external_write(action_name: str) -> bool:
336
+ tokens = set(action_name.replace("-", " ").replace("_", " ").split())
337
+ return bool(tokens & _EXTERNAL_WRITE_TERMS)
338
+
339
+
340
+ def _contains_prompt_injection(text: str) -> bool:
341
+ return any(re.search(pattern, text, flags=re.IGNORECASE) for pattern in _PROMPT_INJECTION_PATTERNS)
342
+
343
+
344
+ def _contains_secret(text: str) -> bool:
345
+ return any(re.search(pattern, text, flags=re.IGNORECASE) for pattern in _SECRET_PATTERNS)
346
+
347
+
348
+ def _contains_system_leak(text: str) -> bool:
349
+ return any(re.search(pattern, text, flags=re.IGNORECASE) for pattern in _SYSTEM_LEAK_PATTERNS)
350
+
351
+
352
+ def _sanitize_value(value: Any, *, pii_mode: str, inspection_mode: str, redactions: list[str]) -> Any:
353
+ if isinstance(value, dict):
354
+ return {key: _sanitize_value(item, pii_mode=pii_mode, inspection_mode=inspection_mode, redactions=redactions) for key, item in value.items()}
355
+ if isinstance(value, list):
356
+ return [_sanitize_value(item, pii_mode=pii_mode, inspection_mode=inspection_mode, redactions=redactions) for item in value]
357
+ if not isinstance(value, str):
358
+ return value
359
+ result = value
360
+ if pii_mode == "redact":
361
+ updated = re.sub(_EMAIL_PATTERN, "[redacted-email]", result, flags=re.IGNORECASE)
362
+ if updated != result:
363
+ redactions.append("input_email_redacted")
364
+ result = updated
365
+ updated = re.sub(_PHONE_PATTERN, "[redacted-phone]", result, flags=re.IGNORECASE)
366
+ if updated != result:
367
+ redactions.append("input_phone_redacted")
368
+ result = updated
369
+ updated = re.sub(r"\b(?:sk-[A-Za-z0-9]{16,}|[A-Za-z0-9_\-]{24,})\b", "[redacted-token]", result)
370
+ if updated != result:
371
+ redactions.append("input_token_redacted")
372
+ result = updated
373
+ if inspection_mode in {"basic", "strict"} and _contains_prompt_injection(result):
374
+ redactions.append("input_prompt_injection_detected")
375
+ return result
376
+
377
+
378
+ def _redact_pii_value(value: Any, redactions: list[str]) -> Any:
379
+ if isinstance(value, dict):
380
+ return {key: _redact_pii_value(item, redactions) for key, item in value.items()}
381
+ if isinstance(value, list):
382
+ return [_redact_pii_value(item, redactions) for item in value]
383
+ if not isinstance(value, str):
384
+ return value
385
+ updated = re.sub(_EMAIL_PATTERN, "[redacted-email]", value, flags=re.IGNORECASE)
386
+ if updated != value:
387
+ redactions.append("output_email_redacted")
388
+ value = updated
389
+ updated = re.sub(_PHONE_PATTERN, "[redacted-phone]", value, flags=re.IGNORECASE)
390
+ if updated != value:
391
+ redactions.append("output_phone_redacted")
392
+ return updated
393
+
394
+
395
+ def _redact_secret_value(value: Any, redactions: list[str]) -> Any:
396
+ if isinstance(value, dict):
397
+ return {key: _redact_secret_value(item, redactions) for key, item in value.items()}
398
+ if isinstance(value, list):
399
+ return [_redact_secret_value(item, redactions) for item in value]
400
+ if not isinstance(value, str):
401
+ return value
402
+ result = value
403
+ for pattern in _SECRET_PATTERNS:
404
+ updated = re.sub(pattern, "[redacted-secret]", result, flags=re.IGNORECASE)
405
+ if updated != result:
406
+ redactions.append("output_secret_redacted")
407
+ result = updated
408
+ return result
409
+
410
+
411
+ def _flatten_value(value: Any) -> str:
412
+ if value is None:
413
+ return ""
414
+ if isinstance(value, str):
415
+ return value
416
+ if isinstance(value, (int, float, bool)):
417
+ return str(value)
418
+ if isinstance(value, dict):
419
+ return " ".join(_flatten_value(item) for item in value.values())
420
+ if isinstance(value, list):
421
+ return " ".join(_flatten_value(item) for item in value)
422
+ return str(value)
423
+
424
+
425
+ def _actor_context(state: dict[str, Any] | None) -> dict[str, bool]:
426
+ if not isinstance(state, dict):
427
+ return {"has_identity": False}
428
+ actor = state.get("actor")
429
+ if not isinstance(actor, dict):
430
+ return {"has_identity": False}
431
+ tenant_id = str(actor.get("tenant_id", "")).strip()
432
+ agent_id = str(actor.get("agent_id", "")).strip()
433
+ user_id = str(actor.get("user_id", "")).strip()
434
+ return {
435
+ "has_identity": bool(tenant_id or agent_id or user_id),
436
+ }
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any
5
+
6
+ from pawly.contracts import Action
7
+
8
+
9
+ SkillHandler = Callable[[dict[str, Any], dict[str, Any]], Any]
10
+
11
+
12
+ class MissingSkillRegistryError(RuntimeError):
13
+ """Raised when run_actions is used before a skill registry is registered."""
14
+
15
+
16
+ class SkillRegistry:
17
+ def __init__(self) -> None:
18
+ self._handlers: dict[str, SkillHandler] = {}
19
+
20
+ def register(self, action_name: str, handler: SkillHandler) -> None:
21
+ normalized = str(action_name).strip()
22
+ if not normalized:
23
+ raise ValueError("action_name must not be empty")
24
+ self._handlers[normalized] = handler
25
+
26
+ def execute(self, action: Action, context: dict[str, Any] | None = None) -> Any:
27
+ handler = self._handlers.get(action.name)
28
+ if handler is None:
29
+ raise KeyError(f"no skill registered for action: {action.name}")
30
+ return handler(dict(action.arguments), dict(context or {}))
31
+
pawly/task_request.py ADDED
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+ from pawly.contracts import Action, Intent, IntentSource
7
+
8
+
9
+ @dataclass(slots=True)
10
+ class TaskRequest:
11
+ task: str
12
+ action: str
13
+ confidence: float
14
+ metadata: dict[str, Any] = field(default_factory=dict)
15
+
16
+ def to_intent(self, *, intent_id: str = "intent-from-task-request") -> Intent:
17
+ return Intent(
18
+ intent_id=intent_id,
19
+ source=IntentSource.EXECUTION_REQUEST,
20
+ action=Action(
21
+ name=self.action,
22
+ arguments={"task": self.task, **self.metadata},
23
+ ),
24
+ summary=self.task,
25
+ confidence=self.confidence,
26
+ metadata=self.metadata,
27
+ )
pawly/types.py ADDED
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from pawly.contracts import Action
4
+ from pawly.contracts import Decision
5
+ from pawly.contracts import DecisionSource
6
+ from pawly.contracts import DecisionState
7
+ from pawly.contracts import Intent
8
+ from pawly.contracts import IntentSource
9
+ from pawly.policy_engine.models import PolicyEvaluation
10
+ from pawly.task_request import TaskRequest
11
+
12
+
13
+ DecisionType = DecisionState
14
+
15
+ IntentAction = Action