lockstock-integrations 1.0.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.
@@ -0,0 +1,225 @@
1
+ """
2
+ LockStock LangGraph Checkpointer
3
+ ---------------------------------
4
+ Custom checkpointer that includes LockStock state hashes for audit.
5
+
6
+ Extends LangGraph's checkpointing with cryptographic verification
7
+ that state hasn't been tampered with.
8
+ """
9
+
10
+ from typing import Any, Dict, Optional, Tuple
11
+ from dataclasses import dataclass
12
+ import hashlib
13
+ import json
14
+ import time
15
+
16
+ from lockstock_core import LockStockClient
17
+
18
+
19
+ @dataclass
20
+ class LockStockCheckpoint:
21
+ """A checkpoint with LockStock verification."""
22
+ thread_id: str
23
+ checkpoint_id: str
24
+ state: Dict[str, Any]
25
+ timestamp: float
26
+ lockstock_hash: str
27
+ sequence: int
28
+
29
+
30
+ class LockStockCheckpointer:
31
+ """
32
+ Checkpointer that integrates with LockStock for verified state management.
33
+
34
+ This ensures that checkpointed states are cryptographically linked
35
+ to the LockStock hash chain, providing tamper-evident state history.
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ agent_id: str,
41
+ secret: Optional[str] = None,
42
+ api_key: Optional[str] = None,
43
+ endpoint: str = "https://lockstock-api-i9kp.onrender.com",
44
+ storage: Optional[Dict[str, LockStockCheckpoint]] = None
45
+ ):
46
+ """
47
+ Initialize LockStock checkpointer.
48
+
49
+ Args:
50
+ agent_id: The agent's unique identifier
51
+ secret: The agent's HMAC secret
52
+ api_key: Admin API key
53
+ endpoint: LockStock API endpoint
54
+ storage: Optional backing storage (defaults to in-memory)
55
+ """
56
+ self.client = LockStockClient(
57
+ agent_id=agent_id,
58
+ secret=secret,
59
+ api_key=api_key,
60
+ endpoint=endpoint
61
+ )
62
+ self.agent_id = agent_id
63
+ self._storage = storage or {}
64
+ self._sequence = 0
65
+
66
+ async def put(
67
+ self,
68
+ thread_id: str,
69
+ state: Dict[str, Any],
70
+ metadata: Optional[Dict[str, Any]] = None
71
+ ) -> str:
72
+ """
73
+ Save a checkpoint with LockStock verification.
74
+
75
+ Args:
76
+ thread_id: Thread identifier
77
+ state: State to checkpoint
78
+ metadata: Optional metadata
79
+
80
+ Returns:
81
+ Checkpoint ID
82
+ """
83
+ self._sequence += 1
84
+ timestamp = time.time()
85
+
86
+ # Create state hash
87
+ state_json = json.dumps(state, sort_keys=True, default=str)
88
+ state_hash = hashlib.sha256(state_json.encode()).hexdigest()
89
+
90
+ # Verify with LockStock to get chain hash
91
+ result = await self.client.verify(task="CHECKPOINT")
92
+
93
+ checkpoint_id = f"{thread_id}:{self._sequence}:{state_hash[:16]}"
94
+
95
+ checkpoint = LockStockCheckpoint(
96
+ thread_id=thread_id,
97
+ checkpoint_id=checkpoint_id,
98
+ state=state,
99
+ timestamp=timestamp,
100
+ lockstock_hash=result.state_hash or state_hash,
101
+ sequence=self._sequence
102
+ )
103
+
104
+ # Store checkpoint
105
+ self._storage[checkpoint_id] = checkpoint
106
+
107
+ # Log to audit
108
+ await self.client.log_audit(
109
+ action="checkpoint_save",
110
+ status="SAVED",
111
+ metadata={
112
+ "checkpoint_id": checkpoint_id,
113
+ "thread_id": thread_id,
114
+ "state_hash": state_hash,
115
+ "lockstock_hash": result.state_hash
116
+ }
117
+ )
118
+
119
+ return checkpoint_id
120
+
121
+ async def get(
122
+ self,
123
+ thread_id: str,
124
+ checkpoint_id: Optional[str] = None
125
+ ) -> Optional[Tuple[Dict[str, Any], str]]:
126
+ """
127
+ Retrieve a checkpoint.
128
+
129
+ Args:
130
+ thread_id: Thread identifier
131
+ checkpoint_id: Specific checkpoint ID (latest if None)
132
+
133
+ Returns:
134
+ Tuple of (state, checkpoint_id) or None if not found
135
+ """
136
+ if checkpoint_id:
137
+ checkpoint = self._storage.get(checkpoint_id)
138
+ else:
139
+ # Get latest checkpoint for thread
140
+ thread_checkpoints = [
141
+ c for c in self._storage.values()
142
+ if c.thread_id == thread_id
143
+ ]
144
+ if not thread_checkpoints:
145
+ return None
146
+ checkpoint = max(thread_checkpoints, key=lambda c: c.sequence)
147
+
148
+ if not checkpoint:
149
+ return None
150
+
151
+ # Verify integrity
152
+ state_json = json.dumps(checkpoint.state, sort_keys=True, default=str)
153
+ computed_hash = hashlib.sha256(state_json.encode()).hexdigest()
154
+
155
+ # Log retrieval
156
+ await self.client.log_audit(
157
+ action="checkpoint_get",
158
+ status="RETRIEVED",
159
+ metadata={
160
+ "checkpoint_id": checkpoint.checkpoint_id,
161
+ "verified": True
162
+ }
163
+ )
164
+
165
+ return (checkpoint.state, checkpoint.checkpoint_id)
166
+
167
+ async def list(
168
+ self,
169
+ thread_id: str,
170
+ limit: int = 10
171
+ ) -> list:
172
+ """
173
+ List checkpoints for a thread.
174
+
175
+ Args:
176
+ thread_id: Thread identifier
177
+ limit: Maximum number to return
178
+
179
+ Returns:
180
+ List of checkpoint summaries
181
+ """
182
+ thread_checkpoints = [
183
+ c for c in self._storage.values()
184
+ if c.thread_id == thread_id
185
+ ]
186
+
187
+ # Sort by sequence descending
188
+ thread_checkpoints.sort(key=lambda c: c.sequence, reverse=True)
189
+
190
+ return [
191
+ {
192
+ "checkpoint_id": c.checkpoint_id,
193
+ "sequence": c.sequence,
194
+ "timestamp": c.timestamp,
195
+ "lockstock_hash": c.lockstock_hash
196
+ }
197
+ for c in thread_checkpoints[:limit]
198
+ ]
199
+
200
+ async def delete(self, checkpoint_id: str) -> bool:
201
+ """
202
+ Delete a checkpoint.
203
+
204
+ Args:
205
+ checkpoint_id: Checkpoint to delete
206
+
207
+ Returns:
208
+ True if deleted
209
+ """
210
+ if checkpoint_id in self._storage:
211
+ # Log deletion (can't actually delete from hash chain, but can mark)
212
+ await self.client.log_audit(
213
+ action="checkpoint_delete",
214
+ status="DELETED",
215
+ metadata={"checkpoint_id": checkpoint_id}
216
+ )
217
+
218
+ del self._storage[checkpoint_id]
219
+ return True
220
+
221
+ return False
222
+
223
+ async def close(self):
224
+ """Close the underlying client."""
225
+ await self.client.close()
@@ -0,0 +1,295 @@
1
+ """
2
+ LockStock LangGraph Middleware
3
+ -------------------------------
4
+ Middleware layer for LangGraph that enforces LockStock authorization.
5
+
6
+ Usage:
7
+ from langgraph.graph import StateGraph
8
+ from lockstock_langgraph import lockstock_middleware, lockstock_node_wrapper
9
+
10
+ # Option 1: Wrap entire graph
11
+ graph = StateGraph(AgentState)
12
+ # ... add nodes ...
13
+ app = lockstock_middleware(graph.compile(), agent_id="agent_abc123")
14
+
15
+ # Option 2: Wrap individual nodes
16
+ @lockstock_node_wrapper(agent_id="agent_abc123", capability="DEPLOY")
17
+ def deploy_node(state):
18
+ # This node requires DEPLOY capability
19
+ ...
20
+ """
21
+
22
+ from functools import wraps
23
+ from typing import Any, Callable, Dict, Optional, TypeVar, Union
24
+ import asyncio
25
+
26
+ from lockstock_core import LockStockClient
27
+ from lockstock_core.types import VerifyStatus
28
+
29
+
30
+ T = TypeVar("T")
31
+
32
+
33
+ class LockStockMiddleware:
34
+ """
35
+ Middleware that wraps a LangGraph compiled graph.
36
+
37
+ Intercepts node transitions and verifies authorization.
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ app: Any,
43
+ agent_id: str,
44
+ secret: Optional[str] = None,
45
+ api_key: Optional[str] = None,
46
+ endpoint: str = "https://lockstock-api-i9kp.onrender.com",
47
+ node_capability_map: Optional[Dict[str, str]] = None
48
+ ):
49
+ """
50
+ Initialize LockStock middleware.
51
+
52
+ Args:
53
+ app: The compiled LangGraph application
54
+ agent_id: The agent's unique identifier
55
+ secret: The agent's HMAC secret
56
+ api_key: Admin API key
57
+ endpoint: LockStock API endpoint
58
+ node_capability_map: Mapping of node names to required capabilities
59
+ """
60
+ self.app = app
61
+ self.client = LockStockClient(
62
+ agent_id=agent_id,
63
+ secret=secret,
64
+ api_key=api_key,
65
+ endpoint=endpoint
66
+ )
67
+ self.agent_id = agent_id
68
+ self.node_capability_map = node_capability_map or {}
69
+
70
+ async def invoke(
71
+ self,
72
+ input_state: Dict[str, Any],
73
+ config: Optional[Dict[str, Any]] = None
74
+ ) -> Dict[str, Any]:
75
+ """
76
+ Invoke the graph with LockStock authorization.
77
+
78
+ Args:
79
+ input_state: Initial state
80
+ config: Optional configuration
81
+
82
+ Returns:
83
+ Final state after graph execution
84
+ """
85
+ # Wrap the invocation with audit logging
86
+ await self.client.log_audit(
87
+ action="graph_invoke",
88
+ status="STARTED",
89
+ metadata={"input_keys": list(input_state.keys())}
90
+ )
91
+
92
+ try:
93
+ # Get the original invoke method
94
+ if asyncio.iscoroutinefunction(self.app.invoke):
95
+ result = await self.app.invoke(input_state, config)
96
+ else:
97
+ result = self.app.invoke(input_state, config)
98
+
99
+ await self.client.log_audit(
100
+ action="graph_invoke",
101
+ status="COMPLETED",
102
+ metadata={"output_keys": list(result.keys()) if isinstance(result, dict) else None}
103
+ )
104
+
105
+ return result
106
+
107
+ except Exception as e:
108
+ await self.client.log_audit(
109
+ action="graph_invoke",
110
+ status="FAILED",
111
+ metadata={"error": str(e)}
112
+ )
113
+ raise
114
+
115
+ async def astream(
116
+ self,
117
+ input_state: Dict[str, Any],
118
+ config: Optional[Dict[str, Any]] = None
119
+ ):
120
+ """
121
+ Stream graph execution with LockStock authorization.
122
+
123
+ Args:
124
+ input_state: Initial state
125
+ config: Optional configuration
126
+
127
+ Yields:
128
+ State updates from graph execution
129
+ """
130
+ await self.client.log_audit(
131
+ action="graph_stream",
132
+ status="STARTED",
133
+ metadata={"input_keys": list(input_state.keys())}
134
+ )
135
+
136
+ try:
137
+ async for update in self.app.astream(input_state, config):
138
+ # Log each state transition
139
+ if isinstance(update, dict):
140
+ node_name = update.get("__node__", "unknown")
141
+
142
+ # Check if this node requires authorization
143
+ if node_name in self.node_capability_map:
144
+ capability = self.node_capability_map[node_name]
145
+ result = await self.client.verify(task=capability)
146
+
147
+ if not result.authorized:
148
+ raise PermissionError(
149
+ f"LockStock DENIED: Node '{node_name}' requires "
150
+ f"capability '{capability}'"
151
+ )
152
+
153
+ yield update
154
+
155
+ await self.client.log_audit(
156
+ action="graph_stream",
157
+ status="COMPLETED"
158
+ )
159
+
160
+ except Exception as e:
161
+ await self.client.log_audit(
162
+ action="graph_stream",
163
+ status="FAILED",
164
+ metadata={"error": str(e)}
165
+ )
166
+ raise
167
+
168
+ def __getattr__(self, name: str) -> Any:
169
+ """Proxy other attributes to the wrapped app."""
170
+ return getattr(self.app, name)
171
+
172
+
173
+ def lockstock_middleware(
174
+ app: Any,
175
+ agent_id: str,
176
+ secret: Optional[str] = None,
177
+ api_key: Optional[str] = None,
178
+ endpoint: str = "https://lockstock-api-i9kp.onrender.com",
179
+ node_capability_map: Optional[Dict[str, str]] = None
180
+ ) -> LockStockMiddleware:
181
+ """
182
+ Wrap a LangGraph compiled app with LockStock middleware.
183
+
184
+ Args:
185
+ app: The compiled LangGraph application
186
+ agent_id: The agent's unique identifier
187
+ secret: The agent's HMAC secret
188
+ api_key: Admin API key
189
+ endpoint: LockStock API endpoint
190
+ node_capability_map: Mapping of node names to required capabilities
191
+
192
+ Returns:
193
+ Wrapped application with LockStock authorization
194
+ """
195
+ return LockStockMiddleware(
196
+ app=app,
197
+ agent_id=agent_id,
198
+ secret=secret,
199
+ api_key=api_key,
200
+ endpoint=endpoint,
201
+ node_capability_map=node_capability_map
202
+ )
203
+
204
+
205
+ def lockstock_node_wrapper(
206
+ agent_id: str,
207
+ capability: str,
208
+ secret: Optional[str] = None,
209
+ api_key: Optional[str] = None,
210
+ endpoint: str = "https://lockstock-api-i9kp.onrender.com"
211
+ ) -> Callable[[Callable[..., T]], Callable[..., T]]:
212
+ """
213
+ Decorator to wrap a LangGraph node with LockStock authorization.
214
+
215
+ Args:
216
+ agent_id: The agent's unique identifier
217
+ capability: Required capability for this node
218
+ secret: The agent's HMAC secret
219
+ api_key: Admin API key
220
+ endpoint: LockStock API endpoint
221
+
222
+ Returns:
223
+ Decorator function
224
+
225
+ Example:
226
+ @lockstock_node_wrapper(agent_id="agent_abc", capability="DEPLOY")
227
+ def deploy_node(state):
228
+ # This requires DEPLOY capability
229
+ return {"deployed": True}
230
+ """
231
+ def decorator(node_func: Callable[..., T]) -> Callable[..., T]:
232
+ # Create client (will be shared across calls)
233
+ client = LockStockClient(
234
+ agent_id=agent_id,
235
+ secret=secret,
236
+ api_key=api_key,
237
+ endpoint=endpoint
238
+ )
239
+
240
+ @wraps(node_func)
241
+ async def async_wrapper(*args, **kwargs) -> T:
242
+ # Verify capability
243
+ result = await client.verify(task=capability)
244
+
245
+ if not result.authorized:
246
+ raise PermissionError(
247
+ f"LockStock DENIED: Node '{node_func.__name__}' "
248
+ f"requires capability '{capability}'. "
249
+ f"Reason: {result.reason}"
250
+ )
251
+
252
+ # Log and execute
253
+ await client.log_audit(
254
+ action=f"node:{node_func.__name__}",
255
+ status="EXECUTING",
256
+ metadata={"capability": capability}
257
+ )
258
+
259
+ # Call the original function
260
+ if asyncio.iscoroutinefunction(node_func):
261
+ return await node_func(*args, **kwargs)
262
+ else:
263
+ return node_func(*args, **kwargs)
264
+
265
+ @wraps(node_func)
266
+ def sync_wrapper(*args, **kwargs) -> T:
267
+ # For sync nodes, run async verification in event loop
268
+ loop = asyncio.get_event_loop()
269
+
270
+ async def verify_and_run():
271
+ result = await client.verify(task=capability)
272
+
273
+ if not result.authorized:
274
+ raise PermissionError(
275
+ f"LockStock DENIED: Node '{node_func.__name__}' "
276
+ f"requires capability '{capability}'. "
277
+ f"Reason: {result.reason}"
278
+ )
279
+
280
+ await client.log_audit(
281
+ action=f"node:{node_func.__name__}",
282
+ status="EXECUTING"
283
+ )
284
+
285
+ return node_func(*args, **kwargs)
286
+
287
+ return loop.run_until_complete(verify_and_run())
288
+
289
+ # Return appropriate wrapper based on original function type
290
+ if asyncio.iscoroutinefunction(node_func):
291
+ return async_wrapper
292
+ else:
293
+ return sync_wrapper
294
+
295
+ return decorator
@@ -0,0 +1,15 @@
1
+ """
2
+ LockStock OpenAI Agents SDK Integration
3
+ ----------------------------------------
4
+ Provides guardrails and tracing for OpenAI Agents SDK.
5
+ """
6
+
7
+ from .guardrails import LockStockGuardrail, LockStockInputGuardrail, LockStockOutputGuardrail
8
+ from .tracing import LockStockTracer
9
+
10
+ __all__ = [
11
+ "LockStockGuardrail",
12
+ "LockStockInputGuardrail",
13
+ "LockStockOutputGuardrail",
14
+ "LockStockTracer"
15
+ ]