lockstock-integrations 1.0.1__py3-none-any.whl → 1.1.1__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,10 @@
1
+ """
2
+ LockStock Core Client
3
+ ---------------------
4
+ Shared client for all SDK integrations.
5
+ """
6
+
7
+ from .client import LockStockClient
8
+ from .types import VerifyResult, AuditEntry, AgentCard
9
+
10
+ __all__ = ["LockStockClient", "VerifyResult", "AuditEntry", "AgentCard"]
@@ -0,0 +1,281 @@
1
+ """
2
+ LockStock Core Client
3
+ ---------------------
4
+ HTTP client for LockStock API interactions.
5
+ """
6
+
7
+ import hashlib
8
+ import hmac
9
+ import time
10
+ from typing import Optional, Dict, Any, List
11
+ import httpx
12
+
13
+ from .types import VerifyResult, VerifyStatus, AuditEntry, AgentCard, map_tool_to_capability, Matrix, get_generator, normalize_task_for_signature
14
+
15
+
16
+ class LockStockClient:
17
+ """
18
+ Client for interacting with the LockStock API.
19
+
20
+ Provides capability verification, audit logging, and identity management.
21
+ """
22
+
23
+ DEFAULT_ENDPOINT = "https://lockstock-api-i9kp.onrender.com"
24
+
25
+ def __init__(
26
+ self,
27
+ agent_id: str,
28
+ secret: Optional[str] = None,
29
+ api_key: Optional[str] = None,
30
+ endpoint: str = DEFAULT_ENDPOINT,
31
+ timeout: float = 30.0
32
+ ):
33
+ """
34
+ Initialize LockStock client.
35
+
36
+ Args:
37
+ agent_id: The agent's unique identifier
38
+ secret: The agent's HMAC secret (for signing requests)
39
+ api_key: Admin API key (for provisioning/management)
40
+ endpoint: LockStock API endpoint
41
+ timeout: Request timeout in seconds
42
+ """
43
+ self.agent_id = agent_id
44
+ self.secret = secret
45
+ self.api_key = api_key
46
+ self.endpoint = endpoint.rstrip("/")
47
+ self.timeout = timeout
48
+
49
+ # State tracking
50
+ self._current_hash = "0" * 64 # Genesis hash
51
+ self._sequence = 0
52
+ self._state_matrix = Matrix.identity()
53
+
54
+ # HTTP client
55
+ self._client = httpx.AsyncClient(timeout=timeout)
56
+
57
+ async def verify(
58
+ self,
59
+ task: str,
60
+ tool_name: Optional[str] = None,
61
+ metadata: Optional[Dict[str, Any]] = None
62
+ ) -> VerifyResult:
63
+ """
64
+ Verify that the agent is authorized to perform a task.
65
+
66
+ Args:
67
+ task: The capability/task to verify (e.g., "DEPLOY", "RESTART")
68
+ tool_name: Optional tool name (will be mapped to capability)
69
+ metadata: Optional metadata to include in audit
70
+
71
+ Returns:
72
+ VerifyResult with authorization status
73
+ """
74
+ # Map tool name to capability if provided
75
+ if tool_name and task == "UNKNOWN":
76
+ task = map_tool_to_capability(tool_name)
77
+
78
+ # Prepare request
79
+ self._sequence += 1
80
+ timestamp = int(time.time())
81
+
82
+ # Apply generator matrix for this task (topological state transition)
83
+ generator = get_generator(task)
84
+ new_matrix = self._state_matrix.multiply(generator)
85
+
86
+ # Create signature with the NEW matrix (uses timestamp, not sequence)
87
+ signature = self._sign_request(task, self._current_hash, timestamp, new_matrix)
88
+
89
+ payload = {
90
+ "client_id": self.agent_id,
91
+ "task": task.lower(),
92
+ "parent_hash": self._current_hash,
93
+ "state_matrix": new_matrix.to_dict(),
94
+ "signature": signature,
95
+ "timestamp": timestamp,
96
+ "sequence": self._sequence
97
+ }
98
+
99
+ try:
100
+ response = await self._client.post(
101
+ f"{self.endpoint}/verify",
102
+ json=payload,
103
+ headers=self._headers()
104
+ )
105
+
106
+ data = response.json()
107
+
108
+ status = VerifyStatus(data.get("status", "rejected"))
109
+
110
+ if status == VerifyStatus.ACCEPTED:
111
+ # Update state from server response
112
+ self._current_hash = data.get("state_hash", self._current_hash)
113
+ if "state_matrix" in data:
114
+ self._state_matrix = Matrix.from_dict(data["state_matrix"])
115
+
116
+ return VerifyResult(
117
+ status=status,
118
+ reason=data.get("reason"),
119
+ state_hash=data.get("state_hash"),
120
+ server_timestamp=data.get("server_timestamp")
121
+ )
122
+
123
+ except httpx.HTTPError as e:
124
+ return VerifyResult(
125
+ status=VerifyStatus.REJECTED,
126
+ reason=f"HTTP error: {str(e)}"
127
+ )
128
+ except Exception as e:
129
+ return VerifyResult(
130
+ status=VerifyStatus.REJECTED,
131
+ reason=f"Client error: {str(e)}"
132
+ )
133
+
134
+ async def verify_tool(self, tool_name: str, tool_input: Optional[Dict] = None) -> VerifyResult:
135
+ """
136
+ Verify authorization for a specific tool call.
137
+
138
+ Args:
139
+ tool_name: Name of the tool being called
140
+ tool_input: The tool's input parameters
141
+
142
+ Returns:
143
+ VerifyResult with authorization status
144
+ """
145
+ capability = map_tool_to_capability(tool_name)
146
+ return await self.verify(task=capability, tool_name=tool_name)
147
+
148
+ async def bootstrap(self) -> Dict[str, Any]:
149
+ """
150
+ Initialize the agent's identity with the server.
151
+
152
+ Returns:
153
+ Bootstrap response with root hash and matrix
154
+ """
155
+ try:
156
+ response = await self._client.post(
157
+ f"{self.endpoint}/bootstrap",
158
+ json={"client_id": self.agent_id},
159
+ headers=self._headers()
160
+ )
161
+
162
+ data = response.json()
163
+
164
+ if "root_hash" in data:
165
+ self._current_hash = data["root_hash"]
166
+ if "root_matrix" in data:
167
+ self._state_matrix = Matrix.from_dict(data["root_matrix"])
168
+ self._sequence = 0
169
+
170
+ return data
171
+
172
+ except Exception as e:
173
+ return {"error": str(e)}
174
+
175
+ async def get_agent_card(self) -> Optional[AgentCard]:
176
+ """
177
+ Retrieve the agent's card from the server.
178
+
179
+ Returns:
180
+ AgentCard if found, None otherwise
181
+ """
182
+ try:
183
+ response = await self._client.get(
184
+ f"{self.endpoint}/api/guard/agents/{self.agent_id}",
185
+ headers=self._headers()
186
+ )
187
+
188
+ if response.status_code == 200:
189
+ data = response.json()
190
+ return AgentCard(
191
+ agent_id=data.get("agent_id", self.agent_id),
192
+ display_name=data.get("display_name", ""),
193
+ capabilities=data.get("capabilities", []),
194
+ status=data.get("status", "unknown"),
195
+ fingerprint=data.get("fingerprint"),
196
+ owner_id=data.get("owner_id"),
197
+ created_at=data.get("created_at")
198
+ )
199
+ return None
200
+
201
+ except Exception:
202
+ return None
203
+
204
+ async def log_audit(
205
+ self,
206
+ action: str,
207
+ status: str,
208
+ metadata: Optional[Dict[str, Any]] = None
209
+ ) -> bool:
210
+ """
211
+ Log an action to the audit trail.
212
+
213
+ Args:
214
+ action: The action that was performed
215
+ status: Status of the action (e.g., "APPROVED", "DENIED")
216
+ metadata: Additional metadata to log
217
+
218
+ Returns:
219
+ True if logged successfully
220
+ """
221
+ # Audit is automatically logged via verify() calls
222
+ # This method is for explicit logging of non-verified actions
223
+ entry = AuditEntry(
224
+ sequence=self._sequence,
225
+ action=action,
226
+ timestamp=int(time.time()),
227
+ state_hash=self._current_hash,
228
+ signature=self._sign_request(action, self._current_hash, self._sequence),
229
+ metadata=metadata or {}
230
+ )
231
+
232
+ # In production, this would POST to an audit endpoint
233
+ # For now, we just track locally
234
+ return True
235
+
236
+ def _sign_request(self, task: str, parent_hash: str, timestamp: int, matrix: Matrix = None) -> str:
237
+ """Create HMAC signature for a request.
238
+
239
+ The signature format matches the Rust server:
240
+ {client_id}|{Task}|{parent_hash}|{a11},{a12},{a21},{a22}|{timestamp}
241
+
242
+ IMPORTANT: The task name must match Rust's Debug format (e.g., "Heartbeat", "FileRead")
243
+ because the server uses {:?} formatting for the Task enum in signature generation.
244
+ """
245
+ if not self.secret:
246
+ return "unsigned"
247
+
248
+ # Use provided matrix or current state
249
+ m = matrix if matrix else self._state_matrix
250
+ matrix_str = f"{m.a11},{m.a12},{m.a21},{m.a22}"
251
+
252
+ # Normalize task to Rust Debug format (e.g., "HEARTBEAT" -> "Heartbeat")
253
+ rust_task = normalize_task_for_signature(task)
254
+
255
+ # Build message in server's expected format
256
+ message = f"{self.agent_id}|{rust_task}|{parent_hash}|{matrix_str}|{timestamp}"
257
+
258
+ signature = hmac.new(
259
+ self.secret.encode(),
260
+ message.encode(),
261
+ hashlib.sha256
262
+ ).hexdigest()
263
+
264
+ return signature
265
+
266
+ def _headers(self) -> Dict[str, str]:
267
+ """Build request headers."""
268
+ headers = {"Content-Type": "application/json"}
269
+ if self.api_key:
270
+ headers["x-admin-key"] = self.api_key
271
+ return headers
272
+
273
+ async def close(self):
274
+ """Close the HTTP client."""
275
+ await self._client.aclose()
276
+
277
+ async def __aenter__(self):
278
+ return self
279
+
280
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
281
+ await self.close()
@@ -0,0 +1,264 @@
1
+ """
2
+ LockStock Core Types
3
+ --------------------
4
+ Shared types for all SDK integrations.
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+ from typing import List, Optional, Dict, Any
9
+ from enum import Enum
10
+
11
+
12
+ class VerifyStatus(str, Enum):
13
+ ACCEPTED = "accepted"
14
+ REJECTED = "rejected"
15
+ BUFFERED = "buffered"
16
+
17
+
18
+ @dataclass
19
+ class VerifyResult:
20
+ """Result of a capability verification request."""
21
+ status: VerifyStatus
22
+ reason: Optional[str] = None
23
+ state_hash: Optional[str] = None
24
+ server_timestamp: Optional[int] = None
25
+
26
+ @property
27
+ def authorized(self) -> bool:
28
+ return self.status == VerifyStatus.ACCEPTED
29
+
30
+
31
+ @dataclass
32
+ class AuditEntry:
33
+ """An entry in the agent's audit trail."""
34
+ sequence: int
35
+ action: str
36
+ timestamp: int
37
+ state_hash: str
38
+ signature: str
39
+ metadata: Dict[str, Any] = field(default_factory=dict)
40
+
41
+
42
+ @dataclass
43
+ class AgentCard:
44
+ """Agent identity and capabilities."""
45
+ agent_id: str
46
+ display_name: str
47
+ capabilities: List[str]
48
+ status: str = "active"
49
+ fingerprint: Optional[str] = None
50
+ owner_id: Optional[str] = None
51
+ created_at: Optional[str] = None
52
+
53
+ def has_capability(self, capability: str) -> bool:
54
+ """Check if agent has a specific capability."""
55
+ return capability.upper() in [c.upper() for c in self.capabilities]
56
+
57
+
58
+ # Mapping from common tool names to LockStock capabilities
59
+ # These map to the Task enum variants in Rust (case-insensitive)
60
+ TOOL_CAPABILITY_MAP = {
61
+ # File operations -> FileRead, FileWrite
62
+ "read": "FILEREAD",
63
+ "read_file": "FILEREAD",
64
+ "Read": "FILEREAD",
65
+ "Glob": "FILEREAD",
66
+ "glob": "FILEREAD",
67
+ "Grep": "FILEREAD",
68
+ "grep": "FILEREAD",
69
+ "write": "FILEWRITE",
70
+ "write_file": "FILEWRITE",
71
+ "Write": "FILEWRITE",
72
+ "edit": "FILEWRITE",
73
+ "Edit": "FILEWRITE",
74
+ "NotebookEdit": "FILEWRITE",
75
+
76
+ # Shell/execution -> Shell (alias for Deploy)
77
+ "bash": "SHELL",
78
+ "Bash": "SHELL",
79
+ "shell": "SHELL",
80
+ "execute": "SHELL",
81
+ "run_command": "SHELL",
82
+ "KillShell": "SHELL",
83
+
84
+ # Network operations -> Network
85
+ "web_fetch": "NETWORK",
86
+ "WebFetch": "NETWORK",
87
+ "web_search": "NETWORK",
88
+ "WebSearch": "NETWORK",
89
+ "http_request": "NETWORK",
90
+ "fetch": "NETWORK",
91
+
92
+ # Database operations -> Database
93
+ "sql": "DATABASE",
94
+ "query": "DATABASE",
95
+ "db_read": "DATABASE",
96
+ "db_write": "DATABASE",
97
+ "LSP": "DATABASE", # Language Server Protocol reads code
98
+
99
+ # Agent operations -> Teleport, Handoff
100
+ "task": "TELEPORT",
101
+ "Task": "TELEPORT",
102
+ "spawn_agent": "TELEPORT",
103
+ "TaskOutput": "TELEPORT",
104
+ "handoff": "HANDOFF",
105
+ "delegate": "DELEGATE",
106
+
107
+ # State operations -> Checkpoint, Teleport
108
+ "checkpoint": "CHECKPOINT",
109
+ "save_state": "CHECKPOINT",
110
+ "export_passport": "TELEPORT",
111
+ "import_passport": "TELEPORT",
112
+
113
+ # Monitoring -> Heartbeat
114
+ "heartbeat": "HEARTBEAT",
115
+ "health_check": "HEARTBEAT",
116
+
117
+ # Operations -> Deploy, Restart, Backup, Rollback
118
+ "deploy": "DEPLOY",
119
+ "restart": "RESTART",
120
+ "backup": "BACKUP",
121
+ "rollback": "ROLLBACK",
122
+
123
+ # Task management tools
124
+ "TaskCreate": "FILEWRITE",
125
+ "TaskUpdate": "FILEWRITE",
126
+ "TaskList": "FILEREAD",
127
+ "TaskGet": "FILEREAD",
128
+
129
+ # Skills
130
+ "Skill": "SHELL",
131
+ "AskUserQuestion": "HEARTBEAT", # Interactive, low privilege
132
+ "EnterPlanMode": "HEARTBEAT",
133
+ "ExitPlanMode": "HEARTBEAT",
134
+ }
135
+
136
+
137
+ def map_tool_to_capability(tool_name: str) -> str:
138
+ """Map a tool name to a LockStock capability."""
139
+ return TOOL_CAPABILITY_MAP.get(tool_name, "UNKNOWN")
140
+
141
+
142
+ # Mapping from any case to Rust Debug format (used in HMAC signatures)
143
+ # The Rust server uses {:?} format which produces these exact strings
144
+ TASK_RUST_DEBUG_FORMAT = {
145
+ # Operations
146
+ "deploy": "Deploy",
147
+ "restart": "Restart",
148
+ "backup": "Backup",
149
+ "rollback": "Rollback",
150
+ # Agent lifecycle
151
+ "heartbeat": "Heartbeat",
152
+ "checkpoint": "Checkpoint",
153
+ "teleport": "Teleport",
154
+ # MCP tool categories
155
+ "fileread": "FileRead",
156
+ "file_read": "FileRead",
157
+ "read": "FileRead",
158
+ "query": "FileRead",
159
+ "filewrite": "FileWrite",
160
+ "file_write": "FileWrite",
161
+ "write": "FileWrite",
162
+ "edit": "FileWrite",
163
+ "network": "Network",
164
+ "http": "Network",
165
+ "fetch": "Network",
166
+ "shell": "Shell",
167
+ "bash": "Shell",
168
+ "exec": "Shell",
169
+ "execute": "Shell",
170
+ "database": "Database",
171
+ "db": "Database",
172
+ "sql": "Database",
173
+ # A2A protocol
174
+ "handoff": "Handoff",
175
+ "hand_off": "Handoff",
176
+ "delegate": "Delegate",
177
+ # Wildcard
178
+ "all": "All",
179
+ "admin": "All",
180
+ }
181
+
182
+
183
+ def normalize_task_for_signature(task: str) -> str:
184
+ """
185
+ Convert a task name to the Rust Debug format used in HMAC signatures.
186
+
187
+ The Rust server uses {:?} format for Task enum in signatures, which produces
188
+ PascalCase variants like 'Heartbeat', 'FileRead', etc.
189
+
190
+ Args:
191
+ task: Task name in any case (e.g., "HEARTBEAT", "heartbeat", "Heartbeat")
192
+
193
+ Returns:
194
+ Task name in Rust Debug format (e.g., "Heartbeat")
195
+ """
196
+ normalized = TASK_RUST_DEBUG_FORMAT.get(task.lower())
197
+ if normalized:
198
+ return normalized
199
+
200
+ # Fallback: try to capitalize properly for unknown tasks
201
+ # This handles cases like "deploy" -> "Deploy"
202
+ return task.capitalize()
203
+
204
+
205
+ # Prime field modulus (F_65537)
206
+ MODULUS = 65537
207
+
208
+
209
+ class Matrix:
210
+ """2x2 matrix over F_65537 for topological state transitions."""
211
+
212
+ def __init__(self, a11: int, a12: int, a21: int, a22: int):
213
+ self.a11 = a11 % MODULUS
214
+ self.a12 = a12 % MODULUS
215
+ self.a21 = a21 % MODULUS
216
+ self.a22 = a22 % MODULUS
217
+
218
+ def multiply(self, other: "Matrix") -> "Matrix":
219
+ """Matrix multiplication in F_65537."""
220
+ return Matrix(
221
+ (self.a11 * other.a11 + self.a12 * other.a21) % MODULUS,
222
+ (self.a11 * other.a12 + self.a12 * other.a22) % MODULUS,
223
+ (self.a21 * other.a11 + self.a22 * other.a21) % MODULUS,
224
+ (self.a21 * other.a12 + self.a22 * other.a22) % MODULUS,
225
+ )
226
+
227
+ def to_dict(self) -> Dict[str, int]:
228
+ """Convert to dictionary for JSON serialization."""
229
+ return {"a11": self.a11, "a12": self.a12, "a21": self.a21, "a22": self.a22}
230
+
231
+ @classmethod
232
+ def from_dict(cls, d: Dict[str, int]) -> "Matrix":
233
+ """Create from dictionary."""
234
+ return cls(d.get("a11", 1), d.get("a12", 0), d.get("a21", 0), d.get("a22", 1))
235
+
236
+ @classmethod
237
+ def identity(cls) -> "Matrix":
238
+ """Return identity matrix."""
239
+ return cls(1, 0, 0, 1)
240
+
241
+
242
+ # Generator matrices for each task (matching Rust server)
243
+ GENERATOR_MATRICES = {
244
+ "DEPLOY": Matrix(1, 1, 0, 1),
245
+ "RESTART": Matrix(1, 0, 1, 1),
246
+ "BACKUP": Matrix(2, 1, 1, 1),
247
+ "ROLLBACK": Matrix(1, 1, 1, 2),
248
+ "HEARTBEAT": Matrix(0, 65536, 1, 0), # 90-degree rotation
249
+ "CHECKPOINT": Matrix(2, 1, 1, 2),
250
+ "TELEPORT": Matrix(3, 1, 1, 1),
251
+ "FILEREAD": Matrix(1, 2, 0, 1),
252
+ "FILEWRITE": Matrix(1, 3, 0, 1),
253
+ "NETWORK": Matrix(1, 0, 2, 1),
254
+ "SHELL": Matrix(1, 4, 0, 1), # Upper triangular (distinct from Deploy, v1.0.8+)
255
+ "DATABASE": Matrix(1, 0, 3, 1),
256
+ "HANDOFF": Matrix(3, 2, 1, 1),
257
+ "DELEGATE": Matrix(1, 1, 2, 3),
258
+ "ALL": Matrix(1, 0, 0, 1), # Identity
259
+ }
260
+
261
+
262
+ def get_generator(task: str) -> Matrix:
263
+ """Get the generator matrix for a task."""
264
+ return GENERATOR_MATRICES.get(task.upper(), Matrix.identity())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lockstock-integrations
3
- Version: 1.0.1
3
+ Version: 1.1.1
4
4
  Summary: LockStock compliance runtime integrations for AI Agent SDKs
