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.
Files changed (65) hide show
  1. openscript_sdk/__init__.py +217 -0
  2. openscript_sdk/contracts/__init__.py +82 -0
  3. openscript_sdk/contracts/interceptor.py +98 -0
  4. openscript_sdk/contracts/server_types.py +87 -0
  5. openscript_sdk/contracts/session_graph.py +123 -0
  6. openscript_sdk/contracts/types.py +76 -0
  7. openscript_sdk/events/__init__.py +0 -0
  8. openscript_sdk/events/approvals.py +309 -0
  9. openscript_sdk/events/models.py +26 -0
  10. openscript_sdk/events/store.py +79 -0
  11. openscript_sdk/events/writer.py +100 -0
  12. openscript_sdk/hooks/claude_code.py +479 -0
  13. openscript_sdk/hooks/claude_code_example.yaml +62 -0
  14. openscript_sdk/hooks/claude_code_strict_example.yaml +73 -0
  15. openscript_sdk/hooks/config.py +144 -0
  16. openscript_sdk/hooks/settings_example.json +28 -0
  17. openscript_sdk/hooks/settings_strict_example.json +28 -0
  18. openscript_sdk/integrations/langchain.py +32 -0
  19. openscript_sdk/integrations/langgraph.py +32 -0
  20. openscript_sdk/interceptors/base.py +15 -0
  21. openscript_sdk/interceptors/event_writer.py +131 -0
  22. openscript_sdk/interceptors/pii.py +168 -0
  23. openscript_sdk/interceptors/threat.py +674 -0
  24. openscript_sdk/logging.py +94 -0
  25. openscript_sdk/middleware/middleware.py +543 -0
  26. openscript_sdk/middleware/tool.py +247 -0
  27. openscript_sdk/observability/__init__.py +11 -0
  28. openscript_sdk/observability/metrics.py +105 -0
  29. openscript_sdk/observability/risk.py +54 -0
  30. openscript_sdk/observability/tracing.py +67 -0
  31. openscript_sdk/policies/compliance.py +261 -0
  32. openscript_sdk/policies/config.py +244 -0
  33. openscript_sdk/policies/harmful_request.py +617 -0
  34. openscript_sdk/policies/normalize.py +489 -0
  35. openscript_sdk/policies/output_schema.py +353 -0
  36. openscript_sdk/policies/policies_example.yaml +36 -0
  37. openscript_sdk/policies/redaction.py +359 -0
  38. openscript_sdk/policies/secrets.py +239 -0
  39. openscript_sdk/policies/semantic.py +348 -0
  40. openscript_sdk/policies/semantic_head.py +9 -0
  41. openscript_sdk/policies/session_taint.py +399 -0
  42. openscript_sdk/policies/taint_store.py +111 -0
  43. openscript_sdk/policies/tool_firewall.py +434 -0
  44. openscript_sdk/policies/tools_example.yaml +81 -0
  45. openscript_sdk/policies/toxicity.py +247 -0
  46. openscript_sdk/py.typed +0 -0
  47. openscript_sdk/server/__init__.py +1 -0
  48. openscript_sdk/server/app.py +153 -0
  49. openscript_sdk/server/auth.py +19 -0
  50. openscript_sdk/server/guard.py +126 -0
  51. openscript_sdk/server/guard_example.yaml +55 -0
  52. openscript_sdk/server/routers/__init__.py +0 -0
  53. openscript_sdk/server/routers/approvals.py +69 -0
  54. openscript_sdk/server/routers/guard.py +231 -0
  55. openscript_sdk/server/routers/sessions.py +80 -0
  56. openscript_sdk/server/routers/sse.py +69 -0
  57. openscript_sdk/server/routers/threat.py +119 -0
  58. openscript_sdk/server/static/dashboard/index.html +209 -0
  59. openscript_sdk/server/static/dashboard/session.html +339 -0
  60. openscript_sdk-0.1.0.dist-info/METADATA +225 -0
  61. openscript_sdk-0.1.0.dist-info/RECORD +65 -0
  62. openscript_sdk-0.1.0.dist-info/WHEEL +5 -0
  63. openscript_sdk-0.1.0.dist-info/entry_points.txt +2 -0
  64. openscript_sdk-0.1.0.dist-info/licenses/LICENSE +190 -0
  65. 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