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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ firewall = firewall.cli:main
@@ -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
+ )