5
5
  Project-URL: Homepage, https://d3cipher.ai
6
6
  Project-URL: Documentation, https://d3cipher.ai/docs
@@ -5,12 +5,15 @@ lockstock_a2a/task_handler.py,sha256=hKpk9smt12nqTlJua861LpdKiiXpcRGRYhCW0SwS438
5
5
  lockstock_claude/__init__.py,sha256=yI1e13JC_KKgYrap2AIkVl5biCsHKfb9rPtgbN1QNtg,320
6
6
  lockstock_claude/hooks.py,sha256=uJ-i3s9pmwcgSIpc84TlxNAyP2a1S8Auyo6o1H3KrcU,5116
7
7
  lockstock_claude/skills.py,sha256=4f6HprTBfjmPvcDzJ40fcsMoF543EybbE0_KM_kBtpQ,4023
8
+ lockstock_core/__init__.py,sha256=Euan9X4O6XBgQ8IYoC5hj2ruTNU0vPLvBTCIPZZNQdU,258
9
+ lockstock_core/client.py,sha256=gNacFIoH9DA9Dw2moyfPHGj7mlexrJ7VxIo0e3iRX8M,9110
10
+ lockstock_core/types.py,sha256=4OtO7PWcZyXxbmuFcoXXxjO3tKHV1oNGpm5yExLTCQE,7426
8
11
  lockstock_langgraph/__init__.py,sha256=7fBIYNCQygvUvG7IQOVY042sNNmu4UwkJoy0VLQn4Ik,425
