openscript-sdk 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.
- openscript_sdk/__init__.py +217 -0
- openscript_sdk/contracts/__init__.py +82 -0
- openscript_sdk/contracts/interceptor.py +98 -0
- openscript_sdk/contracts/server_types.py +87 -0
- openscript_sdk/contracts/session_graph.py +123 -0
- openscript_sdk/contracts/types.py +76 -0
- openscript_sdk/events/__init__.py +0 -0
- openscript_sdk/events/approvals.py +309 -0
- openscript_sdk/events/models.py +26 -0
- openscript_sdk/events/store.py +79 -0
- openscript_sdk/events/writer.py +100 -0
- openscript_sdk/hooks/claude_code.py +479 -0
- openscript_sdk/hooks/claude_code_example.yaml +62 -0
- openscript_sdk/hooks/claude_code_strict_example.yaml +73 -0
- openscript_sdk/hooks/config.py +144 -0
- openscript_sdk/hooks/settings_example.json +28 -0
- openscript_sdk/hooks/settings_strict_example.json +28 -0
- openscript_sdk/integrations/langchain.py +32 -0
- openscript_sdk/integrations/langgraph.py +32 -0
- openscript_sdk/interceptors/base.py +15 -0
- openscript_sdk/interceptors/event_writer.py +131 -0
- openscript_sdk/interceptors/pii.py +168 -0
- openscript_sdk/interceptors/threat.py +674 -0
- openscript_sdk/logging.py +94 -0
- openscript_sdk/middleware/middleware.py +543 -0
- openscript_sdk/middleware/tool.py +247 -0
- openscript_sdk/observability/__init__.py +11 -0
- openscript_sdk/observability/metrics.py +105 -0
- openscript_sdk/observability/risk.py +54 -0
- openscript_sdk/observability/tracing.py +67 -0
- openscript_sdk/policies/compliance.py +261 -0
- openscript_sdk/policies/config.py +244 -0
- openscript_sdk/policies/harmful_request.py +617 -0
- openscript_sdk/policies/normalize.py +489 -0
- openscript_sdk/policies/output_schema.py +353 -0
- openscript_sdk/policies/policies_example.yaml +36 -0
- openscript_sdk/policies/redaction.py +359 -0
- openscript_sdk/policies/secrets.py +239 -0
- openscript_sdk/policies/semantic.py +348 -0
- openscript_sdk/policies/semantic_head.py +9 -0
- openscript_sdk/policies/session_taint.py +399 -0
- openscript_sdk/policies/taint_store.py +111 -0
- openscript_sdk/policies/tool_firewall.py +434 -0
- openscript_sdk/policies/tools_example.yaml +81 -0
- openscript_sdk/policies/toxicity.py +247 -0
- openscript_sdk/py.typed +0 -0
- openscript_sdk/server/__init__.py +1 -0
- openscript_sdk/server/app.py +153 -0
- openscript_sdk/server/auth.py +19 -0
- openscript_sdk/server/guard.py +126 -0
- openscript_sdk/server/guard_example.yaml +55 -0
- openscript_sdk/server/routers/__init__.py +0 -0
- openscript_sdk/server/routers/approvals.py +69 -0
- openscript_sdk/server/routers/guard.py +231 -0
- openscript_sdk/server/routers/sessions.py +80 -0
- openscript_sdk/server/routers/sse.py +69 -0
- openscript_sdk/server/routers/threat.py +119 -0
- openscript_sdk/server/static/dashboard/index.html +209 -0
- openscript_sdk/server/static/dashboard/session.html +339 -0
- openscript_sdk-0.1.0.dist-info/METADATA +225 -0
- openscript_sdk-0.1.0.dist-info/RECORD +65 -0
- openscript_sdk-0.1.0.dist-info/WHEEL +5 -0
- openscript_sdk-0.1.0.dist-info/entry_points.txt +2 -0
- openscript_sdk-0.1.0.dist-info/licenses/LICENSE +190 -0
- openscript_sdk-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""OpenScript: a policy pipeline for LLM agents and the tools they call."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
# Every public name, and the module it lives in. Nothing is imported until a
|
|
9
|
+
# name is first asked for, so a caller that needs one policy does not pay for
|
|
10
|
+
# all of them (a Claude Code hook pays this on every tool call).
|
|
11
|
+
_EXPORTS: dict[str, str] = {
|
|
12
|
+
"ActionBlockedError": "openscript_sdk.contracts.types",
|
|
13
|
+
"ActionContext": "openscript_sdk.contracts.types",
|
|
14
|
+
"ActionTracer": "openscript_sdk.observability.tracing",
|
|
15
|
+
"AgentCapabilities": "openscript_sdk.contracts.types",
|
|
16
|
+
"ApprovalRecord": "openscript_sdk.events.approvals",
|
|
17
|
+
"ApprovalStatus": "openscript_sdk.events.approvals",
|
|
18
|
+
"ApprovalStore": "openscript_sdk.events.approvals",
|
|
19
|
+
"AuditPolicy": "openscript_sdk.interceptors.event_writer",
|
|
20
|
+
"BasePolicy": "openscript_sdk.contracts.interceptor",
|
|
21
|
+
"CompliancePolicy": "openscript_sdk.policies.compliance",
|
|
22
|
+
"EventWriterInterceptor": "openscript_sdk.interceptors.event_writer",
|
|
23
|
+
"FailureMode": "openscript_sdk.contracts.types",
|
|
24
|
+
"GuardScan": "openscript_sdk.contracts.interceptor",
|
|
25
|
+
"HallucinationMode": "openscript_sdk.policies.output_schema",
|
|
26
|
+
"HarmfulRequestPolicy": "openscript_sdk.policies.harmful_request",
|
|
27
|
+
"InMemoryApprovalStore": "openscript_sdk.events.approvals",
|
|
28
|
+
"InMemoryTaintStore": "openscript_sdk.policies.session_taint",
|
|
29
|
+
"Interceptor": "openscript_sdk.contracts.interceptor",
|
|
30
|
+
"InterceptorDecision": "openscript_sdk.contracts.types",
|
|
31
|
+
"InterceptorResult": "openscript_sdk.contracts.types",
|
|
32
|
+
"InternalURLMode": "openscript_sdk.policies.secrets",
|
|
33
|
+
"MetricsRecorder": "openscript_sdk.observability.metrics",
|
|
34
|
+
"NoopInterceptor": "openscript_sdk.interceptors.base",
|
|
35
|
+
"OnInvalid": "openscript_sdk.policies.output_schema",
|
|
36
|
+
"OpenScriptMiddleware": "openscript_sdk.middleware.middleware",
|
|
37
|
+
"OutputSchemaPolicy": "openscript_sdk.policies.output_schema",
|
|
38
|
+
"PHIMode": "openscript_sdk.policies.compliance",
|
|
39
|
+
"PIIInterceptor": "openscript_sdk.interceptors.pii",
|
|
40
|
+
"PIIMode": "openscript_sdk.interceptors.pii",
|
|
41
|
+
"PIIPolicy": "openscript_sdk.interceptors.pii",
|
|
42
|
+
"Policy": "openscript_sdk.contracts.interceptor",
|
|
43
|
+
"PolicyConfig": "openscript_sdk.policies.config",
|
|
44
|
+
"PromptInjectionPolicy": "openscript_sdk.interceptors.threat",
|
|
45
|
+
"RedisApprovalStore": "openscript_sdk.events.approvals",
|
|
46
|
+
"RiskScorer": "openscript_sdk.observability.risk",
|
|
47
|
+
"SecretsPolicy": "openscript_sdk.policies.secrets",
|
|
48
|
+
"SecureAgent": "openscript_sdk.middleware.middleware",
|
|
49
|
+
"SecureTool": "openscript_sdk.middleware.tool",
|
|
50
|
+
"SemanticHead": "openscript_sdk.policies.semantic",
|
|
51
|
+
"SemanticMode": "openscript_sdk.policies.semantic",
|
|
52
|
+
"SemanticPolicy": "openscript_sdk.policies.semantic",
|
|
53
|
+
"SessionTaintPolicy": "openscript_sdk.policies.session_taint",
|
|
54
|
+
"StreamGuard": "openscript_sdk.contracts.interceptor",
|
|
55
|
+
"StreamOutputMode": "openscript_sdk.middleware.middleware",
|
|
56
|
+
"TaintAction": "openscript_sdk.policies.session_taint",
|
|
57
|
+
"TaintStore": "openscript_sdk.policies.session_taint",
|
|
58
|
+
"ThreatInterceptor": "openscript_sdk.interceptors.threat",
|
|
59
|
+
"ToolFirewallPolicy": "openscript_sdk.policies.tool_firewall",
|
|
60
|
+
"ToolRules": "openscript_sdk.policies.tool_firewall",
|
|
61
|
+
"ToxicityPolicy": "openscript_sdk.policies.toxicity",
|
|
62
|
+
"aggregate_risk": "openscript_sdk.observability.risk",
|
|
63
|
+
"configure_logging": "openscript_sdk.logging",
|
|
64
|
+
"find_phi": "openscript_sdk.policies.compliance",
|
|
65
|
+
"find_pii": "openscript_sdk.policies.redaction",
|
|
66
|
+
"find_secrets": "openscript_sdk.policies.secrets",
|
|
67
|
+
"load_policies": "openscript_sdk.policies.config",
|
|
68
|
+
"redact_data": "openscript_sdk.policies.redaction",
|
|
69
|
+
"redact_pii": "openscript_sdk.policies.redaction",
|
|
70
|
+
"redact_secrets": "openscript_sdk.policies.redaction",
|
|
71
|
+
"register_policy": "openscript_sdk.policies.config",
|
|
72
|
+
"validate_tool_call": "openscript_sdk.policies.tool_firewall",
|
|
73
|
+
"wrap_agent": "openscript_sdk.integrations.langchain",
|
|
74
|
+
"wrap_graph_agent": "openscript_sdk.integrations.langgraph",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if TYPE_CHECKING:
|
|
78
|
+
from openscript_sdk.contracts.interceptor import (
|
|
79
|
+
BasePolicy,
|
|
80
|
+
GuardScan,
|
|
81
|
+
Interceptor,
|
|
82
|
+
Policy,
|
|
83
|
+
StreamGuard,
|
|
84
|
+
)
|
|
85
|
+
from openscript_sdk.contracts.types import (
|
|
86
|
+
ActionBlockedError,
|
|
87
|
+
ActionContext,
|
|
88
|
+
AgentCapabilities,
|
|
89
|
+
FailureMode,
|
|
90
|
+
InterceptorDecision,
|
|
91
|
+
InterceptorResult,
|
|
92
|
+
)
|
|
93
|
+
from openscript_sdk.events.approvals import (
|
|
94
|
+
ApprovalRecord,
|
|
95
|
+
ApprovalStatus,
|
|
96
|
+
ApprovalStore,
|
|
97
|
+
InMemoryApprovalStore,
|
|
98
|
+
RedisApprovalStore,
|
|
99
|
+
)
|
|
100
|
+
from openscript_sdk.integrations.langchain import wrap_agent
|
|
101
|
+
from openscript_sdk.integrations.langgraph import wrap_graph_agent
|
|
102
|
+
from openscript_sdk.interceptors.base import NoopInterceptor
|
|
103
|
+
from openscript_sdk.interceptors.event_writer import AuditPolicy, EventWriterInterceptor
|
|
104
|
+
from openscript_sdk.interceptors.pii import PIIInterceptor, PIIMode, PIIPolicy
|
|
105
|
+
from openscript_sdk.interceptors.threat import PromptInjectionPolicy, ThreatInterceptor
|
|
106
|
+
from openscript_sdk.logging import configure_logging
|
|
107
|
+
from openscript_sdk.middleware.middleware import (
|
|
108
|
+
OpenScriptMiddleware,
|
|
109
|
+
SecureAgent,
|
|
110
|
+
StreamOutputMode,
|
|
111
|
+
)
|
|
112
|
+
from openscript_sdk.middleware.tool import SecureTool
|
|
113
|
+
from openscript_sdk.observability.metrics import MetricsRecorder
|
|
114
|
+
from openscript_sdk.observability.risk import RiskScorer, aggregate_risk
|
|
115
|
+
from openscript_sdk.observability.tracing import ActionTracer
|
|
116
|
+
from openscript_sdk.policies.compliance import CompliancePolicy, PHIMode, find_phi
|
|
117
|
+
from openscript_sdk.policies.config import PolicyConfig, load_policies, register_policy
|
|
118
|
+
from openscript_sdk.policies.harmful_request import HarmfulRequestPolicy
|
|
119
|
+
from openscript_sdk.policies.output_schema import (
|
|
120
|
+
HallucinationMode,
|
|
121
|
+
OnInvalid,
|
|
122
|
+
OutputSchemaPolicy,
|
|
123
|
+
)
|
|
124
|
+
from openscript_sdk.policies.redaction import find_pii, redact_data, redact_pii, redact_secrets
|
|
125
|
+
from openscript_sdk.policies.secrets import InternalURLMode, SecretsPolicy, find_secrets
|
|
126
|
+
from openscript_sdk.policies.semantic import SemanticHead, SemanticMode, SemanticPolicy
|
|
127
|
+
from openscript_sdk.policies.session_taint import (
|
|
128
|
+
InMemoryTaintStore,
|
|
129
|
+
SessionTaintPolicy,
|
|
130
|
+
TaintAction,
|
|
131
|
+
TaintStore,
|
|
132
|
+
)
|
|
133
|
+
from openscript_sdk.policies.tool_firewall import (
|
|
134
|
+
ToolFirewallPolicy,
|
|
135
|
+
ToolRules,
|
|
136
|
+
validate_tool_call,
|
|
137
|
+
)
|
|
138
|
+
from openscript_sdk.policies.toxicity import ToxicityPolicy
|
|
139
|
+
|
|
140
|
+
__all__ = [
|
|
141
|
+
"ActionBlockedError",
|
|
142
|
+
"ActionContext",
|
|
143
|
+
"ActionTracer",
|
|
144
|
+
"AgentCapabilities",
|
|
145
|
+
"ApprovalRecord",
|
|
146
|
+
"ApprovalStatus",
|
|
147
|
+
"ApprovalStore",
|
|
148
|
+
"AuditPolicy",
|
|
149
|
+
"BasePolicy",
|
|
150
|
+
"CompliancePolicy",
|
|
151
|
+
"EventWriterInterceptor", # deprecated alias
|
|
152
|
+
"FailureMode",
|
|
153
|
+
"GuardScan",
|
|
154
|
+
"HallucinationMode",
|
|
155
|
+
"HarmfulRequestPolicy",
|
|
156
|
+
"InMemoryApprovalStore",
|
|
157
|
+
"InMemoryTaintStore",
|
|
158
|
+
"Interceptor", # deprecated alias
|
|
159
|
+
"InterceptorDecision",
|
|
160
|
+
"InterceptorResult",
|
|
161
|
+
"InternalURLMode",
|
|
162
|
+
"MetricsRecorder",
|
|
163
|
+
"NoopInterceptor",
|
|
164
|
+
"OnInvalid",
|
|
165
|
+
"OpenScriptMiddleware", # deprecated alias
|
|
166
|
+
"OutputSchemaPolicy",
|
|
167
|
+
"PHIMode",
|
|
168
|
+
"PIIInterceptor", # deprecated alias
|
|
169
|
+
"PIIMode",
|
|
170
|
+
"PIIPolicy",
|
|
171
|
+
"Policy",
|
|
172
|
+
"PolicyConfig",
|
|
173
|
+
"PromptInjectionPolicy",
|
|
174
|
+
"RedisApprovalStore",
|
|
175
|
+
"RiskScorer",
|
|
176
|
+
"SecretsPolicy",
|
|
177
|
+
"SecureAgent",
|
|
178
|
+
"SecureTool",
|
|
179
|
+
"SemanticHead",
|
|
180
|
+
"SemanticMode",
|
|
181
|
+
"SemanticPolicy",
|
|
182
|
+
"SessionTaintPolicy",
|
|
183
|
+
"StreamGuard",
|
|
184
|
+
"StreamOutputMode",
|
|
185
|
+
"TaintAction",
|
|
186
|
+
"TaintStore",
|
|
187
|
+
"ThreatInterceptor", # deprecated alias
|
|
188
|
+
"ToolFirewallPolicy",
|
|
189
|
+
"ToolRules",
|
|
190
|
+
"ToxicityPolicy",
|
|
191
|
+
"aggregate_risk",
|
|
192
|
+
"configure_logging",
|
|
193
|
+
"find_phi",
|
|
194
|
+
"find_pii",
|
|
195
|
+
"find_secrets",
|
|
196
|
+
"load_policies",
|
|
197
|
+
"redact_data",
|
|
198
|
+
"redact_pii",
|
|
199
|
+
"redact_secrets",
|
|
200
|
+
"register_policy",
|
|
201
|
+
"validate_tool_call",
|
|
202
|
+
"wrap_agent",
|
|
203
|
+
"wrap_graph_agent",
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def __getattr__(name: str) -> Any:
|
|
208
|
+
module = _EXPORTS.get(name)
|
|
209
|
+
if module is None:
|
|
210
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
211
|
+
value = getattr(importlib.import_module(module), name)
|
|
212
|
+
globals()[name] = value # asked for once, then a plain attribute
|
|
213
|
+
return value
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def __dir__() -> list[str]:
|
|
217
|
+
return sorted(set(globals()) | set(__all__))
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""OpenScript contract: public framework types and protocols."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
# Every public name, and the module it lives in. Nothing is imported until a
|
|
9
|
+
# name is first asked for, so a caller that needs one policy does not pay for
|
|
10
|
+
# all of them (a Claude Code hook pays this on every tool call).
|
|
11
|
+
_EXPORTS: dict[str, str] = {
|
|
12
|
+
"ActionContext": "openscript_sdk.contracts.types",
|
|
13
|
+
"AgentCapabilities": "openscript_sdk.contracts.types",
|
|
14
|
+
"DetectionResult": "openscript_sdk.contracts.server_types",
|
|
15
|
+
"Event": "openscript_sdk.contracts.server_types",
|
|
16
|
+
"EventType": "openscript_sdk.contracts.server_types",
|
|
17
|
+
"FailureMode": "openscript_sdk.contracts.types",
|
|
18
|
+
"GraphEdge": "openscript_sdk.contracts.session_graph",
|
|
19
|
+
"GraphNode": "openscript_sdk.contracts.session_graph",
|
|
20
|
+
"Interceptor": "openscript_sdk.contracts.interceptor",
|
|
21
|
+
"InterceptorDecision": "openscript_sdk.contracts.types",
|
|
22
|
+
"InterceptorResult": "openscript_sdk.contracts.types",
|
|
23
|
+
"NodeType": "openscript_sdk.contracts.session_graph",
|
|
24
|
+
"PolicyEvalRequest": "openscript_sdk.contracts.server_types",
|
|
25
|
+
"PolicyEvalResponse": "openscript_sdk.contracts.server_types",
|
|
26
|
+
"SessionGraph": "openscript_sdk.contracts.session_graph",
|
|
27
|
+
"ThreatScoreRequest": "openscript_sdk.contracts.server_types",
|
|
28
|
+
"ThreatScoreResponse": "openscript_sdk.contracts.server_types",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from openscript_sdk.contracts.interceptor import Interceptor
|
|
33
|
+
from openscript_sdk.contracts.server_types import (
|
|
34
|
+
DetectionResult,
|
|
35
|
+
Event,
|
|
36
|
+
EventType,
|
|
37
|
+
PolicyEvalRequest,
|
|
38
|
+
PolicyEvalResponse,
|
|
39
|
+
ThreatScoreRequest,
|
|
40
|
+
ThreatScoreResponse,
|
|
41
|
+
)
|
|
42
|
+
from openscript_sdk.contracts.session_graph import GraphEdge, GraphNode, NodeType, SessionGraph
|
|
43
|
+
from openscript_sdk.contracts.types import (
|
|
44
|
+
ActionContext,
|
|
45
|
+
AgentCapabilities,
|
|
46
|
+
FailureMode,
|
|
47
|
+
InterceptorDecision,
|
|
48
|
+
InterceptorResult,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"ActionContext",
|
|
53
|
+
"AgentCapabilities",
|
|
54
|
+
"DetectionResult",
|
|
55
|
+
"Event",
|
|
56
|
+
"EventType",
|
|
57
|
+
"FailureMode",
|
|
58
|
+
"GraphEdge",
|
|
59
|
+
"GraphNode",
|
|
60
|
+
"Interceptor",
|
|
61
|
+
"InterceptorDecision",
|
|
62
|
+
"InterceptorResult",
|
|
63
|
+
"NodeType",
|
|
64
|
+
"PolicyEvalRequest",
|
|
65
|
+
"PolicyEvalResponse",
|
|
66
|
+
"SessionGraph",
|
|
67
|
+
"ThreatScoreRequest",
|
|
68
|
+
"ThreatScoreResponse",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def __getattr__(name: str) -> Any:
|
|
73
|
+
module = _EXPORTS.get(name)
|
|
74
|
+
if module is None:
|
|
75
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
76
|
+
value = getattr(importlib.import_module(module), name)
|
|
77
|
+
globals()[name] = value # asked for once, then a plain attribute
|
|
78
|
+
return value
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def __dir__() -> list[str]:
|
|
82
|
+
return sorted(set(globals()) | set(__all__))
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Protocol, runtime_checkable
|
|
5
|
+
|
|
6
|
+
from openscript_sdk.contracts.types import ActionContext, FailureMode
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class GuardScan:
|
|
11
|
+
"""Result of one incremental scan by a StreamGuard.
|
|
12
|
+
|
|
13
|
+
text is the (possibly redacted) input text; labels/risk mirror the
|
|
14
|
+
owning policy's metadata conventions; deny=True aborts the stream with
|
|
15
|
+
*reason* before the offending text is released.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
text: str
|
|
19
|
+
labels: list[str] = field(default_factory=list)
|
|
20
|
+
risk: float = 0.0
|
|
21
|
+
deny: bool = False
|
|
22
|
+
reason: str = ""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@runtime_checkable
|
|
26
|
+
class StreamGuard(Protocol):
|
|
27
|
+
"""Bounded incremental text scanner for guarded streaming.
|
|
28
|
+
|
|
29
|
+
max_match_len is the guard's honest upper bound on how many characters
|
|
30
|
+
one of its patterns can span — the middleware holds back a tail of that
|
|
31
|
+
size, which is what guarantees a match is never split across the
|
|
32
|
+
released/held boundary. Unbounded constructs must be handled by their
|
|
33
|
+
prefix (e.g. a PEM header) or must deny.
|
|
34
|
+
|
|
35
|
+
final=False means more text may follow: a match ending exactly at the
|
|
36
|
+
end of *text* is tentative (it could extend) and must be left untouched
|
|
37
|
+
and unreported — it sits inside the held tail, so deferring never leaks
|
|
38
|
+
it. final=True (stream end) acts on everything.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
name: str
|
|
42
|
+
max_match_len: int
|
|
43
|
+
|
|
44
|
+
def scan(self, text: str, final: bool = False) -> GuardScan: ...
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@runtime_checkable
|
|
48
|
+
class Policy(Protocol):
|
|
49
|
+
"""Protocol all OpenScript policies must satisfy."""
|
|
50
|
+
|
|
51
|
+
failure_mode: FailureMode
|
|
52
|
+
|
|
53
|
+
async def before_action(self, context: ActionContext) -> ActionContext: ...
|
|
54
|
+
async def after_action(self, context: ActionContext) -> ActionContext: ...
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class BasePolicy:
|
|
58
|
+
"""Concrete base class for policies; subclasses override only the hooks they need."""
|
|
59
|
+
|
|
60
|
+
failure_mode: FailureMode = FailureMode.FAIL_OPEN
|
|
61
|
+
|
|
62
|
+
async def before_action(self, context: ActionContext) -> ActionContext:
|
|
63
|
+
return context
|
|
64
|
+
|
|
65
|
+
async def after_action(self, context: ActionContext) -> ActionContext:
|
|
66
|
+
return context
|
|
67
|
+
|
|
68
|
+
async def on_blocked(
|
|
69
|
+
self,
|
|
70
|
+
context: ActionContext,
|
|
71
|
+
*,
|
|
72
|
+
blocked_by: str,
|
|
73
|
+
phase: str,
|
|
74
|
+
approval_id: str = "",
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Called once when the pipeline refuses an action, on every policy.
|
|
77
|
+
|
|
78
|
+
Optional, and not part of the Policy protocol: SecureAgent looks it up
|
|
79
|
+
by name. It exists for observers. A deny in the before phase stops the
|
|
80
|
+
loop, so policies listed after the one that denied never run — this is
|
|
81
|
+
how an audit policy placed last still gets to record the refusal.
|
|
82
|
+
|
|
83
|
+
*phase* is "before" (the agent never ran), "after" (the response was
|
|
84
|
+
withheld) or "stream" (a guard aborted a guarded stream). The decision
|
|
85
|
+
is already final; the hook cannot change it.
|
|
86
|
+
"""
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
def stream_guard(self) -> StreamGuard | None:
|
|
90
|
+
"""Override to participate in guarded streaming. Only policies whose
|
|
91
|
+
checks are bounded text patterns can do so; policies needing the
|
|
92
|
+
complete output (schema validation, grounding) return None and run
|
|
93
|
+
at stream end."""
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# Deprecated alias — prefer Policy
|
|
98
|
+
Interceptor = Policy
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EventType(StrEnum):
|
|
12
|
+
TURN_STARTED = "turn_start"
|
|
13
|
+
TURN_END = "turn_end"
|
|
14
|
+
TOOL_CALL = "tool_call"
|
|
15
|
+
TOOL_RESULT = "tool_result"
|
|
16
|
+
MEMORY_WRITE = "memory_write"
|
|
17
|
+
MEMORY_READ = "memory_read"
|
|
18
|
+
INTERCEPTOR_BEFORE = "interceptor_before"
|
|
19
|
+
INTERCEPTOR_AFTER = "interceptor_after"
|
|
20
|
+
THREAT_DETECTED = "threat_detected"
|
|
21
|
+
POLICY_EVALUATED = "policy_evaluated"
|
|
22
|
+
AGENT_MESSAGE = "agent_message"
|
|
23
|
+
APPROVAL_REQUESTED = "approval_requested"
|
|
24
|
+
APPROVAL_RESOLVED = "approval_resolved"
|
|
25
|
+
ACTION_BLOCKED = "action_blocked"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Event(BaseModel):
|
|
29
|
+
model_config = {"frozen": True}
|
|
30
|
+
|
|
31
|
+
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
32
|
+
session_id: str
|
|
33
|
+
agent_id: str
|
|
34
|
+
event_type: EventType
|
|
35
|
+
payload: dict[str, Any] = Field(default_factory=dict)
|
|
36
|
+
timestamp: datetime = Field(default_factory=datetime.now)
|
|
37
|
+
sequence_num: int
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ThreatScoreRequest(BaseModel):
|
|
41
|
+
session_id: str
|
|
42
|
+
agent_id: str
|
|
43
|
+
text: str
|
|
44
|
+
turn_number: int
|
|
45
|
+
context: dict[str, Any] = Field(default_factory=dict)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ThreatScoreResponse(BaseModel):
|
|
49
|
+
session_id: str
|
|
50
|
+
score: float
|
|
51
|
+
signals: dict[str, float] = Field(default_factory=dict)
|
|
52
|
+
flagged: bool = False
|
|
53
|
+
reason: str = ""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class PolicyEvalRequest(BaseModel):
|
|
57
|
+
session_id: str
|
|
58
|
+
agent_id: str
|
|
59
|
+
action: str
|
|
60
|
+
capabilities: dict[str, bool] = Field(default_factory=dict)
|
|
61
|
+
context: dict[str, Any] = Field(default_factory=dict)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class PolicyEvalResponse(BaseModel):
|
|
65
|
+
session_id: str
|
|
66
|
+
decision: str
|
|
67
|
+
reason: str = ""
|
|
68
|
+
policies_evaluated: list[str] = Field(default_factory=list)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class DetectionResult(BaseModel):
|
|
72
|
+
regex_score: float = 0.0
|
|
73
|
+
embedding_score: float = 0.0
|
|
74
|
+
final_score: float = 0.0
|
|
75
|
+
method: str = ""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ToolValidateRequest(BaseModel):
|
|
79
|
+
tool_name: str
|
|
80
|
+
args: dict[str, Any] = Field(default_factory=dict)
|
|
81
|
+
role: str | None = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ToolValidateResponse(BaseModel):
|
|
85
|
+
allowed: bool
|
|
86
|
+
reason: str
|
|
87
|
+
requires_approval: bool = False
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from openscript_sdk.contracts.server_types import Event, EventType
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from openscript_sdk.events.store import EventStore
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NodeType(StrEnum):
|
|
15
|
+
TURN = "turn"
|
|
16
|
+
TOOL_CALL = "tool_call"
|
|
17
|
+
MEMORY_WRITE = "memory_write"
|
|
18
|
+
AGENT_MESSAGE = "agent_message"
|
|
19
|
+
INTERCEPTOR = "interceptor"
|
|
20
|
+
THREAT = "threat"
|
|
21
|
+
POLICY = "policy"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_EVENT_TO_NODE: dict[EventType, NodeType] = {
|
|
25
|
+
EventType.TURN_STARTED: NodeType.TURN,
|
|
26
|
+
EventType.TURN_END: NodeType.TURN,
|
|
27
|
+
EventType.TOOL_CALL: NodeType.TOOL_CALL,
|
|
28
|
+
EventType.TOOL_RESULT: NodeType.TOOL_CALL,
|
|
29
|
+
EventType.MEMORY_WRITE: NodeType.MEMORY_WRITE,
|
|
30
|
+
EventType.MEMORY_READ: NodeType.MEMORY_WRITE,
|
|
31
|
+
EventType.INTERCEPTOR_BEFORE: NodeType.INTERCEPTOR,
|
|
32
|
+
EventType.INTERCEPTOR_AFTER: NodeType.INTERCEPTOR,
|
|
33
|
+
EventType.THREAT_DETECTED: NodeType.THREAT,
|
|
34
|
+
EventType.POLICY_EVALUATED: NodeType.POLICY,
|
|
35
|
+
EventType.AGENT_MESSAGE: NodeType.AGENT_MESSAGE,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class GraphNode:
|
|
41
|
+
id: str
|
|
42
|
+
node_type: str
|
|
43
|
+
event: Event
|
|
44
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class GraphEdge:
|
|
49
|
+
source_id: str
|
|
50
|
+
target_id: str
|
|
51
|
+
edge_type: str = "causal"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class SessionGraph:
|
|
56
|
+
session_id: str
|
|
57
|
+
nodes: dict[str, GraphNode] = field(default_factory=dict)
|
|
58
|
+
edges: list[GraphEdge] = field(default_factory=list)
|
|
59
|
+
|
|
60
|
+
def add_node(self, node: GraphNode) -> None:
|
|
61
|
+
self.nodes[node.id] = node
|
|
62
|
+
|
|
63
|
+
def add_edge(self, edge: GraphEdge) -> None:
|
|
64
|
+
self.edges.append(edge)
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def build(cls, session_id: str, events: list[Event]) -> SessionGraph:
|
|
68
|
+
"""Build a DAG from an ordered list of events.
|
|
69
|
+
|
|
70
|
+
Nodes map 1:1 to events. Edges encode sequential causality and
|
|
71
|
+
parent-child relationships (tool calls / memory writes nested
|
|
72
|
+
inside turns).
|
|
73
|
+
"""
|
|
74
|
+
sorted_events = sorted(events, key=lambda e: e.sequence_num)
|
|
75
|
+
graph = cls(session_id=session_id)
|
|
76
|
+
|
|
77
|
+
prev_node_id: str | None = None
|
|
78
|
+
turn_stack: list[str] = []
|
|
79
|
+
|
|
80
|
+
for event in sorted_events:
|
|
81
|
+
node_type = _EVENT_TO_NODE.get(event.event_type, NodeType.AGENT_MESSAGE)
|
|
82
|
+
node = GraphNode(
|
|
83
|
+
id=event.id,
|
|
84
|
+
node_type=node_type.value,
|
|
85
|
+
event=event,
|
|
86
|
+
)
|
|
87
|
+
graph.add_node(node)
|
|
88
|
+
|
|
89
|
+
if prev_node_id is not None:
|
|
90
|
+
graph.add_edge(GraphEdge(source_id=prev_node_id, target_id=node.id))
|
|
91
|
+
|
|
92
|
+
if event.event_type == EventType.TURN_STARTED:
|
|
93
|
+
turn_stack.append(node.id)
|
|
94
|
+
elif event.event_type in (EventType.TOOL_CALL, EventType.MEMORY_WRITE):
|
|
95
|
+
if turn_stack:
|
|
96
|
+
graph.add_edge(
|
|
97
|
+
GraphEdge(
|
|
98
|
+
source_id=turn_stack[-1],
|
|
99
|
+
target_id=node.id,
|
|
100
|
+
edge_type="parent",
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
elif event.event_type == EventType.TURN_END and turn_stack:
|
|
104
|
+
turn_stack.pop()
|
|
105
|
+
|
|
106
|
+
prev_node_id = node.id
|
|
107
|
+
|
|
108
|
+
return graph
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
async def from_live_stream(cls, events: AsyncIterator[Event]) -> SessionGraph:
|
|
112
|
+
collected: list[Event] = []
|
|
113
|
+
session_id = ""
|
|
114
|
+
async for event in events:
|
|
115
|
+
if not session_id:
|
|
116
|
+
session_id = event.session_id
|
|
117
|
+
collected.append(event)
|
|
118
|
+
return cls.build(session_id=session_id, events=collected)
|
|
119
|
+
|
|
120
|
+
@classmethod
|
|
121
|
+
async def from_store(cls, session_id: str, event_store: EventStore) -> SessionGraph:
|
|
122
|
+
events = await event_store.get_events(session_id=session_id)
|
|
123
|
+
return cls.build(session_id=session_id, events=events)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FailureMode(Enum):
|
|
9
|
+
FAIL_CLOSED = "fail_closed"
|
|
10
|
+
FAIL_OPEN = "fail_open"
|
|
11
|
+
FAIL_EXCEPTION = "fail_exception"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InterceptorDecision(Enum):
|
|
15
|
+
ALLOW = "allow"
|
|
16
|
+
DENY = "deny"
|
|
17
|
+
REQUIRE_APPROVAL = "require_approval"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ActionBlockedError(RuntimeError):
|
|
21
|
+
"""Raised by middleware when an interceptor sets decision=DENY or REQUIRE_APPROVAL.
|
|
22
|
+
|
|
23
|
+
Carries the final ActionContext and aggregated risk_score so blocked
|
|
24
|
+
calls surface the same risk signal as allowed ones. When the block is a
|
|
25
|
+
REQUIRE_APPROVAL, approval_id names the pending approval record — once
|
|
26
|
+
a human approves it, retry the action with invoke(..., approval_id=...).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
reason: str = "",
|
|
32
|
+
interceptor: str = "",
|
|
33
|
+
context: ActionContext | None = None,
|
|
34
|
+
risk_score: float = 0.0,
|
|
35
|
+
approval_id: str = "",
|
|
36
|
+
) -> None:
|
|
37
|
+
self.reason = reason
|
|
38
|
+
self.interceptor = interceptor
|
|
39
|
+
self.context = context
|
|
40
|
+
self.risk_score = risk_score
|
|
41
|
+
self.approval_id = approval_id
|
|
42
|
+
msg = (
|
|
43
|
+
f"Action blocked by {interceptor}: {reason}"
|
|
44
|
+
if interceptor
|
|
45
|
+
else f"Action blocked: {reason}"
|
|
46
|
+
)
|
|
47
|
+
super().__init__(msg)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class AgentCapabilities:
|
|
52
|
+
untrusted_input: bool = False
|
|
53
|
+
sensitive_access: bool = False
|
|
54
|
+
external_write: bool = False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class ActionContext:
|
|
59
|
+
action: str
|
|
60
|
+
agent_id: str
|
|
61
|
+
session_id: str
|
|
62
|
+
input_data: dict[str, Any] = field(default_factory=dict)
|
|
63
|
+
output_data: dict[str, Any] = field(default_factory=dict)
|
|
64
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
65
|
+
capabilities: AgentCapabilities = field(default_factory=AgentCapabilities)
|
|
66
|
+
decision: InterceptorDecision = field(default=InterceptorDecision.ALLOW)
|
|
67
|
+
decision_reason: str = ""
|
|
68
|
+
risk_score: float = 0.0
|
|
69
|
+
risk_categories: dict[str, float] = field(default_factory=dict)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class InterceptorResult:
|
|
74
|
+
decision: InterceptorDecision
|
|
75
|
+
reason: str = ""
|
|
76
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
File without changes
|