agsec 0.1.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.
- agsec-0.1.0/PKG-INFO +230 -0
- agsec-0.1.0/README.md +208 -0
- agsec-0.1.0/agsec/__init__.py +12 -0
- agsec-0.1.0/agsec/control.py +65 -0
- agsec-0.1.0/agsec/exceptions.py +6 -0
- agsec-0.1.0/agsec/policy.py +144 -0
- agsec-0.1.0/agsec/registry.py +26 -0
- agsec-0.1.0/agsec/types.py +26 -0
- agsec-0.1.0/agsec.egg-info/PKG-INFO +230 -0
- agsec-0.1.0/agsec.egg-info/SOURCES.txt +16 -0
- agsec-0.1.0/agsec.egg-info/dependency_links.txt +1 -0
- agsec-0.1.0/agsec.egg-info/requires.txt +1 -0
- agsec-0.1.0/agsec.egg-info/top_level.txt +2 -0
- agsec-0.1.0/pyproject.toml +45 -0
- agsec-0.1.0/setup.cfg +4 -0
- agsec-0.1.0/setup.py +36 -0
- agsec-0.1.0/tests/__init__.py +0 -0
- agsec-0.1.0/tests/test_control.py +190 -0
agsec-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agsec
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI Agent Action Firewall core SDK
|
|
5
|
+
Home-page: https://github.com/yourusername/agsec
|
|
6
|
+
Author: Riyandhiman
|
|
7
|
+
Author-email: Riyandhiman <noreply@example.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/yourusername/agsec
|
|
10
|
+
Project-URL: Repository, https://github.com/yourusername/agsec
|
|
11
|
+
Project-URL: Documentation, https://github.com/yourusername/agsec#readme
|
|
12
|
+
Keywords: agent,security,policy,sandbox
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Requires-Python: >=3.8
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: PyYAML>=6.0
|
|
19
|
+
Dynamic: author
|
|
20
|
+
Dynamic: home-page
|
|
21
|
+
Dynamic: requires-python
|
|
22
|
+
|
|
23
|
+
# agsec
|
|
24
|
+
|
|
25
|
+
[](https://pypi.org/project/agsec/)
|
|
26
|
+
[](https://www.python.org/downloads/)
|
|
27
|
+
|
|
28
|
+
AI Agent Action Firewall - A minimal, control layer for agent actions.
|
|
29
|
+
|
|
30
|
+
## Overview
|
|
31
|
+
|
|
32
|
+
`agsec` provides a simple yet powerful way to add safety controls to AI agents. It acts as a "firewall" between agents and real-world actions, allowing you to define policies that approve, block, or review actions before execution.
|
|
33
|
+
|
|
34
|
+
### Why agsec?
|
|
35
|
+
|
|
36
|
+
- **Agent-neutral**: Works with any agent framework (LangChain, custom, etc.)
|
|
37
|
+
- **Declarative policies**: Define rules in YAML or code
|
|
38
|
+
- **Extensible**: Plugin system for custom actions and policies
|
|
39
|
+
- **Production-ready**: Lightweight, fast, and secure
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
- ✅ **Action Registry**: Register and manage agent actions
|
|
44
|
+
- ✅ **Policy Engine**: Flexible rule-based decision making
|
|
45
|
+
- ✅ **YAML Policies**: Human-readable policy definitions
|
|
46
|
+
- ✅ **Context Awareness**: Rules can access parameters and context
|
|
47
|
+
- ✅ **Priority & Matching**: Advanced rule evaluation (priority, all/any matching)
|
|
48
|
+
- ✅ **Audit Logging**: Built-in logging for all decisions
|
|
49
|
+
- ✅ **Python Package**: Easy installation via PyPI
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
### Runtime (for users)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install agsec
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Development (for contributors)
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
git clone https://github.com/yourusername/agsec.git
|
|
63
|
+
cd agsec
|
|
64
|
+
pip install -e .[dev]
|
|
65
|
+
pre-commit install
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Quick Start
|
|
69
|
+
|
|
70
|
+
### Basic Usage
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from agsec import ControlLayer
|
|
74
|
+
|
|
75
|
+
# Create control layer
|
|
76
|
+
control = ControlLayer()
|
|
77
|
+
|
|
78
|
+
# Register an action
|
|
79
|
+
@control.register_action("send_email")
|
|
80
|
+
def send_email(to, subject, body):
|
|
81
|
+
return {"sent_to": to, "status": "success"}
|
|
82
|
+
|
|
83
|
+
# Execute with default allow policy
|
|
84
|
+
result = control.execute("send_email", {"to": "user@example.com", "subject": "Hello", "body": "Hi!"})
|
|
85
|
+
print(result.result) # {"sent_to": "user@example.com", "status": "success"}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### With YAML Policies
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from agsec import ControlLayer
|
|
92
|
+
|
|
93
|
+
policy_yaml = """
|
|
94
|
+
rules:
|
|
95
|
+
- action: payment
|
|
96
|
+
status: block
|
|
97
|
+
reason: "High-value payment blocked"
|
|
98
|
+
conditions:
|
|
99
|
+
amount:
|
|
100
|
+
op: ">"
|
|
101
|
+
value: 10000
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
control = ControlLayer(policy_yaml=policy_yaml)
|
|
105
|
+
|
|
106
|
+
@control.register_action("payment")
|
|
107
|
+
def payment(amount):
|
|
108
|
+
return {"charged": amount}
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
control.execute("payment", {"amount": 15000})
|
|
112
|
+
except Exception as e:
|
|
113
|
+
print(e) # PolicyViolationError: High-value payment blocked
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## API Reference
|
|
117
|
+
|
|
118
|
+
### ControlLayer
|
|
119
|
+
|
|
120
|
+
Main class for managing agent actions and policies.
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
ControlLayer(
|
|
124
|
+
policy_engine=None, # PolicyEngine instance
|
|
125
|
+
action_registry=None, # ActionRegistry instance
|
|
126
|
+
logger=None, # Custom logger
|
|
127
|
+
policy_yaml=None, # YAML policy string
|
|
128
|
+
policy_yaml_path=None # Path to YAML policy file
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
#### Methods
|
|
133
|
+
|
|
134
|
+
- `register_action(name)`: Decorator to register an action function
|
|
135
|
+
- `execute(action, params, context=None)`: Execute an action with policy check
|
|
136
|
+
|
|
137
|
+
### PolicyEngine
|
|
138
|
+
|
|
139
|
+
Handles policy evaluation.
|
|
140
|
+
|
|
141
|
+
#### Methods
|
|
142
|
+
|
|
143
|
+
- `add_rule(rule)`: Add a programmatic rule function
|
|
144
|
+
- `load_rules_from_yaml(yaml_text)`: Load rules from YAML string
|
|
145
|
+
- `load_rules_from_yaml_file(path)`: Load rules from YAML file
|
|
146
|
+
- `evaluate(action, params, context=None)`: Evaluate policy for action
|
|
147
|
+
|
|
148
|
+
### Policy Status
|
|
149
|
+
|
|
150
|
+
- `PolicyStatus.ALLOW`: Allow action execution
|
|
151
|
+
- `PolicyStatus.BLOCK`: Block action execution
|
|
152
|
+
- `PolicyStatus.REVIEW`: Mark for manual review
|
|
153
|
+
|
|
154
|
+
### YAML Policy Schema
|
|
155
|
+
|
|
156
|
+
```yaml
|
|
157
|
+
rules:
|
|
158
|
+
- action: "action_name" # Action to match (* for all)
|
|
159
|
+
status: "allow|block|review" # Decision
|
|
160
|
+
reason: "Optional reason" # Human-readable explanation
|
|
161
|
+
priority: 0 # Higher = evaluated first
|
|
162
|
+
match: "all|any" # Condition matching mode
|
|
163
|
+
conditions: # Parameter/context checks
|
|
164
|
+
param_name:
|
|
165
|
+
op: "==|!=|>|<|>=|<=|in|not_in"
|
|
166
|
+
value: "expected_value"
|
|
167
|
+
context.user_role:
|
|
168
|
+
op: "=="
|
|
169
|
+
value: "admin"
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Development
|
|
173
|
+
|
|
174
|
+
### Setup
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
pip install -e .[dev]
|
|
178
|
+
pre-commit install
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Testing
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
pytest
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Building
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
python -m build
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Releasing
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
./release.sh # Requires PYPI_API_TOKEN env var
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Contributing
|
|
200
|
+
|
|
201
|
+
1. Fork the repository
|
|
202
|
+
2. Create a feature branch
|
|
203
|
+
3. Make your changes
|
|
204
|
+
4. Add tests
|
|
205
|
+
5. Run `pre-commit run --all-files`
|
|
206
|
+
6. Submit a pull request
|
|
207
|
+
|
|
208
|
+
### Code Style
|
|
209
|
+
|
|
210
|
+
- Black for formatting
|
|
211
|
+
- isort for import sorting
|
|
212
|
+
- flake8 for linting
|
|
213
|
+
- pytest for testing
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
|
218
|
+
|
|
219
|
+
## Roadmap
|
|
220
|
+
|
|
221
|
+
- [ ] Web dashboard for policy management
|
|
222
|
+
- [ ] Advanced risk scoring
|
|
223
|
+
- [ ] Multi-agent coordination
|
|
224
|
+
- [ ] Enterprise integrations
|
|
225
|
+
|
|
226
|
+
## Support
|
|
227
|
+
|
|
228
|
+
- Issues: [GitHub Issues](https://github.com/riyandhiman14/agsec/issues)
|
|
229
|
+
- Discussions: [GitHub Discussions](https://github.com/riyandhiman14/agsec/discussions)
|
|
230
|
+
|
agsec-0.1.0/README.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# agsec
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/agsec/)
|
|
4
|
+
[](https://www.python.org/downloads/)
|
|
5
|
+
|
|
6
|
+
AI Agent Action Firewall - A minimal, control layer for agent actions.
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
`agsec` provides a simple yet powerful way to add safety controls to AI agents. It acts as a "firewall" between agents and real-world actions, allowing you to define policies that approve, block, or review actions before execution.
|
|
11
|
+
|
|
12
|
+
### Why agsec?
|
|
13
|
+
|
|
14
|
+
- **Agent-neutral**: Works with any agent framework (LangChain, custom, etc.)
|
|
15
|
+
- **Declarative policies**: Define rules in YAML or code
|
|
16
|
+
- **Extensible**: Plugin system for custom actions and policies
|
|
17
|
+
- **Production-ready**: Lightweight, fast, and secure
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- ✅ **Action Registry**: Register and manage agent actions
|
|
22
|
+
- ✅ **Policy Engine**: Flexible rule-based decision making
|
|
23
|
+
- ✅ **YAML Policies**: Human-readable policy definitions
|
|
24
|
+
- ✅ **Context Awareness**: Rules can access parameters and context
|
|
25
|
+
- ✅ **Priority & Matching**: Advanced rule evaluation (priority, all/any matching)
|
|
26
|
+
- ✅ **Audit Logging**: Built-in logging for all decisions
|
|
27
|
+
- ✅ **Python Package**: Easy installation via PyPI
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
### Runtime (for users)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install agsec
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Development (for contributors)
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
git clone https://github.com/yourusername/agsec.git
|
|
41
|
+
cd agsec
|
|
42
|
+
pip install -e .[dev]
|
|
43
|
+
pre-commit install
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
### Basic Usage
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from agsec import ControlLayer
|
|
52
|
+
|
|
53
|
+
# Create control layer
|
|
54
|
+
control = ControlLayer()
|
|
55
|
+
|
|
56
|
+
# Register an action
|
|
57
|
+
@control.register_action("send_email")
|
|
58
|
+
def send_email(to, subject, body):
|
|
59
|
+
return {"sent_to": to, "status": "success"}
|
|
60
|
+
|
|
61
|
+
# Execute with default allow policy
|
|
62
|
+
result = control.execute("send_email", {"to": "user@example.com", "subject": "Hello", "body": "Hi!"})
|
|
63
|
+
print(result.result) # {"sent_to": "user@example.com", "status": "success"}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### With YAML Policies
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from agsec import ControlLayer
|
|
70
|
+
|
|
71
|
+
policy_yaml = """
|
|
72
|
+
rules:
|
|
73
|
+
- action: payment
|
|
74
|
+
status: block
|
|
75
|
+
reason: "High-value payment blocked"
|
|
76
|
+
conditions:
|
|
77
|
+
amount:
|
|
78
|
+
op: ">"
|
|
79
|
+
value: 10000
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
control = ControlLayer(policy_yaml=policy_yaml)
|
|
83
|
+
|
|
84
|
+
@control.register_action("payment")
|
|
85
|
+
def payment(amount):
|
|
86
|
+
return {"charged": amount}
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
control.execute("payment", {"amount": 15000})
|
|
90
|
+
except Exception as e:
|
|
91
|
+
print(e) # PolicyViolationError: High-value payment blocked
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## API Reference
|
|
95
|
+
|
|
96
|
+
### ControlLayer
|
|
97
|
+
|
|
98
|
+
Main class for managing agent actions and policies.
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
ControlLayer(
|
|
102
|
+
policy_engine=None, # PolicyEngine instance
|
|
103
|
+
action_registry=None, # ActionRegistry instance
|
|
104
|
+
logger=None, # Custom logger
|
|
105
|
+
policy_yaml=None, # YAML policy string
|
|
106
|
+
policy_yaml_path=None # Path to YAML policy file
|
|
107
|
+
)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
#### Methods
|
|
111
|
+
|
|
112
|
+
- `register_action(name)`: Decorator to register an action function
|
|
113
|
+
- `execute(action, params, context=None)`: Execute an action with policy check
|
|
114
|
+
|
|
115
|
+
### PolicyEngine
|
|
116
|
+
|
|
117
|
+
Handles policy evaluation.
|
|
118
|
+
|
|
119
|
+
#### Methods
|
|
120
|
+
|
|
121
|
+
- `add_rule(rule)`: Add a programmatic rule function
|
|
122
|
+
- `load_rules_from_yaml(yaml_text)`: Load rules from YAML string
|
|
123
|
+
- `load_rules_from_yaml_file(path)`: Load rules from YAML file
|
|
124
|
+
- `evaluate(action, params, context=None)`: Evaluate policy for action
|
|
125
|
+
|
|
126
|
+
### Policy Status
|
|
127
|
+
|
|
128
|
+
- `PolicyStatus.ALLOW`: Allow action execution
|
|
129
|
+
- `PolicyStatus.BLOCK`: Block action execution
|
|
130
|
+
- `PolicyStatus.REVIEW`: Mark for manual review
|
|
131
|
+
|
|
132
|
+
### YAML Policy Schema
|
|
133
|
+
|
|
134
|
+
```yaml
|
|
135
|
+
rules:
|
|
136
|
+
- action: "action_name" # Action to match (* for all)
|
|
137
|
+
status: "allow|block|review" # Decision
|
|
138
|
+
reason: "Optional reason" # Human-readable explanation
|
|
139
|
+
priority: 0 # Higher = evaluated first
|
|
140
|
+
match: "all|any" # Condition matching mode
|
|
141
|
+
conditions: # Parameter/context checks
|
|
142
|
+
param_name:
|
|
143
|
+
op: "==|!=|>|<|>=|<=|in|not_in"
|
|
144
|
+
value: "expected_value"
|
|
145
|
+
context.user_role:
|
|
146
|
+
op: "=="
|
|
147
|
+
value: "admin"
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Development
|
|
151
|
+
|
|
152
|
+
### Setup
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
pip install -e .[dev]
|
|
156
|
+
pre-commit install
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Testing
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
pytest
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Building
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
python -m build
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Releasing
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
./release.sh # Requires PYPI_API_TOKEN env var
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Contributing
|
|
178
|
+
|
|
179
|
+
1. Fork the repository
|
|
180
|
+
2. Create a feature branch
|
|
181
|
+
3. Make your changes
|
|
182
|
+
4. Add tests
|
|
183
|
+
5. Run `pre-commit run --all-files`
|
|
184
|
+
6. Submit a pull request
|
|
185
|
+
|
|
186
|
+
### Code Style
|
|
187
|
+
|
|
188
|
+
- Black for formatting
|
|
189
|
+
- isort for import sorting
|
|
190
|
+
- flake8 for linting
|
|
191
|
+
- pytest for testing
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
|
196
|
+
|
|
197
|
+
## Roadmap
|
|
198
|
+
|
|
199
|
+
- [ ] Web dashboard for policy management
|
|
200
|
+
- [ ] Advanced risk scoring
|
|
201
|
+
- [ ] Multi-agent coordination
|
|
202
|
+
- [ ] Enterprise integrations
|
|
203
|
+
|
|
204
|
+
## Support
|
|
205
|
+
|
|
206
|
+
- Issues: [GitHub Issues](https://github.com/riyandhiman14/agsec/issues)
|
|
207
|
+
- Discussions: [GitHub Discussions](https://github.com/riyandhiman14/agsec/discussions)
|
|
208
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .control import ControlLayer
|
|
2
|
+
from .policy import PolicyEngine
|
|
3
|
+
from .registry import ActionRegistry
|
|
4
|
+
from .types import PolicyResult, PolicyStatus
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"ControlLayer",
|
|
8
|
+
"PolicyEngine",
|
|
9
|
+
"PolicyResult",
|
|
10
|
+
"PolicyStatus",
|
|
11
|
+
"ActionRegistry",
|
|
12
|
+
]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
from .exceptions import ActionNotFoundError, PolicyViolationError
|
|
7
|
+
from .policy import PolicyEngine
|
|
8
|
+
from .registry import ActionRegistry
|
|
9
|
+
from .types import ActionExecutionResult, PolicyResult, PolicyStatus
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ControlLayer:
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
policy_engine: Optional[PolicyEngine] = None,
|
|
16
|
+
action_registry: Optional[ActionRegistry] = None,
|
|
17
|
+
logger: Optional[logging.Logger] = None,
|
|
18
|
+
policy_yaml: Optional[str] = None,
|
|
19
|
+
policy_yaml_path: Optional[str] = None,
|
|
20
|
+
):
|
|
21
|
+
self.policy_engine = policy_engine or PolicyEngine()
|
|
22
|
+
if policy_yaml is not None:
|
|
23
|
+
self.policy_engine.load_rules_from_yaml(policy_yaml)
|
|
24
|
+
elif policy_yaml_path is not None:
|
|
25
|
+
self.policy_engine.load_rules_from_yaml_file(policy_yaml_path)
|
|
26
|
+
|
|
27
|
+
self.action_registry = action_registry or ActionRegistry()
|
|
28
|
+
self.logger = logger or logging.getLogger("agsec")
|
|
29
|
+
self.logger.setLevel(logging.DEBUG)
|
|
30
|
+
|
|
31
|
+
def register_action(self, name: str):
|
|
32
|
+
def decorator(func):
|
|
33
|
+
self.action_registry.register(name, func)
|
|
34
|
+
return func
|
|
35
|
+
|
|
36
|
+
return decorator
|
|
37
|
+
|
|
38
|
+
def execute(self, action: str, params: Dict[str, Any], context: Optional[Dict[str, Any]] = None) -> ActionExecutionResult:
|
|
39
|
+
context = context or {}
|
|
40
|
+
|
|
41
|
+
policy: PolicyResult = self.policy_engine.evaluate(action, params, context)
|
|
42
|
+
self.logger.info(f"policy evaluation for action=%s -> %s", action, policy)
|
|
43
|
+
|
|
44
|
+
if policy.status == PolicyStatus.BLOCK:
|
|
45
|
+
self.logger.warning("Blocked action: %s, reason=%s", action, policy.reason)
|
|
46
|
+
raise PolicyViolationError(policy.reason)
|
|
47
|
+
|
|
48
|
+
if policy.status == PolicyStatus.REVIEW:
|
|
49
|
+
self.logger.info("Action requires manual review: %s, reason=%s", action, policy.reason)
|
|
50
|
+
return ActionExecutionResult(action=action, params=params, result=None, policy=policy)
|
|
51
|
+
|
|
52
|
+
if policy.status != PolicyStatus.ALLOW:
|
|
53
|
+
raise PolicyViolationError(f"Unexpected policy status: {policy.status}")
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
act = self.action_registry.get(action)
|
|
57
|
+
except ActionNotFoundError as exc:
|
|
58
|
+
self.logger.error("Action not found: %s", action)
|
|
59
|
+
raise
|
|
60
|
+
|
|
61
|
+
result = act(**params)
|
|
62
|
+
exec_result = ActionExecutionResult(action=action, params=params, result=result, policy=policy)
|
|
63
|
+
|
|
64
|
+
self.logger.info("Executed action: %s, result=%s", action, result)
|
|
65
|
+
return exec_result
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from .exceptions import PolicyViolationError
|
|
9
|
+
from .types import PolicyResult, PolicyStatus
|
|
10
|
+
|
|
11
|
+
PolicyRule = Callable[[str, Dict[str, Any], Optional[Dict[str, Any]]], Optional[PolicyResult]]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _evaluate_condition(value: Any, condition: Any) -> bool:
|
|
15
|
+
if isinstance(condition, dict):
|
|
16
|
+
op = condition.get("op")
|
|
17
|
+
expected = condition.get("value")
|
|
18
|
+
|
|
19
|
+
if op is None or expected is None:
|
|
20
|
+
raise ValueError("Condition must have 'op' and 'value'")
|
|
21
|
+
|
|
22
|
+
if op == "==":
|
|
23
|
+
return value == expected
|
|
24
|
+
if op == "!=":
|
|
25
|
+
return value != expected
|
|
26
|
+
if op == ">":
|
|
27
|
+
return value > expected
|
|
28
|
+
if op == "<":
|
|
29
|
+
return value < expected
|
|
30
|
+
if op == ">=":
|
|
31
|
+
return value >= expected
|
|
32
|
+
if op == "<=":
|
|
33
|
+
return value <= expected
|
|
34
|
+
if op == "in":
|
|
35
|
+
return value in expected
|
|
36
|
+
if op == "not_in":
|
|
37
|
+
return value not in expected
|
|
38
|
+
raise ValueError(f"Unsupported condition operator: {op}")
|
|
39
|
+
|
|
40
|
+
# Scalar condition is treated as equals.
|
|
41
|
+
return value == condition
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _resolve_condition_value(key: str, params: Dict[str, Any], context: Optional[Dict[str, Any]]) -> Any:
|
|
45
|
+
if key.startswith("params."):
|
|
46
|
+
return params.get(key.split(".", 1)[1])
|
|
47
|
+
if key.startswith("context."):
|
|
48
|
+
if context is None:
|
|
49
|
+
return None
|
|
50
|
+
return context.get(key.split(".", 1)[1])
|
|
51
|
+
|
|
52
|
+
if key in params:
|
|
53
|
+
return params[key]
|
|
54
|
+
if context and key in context:
|
|
55
|
+
return context[key]
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _build_rule_from_definition(definition: Dict[str, Any]) -> PolicyRule:
|
|
60
|
+
action_name = definition.get("action")
|
|
61
|
+
status = definition.get("status")
|
|
62
|
+
reason = definition.get("reason", "")
|
|
63
|
+
conditions = definition.get("conditions", {})
|
|
64
|
+
match_type = definition.get("match", "all")
|
|
65
|
+
priority = int(definition.get("priority", 0))
|
|
66
|
+
|
|
67
|
+
if action_name is None or status is None:
|
|
68
|
+
raise ValueError("Each policy rule must include 'action' and 'status'")
|
|
69
|
+
|
|
70
|
+
status_enum = PolicyStatus(status)
|
|
71
|
+
|
|
72
|
+
def rule(action: str, params: Dict[str, Any], context: Optional[Dict[str, Any]] = None) -> Optional[PolicyResult]:
|
|
73
|
+
if action_name != "*" and action != action_name:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
if conditions:
|
|
77
|
+
hits = []
|
|
78
|
+
for key, cond in conditions.items():
|
|
79
|
+
value = _resolve_condition_value(key, params, context)
|
|
80
|
+
if value is None:
|
|
81
|
+
hits.append(False)
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
result = _evaluate_condition(value, cond)
|
|
85
|
+
hits.append(bool(result))
|
|
86
|
+
|
|
87
|
+
if match_type == "all" and not all(hits):
|
|
88
|
+
return None
|
|
89
|
+
if match_type == "any" and not any(hits):
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
return PolicyResult(status=status_enum, reason=reason)
|
|
93
|
+
|
|
94
|
+
setattr(rule, "priority", priority)
|
|
95
|
+
return rule
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class PolicyEngine:
|
|
99
|
+
def __init__(self, rules: Optional[List[PolicyRule]] = None):
|
|
100
|
+
self.rules = rules or []
|
|
101
|
+
self._sort_rules()
|
|
102
|
+
|
|
103
|
+
def add_rule(self, rule: PolicyRule) -> None:
|
|
104
|
+
if not hasattr(rule, "priority"):
|
|
105
|
+
setattr(rule, "priority", 0)
|
|
106
|
+
self.rules.append(rule)
|
|
107
|
+
self._sort_rules()
|
|
108
|
+
|
|
109
|
+
def _sort_rules(self) -> None:
|
|
110
|
+
self.rules.sort(key=lambda r: getattr(r, "priority", 0), reverse=True)
|
|
111
|
+
|
|
112
|
+
def load_rules_from_yaml(self, yaml_text: str) -> None:
|
|
113
|
+
parsed = yaml.safe_load(yaml_text)
|
|
114
|
+
if not isinstance(parsed, dict):
|
|
115
|
+
raise ValueError("YAML policy must contain a top-level mapping with 'rules'")
|
|
116
|
+
|
|
117
|
+
rules = parsed.get("rules")
|
|
118
|
+
if not isinstance(rules, list):
|
|
119
|
+
raise ValueError("'rules' must be a list")
|
|
120
|
+
|
|
121
|
+
for rule_def in rules:
|
|
122
|
+
if not isinstance(rule_def, dict):
|
|
123
|
+
raise ValueError("Each rule must be a mapping")
|
|
124
|
+
self.add_rule(_build_rule_from_definition(rule_def))
|
|
125
|
+
|
|
126
|
+
def load_rules_from_yaml_file(self, path: str) -> None:
|
|
127
|
+
if not os.path.exists(path):
|
|
128
|
+
raise FileNotFoundError(f"YAML policy file not found: {path}")
|
|
129
|
+
|
|
130
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
131
|
+
text = f.read()
|
|
132
|
+
self.load_rules_from_yaml(text)
|
|
133
|
+
|
|
134
|
+
def evaluate(self, action: str, params: Dict[str, Any], context: Optional[Dict[str, Any]] = None) -> PolicyResult:
|
|
135
|
+
context = context or {}
|
|
136
|
+
|
|
137
|
+
for rule in self.rules:
|
|
138
|
+
decision = rule(action, params, context)
|
|
139
|
+
if decision is not None:
|
|
140
|
+
if not isinstance(decision, PolicyResult):
|
|
141
|
+
raise PolicyViolationError(f"Policy rule returned invalid type: {type(decision)}")
|
|
142
|
+
return decision
|
|
143
|
+
|
|
144
|
+
return PolicyResult(status=PolicyStatus.ALLOW, reason="default allow")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Dict
|
|
4
|
+
|
|
5
|
+
from .exceptions import ActionNotFoundError
|
|
6
|
+
|
|
7
|
+
ActionFunc = Callable[..., Any]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ActionRegistry:
|
|
11
|
+
def __init__(self):
|
|
12
|
+
self._actions: Dict[str, ActionFunc] = {}
|
|
13
|
+
|
|
14
|
+
def register(self, name: str, func: ActionFunc) -> ActionFunc:
|
|
15
|
+
if name in self._actions:
|
|
16
|
+
raise ValueError(f"Action '{name}' is already registered")
|
|
17
|
+
self._actions[name] = func
|
|
18
|
+
return func
|
|
19
|
+
|
|
20
|
+
def get(self, name: str) -> ActionFunc:
|
|
21
|
+
if name not in self._actions:
|
|
22
|
+
raise ActionNotFoundError(f"Action '{name}' not registered")
|
|
23
|
+
return self._actions[name]
|
|
24
|
+
|
|
25
|
+
def list_actions(self) -> Dict[str, ActionFunc]:
|
|
26
|
+
return dict(self._actions)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PolicyStatus(str, Enum):
|
|
9
|
+
ALLOW = "allow"
|
|
10
|
+
BLOCK = "block"
|
|
11
|
+
REVIEW = "review"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class PolicyResult:
|
|
16
|
+
status: PolicyStatus
|
|
17
|
+
reason: str = ""
|
|
18
|
+
metadata: Dict[str, Any] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ActionExecutionResult:
|
|
23
|
+
action: str
|
|
24
|
+
params: Dict[str, Any]
|
|
25
|
+
result: Any
|
|
26
|
+
policy: PolicyResult
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agsec
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI Agent Action Firewall core SDK
|
|
5
|
+
Home-page: https://github.com/yourusername/agsec
|
|
6
|
+
Author: Riyandhiman
|
|
7
|
+
Author-email: Riyandhiman <noreply@example.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/yourusername/agsec
|
|
10
|
+
Project-URL: Repository, https://github.com/yourusername/agsec
|
|
11
|
+
Project-URL: Documentation, https://github.com/yourusername/agsec#readme
|
|
12
|
+
Keywords: agent,security,policy,sandbox
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Requires-Python: >=3.8
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: PyYAML>=6.0
|
|
19
|
+
Dynamic: author
|
|
20
|
+
Dynamic: home-page
|
|
21
|
+
Dynamic: requires-python
|
|
22
|
+
|
|
23
|
+
# agsec
|
|
24
|
+
|
|
25
|
+
[](https://pypi.org/project/agsec/)
|
|
26
|
+
[](https://www.python.org/downloads/)
|
|
27
|
+
|
|
28
|
+
AI Agent Action Firewall - A minimal, control layer for agent actions.
|
|
29
|
+
|
|
30
|
+
## Overview
|
|
31
|
+
|
|
32
|
+
`agsec` provides a simple yet powerful way to add safety controls to AI agents. It acts as a "firewall" between agents and real-world actions, allowing you to define policies that approve, block, or review actions before execution.
|
|
33
|
+
|
|
34
|
+
### Why agsec?
|
|
35
|
+
|
|
36
|
+
- **Agent-neutral**: Works with any agent framework (LangChain, custom, etc.)
|
|
37
|
+
- **Declarative policies**: Define rules in YAML or code
|
|
38
|
+
- **Extensible**: Plugin system for custom actions and policies
|
|
39
|
+
- **Production-ready**: Lightweight, fast, and secure
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
- ✅ **Action Registry**: Register and manage agent actions
|
|
44
|
+
- ✅ **Policy Engine**: Flexible rule-based decision making
|
|
45
|
+
- ✅ **YAML Policies**: Human-readable policy definitions
|
|
46
|
+
- ✅ **Context Awareness**: Rules can access parameters and context
|
|
47
|
+
- ✅ **Priority & Matching**: Advanced rule evaluation (priority, all/any matching)
|
|
48
|
+
- ✅ **Audit Logging**: Built-in logging for all decisions
|
|
49
|
+
- ✅ **Python Package**: Easy installation via PyPI
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
### Runtime (for users)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install agsec
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Development (for contributors)
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
git clone https://github.com/yourusername/agsec.git
|
|
63
|
+
cd agsec
|
|
64
|
+
pip install -e .[dev]
|
|
65
|
+
pre-commit install
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Quick Start
|
|
69
|
+
|
|
70
|
+
### Basic Usage
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from agsec import ControlLayer
|
|
74
|
+
|
|
75
|
+
# Create control layer
|
|
76
|
+
control = ControlLayer()
|
|
77
|
+
|
|
78
|
+
# Register an action
|
|
79
|
+
@control.register_action("send_email")
|
|
80
|
+
def send_email(to, subject, body):
|
|
81
|
+
return {"sent_to": to, "status": "success"}
|
|
82
|
+
|
|
83
|
+
# Execute with default allow policy
|
|
84
|
+
result = control.execute("send_email", {"to": "user@example.com", "subject": "Hello", "body": "Hi!"})
|
|
85
|
+
print(result.result) # {"sent_to": "user@example.com", "status": "success"}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### With YAML Policies
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from agsec import ControlLayer
|
|
92
|
+
|
|
93
|
+
policy_yaml = """
|
|
94
|
+
rules:
|
|
95
|
+
- action: payment
|
|
96
|
+
status: block
|
|
97
|
+
reason: "High-value payment blocked"
|
|
98
|
+
conditions:
|
|
99
|
+
amount:
|
|
100
|
+
op: ">"
|
|
101
|
+
value: 10000
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
control = ControlLayer(policy_yaml=policy_yaml)
|
|
105
|
+
|
|
106
|
+
@control.register_action("payment")
|
|
107
|
+
def payment(amount):
|
|
108
|
+
return {"charged": amount}
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
control.execute("payment", {"amount": 15000})
|
|
112
|
+
except Exception as e:
|
|
113
|
+
print(e) # PolicyViolationError: High-value payment blocked
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## API Reference
|
|
117
|
+
|
|
118
|
+
### ControlLayer
|
|
119
|
+
|
|
120
|
+
Main class for managing agent actions and policies.
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
ControlLayer(
|
|
124
|
+
policy_engine=None, # PolicyEngine instance
|
|
125
|
+
action_registry=None, # ActionRegistry instance
|
|
126
|
+
logger=None, # Custom logger
|
|
127
|
+
policy_yaml=None, # YAML policy string
|
|
128
|
+
policy_yaml_path=None # Path to YAML policy file
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
#### Methods
|
|
133
|
+
|
|
134
|
+
- `register_action(name)`: Decorator to register an action function
|
|
135
|
+
- `execute(action, params, context=None)`: Execute an action with policy check
|
|
136
|
+
|
|
137
|
+
### PolicyEngine
|
|
138
|
+
|
|
139
|
+
Handles policy evaluation.
|
|
140
|
+
|
|
141
|
+
#### Methods
|
|
142
|
+
|
|
143
|
+
- `add_rule(rule)`: Add a programmatic rule function
|
|
144
|
+
- `load_rules_from_yaml(yaml_text)`: Load rules from YAML string
|
|
145
|
+
- `load_rules_from_yaml_file(path)`: Load rules from YAML file
|
|
146
|
+
- `evaluate(action, params, context=None)`: Evaluate policy for action
|
|
147
|
+
|
|
148
|
+
### Policy Status
|
|
149
|
+
|
|
150
|
+
- `PolicyStatus.ALLOW`: Allow action execution
|
|
151
|
+
- `PolicyStatus.BLOCK`: Block action execution
|
|
152
|
+
- `PolicyStatus.REVIEW`: Mark for manual review
|
|
153
|
+
|
|
154
|
+
### YAML Policy Schema
|
|
155
|
+
|
|
156
|
+
```yaml
|
|
157
|
+
rules:
|
|
158
|
+
- action: "action_name" # Action to match (* for all)
|
|
159
|
+
status: "allow|block|review" # Decision
|
|
160
|
+
reason: "Optional reason" # Human-readable explanation
|
|
161
|
+
priority: 0 # Higher = evaluated first
|
|
162
|
+
match: "all|any" # Condition matching mode
|
|
163
|
+
conditions: # Parameter/context checks
|
|
164
|
+
param_name:
|
|
165
|
+
op: "==|!=|>|<|>=|<=|in|not_in"
|
|
166
|
+
value: "expected_value"
|
|
167
|
+
context.user_role:
|
|
168
|
+
op: "=="
|
|
169
|
+
value: "admin"
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Development
|
|
173
|
+
|
|
174
|
+
### Setup
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
pip install -e .[dev]
|
|
178
|
+
pre-commit install
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Testing
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
pytest
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Building
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
python -m build
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Releasing
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
./release.sh # Requires PYPI_API_TOKEN env var
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Contributing
|
|
200
|
+
|
|
201
|
+
1. Fork the repository
|
|
202
|
+
2. Create a feature branch
|
|
203
|
+
3. Make your changes
|
|
204
|
+
4. Add tests
|
|
205
|
+
5. Run `pre-commit run --all-files`
|
|
206
|
+
6. Submit a pull request
|
|
207
|
+
|
|
208
|
+
### Code Style
|
|
209
|
+
|
|
210
|
+
- Black for formatting
|
|
211
|
+
- isort for import sorting
|
|
212
|
+
- flake8 for linting
|
|
213
|
+
- pytest for testing
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
|
218
|
+
|
|
219
|
+
## Roadmap
|
|
220
|
+
|
|
221
|
+
- [ ] Web dashboard for policy management
|
|
222
|
+
- [ ] Advanced risk scoring
|
|
223
|
+
- [ ] Multi-agent coordination
|
|
224
|
+
- [ ] Enterprise integrations
|
|
225
|
+
|
|
226
|
+
## Support
|
|
227
|
+
|
|
228
|
+
- Issues: [GitHub Issues](https://github.com/riyandhiman14/agsec/issues)
|
|
229
|
+
- Discussions: [GitHub Discussions](https://github.com/riyandhiman14/agsec/discussions)
|
|
230
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
agsec/__init__.py
|
|
5
|
+
agsec/control.py
|
|
6
|
+
agsec/exceptions.py
|
|
7
|
+
agsec/policy.py
|
|
8
|
+
agsec/registry.py
|
|
9
|
+
agsec/types.py
|
|
10
|
+
agsec.egg-info/PKG-INFO
|
|
11
|
+
agsec.egg-info/SOURCES.txt
|
|
12
|
+
agsec.egg-info/dependency_links.txt
|
|
13
|
+
agsec.egg-info/requires.txt
|
|
14
|
+
agsec.egg-info/top_level.txt
|
|
15
|
+
tests/__init__.py
|
|
16
|
+
tests/test_control.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
PyYAML>=6.0
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agsec"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "AI Agent Action Firewall core SDK"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
keywords = ["agent", "security", "policy", "sandbox"]
|
|
13
|
+
authors = [
|
|
14
|
+
{name = "Riyandhiman", email = "noreply@example.com"}
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"PyYAML>=6.0"
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
"Homepage" = "https://github.com/yourusername/agsec"
|
|
27
|
+
"Repository" = "https://github.com/yourusername/agsec"
|
|
28
|
+
"Documentation" = "https://github.com/yourusername/agsec#readme"
|
|
29
|
+
|
|
30
|
+
[tool.pytest.ini_options]
|
|
31
|
+
minversion = "6.0"
|
|
32
|
+
addopts = "-ra -q"
|
|
33
|
+
testpaths = ["tests"]
|
|
34
|
+
|
|
35
|
+
[tool.black]
|
|
36
|
+
line-length = 88
|
|
37
|
+
target-version = ['py38']
|
|
38
|
+
|
|
39
|
+
[tool.isort]
|
|
40
|
+
profile = "black"
|
|
41
|
+
line_length = 88
|
|
42
|
+
|
|
43
|
+
[tool.flake8]
|
|
44
|
+
max-line-length = 88
|
|
45
|
+
extend-ignore = ["E203", "W503"]
|
agsec-0.1.0/setup.cfg
ADDED
agsec-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
with open("README.md", "r", encoding="utf-8") as fh:
|
|
4
|
+
long_description = fh.read()
|
|
5
|
+
|
|
6
|
+
setup(
|
|
7
|
+
name="agsec",
|
|
8
|
+
version="0.1.0",
|
|
9
|
+
author="Riyandhiman",
|
|
10
|
+
author_email="noreply@example.com",
|
|
11
|
+
description="AI Agent Action Firewall core SDK",
|
|
12
|
+
long_description=long_description,
|
|
13
|
+
long_description_content_type="text/markdown",
|
|
14
|
+
url="https://github.com/yourusername/agsec",
|
|
15
|
+
packages=find_packages(),
|
|
16
|
+
classifiers=[
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
],
|
|
21
|
+
python_requires=">=3.8",
|
|
22
|
+
install_requires=[
|
|
23
|
+
"PyYAML>=6.0",
|
|
24
|
+
],
|
|
25
|
+
extras_require={
|
|
26
|
+
"dev": [
|
|
27
|
+
"pre-commit>=3.0",
|
|
28
|
+
"black>=24.0",
|
|
29
|
+
"isort>=5.0",
|
|
30
|
+
"flake8>=7.0",
|
|
31
|
+
"pytest>=6.0",
|
|
32
|
+
"build>=1.0",
|
|
33
|
+
"twine>=4.0",
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from agsec import ControlLayer, PolicyEngine, PolicyResult, PolicyStatus
|
|
4
|
+
from agsec.exceptions import PolicyViolationError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_control_execute_allow(monkeypatch):
|
|
8
|
+
control = ControlLayer(policy_engine=PolicyEngine())
|
|
9
|
+
|
|
10
|
+
@control.register_action("noop")
|
|
11
|
+
def noop():
|
|
12
|
+
return "ok"
|
|
13
|
+
|
|
14
|
+
result = control.execute("noop", {})
|
|
15
|
+
assert result.result == "ok"
|
|
16
|
+
assert result.policy.status == PolicyStatus.ALLOW
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_control_execute_block():
|
|
20
|
+
engine = PolicyEngine()
|
|
21
|
+
|
|
22
|
+
def deny_all(action, params, context):
|
|
23
|
+
return PolicyResult(status=PolicyStatus.BLOCK, reason="denied")
|
|
24
|
+
|
|
25
|
+
engine.add_rule(deny_all)
|
|
26
|
+
control = ControlLayer(policy_engine=engine)
|
|
27
|
+
|
|
28
|
+
@control.register_action("noop")
|
|
29
|
+
def noop():
|
|
30
|
+
return "ok"
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
control.execute("noop", {})
|
|
34
|
+
assert False, "Expected PolicyViolationError"
|
|
35
|
+
except PolicyViolationError:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_policy_review():
|
|
40
|
+
engine = PolicyEngine()
|
|
41
|
+
|
|
42
|
+
def review_payment(action, params, context):
|
|
43
|
+
if action == "payment" and params.get("amount") > 5000:
|
|
44
|
+
return PolicyResult(status=PolicyStatus.REVIEW, reason="manual review required")
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
engine.add_rule(review_payment)
|
|
48
|
+
control = ControlLayer(policy_engine=engine)
|
|
49
|
+
|
|
50
|
+
@control.register_action("payment")
|
|
51
|
+
def payment(amount):
|
|
52
|
+
return {"charged": amount}
|
|
53
|
+
|
|
54
|
+
result = control.execute("payment", {"amount": 6000})
|
|
55
|
+
assert result.result is None
|
|
56
|
+
assert result.policy.status == PolicyStatus.REVIEW
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_policy_engine_load_yaml_block():
|
|
60
|
+
yaml_rules = """
|
|
61
|
+
rules:
|
|
62
|
+
- action: payment
|
|
63
|
+
status: block
|
|
64
|
+
reason: "Amount over limit"
|
|
65
|
+
conditions:
|
|
66
|
+
amount:
|
|
67
|
+
op: ">"
|
|
68
|
+
value: 10000
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
engine = PolicyEngine()
|
|
72
|
+
engine.load_rules_from_yaml(yaml_rules)
|
|
73
|
+
|
|
74
|
+
control = ControlLayer(policy_engine=engine)
|
|
75
|
+
|
|
76
|
+
@control.register_action("payment")
|
|
77
|
+
def payment(amount):
|
|
78
|
+
return {"charged": amount}
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
control.execute("payment", {"amount": 15000})
|
|
82
|
+
assert False, "Expected PolicyViolationError"
|
|
83
|
+
except PolicyViolationError as e:
|
|
84
|
+
assert "Amount over limit" in str(e)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_control_layer_load_policy_yaml_direct():
|
|
88
|
+
yaml_rules = """
|
|
89
|
+
rules:
|
|
90
|
+
- action: send_email
|
|
91
|
+
status: allow
|
|
92
|
+
reason: "always allow"
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
control = ControlLayer(policy_yaml=yaml_rules)
|
|
96
|
+
|
|
97
|
+
@control.register_action("send_email")
|
|
98
|
+
def send_email(to):
|
|
99
|
+
return {"sent_to": to}
|
|
100
|
+
|
|
101
|
+
result = control.execute("send_email", {"to": "x@example.com"})
|
|
102
|
+
assert result.result == {"sent_to": "x@example.com"}
|
|
103
|
+
assert result.policy.status == PolicyStatus.ALLOW
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_policy_engine_yaml_priority():
|
|
107
|
+
yaml_rules = """
|
|
108
|
+
rules:
|
|
109
|
+
- action: payment
|
|
110
|
+
status: allow
|
|
111
|
+
priority: 0
|
|
112
|
+
- action: payment
|
|
113
|
+
status: block
|
|
114
|
+
reason: "Higher priority block"
|
|
115
|
+
priority: 100
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
engine = PolicyEngine()
|
|
119
|
+
engine.load_rules_from_yaml(yaml_rules)
|
|
120
|
+
|
|
121
|
+
control = ControlLayer(policy_engine=engine)
|
|
122
|
+
|
|
123
|
+
@control.register_action("payment")
|
|
124
|
+
def payment(amount):
|
|
125
|
+
return {"charged": amount}
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
control.execute("payment", {"amount": 10})
|
|
129
|
+
assert False, "Expected PolicyViolationError due to higher priority block"
|
|
130
|
+
except PolicyViolationError as e:
|
|
131
|
+
assert "Higher priority block" in str(e)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_policy_engine_yaml_match_any():
|
|
135
|
+
yaml_rules = """
|
|
136
|
+
rules:
|
|
137
|
+
- action: data_export
|
|
138
|
+
status: block
|
|
139
|
+
match: any
|
|
140
|
+
conditions:
|
|
141
|
+
table:
|
|
142
|
+
op: "=="
|
|
143
|
+
value: "sensitive"
|
|
144
|
+
export_type:
|
|
145
|
+
op: "=="
|
|
146
|
+
value: "external"
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
engine = PolicyEngine()
|
|
150
|
+
engine.load_rules_from_yaml(yaml_rules)
|
|
151
|
+
|
|
152
|
+
control = ControlLayer(policy_engine=engine)
|
|
153
|
+
|
|
154
|
+
@control.register_action("data_export")
|
|
155
|
+
def data_export(table, export_type):
|
|
156
|
+
return {"ok": True}
|
|
157
|
+
|
|
158
|
+
# should block on table match
|
|
159
|
+
try:
|
|
160
|
+
control.execute("data_export", {"table": "sensitive", "export_type": "internal"})
|
|
161
|
+
assert False
|
|
162
|
+
except PolicyViolationError:
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_policy_engine_yaml_context_condition():
|
|
167
|
+
yaml_rules = """
|
|
168
|
+
rules:
|
|
169
|
+
- action: password_reset
|
|
170
|
+
status: block
|
|
171
|
+
conditions:
|
|
172
|
+
context.user_role:
|
|
173
|
+
op: "=="
|
|
174
|
+
value: "guest"
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
engine = PolicyEngine()
|
|
178
|
+
engine.load_rules_from_yaml(yaml_rules)
|
|
179
|
+
|
|
180
|
+
control = ControlLayer(policy_engine=engine)
|
|
181
|
+
|
|
182
|
+
@control.register_action("password_reset")
|
|
183
|
+
def password_reset(user_id):
|
|
184
|
+
return {"reset": user_id}
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
control.execute("password_reset", {"user_id": "u1"}, context={"user_role": "guest"})
|
|
188
|
+
assert False
|
|
189
|
+
except PolicyViolationError:
|
|
190
|
+
pass
|