pawly 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. pawly/__init__.py +154 -0
  2. pawly/__pycache__/__init__.cpython-311.pyc +0 -0
  3. pawly/__pycache__/__init__.cpython-313.pyc +0 -0
  4. pawly/__pycache__/action_selection.cpython-311.pyc +0 -0
  5. pawly/__pycache__/action_selection.cpython-313.pyc +0 -0
  6. pawly/__pycache__/api.cpython-311.pyc +0 -0
  7. pawly/__pycache__/api.cpython-313.pyc +0 -0
  8. pawly/__pycache__/contracts.cpython-311.pyc +0 -0
  9. pawly/__pycache__/contracts.cpython-313.pyc +0 -0
  10. pawly/__pycache__/decision_engine.cpython-311.pyc +0 -0
  11. pawly/__pycache__/decision_engine.cpython-313.pyc +0 -0
  12. pawly/__pycache__/pawprint_loader.cpython-311.pyc +0 -0
  13. pawly/__pycache__/pawprint_loader.cpython-313.pyc +0 -0
  14. pawly/__pycache__/protected_oss.cpython-311.pyc +0 -0
  15. pawly/__pycache__/protected_oss.cpython-313.pyc +0 -0
  16. pawly/__pycache__/runtime.cpython-311.pyc +0 -0
  17. pawly/__pycache__/runtime.cpython-313.pyc +0 -0
  18. pawly/__pycache__/runtime_bootstrap.cpython-311.pyc +0 -0
  19. pawly/__pycache__/runtime_bootstrap.cpython-313.pyc +0 -0
  20. pawly/__pycache__/runtime_decision.cpython-311.pyc +0 -0
  21. pawly/__pycache__/runtime_decision.cpython-313.pyc +0 -0
  22. pawly/__pycache__/runtime_execute.cpython-311.pyc +0 -0
  23. pawly/__pycache__/runtime_execute.cpython-313.pyc +0 -0
  24. pawly/__pycache__/runtime_flow.cpython-311.pyc +0 -0
  25. pawly/__pycache__/runtime_flow.cpython-313.pyc +0 -0
  26. pawly/__pycache__/runtime_ids.cpython-311.pyc +0 -0
  27. pawly/__pycache__/runtime_ids.cpython-313.pyc +0 -0
  28. pawly/__pycache__/runtime_report.cpython-311.pyc +0 -0
  29. pawly/__pycache__/runtime_report.cpython-313.pyc +0 -0
  30. pawly/__pycache__/runtime_request.cpython-311.pyc +0 -0
  31. pawly/__pycache__/runtime_request.cpython-313.pyc +0 -0
  32. pawly/__pycache__/runtime_result.cpython-311.pyc +0 -0
  33. pawly/__pycache__/runtime_result.cpython-313.pyc +0 -0
  34. pawly/__pycache__/runtime_scoring.cpython-311.pyc +0 -0
  35. pawly/__pycache__/runtime_scoring.cpython-313.pyc +0 -0
  36. pawly/__pycache__/shield.cpython-311.pyc +0 -0
  37. pawly/__pycache__/shield.cpython-313.pyc +0 -0
  38. pawly/__pycache__/skill_registry.cpython-311.pyc +0 -0
  39. pawly/__pycache__/skill_registry.cpython-313.pyc +0 -0
  40. pawly/__pycache__/task_request.cpython-311.pyc +0 -0
  41. pawly/__pycache__/task_request.cpython-313.pyc +0 -0
  42. pawly/__pycache__/types.cpython-311.pyc +0 -0
  43. pawly/__pycache__/types.cpython-313.pyc +0 -0
  44. pawly/action_selection.py +64 -0
  45. pawly/adapters/__init__.py +25 -0
  46. pawly/adapters/__pycache__/__init__.cpython-311.pyc +0 -0
  47. pawly/adapters/__pycache__/__init__.cpython-313.pyc +0 -0
  48. pawly/adapters/__pycache__/claude_skills.cpython-311.pyc +0 -0
  49. pawly/adapters/__pycache__/claude_skills.cpython-313.pyc +0 -0
  50. pawly/adapters/__pycache__/openai_agents.cpython-311.pyc +0 -0
  51. pawly/adapters/__pycache__/openai_agents.cpython-313.pyc +0 -0
  52. pawly/adapters/claude_skills.py +210 -0
  53. pawly/adapters/openai_agents.py +208 -0
  54. pawly/api.py +57 -0
  55. pawly/approval/__init__.py +18 -0
  56. pawly/approval/__pycache__/__init__.cpython-311.pyc +0 -0
  57. pawly/approval/__pycache__/__init__.cpython-313.pyc +0 -0
  58. pawly/approval/__pycache__/callback.cpython-311.pyc +0 -0
  59. pawly/approval/__pycache__/callback.cpython-313.pyc +0 -0
  60. pawly/approval/__pycache__/handler.cpython-311.pyc +0 -0
  61. pawly/approval/__pycache__/handler.cpython-313.pyc +0 -0
  62. pawly/approval/__pycache__/models.cpython-311.pyc +0 -0
  63. pawly/approval/__pycache__/models.cpython-313.pyc +0 -0
  64. pawly/approval/__pycache__/queue.cpython-311.pyc +0 -0
  65. pawly/approval/__pycache__/queue.cpython-313.pyc +0 -0
  66. pawly/approval/__pycache__/router.cpython-311.pyc +0 -0
  67. pawly/approval/__pycache__/router.cpython-313.pyc +0 -0
  68. pawly/approval/__pycache__/timeout.cpython-311.pyc +0 -0
  69. pawly/approval/__pycache__/timeout.cpython-313.pyc +0 -0
  70. pawly/approval/callback.py +16 -0
  71. pawly/approval/handler.py +34 -0
  72. pawly/approval/models.py +119 -0
  73. pawly/approval/queue.py +109 -0
  74. pawly/approval/router.py +66 -0
  75. pawly/approval/timeout.py +18 -0
  76. pawly/audit/__pycache__/diff.cpython-311.pyc +0 -0
  77. pawly/audit/__pycache__/diff.cpython-313.pyc +0 -0
  78. pawly/audit/__pycache__/events.cpython-311.pyc +0 -0
  79. pawly/audit/__pycache__/events.cpython-313.pyc +0 -0
  80. pawly/audit/__pycache__/ledger.cpython-311.pyc +0 -0
  81. pawly/audit/__pycache__/ledger.cpython-313.pyc +0 -0
  82. pawly/audit/__pycache__/replay.cpython-311.pyc +0 -0
  83. pawly/audit/__pycache__/replay.cpython-313.pyc +0 -0
  84. pawly/audit/diff.py +24 -0
  85. pawly/audit/events.py +156 -0
  86. pawly/audit/ledger.py +39 -0
  87. pawly/audit/replay.py +45 -0
  88. pawly/backends/__init__.py +39 -0
  89. pawly/backends/__pycache__/__init__.cpython-311.pyc +0 -0
  90. pawly/backends/__pycache__/__init__.cpython-313.pyc +0 -0
  91. pawly/backends/__pycache__/approval.cpython-311.pyc +0 -0
  92. pawly/backends/__pycache__/approval.cpython-313.pyc +0 -0
  93. pawly/backends/__pycache__/audit.cpython-311.pyc +0 -0
  94. pawly/backends/__pycache__/audit.cpython-313.pyc +0 -0
  95. pawly/backends/__pycache__/reviewer.cpython-311.pyc +0 -0
  96. pawly/backends/__pycache__/reviewer.cpython-313.pyc +0 -0
  97. pawly/backends/__pycache__/risk.cpython-311.pyc +0 -0
  98. pawly/backends/__pycache__/risk.cpython-313.pyc +0 -0
  99. pawly/backends/approval.py +85 -0
  100. pawly/backends/audit.py +189 -0
  101. pawly/backends/reviewer.py +127 -0
  102. pawly/backends/risk.py +71 -0
  103. pawly/budget/__pycache__/checker.cpython-311.pyc +0 -0
  104. pawly/budget/__pycache__/checker.cpython-313.pyc +0 -0
  105. pawly/budget/__pycache__/state.cpython-311.pyc +0 -0
  106. pawly/budget/__pycache__/state.cpython-313.pyc +0 -0
  107. pawly/budget/checker.py +37 -0
  108. pawly/budget/state.py +15 -0
  109. pawly/cli.py +44 -0
  110. pawly/contracts.py +153 -0
  111. pawly/decision_engine.py +6 -0
  112. pawly/escalation/__pycache__/patterns.cpython-311.pyc +0 -0
  113. pawly/escalation/__pycache__/patterns.cpython-313.pyc +0 -0
  114. pawly/escalation/patterns.py +33 -0
  115. pawly/gateway/__init__.py +4 -0
  116. pawly/gateway/__pycache__/__init__.cpython-311.pyc +0 -0
  117. pawly/gateway/__pycache__/__init__.cpython-313.pyc +0 -0
  118. pawly/gateway/__pycache__/adapter_support.cpython-311.pyc +0 -0
  119. pawly/gateway/__pycache__/adapter_support.cpython-313.pyc +0 -0
  120. pawly/gateway/__pycache__/protocol.cpython-311.pyc +0 -0
  121. pawly/gateway/__pycache__/protocol.cpython-313.pyc +0 -0
  122. pawly/gateway/__pycache__/result.cpython-311.pyc +0 -0
  123. pawly/gateway/__pycache__/result.cpython-313.pyc +0 -0
  124. pawly/gateway/__pycache__/wrapper.cpython-311.pyc +0 -0
  125. pawly/gateway/__pycache__/wrapper.cpython-313.pyc +0 -0
  126. pawly/gateway/adapter_support.py +56 -0
  127. pawly/gateway/protocol.py +24 -0
  128. pawly/gateway/result.py +67 -0
  129. pawly/gateway/wrapper.py +273 -0
  130. pawly/loader/__pycache__/schema_loader.cpython-311.pyc +0 -0
  131. pawly/loader/__pycache__/schema_loader.cpython-313.pyc +0 -0
  132. pawly/loader/__pycache__/yaml_loader.cpython-311.pyc +0 -0
  133. pawly/loader/__pycache__/yaml_loader.cpython-313.pyc +0 -0
  134. pawly/loader/schema_loader.py +51 -0
  135. pawly/loader/yaml_loader.py +164 -0
  136. pawly/memory/__pycache__/store.cpython-311.pyc +0 -0
  137. pawly/memory/__pycache__/store.cpython-313.pyc +0 -0
  138. pawly/memory/__pycache__/tiers.cpython-311.pyc +0 -0
  139. pawly/memory/__pycache__/tiers.cpython-313.pyc +0 -0
  140. pawly/memory/store.py +23 -0
  141. pawly/memory/tiers.py +9 -0
  142. pawly/middleware/__init__.py +1 -0
  143. pawly/middleware/__pycache__/__init__.cpython-311.pyc +0 -0
  144. pawly/middleware/__pycache__/__init__.cpython-313.pyc +0 -0
  145. pawly/middleware/__pycache__/hooks.cpython-311.pyc +0 -0
  146. pawly/middleware/__pycache__/hooks.cpython-313.pyc +0 -0
  147. pawly/middleware/hooks.py +20 -0
  148. pawly/pawprint_loader.py +301 -0
  149. pawly/performance/__pycache__/metrics.cpython-311.pyc +0 -0
  150. pawly/performance/__pycache__/metrics.cpython-313.pyc +0 -0
  151. pawly/performance/__pycache__/report.cpython-311.pyc +0 -0
  152. pawly/performance/__pycache__/report.cpython-313.pyc +0 -0
  153. pawly/performance/metrics.py +48 -0
  154. pawly/performance/report.py +44 -0
  155. pawly/policy/__init__.py +15 -0
  156. pawly/policy/__pycache__/__init__.cpython-311.pyc +0 -0
  157. pawly/policy/__pycache__/__init__.cpython-313.pyc +0 -0
  158. pawly/policy/__pycache__/base.cpython-311.pyc +0 -0
  159. pawly/policy/__pycache__/base.cpython-313.pyc +0 -0
  160. pawly/policy/__pycache__/heuristic.cpython-311.pyc +0 -0
  161. pawly/policy/__pycache__/heuristic.cpython-313.pyc +0 -0
  162. pawly/policy/__pycache__/resolve.cpython-311.pyc +0 -0
  163. pawly/policy/__pycache__/resolve.cpython-313.pyc +0 -0
  164. pawly/policy/base.py +84 -0
  165. pawly/policy/cloud.py +18 -0
  166. pawly/policy/heuristic.py +141 -0
  167. pawly/policy/resolve.py +60 -0
  168. pawly/policy_engine/__pycache__/decision.cpython-311.pyc +0 -0
  169. pawly/policy_engine/__pycache__/decision.cpython-313.pyc +0 -0
  170. pawly/policy_engine/__pycache__/engine.cpython-311.pyc +0 -0
  171. pawly/policy_engine/__pycache__/engine.cpython-313.pyc +0 -0
  172. pawly/policy_engine/__pycache__/models.cpython-311.pyc +0 -0
  173. pawly/policy_engine/__pycache__/models.cpython-313.pyc +0 -0
  174. pawly/policy_engine/__pycache__/runtime_overlay.cpython-311.pyc +0 -0
  175. pawly/policy_engine/__pycache__/runtime_overlay.cpython-313.pyc +0 -0
  176. pawly/policy_engine/decision.py +40 -0
  177. pawly/policy_engine/engine.py +152 -0
  178. pawly/policy_engine/models.py +62 -0
  179. pawly/policy_engine/risk.py +24 -0
  180. pawly/policy_engine/runtime_overlay.py +37 -0
  181. pawly/protected_oss.py +168 -0
  182. pawly/runtime.py +1004 -0
  183. pawly/runtime_bootstrap.py +104 -0
  184. pawly/runtime_decision.py +52 -0
  185. pawly/runtime_execute.py +59 -0
  186. pawly/runtime_flow.py +50 -0
  187. pawly/runtime_ids.py +19 -0
  188. pawly/runtime_report.py +18 -0
  189. pawly/runtime_request.py +23 -0
  190. pawly/runtime_result.py +142 -0
  191. pawly/runtime_scoring.py +25 -0
  192. pawly/schemas/decision.schema.json +38 -0
  193. pawly/schemas/intent.schema.json +33 -0
  194. pawly/schemas/report.schema.json +36 -0
  195. pawly/shield.py +436 -0
  196. pawly/skill_registry.py +31 -0
  197. pawly/task_request.py +27 -0
  198. pawly/types.py +15 -0
  199. pawly/validator/__pycache__/validator.cpython-311.pyc +0 -0
  200. pawly/validator/__pycache__/validator.cpython-313.pyc +0 -0
  201. pawly/validator/validator.py +177 -0
  202. pawly-0.1.0.dist-info/METADATA +7 -0
  203. pawly-0.1.0.dist-info/RECORD +206 -0
  204. pawly-0.1.0.dist-info/WHEEL +4 -0
  205. pawly-0.1.0.dist-info/entry_points.txt +2 -0
  206. pawly-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,301 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from pawly.loader.yaml_loader import load_yaml_file
