hydracuda 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.
@@ -0,0 +1,25 @@
1
+ name: Publish HYDRACUDA to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ environment: pypi
11
+ permissions:
12
+ id-token: write
13
+ contents: read
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.12"
20
+
21
+ - run: pip install build
22
+
23
+ - run: python -m build
24
+
25
+ - uses: pypa/gh-action-pypi-publish@v1.12.3
@@ -0,0 +1,17 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.pyc
4
+ .env
5
+ .venv
6
+ .envrc
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .dist/
10
+ build/
11
+ dist/
12
+ *.egg-info/
13
+ tests/test_live.py
14
+ baracuda.yaml
15
+ hydracuda.yaml
16
+ .baracuda/
17
+ .hydracuda/
@@ -0,0 +1 @@
1
+ 404: Not Found
@@ -0,0 +1,246 @@
1
+ Metadata-Version: 2.4
2
+ Name: hydracuda
3
+ Version: 0.1.0
4
+ Summary: Hybrid Runtime Access Control for Untrusted Delegated Actions. Runtime policy enforcement proxy for AI tool calls.
5
+ Project-URL: Homepage, https://github.com/Xtrinel-Group/HYDRACUDA
6
+ Project-URL: Issues, https://github.com/Xtrinel-Group/HYDRACUDA/issues
7
+ License: 404: Not Found
8
+ License-File: LICENSE
9
+ Keywords: ai,guardrails,llm,mcp,policy,security
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Security
14
+ Requires-Python: >=3.10
15
+ Requires-Dist: aiosqlite>=0.19
16
+ Requires-Dist: pyyaml>=6.0
17
+ Description-Content-Type: text/markdown
18
+
19
+ <p align="center">
20
+ <img src="https://assets.xtrinel.com/hydracuda-full.svg" alt="HYDRACUDA" width="480" />
21
+ </p>
22
+
23
+ # HYDRACUDA
24
+
25
+ Hybrid Runtime Access Control for Untrusted Delegated Actions.
26
+
27
+ HYDRACUDA is a lightweight, open source policy enforcement layer for AI tool calls.
28
+ Define a YAML policy file, drop HYDRACUDA in front of any MCP server or LLM tool layer, and every call is either allowed, denied, or held for review before it executes.
29
+
30
+ The goal is simple: prevent tool-call abuse and out-of-scope actions while keeping everything local, auditable, and easy to reason about.
31
+
32
+ ---
33
+
34
+ ## Features
35
+
36
+ - **Language-agnostic policy file**
37
+ Human-readable `hydracuda.yaml` drives all decisions. Version-controlled, reviewable, and independent of any specific model or framework.
38
+
39
+ - **Pre-dispatch enforcement**
40
+ Tool calls are intercepted before they execute, not after. The model cannot bypass the decision by reprompting.
41
+
42
+ - **Three decisions: allow, deny, review**
43
+ - `allow` forwards the call to your handler
44
+ - `deny` blocks the call with a clear reason
45
+ - `review` is reserved for high-risk actions that will require human approval in a later version
46
+
47
+ - **Local audit logging**
48
+ Every decision is written to a SQLite audit log on disk. No telemetry, no external service, no cloud dependency.
49
+
50
+ - **Two primary use cases**
51
+ - **Production guardrail** for AI-integrated applications
52
+ - **Engagement scope enforcement** for red teams using AI assistants during assessments
53
+
54
+ ---
55
+
56
+ ## Installation
57
+
58
+ HYDRACUDA targets Python 3.10 and above.
59
+
60
+ ```bash
61
+ pip install hydracuda
62
+ ```
63
+
64
+ To work on the project locally:
65
+
66
+ ```bash
67
+ git clone https://github.com/Xtrinel-Group/HYDRACUDA.git
68
+ cd HYDRACUDA
69
+ pip install -e .
70
+ ```
71
+
72
+ ---
73
+
74
+ ## Quick Start
75
+
76
+ From a new or existing project directory:
77
+
78
+ ```bash
79
+ hydracuda init
80
+ ```
81
+
82
+ This writes a starter `hydracuda.yaml` with commented examples.
83
+
84
+ Validate the policy:
85
+
86
+ ```bash
87
+ hydracuda check hydracuda.yaml
88
+ ```
89
+
90
+ If validation passes, you can integrate HYDRACUDA into your tool-calling layer.
91
+
92
+ ### Minimal integration example
93
+
94
+ ```python
95
+ import asyncio
96
+ from hydracuda.policy import load_policy
97
+ from hydracuda.engine import PolicyEngine
98
+ from hydracuda.proxy import ToolCallProxy
99
+
100
+
101
+ async def handle_tool_call(tool_name: str, params: dict) -> dict:
102
+ # Your existing tool dispatch logic goes here.
103
+ # For example, calling into an MCP server or a local command.
104
+ return {"status": "ok", "tool": tool_name, "params": params}
105
+
106
+
107
+ async def main() -> None:
108
+ policy = load_policy("hydracuda.yaml")
109
+ engine = PolicyEngine(policy)
110
+ proxy = ToolCallProxy(engine, audit_path=policy.audit_path)
111
+
112
+ # This is what your LLM agent would have requested.
113
+ tool_name = "read_file"
114
+ params = {"path": "/tmp/example.txt"}
115
+
116
+ result = await proxy.call(tool_name, params, handler=handle_tool_call)
117
+ print(result)
118
+
119
+
120
+ if __name__ == "__main__":
121
+ asyncio.run(main())
122
+ ```
123
+
124
+ In your real application, the LLM agent calls `proxy.call(...)` instead of invoking tools directly.
125
+
126
+ ---
127
+
128
+ ## Policy File
129
+
130
+ HYDRACUDA policies are defined in YAML. The file created by `hydracuda init` looks similar to this:
131
+
132
+ ```yaml
133
+ version: 1
134
+ mode: enforce # enforce | shadow | review
135
+
136
+ tools:
137
+ read_file:
138
+ allow: true
139
+ parameterRules:
140
+ path:
141
+ denyPatterns:
142
+ - "\\.\\." # block path traversal
143
+ - "/etc/"
144
+ - "/root/"
145
+
146
+ delete_record:
147
+ allow: false
148
+ reason: "Destructive operation. Blocked by default."
149
+
150
+ execute_shell:
151
+ allow: review
152
+ rateLimit: "3/minute"
153
+
154
+ audit:
155
+ path: .hydracuda/audit.db
156
+ ```
157
+
158
+ Key concepts:
159
+
160
+ - **mode**
161
+ - `enforce`: violations are blocked
162
+ - `shadow`: violations are logged but not blocked
163
+ - `review`: intended for future human-approval workflows
164
+
165
+ - **tools**
166
+ Each tool has an `allow` value:
167
+ - `true` → allowed (subject to parameter rules)
168
+ - `false` → always denied
169
+ - `"review"` → queued for human review in a future release
170
+
171
+ - **parameterRules**
172
+ `denyPatterns` are Python regular expressions evaluated against the string value of the parameter.
173
+
174
+ - **audit.path**
175
+ Path to the SQLite audit database. The parent directory is created automatically if it does not exist.
176
+
177
+ ---
178
+
179
+ ## Audit Log
180
+
181
+ HYDRACUDA writes one row per tool call decision to a local SQLite database.
182
+
183
+ Table schema:
184
+
185
+ - `id` (integer primary key)
186
+ - `timestamp` (ISO 8601 string)
187
+ - `tool` (tool name)
188
+ - `action` (`allow`, `deny`, `review`)
189
+ - `reason` (short explanation)
190
+ - `params` (JSON-encoded parameters)
191
+
192
+ This makes it easy to:
193
+
194
+ - Review which tools are actually used in production
195
+ - See which policies are firing most often
196
+ - Build dashboards or alerts on top of the audit data
197
+
198
+ ---
199
+
200
+ ## Relationship to VAAST
201
+
202
+ HYDRACUDA is maintained by Xtrinel, the team behind **VAAST**, an AI security scanner focused on AI attack surfaces and MCP tool-call abuse.
203
+
204
+ - VAAST is **offensive**: it discovers tool-call abuse and prompt injection vulnerabilities in AI-integrated applications before they reach production.
205
+ - HYDRACUDA is **defensive**: it enforces the policies that prevent those same vulnerabilities from being exploited at runtime.
206
+
207
+ They are fully decoupled. HYDRACUDA does not require VAAST, but future versions will support importing VAAST findings to auto-generate policy templates.
208
+
209
+ ---
210
+
211
+ ## Documentation
212
+
213
+ For full documentation, examples, and integration guides:
214
+
215
+ - https://docs.xtrinel.com/hydracuda
216
+
217
+ ---
218
+
219
+ ## Logo and Branding
220
+
221
+ HYDRACUDA assets are available under the Xtrinel brand guidelines:
222
+
223
+ - Full wordmark: `https://assets.xtrinel.com/hydracuda-full.svg`
224
+ - Icon: `https://assets.xtrinel.com/hydracuda.svg`
225
+
226
+ You can use these in dashboards, internal docs, or integrations that surface HYDRACUDA decisions.
227
+
228
+ ---
229
+
230
+ ## Contributing
231
+
232
+ Contributions are welcome.
233
+
234
+ 1. Fork the repository
235
+ 2. Create a feature branch
236
+ 3. Add tests for any new behavior
237
+ 4. Run `pytest`
238
+ 5. Open a pull request with a clear description of the change
239
+
240
+ Please keep new features focused and security-oriented. If you are proposing a change to the policy format, open an issue first for discussion.
241
+
242
+ ---
243
+
244
+ ## License
245
+
246
+ HYDRACUDA is released under the MIT License. See `LICENSE` for details.
@@ -0,0 +1,228 @@
1
+ <p align="center">
2
+ <img src="https://assets.xtrinel.com/hydracuda-full.svg" alt="HYDRACUDA" width="480" />
3
+ </p>
4
+
5
+ # HYDRACUDA
6
+
7
+ Hybrid Runtime Access Control for Untrusted Delegated Actions.
8
+
9
+ HYDRACUDA is a lightweight, open source policy enforcement layer for AI tool calls.
10
+ Define a YAML policy file, drop HYDRACUDA in front of any MCP server or LLM tool layer, and every call is either allowed, denied, or held for review before it executes.
11
+
12
+ The goal is simple: prevent tool-call abuse and out-of-scope actions while keeping everything local, auditable, and easy to reason about.
13
+
14
+ ---
15
+
16
+ ## Features
17
+
18
+ - **Language-agnostic policy file**
19
+ Human-readable `hydracuda.yaml` drives all decisions. Version-controlled, reviewable, and independent of any specific model or framework.
20
+
21
+ - **Pre-dispatch enforcement**
22
+ Tool calls are intercepted before they execute, not after. The model cannot bypass the decision by reprompting.
23
+
24
+ - **Three decisions: allow, deny, review**
25
+ - `allow` forwards the call to your handler
26
+ - `deny` blocks the call with a clear reason
27
+ - `review` is reserved for high-risk actions that will require human approval in a later version
28
+
29
+ - **Local audit logging**
30
+ Every decision is written to a SQLite audit log on disk. No telemetry, no external service, no cloud dependency.
31
+
32
+ - **Two primary use cases**
33
+ - **Production guardrail** for AI-integrated applications
34
+ - **Engagement scope enforcement** for red teams using AI assistants during assessments
35
+
36
+ ---
37
+
38
+ ## Installation
39
+
40
+ HYDRACUDA targets Python 3.10 and above.
41
+
42
+ ```bash
43
+ pip install hydracuda
44
+ ```
45
+
46
+ To work on the project locally:
47
+
48
+ ```bash
49
+ git clone https://github.com/Xtrinel-Group/HYDRACUDA.git
50
+ cd HYDRACUDA
51
+ pip install -e .
52
+ ```
53
+
54
+ ---
55
+
56
+ ## Quick Start
57
+
58
+ From a new or existing project directory:
59
+
60
+ ```bash
61
+ hydracuda init
62
+ ```
63
+
64
+ This writes a starter `hydracuda.yaml` with commented examples.
65
+
66
+ Validate the policy:
67
+
68
+ ```bash
69
+ hydracuda check hydracuda.yaml
70
+ ```
71
+
72
+ If validation passes, you can integrate HYDRACUDA into your tool-calling layer.
73
+
74
+ ### Minimal integration example
75
+
76
+ ```python
77
+ import asyncio
78
+ from hydracuda.policy import load_policy
79
+ from hydracuda.engine import PolicyEngine
80
+ from hydracuda.proxy import ToolCallProxy
81
+
82
+
83
+ async def handle_tool_call(tool_name: str, params: dict) -> dict:
84
+ # Your existing tool dispatch logic goes here.
85
+ # For example, calling into an MCP server or a local command.
86
+ return {"status": "ok", "tool": tool_name, "params": params}
87
+
88
+
89
+ async def main() -> None:
90
+ policy = load_policy("hydracuda.yaml")
91
+ engine = PolicyEngine(policy)
92
+ proxy = ToolCallProxy(engine, audit_path=policy.audit_path)
93
+
94
+ # This is what your LLM agent would have requested.
95
+ tool_name = "read_file"
96
+ params = {"path": "/tmp/example.txt"}
97
+
98
+ result = await proxy.call(tool_name, params, handler=handle_tool_call)
99
+ print(result)
100
+
101
+
102
+ if __name__ == "__main__":
103
+ asyncio.run(main())
104
+ ```
105
+
106
+ In your real application, the LLM agent calls `proxy.call(...)` instead of invoking tools directly.
107
+
108
+ ---
109
+
110
+ ## Policy File
111
+
112
+ HYDRACUDA policies are defined in YAML. The file created by `hydracuda init` looks similar to this:
113
+
114
+ ```yaml
115
+ version: 1
116
+ mode: enforce # enforce | shadow | review
117
+
118
+ tools:
119
+ read_file:
120
+ allow: true
121
+ parameterRules:
122
+ path:
123
+ denyPatterns:
124
+ - "\\.\\." # block path traversal
125
+ - "/etc/"
126
+ - "/root/"
127
+
128
+ delete_record:
129
+ allow: false
130
+ reason: "Destructive operation. Blocked by default."
131
+
132
+ execute_shell:
133
+ allow: review
134
+ rateLimit: "3/minute"
135
+
136
+ audit:
137
+ path: .hydracuda/audit.db
138
+ ```
139
+
140
+ Key concepts:
141
+
142
+ - **mode**
143
+ - `enforce`: violations are blocked
144
+ - `shadow`: violations are logged but not blocked
145
+ - `review`: intended for future human-approval workflows
146
+
147
+ - **tools**
148
+ Each tool has an `allow` value:
149
+ - `true` → allowed (subject to parameter rules)
150
+ - `false` → always denied
151
+ - `"review"` → queued for human review in a future release
152
+
153
+ - **parameterRules**
154
+ `denyPatterns` are Python regular expressions evaluated against the string value of the parameter.
155
+
156
+ - **audit.path**
157
+ Path to the SQLite audit database. The parent directory is created automatically if it does not exist.
158
+
159
+ ---
160
+
161
+ ## Audit Log
162
+
163
+ HYDRACUDA writes one row per tool call decision to a local SQLite database.
164
+
165
+ Table schema:
166
+
167
+ - `id` (integer primary key)
168
+ - `timestamp` (ISO 8601 string)
169
+ - `tool` (tool name)
170
+ - `action` (`allow`, `deny`, `review`)
171
+ - `reason` (short explanation)
172
+ - `params` (JSON-encoded parameters)
173
+
174
+ This makes it easy to:
175
+
176
+ - Review which tools are actually used in production
177
+ - See which policies are firing most often
178
+ - Build dashboards or alerts on top of the audit data
179
+
180
+ ---
181
+
182
+ ## Relationship to VAAST
183
+
184
+ HYDRACUDA is maintained by Xtrinel, the team behind **VAAST**, an AI security scanner focused on AI attack surfaces and MCP tool-call abuse.
185
+
186
+ - VAAST is **offensive**: it discovers tool-call abuse and prompt injection vulnerabilities in AI-integrated applications before they reach production.
187
+ - HYDRACUDA is **defensive**: it enforces the policies that prevent those same vulnerabilities from being exploited at runtime.
188
+
189
+ They are fully decoupled. HYDRACUDA does not require VAAST, but future versions will support importing VAAST findings to auto-generate policy templates.
190
+
191
+ ---
192
+
193
+ ## Documentation
194
+
195
+ For full documentation, examples, and integration guides:
196
+
197
+ - https://docs.xtrinel.com/hydracuda
198
+
199
+ ---
200
+
201
+ ## Logo and Branding
202
+
203
+ HYDRACUDA assets are available under the Xtrinel brand guidelines:
204
+
205
+ - Full wordmark: `https://assets.xtrinel.com/hydracuda-full.svg`
206
+ - Icon: `https://assets.xtrinel.com/hydracuda.svg`
207
+
208
+ You can use these in dashboards, internal docs, or integrations that surface HYDRACUDA decisions.
209
+
210
+ ---
211
+
212
+ ## Contributing
213
+
214
+ Contributions are welcome.
215
+
216
+ 1. Fork the repository
217
+ 2. Create a feature branch
218
+ 3. Add tests for any new behavior
219
+ 4. Run `pytest`
220
+ 5. Open a pull request with a clear description of the change
221
+
222
+ Please keep new features focused and security-oriented. If you are proposing a change to the policy format, open an issue first for discussion.
223
+
224
+ ---
225
+
226
+ ## License
227
+
228
+ HYDRACUDA is released under the MIT License. See `LICENSE` for details.
@@ -0,0 +1,10 @@
1
+ # baracuda (deprecated)
2
+
3
+ This package has been renamed to **hydracuda**.
4
+
5
+ Please uninstall this package and install the new one:
6
+
7
+ ```
8
+ pip uninstall baracuda
9
+ pip install hydracuda
10
+ ```
@@ -0,0 +1,7 @@
1
+ raise ImportError(
2
+ "\n\nThe 'baracuda' package has been renamed to 'hydracuda'.\n"
3
+ "Please update your environment:\n\n"
4
+ " pip uninstall baracuda\n"
5
+ " pip install hydracuda\n\n"
6
+ "GitHub: https://github.com/Xtrinel-Group/HYDRACUDA\n"
7
+ )
@@ -0,0 +1,16 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "baracuda"
7
+ version = "0.2.0"
8
+ description = "Deprecated. This package has been renamed to hydracuda. Run: pip install hydracuda"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.10"
12
+ dependencies = []
13
+
14
+ [project.urls]
15
+ Homepage = "https://xtrinel.com/hydracuda"
16
+ Repository = "https://github.com/Xtrinel-Group/HYDRACUDA"
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "hydracuda"
7
+ version = "0.1.0"
8
+ description = "Hybrid Runtime Access Control for Untrusted Delegated Actions. Runtime policy enforcement proxy for AI tool calls."
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ requires-python = ">=3.10"
12
+ keywords = ["ai", "security", "mcp", "llm", "guardrails", "policy"]
13
+ classifiers = [
14
+ "License :: OSI Approved :: MIT License",
15
+ "Programming Language :: Python :: 3",
16
+ "Topic :: Security",
17
+ "Development Status :: 3 - Alpha",
18
+ ]
19
+ dependencies = [
20
+ "pyyaml>=6.0",
21
+ "aiosqlite>=0.19",
22
+ ]
23
+
24
+ [project.urls]
25
+ Homepage = "https://github.com/Xtrinel-Group/HYDRACUDA"
26
+ Issues = "https://github.com/Xtrinel-Group/HYDRACUDA/issues"
27
+
28
+ [project.scripts]
29
+ hydracuda = "hydracuda.cli:main"
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ packages = ["src/hydracuda"]
@@ -0,0 +1,9 @@
1
+ """HYDRACUDA - Runtime policy enforcement for AI tool calls."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from hydracuda.engine import PolicyEngine
6
+ from hydracuda.policy import load_policy
7
+ from hydracuda.proxy import ToolCallProxy
8
+
9
+ __all__ = ["PolicyEngine", "load_policy", "ToolCallProxy"]
@@ -0,0 +1,87 @@
1
+ """CLI interface for HYDRACUDA."""
2
+
3
+ import argparse
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from hydracuda.policy import load_policy
8
+
9
+ STARTER_POLICY = """\
10
+ version: 1
11
+ mode: enforce
12
+ audit_path: .hydracuda/audit.db
13
+
14
+ tools:
15
+ read_file:
16
+ allow: true
17
+ parameter_rules:
18
+ path:
19
+ deny_patterns:
20
+ - "/etc/"
21
+ - "\\\\.\\\\."
22
+ - "/root/"
23
+
24
+ delete_record:
25
+ allow: false
26
+ reason: "Destructive operation. Blocked by default."
27
+
28
+ execute_shell:
29
+ allow: "review"
30
+ rate_limit: "3/minute"
31
+
32
+ audit:
33
+ path: .hydracuda/audit.db
34
+ """
35
+
36
+
37
+ def cmd_init(args: argparse.Namespace) -> None:
38
+ """Write a starter hydracuda.yaml to the current directory."""
39
+ target = Path("hydracuda.yaml")
40
+ if target.exists():
41
+ print(f"{target} already exists. Not overwriting.")
42
+ return
43
+
44
+ target.write_text(STARTER_POLICY)
45
+ print(f"Created {target} with starter policy. Edit it to match your tools.")
46
+
47
+
48
+ def cmd_check(args: argparse.Namespace) -> None:
49
+ """Validate a policy file and print a summary."""
50
+ try:
51
+ policy = load_policy(args.policy_file)
52
+ except ValueError as e:
53
+ print(f"Validation failed: {e}")
54
+ sys.exit(1)
55
+
56
+ tool_count = len(policy.tools)
57
+ print(f"Policy valid. Mode: {policy.mode}, Tools configured: {tool_count}")
58
+
59
+
60
+ def main() -> None:
61
+ """Entry point for the hydracuda CLI."""
62
+ parser = argparse.ArgumentParser(
63
+ prog="hydracuda",
64
+ description="HYDRACUDA - Runtime policy enforcement for AI tool calls.",
65
+ )
66
+ subparsers = parser.add_subparsers(dest="command")
67
+
68
+ subparsers.add_parser("init", help="Create a starter hydracuda.yaml in the current directory")
69
+
70
+ check_parser = subparsers.add_parser("check", help="Validate a policy file")
71
+ check_parser.add_argument("policy_file", help="Path to the policy YAML file")
72
+
73
+ args = parser.parse_args()
74
+
75
+ if args.command is None:
76
+ parser.print_usage()
77
+ sys.exit(0)
78
+
79
+ commands = {
80
+ "init": cmd_init,
81
+ "check": cmd_check,
82
+ }
83
+ commands[args.command](args)
84
+
85
+
86
+ if __name__ == "__main__":
87
+ main()
@@ -0,0 +1,73 @@
1
+ """Policy evaluation core for HYDRACUDA."""
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+
6
+ from hydracuda.policy import Policy
7
+
8
+
9
+ @dataclass
10
+ class Decision:
11
+ """Result of evaluating a tool call against a policy."""
12
+
13
+ action: str
14
+ reason: str
15
+ tool: str
16
+ params: dict
17
+
18
+
19
+ class PolicyEngine:
20
+ """Evaluates tool calls against a loaded policy."""
21
+
22
+ def __init__(self, policy: Policy):
23
+ self.policy = policy
24
+
25
+ def evaluate(self, tool_name: str, params: dict) -> Decision:
26
+ """Evaluate a tool call and return an allow/deny/review decision."""
27
+ if tool_name not in self.policy.tools:
28
+ return Decision(
29
+ action="deny",
30
+ reason=f"{tool_name}: not listed in policy — default deny",
31
+ tool=tool_name,
32
+ params=params,
33
+ )
34
+
35
+ tool_policy = self.policy.tools[tool_name]
36
+
37
+ if tool_policy.allow is False:
38
+ return Decision(
39
+ action="deny",
40
+ reason="tool blocked by policy",
41
+ tool=tool_name,
42
+ params=params,
43
+ )
44
+
45
+ if tool_policy.allow == "review":
46
+ return Decision(
47
+ action="review",
48
+ reason="tool requires human review",
49
+ tool=tool_name,
50
+ params=params,
51
+ )
52
+
53
+ if tool_policy.parameter_rules:
54
+ for param_name, rules in tool_policy.parameter_rules.items():
55
+ if param_name not in params:
56
+ continue
57
+ deny_patterns = rules.get("deny_patterns", [])
58
+ param_value = str(params[param_name])
59
+ for pattern in deny_patterns:
60
+ if re.search(pattern, param_value):
61
+ return Decision(
62
+ action="deny",
63
+ reason=f"{tool_name}: parameter '{param_name}' matched deny pattern '{pattern}'",
64
+ tool=tool_name,
65
+ params=params,
66
+ )
67
+
68
+ return Decision(
69
+ action="allow",
70
+ reason="all policy checks passed",
71
+ tool=tool_name,
72
+ params=params,
73
+ )
@@ -0,0 +1,84 @@
1
+ """YAML policy parser and validator for HYDRACUDA."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+ from typing import Union
6
+
7
+ import yaml
8
+
9
+
10
+ @dataclass
11
+ class ToolPolicy:
12
+ """Policy configuration for a single tool."""
13
+
14
+ allow: Union[bool, str] = True
15
+ rate_limit: str | None = None
16
+ parameter_rules: dict[str, dict] | None = None
17
+
18
+
19
+ @dataclass
20
+ class Policy:
21
+ """Top-level policy configuration parsed from hydracuda.yaml."""
22
+
23
+ version: int
24
+ tools: dict[str, ToolPolicy]
25
+ mode: str = "enforce"
26
+ audit_path: str = ".hydracuda/audit.db"
27
+
28
+
29
+ VALID_MODES = {"enforce", "shadow", "review"}
30
+
31
+
32
+ def load_policy(path: str) -> Policy:
33
+ """Load and validate a hydracuda.yaml policy file.
34
+
35
+ Raises ValueError with a descriptive message on invalid input.
36
+ """
37
+ policy_path = Path(path)
38
+ if not policy_path.exists():
39
+ raise ValueError(f"Policy file not found: {path}")
40
+
41
+ with open(policy_path) as f:
42
+ raw = yaml.safe_load(f)
43
+
44
+ if not isinstance(raw, dict):
45
+ raise ValueError(f"Policy file must be a YAML mapping, got {type(raw).__name__}")
46
+
47
+ if "version" not in raw:
48
+ raise ValueError("Policy file missing required key: 'version'")
49
+
50
+ version = raw["version"]
51
+ if not isinstance(version, int):
52
+ raise ValueError(f"'version' must be an integer, got {type(version).__name__}")
53
+
54
+ mode = raw.get("mode", "enforce")
55
+ if mode not in VALID_MODES:
56
+ raise ValueError(f"'mode' must be one of {sorted(VALID_MODES)}, got '{mode}'")
57
+
58
+ if "tools" not in raw:
59
+ raise ValueError("Policy file missing required key: 'tools'")
60
+
61
+ raw_tools = raw["tools"]
62
+ if not isinstance(raw_tools, dict):
63
+ raise ValueError("'tools' must be a mapping of tool names to configurations")
64
+
65
+ tools: dict[str, ToolPolicy] = {}
66
+ for tool_name, tool_conf in raw_tools.items():
67
+ if not isinstance(tool_conf, dict):
68
+ raise ValueError(f"Tool '{tool_name}' configuration must be a mapping")
69
+
70
+ allow = tool_conf.get("allow", True)
71
+ if allow not in (True, False, "review"):
72
+ raise ValueError(
73
+ f"Tool '{tool_name}': 'allow' must be true, false, or 'review', got '{allow}'"
74
+ )
75
+
76
+ tools[tool_name] = ToolPolicy(
77
+ allow=allow,
78
+ rate_limit=tool_conf.get("rate_limit"),
79
+ parameter_rules=tool_conf.get("parameter_rules"),
80
+ )
81
+
82
+ audit_path = raw.get("audit_path", ".hydracuda/audit.db")
83
+
84
+ return Policy(version=version, mode=mode, tools=tools, audit_path=audit_path)
@@ -0,0 +1,58 @@
1
+ """Tool call interception layer for HYDRACUDA."""
2
+
3
+ import json
4
+ from datetime import datetime, timezone
5
+ from pathlib import Path
6
+
7
+ import aiosqlite
8
+
9
+ from hydracuda.engine import PolicyEngine
10
+
11
+
12
+ class ToolCallProxy:
13
+ """Intercepts tool calls, enforces policy, and writes an audit log."""
14
+
15
+ def __init__(self, engine: PolicyEngine, audit_path: str | None = None):
16
+ self.engine = engine
17
+ self.audit_path = audit_path or engine.policy.audit_path
18
+
19
+ async def call(self, tool_name: str, params: dict, handler) -> dict:
20
+ """Evaluate, log, and optionally execute a tool call."""
21
+ decision = self.engine.evaluate(tool_name, params)
22
+ await self._write_audit(decision)
23
+
24
+ if decision.action == "deny":
25
+ raise PermissionError(decision.reason)
26
+
27
+ if decision.action == "review":
28
+ raise NotImplementedError(f"tool call queued for human review: {tool_name}")
29
+
30
+ return await handler(tool_name, params)
31
+
32
+ async def _write_audit(self, decision) -> None:
33
+ """Append a decision to the SQLite audit log."""
34
+ db_path = Path(self.audit_path)
35
+ db_path.parent.mkdir(parents=True, exist_ok=True)
36
+
37
+ async with aiosqlite.connect(str(db_path)) as db:
38
+ await db.execute(
39
+ """CREATE TABLE IF NOT EXISTS audit_log (
40
+ id INTEGER PRIMARY KEY,
41
+ timestamp TEXT,
42
+ tool TEXT,
43
+ action TEXT,
44
+ reason TEXT,
45
+ params TEXT
46
+ )"""
47
+ )
48
+ await db.execute(
49
+ "INSERT INTO audit_log (timestamp, tool, action, reason, params) VALUES (?, ?, ?, ?, ?)",
50
+ (
51
+ datetime.now(timezone.utc).isoformat(),
52
+ decision.tool,
53
+ decision.action,
54
+ decision.reason,
55
+ json.dumps(decision.params),
56
+ ),
57
+ )
58
+ await db.commit()
@@ -0,0 +1,90 @@
1
+ """Unit tests for the HYDRACUDA policy engine."""
2
+
3
+ import pytest
4
+
5
+ from hydracuda.engine import PolicyEngine
6
+ from hydracuda.policy import Policy, ToolPolicy
7
+
8
+
9
+ def make_policy(tools: dict[str, ToolPolicy]) -> Policy:
10
+ return Policy(version=1, tools=tools)
11
+
12
+
13
+ def test_unconfigured_tool_defaults_to_deny():
14
+ engine = PolicyEngine(make_policy({}))
15
+ decision = engine.evaluate("unknown_tool", {"arg": "value"})
16
+
17
+ assert decision.action == "deny"
18
+ assert "not listed in policy" in decision.reason
19
+
20
+
21
+ def test_tool_explicitly_denied():
22
+ engine = PolicyEngine(make_policy({
23
+ "dangerous_tool": ToolPolicy(allow=False),
24
+ }))
25
+ decision = engine.evaluate("dangerous_tool", {})
26
+
27
+ assert decision.action == "deny"
28
+ assert decision.reason == "tool blocked by policy"
29
+
30
+
31
+ def test_tool_set_to_review():
32
+ engine = PolicyEngine(make_policy({
33
+ "sensitive_tool": ToolPolicy(allow="review"),
34
+ }))
35
+ decision = engine.evaluate("sensitive_tool", {"x": 1})
36
+
37
+ assert decision.action == "review"
38
+
39
+
40
+ def test_deny_pattern_match():
41
+ engine = PolicyEngine(make_policy({
42
+ "read_file": ToolPolicy(
43
+ allow=True,
44
+ parameter_rules={
45
+ "path": {"deny_patterns": ["/etc/", "\\.\\."]}
46
+ },
47
+ ),
48
+ }))
49
+ decision = engine.evaluate("read_file", {"path": "/etc/passwd"})
50
+
51
+ assert decision.action == "deny"
52
+ assert "read_file" in decision.reason
53
+ assert "path" in decision.reason
54
+ assert "/etc/" in decision.reason
55
+
56
+
57
+ def test_no_deny_pattern_match_returns_allow():
58
+ engine = PolicyEngine(make_policy({
59
+ "read_file": ToolPolicy(
60
+ allow=True,
61
+ parameter_rules={
62
+ "path": {"deny_patterns": ["/etc/", "\\.\\."]}
63
+ },
64
+ ),
65
+ }))
66
+ decision = engine.evaluate("read_file", {"path": "/home/user/notes.txt"})
67
+
68
+ assert decision.action == "allow"
69
+ assert decision.reason == "all policy checks passed"
70
+
71
+
72
+ def test_unlisted_tool_denied_by_default():
73
+ engine = PolicyEngine(make_policy({
74
+ "read_file": ToolPolicy(allow=True),
75
+ }))
76
+ decision = engine.evaluate("send_email", {"to": "admin@corp.com"})
77
+
78
+ assert decision.action == "deny"
79
+ assert "send_email" in decision.reason
80
+ assert "default deny" in decision.reason
81
+
82
+
83
+ def test_delete_record_blocked():
84
+ engine = PolicyEngine(make_policy({
85
+ "delete_record": ToolPolicy(allow=False),
86
+ }))
87
+ decision = engine.evaluate("delete_record", {"id": "usr_123"})
88
+
89
+ assert decision.action == "deny"
90
+ assert decision.reason == "tool blocked by policy"