9
12
  lockstock_langgraph/checkpointer.py,sha256=XJ2c_92OSBq4aNpOT112nTy3yI3stFcSCb8OTx04nos,6317
10
13
  lockstock_langgraph/middleware.py,sha256=ZSwoxdkn3cC6aIBnMgd1jYGHYKa1yQXRwnts3Ds4sfQ,9174
11
14
  lockstock_openai/__init__.py,sha256=Pc4phRUJEqrnREggyGJnc1HDKa7puVAR6-G9WZ4dtXM,402
12
- lockstock_openai/guardrails.py,sha256=10wYzpR900-41A0eacC2dyUpgy-6yLSJRTOZydjk_c8,7119
15
+ lockstock_openai/guardrails.py,sha256=fy84bDrhy6CVk2b4_yOl5XTxw9Lp1HT4Mw9WcaAHOsg,11920
13
16
  lockstock_openai/tracing.py,sha256=VQWzG0y6t8q5rJZFbbP-bi9tsV0KaUqqH4Kr9-vr_e8,6025
14
- lockstock_integrations-1.0.1.dist-info/METADATA,sha256=AiOMxN-VM6v3z6LAq74MkrkppjbOQt_ceLhNdxekviE,5629
15
- lockstock_integrations-1.0.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
16
- lockstock_integrations-1.0.1.dist-info/RECORD,,
17
+ lockstock_integrations-1.1.1.dist-info/METADATA,sha256=H022KMhtIDctPxNHkdc70WRfzjyO72xwK6Ed5u69Au8,5629
18
+ lockstock_integrations-1.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
19
+ lockstock_integrations-1.1.1.dist-info/RECORD,,
@@ -1,13 +1,19 @@
1
1
  """
2
2
  LockStock OpenAI Agents SDK Guardrails
3
3
  ---------------------------------------
4
- Guardrail implementations for OpenAI Agents SDK.
4
+ Chain-based guardrail implementations for OpenAI Agents SDK.
5
+
6
+ NO SECRETS. ONLY CHAIN STATE.
7
+
8
+ The Guard daemon tracks chain state (hash, matrix, sequence).
9
+ When signing, Guard uses current_hash as the HMAC key.
5
10
 
6
11
  Usage:
7
12
  from agents import Agent
8
13
  from lockstock_openai import LockStockGuardrail
9
14
 
10
- guardrail = LockStockGuardrail(agent_id="agent_abc123")
15
+ # Chain Authority Mode (recommended)
16
+ guardrail = LockStockGuardrail.from_liberty("agent_abc123")
11
17
 
12
18
  agent = Agent(
13
19
  name="my-agent",
@@ -16,11 +22,10 @@ Usage:
16
22
  )
17
23
  """