10
+ from pawly.validator.validator import PawprintValidator, SchemaValidationError
11
+
12
+ LOGGER = logging.getLogger(__name__)
13
+
14
+ PROTECTED_SKILL_WARNING = (
15
+ "This skill declares skill-protection metadata. Open-source Pawly provides baseline protection only. "
16
+ "For stronger skill-protection controls and managed enforcement, upgrade at https://aploy.ai."
17
+ )
18
+
19
+ PRIVATE_SKILL_FIELDS = {
20
+ "raw_prompt",
21
+ "core_prompt",
22
+ "private_prompt",
23
+ "private_rubric",
24
+ "private_examples",
25
+ "private_assets",
26
+ "private_notes",
27
+ "internal_rules",
28
+ "developer_secret",
29
+ "hidden_instructions",
30
+ }
31
+
32
+ PUBLIC_SKILL_CARD_FIELDS = {
33
+ "input_schema",
34
+ "output_schema",
35
+ "public_usage_notes",
36
+ }
37
+
38
+
39
+ @dataclass(slots=True)
40
+ class SkillProtection:
41
+ level: str
42
+ raw_prompt_visible_to_model: bool
43
+ examples_visible_to_model: bool
44
+ allow_prompt_export: bool
45
+ allow_training_use: bool
46
+ allow_distillation: bool
47
+ require_no_train_route: bool
48
+ watermark_outputs: bool
49
+ monitor_extraction: bool
50
+ max_calls_per_user_per_day: float | None = None
51
+
52
+ def to_dict(self) -> dict[str, Any]:
53
+ payload = {
54
+ "level": self.level,
55
+ "raw_prompt_visible_to_model": self.raw_prompt_visible_to_model,
56
+ "examples_visible_to_model": self.examples_visible_to_model,
57
+ "allow_prompt_export": self.allow_prompt_export,
58
+ "allow_training_use": self.allow_training_use,
59
+ "allow_distillation": self.allow_distillation,
60
+ "require_no_train_route": self.require_no_train_route,
61
+ "watermark_outputs": self.watermark_outputs,
62
+ "monitor_extraction": self.monitor_extraction,
63
+ }
64
+ if self.max_calls_per_user_per_day is not None:
65
+ payload["max_calls_per_user_per_day"] = self.max_calls_per_user_per_day
66
+ return payload
67
+
68
+
69
+ @dataclass(slots=True)
70
+ class SkillLicense:
71
+ type: str
72
+ attribution_required: bool
73
+
74
+ def to_dict(self) -> dict[str, Any]:
75
+ return {
76
+ "type": self.type,
77
+ "attribution_required": self.attribution_required,
78
+ }
79
+
80
+
81
+ @dataclass(slots=True)
82
+ class SkillMetadata:
83
+ protection: SkillProtection | None = None
84
+ license: SkillLicense | None = None
85
+ model_visible_context: dict[str, Any] = field(default_factory=dict)
86
+
87
+ def to_dict(self) -> dict[str, Any]:
88
+ payload: dict[str, Any] = {}
89
+ if self.protection is not None:
90
+ payload["protection"] = self.protection.to_dict()
91
+ if self.license is not None:
92
+ payload["license"] = self.license.to_dict()
93
+ if self.model_visible_context:
94
+ payload["model_visible_context"] = dict(self.model_visible_context)
95
+ return payload
96
+
97
+
98
+ @dataclass(slots=True)
99
+ class ProtectionConfig:
100
+ level: str = "standard"
101
+ assets: list[str] = field(default_factory=list)
102
+ handling: str = "auto"
103
+
104
+ def to_dict(self) -> dict[str, Any]:
105
+ return {
106
+ "level": self.level,
107
+ "assets": list(self.assets),
108
+ "handling": self.handling,
109
+ }
110
+
111
+
112
+ @dataclass(slots=True)
113
+ class PawprintConfig:
114
+ id: str
115
+ name: str
116
+ description: str
117
+ capabilities: list[str] = field(default_factory=list)
118
+ allowed_actions: list[str] = field(default_factory=list)
119
+ review_actions: list[str] = field(default_factory=list)
120
+ blocked_actions: list[str] = field(default_factory=list)
121
+ protection: ProtectionConfig | None = None
122
+ skill_metadata: SkillMetadata | None = None
123
+ model_visible_skill_context: dict[str, Any] = field(default_factory=dict)
124
+
125
+ def to_dict(self) -> dict[str, Any]:
126
+ payload = {
127
+ "id": self.id,
128
+ "name": self.name,
129
+ "description": self.description,
130
+ "capabilities": list(self.capabilities),
131
+ "actions": {
132
+ "allowed": list(self.allowed_actions),
133
+ "requiring_review": list(self.review_actions),
134
+ "blocked": list(self.blocked_actions),
135
+ },
136
+ }
137
+ if self.protection is not None:
138
+ payload["protection"] = self.protection.to_dict()
139
+ if self.skill_metadata is not None:
140
+ payload["skill"] = self.skill_metadata.to_dict()
141
+ if self.model_visible_skill_context:
142
+ payload["model_visible_skill_context"] = dict(self.model_visible_skill_context)
143
+ return payload
144
+
145
+ def resolved_protection(self) -> ProtectionConfig:
146
+ if self.protection is not None:
147
+ return self.protection
148
+ return ProtectionConfig()
149
+
150
+
151
+ @dataclass(slots=True)
152
+ class LoadedPawprint:
153
+ source_path: Path
154
+ raw_document: dict[str, Any]
155
+ config: PawprintConfig
156
+
157
+
158
+ def load_pawprint_file(path: str | Path, validator: PawprintValidator | None = None) -> LoadedPawprint:
159
+ resolved_path = Path(path)
160
+ raw_document = _load_document(resolved_path)
161
+ validated_validator = validator or PawprintValidator()
162
+ validation = validated_validator.validate_agent_config(raw_document)
163
+ if not validation.valid:
164
+ raise SchemaValidationError("; ".join(validation.errors))
165
+ return LoadedPawprint(
166
+ source_path=resolved_path,
167
+ raw_document=raw_document,
168
+ config=parse_pawprint_document(raw_document),
169
+ )
170
+
171
+
172
+ def parse_pawprint_document(raw_document: dict[str, Any]) -> PawprintConfig:
173
+ metadata = _mapping(raw_document.get("metadata"))
174
+ boundaries = _mapping(raw_document.get("boundaries"))
175
+ protection = _mapping(raw_document.get("protection"))
176
+ skill = _mapping(raw_document.get("skill"))
177
+ capabilities = raw_document.get("capabilities", [])
178
+ skill_metadata = _parse_skill_metadata(metadata, skill)
179
+
180
+ return PawprintConfig(
181
+ id=str(metadata["id"]),
182
+ name=str(metadata["name"]),
183
+ description=str(metadata["description"]),
184
+ capabilities=[
185
+ str(item.get("name", "")).strip()
186
+ for item in capabilities
187
+ if isinstance(item, dict) and str(item.get("name", "")).strip()
188
+ ],
189
+ allowed_actions=_string_list(boundaries.get("allow", [])),
190
+ review_actions=_string_list(boundaries.get("review", [])),
191
+ blocked_actions=_string_list(boundaries.get("block", [])),
192
+ protection=_parse_protection_config(protection),
193
+ skill_metadata=skill_metadata,
194
+ model_visible_skill_context={} if skill_metadata is None else dict(skill_metadata.model_visible_context),
195
+ )
196
+
197
+
198
+ def _load_document(path: Path) -> dict[str, Any]:
199
+ suffix = path.suffix.lower()
200
+ if suffix == ".json":
201
+ data = json.loads(path.read_text(encoding="utf-8"))
202
+ else:
203
+ data = load_yaml_file(path)
204
+ if not isinstance(data, dict):
205
+ raise SchemaValidationError("Pawprint document must be a JSON or YAML object")
206
+ return data
207
+
208
+
209
+ def _mapping(value: Any) -> dict[str, Any]:
210
+ if isinstance(value, dict):
211
+ return value
212
+ return {}
213
+
214
+
215
+ def _string_list(value: Any) -> list[str]:
216
+ if not isinstance(value, list):
217
+ return []
218
+ return [str(item).strip() for item in value if str(item).strip()]
219
+
220
+
221
+ def _parse_skill_metadata(metadata: dict[str, Any], skill: dict[str, Any]) -> SkillMetadata | None:
222
+ if not skill:
223
+ return None
224
+
225
+ protection = _parse_skill_protection(skill.get("protection"))
226
+ if protection is not None and protection.level in {"protected", "vault"}:
227
+ LOGGER.warning(PROTECTED_SKILL_WARNING)
228
+
229
+ license = _parse_skill_license(skill.get("license"))
230
+ return SkillMetadata(
231
+ protection=protection,
232
+ license=license,
233
+ model_visible_context=build_model_visible_skill_context(metadata, skill),
234
+ )
235
+
236
+
237
+ def _parse_skill_protection(value: Any) -> SkillProtection | None:
238
+ mapping = _mapping(value)
239
+ if not mapping:
240
+ return None
241
+ limit = mapping.get("max_calls_per_user_per_day")
242
+ max_calls = None if limit is None else float(limit)
243
+ return SkillProtection(
244
+ level=str(mapping.get("level", "")),
245
+ raw_prompt_visible_to_model=bool(mapping.get("raw_prompt_visible_to_model")),
246
+ examples_visible_to_model=bool(mapping.get("examples_visible_to_model")),
247
+ allow_prompt_export=bool(mapping.get("allow_prompt_export")),
248
+ allow_training_use=bool(mapping.get("allow_training_use")),
249
+ allow_distillation=bool(mapping.get("allow_distillation")),
250
+ require_no_train_route=bool(mapping.get("require_no_train_route")),
251
+ watermark_outputs=bool(mapping.get("watermark_outputs")),
252
+ monitor_extraction=bool(mapping.get("monitor_extraction")),
253
+ max_calls_per_user_per_day=max_calls,
254
+ )
255
+
256
+
257
+ def _parse_skill_license(value: Any) -> SkillLicense | None:
258
+ mapping = _mapping(value)
259
+ if not mapping:
260
+ return None
261
+ return SkillLicense(
262
+ type=str(mapping.get("type", "")),
263
+ attribution_required=bool(mapping.get("attribution_required")),
264
+ )
265
+
266
+
267
+ def _parse_protection_config(value: Any) -> ProtectionConfig | None:
268
+ mapping = _mapping(value)
269
+ if not mapping:
270
+ return None
271
+ return ProtectionConfig(
272
+ level=str(mapping.get("level", "standard")).strip() or "standard",
273
+ assets=_string_list(mapping.get("assets", [])),
274
+ handling=str(mapping.get("handling", "auto")).strip() or "auto",
275
+ )
276
+
277
+
278
+ def build_model_visible_skill_context(metadata: dict[str, Any], skill: dict[str, Any]) -> dict[str, Any]:
279
+ context: dict[str, Any] = {}
280
+ name = metadata.get("name")
281
+ description = metadata.get("description")
282
+ if isinstance(name, str) and name.strip():
283
+ context["name"] = name
284
+ if isinstance(description, str) and description.strip():
285
+ context["description"] = description
286
+
287
+ for key in PUBLIC_SKILL_CARD_FIELDS:
288
+ value = skill.get(key)
289
+ if value is not None:
290
+ context[key] = value
291
+
292
+ public_card = skill.get("public_skill_card")
293
+ if isinstance(public_card, dict):
294
+ for key, value in public_card.items():
295
+ if key in PRIVATE_SKILL_FIELDS:
296
+ continue
297
+ context[key] = value
298
+
299
+ for key in PRIVATE_SKILL_FIELDS:
300
+ context.pop(key, None)
301
+ return context
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import Counter
4
+
5
+
6
+ def summarize_decisions(decisions: list[dict]) -> dict:
7
+ counts = Counter(item.get("type", "unknown") for item in decisions)
8
+ total = sum(counts.values())
9
+ return {
10
+ "total_decisions": total,
11
+ "allow": counts.get("allow", 0),
12
+ "deny": counts.get("deny", 0),
13
+ "require_approval": counts.get("require_approval", 0),
14
+ "simulate": counts.get("simulate", 0),
15
+ }
16
+
17
+
18
+ def summarize_policy_evaluations(decisions: list[dict]) -> dict:
19
+ counts = Counter(item.get("policy_evaluation", {}).get("type", "unknown") for item in decisions)
20
+ total = sum(counts.values())
21
+ return {
22
+ "total_policy_evaluations": total,
23
+ "allow": counts.get("allow", 0),
24
+ "deny": counts.get("deny", 0),
25
+ "require_approval": counts.get("require_approval", 0),
26
+ "simulate": counts.get("simulate", 0),
27
+ }
28
+
29
+
30
+ def summarize_runtime_overlays(decisions: list[dict]) -> dict:
31
+ overlay_decisions = 0
32
+ budget_exhausted = 0
33
+ budget_warning_decisions = 0
34
+ for item in decisions:
35
+ overlays = item.get("runtime_overlays", {})
36
+ budget = overlays.get("budget", {})
37
+ has_overlay = bool(overlays.get("overlay_applied"))
38
+ if budget.get("exhausted"):
39
+ budget_exhausted += 1
40
+ if budget.get("warnings"):
41
+ budget_warning_decisions += 1
42
+ if has_overlay:
43
+ overlay_decisions += 1
44
+ return {
45
+ "overlay_decisions": overlay_decisions,
46
+ "budget_exhausted": budget_exhausted,
47
+ "budget_warning_decisions": budget_warning_decisions,
48
+ }
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+
5
+ from pawly.pawprint_loader import PawprintConfig
6
+ from pawly.performance.metrics import summarize_decisions, summarize_policy_evaluations, summarize_runtime_overlays
7
+
8
+
9
+ def build_daily_report(agent_config: PawprintConfig, decisions: list[dict]) -> dict:
10
+ metrics = summarize_decisions(decisions)
11
+ policy_metrics = summarize_policy_evaluations(decisions)
12
+ overlay_metrics = summarize_runtime_overlays(decisions)
13
+ report = {
14
+ "worker_id": agent_config.id,
15
+ "generated_at": datetime.now(timezone.utc).isoformat(),
16
+ "summary": {
17
+ "status": "failed" if metrics["deny"] else "needs-review" if (metrics["require_approval"] or metrics["simulate"]) else "ok",
18
+ "notes": [
19
+ f"total_decisions={metrics['total_decisions']}",
20
+ f"allow={metrics['allow']}",
21
+ f"require_approval={metrics['require_approval']}",
22
+ f"deny={metrics['deny']}",
23
+ f"simulate={metrics['simulate']}",
24
+ ],
25
+ },
26
+ "policy_summary": {
27
+ "status": "failed" if policy_metrics["deny"] else "needs-review" if (policy_metrics["require_approval"] or policy_metrics["simulate"]) else "ok",
28
+ "notes": [
29
+ f"total_policy_evaluations={policy_metrics['total_policy_evaluations']}",
30
+ f"allow={policy_metrics['allow']}",
31
+ f"require_approval={policy_metrics['require_approval']}",
32
+ f"deny={policy_metrics['deny']}",
33
+ f"simulate={policy_metrics['simulate']}",
34
+ ],
35
+ },
36
+ "runtime_overlay_summary": {
37
+ "notes": [
38
+ f"overlay_decisions={overlay_metrics['overlay_decisions']}",
39
+ f"budget_exhausted={overlay_metrics['budget_exhausted']}",
40
+ f"budget_warning_decisions={overlay_metrics['budget_warning_decisions']}",
41
+ ],
42
+ },
43
+ }
44
+ return report
@@ -0,0 +1,15 @@
1
+ """Action-scoring policy abstractions for Pawly OSS."""
2
+
3
+ from .base import Policy
4
+ from .heuristic import DefaultOssPolicy, HeuristicPolicy
5
+ from .resolve import resolve_action_routing_policies, resolve_reviewer_policy, resolve_reviewer_selection, resolve_scoring_policy
6
+
7
+ __all__ = [
8
+ "DefaultOssPolicy",
9
+ "HeuristicPolicy",
10
+ "Policy",
11
+ "resolve_action_routing_policies",
12
+ "resolve_reviewer_policy",
13
+ "resolve_reviewer_selection",
14
+ "resolve_scoring_policy",
15
+ ]
pawly/policy/base.py ADDED
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import Sequence
5
+ from typing import Any
6
+
7
+ from pawly.contracts import Action, PolicyScore
8
+
9
+
10
+ class ScoringPolicyUnavailableError(RuntimeError):
11
+ """Raised when a scoring policy is configured but cannot be used."""
12
+
13
+
14
+ class Policy(ABC):
15
+ """Scores already-filtered candidate actions without executing or constraining them."""
16
+
17
+ name = "custom"
18
+ source = "custom"
19
+ policy_source = "custom"
20
+ supports_scoring = None
21
+ supports_scoring_decision = False
22
+
23
+ def source_name(self) -> str:
24
+ return str(getattr(self, "source", getattr(self, "policy_source", "custom")))
25
+
26
+ def is_scoring_capable(self) -> bool:
27
+ support_mode = getattr(self, "supports_scoring", getattr(self, "supports_scoring_decision", False))
28
+ if support_mode is None:
29
+ support_mode = getattr(self, "supports_scoring_decision", False)
30
+ return bool(support_mode is True)
31
+
32
+ def scoring_support_mode(self) -> str:
33
+ support_mode = getattr(self, "supports_scoring", getattr(self, "supports_scoring_decision", False))
34
+ if support_mode is None:
35
+ support_mode = getattr(self, "supports_scoring_decision", False)
36
+ if support_mode is True:
37
+ return "native"
38
+ if support_mode == "fallback":
39
+ return "fallback"
40
+ return "none"
41
+
42
+ def is_scoring_available(self) -> bool:
43
+ return self.is_scoring_capable()
44
+
45
+ def scoring_unavailable_reason(self) -> str | None:
46
+ return None
47
+
48
+ def local_policy(self) -> "Policy":
49
+ return self
50
+
51
+ def fallback_scoring_policy(self) -> "Policy | None":
52
+ return None
53
+
54
+ @abstractmethod
55
+ def evaluate(self, state: Any, actions: Sequence[Action]) -> list[PolicyScore]:
56
+ """Return one score per action in the same order as the input sequence."""
57
+
58
+
59
+ _SOURCE_PREFIX = "source:"
60
+
61
+
62
+ def score_source(score: PolicyScore, default_source: str = "custom") -> str:
63
+ for tag in score.audit_tags:
64
+ if tag.startswith(_SOURCE_PREFIX):
65
+ return tag.split(":", 1)[1]
66
+ return default_source
67
+
68
+
69
+ def tag_scores(scores: Sequence[PolicyScore], source: str) -> list[PolicyScore]:
70
+ tagged_scores: list[PolicyScore] = []
71
+ source_tag = f"{_SOURCE_PREFIX}{source}"
72
+ for score in scores:
73
+ audit_tags = [tag for tag in score.audit_tags if not tag.startswith(_SOURCE_PREFIX)]
74
+ audit_tags.append(source_tag)
75
+ tagged_scores.append(
76
+ PolicyScore(
77
+ risk_score=score.risk_score,
78
+ reason_codes=list(score.reason_codes),
79
+ matched_rules=list(score.matched_rules),
80
+ audit_tags=audit_tags,
81
+ uncertainty=score.uncertainty,
82
+ )
83
+ )
84
+ return tagged_scores
pawly/policy/cloud.py ADDED
@@ -0,0 +1,18 @@
1
+ """Compatibility shim for the optional external pawly-cloud package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib import import_module
6
+
7
+
8
+ def __getattr__(name: str):
9
+ if name != "CloudPolicy":
10
+ raise AttributeError(name)
11
+ try:
12
+ module = import_module("pawly_cloud")
13
+ except ModuleNotFoundError as exc:
14
+ raise ModuleNotFoundError(
15
+ "CloudPolicy moved to the optional sibling package 'pawly-cloud'. "
16
+ "Install or add 'pawly-cloud' and import CloudPolicy from 'pawly_cloud'."
17
+ ) from exc
18
+ return getattr(module, "CloudPolicy")
@@ -0,0 +1,141 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping, Sequence
4
+ from typing import Any
5
+
6
+ from pawly.contracts import Action, PolicyScore
7
+
8
+ from .base import Policy, tag_scores
9
+
10
+
11
+ _HIGH_IMPACT_TERMS = frozenset({"approve", "delete", "deploy", "publish", "refund", "remove", "send"})
12
+ _LOW_IMPACT_TERMS = frozenset({"answer", "draft", "fetch", "list", "preview", "read", "summarize", "view"})
13
+
14
+
15
+ class HeuristicPolicy(Policy):
16
+ """Deterministic OSS baseline for ranking already-allowed actions."""
17
+
18
+ name = "heuristic"
19
+ source = "heuristic"
20
+ policy_source = "heuristic"
21
+ supports_scoring = "fallback"
22
+ supports_scoring_decision = False
23
+
24
+ def evaluate(self, state: Any, actions: Sequence[Action]) -> list[PolicyScore]:
25
+ preferred_targets = _preferred_targets(state)
26
+ recent_failures = _recent_failures(state)
27
+ return tag_scores(
28
+ [
29
+ _score_action(
30
+ action,
31
+ preferred_targets=preferred_targets,
32
+ recent_failures=recent_failures,
33
+ )
34
+ for action in actions
35
+ ],
36
+ self.source_name(),
37
+ )
38
+
39
+
40
+ DefaultOssPolicy = HeuristicPolicy
41
+
42
+
43
+ def _score_action(
44
+ action: Action,
45
+ *,
46
+ preferred_targets: set[str],
47
+ recent_failures: set[str],
48
+ ) -> PolicyScore:
49
+ score = 0.5
50
+ reason_codes: list[str] = []
51
+ matched_rules: list[str] = []
52
+ audit_tags: list[str] = ["policy:heuristic"]
53
+
54
+ if action.target:
55
+ normalized_target = action.target.strip().lower()
56
+ if normalized_target in preferred_targets:
57
+ score -= 0.1
58
+ reason_codes.append("preferred_target")
59
+ audit_tags.append(f"target:{normalized_target}")
60
+ else:
61
+ score += 0.05
62
+ reason_codes.append("unrecognized_target")
63
+ else:
64
+ score += 0.03
65
+ reason_codes.append("missing_target")
66
+
67
+ normalized_name = action.name.strip().lower()
68
+ if normalized_name in recent_failures:
69
+ score += 0.2
70
+ reason_codes.append("recent_failure")
71
+ audit_tags.append("history:retry")
72
+
73
+ action_terms = _tokenize(action.name)
74
+ if action_terms & _LOW_IMPACT_TERMS:
75
+ score -= 0.05
76
+ reason_codes.append("low_friction_action")
77
+
78
+ complexity_penalty = _complexity_penalty(action)
79
+ if complexity_penalty:
80
+ score += complexity_penalty
81
+ reason_codes.append("action_complexity")
82
+
83
+ impact_adjustment, impact_code = _impact_adjustment(action.name)
84
+ score += impact_adjustment
85
+ if impact_code is not None:
86
+ reason_codes.append(impact_code)
87
+
88
+ return PolicyScore(
89
+ risk_score=_clamp(score),
90
+ reason_codes=reason_codes,
91
+ matched_rules=matched_rules,
92
+ audit_tags=audit_tags,
93
+ )
94
+
95
+
96
+ def _preferred_targets(state: Any) -> set[str]:
97
+ if not isinstance(state, Mapping):
98
+ return set()
99
+ raw_targets = state.get("preferred_targets", [])
100
+ if not isinstance(raw_targets, Sequence) or isinstance(raw_targets, (str, bytes)):
101
+ return set()
102
+ return {str(item).strip().lower() for item in raw_targets if str(item).strip()}
103
+
104
+
105
+ def _recent_failures(state: Any) -> set[str]:
106
+ if not isinstance(state, Mapping):
107
+ return set()
108
+ raw_failures = state.get("recent_failures", [])
109
+ if not isinstance(raw_failures, Sequence) or isinstance(raw_failures, (str, bytes)):
110
+ return set()
111
+ return {str(item).strip().lower() for item in raw_failures if str(item).strip()}
112
+
113
+
114
+ def _complexity_penalty(action: Action) -> float:
115
+ penalty = 0.0
116
+ if len(action.arguments) > 4:
117
+ penalty += 0.05
118
+ for value in action.arguments.values():
119
+ if isinstance(value, Mapping):
120
+ penalty += 0.05
121
+ elif isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
122
+ penalty += 0.03
123
+ return min(penalty, 0.15)
124
+
125
+
126
+ def _impact_adjustment(action_name: str) -> tuple[float, str | None]:
127
+ terms = _tokenize(action_name)
128
+ if terms & _HIGH_IMPACT_TERMS:
129
+ return 0.2, "high_impact_action"
130
+ if terms & _LOW_IMPACT_TERMS:
131
+ return -0.1, "low_impact_action"
132
+ return 0.0, None
133
+
134
+
135
+ def _tokenize(value: str) -> set[str]:
136
+ normalized = value.replace("-", " ").replace("_", " ").lower()
137
+ return {token for token in normalized.split() if token}
138
+
139
+
140
+ def _clamp(value: float) -> float:
141
+ return max(0.0, min(1.0, round(value, 4)))