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
pawly/gateway/result.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
|
|
6
|
+
from pawly.contracts import Decision, Intent
|
|
7
|
+
from pawly.audit.diff import diff_actions
|
|
8
|
+
from pawly.audit.events import AuditEvent
|
|
9
|
+
from pawly.loader.schema_loader import load_pawprint_version
|
|
10
|
+
from pawly.runtime_result import RuntimeDecisionResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_gateway_payload(
|
|
14
|
+
decision_result: RuntimeDecisionResult,
|
|
15
|
+
*,
|
|
16
|
+
execution: dict[str, Any],
|
|
17
|
+
approval: dict[str, Any] | None = None,
|
|
18
|
+
) -> dict[str, Any]:
|
|
19
|
+
payload = {
|
|
20
|
+
**decision_result.to_dict(),
|
|
21
|
+
"execution": execution,
|
|
22
|
+
}
|
|
23
|
+
if approval is not None:
|
|
24
|
+
payload["approval"] = approval
|
|
25
|
+
return payload
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_governed_execution_event(
|
|
29
|
+
*,
|
|
30
|
+
event_id: str | None,
|
|
31
|
+
original_intent: Intent,
|
|
32
|
+
decision_result: RuntimeDecisionResult,
|
|
33
|
+
payload: dict[str, Any],
|
|
34
|
+
) -> AuditEvent:
|
|
35
|
+
executed_action = payload.get("execution", {}).get("used_action")
|
|
36
|
+
action = original_intent.action.to_dict()
|
|
37
|
+
return AuditEvent.from_governed_execution(
|
|
38
|
+
event_id=event_id or f"event-{uuid4().hex}",
|
|
39
|
+
decision_id=decision_result.decision_id,
|
|
40
|
+
agent_id=decision_result.agent_id,
|
|
41
|
+
pawprint_version=decision_result.pawprint_version or load_pawprint_version(),
|
|
42
|
+
outcome=decision_result.decision.type.value,
|
|
43
|
+
original_intent=original_intent.to_dict(),
|
|
44
|
+
normalized_intent=decision_result.intent.to_dict(),
|
|
45
|
+
action=action,
|
|
46
|
+
policy_evaluation=decision_result.policy_evaluation.to_dict(),
|
|
47
|
+
runtime_overlays=decision_result.runtime_overlays,
|
|
48
|
+
policy_references=decision_result.decision.matched_rules,
|
|
49
|
+
final_decision=decision_result.decision.to_dict(),
|
|
50
|
+
reason_codes=decision_result.decision.reason_codes,
|
|
51
|
+
approval=payload.get("approval"),
|
|
52
|
+
executed_action=executed_action,
|
|
53
|
+
action_diff=diff_actions(action, executed_action),
|
|
54
|
+
execution=payload.get("execution"),
|
|
55
|
+
execution_result_ref=_execution_result_ref(payload.get("execution", {}).get("result")),
|
|
56
|
+
risk_score=decision_result.decision.risk_score,
|
|
57
|
+
escalated_to=decision_result.policy_evaluation.handoff_target,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _execution_result_ref(result: Any) -> str | None:
|
|
62
|
+
if isinstance(result, dict):
|
|
63
|
+
for key in ("result_ref", "artifact_ref", "reference", "uri", "path"):
|
|
64
|
+
value = result.get(key)
|
|
65
|
+
if isinstance(value, str) and value:
|
|
66
|
+
return value
|
|
67
|
+
return None
|
pawly/gateway/wrapper.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Callable
|
|
5
|
+
|
|
6
|
+
from pawly.approval.handler import ApprovalHandler
|
|
7
|
+
from pawly.approval.models import ApprovalStatus
|
|
8
|
+
from pawly.approval.router import ApprovalRouter
|
|
9
|
+
from pawly.backends.approval import ApprovalBackend, LocalApprovalBackend
|
|
10
|
+
from pawly.backends.audit import AuditSink
|
|
11
|
+
from pawly.backends.reviewer import ReviewerPolicy
|
|
12
|
+
from pawly.backends.risk import RiskProvider
|
|
13
|
+
from pawly.decision_engine import DecisionEngine
|
|
14
|
+
from pawly.gateway.protocol import ExecutorFunc
|
|
15
|
+
from pawly.gateway.result import build_gateway_payload, build_governed_execution_event
|
|
16
|
+
from pawly.policy import resolve_reviewer_selection
|
|
17
|
+
from pawly.runtime_request import build_task_request_intent
|
|
18
|
+
from pawly.runtime_result import RuntimeDecisionResult
|
|
19
|
+
from pawly.types import Intent
|
|
20
|
+
|
|
21
|
+
ExecuteFn = Callable[[str, str, float, dict[str, Any] | None], Any]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ExecutionGateway:
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
decision_engine: DecisionEngine,
|
|
28
|
+
approval_handler: ApprovalHandler | None = None,
|
|
29
|
+
policy: str = "rules",
|
|
30
|
+
approval_router: ApprovalRouter | None = None,
|
|
31
|
+
approval_backend: ApprovalBackend | None = None,
|
|
32
|
+
runtime: DecisionEngine | None = None,
|
|
33
|
+
reviewer: str | None = None,
|
|
34
|
+
) -> None:
|
|
35
|
+
self.runtime = runtime or decision_engine
|
|
36
|
+
self.policy = reviewer or policy
|
|
37
|
+
self.reviewer = self.policy
|
|
38
|
+
self.approval_backend = approval_backend or LocalApprovalBackend(
|
|
39
|
+
handler=approval_handler,
|
|
40
|
+
router=approval_router,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def execute(
|
|
44
|
+
self,
|
|
45
|
+
*,
|
|
46
|
+
task: str,
|
|
47
|
+
action: str,
|
|
48
|
+
confidence: float,
|
|
49
|
+
executor: ExecutorFunc,
|
|
50
|
+
metadata: dict[str, Any] | None = None,
|
|
51
|
+
) -> dict[str, Any]:
|
|
52
|
+
return self.execute_intent(
|
|
53
|
+
build_task_request_intent(
|
|
54
|
+
task=task,
|
|
55
|
+
action=action,
|
|
56
|
+
confidence=confidence,
|
|
57
|
+
metadata=metadata,
|
|
58
|
+
),
|
|
59
|
+
executor,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def execute_intent(self, intent: Intent, executor: ExecutorFunc) -> dict[str, Any]:
|
|
63
|
+
decision_result = (
|
|
64
|
+
self.runtime.evaluate_intent_result(intent)
|
|
65
|
+
if hasattr(self.runtime, "evaluate_intent_result")
|
|
66
|
+
else None
|
|
67
|
+
)
|
|
68
|
+
if decision_result is None:
|
|
69
|
+
from pawly.runtime_result import RuntimeDecisionResult
|
|
70
|
+
|
|
71
|
+
decision_result = RuntimeDecisionResult.from_dict(self.runtime.evaluate_intent(intent))
|
|
72
|
+
if decision_result.decision.type.value == "require_approval":
|
|
73
|
+
record = self.approval_backend.submit(intent, decision_result.decision)
|
|
74
|
+
approval = self.approval_backend.status_payload(record)
|
|
75
|
+
if record.status == ApprovalStatus.APPROVED:
|
|
76
|
+
approved_intent = record.approved_intent()
|
|
77
|
+
result = executor(approved_intent)
|
|
78
|
+
payload = build_gateway_payload(
|
|
79
|
+
decision_result,
|
|
80
|
+
approval=approval,
|
|
81
|
+
execution={
|
|
82
|
+
"attempted": True,
|
|
83
|
+
"executed": True,
|
|
84
|
+
"blocked_by": None,
|
|
85
|
+
"result": result,
|
|
86
|
+
"used_action": approved_intent.action.to_dict(),
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
self._append_governed_execution_event(intent, decision_result, payload)
|
|
90
|
+
return payload
|
|
91
|
+
blocked_by = "expired" if record.status == ApprovalStatus.EXPIRED else "require_approval"
|
|
92
|
+
payload = build_gateway_payload(
|
|
93
|
+
decision_result,
|
|
94
|
+
approval=approval,
|
|
95
|
+
execution={
|
|
96
|
+
"attempted": False,
|
|
97
|
+
"executed": False,
|
|
98
|
+
"blocked_by": blocked_by,
|
|
99
|
+
"reviewer": self.policy,
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
self._append_governed_execution_event(intent, decision_result, payload)
|
|
103
|
+
return payload
|
|
104
|
+
if decision_result.decision.type.value == "simulate":
|
|
105
|
+
payload = build_gateway_payload(
|
|
106
|
+
decision_result,
|
|
107
|
+
execution={
|
|
108
|
+
"attempted": False,
|
|
109
|
+
"executed": False,
|
|
110
|
+
"blocked_by": "simulate",
|
|
111
|
+
"reviewer": self.policy,
|
|
112
|
+
},
|
|
113
|
+
)
|
|
114
|
+
self._append_governed_execution_event(intent, decision_result, payload)
|
|
115
|
+
return payload
|
|
116
|
+
if decision_result.decision.type.value != "allow":
|
|
117
|
+
payload = build_gateway_payload(
|
|
118
|
+
decision_result,
|
|
119
|
+
execution={
|
|
120
|
+
"attempted": False,
|
|
121
|
+
"executed": False,
|
|
122
|
+
"blocked_by": decision_result.decision.type.value,
|
|
123
|
+
"reviewer": self.policy,
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
self._append_governed_execution_event(intent, decision_result, payload)
|
|
127
|
+
return payload
|
|
128
|
+
|
|
129
|
+
result = executor(intent)
|
|
130
|
+
payload = build_gateway_payload(
|
|
131
|
+
decision_result,
|
|
132
|
+
execution={
|
|
133
|
+
"attempted": True,
|
|
134
|
+
"executed": True,
|
|
135
|
+
"blocked_by": None,
|
|
136
|
+
"reviewer": self.policy,
|
|
137
|
+
"result": result,
|
|
138
|
+
"used_action": intent.action.to_dict(),
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
self._append_governed_execution_event(intent, decision_result, payload)
|
|
142
|
+
return payload
|
|
143
|
+
|
|
144
|
+
def _append_governed_execution_event(
|
|
145
|
+
self,
|
|
146
|
+
original_intent: Intent,
|
|
147
|
+
decision_result: RuntimeDecisionResult,
|
|
148
|
+
payload: dict[str, Any],
|
|
149
|
+
) -> None:
|
|
150
|
+
event = build_governed_execution_event(
|
|
151
|
+
event_id=self.runtime.next_event_id() if hasattr(self.runtime, "next_event_id") else None,
|
|
152
|
+
original_intent=original_intent,
|
|
153
|
+
decision_result=decision_result,
|
|
154
|
+
payload=payload,
|
|
155
|
+
)
|
|
156
|
+
if hasattr(self.runtime, "audit_sink"):
|
|
157
|
+
self.runtime.audit_sink.append(event)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def wrap_executor(
|
|
161
|
+
executor: ExecutorFunc,
|
|
162
|
+
pawprint: str | Path,
|
|
163
|
+
policy: str = "rules",
|
|
164
|
+
*,
|
|
165
|
+
approval_handler: ApprovalHandler | None = None,
|
|
166
|
+
approval_router: ApprovalRouter | None = None,
|
|
167
|
+
approval_backend: ApprovalBackend | None = None,
|
|
168
|
+
policy_impl: ReviewerPolicy | None = None,
|
|
169
|
+
risk_provider: RiskProvider | None = None,
|
|
170
|
+
audit_sink: AuditSink | None = None,
|
|
171
|
+
audit_path: str | Path | None = None,
|
|
172
|
+
reviewer: str | None = None,
|
|
173
|
+
reviewer_backend: ReviewerPolicy | None = None,
|
|
174
|
+
) -> ExecutorFunc:
|
|
175
|
+
selected_policy, selected_policy_impl = resolve_reviewer_selection(
|
|
176
|
+
policy=policy,
|
|
177
|
+
policy_impl=policy_impl,
|
|
178
|
+
reviewer=reviewer,
|
|
179
|
+
reviewer_backend=reviewer_backend,
|
|
180
|
+
)
|
|
181
|
+
decision_engine = DecisionEngine(
|
|
182
|
+
pawprint,
|
|
183
|
+
audit_path=audit_path,
|
|
184
|
+
policy=selected_policy,
|
|
185
|
+
policy_impl=selected_policy_impl,
|
|
186
|
+
risk_provider=risk_provider,
|
|
187
|
+
audit_sink=audit_sink,
|
|
188
|
+
)
|
|
189
|
+
gateway = ExecutionGateway(
|
|
190
|
+
decision_engine,
|
|
191
|
+
approval_handler=approval_handler,
|
|
192
|
+
policy=selected_policy,
|
|
193
|
+
approval_router=approval_router,
|
|
194
|
+
approval_backend=approval_backend,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def wrapped(intent: Intent) -> dict[str, Any]:
|
|
198
|
+
return gateway.execute_intent(intent, executor)
|
|
199
|
+
|
|
200
|
+
return wrapped
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def wrap_execute_fn(
|
|
204
|
+
fn: ExecuteFn,
|
|
205
|
+
pawprint: str | Path,
|
|
206
|
+
policy: str = "rules",
|
|
207
|
+
*,
|
|
208
|
+
approval_handler: ApprovalHandler | None = None,
|
|
209
|
+
approval_router: ApprovalRouter | None = None,
|
|
210
|
+
approval_backend: ApprovalBackend | None = None,
|
|
211
|
+
policy_impl: ReviewerPolicy | None = None,
|
|
212
|
+
risk_provider: RiskProvider | None = None,
|
|
213
|
+
audit_sink: AuditSink | None = None,
|
|
214
|
+
audit_path: str | Path | None = None,
|
|
215
|
+
reviewer: str | None = None,
|
|
216
|
+
reviewer_backend: ReviewerPolicy | None = None,
|
|
217
|
+
) -> ExecuteFn:
|
|
218
|
+
selected_policy, selected_policy_impl = resolve_reviewer_selection(
|
|
219
|
+
policy=policy,
|
|
220
|
+
policy_impl=policy_impl,
|
|
221
|
+
reviewer=reviewer,
|
|
222
|
+
reviewer_backend=reviewer_backend,
|
|
223
|
+
)
|
|
224
|
+
decision_engine = DecisionEngine(
|
|
225
|
+
pawprint,
|
|
226
|
+
audit_path=audit_path,
|
|
227
|
+
policy=selected_policy,
|
|
228
|
+
policy_impl=selected_policy_impl,
|
|
229
|
+
risk_provider=risk_provider,
|
|
230
|
+
audit_sink=audit_sink,
|
|
231
|
+
)
|
|
232
|
+
gateway = ExecutionGateway(
|
|
233
|
+
decision_engine,
|
|
234
|
+
approval_handler=approval_handler,
|
|
235
|
+
policy=selected_policy,
|
|
236
|
+
approval_router=approval_router,
|
|
237
|
+
approval_backend=approval_backend,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def wrapped(task: str, action: str, confidence: float, metadata: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
241
|
+
return gateway.execute(
|
|
242
|
+
task=task,
|
|
243
|
+
action=action,
|
|
244
|
+
confidence=confidence,
|
|
245
|
+
metadata=metadata,
|
|
246
|
+
executor=lambda intent: _call_execute_fn_from_intent(fn, intent),
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
return wrapped
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def wrap_framework_adapter(
|
|
253
|
+
decision_engine: DecisionEngine,
|
|
254
|
+
framework: str = "generic",
|
|
255
|
+
*,
|
|
256
|
+
approval_handler: ApprovalHandler | None = None,
|
|
257
|
+
approval_router: ApprovalRouter | None = None,
|
|
258
|
+
approval_backend: ApprovalBackend | None = None,
|
|
259
|
+
) -> ExecutionGateway:
|
|
260
|
+
del framework
|
|
261
|
+
return ExecutionGateway(
|
|
262
|
+
decision_engine,
|
|
263
|
+
approval_handler=approval_handler,
|
|
264
|
+
policy="rules",
|
|
265
|
+
approval_router=approval_router,
|
|
266
|
+
approval_backend=approval_backend,
|
|
267
|
+
)
|
|
268
|
+
def _call_execute_fn_from_intent(fn: ExecuteFn, intent: Intent) -> Any:
|
|
269
|
+
metadata = dict(intent.metadata)
|
|
270
|
+
for key, value in intent.action.arguments.items():
|
|
271
|
+
if key != "task":
|
|
272
|
+
metadata[key] = value
|
|
273
|
+
return fn(intent.summary, intent.action.name, intent.confidence, metadata)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib import resources
|
|
4
|
+
import json
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PawprintDependencyError(RuntimeError):
|
|
10
|
+
"""Raised when the runtime cannot load schema assets."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
PAWLY_SCHEMA_DIR = Path(__file__).resolve().parents[1] / "schemas"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _schema_path(name: str) -> Path:
|
|
17
|
+
if name == "pawprint.schema.json":
|
|
18
|
+
schema_path = _pawprint_resource_path("schemas", name)
|
|
19
|
+
else:
|
|
20
|
+
schema_path = PAWLY_SCHEMA_DIR / name
|
|
21
|
+
if not schema_path.exists():
|
|
22
|
+
raise PawprintDependencyError(f"schema not found: {schema_path}")
|
|
23
|
+
return schema_path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@lru_cache(maxsize=None)
|
|
27
|
+
def load_schema(name: str) -> dict:
|
|
28
|
+
schema_path = _schema_path(name)
|
|
29
|
+
return json.loads(schema_path.read_text(encoding="utf-8"))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@lru_cache(maxsize=1)
|
|
33
|
+
def load_pawprint_version() -> str:
|
|
34
|
+
version_path = _pawprint_resource_path("VERSION")
|
|
35
|
+
if not version_path.exists():
|
|
36
|
+
raise PawprintDependencyError(f"pawprint version file not found: {version_path}")
|
|
37
|
+
return version_path.read_text(encoding="utf-8").strip()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def resolve_schema_path(name: str) -> Path:
|
|
41
|
+
return _schema_path(name)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _pawprint_resource_path(*parts: str) -> Path:
|
|
45
|
+
try:
|
|
46
|
+
resource = resources.files("pawprint").joinpath(*parts)
|
|
47
|
+
except ModuleNotFoundError as exc:
|
|
48
|
+
raise PawprintDependencyError(
|
|
49
|
+
"pawprint package is not installed. Install 'pawprint' before using pawly."
|
|
50
|
+
) from exc
|
|
51
|
+
return Path(str(resource))
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class YAMLParseError(ValueError):
|
|
8
|
+
"""Raised when the lightweight YAML parser cannot parse a document."""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def load_yaml_file(path: str | Path) -> dict:
|
|
12
|
+
text = Path(path).read_text(encoding="utf-8")
|
|
13
|
+
return loads(text)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def loads(text: str):
|
|
17
|
+
lines = []
|
|
18
|
+
for number, raw in enumerate(text.splitlines(), start=1):
|
|
19
|
+
if not raw.strip() or raw.lstrip().startswith("#"):
|
|
20
|
+
continue
|
|
21
|
+
indent = len(raw) - len(raw.lstrip(" "))
|
|
22
|
+
lines.append((number, indent, raw.strip()))
|
|
23
|
+
if not lines:
|
|
24
|
+
return {}
|
|
25
|
+
value, index = _parse_block(lines, 0, lines[0][1])
|
|
26
|
+
if index != len(lines):
|
|
27
|
+
raise YAMLParseError(f"Unexpected trailing content near line {lines[index][0]}")
|
|
28
|
+
return value
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _parse_block(lines, index: int, indent: int):
|
|
32
|
+
if index >= len(lines):
|
|
33
|
+
return {}, index
|
|
34
|
+
if lines[index][1] != indent:
|
|
35
|
+
raise YAMLParseError(f"Unexpected indentation near line {lines[index][0]}")
|
|
36
|
+
if lines[index][2].startswith("- "):
|
|
37
|
+
return _parse_sequence(lines, index, indent)
|
|
38
|
+
return _parse_mapping(lines, index, indent)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _parse_mapping(lines, index: int, indent: int):
|
|
42
|
+
mapping = {}
|
|
43
|
+
while index < len(lines):
|
|
44
|
+
line_no, line_indent, text = lines[index]
|
|
45
|
+
if line_indent < indent:
|
|
46
|
+
break
|
|
47
|
+
if line_indent > indent:
|
|
48
|
+
raise YAMLParseError(f"Unexpected indentation near line {line_no}")
|
|
49
|
+
if text.startswith("- "):
|
|
50
|
+
break
|
|
51
|
+
key, has_value, remainder = _split_key_value(text, line_no)
|
|
52
|
+
index += 1
|
|
53
|
+
if has_value:
|
|
54
|
+
mapping[key] = _parse_scalar(remainder)
|
|
55
|
+
continue
|
|
56
|
+
if index >= len(lines) or lines[index][1] <= indent:
|
|
57
|
+
mapping[key] = {}
|
|
58
|
+
continue
|
|
59
|
+
child, index = _parse_block(lines, index, lines[index][1])
|
|
60
|
+
mapping[key] = child
|
|
61
|
+
return mapping, index
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _parse_sequence(lines, index: int, indent: int):
|
|
65
|
+
items = []
|
|
66
|
+
while index < len(lines):
|
|
67
|
+
line_no, line_indent, text = lines[index]
|
|
68
|
+
if line_indent < indent:
|
|
69
|
+
break
|
|
70
|
+
if line_indent != indent or not text.startswith("- "):
|
|
71
|
+
break
|
|
72
|
+
content = text[2:].strip()
|
|
73
|
+
index += 1
|
|
74
|
+
if not content:
|
|
75
|
+
if index >= len(lines) or lines[index][1] <= indent:
|
|
76
|
+
items.append(None)
|
|
77
|
+
continue
|
|
78
|
+
child, index = _parse_block(lines, index, lines[index][1])
|
|
79
|
+
items.append(child)
|
|
80
|
+
continue
|
|
81
|
+
if _looks_like_mapping(content):
|
|
82
|
+
key, has_value, remainder = _split_key_value(content, line_no)
|
|
83
|
+
item = {}
|
|
84
|
+
if has_value:
|
|
85
|
+
item[key] = _parse_scalar(remainder)
|
|
86
|
+
else:
|
|
87
|
+
item[key] = {}
|
|
88
|
+
if index < len(lines) and lines[index][1] > indent:
|
|
89
|
+
child, index = _parse_block(lines, index, lines[index][1])
|
|
90
|
+
if not isinstance(child, dict):
|
|
91
|
+
raise YAMLParseError(f"List item near line {line_no} must contain a mapping")
|
|
92
|
+
item.update(child)
|
|
93
|
+
items.append(item)
|
|
94
|
+
continue
|
|
95
|
+
items.append(_parse_scalar(content))
|
|
96
|
+
return items, index
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _split_key_value(text: str, line_no: int):
|
|
100
|
+
if ":" not in text:
|
|
101
|
+
raise YAMLParseError(f"Expected ':' in mapping near line {line_no}")
|
|
102
|
+
key, remainder = text.split(":", 1)
|
|
103
|
+
key = key.strip()
|
|
104
|
+
if not key:
|
|
105
|
+
raise YAMLParseError(f"Empty key near line {line_no}")
|
|
106
|
+
remainder = remainder.strip()
|
|
107
|
+
return key, bool(remainder), remainder
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _looks_like_mapping(text: str) -> bool:
|
|
111
|
+
if text.startswith("[") or text.startswith("{"):
|
|
112
|
+
return False
|
|
113
|
+
return ":" in text
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _parse_scalar(value: str):
|
|
117
|
+
if value.startswith("[") and value.endswith("]"):
|
|
118
|
+
inner = value[1:-1].strip()
|
|
119
|
+
if not inner:
|
|
120
|
+
return []
|
|
121
|
+
return [_parse_scalar(part.strip()) for part in _split_flow_items(inner)]
|
|
122
|
+
if value.startswith(('"', "'")) and value.endswith(('"', "'")):
|
|
123
|
+
return value[1:-1]
|
|
124
|
+
lowered = value.lower()
|
|
125
|
+
if lowered == "true":
|
|
126
|
+
return True
|
|
127
|
+
if lowered == "false":
|
|
128
|
+
return False
|
|
129
|
+
if lowered in {"null", "none"}:
|
|
130
|
+
return None
|
|
131
|
+
if re.fullmatch(r"-?[0-9]+", value):
|
|
132
|
+
return int(value)
|
|
133
|
+
if re.fullmatch(r"-?[0-9]+\.[0-9]+", value):
|
|
134
|
+
return float(value)
|
|
135
|
+
return value
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _split_flow_items(text: str) -> list[str]:
|
|
139
|
+
items = []
|
|
140
|
+
current = []
|
|
141
|
+
quote = None
|
|
142
|
+
depth = 0
|
|
143
|
+
for char in text:
|
|
144
|
+
if quote:
|
|
145
|
+
current.append(char)
|
|
146
|
+
if char == quote:
|
|
147
|
+
quote = None
|
|
148
|
+
continue
|
|
149
|
+
if char in {'"', "'"}:
|
|
150
|
+
quote = char
|
|
151
|
+
current.append(char)
|
|
152
|
+
continue
|
|
153
|
+
if char in "[{":
|
|
154
|
+
depth += 1
|
|
155
|
+
elif char in "]}":
|
|
156
|
+
depth -= 1
|
|
157
|
+
if char == "," and depth == 0:
|
|
158
|
+
items.append("".join(current).strip())
|
|
159
|
+
current = []
|
|
160
|
+
continue
|
|
161
|
+
current.append(char)
|
|
162
|
+
if current:
|
|
163
|
+
items.append("".join(current).strip())
|
|
164
|
+
return items
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
pawly/memory/store.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pawly.memory.tiers import MemoryTier
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MemoryStore:
|
|
10
|
+
def __init__(self, mode: str = MemoryTier.NONE.value) -> None:
|
|
11
|
+
self.mode = MemoryTier(mode)
|
|
12
|
+
self._store: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
|
13
|
+
|
|
14
|
+
def remember(self, category: str, payload: dict[str, Any]) -> None:
|
|
15
|
+
if self.mode == MemoryTier.NONE:
|
|
16
|
+
return
|
|
17
|
+
self._store[category].append(payload)
|
|
18
|
+
|
|
19
|
+
def recall(self, category: str) -> list[dict[str, Any]]:
|
|
20
|
+
return list(self._store.get(category, []))
|
|
21
|
+
|
|
22
|
+
def clear(self) -> None:
|
|
23
|
+
self._store.clear()
|
pawly/memory/tiers.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Middleware helpers for the Pawly execution-boundary controller."""
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
Hook = Callable[[dict], None]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class HookRegistry:
|
|
10
|
+
def __init__(self) -> None:
|
|
11
|
+
self.before_decision: list[Hook] = []
|
|
12
|
+
self.after_decision: list[Hook] = []
|
|
13
|
+
|
|
14
|
+
def run_before(self, payload: dict) -> None:
|
|
15
|
+
for hook in self.before_decision:
|
|
16
|
+
hook(payload)
|
|
17
|
+
|
|
18
|
+
def run_after(self, payload: dict) -> None:
|
|
19
|
+
for hook in self.after_decision:
|
|
20
|
+
hook(payload)
|