agent-firewall-security 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_firewall_security-1.0.0.dist-info/METADATA +170 -0
- agent_firewall_security-1.0.0.dist-info/RECORD +34 -0
- agent_firewall_security-1.0.0.dist-info/WHEEL +5 -0
- agent_firewall_security-1.0.0.dist-info/entry_points.txt +2 -0
- agent_firewall_security-1.0.0.dist-info/top_level.txt +1 -0
- firewall/__init__.py +80 -0
- firewall/adapters/__init__.py +30 -0
- firewall/adapters/anthropic.py +324 -0
- firewall/adapters/generic.py +237 -0
- firewall/adapters/normalize.py +108 -0
- firewall/adapters/openai.py +363 -0
- firewall/attenuation.py +194 -0
- firewall/authorization.py +236 -0
- firewall/capability.py +287 -0
- firewall/cli.py +359 -0
- firewall/delegation.py +252 -0
- firewall/engine.py +2017 -0
- firewall/evidence.py +259 -0
- firewall/explain.py +165 -0
- firewall/http.py +478 -0
- firewall/identity.py +508 -0
- firewall/key_management.py +577 -0
- firewall/key_store.py +755 -0
- firewall/lifecycle.py +244 -0
- firewall/lifecycle_store.py +599 -0
- firewall/mcp.py +245 -0
- firewall/namespace.py +190 -0
- firewall/protect.py +72 -0
- firewall/replay.py +160 -0
- firewall/revocation.py +378 -0
- firewall/revocation_store.py +288 -0
- firewall/sdk.py +1206 -0
- firewall/tools.py +177 -0
- firewall/transport.py +222 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-firewall-security
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Security and authorization layer for AI agents and automated tool use.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Shubhbhangoo/agent-firewall
|
|
6
|
+
Project-URL: Repository, https://github.com/Shubhbhangoo/agent-firewall
|
|
7
|
+
Project-URL: Issues, https://github.com/Shubhbhangoo/agent-firewall/issues
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: PyYAML>=6.0
|
|
11
|
+
Requires-Dist: mcp>=2.0
|
|
12
|
+
Requires-Dist: cryptography>=41.0
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=9.0; extra == "dev"
|
|
15
|
+
Requires-Dist: hypothesis>=6.0; extra == "dev"
|
|
16
|
+
|
|
17
|
+
Agent Firewall
|
|
18
|
+
|
|
19
|
+
Security and authorization infrastructure for AI agents and automated tool use.
|
|
20
|
+
|
|
21
|
+
Agent Firewall provides a capability-based security layer between agents and the actions they are allowed to perform.
|
|
22
|
+
|
|
23
|
+
## v1.0
|
|
24
|
+
|
|
25
|
+
Agent Firewall v1.0 is the first stable release.
|
|
26
|
+
|
|
27
|
+
### Core security
|
|
28
|
+
|
|
29
|
+
- Capability-based authorization
|
|
30
|
+
- Cryptographic capability verification
|
|
31
|
+
- Capability attenuation
|
|
32
|
+
- Capability delegation
|
|
33
|
+
- Capability revocation
|
|
34
|
+
- Replay protection
|
|
35
|
+
- Lifecycle recording
|
|
36
|
+
- Expiration enforcement
|
|
37
|
+
- Issuer trust management
|
|
38
|
+
- Signing-key rotation
|
|
39
|
+
- Key retirement
|
|
40
|
+
- Fail-closed authorization semantics
|
|
41
|
+
|
|
42
|
+
### Persistent security state
|
|
43
|
+
|
|
44
|
+
v1.0 supports persistent managed signing keys and issuer trust state.
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import os
|
|
48
|
+
|
|
49
|
+
from firewall.sdk import FirewallSDK
|
|
50
|
+
|
|
51
|
+
master_key = os.urandom(32)
|
|
52
|
+
|
|
53
|
+
sdk = FirewallSDK(
|
|
54
|
+
key_store_path="firewall-keys.db",
|
|
55
|
+
master_key=master_key,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
sdk.generate_key("key-1")
|
|
59
|
+
|
|
60
|
+
capability = sdk.issue(
|
|
61
|
+
agent="agent-a",
|
|
62
|
+
capability="payments.send",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
Private signing keys are encrypted at rest.
|
|
66
|
+
|
|
67
|
+
The master key is supplied by the application and is never stored by Agent Firewall.
|
|
68
|
+
|
|
69
|
+
Key rotation
|
|
70
|
+
sdk.rotate_key("key-2")
|
|
71
|
+
|
|
72
|
+
Rotation creates a new active signing key and retires the previous one.
|
|
73
|
+
|
|
74
|
+
Previously issued capabilities are not automatically revoked.
|
|
75
|
+
|
|
76
|
+
Issuer trust
|
|
77
|
+
sdk.trust_issuer("issuer-a")
|
|
78
|
+
sdk.revoke_issuer("issuer-a")
|
|
79
|
+
|
|
80
|
+
Issuer trust state can survive SDK restart when persistent key storage is enabled.
|
|
81
|
+
|
|
82
|
+
Legacy issuance
|
|
83
|
+
|
|
84
|
+
The existing direct-key API remains supported:
|
|
85
|
+
|
|
86
|
+
sdk.issue(
|
|
87
|
+
private_key=private_key,
|
|
88
|
+
agent="agent-a",
|
|
89
|
+
capability="payments.send",
|
|
90
|
+
)
|
|
91
|
+
CLI
|
|
92
|
+
firewall init
|
|
93
|
+
firewall validate
|
|
94
|
+
firewall inspect-token
|
|
95
|
+
firewall explain
|
|
96
|
+
Security testing
|
|
97
|
+
|
|
98
|
+
The project includes:
|
|
99
|
+
|
|
100
|
+
unit tests
|
|
101
|
+
integration tests
|
|
102
|
+
property-based tests
|
|
103
|
+
state-machine tests
|
|
104
|
+
persistence failure tests
|
|
105
|
+
adapter interoperability tests
|
|
106
|
+
benchmark coverage
|
|
107
|
+
|
|
108
|
+
The CI security matrix runs the complete test suite on Python 3.10, 3.11, and 3.12.
|
|
109
|
+
|
|
110
|
+
Installation
|
|
111
|
+
pip install agent-firewall
|
|
112
|
+
Documentation
|
|
113
|
+
|
|
114
|
+
See:
|
|
115
|
+
|
|
116
|
+
docs/v1.0-api-contract.md
|
|
117
|
+
docs/v1.0-security.md
|
|
118
|
+
docs/v1.0-key-management.md
|
|
119
|
+
|
|
120
|
+
### `CHANGELOG.md`
|
|
121
|
+
|
|
122
|
+
Create or update:
|
|
123
|
+
|
|
124
|
+
```md
|
|
125
|
+
# Changelog
|
|
126
|
+
|
|
127
|
+
All notable changes to Agent Firewall are documented here.
|
|
128
|
+
|
|
129
|
+
## [1.0.0] - 2026-08-23
|
|
130
|
+
|
|
131
|
+
### Added
|
|
132
|
+
|
|
133
|
+
- Stable v1.0 public API contract
|
|
134
|
+
- Managed capability signing keys
|
|
135
|
+
- Signing-key rotation
|
|
136
|
+
- Signing-key retirement
|
|
137
|
+
- Persistent encrypted signing-key storage
|
|
138
|
+
- Persistent issuer trust state
|
|
139
|
+
- Issuer trust and revocation management
|
|
140
|
+
- Persistent lifecycle and revocation support
|
|
141
|
+
- Generic tool adapter
|
|
142
|
+
- OpenAI tool adapter
|
|
143
|
+
- Anthropic tool adapter
|
|
144
|
+
- Capability normalization
|
|
145
|
+
- Capability explanation and denial reporting
|
|
146
|
+
- CLI commands:
|
|
147
|
+
- `firewall init`
|
|
148
|
+
- `firewall validate`
|
|
149
|
+
- `firewall inspect-token`
|
|
150
|
+
- `firewall explain`
|
|
151
|
+
- Property-based security tests
|
|
152
|
+
- Lifecycle state-machine security tests
|
|
153
|
+
- Persistence failure and corruption tests
|
|
154
|
+
- CI security regression matrix
|
|
155
|
+
- Python 3.10, 3.11, and 3.12 CI coverage
|
|
156
|
+
- v1.0 security and key-management documentation
|
|
157
|
+
|
|
158
|
+
### Security
|
|
159
|
+
|
|
160
|
+
- Authorization fails closed when critical security state cannot be verified.
|
|
161
|
+
- Persistent key-store failures do not silently fall back to cached or newly generated signing authority.
|
|
162
|
+
- Retiring or rotating a key does not implicitly grant or revoke capability authority.
|
|
163
|
+
- Revoked, expired, replayed, and denied capabilities cannot transition into successful use.
|
|
164
|
+
- Private signing-key material is encrypted at rest in persistent storage.
|
|
165
|
+
|
|
166
|
+
### Compatibility
|
|
167
|
+
|
|
168
|
+
- Existing direct `private_key=` capability issuance remains supported.
|
|
169
|
+
- v0.9 CLI commands remain available.
|
|
170
|
+
- Existing adapter security semantics continue to use the same authorization core.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
firewall/__init__.py,sha256=14LJj6z-Gtjt5nrtVX9CCnAkK3wZSkaMXXFMHHAqi7A,1462
|
|
2
|
+
firewall/attenuation.py,sha256=qH8BO76yV18yjwo9g-0_zrrWn9KczNIepQXgL0Dp61g,4580
|
|
3
|
+
firewall/authorization.py,sha256=qLIZOxLArYhzuI4cKuKoYs4nfyabCA5R5Zy9nwHURQU,5053
|
|
4
|
+
firewall/capability.py,sha256=fBwU7LXO_YvA6ArwuM8ikNFGwGkJqH9Quamx4U0z4l4,6808
|
|
5
|
+
firewall/cli.py,sha256=N_YTFrrMWFUsvPu7neu-1gkai-a2OvK2jpNS5HeFvPc,7250
|
|
6
|
+
firewall/delegation.py,sha256=eaglhsP4a-fq0uiJZj9bQ7T_7x4PhamybJZvu3s_Iv8,5747
|
|
7
|
+
firewall/engine.py,sha256=52mhEF1TbB4NU21eVWdSkegKKPDRJ4mXNeU0ddBb-1w,51396
|
|
8
|
+
firewall/evidence.py,sha256=S5IYz6HA0-3uIIsNADnw3xpXZL8id3c-yQl8I8d5fjA,5571
|
|
9
|
+
firewall/explain.py,sha256=iKjpmE0OjdPQ9gUMPFjxCEPBCKUFAGyo_QiStCNU5V4,3773
|
|
10
|
+
firewall/http.py,sha256=OHgZ5r4-Atn3DT5P4K98LF705YyJ9-tqQRsOJveEvSA,12097
|
|
11
|
+
firewall/identity.py,sha256=wEo5tqsgRqGZdjGSU3WgmLhk4VuqHykeUm2bTsO93M8,11566
|
|
12
|
+
firewall/key_management.py,sha256=DfG4ntrB2SRJ3LLMT-zp5nVaBbCefo4pzlFBHNlzHUY,14194
|
|
13
|
+
firewall/key_store.py,sha256=IuJhFdCsTTAyOSuOpY0XWfcfQEfo6y0ApQ8flZDxZRE,19743
|
|
14
|
+
firewall/lifecycle.py,sha256=6jVuaE5cNS7NR2t75kOEZb8D1RRwKd2ax9p-B9QZQ1g,6178
|
|
15
|
+
firewall/lifecycle_store.py,sha256=UtBwGV2nQj23w1ujSyvbHaOyTksaW7Ve4XuAQcFsf90,15721
|
|
16
|
+
firewall/mcp.py,sha256=a9iIhNAfJlbYFCQkeIV9ZUlUtkS9eLGvwEAZVkhz2rM,6123
|
|
17
|
+
firewall/namespace.py,sha256=FyvFy48bTrAj8uYyiwLsKSlzS7EphXPYCbONTLgZppQ,3898
|
|
18
|
+
firewall/protect.py,sha256=0a4Q3C_ImoXVWNTBBvQWqRcdMS8oJ4mk_pCcRbVzBzY,1594
|
|
19
|
+
firewall/replay.py,sha256=YwAJr1e9GmtTXG8UwK1nb8v8JwQJL2fD95sm8L985OY,3308
|
|
20
|
+
firewall/revocation.py,sha256=GVoC6XCsK1CNwv93JaY_Ccx2R4jOn7Jt7FynSDnXdjQ,9368
|
|
21
|
+
firewall/revocation_store.py,sha256=GlN4xpYa_fctadIgXJvIIHgQ_Q1Zy7-emAoiBA8l5L4,7366
|
|
22
|
+
firewall/sdk.py,sha256=dqRLg_L2ZEEO-nA3zA9ysDzkjxKSvIJSQBhQ49z0H3w,31781
|
|
23
|
+
firewall/tools.py,sha256=e6U8-QaSq1I2V4__ennRkm0Bn7JiWDfsILpxygcqNXw,3844
|
|
24
|
+
firewall/transport.py,sha256=_Rc-RXzHmUEuHhKtDh-e59qOZATdaZH1k5RnPijPsK8,4431
|
|
25
|
+
firewall/adapters/__init__.py,sha256=X-YTiKltfmNe1D059IayH0He17g78CWPXcqvu_4GDPg,485
|
|
26
|
+
firewall/adapters/anthropic.py,sha256=J1sVsaA7TlUAX6DiRlxWD44LzjsnUg3YKMIZ8gyyAX0,7612
|
|
27
|
+
firewall/adapters/generic.py,sha256=-Yk7IIQfDslFKGelYFGLp3gWMGu7lnMRGmPp0M9u4xk,5348
|
|
28
|
+
firewall/adapters/normalize.py,sha256=B6wPyrR_BfkIV84SlpeHvUhuRsLcipqwG89L-67C4lk,2392
|
|
29
|
+
firewall/adapters/openai.py,sha256=A_zjAul82qXwXQs0f2nMhTuIofRRMy-EVDThnanIbU0,8701
|
|
30
|
+
agent_firewall_security-1.0.0.dist-info/METADATA,sha256=rwGijSu8DtBumClmYJ_2Ts_ToneYVojQ8SUDeEARGn4,4494
|
|
31
|
+
agent_firewall_security-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
32
|
+
agent_firewall_security-1.0.0.dist-info/entry_points.txt,sha256=ATMBlCNaTTSfEWeCo_NhmiEfc0e6BSt-51c4fAUsCNk,47
|
|
33
|
+
agent_firewall_security-1.0.0.dist-info/top_level.txt,sha256=eCyrxLxHgVZSGULyFkPUP0MBc5YLd2aBS7nRRC-kUac,9
|
|
34
|
+
agent_firewall_security-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
firewall
|
firewall/__init__.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from .engine import (
|
|
2
|
+
Firewall,
|
|
3
|
+
Decision,
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
from .sdk import (
|
|
7
|
+
FirewallSDK,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from .capability import (
|
|
11
|
+
Capability,
|
|
12
|
+
CapabilityVerifier,
|
|
13
|
+
capability_fingerprint,
|
|
14
|
+
generate_capability_key_pair,
|
|
15
|
+
sign_capability,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from .delegation import (
|
|
19
|
+
Delegation,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from .revocation import (
|
|
23
|
+
RevocationRegistry,
|
|
24
|
+
RevocationRecord,
|
|
25
|
+
RevocationError,
|
|
26
|
+
AlreadyRevokedError,
|
|
27
|
+
InvalidFingerprintError,
|
|
28
|
+
RevokedCapabilityError,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
from .lifecycle import (
|
|
32
|
+
LifecycleEvent,
|
|
33
|
+
LifecycleEventType,
|
|
34
|
+
LifecycleRecorder,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
from .lifecycle_store import (
|
|
38
|
+
LifecycleStore,
|
|
39
|
+
LifecycleStoreError,
|
|
40
|
+
LifecycleStoreClosedError,
|
|
41
|
+
SQLiteLifecycleStore,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
# Engine
|
|
46
|
+
"Firewall",
|
|
47
|
+
"Decision",
|
|
48
|
+
|
|
49
|
+
# SDK
|
|
50
|
+
"FirewallSDK",
|
|
51
|
+
|
|
52
|
+
# Capabilities
|
|
53
|
+
"Capability",
|
|
54
|
+
"CapabilityVerifier",
|
|
55
|
+
"capability_fingerprint",
|
|
56
|
+
"generate_capability_key_pair",
|
|
57
|
+
"sign_capability",
|
|
58
|
+
|
|
59
|
+
# Delegation
|
|
60
|
+
"Delegation",
|
|
61
|
+
|
|
62
|
+
# Revocation
|
|
63
|
+
"RevocationRegistry",
|
|
64
|
+
"RevocationRecord",
|
|
65
|
+
"RevocationError",
|
|
66
|
+
"AlreadyRevokedError",
|
|
67
|
+
"InvalidFingerprintError",
|
|
68
|
+
"RevokedCapabilityError",
|
|
69
|
+
|
|
70
|
+
# Lifecycle
|
|
71
|
+
"LifecycleEvent",
|
|
72
|
+
"LifecycleEventType",
|
|
73
|
+
"LifecycleRecorder",
|
|
74
|
+
|
|
75
|
+
# Lifecycle persistence
|
|
76
|
+
"LifecycleStore",
|
|
77
|
+
"LifecycleStoreError",
|
|
78
|
+
"LifecycleStoreClosedError",
|
|
79
|
+
"SQLiteLifecycleStore",
|
|
80
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from .generic import (
|
|
2
|
+
GenericToolAdapter,
|
|
3
|
+
GenericToolCall,
|
|
4
|
+
generic_tool,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
from .openai import (
|
|
8
|
+
OpenAITool,
|
|
9
|
+
openai_tool,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from .anthropic import (
|
|
13
|
+
AnthropicTool,
|
|
14
|
+
anthropic_tool,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from .normalize import (
|
|
18
|
+
normalize_tool_call,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"GenericToolAdapter",
|
|
23
|
+
"GenericToolCall",
|
|
24
|
+
"generic_tool",
|
|
25
|
+
"OpenAITool",
|
|
26
|
+
"openai_tool",
|
|
27
|
+
"AnthropicTool",
|
|
28
|
+
"anthropic_tool",
|
|
29
|
+
"normalize_tool_call",
|
|
30
|
+
]
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Optional
|
|
4
|
+
|
|
5
|
+
from firewall.adapters.generic import (
|
|
6
|
+
GenericToolCall,
|
|
7
|
+
)
|
|
8
|
+
from firewall.capability import Capability
|
|
9
|
+
from firewall.sdk import FirewallSDK
|
|
10
|
+
from firewall.tools import ProtectedTool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AnthropicTool:
|
|
14
|
+
"""
|
|
15
|
+
Thin Anthropic-style tool adapter.
|
|
16
|
+
|
|
17
|
+
Expected tool-call shape:
|
|
18
|
+
|
|
19
|
+
{
|
|
20
|
+
"id": "toolu_123",
|
|
21
|
+
"name": "send_payment",
|
|
22
|
+
"input": {
|
|
23
|
+
"amount": 50
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
Authorization is delegated to FirewallSDK.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
*,
|
|
33
|
+
sdk: FirewallSDK,
|
|
34
|
+
capability: Capability,
|
|
35
|
+
handler: Callable[..., Any],
|
|
36
|
+
name: Optional[str] = None,
|
|
37
|
+
description: Optional[str] = None,
|
|
38
|
+
input_schema: Optional[dict] = None,
|
|
39
|
+
action: Optional[str] = None,
|
|
40
|
+
request_builder: Optional[
|
|
41
|
+
Callable[[dict[str, Any]], dict[str, Any]]
|
|
42
|
+
] = None,
|
|
43
|
+
):
|
|
44
|
+
if not isinstance(
|
|
45
|
+
sdk,
|
|
46
|
+
FirewallSDK,
|
|
47
|
+
):
|
|
48
|
+
raise TypeError(
|
|
49
|
+
"sdk must be a FirewallSDK"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if not isinstance(
|
|
53
|
+
capability,
|
|
54
|
+
Capability,
|
|
55
|
+
):
|
|
56
|
+
raise TypeError(
|
|
57
|
+
"capability must be a Capability"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if not callable(handler):
|
|
61
|
+
raise TypeError(
|
|
62
|
+
"handler must be callable"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if name is None:
|
|
66
|
+
name = getattr(
|
|
67
|
+
handler,
|
|
68
|
+
"__name__",
|
|
69
|
+
None,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if not isinstance(
|
|
73
|
+
name,
|
|
74
|
+
str,
|
|
75
|
+
):
|
|
76
|
+
raise TypeError(
|
|
77
|
+
"name must be a string"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
name = name.strip()
|
|
81
|
+
|
|
82
|
+
if not name:
|
|
83
|
+
raise ValueError(
|
|
84
|
+
"name cannot be empty"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if description is None:
|
|
88
|
+
description = (
|
|
89
|
+
getattr(
|
|
90
|
+
handler,
|
|
91
|
+
"__doc__",
|
|
92
|
+
None,
|
|
93
|
+
)
|
|
94
|
+
or ""
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if not isinstance(
|
|
98
|
+
description,
|
|
99
|
+
str,
|
|
100
|
+
):
|
|
101
|
+
raise TypeError(
|
|
102
|
+
"description must be a string"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if input_schema is None:
|
|
106
|
+
input_schema = {
|
|
107
|
+
"type": "object",
|
|
108
|
+
"properties": {},
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if not isinstance(
|
|
112
|
+
input_schema,
|
|
113
|
+
dict,
|
|
114
|
+
):
|
|
115
|
+
raise TypeError(
|
|
116
|
+
"input_schema must be a dictionary"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if (
|
|
120
|
+
action is not None
|
|
121
|
+
and not isinstance(
|
|
122
|
+
action,
|
|
123
|
+
str,
|
|
124
|
+
)
|
|
125
|
+
):
|
|
126
|
+
raise TypeError(
|
|
127
|
+
"action must be a string"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if (
|
|
131
|
+
action is not None
|
|
132
|
+
and not action.strip()
|
|
133
|
+
):
|
|
134
|
+
raise ValueError(
|
|
135
|
+
"action cannot be empty"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if (
|
|
139
|
+
request_builder is not None
|
|
140
|
+
and not callable(request_builder)
|
|
141
|
+
):
|
|
142
|
+
raise TypeError(
|
|
143
|
+
"request_builder must be callable"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
self.sdk = sdk
|
|
147
|
+
self.capability = capability
|
|
148
|
+
self.name = name
|
|
149
|
+
self.description = description
|
|
150
|
+
self.input_schema = dict(
|
|
151
|
+
input_schema
|
|
152
|
+
)
|
|
153
|
+
self.action = action
|
|
154
|
+
self.request_builder = (
|
|
155
|
+
request_builder
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
self.tool = ProtectedTool(
|
|
159
|
+
sdk=sdk,
|
|
160
|
+
capability=capability,
|
|
161
|
+
handler=handler,
|
|
162
|
+
action=action,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# ========================================================
|
|
166
|
+
# Anthropic tool definition
|
|
167
|
+
# ========================================================
|
|
168
|
+
|
|
169
|
+
def definition(self) -> dict:
|
|
170
|
+
return {
|
|
171
|
+
"name": self.name,
|
|
172
|
+
"description": self.description,
|
|
173
|
+
"input_schema": dict(
|
|
174
|
+
self.input_schema
|
|
175
|
+
),
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# ========================================================
|
|
179
|
+
# Normalize
|
|
180
|
+
# ========================================================
|
|
181
|
+
|
|
182
|
+
def normalize(
|
|
183
|
+
self,
|
|
184
|
+
call: dict,
|
|
185
|
+
) -> GenericToolCall:
|
|
186
|
+
if not isinstance(
|
|
187
|
+
call,
|
|
188
|
+
dict,
|
|
189
|
+
):
|
|
190
|
+
raise TypeError(
|
|
191
|
+
"call must be a dictionary"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
name = call.get(
|
|
195
|
+
"name"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if name != self.name:
|
|
199
|
+
raise ValueError(
|
|
200
|
+
"tool call name does not match adapter"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
arguments = call.get(
|
|
204
|
+
"input",
|
|
205
|
+
{},
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
if not isinstance(
|
|
209
|
+
arguments,
|
|
210
|
+
dict,
|
|
211
|
+
):
|
|
212
|
+
raise TypeError(
|
|
213
|
+
"tool call input must be a dictionary"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
return GenericToolCall(
|
|
217
|
+
name=name,
|
|
218
|
+
arguments=dict(
|
|
219
|
+
arguments
|
|
220
|
+
),
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# ========================================================
|
|
224
|
+
# Authorization
|
|
225
|
+
# ========================================================
|
|
226
|
+
|
|
227
|
+
def authorize(
|
|
228
|
+
self,
|
|
229
|
+
call: dict,
|
|
230
|
+
):
|
|
231
|
+
normalized = self.normalize(
|
|
232
|
+
call
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
arguments = normalized.arguments
|
|
236
|
+
|
|
237
|
+
if self.request_builder is not None:
|
|
238
|
+
request = self.request_builder(
|
|
239
|
+
arguments
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
if not isinstance(
|
|
243
|
+
request,
|
|
244
|
+
dict,
|
|
245
|
+
):
|
|
246
|
+
raise TypeError(
|
|
247
|
+
"request_builder must return a dictionary"
|
|
248
|
+
)
|
|
249
|
+
else:
|
|
250
|
+
request = {
|
|
251
|
+
"tool": normalized.name,
|
|
252
|
+
"input": dict(
|
|
253
|
+
arguments
|
|
254
|
+
),
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
action = (
|
|
258
|
+
self.action
|
|
259
|
+
or self.capability.capability
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
return self.sdk.authorize(
|
|
263
|
+
self.capability,
|
|
264
|
+
action,
|
|
265
|
+
request,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# ========================================================
|
|
269
|
+
# Execute
|
|
270
|
+
# ========================================================
|
|
271
|
+
|
|
272
|
+
def execute(
|
|
273
|
+
self,
|
|
274
|
+
call: dict,
|
|
275
|
+
):
|
|
276
|
+
normalized = self.normalize(
|
|
277
|
+
call
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
result = self.authorize(
|
|
281
|
+
call
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
if not result.allowed:
|
|
285
|
+
raise PermissionError(
|
|
286
|
+
result.reason
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
return self.tool.handler(
|
|
290
|
+
**normalized.arguments
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
def __call__(
|
|
294
|
+
self,
|
|
295
|
+
call: dict,
|
|
296
|
+
):
|
|
297
|
+
return self.execute(
|
|
298
|
+
call
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def anthropic_tool(
|
|
303
|
+
*,
|
|
304
|
+
sdk: FirewallSDK,
|
|
305
|
+
capability: Capability,
|
|
306
|
+
handler: Callable[..., Any],
|
|
307
|
+
name: Optional[str] = None,
|
|
308
|
+
description: Optional[str] = None,
|
|
309
|
+
input_schema: Optional[dict] = None,
|
|
310
|
+
action: Optional[str] = None,
|
|
311
|
+
request_builder: Optional[
|
|
312
|
+
Callable[[dict[str, Any]], dict[str, Any]]
|
|
313
|
+
] = None,
|
|
314
|
+
) -> AnthropicTool:
|
|
315
|
+
return AnthropicTool(
|
|
316
|
+
sdk=sdk,
|
|
317
|
+
capability=capability,
|
|
318
|
+
handler=handler,
|
|
319
|
+
name=name,
|
|
320
|
+
description=description,
|
|
321
|
+
input_schema=input_schema,
|
|
322
|
+
action=action,
|
|
323
|
+
request_builder=request_builder,
|
|
324
|
+
)
|