18
24
 
19
- from typing import Any, Dict, Optional, List
25
+ from typing import Any, Dict, Optional
20
26
  from dataclasses import dataclass
21
27
 
22
- from lockstock_core import LockStockClient
23
- from lockstock_core.types import map_tool_to_capability, VerifyStatus
28
+ from lockstock_core.types import map_tool_to_capability
24
29
 
25
30
 
26
31
  @dataclass
@@ -33,41 +38,56 @@ class GuardrailResult:
33
38
 
34
39
  class LockStockGuardrail:
35
40
  """
36
- Base guardrail for OpenAI Agents SDK.
41
+ Chain-based guardrail for OpenAI Agents SDK.
42
+
43
+ NO SECRETS STORED. Chain state is the authentication.
37
44
 
38
- Validates that the agent has permission to perform requested actions.
45
+ The Guard daemon:
46
+ 1. Tracks current chain state (hash, matrix, sequence)
47
+ 2. Uses current_hash as HMAC key for signatures
48
+ 3. Returns signatures (not secrets!)
39
49
  """
40
50
 
41
51
  name = "lockstock"
42
- description = "LockStock capability authorization guardrail"
52
+ description = "LockStock chain-based authorization guardrail"
43
53
 
44
54
  def __init__(
45
55
  self,
46
56
  agent_id: str,
47
- secret: Optional[str] = None,
48
- api_key: Optional[str] = None,
57
+ guard_socket: str = "/var/run/lockstock-guard/guard.sock",
49
58
  endpoint: str = "https://lockstock-api-i9kp.onrender.com",
50
59
  block_on_failure: bool = True
51
60
  ):
