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