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/shield.py
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field, replace
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pawly.action_selection import ActionCandidate
|
|
8
|
+
from pawly.contracts import Action, PolicyScore
|
|
9
|
+
from pawly.pawprint_loader import PawprintConfig
|
|
10
|
+
|
|
11
|
+
_EXTERNAL_WRITE_TERMS = frozenset({"delete", "email", "message", "post", "purchase", "send", "transfer", "update"})
|
|
12
|
+
_PROMPT_INJECTION_PATTERNS = (
|
|
13
|
+
r"ignore previous instructions",
|
|
14
|
+
r"reveal system prompt",
|
|
15
|
+
r"show hidden instructions",
|
|
16
|
+
r"dump tool schema",
|
|
17
|
+
r"bypass policy",
|
|
18
|
+
r"print hidden instructions",
|
|
19
|
+
r"show retrieved context",
|
|
20
|
+
)
|
|
21
|
+
_SECRET_PATTERNS = (
|
|
22
|
+
r"sk-[a-zA-Z0-9]{16,}",
|
|
23
|
+
r"api[_-]?key[\s:=]+[a-zA-Z0-9_\-]{8,}",
|
|
24
|
+
r"token[\s:=]+[a-zA-Z0-9_\-]{8,}",
|
|
25
|
+
r"-----BEGIN [A-Z ]+PRIVATE KEY-----",
|
|
26
|
+
)
|
|
27
|
+
_EMAIL_PATTERN = r"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b"
|
|
28
|
+
_PHONE_PATTERN = r"\b(?:\+?\d[\d .-]{7,}\d)\b"
|
|
29
|
+
_SYSTEM_LEAK_PATTERNS = (
|
|
30
|
+
r"system prompt",
|
|
31
|
+
r"developer instructions",
|
|
32
|
+
r"hidden instructions",
|
|
33
|
+
r"tool schema",
|
|
34
|
+
r"retrieved context",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(slots=True)
|
|
39
|
+
class ShieldInputPolicy:
|
|
40
|
+
inspection: str
|
|
41
|
+
pii: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(slots=True)
|
|
45
|
+
class ShieldExecutionPolicy:
|
|
46
|
+
approval: str
|
|
47
|
+
rate_limit: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(slots=True)
|
|
51
|
+
class ShieldOutputPolicy:
|
|
52
|
+
inspection: str
|
|
53
|
+
pii: str
|
|
54
|
+
secret_handling: str
|
|
55
|
+
failure_mode: str
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(slots=True)
|
|
59
|
+
class ShieldTracePolicy:
|
|
60
|
+
input_storage: str
|
|
61
|
+
output_storage: str
|
|
62
|
+
cot_storage: str = "none"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(slots=True)
|
|
66
|
+
class ShieldAuditPolicy:
|
|
67
|
+
mode: str
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(slots=True)
|
|
71
|
+
class ShieldEnvelope:
|
|
72
|
+
mode: str
|
|
73
|
+
level: str
|
|
74
|
+
handling: str
|
|
75
|
+
assets: list[str] = field(default_factory=list)
|
|
76
|
+
input: ShieldInputPolicy = field(default_factory=lambda: ShieldInputPolicy("basic", "off"))
|
|
77
|
+
execution: ShieldExecutionPolicy = field(default_factory=lambda: ShieldExecutionPolicy("high_risk", "default"))
|
|
78
|
+
output: ShieldOutputPolicy = field(default_factory=lambda: ShieldOutputPolicy("basic", "off", "redact", "redact"))
|
|
79
|
+
trace: ShieldTracePolicy = field(default_factory=lambda: ShieldTracePolicy("redacted", "redacted"))
|
|
80
|
+
audit: ShieldAuditPolicy = field(default_factory=lambda: ShieldAuditPolicy("standard"))
|
|
81
|
+
|
|
82
|
+
def to_dict(self) -> dict[str, Any]:
|
|
83
|
+
return {
|
|
84
|
+
"mode": self.mode,
|
|
85
|
+
"level": self.level,
|
|
86
|
+
"handling": self.handling,
|
|
87
|
+
"assets": list(self.assets),
|
|
88
|
+
"input": {"inspection": self.input.inspection, "pii": self.input.pii},
|
|
89
|
+
"execution": {"approval": self.execution.approval, "rate_limit": self.execution.rate_limit},
|
|
90
|
+
"output": {
|
|
91
|
+
"inspection": self.output.inspection,
|
|
92
|
+
"pii": self.output.pii,
|
|
93
|
+
"secret_handling": self.output.secret_handling,
|
|
94
|
+
"failure_mode": self.output.failure_mode,
|
|
95
|
+
},
|
|
96
|
+
"trace": {
|
|
97
|
+
"input_storage": self.trace.input_storage,
|
|
98
|
+
"output_storage": self.trace.output_storage,
|
|
99
|
+
"cot_storage": self.trace.cot_storage,
|
|
100
|
+
},
|
|
101
|
+
"audit": {"mode": self.audit.mode},
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass(slots=True)
|
|
106
|
+
class OutputProtectionResult:
|
|
107
|
+
status: str
|
|
108
|
+
result: Any
|
|
109
|
+
reasons: list[str] = field(default_factory=list)
|
|
110
|
+
redactions: list[str] = field(default_factory=list)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class ShieldPolicy:
|
|
114
|
+
def envelope_for(self, pawprint: PawprintConfig) -> ShieldEnvelope:
|
|
115
|
+
config = pawprint.resolved_protection()
|
|
116
|
+
assets = list(config.assets)
|
|
117
|
+
envelope = _base_envelope(config.level, config.handling, assets)
|
|
118
|
+
envelope = _apply_handling_adjustments(envelope)
|
|
119
|
+
return envelope
|
|
120
|
+
|
|
121
|
+
def apply_to_candidate(
|
|
122
|
+
self,
|
|
123
|
+
candidate: ActionCandidate,
|
|
124
|
+
*,
|
|
125
|
+
pawprint: PawprintConfig,
|
|
126
|
+
state: dict[str, Any] | None = None,
|
|
127
|
+
) -> tuple[str, ActionCandidate, list[str]]:
|
|
128
|
+
envelope = self.envelope_for(pawprint)
|
|
129
|
+
reasons: list[str] = []
|
|
130
|
+
action = candidate.action
|
|
131
|
+
risk_score = 1.0 if candidate.score.risk_score is None else float(candidate.score.risk_score)
|
|
132
|
+
action_name = action.name.strip().lower()
|
|
133
|
+
action_text = _flatten_value(action.arguments).lower()
|
|
134
|
+
actor_context = _actor_context(state)
|
|
135
|
+
|
|
136
|
+
requires_review = candidate.requires_review
|
|
137
|
+
boundary_type = candidate.boundary_type
|
|
138
|
+
|
|
139
|
+
is_external_write = "external_write" in envelope.assets and _looks_like_external_write(action_name)
|
|
140
|
+
if is_external_write and envelope.execution.approval in {"external_write", "always"}:
|
|
141
|
+
requires_review = True
|
|
142
|
+
boundary_type = "review"
|
|
143
|
+
reasons.append("external_write_requires_review")
|
|
144
|
+
|
|
145
|
+
if _contains_secret(action_text):
|
|
146
|
+
risk_score = max(risk_score, 0.92)
|
|
147
|
+
reasons.append("secret_like_argument_detected")
|
|
148
|
+
if envelope.level == "confidential":
|
|
149
|
+
return "block", _replace_candidate(candidate, boundary_type="blocked", requires_review=True, risk_score=risk_score, reasons=reasons), reasons
|
|
150
|
+
requires_review = True
|
|
151
|
+
boundary_type = "review"
|
|
152
|
+
|
|
153
|
+
if _contains_prompt_injection(action_text):
|
|
154
|
+
risk_score = max(risk_score, 0.8)
|
|
155
|
+
reasons.append("prompt_injection_pattern_detected")
|
|
156
|
+
if envelope.level in {"protected", "confidential"} or envelope.handling == "strict":
|
|
157
|
+
requires_review = True
|
|
158
|
+
boundary_type = "review"
|
|
159
|
+
|
|
160
|
+
if envelope.execution.approval == "high_risk" and risk_score >= 0.8:
|
|
161
|
+
requires_review = True
|
|
162
|
+
boundary_type = "review"
|
|
163
|
+
reasons.append("high_risk_requires_review")
|
|
164
|
+
|
|
165
|
+
if envelope.execution.approval == "always":
|
|
166
|
+
requires_review = True
|
|
167
|
+
boundary_type = "review"
|
|
168
|
+
reasons.append("always_requires_review")
|
|
169
|
+
|
|
170
|
+
if is_external_write and envelope.level in {"protected", "confidential"} and not actor_context["has_identity"]:
|
|
171
|
+
requires_review = True
|
|
172
|
+
boundary_type = "review"
|
|
173
|
+
reasons.append("missing_actor_context_requires_review")
|
|
174
|
+
|
|
175
|
+
if envelope.level == "confidential" and risk_score >= 0.9:
|
|
176
|
+
reasons.append("confidential_high_risk_requires_review")
|
|
177
|
+
requires_review = True
|
|
178
|
+
boundary_type = "review"
|
|
179
|
+
|
|
180
|
+
updated = _replace_candidate(candidate, boundary_type=boundary_type, requires_review=requires_review, risk_score=risk_score, reasons=reasons)
|
|
181
|
+
return ("review" if requires_review else "allow"), updated, reasons
|
|
182
|
+
|
|
183
|
+
def sanitize_action(self, action: Action, envelope: ShieldEnvelope) -> tuple[Action, list[str]]:
|
|
184
|
+
redactions: list[str] = []
|
|
185
|
+
updated_arguments = _sanitize_value(
|
|
186
|
+
action.arguments,
|
|
187
|
+
pii_mode=envelope.input.pii,
|
|
188
|
+
inspection_mode=envelope.input.inspection,
|
|
189
|
+
redactions=redactions,
|
|
190
|
+
)
|
|
191
|
+
if updated_arguments == action.arguments:
|
|
192
|
+
return action, redactions
|
|
193
|
+
return Action(name=action.name, arguments=updated_arguments, target=action.target), redactions
|
|
194
|
+
|
|
195
|
+
def protect_output(self, result: Any, envelope: ShieldEnvelope) -> OutputProtectionResult:
|
|
196
|
+
inspected = result
|
|
197
|
+
redactions: list[str] = []
|
|
198
|
+
reasons: list[str] = []
|
|
199
|
+
|
|
200
|
+
if envelope.output.pii == "redact":
|
|
201
|
+
inspected = _redact_pii_value(inspected, redactions)
|
|
202
|
+
|
|
203
|
+
detected_issue = False
|
|
204
|
+
if envelope.output.inspection != "off":
|
|
205
|
+
flattened = _flatten_value(inspected).lower()
|
|
206
|
+
if _contains_secret(flattened):
|
|
207
|
+
detected_issue = True
|
|
208
|
+
reasons.append("output_secret_detected")
|
|
209
|
+
if envelope.output.inspection == "strict" and _contains_system_leak(flattened):
|
|
210
|
+
detected_issue = True
|
|
211
|
+
reasons.append("output_system_leak_detected")
|
|
212
|
+
|
|
213
|
+
if detected_issue and envelope.output.secret_handling == "redact":
|
|
214
|
+
inspected = _redact_secret_value(inspected, redactions)
|
|
215
|
+
if detected_issue and envelope.output.secret_handling == "review":
|
|
216
|
+
return OutputProtectionResult(status="needs_review", result=inspected, reasons=reasons, redactions=redactions)
|
|
217
|
+
if detected_issue and envelope.output.secret_handling == "block":
|
|
218
|
+
return OutputProtectionResult(status="blocked", result=None, reasons=reasons, redactions=redactions)
|
|
219
|
+
|
|
220
|
+
if detected_issue and envelope.output.failure_mode == "review":
|
|
221
|
+
return OutputProtectionResult(status="needs_review", result=inspected, reasons=reasons, redactions=redactions)
|
|
222
|
+
if detected_issue and envelope.output.failure_mode == "block":
|
|
223
|
+
return OutputProtectionResult(status="blocked", result=None, reasons=reasons, redactions=redactions)
|
|
224
|
+
|
|
225
|
+
return OutputProtectionResult(status="completed", result=inspected, reasons=reasons, redactions=redactions)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _base_envelope(level: str, handling: str, assets: list[str]) -> ShieldEnvelope:
|
|
229
|
+
pii_mode = "redact" if "customer_data" in assets else "off"
|
|
230
|
+
if level == "open":
|
|
231
|
+
envelope = ShieldEnvelope(
|
|
232
|
+
mode="opaque",
|
|
233
|
+
level="open",
|
|
234
|
+
handling=handling,
|
|
235
|
+
assets=assets,
|
|
236
|
+
input=ShieldInputPolicy("off", "off"),
|
|
237
|
+
execution=ShieldExecutionPolicy("none", "default"),
|
|
238
|
+
output=ShieldOutputPolicy("basic", "off", "redact", "redact"),
|
|
239
|
+
trace=ShieldTracePolicy("redacted", "redacted"),
|
|
240
|
+
audit=ShieldAuditPolicy("minimal"),
|
|
241
|
+
)
|
|
242
|
+
elif level == "protected":
|
|
243
|
+
envelope = ShieldEnvelope(
|
|
244
|
+
mode="opaque",
|
|
245
|
+
level="protected",
|
|
246
|
+
handling=handling,
|
|
247
|
+
assets=assets,
|
|
248
|
+
input=ShieldInputPolicy("basic", pii_mode),
|
|
249
|
+
execution=ShieldExecutionPolicy("external_write" if "external_write" in assets else "high_risk", "strict" if {"paid_api", "internal_workflow"} & set(assets) else "default"),
|
|
250
|
+
output=ShieldOutputPolicy("strict", pii_mode, "review", "review" if "external_write" in assets else "redact"),
|
|
251
|
+
trace=ShieldTracePolicy("summary" if {"customer_data", "private_knowledge"} & set(assets) else "redacted", "summary" if {"customer_data", "private_knowledge"} & set(assets) else "redacted"),
|
|
252
|
+
audit=ShieldAuditPolicy("detailed"),
|
|
253
|
+
)
|
|
254
|
+
elif level == "confidential":
|
|
255
|
+
envelope = ShieldEnvelope(
|
|
256
|
+
mode="opaque",
|
|
257
|
+
level="confidential",
|
|
258
|
+
handling=handling,
|
|
259
|
+
assets=assets,
|
|
260
|
+
input=ShieldInputPolicy("strict", "redact"),
|
|
261
|
+
execution=ShieldExecutionPolicy("external_write" if "external_write" in assets else "high_risk", "strict"),
|
|
262
|
+
output=ShieldOutputPolicy("strict", "redact", "block", "review"),
|
|
263
|
+
trace=ShieldTracePolicy("summary", "summary"),
|
|
264
|
+
audit=ShieldAuditPolicy("detailed"),
|
|
265
|
+
)
|
|
266
|
+
else:
|
|
267
|
+
envelope = ShieldEnvelope(
|
|
268
|
+
mode="opaque",
|
|
269
|
+
level="standard",
|
|
270
|
+
handling=handling,
|
|
271
|
+
assets=assets,
|
|
272
|
+
input=ShieldInputPolicy("basic", pii_mode),
|
|
273
|
+
execution=ShieldExecutionPolicy("high_risk", "default"),
|
|
274
|
+
output=ShieldOutputPolicy("basic", pii_mode, "redact", "redact"),
|
|
275
|
+
trace=ShieldTracePolicy("redacted", "redacted"),
|
|
276
|
+
audit=ShieldAuditPolicy("standard"),
|
|
277
|
+
)
|
|
278
|
+
return envelope
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _apply_handling_adjustments(envelope: ShieldEnvelope) -> ShieldEnvelope:
|
|
282
|
+
assets = set(envelope.assets)
|
|
283
|
+
trace_input = envelope.trace.input_storage
|
|
284
|
+
trace_output = envelope.trace.output_storage
|
|
285
|
+
output_failure = envelope.output.failure_mode
|
|
286
|
+
execution_approval = envelope.execution.approval
|
|
287
|
+
audit_mode = envelope.audit.mode
|
|
288
|
+
input_inspection = envelope.input.inspection
|
|
289
|
+
output_inspection = envelope.output.inspection
|
|
290
|
+
|
|
291
|
+
if envelope.handling == "cautious":
|
|
292
|
+
trace_input = "summary" if trace_input == "raw" else trace_input
|
|
293
|
+
trace_output = "summary" if trace_output == "raw" else trace_output
|
|
294
|
+
if output_failure == "allow":
|
|
295
|
+
output_failure = "redact"
|
|
296
|
+
if output_failure == "redact" and "external_write" in assets:
|
|
297
|
+
output_failure = "review"
|
|
298
|
+
if "external_write" in assets:
|
|
299
|
+
execution_approval = "external_write"
|
|
300
|
+
elif envelope.handling == "strict":
|
|
301
|
+
input_inspection = "strict"
|
|
302
|
+
output_inspection = "strict"
|
|
303
|
+
trace_input = "summary" if trace_input not in {"summary", "none"} else trace_input
|
|
304
|
+
trace_output = "summary" if trace_output not in {"summary", "none"} else trace_output
|
|
305
|
+
if envelope.level == "confidential" and "external_write" in assets:
|
|
306
|
+
execution_approval = "always"
|
|
307
|
+
if envelope.level != "open":
|
|
308
|
+
audit_mode = "detailed"
|
|
309
|
+
|
|
310
|
+
return ShieldEnvelope(
|
|
311
|
+
mode=envelope.mode,
|
|
312
|
+
level=envelope.level,
|
|
313
|
+
handling=envelope.handling,
|
|
314
|
+
assets=list(envelope.assets),
|
|
315
|
+
input=ShieldInputPolicy(input_inspection, envelope.input.pii),
|
|
316
|
+
execution=ShieldExecutionPolicy(execution_approval, envelope.execution.rate_limit),
|
|
317
|
+
output=ShieldOutputPolicy(output_inspection, envelope.output.pii, envelope.output.secret_handling, output_failure),
|
|
318
|
+
trace=ShieldTracePolicy(trace_input, trace_output, envelope.trace.cot_storage),
|
|
319
|
+
audit=ShieldAuditPolicy(audit_mode),
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _replace_candidate(candidate: ActionCandidate, *, boundary_type: str, requires_review: bool, risk_score: float, reasons: list[str]) -> ActionCandidate:
|
|
324
|
+
score = PolicyScore(
|
|
325
|
+
risk_score=round(min(1.0, max(0.0, risk_score)), 4),
|
|
326
|
+
reason_codes=list(dict.fromkeys([*candidate.score.reason_codes, *reasons])),
|
|
327
|
+
matched_rules=list(candidate.score.matched_rules),
|
|
328
|
+
audit_tags=list(dict.fromkeys([*candidate.score.audit_tags, *[f"shield:{reason}" for reason in reasons]])),
|
|
329
|
+
uncertainty=candidate.score.uncertainty,
|
|
330
|
+
)
|
|
331
|
+
reason = reasons[0] if reasons else candidate.reason
|
|
332
|
+
return replace(candidate, boundary_type=boundary_type, requires_review=requires_review, score=score, reason=reason)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _looks_like_external_write(action_name: str) -> bool:
|
|
336
|
+
tokens = set(action_name.replace("-", " ").replace("_", " ").split())
|
|
337
|
+
return bool(tokens & _EXTERNAL_WRITE_TERMS)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _contains_prompt_injection(text: str) -> bool:
|
|
341
|
+
return any(re.search(pattern, text, flags=re.IGNORECASE) for pattern in _PROMPT_INJECTION_PATTERNS)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _contains_secret(text: str) -> bool:
|
|
345
|
+
return any(re.search(pattern, text, flags=re.IGNORECASE) for pattern in _SECRET_PATTERNS)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _contains_system_leak(text: str) -> bool:
|
|
349
|
+
return any(re.search(pattern, text, flags=re.IGNORECASE) for pattern in _SYSTEM_LEAK_PATTERNS)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _sanitize_value(value: Any, *, pii_mode: str, inspection_mode: str, redactions: list[str]) -> Any:
|
|
353
|
+
if isinstance(value, dict):
|
|
354
|
+
return {key: _sanitize_value(item, pii_mode=pii_mode, inspection_mode=inspection_mode, redactions=redactions) for key, item in value.items()}
|
|
355
|
+
if isinstance(value, list):
|
|
356
|
+
return [_sanitize_value(item, pii_mode=pii_mode, inspection_mode=inspection_mode, redactions=redactions) for item in value]
|
|
357
|
+
if not isinstance(value, str):
|
|
358
|
+
return value
|
|
359
|
+
result = value
|
|
360
|
+
if pii_mode == "redact":
|
|
361
|
+
updated = re.sub(_EMAIL_PATTERN, "[redacted-email]", result, flags=re.IGNORECASE)
|
|
362
|
+
if updated != result:
|
|
363
|
+
redactions.append("input_email_redacted")
|
|
364
|
+
result = updated
|
|
365
|
+
updated = re.sub(_PHONE_PATTERN, "[redacted-phone]", result, flags=re.IGNORECASE)
|
|
366
|
+
if updated != result:
|
|
367
|
+
redactions.append("input_phone_redacted")
|
|
368
|
+
result = updated
|
|
369
|
+
updated = re.sub(r"\b(?:sk-[A-Za-z0-9]{16,}|[A-Za-z0-9_\-]{24,})\b", "[redacted-token]", result)
|
|
370
|
+
if updated != result:
|
|
371
|
+
redactions.append("input_token_redacted")
|
|
372
|
+
result = updated
|
|
373
|
+
if inspection_mode in {"basic", "strict"} and _contains_prompt_injection(result):
|
|
374
|
+
redactions.append("input_prompt_injection_detected")
|
|
375
|
+
return result
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _redact_pii_value(value: Any, redactions: list[str]) -> Any:
|
|
379
|
+
if isinstance(value, dict):
|
|
380
|
+
return {key: _redact_pii_value(item, redactions) for key, item in value.items()}
|
|
381
|
+
if isinstance(value, list):
|
|
382
|
+
return [_redact_pii_value(item, redactions) for item in value]
|
|
383
|
+
if not isinstance(value, str):
|
|
384
|
+
return value
|
|
385
|
+
updated = re.sub(_EMAIL_PATTERN, "[redacted-email]", value, flags=re.IGNORECASE)
|
|
386
|
+
if updated != value:
|
|
387
|
+
redactions.append("output_email_redacted")
|
|
388
|
+
value = updated
|
|
389
|
+
updated = re.sub(_PHONE_PATTERN, "[redacted-phone]", value, flags=re.IGNORECASE)
|
|
390
|
+
if updated != value:
|
|
391
|
+
redactions.append("output_phone_redacted")
|
|
392
|
+
return updated
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _redact_secret_value(value: Any, redactions: list[str]) -> Any:
|
|
396
|
+
if isinstance(value, dict):
|
|
397
|
+
return {key: _redact_secret_value(item, redactions) for key, item in value.items()}
|
|
398
|
+
if isinstance(value, list):
|
|
399
|
+
return [_redact_secret_value(item, redactions) for item in value]
|
|
400
|
+
if not isinstance(value, str):
|
|
401
|
+
return value
|
|
402
|
+
result = value
|
|
403
|
+
for pattern in _SECRET_PATTERNS:
|
|
404
|
+
updated = re.sub(pattern, "[redacted-secret]", result, flags=re.IGNORECASE)
|
|
405
|
+
if updated != result:
|
|
406
|
+
redactions.append("output_secret_redacted")
|
|
407
|
+
result = updated
|
|
408
|
+
return result
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _flatten_value(value: Any) -> str:
|
|
412
|
+
if value is None:
|
|
413
|
+
return ""
|
|
414
|
+
if isinstance(value, str):
|
|
415
|
+
return value
|
|
416
|
+
if isinstance(value, (int, float, bool)):
|
|
417
|
+
return str(value)
|
|
418
|
+
if isinstance(value, dict):
|
|
419
|
+
return " ".join(_flatten_value(item) for item in value.values())
|
|
420
|
+
if isinstance(value, list):
|
|
421
|
+
return " ".join(_flatten_value(item) for item in value)
|
|
422
|
+
return str(value)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _actor_context(state: dict[str, Any] | None) -> dict[str, bool]:
|
|
426
|
+
if not isinstance(state, dict):
|
|
427
|
+
return {"has_identity": False}
|
|
428
|
+
actor = state.get("actor")
|
|
429
|
+
if not isinstance(actor, dict):
|
|
430
|
+
return {"has_identity": False}
|
|
431
|
+
tenant_id = str(actor.get("tenant_id", "")).strip()
|
|
432
|
+
agent_id = str(actor.get("agent_id", "")).strip()
|
|
433
|
+
user_id = str(actor.get("user_id", "")).strip()
|
|
434
|
+
return {
|
|
435
|
+
"has_identity": bool(tenant_id or agent_id or user_id),
|
|
436
|
+
}
|
pawly/skill_registry.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pawly.contracts import Action
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
SkillHandler = Callable[[dict[str, Any], dict[str, Any]], Any]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MissingSkillRegistryError(RuntimeError):
|
|
13
|
+
"""Raised when run_actions is used before a skill registry is registered."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SkillRegistry:
|
|
17
|
+
def __init__(self) -> None:
|
|
18
|
+
self._handlers: dict[str, SkillHandler] = {}
|
|
19
|
+
|
|
20
|
+
def register(self, action_name: str, handler: SkillHandler) -> None:
|
|
21
|
+
normalized = str(action_name).strip()
|
|
22
|
+
if not normalized:
|
|
23
|
+
raise ValueError("action_name must not be empty")
|
|
24
|
+
self._handlers[normalized] = handler
|
|
25
|
+
|
|
26
|
+
def execute(self, action: Action, context: dict[str, Any] | None = None) -> Any:
|
|
27
|
+
handler = self._handlers.get(action.name)
|
|
28
|
+
if handler is None:
|
|
29
|
+
raise KeyError(f"no skill registered for action: {action.name}")
|
|
30
|
+
return handler(dict(action.arguments), dict(context or {}))
|
|
31
|
+
|
pawly/task_request.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pawly.contracts import Action, Intent, IntentSource
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(slots=True)
|
|
10
|
+
class TaskRequest:
|
|
11
|
+
task: str
|
|
12
|
+
action: str
|
|
13
|
+
confidence: float
|
|
14
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
15
|
+
|
|
16
|
+
def to_intent(self, *, intent_id: str = "intent-from-task-request") -> Intent:
|
|
17
|
+
return Intent(
|
|
18
|
+
intent_id=intent_id,
|
|
19
|
+
source=IntentSource.EXECUTION_REQUEST,
|
|
20
|
+
action=Action(
|
|
21
|
+
name=self.action,
|
|
22
|
+
arguments={"task": self.task, **self.metadata},
|
|
23
|
+
),
|
|
24
|
+
summary=self.task,
|
|
25
|
+
confidence=self.confidence,
|
|
26
|
+
metadata=self.metadata,
|
|
27
|
+
)
|
pawly/types.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pawly.contracts import Action
|
|
4
|
+
from pawly.contracts import Decision
|
|
5
|
+
from pawly.contracts import DecisionSource
|
|
6
|
+
from pawly.contracts import DecisionState
|
|
7
|
+
from pawly.contracts import Intent
|
|
8
|
+
from pawly.contracts import IntentSource
|
|
9
|
+
from pawly.policy_engine.models import PolicyEvaluation
|
|
10
|
+
from pawly.task_request import TaskRequest
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
DecisionType = DecisionState
|
|
14
|
+
|
|
15
|
+
IntentAction = Action
|
|
Binary file
|
|
Binary file
|