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/runtime.py ADDED
@@ -0,0 +1,1004 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import re
5
+ from collections.abc import Mapping, Sequence
6
+ from dataclasses import replace
7
+ from typing import Any
8
+
9
+ from pawly.action_selection import ActionCandidate, ActionDecision
10
+ from pawly.approval.models import ApprovalStatus
11
+ from pawly.backends.approval import ApprovalBackend
12
+ from pawly.audit.events import AuditEvent
13
+ from pawly.contracts import Action, Decision, DecisionState, PolicyScore
14
+ from pawly.backends.audit import AuditSink
15
+ from pawly.backends.reviewer import ReviewerPolicy
16
+ from pawly.backends.risk import RiskProvider
17
+ from pawly.budget.state import BudgetState
18
+ from pawly.loader.schema_loader import load_pawprint_version
19
+ from pawly.pawprint_loader import PawprintConfig
20
+ from pawly.policy.base import Policy, score_source as policy_score_source
21
+ from pawly.runtime_bootstrap import RuntimeConfig, RuntimeServices, ScoringPolicyFallbackMode, bootstrap_runtime
22
+ from pawly.runtime_decision import evaluate_core_policy
23
+ from pawly.runtime_execute import evaluate_runtime_intent
24
+ from pawly.runtime_request import build_task_request_intent
25
+ from pawly.runtime_report import build_validated_runtime_report
26
+ from pawly.runtime_result import RuntimeDecisionResult
27
+ from pawly.runtime_scoring import score_actions as score_runtime_actions
28
+ from pawly.shield import OutputProtectionResult, ShieldEnvelope, ShieldPolicy
29
+ from pawly.skill_registry import MissingSkillRegistryError, SkillRegistry
30
+ from pawly.types import Intent, IntentSource, PolicyEvaluation
31
+
32
+ LOGGER = logging.getLogger(__name__)
33
+ _SUMMARY_EMAIL_PATTERN = r"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b"
34
+ _SUMMARY_PHONE_PATTERN = r"\b(?:\+?\d[\d .-]{7,}\d)\b"
35
+
36
+
37
+ class DecisionEngine:
38
+ def __init__(
39
+ self,
40
+ agent_path: str | Path,
41
+ audit_path: str | Path | None = None,
42
+ *,
43
+ policy: str = "rules",
44
+ policy_impl: ReviewerPolicy | None = None,
45
+ risk_provider: RiskProvider | None = None,
46
+ audit_sink: AuditSink | None = None,
47
+ scoring_policy_fallback_mode: ScoringPolicyFallbackMode = "review",
48
+ scoring_policy: Policy | str | None = None,
49
+ reviewer: str | None = None,
50
+ reviewer_backend: ReviewerPolicy | None = None,
51
+ approval_backend: ApprovalBackend | None = None,
52
+ ) -> None:
53
+ if scoring_policy_fallback_mode not in {"review", "heuristic", "deny"}:
54
+ raise ValueError("scoring_policy_fallback_mode must be 'review', 'heuristic', or 'deny'")
55
+ bootstrap = bootstrap_runtime(
56
+ agent_path=agent_path,
57
+ audit_path=audit_path,
58
+ policy=policy,
59
+ policy_impl=policy_impl,
60
+ risk_provider=risk_provider,
61
+ audit_sink=audit_sink,
62
+ scoring_policy_fallback_mode=scoring_policy_fallback_mode,
63
+ scoring_policy=scoring_policy,
64
+ reviewer=reviewer,
65
+ reviewer_backend=reviewer_backend,
66
+ )
67
+ self.config: RuntimeConfig = bootstrap.config
68
+ self.services: RuntimeServices = bootstrap.services
69
+
70
+ self.agent_path = self.config.agent_path
71
+ self.validator = self.services.validator
72
+ self.raw_pawprint_config = self.config.raw_pawprint_config
73
+ self.pawprint_config = self.config.pawprint_config
74
+ self.raw_agent_config = self.raw_pawprint_config
75
+ self.agent_config = self.pawprint_config
76
+ self.reviewer = self.config.reviewer_name
77
+ self.policy = self.config.reviewer_policy
78
+ self.reviewer_backend = self.policy
79
+ self.local_scoring_policy = self.config.local_scoring_policy
80
+ self.scoring_policy = self.config.scoring_policy
81
+ self.fallback_scoring_policy = self.config.fallback_scoring_policy
82
+ self.audit_sink = self.services.audit_sink
83
+ self.audit_ledger = self.services.audit_ledger
84
+ self.budget_state = self.services.budget_state
85
+ self.memory_store = self.services.memory_store
86
+ self.hooks = self.services.hooks
87
+ self.ids = self.services.ids
88
+ self._decisions = self.services.decisions
89
+ self.skill_registry: SkillRegistry | None = None
90
+ self.shield_policy = ShieldPolicy()
91
+ self.approval_backend = approval_backend
92
+
93
+ def evaluate(self, task: str, action: str, confidence: float, metadata: dict[str, Any] | None = None) -> dict[str, Any]:
94
+ return self.evaluate_result(task, action, confidence, metadata).to_dict()
95
+
96
+ def evaluate_result(
97
+ self,
98
+ task: str,
99
+ action: str,
100
+ confidence: float,
101
+ metadata: dict[str, Any] | None = None,
102
+ ) -> RuntimeDecisionResult:
103
+ intent = build_task_request_intent(
104
+ task=task,
105
+ action=action,
106
+ confidence=confidence,
107
+ metadata=metadata,
108
+ intent_id=self.ids.next_intent_id(),
109
+ )
110
+ return self.evaluate_intent_result(intent)
111
+
112
+ def evaluate_intent(self, intent: Intent) -> dict[str, Any]:
113
+ return self.evaluate_intent_result(intent).to_dict()
114
+
115
+ def evaluate_intent_result(self, intent: Intent) -> RuntimeDecisionResult:
116
+ return evaluate_runtime_intent(
117
+ config=self.config,
118
+ services=self.services,
119
+ intent=intent,
120
+ )
121
+
122
+ def next_event_id(self) -> str:
123
+ return self.ids.next_event_id()
124
+
125
+ def score_actions(
126
+ self,
127
+ actions: Sequence[Action],
128
+ state: Mapping[str, Any] | None = None,
129
+ ) -> list[PolicyScore]:
130
+ return score_runtime_actions(
131
+ policy=self.local_scoring_policy,
132
+ actions=actions,
133
+ state=state,
134
+ )
135
+
136
+ def decide_actions(
137
+ self,
138
+ state: Mapping[str, Any] | None,
139
+ actions: Sequence[Action],
140
+ pawprint_config: PawprintConfig | None = None,
141
+ ) -> ActionDecision:
142
+ pawprint = pawprint_config or self.pawprint_config
143
+ classified = _classify_actions(actions, pawprint)
144
+ scoring_resolution = _resolve_scoring_policy(self)
145
+
146
+ allowed_candidates = _build_candidates(
147
+ actions=classified["allow"],
148
+ boundary_type="allow",
149
+ requires_review=False,
150
+ scores=score_runtime_actions(
151
+ policy=scoring_resolution["policy"],
152
+ actions=classified["allow"],
153
+ state=state,
154
+ ),
155
+ default_source=str(scoring_resolution["default_source"]),
156
+ )
157
+ review_candidates = _build_candidates(
158
+ actions=classified["review"],
159
+ boundary_type="review",
160
+ requires_review=True,
161
+ scores=score_runtime_actions(
162
+ policy=scoring_resolution["policy"],
163
+ actions=classified["review"],
164
+ state=state,
165
+ ),
166
+ default_source=str(scoring_resolution["default_source"]),
167
+ )
168
+ promoted_review_candidates, retained_allow_candidates = _apply_cloud_review_guardrails(
169
+ state=state,
170
+ candidates=allowed_candidates,
171
+ )
172
+ allowed_candidates = retained_allow_candidates
173
+ review_candidates = [*review_candidates, *promoted_review_candidates]
174
+ allowed_candidates, review_candidates, blocked_actions, protection_metadata = _apply_shield_policy(
175
+ engine=self,
176
+ state=state,
177
+ pawprint=pawprint,
178
+ allowed_candidates=allowed_candidates,
179
+ review_candidates=review_candidates,
180
+ blocked_actions=classified["blocked"],
181
+ )
182
+
183
+ ranked = sorted(
184
+ [*allowed_candidates, *review_candidates],
185
+ key=_candidate_rank_key,
186
+ )
187
+ best = ranked[0] if ranked else None
188
+ decision = ActionDecision(
189
+ trace_id=self.ids.next_event_id(),
190
+ selected_action=None if best is None else best.action,
191
+ requires_review=False if best is None else best.requires_review,
192
+ decision_source=None if best is None else best.decision_source,
193
+ boundary_type=None if best is None else best.boundary_type,
194
+ reason=None if best is None else best.reason,
195
+ uncertainty=None if best is None else best.uncertainty,
196
+ protection=protection_metadata,
197
+ allowed_actions=allowed_candidates,
198
+ review_required_actions=review_candidates,
199
+ blocked_actions=blocked_actions,
200
+ )
201
+ self.log_decision(decision)
202
+ return decision
203
+
204
+ def register_skills(self, skill_registry: SkillRegistry) -> "DecisionEngine":
205
+ self.skill_registry = skill_registry
206
+ return self
207
+
208
+ def bind_skills(self, skill_registry: SkillRegistry) -> "DecisionEngine":
209
+ return self.register_skills(skill_registry)
210
+
211
+ def register_approval_backend(self, approval_backend: ApprovalBackend) -> "DecisionEngine":
212
+ self.approval_backend = approval_backend
213
+ return self
214
+
215
+ def run_actions(
216
+ self,
217
+ *,
218
+ state: Mapping[str, Any] | None,
219
+ actions: Sequence[Action],
220
+ context: Mapping[str, Any] | None = None,
221
+ pawprint_config: PawprintConfig | None = None,
222
+ ) -> dict[str, Any]:
223
+ decision = self.decide_actions(
224
+ state=state,
225
+ actions=actions,
226
+ pawprint_config=pawprint_config,
227
+ )
228
+ if decision.selected_action is None:
229
+ payload = {
230
+ "status": "blocked",
231
+ "decision": decision.to_dict(),
232
+ "result": None,
233
+ }
234
+ self._append_run_actions_audit(
235
+ state=state,
236
+ context=context,
237
+ decision=decision,
238
+ execution_status="blocked",
239
+ executed_action=None,
240
+ result=None,
241
+ error=None,
242
+ redactions=[],
243
+ envelope=self.shield_policy.envelope_for(pawprint_config or self.pawprint_config),
244
+ protection_enabled=(pawprint_config or self.pawprint_config).protection is not None,
245
+ )
246
+ return payload
247
+ if decision.requires_review:
248
+ approval_payload = None
249
+ approved_action = None
250
+ if self.approval_backend is not None and decision.selected_action is not None:
251
+ approval_record = self.approval_backend.submit(
252
+ _build_review_intent(self, decision.selected_action, state, context),
253
+ _build_review_decision(decision),
254
+ )
255
+ approval_payload = self.approval_backend.status_payload(approval_record)
256
+ if approval_record.status == ApprovalStatus.APPROVED:
257
+ approved_action = approval_record.approved_action()
258
+ if approved_action is not None:
259
+ decision = _replace_selected_action(decision, approved_action)
260
+ else:
261
+ payload = {
262
+ "status": "needs_review",
263
+ "decision": decision.to_dict(),
264
+ "result": None,
265
+ }
266
+ if approval_payload is not None:
267
+ payload["approval"] = approval_payload
268
+ self._append_run_actions_audit(
269
+ state=state,
270
+ context=context,
271
+ decision=decision,
272
+ execution_status="needs_review",
273
+ executed_action=decision.selected_action,
274
+ result=None,
275
+ error=None,
276
+ redactions=[],
277
+ envelope=self.shield_policy.envelope_for(pawprint_config or self.pawprint_config),
278
+ protection_enabled=(pawprint_config or self.pawprint_config).protection is not None,
279
+ approval_payload=approval_payload,
280
+ )
281
+ return payload
282
+ else:
283
+ approval_payload = None
284
+ if self.skill_registry is None:
285
+ raise MissingSkillRegistryError("run_actions requires a registered SkillRegistry. Call register_skills(...) first.")
286
+
287
+ pawprint = pawprint_config or self.pawprint_config
288
+ envelope = self.shield_policy.envelope_for(pawprint)
289
+ sanitized_action, redactions = self.shield_policy.sanitize_action(decision.selected_action, envelope)
290
+
291
+ try:
292
+ raw_result = self.skill_registry.execute(sanitized_action, dict(context or {}))
293
+ except Exception as exc:
294
+ safe_error = _sanitize_error(exc)
295
+ self._append_run_actions_audit(
296
+ state=state,
297
+ context=context,
298
+ decision=decision,
299
+ execution_status="failed",
300
+ executed_action=sanitized_action,
301
+ result=None,
302
+ error=safe_error,
303
+ redactions=redactions,
304
+ envelope=envelope,
305
+ protection_enabled=pawprint.protection is not None,
306
+ approval_payload=approval_payload,
307
+ )
308
+ payload = {
309
+ "status": "failed",
310
+ "decision": decision.to_dict(),
311
+ "error": safe_error,
312
+ "result": None,
313
+ }
314
+ if approval_payload is not None:
315
+ payload["approval"] = approval_payload
316
+ return payload
317
+
318
+ reviewed_result = self.apply_output_protection(
319
+ raw_result,
320
+ decision=decision,
321
+ state=state,
322
+ context=context or {},
323
+ envelope=envelope,
324
+ )
325
+ combined_redactions = [*redactions, *reviewed_result.redactions]
326
+
327
+ if reviewed_result.status == "needs_review":
328
+ self._append_run_actions_audit(
329
+ state=state,
330
+ context=context,
331
+ decision=decision,
332
+ execution_status="needs_review",
333
+ executed_action=sanitized_action,
334
+ result=reviewed_result.result,
335
+ error=None,
336
+ redactions=combined_redactions,
337
+ protection_reasons=reviewed_result.reasons,
338
+ envelope=envelope,
339
+ protection_enabled=pawprint.protection is not None,
340
+ approval_payload=approval_payload,
341
+ )
342
+ payload = {
343
+ "status": "needs_review",
344
+ "decision": decision.to_dict(),
345
+ "result": reviewed_result.result,
346
+ }
347
+ if approval_payload is not None:
348
+ payload["approval"] = approval_payload
349
+ return payload
350
+ if reviewed_result.status == "blocked":
351
+ self._append_run_actions_audit(
352
+ state=state,
353
+ context=context,
354
+ decision=decision,
355
+ execution_status="blocked",
356
+ executed_action=sanitized_action,
357
+ result=None,
358
+ error=None,
359
+ redactions=combined_redactions,
360
+ protection_reasons=reviewed_result.reasons,
361
+ envelope=envelope,
362
+ protection_enabled=pawprint.protection is not None,
363
+ approval_payload=approval_payload,
364
+ )
365
+ payload = {
366
+ "status": "blocked",
367
+ "decision": decision.to_dict(),
368
+ "result": None,
369
+ }
370
+ if approval_payload is not None:
371
+ payload["approval"] = approval_payload
372
+ return payload
373
+
374
+ self._append_run_actions_audit(
375
+ state=state,
376
+ context=context,
377
+ decision=decision,
378
+ execution_status="completed",
379
+ executed_action=sanitized_action,
380
+ result=reviewed_result.result,
381
+ error=None,
382
+ redactions=combined_redactions,
383
+ protection_reasons=reviewed_result.reasons,
384
+ envelope=envelope,
385
+ protection_enabled=pawprint.protection is not None,
386
+ approval_payload=approval_payload,
387
+ )
388
+ payload = {
389
+ "status": "completed",
390
+ "decision": decision.to_dict(),
391
+ "result": reviewed_result.result,
392
+ }
393
+ if approval_payload is not None:
394
+ payload["approval"] = approval_payload
395
+ return payload
396
+
397
+ def apply_output_protection(
398
+ self,
399
+ result: Any,
400
+ *,
401
+ decision: ActionDecision,
402
+ state: Mapping[str, Any] | None,
403
+ context: Mapping[str, Any] | None,
404
+ envelope: ShieldEnvelope | None = None,
405
+ ) -> OutputProtectionResult:
406
+ del state, context, decision
407
+ selected_envelope = envelope or self.shield_policy.envelope_for(self.pawprint_config)
408
+ return self.shield_policy.protect_output(result, selected_envelope)
409
+
410
+ def _append_run_actions_audit(
411
+ self,
412
+ *,
413
+ state: Mapping[str, Any] | None,
414
+ context: Mapping[str, Any] | None,
415
+ decision: ActionDecision,
416
+ execution_status: str,
417
+ executed_action: Action | None,
418
+ result: Any,
419
+ error: str | None,
420
+ redactions: list[str],
421
+ protection_reasons: list[str] | None = None,
422
+ envelope: ShieldEnvelope | None = None,
423
+ protection_enabled: bool = False,
424
+ approval_payload: dict[str, Any] | None = None,
425
+ ) -> None:
426
+ selected_action = decision.selected_action
427
+ action_name = None if selected_action is None else selected_action.name
428
+ selected_action_payload = _audit_action_payload(
429
+ selected_action,
430
+ envelope=envelope,
431
+ protection_enabled=protection_enabled,
432
+ )
433
+ executed_action_payload = _audit_action_payload(
434
+ executed_action,
435
+ envelope=envelope,
436
+ protection_enabled=protection_enabled,
437
+ )
438
+ event = AuditEvent.from_governed_execution(
439
+ event_id=self.ids.next_event_id(),
440
+ decision_id=decision.trace_id or self.ids.next_decision_id(),
441
+ agent_id=self.pawprint_config.id,
442
+ pawprint_version=load_pawprint_version(),
443
+ outcome=execution_status,
444
+ original_intent=_build_run_actions_request_payload(state, context, selected_action_payload),
445
+ normalized_intent=_build_run_actions_request_payload(state, context, selected_action_payload),
446
+ action={"name": action_name},
447
+ policy_evaluation={},
448
+ runtime_overlays={
449
+ "run_actions": True,
450
+ "protection": None if decision.protection is None else dict(decision.protection),
451
+ },
452
+ policy_references=[],
453
+ final_decision=decision.to_dict(),
454
+ reason_codes=list(protection_reasons or []),
455
+ approval=approval_payload,
456
+ executed_action=executed_action_payload,
457
+ action_diff=None,
458
+ execution={
459
+ "attempted": executed_action is not None,
460
+ "executed": execution_status == "completed",
461
+ "blocked_by": None if execution_status == "completed" else execution_status,
462
+ "result_summary": _audit_result_summary(
463
+ result,
464
+ envelope=envelope,
465
+ protection_enabled=protection_enabled,
466
+ ),
467
+ "error": error,
468
+ },
469
+ execution_result_ref=None,
470
+ risk_score=_selected_risk_score(decision),
471
+ escalated_to=None,
472
+ tenant_id=_actor_value(state, "tenant_id"),
473
+ user_id=_actor_value(state, "user_id"),
474
+ protection_level=None if decision.protection is None else str(decision.protection.get("level", "")) or None,
475
+ protection_handling=None if decision.protection is None else str(decision.protection.get("handling", "")) or None,
476
+ protection_assets=None if decision.protection is None else [str(item) for item in decision.protection.get("assets", [])],
477
+ action_argument_summary=_action_argument_summary(selected_action_payload),
478
+ output_summary=_audit_result_summary(
479
+ result,
480
+ envelope=envelope,
481
+ protection_enabled=protection_enabled,
482
+ ),
483
+ error_summary=error,
484
+ )
485
+ if redactions:
486
+ event.redactions_applied = list(dict.fromkeys(redactions))
487
+ self.audit_sink.append(event)
488
+
489
+ def _evaluate_core_policy(self, intent: Intent) -> PolicyEvaluation:
490
+ return evaluate_core_policy(
491
+ self.policy,
492
+ intent=intent,
493
+ pawprint=self.pawprint_config,
494
+ )
495
+
496
+ def build_report(self) -> dict[str, Any]:
497
+ return build_validated_runtime_report(
498
+ validator=self.validator,
499
+ pawprint=self.pawprint_config,
500
+ decisions=self._decisions,
501
+ )
502
+
503
+ def log_decision(self, decision: ActionDecision) -> dict[str, object]:
504
+ payload = _build_action_decision_log(self, decision)
505
+ LOGGER.info("action_decision=%s", payload)
506
+ return payload
507
+
508
+
509
+ PawlyRuntime = DecisionEngine
510
+
511
+
512
+ def _classify_actions(actions: Sequence[Action], pawprint: PawprintConfig) -> dict[str, list[Action]]:
513
+ blocked = {_normalize_name(name) for name in pawprint.blocked_actions}
514
+ review = {_normalize_name(name) for name in pawprint.review_actions}
515
+ allowed = {_normalize_name(name) for name in pawprint.allowed_actions}
516
+
517
+ buckets: dict[str, list[Action]] = {
518
+ "allow": [],
519
+ "review": [],
520
+ "blocked": [],
521
+ }
522
+ for action in actions:
523
+ normalized = _normalize_name(action.name)
524
+ if normalized in blocked:
525
+ buckets["blocked"].append(action)
526
+ elif normalized in review:
527
+ buckets["review"].append(action)
528
+ elif normalized in allowed:
529
+ buckets["allow"].append(action)
530
+ return buckets
531
+
532
+
533
+ def _build_candidates(
534
+ *,
535
+ actions: Sequence[Action],
536
+ boundary_type: str,
537
+ requires_review: bool,
538
+ scores: Sequence[PolicyScore],
539
+ default_source: str,
540
+ ) -> list[ActionCandidate]:
541
+ return [
542
+ ActionCandidate(
543
+ action=action,
544
+ boundary_type=boundary_type,
545
+ requires_review=requires_review,
546
+ decision_source=policy_score_source(score, default_source),
547
+ score=score,
548
+ reason=_candidate_reason(boundary_type, requires_review, score),
549
+ uncertainty=score.uncertainty,
550
+ )
551
+ for action, score in zip(actions, scores, strict=False)
552
+ ]
553
+
554
+
555
+ def _candidate_rank_key(candidate: ActionCandidate) -> tuple[float, int]:
556
+ risk_score = 1.0 if candidate.score.risk_score is None else float(candidate.score.risk_score)
557
+ review_penalty = 1 if candidate.requires_review else 0
558
+ return (risk_score, review_penalty)
559
+
560
+
561
+ def _apply_cloud_review_guardrails(
562
+ *,
563
+ state: Mapping[str, Any] | None,
564
+ candidates: Sequence[ActionCandidate],
565
+ ) -> tuple[list[ActionCandidate], list[ActionCandidate]]:
566
+ threshold = _uncertainty_review_threshold(state)
567
+ promoted_review: list[ActionCandidate] = []
568
+ retained_allow: list[ActionCandidate] = []
569
+ for candidate in candidates:
570
+ escalation = _escalation_recommendation(candidate)
571
+ if escalation == "human_handoff":
572
+ promoted_review.append(
573
+ replace(
574
+ candidate,
575
+ boundary_type="review",
576
+ requires_review=True,
577
+ reason="cloud_candidate_handoff_recommended",
578
+ )
579
+ )
580
+ continue
581
+ if escalation == "require_review":
582
+ promoted_review.append(
583
+ replace(
584
+ candidate,
585
+ boundary_type="review",
586
+ requires_review=True,
587
+ reason="cloud_candidate_requires_review",
588
+ )
589
+ )
590
+ continue
591
+ if candidate.uncertainty is not None and candidate.uncertainty >= threshold:
592
+ promoted_review.append(
593
+ replace(
594
+ candidate,
595
+ boundary_type="review",
596
+ requires_review=True,
597
+ reason="cloud_candidate_uncertainty_requires_review",
598
+ )
599
+ )
600
+ continue
601
+ retained_allow.append(candidate)
602
+ return promoted_review, retained_allow
603
+
604
+
605
+ def _uncertainty_review_threshold(state: Mapping[str, Any] | None) -> float:
606
+ default_threshold = 0.6
607
+ if not isinstance(state, Mapping):
608
+ return default_threshold
609
+ raw_value = state.get("uncertainty_review_threshold")
610
+ if raw_value is None:
611
+ cloud_policy = state.get("cloud_policy")
612
+ if isinstance(cloud_policy, Mapping):
613
+ raw_value = cloud_policy.get("uncertainty_review_threshold")
614
+ try:
615
+ if raw_value is None:
616
+ return default_threshold
617
+ return float(raw_value)
618
+ except (TypeError, ValueError):
619
+ return default_threshold
620
+
621
+
622
+ def _escalation_recommendation(candidate: ActionCandidate) -> str | None:
623
+ for tag in candidate.score.audit_tags:
624
+ if tag.startswith("escalation:"):
625
+ value = tag.split(":", 1)[1].strip()
626
+ return value or None
627
+ return None
628
+
629
+
630
+ def _normalize_name(value: str) -> str:
631
+ return value.strip().lower()
632
+
633
+
634
+ def _resolve_scoring_policy(engine: DecisionEngine) -> dict[str, object]:
635
+ policy = engine.scoring_policy
636
+ if policy.is_scoring_available():
637
+ return {
638
+ "policy": policy,
639
+ "default_source": policy.source_name(),
640
+ "fallback_used": False,
641
+ }
642
+
643
+ reason = policy.scoring_unavailable_reason()
644
+ if policy.source_name() == "cloud":
645
+ LOGGER.warning(
646
+ "CloudPolicy is configured but Pawly Cloud credentials are missing. Falling back to local Pawprint boundary preferences."
647
+ )
648
+ else:
649
+ LOGGER.warning(
650
+ "Scoring policy '%s' is unavailable. Falling back to the local scoring policy. %s",
651
+ getattr(policy, "name", policy.source_name()),
652
+ reason or "No additional details provided.",
653
+ )
654
+ return {
655
+ "policy": engine.fallback_scoring_policy,
656
+ "default_source": "fallback",
657
+ "fallback_used": True,
658
+ }
659
+
660
+
661
+ def _candidate_reason(boundary_type: str, requires_review: bool, score: PolicyScore) -> str:
662
+ if score.reason_codes:
663
+ return score.reason_codes[0]
664
+ if requires_review:
665
+ return f"{boundary_type}_candidate_requires_review"
666
+ return f"{boundary_type}_candidate_selected"
667
+
668
+
669
+ def _build_action_decision_log(engine: DecisionEngine, decision: ActionDecision) -> dict[str, object]:
670
+ selected_candidate = _selected_candidate(decision)
671
+ blocked_action = decision.blocked_actions[0] if decision.blocked_actions else None
672
+ fallback_used = _policy_fallback_used(decision)
673
+ policy = _logged_policy(engine, fallback_used)
674
+
675
+ action_name: str | None
676
+ if selected_candidate is not None:
677
+ action_name = selected_candidate.action.name
678
+ elif blocked_action is not None:
679
+ action_name = blocked_action.name
680
+ else:
681
+ action_name = None
682
+
683
+ boundary_type = decision.boundary_type
684
+ if boundary_type is None and blocked_action is not None:
685
+ boundary_type = "block"
686
+
687
+ decision_name = "deny"
688
+ if decision.selected_action is not None:
689
+ decision_name = "require_review" if decision.requires_review else "allow"
690
+
691
+ scores = _scores_payload(decision)
692
+ uncertainty = None if selected_candidate is None else selected_candidate.uncertainty
693
+ reason = decision.reason
694
+ if reason is None and blocked_action is not None:
695
+ reason = "blocked_by_pawprint_boundary"
696
+
697
+ payload: dict[str, object] = {
698
+ "trace_id": decision.trace_id or engine.ids.next_event_id(),
699
+ "action": action_name,
700
+ "decision": decision_name,
701
+ "boundary_type": boundary_type,
702
+ "requires_review": decision.requires_review,
703
+ "decision_source": _decision_source(engine, decision, selected_candidate, fallback_used),
704
+ "policy_name": getattr(policy, "name", policy.source_name()),
705
+ "policy_supports_scoring": policy.is_scoring_capable(),
706
+ "policy_fallback_used": fallback_used,
707
+ "reason": reason,
708
+ }
709
+ if uncertainty is not None:
710
+ payload["uncertainty"] = uncertainty
711
+ if scores:
712
+ payload["scores"] = scores
713
+ return payload
714
+
715
+
716
+ def _selected_candidate(decision: ActionDecision) -> ActionCandidate | None:
717
+ if decision.selected_action is None:
718
+ return None
719
+ for candidate in [*decision.allowed_actions, *decision.review_required_actions]:
720
+ if candidate.action == decision.selected_action:
721
+ return candidate
722
+ return None
723
+
724
+
725
+ def _policy_fallback_used(decision: ActionDecision) -> bool:
726
+ return decision.decision_source == "fallback"
727
+
728
+
729
+ def _logged_policy(
730
+ engine: DecisionEngine,
731
+ fallback_used: bool,
732
+ ) -> Policy:
733
+ return engine.fallback_scoring_policy if fallback_used else engine.scoring_policy
734
+
735
+
736
+ def _decision_source(
737
+ engine: DecisionEngine,
738
+ decision: ActionDecision,
739
+ selected_candidate: ActionCandidate | None,
740
+ fallback_used: bool,
741
+ ) -> str:
742
+ if decision.decision_source is not None:
743
+ return decision.decision_source
744
+ if selected_candidate is not None:
745
+ return selected_candidate.decision_source
746
+ return engine.fallback_scoring_policy.source_name() if fallback_used else engine.scoring_policy.source_name()
747
+
748
+
749
+ def _scores_payload(decision: ActionDecision) -> list[dict[str, object]]:
750
+ candidates = [*decision.allowed_actions, *decision.review_required_actions]
751
+ payload: list[dict[str, object]] = []
752
+ for candidate in candidates:
753
+ score_payload = candidate.score.to_dict()
754
+ payload.append(
755
+ {
756
+ "action": candidate.action.name,
757
+ "boundary_type": candidate.boundary_type,
758
+ "decision_source": candidate.decision_source,
759
+ "score": score_payload,
760
+ }
761
+ )
762
+ return payload
763
+
764
+
765
+ def _apply_shield_policy(
766
+ *,
767
+ engine: DecisionEngine,
768
+ state: Mapping[str, Any] | None,
769
+ pawprint: PawprintConfig,
770
+ allowed_candidates: list[ActionCandidate],
771
+ review_candidates: list[ActionCandidate],
772
+ blocked_actions: list[Action],
773
+ ) -> tuple[list[ActionCandidate], list[ActionCandidate], list[Action], dict[str, object]]:
774
+ protection_reasons: list[str] = []
775
+ next_allowed: list[ActionCandidate] = []
776
+ next_review: list[ActionCandidate] = []
777
+ next_blocked: list[Action] = list(blocked_actions)
778
+
779
+ for candidate in allowed_candidates:
780
+ outcome, updated, reasons = engine.shield_policy.apply_to_candidate(candidate, pawprint=pawprint, state=dict(state or {}))
781
+ protection_reasons.extend(reasons)
782
+ if outcome == "block":
783
+ next_blocked.append(updated.action)
784
+ elif outcome == "review":
785
+ next_review.append(updated)
786
+ else:
787
+ next_allowed.append(updated)
788
+
789
+ for candidate in review_candidates:
790
+ outcome, updated, reasons = engine.shield_policy.apply_to_candidate(candidate, pawprint=pawprint, state=dict(state or {}))
791
+ protection_reasons.extend(reasons)
792
+ if outcome == "block":
793
+ next_blocked.append(updated.action)
794
+ else:
795
+ next_review.append(updated)
796
+
797
+ envelope = engine.shield_policy.envelope_for(pawprint)
798
+ protection = {
799
+ "level": envelope.level,
800
+ "handling": envelope.handling,
801
+ "assets": list(envelope.assets),
802
+ "mode": envelope.mode,
803
+ "reasons": list(dict.fromkeys(protection_reasons)),
804
+ }
805
+ deduped_review = _dedupe_candidates(next_review)
806
+ deduped_allowed = _dedupe_candidates(next_allowed)
807
+ deduped_blocked = _dedupe_actions(next_blocked)
808
+ return deduped_allowed, deduped_review, deduped_blocked, protection
809
+
810
+
811
+ def _dedupe_candidates(candidates: list[ActionCandidate]) -> list[ActionCandidate]:
812
+ seen: set[tuple[str, tuple[tuple[str, str], ...], str | None]] = set()
813
+ deduped: list[ActionCandidate] = []
814
+ for candidate in candidates:
815
+ key = (
816
+ candidate.action.name,
817
+ tuple(sorted((str(k), repr(v)) for k, v in candidate.action.arguments.items())),
818
+ candidate.action.target,
819
+ )
820
+ if key in seen:
821
+ continue
822
+ seen.add(key)
823
+ deduped.append(candidate)
824
+ return deduped
825
+
826
+
827
+ def _dedupe_actions(actions: list[Action]) -> list[Action]:
828
+ seen: set[tuple[str, tuple[tuple[str, str], ...], str | None]] = set()
829
+ deduped: list[Action] = []
830
+ for action in actions:
831
+ key = (
832
+ action.name,
833
+ tuple(sorted((str(k), repr(v)) for k, v in action.arguments.items())),
834
+ action.target,
835
+ )
836
+ if key in seen:
837
+ continue
838
+ seen.add(key)
839
+ deduped.append(action)
840
+ return deduped
841
+
842
+
843
+ def _selected_risk_score(decision: ActionDecision) -> float | None:
844
+ candidate = _selected_candidate(decision)
845
+ if candidate is None:
846
+ return None
847
+ return candidate.score.risk_score
848
+
849
+
850
+ def _build_run_actions_request_payload(
851
+ state: Mapping[str, Any] | None,
852
+ context: Mapping[str, Any] | None,
853
+ selected_action: dict[str, Any] | None,
854
+ ) -> dict[str, Any]:
855
+ return {
856
+ "state": dict(state or {}),
857
+ "context": _safe_summary(dict(context or {})),
858
+ "selected_action": selected_action,
859
+ }
860
+
861
+
862
+ def _audit_action_payload(
863
+ action: Action | None,
864
+ *,
865
+ envelope: ShieldEnvelope | None,
866
+ protection_enabled: bool,
867
+ ) -> dict[str, Any] | None:
868
+ if action is None:
869
+ return None
870
+ if not protection_enabled or envelope is None:
871
+ return action.to_dict()
872
+ if envelope.trace.input_storage == "none":
873
+ return {"name": action.name}
874
+ if envelope.trace.input_storage == "summary":
875
+ return {
876
+ "name": action.name,
877
+ "arguments": _safe_summary(action.arguments),
878
+ **({"target": action.target} if action.target is not None else {}),
879
+ }
880
+ redacted_arguments = {key: "[redacted]" for key in action.arguments}
881
+ payload: dict[str, Any] = {"name": action.name, "arguments": redacted_arguments}
882
+ if action.target is not None:
883
+ payload["target"] = action.target
884
+ return payload
885
+
886
+
887
+ def _audit_result_summary(
888
+ result: Any,
889
+ *,
890
+ envelope: ShieldEnvelope | None,
891
+ protection_enabled: bool,
892
+ ) -> Any:
893
+ if not protection_enabled or envelope is None:
894
+ return _safe_summary(result)
895
+ if envelope.trace.output_storage == "none":
896
+ return None
897
+ if envelope.trace.output_storage == "summary":
898
+ return _safe_summary(result)
899
+ return "[redacted-output]"
900
+
901
+
902
+ def _safe_summary(value: Any) -> Any:
903
+ if value is None:
904
+ return None
905
+ if isinstance(value, dict):
906
+ summary: dict[str, Any] = {}
907
+ for key, item in value.items():
908
+ normalized = str(key).strip().lower()
909
+ if normalized in {"chain_of_thought", "cot", "reasoning", "internal_prompt", "system_prompt"}:
910
+ summary[str(key)] = "[redacted]"
911
+ continue
912
+ summary[str(key)] = _safe_summary(item)
913
+ return summary
914
+ if isinstance(value, list):
915
+ return [_safe_summary(item) for item in value[:10]]
916
+ if isinstance(value, str):
917
+ lowered = value.lower()
918
+ if any(token in lowered for token in ("api key", "authorization", "private key", "system prompt", "hidden instructions")):
919
+ return "[redacted]"
920
+ trimmed = value[:200]
921
+ trimmed = re.sub(_SUMMARY_EMAIL_PATTERN, "[redacted-email]", trimmed, flags=re.IGNORECASE)
922
+ trimmed = re.sub(_SUMMARY_PHONE_PATTERN, "[redacted-phone]", trimmed, flags=re.IGNORECASE)
923
+ return trimmed
924
+ if isinstance(value, (int, float, bool)):
925
+ return value
926
+ return str(value)[:200]
927
+
928
+
929
+ def _actor_value(state: Mapping[str, Any] | None, field: str) -> str | None:
930
+ if not isinstance(state, Mapping):
931
+ return None
932
+ actor = state.get("actor")
933
+ if not isinstance(actor, Mapping):
934
+ return None
935
+ value = str(actor.get(field, "")).strip()
936
+ return value or None
937
+
938
+
939
+ def _action_argument_summary(action_payload: dict[str, Any] | None) -> dict[str, Any] | None:
940
+ if not isinstance(action_payload, dict):
941
+ return None
942
+ arguments = action_payload.get("arguments")
943
+ if not isinstance(arguments, dict):
944
+ return None
945
+ return dict(arguments)
946
+
947
+
948
+ def _sanitize_error(exc: Exception) -> str:
949
+ text = str(exc).strip() or exc.__class__.__name__
950
+ lowered = text.lower()
951
+ if any(token in lowered for token in ("secret", "token", "api key", "private key", "authorization")):
952
+ return "skill_execution_failed"
953
+ return text[:200]
954
+
955
+
956
+ def _build_review_decision(decision: ActionDecision) -> Decision:
957
+ return Decision(
958
+ type=DecisionState.REQUIRE_APPROVAL,
959
+ reason=decision.reason or "candidate_action_requires_review",
960
+ source=decision.decision_source,
961
+ reason_codes=[] if decision.protection is None else [str(item) for item in decision.protection.get("reasons", [])],
962
+ matched_rules=[],
963
+ risk_score=_selected_risk_score(decision),
964
+ audit_tags=["candidate-action:review"],
965
+ )
966
+
967
+
968
+ def _build_review_intent(
969
+ engine: DecisionEngine,
970
+ action: Action,
971
+ state: Mapping[str, Any] | None,
972
+ context: Mapping[str, Any] | None,
973
+ ) -> Intent:
974
+ summary = ""
975
+ confidence = 0.0
976
+ if isinstance(state, Mapping):
977
+ conversation = state.get("conversation")
978
+ if isinstance(conversation, Mapping):
979
+ summary = str(conversation.get("current_text", "")).strip()
980
+ request = state.get("request")
981
+ if isinstance(request, Mapping):
982
+ raw_confidence = request.get("confidence")
983
+ try:
984
+ if raw_confidence is not None:
985
+ confidence = float(raw_confidence)
986
+ except (TypeError, ValueError):
987
+ confidence = 0.0
988
+ metadata: dict[str, Any] = {}
989
+ if isinstance(state, Mapping):
990
+ metadata["state"] = dict(state)
991
+ if isinstance(context, Mapping):
992
+ metadata["context"] = dict(context)
993
+ return Intent(
994
+ intent_id=engine.ids.next_intent_id(),
995
+ source=IntentSource.EXECUTION_REQUEST,
996
+ action=action,
997
+ summary=summary,
998
+ confidence=confidence,
999
+ metadata=metadata,
1000
+ )
1001
+
1002
+
1003
+ def _replace_selected_action(decision: ActionDecision, action: Action) -> ActionDecision:
1004
+ return replace(decision, selected_action=action)