52
61
  """
53
- Initialize LockStock guardrail.
62
+ Initialize chain-based guardrail.
54
63
 
55
64
  Args:
56
65
  agent_id: The agent's unique identifier
57
- secret: The agent's HMAC secret
58
- api_key: Admin API key
66
+ guard_socket: Path to Guard daemon Unix socket
59
67
  endpoint: LockStock API endpoint
60
68
  block_on_failure: If True, block execution on auth failure
69
+
70
+ NO SECRET PARAMETER! Secrets don't exist in this system.
61
71
  """
62
- self.client = LockStockClient(
63
- agent_id=agent_id,
64
- secret=secret,
65
- api_key=api_key,
66
- endpoint=endpoint
67
- )
68
72
  self.agent_id = agent_id
73
+ self.guard_socket = guard_socket
74
+ self.endpoint = endpoint
69
75
  self.block_on_failure = block_on_failure
70
76
 
77
+ # Chain state (cached locally, synced from Guard)
78
+ self.current_hash: Optional[str] = None
79
+ self.current_sequence: int = 0
80
+ self.current_matrix: Optional[Dict[str, int]] = None
81
+ self._chain_initialized = False
82
+
83
+ # Guard client (for chain operations)
84
+ from lockstock_guard.client import LibertyClient
85
+ self._guard = LibertyClient(socket_path=guard_socket)
86
+
87
+ # HTTP client (for server communication)
88
+ import httpx
89
+ self._http = httpx.AsyncClient(timeout=30.0)
90
+
71
91
  @classmethod
