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