tollgate 1.0.2__py3-none-any.whl → 1.0.4__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.
- tollgate/__init__.py +6 -2
- tollgate/approvals.py +39 -22
- tollgate/grants.py +103 -0
- tollgate/interceptors/openai.py +19 -1
- tollgate/policy.py +12 -2
- tollgate/tower.py +59 -4
- tollgate/types.py +25 -0
- {tollgate-1.0.2.dist-info → tollgate-1.0.4.dist-info}/METADATA +28 -1
- {tollgate-1.0.2.dist-info → tollgate-1.0.4.dist-info}/RECORD +11 -10
- {tollgate-1.0.2.dist-info → tollgate-1.0.4.dist-info}/WHEEL +0 -0
- {tollgate-1.0.2.dist-info → tollgate-1.0.4.dist-info}/licenses/LICENSE +0 -0
tollgate/__init__.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
from .approvals import (
|
|
2
|
-
ApprovalOutcome,
|
|
3
2
|
ApprovalStore,
|
|
4
3
|
Approver,
|
|
5
4
|
AsyncQueueApprover,
|
|
@@ -15,23 +14,26 @@ from .exceptions import (
|
|
|
15
14
|
TollgateDenied,
|
|
16
15
|
TollgateError,
|
|
17
16
|
)
|
|
17
|
+
from .grants import InMemoryGrantStore
|
|
18
18
|
from .helpers import guard, wrap_tool
|
|
19
19
|
from .policy import PolicyEvaluator, YamlPolicyEvaluator
|
|
20
20
|
from .registry import ToolRegistry
|
|
21
21
|
from .tower import ControlTower
|
|
22
22
|
from .types import (
|
|
23
23
|
AgentContext,
|
|
24
|
+
ApprovalOutcome,
|
|
24
25
|
AuditEvent,
|
|
25
26
|
Decision,
|
|
26
27
|
DecisionType,
|
|
27
28
|
Effect,
|
|
29
|
+
Grant,
|
|
28
30
|
Intent,
|
|
29
31
|
NormalizedToolCall,
|
|
30
32
|
Outcome,
|
|
31
33
|
ToolRequest,
|
|
32
34
|
)
|
|
33
35
|
|
|
34
|
-
__version__ = "1.0.
|
|
36
|
+
__version__ = "1.0.4"
|
|
35
37
|
|
|
36
38
|
__all__ = [
|
|
37
39
|
"ControlTower",
|
|
@@ -42,6 +44,7 @@ __all__ = [
|
|
|
42
44
|
"Decision",
|
|
43
45
|
"DecisionType",
|
|
44
46
|
"Effect",
|
|
47
|
+
"Grant",
|
|
45
48
|
"AuditEvent",
|
|
46
49
|
"Outcome",
|
|
47
50
|
"ApprovalOutcome",
|
|
@@ -57,6 +60,7 @@ __all__ = [
|
|
|
57
60
|
"ToolRegistry",
|
|
58
61
|
"PolicyEvaluator",
|
|
59
62
|
"YamlPolicyEvaluator",
|
|
63
|
+
"InMemoryGrantStore",
|
|
60
64
|
"TollgateError",
|
|
61
65
|
"TollgateDenied",
|
|
62
66
|
"TollgateApprovalDenied",
|
tollgate/approvals.py
CHANGED
|
@@ -56,6 +56,7 @@ class InMemoryApprovalStore(ApprovalStore):
|
|
|
56
56
|
def __init__(self):
|
|
57
57
|
self._requests: dict[str, dict[str, Any]] = {}
|
|
58
58
|
self._events: dict[str, asyncio.Event] = {}
|
|
59
|
+
self._lock = asyncio.Lock()
|
|
59
60
|
|
|
60
61
|
async def create_request(
|
|
61
62
|
self, agent_ctx, intent, tool_request, request_hash, reason, expiry
|
|
@@ -77,19 +78,21 @@ class InMemoryApprovalStore(ApprovalStore):
|
|
|
77
78
|
async def set_decision(
|
|
78
79
|
self, approval_id, outcome, decided_by, decided_at, request_hash
|
|
79
80
|
):
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
81
|
+
# Security: Use lock for atomic read-modify-write operation
|
|
82
|
+
async with self._lock:
|
|
83
|
+
if approval_id in self._requests:
|
|
84
|
+
req = self._requests[approval_id]
|
|
85
|
+
# Replay protection: hash must match
|
|
86
|
+
if req["request_hash"] != request_hash:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
"Request hash mismatch. Approval bound to a different request."
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
req["outcome"] = outcome
|
|
92
|
+
req["decided_by"] = decided_by
|
|
93
|
+
req["decided_at"] = decided_at
|
|
94
|
+
if approval_id in self._events:
|
|
95
|
+
self._events[approval_id].set()
|
|
93
96
|
|
|
94
97
|
async def get_request(self, approval_id):
|
|
95
98
|
return self._requests.get(approval_id)
|
|
@@ -172,21 +175,35 @@ class AutoApprover:
|
|
|
172
175
|
class CliApprover:
|
|
173
176
|
"""Async-wrapped CLI approver for development."""
|
|
174
177
|
|
|
175
|
-
def __init__(self, show_emojis: bool = True):
|
|
178
|
+
def __init__(self, show_emojis: bool = True, timeout: float = 300.0):
|
|
179
|
+
"""
|
|
180
|
+
Initialize CliApprover.
|
|
181
|
+
|
|
182
|
+
:param show_emojis: Whether to display emojis in prompts.
|
|
183
|
+
:param timeout: Timeout in seconds for user input (default 5 minutes).
|
|
184
|
+
"""
|
|
176
185
|
self.show_emojis = show_emojis
|
|
186
|
+
self.timeout = timeout
|
|
177
187
|
|
|
178
188
|
async def request_approval_async(
|
|
179
189
|
self, agent_ctx, intent, tool_request, _hash, reason
|
|
180
190
|
) -> ApprovalOutcome:
|
|
181
191
|
loop = asyncio.get_event_loop()
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
192
|
+
try:
|
|
193
|
+
return await asyncio.wait_for(
|
|
194
|
+
loop.run_in_executor(
|
|
195
|
+
None,
|
|
196
|
+
self._sync_request,
|
|
197
|
+
agent_ctx,
|
|
198
|
+
intent,
|
|
199
|
+
tool_request,
|
|
200
|
+
reason,
|
|
201
|
+
),
|
|
202
|
+
timeout=self.timeout,
|
|
203
|
+
)
|
|
204
|
+
except asyncio.TimeoutError:
|
|
205
|
+
print("\nApproval request timed out.")
|
|
206
|
+
return ApprovalOutcome.TIMEOUT
|
|
190
207
|
|
|
191
208
|
def _sync_request(self, agent_ctx, intent, tool_request, reason) -> ApprovalOutcome:
|
|
192
209
|
prefix = "🚦 " if self.show_emojis else ""
|
tollgate/grants.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
from .types import AgentContext, Grant, ToolRequest
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class InMemoryGrantStore:
|
|
8
|
+
"""In-memory store for action grants with thread-safe matching logic."""
|
|
9
|
+
|
|
10
|
+
def __init__(self):
|
|
11
|
+
self._grants: dict[str, Grant] = {}
|
|
12
|
+
self._usage_counts: dict[str, int] = {}
|
|
13
|
+
self._lock = asyncio.Lock()
|
|
14
|
+
|
|
15
|
+
async def create_grant(self, grant: Grant) -> str:
|
|
16
|
+
"""Store a new grant."""
|
|
17
|
+
async with self._lock:
|
|
18
|
+
self._grants[grant.id] = grant
|
|
19
|
+
self._usage_counts[grant.id] = 0
|
|
20
|
+
return grant.id
|
|
21
|
+
|
|
22
|
+
async def find_matching_grant(
|
|
23
|
+
self, agent_ctx: AgentContext, tool_request: ToolRequest
|
|
24
|
+
) -> Grant | None:
|
|
25
|
+
"""Find a non-expired grant that matches the request."""
|
|
26
|
+
now = time.time()
|
|
27
|
+
async with self._lock:
|
|
28
|
+
for grant in self._grants.values():
|
|
29
|
+
# 1. Skip expired
|
|
30
|
+
if grant.expires_at <= now:
|
|
31
|
+
continue
|
|
32
|
+
|
|
33
|
+
# 2. Match agent_id
|
|
34
|
+
if grant.agent_id is not None and grant.agent_id != agent_ctx.agent_id:
|
|
35
|
+
continue
|
|
36
|
+
|
|
37
|
+
# 3. Match effect
|
|
38
|
+
if grant.effect is not None and grant.effect != tool_request.effect:
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
# 4. Match tool (exact or prefix with *)
|
|
42
|
+
if grant.tool is not None:
|
|
43
|
+
if grant.tool.endswith("*"):
|
|
44
|
+
prefix = grant.tool[:-1]
|
|
45
|
+
if not tool_request.tool.startswith(prefix):
|
|
46
|
+
continue
|
|
47
|
+
elif grant.tool != tool_request.tool:
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
# 5. Match action
|
|
51
|
+
if grant.action is not None and grant.action != tool_request.action:
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
# 6. Match resource_type
|
|
55
|
+
if (
|
|
56
|
+
grant.resource_type is not None
|
|
57
|
+
and grant.resource_type != tool_request.resource_type
|
|
58
|
+
):
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
# Match found! Increment usage count
|
|
62
|
+
self._usage_counts[grant.id] += 1
|
|
63
|
+
return grant
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
async def get_usage_count(self, grant_id: str) -> int:
|
|
67
|
+
"""Get the number of times a grant has been used."""
|
|
68
|
+
async with self._lock:
|
|
69
|
+
return self._usage_counts.get(grant_id, 0)
|
|
70
|
+
|
|
71
|
+
async def revoke_grant(self, grant_id: str) -> bool:
|
|
72
|
+
"""Remove a grant by ID."""
|
|
73
|
+
async with self._lock:
|
|
74
|
+
if grant_id in self._grants:
|
|
75
|
+
del self._grants[grant_id]
|
|
76
|
+
if grant_id in self._usage_counts:
|
|
77
|
+
del self._usage_counts[grant_id]
|
|
78
|
+
return True
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
async def list_active_grants(self, agent_id: str | None = None) -> list[Grant]:
|
|
82
|
+
"""List all non-expired grants, optionally filtered by agent."""
|
|
83
|
+
now = time.time()
|
|
84
|
+
async with self._lock:
|
|
85
|
+
active = []
|
|
86
|
+
for grant in self._grants.values():
|
|
87
|
+
if grant.expires_at > now:
|
|
88
|
+
if agent_id is None or grant.agent_id == agent_id:
|
|
89
|
+
active.append(grant)
|
|
90
|
+
return active
|
|
91
|
+
|
|
92
|
+
async def cleanup_expired(self) -> int:
|
|
93
|
+
"""Remove all expired grants from the store."""
|
|
94
|
+
now = time.time()
|
|
95
|
+
async with self._lock:
|
|
96
|
+
to_remove = [
|
|
97
|
+
gid for gid, g in self._grants.items() if g.expires_at <= now
|
|
98
|
+
]
|
|
99
|
+
for gid in to_remove:
|
|
100
|
+
del self._grants[gid]
|
|
101
|
+
if gid in self._usage_counts:
|
|
102
|
+
del self._usage_counts[gid]
|
|
103
|
+
return len(to_remove)
|
tollgate/interceptors/openai.py
CHANGED
|
@@ -3,6 +3,7 @@ import json
|
|
|
3
3
|
from collections.abc import Callable
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
+
from ..exceptions import TollgateDenied
|
|
6
7
|
from ..registry import ToolRegistry
|
|
7
8
|
from ..tower import ControlTower
|
|
8
9
|
from ..types import AgentContext, Intent, NormalizedToolCall, ToolRequest
|
|
@@ -26,10 +27,23 @@ class OpenAIAdapter:
|
|
|
26
27
|
kwargs = {}
|
|
27
28
|
|
|
28
29
|
tool_name = tc_dict.get("function", {}).get("name") or tc_dict.get("name")
|
|
30
|
+
|
|
31
|
+
# Security: Validate tool_name is not None or empty
|
|
32
|
+
if not tool_name:
|
|
33
|
+
raise TollgateDenied("Tool call missing required 'name' field")
|
|
34
|
+
|
|
29
35
|
args_str = tc_dict.get("function", {}).get("arguments") or tc_dict.get(
|
|
30
36
|
"arguments"
|
|
31
37
|
)
|
|
32
|
-
|
|
38
|
+
|
|
39
|
+
# Security: Safe JSON parsing with proper error handling
|
|
40
|
+
if isinstance(args_str, str):
|
|
41
|
+
try:
|
|
42
|
+
args = json.loads(args_str)
|
|
43
|
+
except json.JSONDecodeError as e:
|
|
44
|
+
raise TollgateDenied(f"Invalid JSON in tool arguments: {e.msg}") from e
|
|
45
|
+
else:
|
|
46
|
+
args = args_str if args_str is not None else {}
|
|
33
47
|
|
|
34
48
|
registry_key = f"openai:{tool_name}"
|
|
35
49
|
effect, resource_type, manifest_version = self.registry.resolve_tool(
|
|
@@ -48,6 +62,10 @@ class OpenAIAdapter:
|
|
|
48
62
|
manifest_version=manifest_version,
|
|
49
63
|
)
|
|
50
64
|
|
|
65
|
+
# Security: Handle missing tool in tool_map
|
|
66
|
+
if tool_name not in self.tool_map:
|
|
67
|
+
raise TollgateDenied(f"Unknown tool: {tool_name}")
|
|
68
|
+
|
|
51
69
|
func = self.tool_map[tool_name]
|
|
52
70
|
|
|
53
71
|
async def _exec_async():
|
tollgate/policy.py
CHANGED
|
@@ -20,6 +20,10 @@ class PolicyEvaluator(Protocol):
|
|
|
20
20
|
class YamlPolicyEvaluator:
|
|
21
21
|
"""YAML-based policy evaluator with safe defaults."""
|
|
22
22
|
|
|
23
|
+
# Security: Whitelist of allowed attributes for agent_ctx and intent matching
|
|
24
|
+
ALLOWED_AGENT_ATTRS = frozenset({"agent_id", "version", "environment", "role"})
|
|
25
|
+
ALLOWED_INTENT_ATTRS = frozenset({"action", "reason", "session_id"})
|
|
26
|
+
|
|
23
27
|
def __init__(
|
|
24
28
|
self,
|
|
25
29
|
policy_path: str | Path,
|
|
@@ -108,15 +112,21 @@ class YamlPolicyEvaluator:
|
|
|
108
112
|
if "effect" in rule and rule["effect"] != req.effect.value:
|
|
109
113
|
return False
|
|
110
114
|
|
|
111
|
-
# Match Agent Context
|
|
115
|
+
# Match Agent Context (with attribute whitelist)
|
|
112
116
|
if "agent" in rule:
|
|
113
117
|
for key, expected_val in rule["agent"].items():
|
|
118
|
+
# Security: Only allow whitelisted attributes
|
|
119
|
+
if key not in self.ALLOWED_AGENT_ATTRS:
|
|
120
|
+
continue
|
|
114
121
|
if getattr(agent_ctx, key, None) != expected_val:
|
|
115
122
|
return False
|
|
116
123
|
|
|
117
|
-
# Match Intent
|
|
124
|
+
# Match Intent (with attribute whitelist)
|
|
118
125
|
if "intent" in rule:
|
|
119
126
|
for key, expected_val in rule["intent"].items():
|
|
127
|
+
# Security: Only allow whitelisted attributes
|
|
128
|
+
if key not in self.ALLOWED_INTENT_ATTRS:
|
|
129
|
+
continue
|
|
120
130
|
if getattr(intent, key, None) != expected_val:
|
|
121
131
|
return False
|
|
122
132
|
|
tollgate/tower.py
CHANGED
|
@@ -32,11 +32,13 @@ class ControlTower:
|
|
|
32
32
|
policy: PolicyEvaluator,
|
|
33
33
|
approver: Approver,
|
|
34
34
|
audit: AuditSink,
|
|
35
|
+
grant_store: Any | None = None,
|
|
35
36
|
redact_fn: Callable[[dict[str, Any]], dict[str, Any]] | None = None,
|
|
36
37
|
):
|
|
37
38
|
self.policy = policy
|
|
38
39
|
self.approver = approver
|
|
39
40
|
self.audit = audit
|
|
41
|
+
self.grant_store = grant_store
|
|
40
42
|
self.redact_fn = redact_fn or self._default_redact
|
|
41
43
|
|
|
42
44
|
@staticmethod
|
|
@@ -86,6 +88,26 @@ class ControlTower:
|
|
|
86
88
|
|
|
87
89
|
# 3. Handle ASK
|
|
88
90
|
if decision.decision == DecisionType.ASK:
|
|
91
|
+
# 3.1 Check Grants
|
|
92
|
+
if self.grant_store:
|
|
93
|
+
matching_grant = await self.grant_store.find_matching_grant(
|
|
94
|
+
agent_ctx, tool_request
|
|
95
|
+
)
|
|
96
|
+
if matching_grant:
|
|
97
|
+
# Grant found! Proceed to execution without asking approver
|
|
98
|
+
result = await self._execute_and_log(
|
|
99
|
+
correlation_id,
|
|
100
|
+
request_hash,
|
|
101
|
+
agent_ctx,
|
|
102
|
+
intent,
|
|
103
|
+
tool_request,
|
|
104
|
+
decision,
|
|
105
|
+
exec_async,
|
|
106
|
+
grant_id=matching_grant.id,
|
|
107
|
+
)
|
|
108
|
+
return result
|
|
109
|
+
|
|
110
|
+
# 3.2 Request Approval if no grant found
|
|
89
111
|
outcome = await self.approver.request_approval_async(
|
|
90
112
|
agent_ctx, intent, tool_request, request_hash, decision.reason
|
|
91
113
|
)
|
|
@@ -120,14 +142,36 @@ class ControlTower:
|
|
|
120
142
|
)
|
|
121
143
|
raise TollgateApprovalDenied(f"Approval failed: {outcome.value}")
|
|
122
144
|
|
|
123
|
-
# 4. Execute tool
|
|
145
|
+
# 4. Execute tool (Policy ALLOW or Approval APPROVED)
|
|
146
|
+
return await self._execute_and_log(
|
|
147
|
+
correlation_id,
|
|
148
|
+
request_hash,
|
|
149
|
+
agent_ctx,
|
|
150
|
+
intent,
|
|
151
|
+
tool_request,
|
|
152
|
+
decision,
|
|
153
|
+
exec_async,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
async def _execute_and_log(
|
|
157
|
+
self,
|
|
158
|
+
correlation_id: str,
|
|
159
|
+
request_hash: str,
|
|
160
|
+
agent_ctx: AgentContext,
|
|
161
|
+
intent: Intent,
|
|
162
|
+
tool_request: ToolRequest,
|
|
163
|
+
decision: Decision,
|
|
164
|
+
exec_async: Callable[[], Awaitable[Any]],
|
|
165
|
+
grant_id: str | None = None,
|
|
166
|
+
) -> Any:
|
|
167
|
+
"""Internal helper to execute tool and log result."""
|
|
124
168
|
result = None
|
|
125
169
|
outcome = Outcome.EXECUTED
|
|
126
170
|
try:
|
|
127
171
|
result = await exec_async()
|
|
128
172
|
except Exception as e:
|
|
129
173
|
outcome = Outcome.FAILED
|
|
130
|
-
result_summary =
|
|
174
|
+
result_summary = self._sanitize_exception(e)
|
|
131
175
|
self._log(
|
|
132
176
|
correlation_id,
|
|
133
177
|
request_hash,
|
|
@@ -136,11 +180,12 @@ class ControlTower:
|
|
|
136
180
|
tool_request,
|
|
137
181
|
decision,
|
|
138
182
|
outcome,
|
|
183
|
+
grant_id=grant_id,
|
|
139
184
|
result_summary=result_summary,
|
|
140
185
|
)
|
|
141
186
|
raise
|
|
142
187
|
|
|
143
|
-
#
|
|
188
|
+
# Final Audit
|
|
144
189
|
result_summary = self._truncate_result(result)
|
|
145
190
|
self._log(
|
|
146
191
|
correlation_id,
|
|
@@ -150,6 +195,7 @@ class ControlTower:
|
|
|
150
195
|
tool_request,
|
|
151
196
|
decision,
|
|
152
197
|
outcome,
|
|
198
|
+
grant_id=grant_id,
|
|
153
199
|
result_summary=result_summary,
|
|
154
200
|
)
|
|
155
201
|
|
|
@@ -176,7 +222,9 @@ class ControlTower:
|
|
|
176
222
|
async def _exec():
|
|
177
223
|
return exec_sync()
|
|
178
224
|
|
|
179
|
-
return asyncio.run(
|
|
225
|
+
return asyncio.run(
|
|
226
|
+
self.execute_async(agent_ctx, intent, tool_request, _exec)
|
|
227
|
+
)
|
|
180
228
|
|
|
181
229
|
def _log(
|
|
182
230
|
self,
|
|
@@ -188,6 +236,7 @@ class ControlTower:
|
|
|
188
236
|
decision: Decision,
|
|
189
237
|
outcome: Outcome,
|
|
190
238
|
approval_id: str | None = None,
|
|
239
|
+
grant_id: str | None = None,
|
|
191
240
|
result_summary: str | None = None,
|
|
192
241
|
):
|
|
193
242
|
# Redact params before logging
|
|
@@ -211,12 +260,18 @@ class ControlTower:
|
|
|
211
260
|
decision=decision,
|
|
212
261
|
outcome=outcome,
|
|
213
262
|
approval_id=approval_id,
|
|
263
|
+
grant_id=grant_id,
|
|
214
264
|
result_summary=result_summary,
|
|
215
265
|
policy_version=decision.policy_version,
|
|
216
266
|
manifest_version=req.manifest_version,
|
|
217
267
|
)
|
|
218
268
|
self.audit.emit(event)
|
|
219
269
|
|
|
270
|
+
def _sanitize_exception(self, e: Exception) -> str:
|
|
271
|
+
"""Sanitize exception message to avoid leaking sensitive data."""
|
|
272
|
+
# Only include the exception type and a generic message for security
|
|
273
|
+
return f"{type(e).__name__}: Execution failed"
|
|
274
|
+
|
|
220
275
|
def _truncate_result(self, result: Any, max_chars: int = 200) -> str | None:
|
|
221
276
|
if result is None:
|
|
222
277
|
return None
|
tollgate/types.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import uuid
|
|
1
2
|
from collections.abc import Awaitable, Callable
|
|
2
3
|
from dataclasses import asdict, dataclass, field
|
|
3
4
|
from enum import Enum
|
|
@@ -92,6 +93,28 @@ class Decision:
|
|
|
92
93
|
return d
|
|
93
94
|
|
|
94
95
|
|
|
96
|
+
@dataclass(frozen=True)
|
|
97
|
+
class Grant:
|
|
98
|
+
"""A grant that allows bypassing human approval for specific actions."""
|
|
99
|
+
|
|
100
|
+
agent_id: str | None # None = any agent
|
|
101
|
+
effect: Effect | None # None = any effect
|
|
102
|
+
tool: str | None # None = any tool, supports prefix like "mcp:*"
|
|
103
|
+
action: str | None # None = any action
|
|
104
|
+
resource_type: str | None # None = any resource
|
|
105
|
+
expires_at: float # Unix timestamp
|
|
106
|
+
granted_by: str
|
|
107
|
+
created_at: float
|
|
108
|
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
109
|
+
reason: str | None = None
|
|
110
|
+
|
|
111
|
+
def to_dict(self) -> dict[str, Any]:
|
|
112
|
+
d = asdict(self)
|
|
113
|
+
if self.effect:
|
|
114
|
+
d["effect"] = self.effect.value
|
|
115
|
+
return d
|
|
116
|
+
|
|
117
|
+
|
|
95
118
|
@dataclass(frozen=True)
|
|
96
119
|
class AuditEvent:
|
|
97
120
|
timestamp: str
|
|
@@ -103,6 +126,7 @@ class AuditEvent:
|
|
|
103
126
|
decision: Decision
|
|
104
127
|
outcome: Outcome
|
|
105
128
|
approval_id: str | None = None
|
|
129
|
+
grant_id: str | None = None
|
|
106
130
|
result_summary: str | None = None
|
|
107
131
|
policy_version: str | None = None
|
|
108
132
|
manifest_version: str | None = None
|
|
@@ -118,6 +142,7 @@ class AuditEvent:
|
|
|
118
142
|
"decision": self.decision.to_dict(),
|
|
119
143
|
"outcome": self.outcome.value,
|
|
120
144
|
"approval_id": self.approval_id,
|
|
145
|
+
"grant_id": self.grant_id,
|
|
121
146
|
"result_summary": self.result_summary,
|
|
122
147
|
"policy_version": self.policy_version,
|
|
123
148
|
"manifest_version": self.manifest_version,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tollgate
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: Runtime enforcement layer for AI agent tool calls using Identity + Intent + Policy
|
|
5
5
|
Author: Tollgate Maintainers
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -29,6 +29,9 @@ Runtime enforcement layer for AI agent tool calls using **Identity + Intent + Po
|
|
|
29
29
|
|
|
30
30
|
`tollgate` provides a deterministic safety boundary for AI agents. It ensures every tool call is validated against a policy before execution, with support for async human-in-the-loop approvals, framework interception (MCP, Strands, LangChain, OpenAI), and structured audit logging.
|
|
31
31
|
|
|
32
|
+
> [!CAUTION]
|
|
33
|
+
> **Disclaimer**: This project is an exploratory implementation intended for learning and discussion. It is not production-hardened and comes with no guarantees.
|
|
34
|
+
|
|
32
35
|
**[🚀 Quickstart Guide](https://github.com/ravi-labs/tollgate/blob/main/QUICKSTART.md) | [📊 Integration Comparison](https://github.com/ravi-labs/tollgate/blob/main/COMPARISON.md)**
|
|
33
36
|
|
|
34
37
|
```
|
|
@@ -74,6 +77,30 @@ Runtime enforcement layer for AI agent tool calls using **Identity + Intent + Po
|
|
|
74
77
|
|
|
75
78
|
## 🚀 v1 Integrations
|
|
76
79
|
|
|
80
|
+
### 🎟️ Session Grants
|
|
81
|
+
Grants allow you to pre-authorize specific actions for an agent session, bypassing human-in-the-loop approvals for repetitive or low-risk tasks.
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from tollgate import Grant, InMemoryGrantStore, Effect
|
|
85
|
+
|
|
86
|
+
# 1. Setup a grant store
|
|
87
|
+
grant_store = InMemoryGrantStore()
|
|
88
|
+
tower = ControlTower(..., grant_store=grant_store)
|
|
89
|
+
|
|
90
|
+
# 2. Issue a grant (e.g., after initial human approval)
|
|
91
|
+
grant = Grant(
|
|
92
|
+
agent_id="my-agent",
|
|
93
|
+
effect=Effect.WRITE,
|
|
94
|
+
tool="mcp:*", # Wildcard prefix: matches any MCP tool (e.g., "mcp:server.write")
|
|
95
|
+
action=None, # Wildcard: matches any action
|
|
96
|
+
resource_type=None,
|
|
97
|
+
expires_at=time.time() + 3600, # Valid for 1 hour
|
|
98
|
+
granted_by="admin-user",
|
|
99
|
+
created_at=time.time()
|
|
100
|
+
)
|
|
101
|
+
await grant_store.create_grant(grant)
|
|
102
|
+
```
|
|
103
|
+
|
|
77
104
|
### MCP (Model Context Protocol)
|
|
78
105
|
Wrap an MCP client to gate all tool calls:
|
|
79
106
|
```python
|
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
tollgate/__init__.py,sha256=
|
|
2
|
-
tollgate/approvals.py,sha256=
|
|
1
|
+
tollgate/__init__.py,sha256=6Q6_5rZP0-LOOX-ARz73_wL3EBjxjNnDSIMozJaf2oQ,1411
|
|
2
|
+
tollgate/approvals.py,sha256=lalvun6i5yaZ2EGZ-apQJBrLVmht5OfwgiUFsImPdYE,7814
|
|
3
3
|
tollgate/audit.py,sha256=ugMhuuLoyBNdYD2S_MjGN_ac4nHdtRz15MElqElREIs,1279
|
|
4
4
|
tollgate/exceptions.py,sha256=2yYY3esnHz26dyJlx_Cd_J64ryhexrgc4KliDmiVJSs,882
|
|
5
|
+
tollgate/grants.py,sha256=nPQ0hb2mxFDq6axzFc5mOgSHiT01SiSqhwbQBdFPkaY,3738
|
|
5
6
|
tollgate/helpers.py,sha256=ZFMi19_ogpTlV_svFtgJ9kkmA1sPte1dmDnHYVdWGyo,1657
|
|
6
|
-
tollgate/policy.py,sha256=
|
|
7
|
+
tollgate/policy.py,sha256=1_6HcbdpGfZtwmgqapqeRzItc_wbEg_L-fOA-BVIWNg,6110
|
|
7
8
|
tollgate/registry.py,sha256=ENKT-GnwFq0xXZj6qNlKFhAGYsxxBwtZsAdPZp2qlHM,2031
|
|
8
|
-
tollgate/tower.py,sha256=
|
|
9
|
-
tollgate/types.py,sha256=
|
|
9
|
+
tollgate/tower.py,sha256=JI8BGSCC7u0hkxIJmtrH9jflhTKm7py44JUJoz1NJbU,8536
|
|
10
|
+
tollgate/types.py,sha256=Ks3HNawhPzEAggTCab7NLb8zl06777ehEusASJWSj_g,3811
|
|
10
11
|
tollgate/integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
12
|
tollgate/integrations/mcp.py,sha256=jBy1d64Yc55fO4JSVfhKqiffVCZdBMmmNLRta9SVfDE,1750
|
|
12
13
|
tollgate/integrations/strands.py,sha256=_AWsIyva_iwLFIQeTMxxbEzLSjneo0YumWfY9oayKeM,3003
|
|
13
14
|
tollgate/interceptors/__init__.py,sha256=0c3MYyVKGYrBOZ1mMolgrAowrqZrULzAl0vv-s8IFFg,305
|
|
14
15
|
tollgate/interceptors/base.py,sha256=uJxHzH0eurcGEVknbk1kQkk_2u2qNtnM--ZRCp52Wyo,1390
|
|
15
16
|
tollgate/interceptors/langchain.py,sha256=_8vXCjWkRKeTlxtXm33a67Gf4po9YiHS7faThMJLohc,2963
|
|
16
|
-
tollgate/interceptors/openai.py,sha256
|
|
17
|
-
tollgate-1.0.
|
|
18
|
-
tollgate-1.0.
|
|
19
|
-
tollgate-1.0.
|
|
20
|
-
tollgate-1.0.
|
|
17
|
+
tollgate/interceptors/openai.py,sha256=cHnOQ8keQJRYBm_1RpohKWk3D9Ask22hpuQghm9iahU,3337
|
|
18
|
+
tollgate-1.0.4.dist-info/METADATA,sha256=JnYhkyYWulY-WQ0cEuGfvsk6gQsDthGtvrTqCZIlAgk,7748
|
|
19
|
+
tollgate-1.0.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
20
|
+
tollgate-1.0.4.dist-info/licenses/LICENSE,sha256=EZ9SehMCkcatlggcoT7WV0tx-ku4OsAoQf9LmJZvG1g,10806
|
|
21
|
+
tollgate-1.0.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|