72
92
  def from_liberty(
73
93
  cls,
@@ -77,52 +97,77 @@ class LockStockGuardrail:
77
97
  block_on_failure: bool = True
78
98
  ):
79
99
  """
80
- Create guardrail by reading secret from Liberty Guard daemon.
100
+ Create guardrail using Chain Authority Mode.
101
+
102
+ NO SECRET RETRIEVAL. NO SECRET STORAGE.
81
103
 
82
- This is the recommended method for enterprise deployments where secrets
83
- are hardware-bound and managed by the lockstock-guard daemon.
104
+ The Guard daemon manages chain state. We just track it locally.
84
105
 
85
106
  Args:
86
- agent_id: The agent's unique identifier
87
- socket_path: Path to Liberty Guard Unix socket
107
+ agent_id: Agent identifier (from provisioning)
108
+ socket_path: Path to Guard daemon Unix socket
88
109
  endpoint: LockStock API endpoint
89
- block_on_failure: If True, block execution on auth failure
110
+ block_on_failure: Whether to block on auth failure
90
111
 
91
112
  Returns:
92
- LockStockGuardrail instance with secret loaded from Guard daemon
113
+ LockStockGuardrail instance using chain-based auth
93
114
 
94
115
  Example:
95
116
  >>> guardrail = LockStockGuardrail.from_liberty("agent_abc123")
96
117
  >>> agent = Agent(guardrails=[guardrail])
97
118
  """
98
- import json
99
- from lockstock_guard.client import LibertyClient
100
-
101
- # Retrieve secret from Guard daemon
102
- guard_client = LibertyClient(socket_path=socket_path)
103
- try:
104
- secret_data = json.loads(guard_client.get(agent_id))
105
- secret = secret_data["value"]
106
- finally:
107
- guard_client.close()
108
-
109
- # Create guardrail with retrieved secret
110
119
  return cls(
111
120
  agent_id=agent_id,
112
- secret=secret,
121
+ guard_socket=socket_path,
113
122
  endpoint=endpoint,
114
123
  block_on_failure=block_on_failure
115
124
  )
