tollgate 1.0.2__tar.gz → 1.0.3__tar.gz
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-1.0.2 → tollgate-1.0.3}/.claude/settings.local.json +4 -1
- {tollgate-1.0.2 → tollgate-1.0.3}/COMPARISON.md +3 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/PKG-INFO +4 -1
- {tollgate-1.0.2 → tollgate-1.0.3}/QUICKSTART.md +3 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/README.md +3 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/pyproject.toml +1 -1
- {tollgate-1.0.2 → tollgate-1.0.3}/src/tollgate/__init__.py +1 -1
- {tollgate-1.0.2 → tollgate-1.0.3}/src/tollgate/approvals.py +39 -22
- {tollgate-1.0.2 → tollgate-1.0.3}/src/tollgate/interceptors/openai.py +19 -1
- {tollgate-1.0.2 → tollgate-1.0.3}/src/tollgate/policy.py +12 -2
- {tollgate-1.0.2 → tollgate-1.0.3}/src/tollgate/tower.py +35 -1
- {tollgate-1.0.2 → tollgate-1.0.3}/.gitignore +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/CHANGELOG.md +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/CONTRIBUTING.md +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/LICENSE +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/Makefile +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/SECURITY.md +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/examples/mcp_minimal/audit.jsonl +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/examples/mcp_minimal/demo.py +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/examples/mcp_minimal/manifest.yaml +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/examples/mcp_minimal/policy.yaml +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/examples/mock_tickets/README.md +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/examples/mock_tickets/agent.py +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/examples/mock_tickets/demo.py +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/examples/mock_tickets/manifest.yaml +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/examples/mock_tickets/tickets.json +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/examples/mock_tickets/tools.py +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/examples/strands_minimal/audit.jsonl +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/examples/strands_minimal/demo.py +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/examples/strands_minimal/manifest.yaml +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/examples/strands_minimal/policy.yaml +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/policies/default.yaml +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/specs/audit_event.schema.json +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/specs/decision.schema.json +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/specs/identity.schema.json +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/specs/intent.schema.json +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/specs/tool_request.schema.json +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/src/tollgate/audit.py +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/src/tollgate/exceptions.py +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/src/tollgate/helpers.py +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/src/tollgate/integrations/__init__.py +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/src/tollgate/integrations/mcp.py +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/src/tollgate/integrations/strands.py +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/src/tollgate/interceptors/__init__.py +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/src/tollgate/interceptors/base.py +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/src/tollgate/interceptors/langchain.py +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/src/tollgate/registry.py +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/src/tollgate/types.py +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/tests/test_adapters_v1.py +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/tests/test_audit_integrity_v1.py +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/tests/test_deferred_v1.py +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/tests/test_helpers_v1.py +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/tests/test_integrations_v1.py +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/tests/test_policy_v1.py +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/tests/test_registry_v1.py +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/tests/test_security_v1.py +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/tests/test_tower_v1.py +0 -0
- {tollgate-1.0.2 → tollgate-1.0.3}/tests/test_v1_integrations.py +0 -0
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
"Bash(.venv/bin/ruff format:*)",
|
|
11
11
|
"Bash(git init:*)",
|
|
12
12
|
"Bash(gh auth status:*)",
|
|
13
|
-
"Bash(.venv/bin/python:*)"
|
|
13
|
+
"Bash(.venv/bin/python:*)",
|
|
14
|
+
"Bash(python -m pytest tests/ -v)",
|
|
15
|
+
"Bash(python -m ruff:*)",
|
|
16
|
+
"Bash(python3 -m ruff check src/)"
|
|
14
17
|
]
|
|
15
18
|
}
|
|
16
19
|
}
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
This guide demonstrates the minimal changes required to integrate `tollgate` into your existing AI agent workflows.
|
|
4
4
|
|
|
5
|
+
> [!CAUTION]
|
|
6
|
+
> **Disclaimer**: This project is an exploratory implementation intended for learning and discussion. It is not production-hardened and comes with no guarantees.
|
|
7
|
+
|
|
5
8
|
---
|
|
6
9
|
|
|
7
10
|
## 🔹 Scenario 1: Plain Python Tools
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tollgate
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.3
|
|
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
|
```
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Get started with `tollgate` in minutes. This guide covers the basic setup and integration options for various AI frameworks.
|
|
4
4
|
|
|
5
|
+
> [!CAUTION]
|
|
6
|
+
> **Disclaimer**: This project is an exploratory implementation intended for learning and discussion. It is not production-hardened and comes with no guarantees.
|
|
7
|
+
|
|
5
8
|
---
|
|
6
9
|
|
|
7
10
|
## 0. Key Concepts
|
|
@@ -4,6 +4,9 @@ Runtime enforcement layer for AI agent tool calls using **Identity + Intent + Po
|
|
|
4
4
|
|
|
5
5
|
`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.
|
|
6
6
|
|
|
7
|
+
> [!CAUTION]
|
|
8
|
+
> **Disclaimer**: This project is an exploratory implementation intended for learning and discussion. It is not production-hardened and comes with no guarantees.
|
|
9
|
+
|
|
7
10
|
**[🚀 Quickstart Guide](https://github.com/ravi-labs/tollgate/blob/main/QUICKSTART.md) | [📊 Integration Comparison](https://github.com/ravi-labs/tollgate/blob/main/COMPARISON.md)**
|
|
8
11
|
|
|
9
12
|
```
|
|
@@ -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 ""
|
|
@@ -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():
|
|
@@ -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
|
|
|
@@ -127,7 +127,8 @@ class ControlTower:
|
|
|
127
127
|
result = await exec_async()
|
|
128
128
|
except Exception as e:
|
|
129
129
|
outcome = Outcome.FAILED
|
|
130
|
-
|
|
130
|
+
# Security: Sanitize exception message to avoid info disclosure
|
|
131
|
+
result_summary = self._sanitize_exception(e)
|
|
131
132
|
self._log(
|
|
132
133
|
correlation_id,
|
|
133
134
|
request_hash,
|
|
@@ -222,3 +223,36 @@ class ControlTower:
|
|
|
222
223
|
return None
|
|
223
224
|
s = str(result)
|
|
224
225
|
return s[:max_chars] + "..." if len(s) > max_chars else s
|
|
226
|
+
|
|
227
|
+
def _sanitize_exception(self, e: Exception, max_chars: int = 100) -> str:
|
|
228
|
+
"""
|
|
229
|
+
Sanitize exception message for audit logging.
|
|
230
|
+
|
|
231
|
+
Removes potentially sensitive information like file paths, stack traces,
|
|
232
|
+
and internal details while preserving the exception type.
|
|
233
|
+
"""
|
|
234
|
+
exc_type = type(e).__name__
|
|
235
|
+
|
|
236
|
+
# For common safe exceptions, include a truncated message
|
|
237
|
+
safe_exceptions = {
|
|
238
|
+
"ValueError",
|
|
239
|
+
"TypeError",
|
|
240
|
+
"KeyError",
|
|
241
|
+
"IndexError",
|
|
242
|
+
"AttributeError",
|
|
243
|
+
"RuntimeError",
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if exc_type in safe_exceptions:
|
|
247
|
+
msg = str(e)
|
|
248
|
+
# Remove file paths (Unix and Windows style)
|
|
249
|
+
import re
|
|
250
|
+
|
|
251
|
+
msg = re.sub(r"['\"]?(/[^\s'\"]+|[A-Za-z]:\\[^\s'\"]+)['\"]?", "[PATH]", msg)
|
|
252
|
+
# Truncate to avoid leaking too much info
|
|
253
|
+
if len(msg) > max_chars:
|
|
254
|
+
msg = msg[:max_chars] + "..."
|
|
255
|
+
return f"{exc_type}: {msg}"
|
|
256
|
+
|
|
257
|
+
# For unknown exceptions, only log the type
|
|
258
|
+
return f"{exc_type}: [details redacted]"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|