frenum 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.
- frenum-0.1.0/.gitignore +10 -0
- frenum-0.1.0/LICENSE +21 -0
- frenum-0.1.0/PKG-INFO +233 -0
- frenum-0.1.0/README.md +197 -0
- frenum-0.1.0/examples/basic_usage.py +51 -0
- frenum-0.1.0/examples/config.yaml +48 -0
- frenum-0.1.0/pyproject.toml +49 -0
- frenum-0.1.0/src/frenum/__init__.py +25 -0
- frenum-0.1.0/src/frenum/_types.py +85 -0
- frenum-0.1.0/src/frenum/adapters/__init__.py +0 -0
- frenum-0.1.0/src/frenum/adapters/langgraph.py +92 -0
- frenum-0.1.0/src/frenum/audit.py +108 -0
- frenum-0.1.0/src/frenum/engine.py +134 -0
- frenum-0.1.0/src/frenum/reporter.py +204 -0
- frenum-0.1.0/src/frenum/rules.py +191 -0
- frenum-0.1.0/tests/conftest.py +87 -0
- frenum-0.1.0/tests/test_audit.py +86 -0
- frenum-0.1.0/tests/test_engine.py +96 -0
- frenum-0.1.0/tests/test_reporter.py +174 -0
- frenum-0.1.0/tests/test_rules.py +130 -0
- frenum-0.1.0/uv.lock +1113 -0
frenum-0.1.0/.gitignore
ADDED
frenum-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Terry Li
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
frenum-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: frenum
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Deterministic, config-driven guardrails for LLM agent tool calls
|
|
5
|
+
Project-URL: Homepage, https://github.com/terry-li-hm/frenum
|
|
6
|
+
Project-URL: Repository, https://github.com/terry-li-hm/frenum
|
|
7
|
+
Author-email: Terry Li <terry.li.hm@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agent,ai,audit,compliance,guardrails,llm,tool-calls
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Security
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Provides-Extra: all
|
|
23
|
+
Requires-Dist: langchain-core>=0.3; extra == 'all'
|
|
24
|
+
Requires-Dist: langgraph>=0.2; extra == 'all'
|
|
25
|
+
Requires-Dist: pyyaml>=6.0; extra == 'all'
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pyyaml>=6.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
30
|
+
Provides-Extra: langgraph
|
|
31
|
+
Requires-Dist: langchain-core>=0.3; extra == 'langgraph'
|
|
32
|
+
Requires-Dist: langgraph>=0.2; extra == 'langgraph'
|
|
33
|
+
Provides-Extra: yaml
|
|
34
|
+
Requires-Dist: pyyaml>=6.0; extra == 'yaml'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# frenum
|
|
38
|
+
|
|
39
|
+
> Deterministic, config-driven guardrails for LLM agent tool calls.
|
|
40
|
+
> Zero-LLM enforcement. Compliance audit trail built in.
|
|
41
|
+
|
|
42
|
+
Named after the Latin word for *bridle* or *restraint*. The rein that keeps your LLM agent in check.
|
|
43
|
+
|
|
44
|
+
## Why
|
|
45
|
+
|
|
46
|
+
LLM agents can now call tools autonomously — execute SQL, send emails, make API calls. Existing guardrail frameworks either use LLM calls in the enforcement path (non-deterministic, slow, expensive) or require writing Python code that compliance teams can't review.
|
|
47
|
+
|
|
48
|
+
Frenum is different:
|
|
49
|
+
|
|
50
|
+
- **YAML config** — rules live in config files that compliance teams can review, version, and audit without reading Python
|
|
51
|
+
- **Zero-LLM enforcement** — every decision is deterministic and reproducible. No AI in the guardrail path.
|
|
52
|
+
- **Compliance-first** — OPA-inspired audit trail as a first-class feature, not an afterthought
|
|
53
|
+
- **Framework-agnostic** — works standalone or with LangGraph, CrewAI, or any agent framework
|
|
54
|
+
|
|
55
|
+
| Feature | Frenum | NeMo Guardrails | LangChain Middleware | OpenAI Agents SDK |
|
|
56
|
+
|---|---|---|---|---|
|
|
57
|
+
| Config-driven (YAML) | Yes | Colang DSL | Python code | Python decorators |
|
|
58
|
+
| Zero-LLM enforcement | Yes | No (LLM in loop) | Yes | Yes |
|
|
59
|
+
| Audit trail | Built-in | Logging only | No | No |
|
|
60
|
+
| Framework-agnostic | Yes | LangChain | LangChain only | OpenAI only |
|
|
61
|
+
| Dependencies | Zero (stdlib) | Heavy | LangChain | OpenAI SDK |
|
|
62
|
+
|
|
63
|
+
## Quick Start
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install frenum[yaml]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from frenum import Engine, ToolCall
|
|
71
|
+
|
|
72
|
+
engine = Engine.from_yaml("rules.yaml")
|
|
73
|
+
|
|
74
|
+
# Evaluate a tool call
|
|
75
|
+
result = engine.evaluate(
|
|
76
|
+
ToolCall(name="execute_sql", args={"query": "DROP TABLE users"})
|
|
77
|
+
)
|
|
78
|
+
print(result.decision) # Decision.BLOCK
|
|
79
|
+
print(result.reason) # "Pattern matched in field 'query': DROP TABLE"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## YAML Config
|
|
83
|
+
|
|
84
|
+
```yaml
|
|
85
|
+
version: "1.0"
|
|
86
|
+
policy_version: "1.0.0"
|
|
87
|
+
|
|
88
|
+
rules:
|
|
89
|
+
# Block dangerous SQL patterns
|
|
90
|
+
- name: block_sql_injection
|
|
91
|
+
type: regex_block
|
|
92
|
+
applies_to: ["execute_sql", "run_query"]
|
|
93
|
+
params:
|
|
94
|
+
fields: ["query"]
|
|
95
|
+
patterns:
|
|
96
|
+
- "(?i)(DROP|DELETE|TRUNCATE)\\s+TABLE"
|
|
97
|
+
|
|
98
|
+
# Require confirmation IDs on sensitive operations
|
|
99
|
+
- name: require_confirmation
|
|
100
|
+
type: regex_require
|
|
101
|
+
applies_to: ["send_email", "transfer_funds"]
|
|
102
|
+
params:
|
|
103
|
+
fields: ["confirmation_id"]
|
|
104
|
+
pattern: "^CONF-[A-Z0-9]{8}$"
|
|
105
|
+
|
|
106
|
+
# Scan all tool calls for PII leakage
|
|
107
|
+
- name: detect_pii
|
|
108
|
+
type: pii_detect
|
|
109
|
+
applies_to: ["*"]
|
|
110
|
+
params:
|
|
111
|
+
detectors: [email, phone_intl, hk_id, credit_card]
|
|
112
|
+
action: block
|
|
113
|
+
|
|
114
|
+
# Role-based tool access
|
|
115
|
+
- name: tool_entitlement
|
|
116
|
+
type: entitlement
|
|
117
|
+
applies_to: ["*"]
|
|
118
|
+
params:
|
|
119
|
+
roles:
|
|
120
|
+
analyst: ["search", "get_data", "summarize"]
|
|
121
|
+
admin: ["*"]
|
|
122
|
+
default: block
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Rule Types
|
|
126
|
+
|
|
127
|
+
| Type | Purpose | Key Params |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| `regex_block` | Block if field matches pattern | `fields`, `patterns` |
|
|
130
|
+
| `regex_require` | Block if required field is missing/invalid | `fields`, `pattern` |
|
|
131
|
+
| `pii_detect` | Scan args for PII (email, phone, HKID, etc.) | `detectors`, `action` |
|
|
132
|
+
| `entitlement` | Role-based tool access control | `roles`, `default` |
|
|
133
|
+
|
|
134
|
+
## Audit Trail
|
|
135
|
+
|
|
136
|
+
Every evaluation produces a structured JSON record (JSONL format, OPA-inspired):
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from frenum import AuditLogger, Engine
|
|
140
|
+
|
|
141
|
+
logger = AuditLogger("audit.jsonl")
|
|
142
|
+
engine = Engine.from_yaml("rules.yaml", audit_logger=logger.log)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Each record includes: `decision_id`, `timestamp`, `policy_version`, `tool_name`, `tool_args` (redacted), `decision`, `rules_evaluated`, `blocking_rule`, `human_override`, `trace_id`.
|
|
146
|
+
|
|
147
|
+
## Audit Reports
|
|
148
|
+
|
|
149
|
+
Generate compliance-ready summaries from audit logs:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from frenum import AuditReporter
|
|
153
|
+
|
|
154
|
+
reporter = AuditReporter("audit.jsonl")
|
|
155
|
+
report = reporter.generate()
|
|
156
|
+
print(report.to_text())
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
========================================
|
|
161
|
+
OBEX AUDIT REPORT
|
|
162
|
+
========================================
|
|
163
|
+
Period: 2026-02-28 10:00 to 2026-02-28 16:00
|
|
164
|
+
Policy versions: 1.0.0
|
|
165
|
+
|
|
166
|
+
Total evaluations: 500
|
|
167
|
+
Allow: 450 (90.0%)
|
|
168
|
+
Block: 50 (10.0%)
|
|
169
|
+
|
|
170
|
+
Top blocked tools:
|
|
171
|
+
1. execute_sql — 25 blocks
|
|
172
|
+
2. send_email — 15 blocks
|
|
173
|
+
|
|
174
|
+
Top triggered rules:
|
|
175
|
+
1. block_sql_injection — 20 triggers
|
|
176
|
+
2. detect_pii — 18 triggers
|
|
177
|
+
|
|
178
|
+
Human override rate: 4.0% (2 of 50 blocks overridden)
|
|
179
|
+
========================================
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## LangGraph Integration
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
pip install frenum[langgraph]
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from langgraph.prebuilt import ToolNode
|
|
190
|
+
from frenum import Engine
|
|
191
|
+
from frenum.adapters.langgraph import guarded_tool_node
|
|
192
|
+
|
|
193
|
+
tools = [search, calculator]
|
|
194
|
+
engine = Engine.from_yaml("rules.yaml")
|
|
195
|
+
safe_tools = guarded_tool_node(ToolNode(tools), engine)
|
|
196
|
+
|
|
197
|
+
builder.add_node("tools", safe_tools)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Blocked tool calls return a `ToolMessage` with the block reason — the LLM sees why its call was rejected and can adjust.
|
|
201
|
+
|
|
202
|
+
## Programmatic Use (No YAML)
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from frenum import Engine, ToolCall
|
|
206
|
+
from frenum._types import RuleConfig, RuleType
|
|
207
|
+
|
|
208
|
+
engine = Engine(rules=[
|
|
209
|
+
RuleConfig(
|
|
210
|
+
name="block_drops",
|
|
211
|
+
rule_type=RuleType.REGEX_BLOCK,
|
|
212
|
+
params={"fields": ["query"], "patterns": [r"(?i)DROP\s+TABLE"]},
|
|
213
|
+
applies_to=["execute_sql"],
|
|
214
|
+
),
|
|
215
|
+
])
|
|
216
|
+
|
|
217
|
+
result = engine.evaluate(ToolCall(name="execute_sql", args={"query": "SELECT 1"}))
|
|
218
|
+
assert result.decision.value == "allow"
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Zero dependencies — the core engine runs on stdlib alone.
|
|
222
|
+
|
|
223
|
+
## Design Philosophy
|
|
224
|
+
|
|
225
|
+
- **Hooks > prompts** for mechanical rules. If a rule is regex-matchable, enforce it in code, not in the system prompt.
|
|
226
|
+
- **Fail closed.** If a rule errors, the tool call is blocked.
|
|
227
|
+
- **No LLM in the enforcement path.** Every decision is deterministic and reproducible.
|
|
228
|
+
- **Config is reviewable.** Compliance teams review YAML, not Python.
|
|
229
|
+
- **Audit schema inspired by OPA.** Decision logs follow established policy-engine conventions.
|
|
230
|
+
|
|
231
|
+
## License
|
|
232
|
+
|
|
233
|
+
MIT
|
frenum-0.1.0/README.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frenum
|
|
2
|
+
|
|
3
|
+
> Deterministic, config-driven guardrails for LLM agent tool calls.
|
|
4
|
+
> Zero-LLM enforcement. Compliance audit trail built in.
|
|
5
|
+
|
|
6
|
+
Named after the Latin word for *bridle* or *restraint*. The rein that keeps your LLM agent in check.
|
|
7
|
+
|
|
8
|
+
## Why
|
|
9
|
+
|
|
10
|
+
LLM agents can now call tools autonomously — execute SQL, send emails, make API calls. Existing guardrail frameworks either use LLM calls in the enforcement path (non-deterministic, slow, expensive) or require writing Python code that compliance teams can't review.
|
|
11
|
+
|
|
12
|
+
Frenum is different:
|
|
13
|
+
|
|
14
|
+
- **YAML config** — rules live in config files that compliance teams can review, version, and audit without reading Python
|
|
15
|
+
- **Zero-LLM enforcement** — every decision is deterministic and reproducible. No AI in the guardrail path.
|
|
16
|
+
- **Compliance-first** — OPA-inspired audit trail as a first-class feature, not an afterthought
|
|
17
|
+
- **Framework-agnostic** — works standalone or with LangGraph, CrewAI, or any agent framework
|
|
18
|
+
|
|
19
|
+
| Feature | Frenum | NeMo Guardrails | LangChain Middleware | OpenAI Agents SDK |
|
|
20
|
+
|---|---|---|---|---|
|
|
21
|
+
| Config-driven (YAML) | Yes | Colang DSL | Python code | Python decorators |
|
|
22
|
+
| Zero-LLM enforcement | Yes | No (LLM in loop) | Yes | Yes |
|
|
23
|
+
| Audit trail | Built-in | Logging only | No | No |
|
|
24
|
+
| Framework-agnostic | Yes | LangChain | LangChain only | OpenAI only |
|
|
25
|
+
| Dependencies | Zero (stdlib) | Heavy | LangChain | OpenAI SDK |
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install frenum[yaml]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from frenum import Engine, ToolCall
|
|
35
|
+
|
|
36
|
+
engine = Engine.from_yaml("rules.yaml")
|
|
37
|
+
|
|
38
|
+
# Evaluate a tool call
|
|
39
|
+
result = engine.evaluate(
|
|
40
|
+
ToolCall(name="execute_sql", args={"query": "DROP TABLE users"})
|
|
41
|
+
)
|
|
42
|
+
print(result.decision) # Decision.BLOCK
|
|
43
|
+
print(result.reason) # "Pattern matched in field 'query': DROP TABLE"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## YAML Config
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
version: "1.0"
|
|
50
|
+
policy_version: "1.0.0"
|
|
51
|
+
|
|
52
|
+
rules:
|
|
53
|
+
# Block dangerous SQL patterns
|
|
54
|
+
- name: block_sql_injection
|
|
55
|
+
type: regex_block
|
|
56
|
+
applies_to: ["execute_sql", "run_query"]
|
|
57
|
+
params:
|
|
58
|
+
fields: ["query"]
|
|
59
|
+
patterns:
|
|
60
|
+
- "(?i)(DROP|DELETE|TRUNCATE)\\s+TABLE"
|
|
61
|
+
|
|
62
|
+
# Require confirmation IDs on sensitive operations
|
|
63
|
+
- name: require_confirmation
|
|
64
|
+
type: regex_require
|
|
65
|
+
applies_to: ["send_email", "transfer_funds"]
|
|
66
|
+
params:
|
|
67
|
+
fields: ["confirmation_id"]
|
|
68
|
+
pattern: "^CONF-[A-Z0-9]{8}$"
|
|
69
|
+
|
|
70
|
+
# Scan all tool calls for PII leakage
|
|
71
|
+
- name: detect_pii
|
|
72
|
+
type: pii_detect
|
|
73
|
+
applies_to: ["*"]
|
|
74
|
+
params:
|
|
75
|
+
detectors: [email, phone_intl, hk_id, credit_card]
|
|
76
|
+
action: block
|
|
77
|
+
|
|
78
|
+
# Role-based tool access
|
|
79
|
+
- name: tool_entitlement
|
|
80
|
+
type: entitlement
|
|
81
|
+
applies_to: ["*"]
|
|
82
|
+
params:
|
|
83
|
+
roles:
|
|
84
|
+
analyst: ["search", "get_data", "summarize"]
|
|
85
|
+
admin: ["*"]
|
|
86
|
+
default: block
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Rule Types
|
|
90
|
+
|
|
91
|
+
| Type | Purpose | Key Params |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `regex_block` | Block if field matches pattern | `fields`, `patterns` |
|
|
94
|
+
| `regex_require` | Block if required field is missing/invalid | `fields`, `pattern` |
|
|
95
|
+
| `pii_detect` | Scan args for PII (email, phone, HKID, etc.) | `detectors`, `action` |
|
|
96
|
+
| `entitlement` | Role-based tool access control | `roles`, `default` |
|
|
97
|
+
|
|
98
|
+
## Audit Trail
|
|
99
|
+
|
|
100
|
+
Every evaluation produces a structured JSON record (JSONL format, OPA-inspired):
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from frenum import AuditLogger, Engine
|
|
104
|
+
|
|
105
|
+
logger = AuditLogger("audit.jsonl")
|
|
106
|
+
engine = Engine.from_yaml("rules.yaml", audit_logger=logger.log)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Each record includes: `decision_id`, `timestamp`, `policy_version`, `tool_name`, `tool_args` (redacted), `decision`, `rules_evaluated`, `blocking_rule`, `human_override`, `trace_id`.
|
|
110
|
+
|
|
111
|
+
## Audit Reports
|
|
112
|
+
|
|
113
|
+
Generate compliance-ready summaries from audit logs:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from frenum import AuditReporter
|
|
117
|
+
|
|
118
|
+
reporter = AuditReporter("audit.jsonl")
|
|
119
|
+
report = reporter.generate()
|
|
120
|
+
print(report.to_text())
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
========================================
|
|
125
|
+
OBEX AUDIT REPORT
|
|
126
|
+
========================================
|
|
127
|
+
Period: 2026-02-28 10:00 to 2026-02-28 16:00
|
|
128
|
+
Policy versions: 1.0.0
|
|
129
|
+
|
|
130
|
+
Total evaluations: 500
|
|
131
|
+
Allow: 450 (90.0%)
|
|
132
|
+
Block: 50 (10.0%)
|
|
133
|
+
|
|
134
|
+
Top blocked tools:
|
|
135
|
+
1. execute_sql — 25 blocks
|
|
136
|
+
2. send_email — 15 blocks
|
|
137
|
+
|
|
138
|
+
Top triggered rules:
|
|
139
|
+
1. block_sql_injection — 20 triggers
|
|
140
|
+
2. detect_pii — 18 triggers
|
|
141
|
+
|
|
142
|
+
Human override rate: 4.0% (2 of 50 blocks overridden)
|
|
143
|
+
========================================
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## LangGraph Integration
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
pip install frenum[langgraph]
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from langgraph.prebuilt import ToolNode
|
|
154
|
+
from frenum import Engine
|
|
155
|
+
from frenum.adapters.langgraph import guarded_tool_node
|
|
156
|
+
|
|
157
|
+
tools = [search, calculator]
|
|
158
|
+
engine = Engine.from_yaml("rules.yaml")
|
|
159
|
+
safe_tools = guarded_tool_node(ToolNode(tools), engine)
|
|
160
|
+
|
|
161
|
+
builder.add_node("tools", safe_tools)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Blocked tool calls return a `ToolMessage` with the block reason — the LLM sees why its call was rejected and can adjust.
|
|
165
|
+
|
|
166
|
+
## Programmatic Use (No YAML)
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from frenum import Engine, ToolCall
|
|
170
|
+
from frenum._types import RuleConfig, RuleType
|
|
171
|
+
|
|
172
|
+
engine = Engine(rules=[
|
|
173
|
+
RuleConfig(
|
|
174
|
+
name="block_drops",
|
|
175
|
+
rule_type=RuleType.REGEX_BLOCK,
|
|
176
|
+
params={"fields": ["query"], "patterns": [r"(?i)DROP\s+TABLE"]},
|
|
177
|
+
applies_to=["execute_sql"],
|
|
178
|
+
),
|
|
179
|
+
])
|
|
180
|
+
|
|
181
|
+
result = engine.evaluate(ToolCall(name="execute_sql", args={"query": "SELECT 1"}))
|
|
182
|
+
assert result.decision.value == "allow"
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Zero dependencies — the core engine runs on stdlib alone.
|
|
186
|
+
|
|
187
|
+
## Design Philosophy
|
|
188
|
+
|
|
189
|
+
- **Hooks > prompts** for mechanical rules. If a rule is regex-matchable, enforce it in code, not in the system prompt.
|
|
190
|
+
- **Fail closed.** If a rule errors, the tool call is blocked.
|
|
191
|
+
- **No LLM in the enforcement path.** Every decision is deterministic and reproducible.
|
|
192
|
+
- **Config is reviewable.** Compliance teams review YAML, not Python.
|
|
193
|
+
- **Audit schema inspired by OPA.** Decision logs follow established policy-engine conventions.
|
|
194
|
+
|
|
195
|
+
## License
|
|
196
|
+
|
|
197
|
+
MIT
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Basic usage of frenum — evaluate tool calls against YAML rules."""
|
|
2
|
+
|
|
3
|
+
from frenum import AuditLogger, AuditReporter, Engine, ToolCall
|
|
4
|
+
|
|
5
|
+
# Load rules from YAML config
|
|
6
|
+
engine = Engine.from_yaml("examples/config.yaml", audit_logger=AuditLogger("audit.jsonl").log)
|
|
7
|
+
|
|
8
|
+
# A safe query by an authorized user — passes through
|
|
9
|
+
safe = ToolCall(
|
|
10
|
+
name="execute_sql",
|
|
11
|
+
args={"query": "SELECT * FROM users WHERE id = 1"},
|
|
12
|
+
metadata={"role": "operator"},
|
|
13
|
+
)
|
|
14
|
+
result = engine.evaluate(safe)
|
|
15
|
+
print(f"Safe query: {result.decision.value}") # allow
|
|
16
|
+
|
|
17
|
+
# A dangerous query — blocked by regex rule
|
|
18
|
+
dangerous = ToolCall(
|
|
19
|
+
name="execute_sql",
|
|
20
|
+
args={"query": "DROP TABLE users"},
|
|
21
|
+
metadata={"role": "operator"},
|
|
22
|
+
)
|
|
23
|
+
result = engine.evaluate(dangerous)
|
|
24
|
+
print(f"DROP TABLE: {result.decision.value}") # block
|
|
25
|
+
print(f" Reason: {result.reason}")
|
|
26
|
+
|
|
27
|
+
# PII in arguments — blocked
|
|
28
|
+
pii = ToolCall(
|
|
29
|
+
name="search",
|
|
30
|
+
args={"query": "Contact alice@example.com"},
|
|
31
|
+
metadata={"role": "analyst"},
|
|
32
|
+
)
|
|
33
|
+
result = engine.evaluate(pii)
|
|
34
|
+
print(f"PII leak: {result.decision.value}") # block
|
|
35
|
+
print(f" Reason: {result.reason}")
|
|
36
|
+
|
|
37
|
+
# Unauthorized tool access — analyst can't run SQL
|
|
38
|
+
unauthorized = ToolCall(
|
|
39
|
+
name="execute_sql",
|
|
40
|
+
args={"query": "SELECT 1"},
|
|
41
|
+
metadata={"role": "analyst"},
|
|
42
|
+
)
|
|
43
|
+
result = engine.evaluate(unauthorized)
|
|
44
|
+
print(f"Unauthed: {result.decision.value}") # block
|
|
45
|
+
print(f" Reason: {result.reason}")
|
|
46
|
+
|
|
47
|
+
# Generate audit report
|
|
48
|
+
print()
|
|
49
|
+
reporter = AuditReporter("audit.jsonl")
|
|
50
|
+
report = reporter.generate()
|
|
51
|
+
print(report.to_text())
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# limen configuration example
|
|
2
|
+
# YAML rules that compliance teams can review without reading Python.
|
|
3
|
+
|
|
4
|
+
version: "1.0"
|
|
5
|
+
policy_version: "1.0.0"
|
|
6
|
+
|
|
7
|
+
rules:
|
|
8
|
+
# Block dangerous SQL patterns
|
|
9
|
+
- name: block_sql_injection
|
|
10
|
+
type: regex_block
|
|
11
|
+
applies_to: ["execute_sql", "run_query"]
|
|
12
|
+
params:
|
|
13
|
+
fields: ["query"]
|
|
14
|
+
patterns:
|
|
15
|
+
- "(?i)(DROP|DELETE|TRUNCATE)\\s+TABLE"
|
|
16
|
+
- "(?i);\\s*--"
|
|
17
|
+
- "(?i)UNION\\s+SELECT"
|
|
18
|
+
|
|
19
|
+
# Require confirmation IDs on sensitive operations
|
|
20
|
+
- name: require_confirmation
|
|
21
|
+
type: regex_require
|
|
22
|
+
applies_to: ["send_email", "transfer_funds"]
|
|
23
|
+
params:
|
|
24
|
+
fields: ["confirmation_id"]
|
|
25
|
+
pattern: "^CONF-[A-Z0-9]{8}$"
|
|
26
|
+
|
|
27
|
+
# Scan all tool outputs for PII
|
|
28
|
+
- name: detect_pii
|
|
29
|
+
type: pii_detect
|
|
30
|
+
applies_to: ["*"]
|
|
31
|
+
params:
|
|
32
|
+
detectors:
|
|
33
|
+
- email
|
|
34
|
+
- phone_intl
|
|
35
|
+
- hk_id
|
|
36
|
+
- credit_card
|
|
37
|
+
action: block
|
|
38
|
+
|
|
39
|
+
# Role-based tool access
|
|
40
|
+
- name: tool_entitlement
|
|
41
|
+
type: entitlement
|
|
42
|
+
applies_to: ["*"]
|
|
43
|
+
params:
|
|
44
|
+
roles:
|
|
45
|
+
analyst: ["search", "get_data", "summarize"]
|
|
46
|
+
admin: ["*"]
|
|
47
|
+
operator: ["search", "get_data", "execute_sql"]
|
|
48
|
+
default: block
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "frenum"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Deterministic, config-driven guardrails for LLM agent tool calls"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
authors = [{ name = "Terry Li", email = "terry.li.hm@gmail.com" }]
|
|
9
|
+
keywords = ["ai", "agent", "guardrails", "llm", "tool-calls", "compliance", "audit"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.10",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Topic :: Security",
|
|
20
|
+
"Topic :: Software Development :: Libraries",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://github.com/terry-li-hm/frenum"
|
|
25
|
+
Repository = "https://github.com/terry-li-hm/frenum"
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
yaml = ["pyyaml>=6.0"]
|
|
29
|
+
langgraph = ["langgraph>=0.2", "langchain-core>=0.3"]
|
|
30
|
+
all = ["frenum[yaml,langgraph]"]
|
|
31
|
+
dev = ["pytest>=8.0", "ruff>=0.8", "pyyaml>=6.0"]
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["hatchling"]
|
|
35
|
+
build-backend = "hatchling.build"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/frenum"]
|
|
39
|
+
|
|
40
|
+
[tool.ruff]
|
|
41
|
+
target-version = "py310"
|
|
42
|
+
line-length = 100
|
|
43
|
+
|
|
44
|
+
[tool.ruff.lint]
|
|
45
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
testpaths = ["tests"]
|
|
49
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""frenum — Deterministic, config-driven guardrails for LLM agent tool calls.
|
|
2
|
+
|
|
3
|
+
Named after the Latin word for 'bridle' or 'restraint'.
|
|
4
|
+
The rein that keeps your LLM agent in check.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from frenum._types import Decision, EvalResult, RuleConfig, RuleResult, ToolCall, ToolCallBlocked
|
|
8
|
+
from frenum.audit import AuditLogger
|
|
9
|
+
from frenum.engine import Engine
|
|
10
|
+
from frenum.reporter import AuditReporter, Report
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"AuditLogger",
|
|
16
|
+
"AuditReporter",
|
|
17
|
+
"Decision",
|
|
18
|
+
"Engine",
|
|
19
|
+
"EvalResult",
|
|
20
|
+
"Report",
|
|
21
|
+
"RuleConfig",
|
|
22
|
+
"RuleResult",
|
|
23
|
+
"ToolCall",
|
|
24
|
+
"ToolCallBlocked",
|
|
25
|
+
]
|