116
125
 
126
+ async def _ensure_chain_initialized(self):
127
+ """
128
+ Sync chain state from Guard daemon on first use.
129
+
130
+ Guard fetches current state from server and caches it.
131
+ We sync that cached state to our local tracking.
132
+ """
133
+ if self._chain_initialized:
134
+ return
135
+
136
+ try:
137
+ # Ask Guard to sync from server
138
+ sync_result = self._guard.sync_chain(self.agent_id)
139
+
140
+ if not sync_result.get("success"):
141
+ raise Exception(f"Chain sync failed: {sync_result}")
142
+
143
+ # Update local chain state cache
144
+ self.current_hash = sync_result.get("current_hash")
145
+ self.current_sequence = sync_result.get("sequence", 0)
146
+ self.current_matrix = sync_result.get("state_matrix")
147
+ self._chain_initialized = True
148
+
149
+ except Exception as e:
150
+ raise Exception(
151
+ f"Failed to initialize chain state for {self.agent_id}. "
152
+ f"Ensure Guard daemon is running and agent is provisioned. "
153
+ f"Error: {e}"
154
+ )
155
+
117
156
  async def validate(
118
157
  self,
119
158
  agent: Any,
120
159
  message: Dict[str, Any]
121
160
  ) -> GuardrailResult:
122
161
  """
123
- Validate a message before it's processed or sent.
162
+ Validate a message using chain-based authentication.
163
+
164
+ Flow:
165
+ 1. Check if action requires authorization
166
+ 2. Ask Guard to sign (Guard uses current_hash as HMAC key)
167
+ 3. Send Guard's signature to server for verification
168
+ 4. Update local chain state if verified
124
169
 
125
- This method is called by the OpenAI Agents SDK.
170
+ NO SECRETS are involved. Only chain state.
126
171
 
127
172
  Args:
128
173
  agent: The agent instance
@@ -134,42 +179,146 @@ class LockStockGuardrail:
134
179
  # Check for tool calls in assistant messages
135
180
  if message.get("role") == "assistant" and "tool_calls" in message:
136
181
  for tool_call in message.get("tool_calls", []):
137
- if "function" in tool_call:
138
- function_name = tool_call["function"].get("name", "")
182
+ if "function" not in tool_call:
183
+ continue
184
+
185
+ function_name = tool_call["function"].get("name", "")
186
+
187
+ # Map tool to capability
188
+ capability = map_tool_to_capability(function_name)
189
+
190
+ # Ensure chain state is synced
191
+ await self._ensure_chain_initialized()
192
+
193
+ try:
194
+ # ========================================================
195
+ # CHAIN AUTHORITY MODE
196
+ # ========================================================
197
+ # Guard daemon:
198
+ # 1. Looks up current chain state for this agent
199
+ # 2. Calculates: new_matrix = current_matrix × generator[task]
200
+ # 3. Signs with: HMAC(current_hash, message)
201
+ # ↑ current_hash IS the key! No separate secret!
202
+ # 4. Calculates: new_hash = SHA256(...)
203
+ # 5. Returns: {signature, new_hash, new_matrix, ...}
204
+ # ========================================================
205
+
206
+ sign_result = self._guard.sign_and_advance(
207
+ agent_id=self.agent_id,
208
+ task=capability,
209
+ parent_hash=self.current_hash
210
+ )
139
211
 
140
- # Map to capability
141
- capability = map_tool_to_capability(function_name)
212
+ # Guard returned:
213
+ # - signature: HMAC computed using current_hash as key
214
+ # - new_hash: Next chain head
215
+ # - new_sequence: Incremented sequence
216
+ # - new_matrix: After applying generator
217
+ # - timestamp: Unix timestamp used in signature
218
+
219
+ except Exception as e:
220
+ return GuardrailResult(
221
+ passed=False,
222
+ reason=f"Guard signing failed: {e}"
223
+ )
142
224
 
143
- # Verify with LockStock
144
- result = await self.client.verify(
225
+ # Send Guard's signature to LockStock server for verification
226
+ try:
227
+ verify_result = await self._verify_with_server(
145
228
  task=capability,
146
- tool_name=function_name
229
+ signature=sign_result["signature"],
230
+ parent_hash=self.current_hash,
231
+ state_matrix=sign_result["new_matrix"],
232
+ timestamp=sign_result["timestamp"],
233
+ sequence=sign_result["new_sequence"]
147
234
  )
148
235
 
149
- if not result.authorized:
236
+ if not verify_result["authorized"]:
150
237
  if self.block_on_failure:
151
238
  return GuardrailResult(
152
239
  passed=False,
153
240
  reason=f"LockStock DENIED: {function_name} requires {capability}. "
154
- f"Agent {self.agent_id} lacks this capability."
241
+ f"Reason: {verify_result.get('reason', 'Unknown')}"
155
242
  )
156
243
 
244
+ # Update local chain state cache
245
+ self.current_hash = sign_result["new_hash"]
246
+ self.current_sequence = sign_result["new_sequence"]
247
+ self.current_matrix = sign_result["new_matrix"]
248
+
249
+ except Exception as e:
250
+ return GuardrailResult(
251
+ passed=False,
252
+ reason=f"Server verification failed: {e}"
253
+ )
254
+
157
255
  return GuardrailResult(passed=True)
158
256
 
257
+ async def _verify_with_server(
258
+ self,
259
+ task: str,
260
+ signature: str,
261
+ parent_hash: str,
262
+ state_matrix: Dict[str, int],
263
+ timestamp: int,
264
+ sequence: int
265
+ ) -> Dict[str, Any]:
266
+ """
267
+ Send Guard-signed request to LockStock server.
268
+
269
+ The signature was created by Guard using current_hash as HMAC key.
270
+ Server will verify by:
271
+ 1. Looking up its current_hash for this agent
272
+ 2. Recalculating HMAC using that hash
273
+ 3. Comparing to signature we send
274
+
275
+ Args:
276
+ task: Capability being verified
277
+ signature: HMAC signature from Guard (signed with current_hash)
278
+ parent_hash: Parent hash in chain
279
+ state_matrix: New state matrix
280
+ timestamp: Timestamp used in signature
281
+ sequence: Sequence number
282
+
283
+ Returns:
284
+ Dict with authorized (bool) and reason (str)
285
+ """
286
+ payload = {
287
+ "client_id": self.agent_id,
288
+ "task": task.lower(),
289
+ "parent_hash": parent_hash,
290
+ "state_matrix": state_matrix,
291
+ "signature": signature, # ← From Guard, signed with current_hash
292
+ "timestamp": timestamp,
293
+ "sequence": sequence
294
+ }
295
+
296
+ response = await self._http.post(
297
+ f"{self.endpoint}/verify",
298
+ json=payload
299
+ )
300
+
301
+ data = response.json()
302
+
303
+ return {
304
+ "authorized": data.get("status") == "accepted",
305
+ "reason": data.get("reason"),
306
+ "state_hash": data.get("state_hash")
307
+ }
308
+
159
309
  async def close(self):
160
- """Close the underlying client."""
161
- await self.client.close()
310
+ """Close connections."""
311
+ await self._http.aclose()
312
+ self._guard.close()
162
313
 
163
314
 
164
315
  class LockStockInputGuardrail(LockStockGuardrail):
165
316
  """
166
- Input guardrail for OpenAI Agents SDK.
167
-
168
- Validates incoming messages/requests before processing.
317
+ Input guardrail using chain-based authentication.
169
318
  """
170
319
 
171
320
  name = "lockstock_input"
172
- description = "LockStock input validation guardrail"
321
+ description = "LockStock chain-based input validation guardrail"
173
322
 
174
323
  async def validate_input(
175
324
  self,
@@ -177,35 +326,19 @@ class LockStockInputGuardrail(LockStockGuardrail):
177
326
  user_input: str,
178
327
  context: Optional[Dict[str, Any]] = None
179
328
  ) -> GuardrailResult:
180
- """
181
- Validate user input before processing.
182
-
183
- Args:
184
- agent: The agent instance
185
- user_input: The user's input string
186
- context: Optional context dictionary
187
-
188
- Returns:
189
- GuardrailResult indicating if validation passed
190
- """
191
- # Input validation can check for:
192
- # - Sensitive data patterns
193
- # - Rate limiting
194
- # - Input size limits
195
-
196
- # For now, pass through - capability checks happen at tool use
329
+ """Validate user input."""
330
+ # Input validation - pass through for now
331
+ # Capability checks happen at tool use
197
332
  return GuardrailResult(passed=True)
198
333
 
199
334
 
200
335
  class LockStockOutputGuardrail(LockStockGuardrail):
201
336
  """
202
- Output guardrail for OpenAI Agents SDK.
203
-
204
- Validates agent outputs before returning to user.
337
+ Output guardrail using chain-based authentication.
205
338
  """
206
339
 
207
340
  name = "lockstock_output"
208
- description = "LockStock output validation guardrail"
341
+ description = "LockStock chain-based output validation guardrail"
209
342
 
210
343
  async def validate_output(
211
344
  self,
@@ -213,27 +346,7 @@ class LockStockOutputGuardrail(LockStockGuardrail):
213
346
  output: str,
214
347
  context: Optional[Dict[str, Any]] = None
215
348
  ) -> GuardrailResult:
216
- """
217
- Validate agent output before returning.
218
-
219
- Args:
220
- agent: The agent instance
221
- output: The agent's output string
222
- context: Optional context dictionary
223
-
224
- Returns:
225
- GuardrailResult indicating if validation passed
226
- """
227
- # Output validation can check for:
228
- # - Sensitive data leakage
229
- # - Compliance requirements
230
- # - Output format requirements
231
-
232
- # Log the output to audit trail
233
- await self.client.log_audit(
234
- action="output",
235
- status="COMPLETED",
236
- metadata={"output_length": len(output)}
237
- )
238
-
349
+ """Validate agent output."""
350
+ # Output validation - pass through for now
351
+ # Could log to audit trail here
239
352
  return GuardrailResult(passed=True)