agentwall 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.
- agentwall-0.1.0/.gitignore +5 -0
- agentwall-0.1.0/PKG-INFO +642 -0
- agentwall-0.1.0/PLAN.md +1023 -0
- agentwall-0.1.0/docs/README.md +612 -0
- agentwall-0.1.0/pyproject.toml +53 -0
- agentwall-0.1.0/setup.sh +154 -0
- agentwall-0.1.0/src/agentfirewall/__init__.py +3 -0
- agentwall-0.1.0/src/agentfirewall/agents/__init__.py +0 -0
- agentwall-0.1.0/src/agentfirewall/audit.py +97 -0
- agentwall-0.1.0/src/agentfirewall/cli.py +379 -0
- agentwall-0.1.0/src/agentfirewall/engine.py +199 -0
- agentwall-0.1.0/src/agentfirewall/hooks/__init__.py +0 -0
- agentwall-0.1.0/src/agentfirewall/hooks/shell.py +123 -0
- agentwall-0.1.0/src/agentfirewall/network/__init__.py +0 -0
- agentwall-0.1.0/src/agentfirewall/presets/__init__.py +102 -0
- agentwall-0.1.0/src/agentfirewall/process.py +78 -0
- agentwall-0.1.0/src/agentfirewall/sandbox.py +485 -0
- agentwall-0.1.0/src/agentfirewall/schema.py +246 -0
- agentwall-0.1.0/src/agentfirewall/watchers/__init__.py +0 -0
- agentwall-0.1.0/src/agentfirewall/watchers/filesystem.py +135 -0
- agentwall-0.1.0/test.sh +17 -0
- agentwall-0.1.0/tests/__init__.py +0 -0
- agentwall-0.1.0/tests/test_audit.py +127 -0
- agentwall-0.1.0/tests/test_cli.py +124 -0
- agentwall-0.1.0/tests/test_cli_phase2.py +83 -0
- agentwall-0.1.0/tests/test_cli_protect.py +173 -0
- agentwall-0.1.0/tests/test_engine.py +206 -0
- agentwall-0.1.0/tests/test_hooks.py +135 -0
- agentwall-0.1.0/tests/test_presets.py +39 -0
- agentwall-0.1.0/tests/test_process.py +119 -0
- agentwall-0.1.0/tests/test_sandbox.py +539 -0
- agentwall-0.1.0/tests/test_schema.py +166 -0
- agentwall-0.1.0/tests/test_watcher.py +178 -0
agentwall-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentwall
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A dotfile-driven firewall that protects the OS from destructive LLM agent tool calls
|
|
5
|
+
Author: Wissam El-Labban, prad
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: agent,ai-safety,firewall,llm,security
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: MacOS
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Security
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: click>=8.0
|
|
21
|
+
Requires-Dist: psutil>=5.9
|
|
22
|
+
Requires-Dist: pyyaml>=6.0
|
|
23
|
+
Requires-Dist: watchdog>=3.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
27
|
+
Provides-Extra: sandbox
|
|
28
|
+
Requires-Dist: fusepy>=3.0; extra == 'sandbox'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# Agent Firewall — Technical Documentation
|
|
32
|
+
|
|
33
|
+
## Overview
|
|
34
|
+
|
|
35
|
+
`agentfirewall` is a filesystem-level security tool that protects your OS from destructive LLM agent tool calls. It works as a hidden `.agentfirewall/` directory (similar to `.git/`) placed at the root of any project, defining rules that govern what commands, file operations, and network connections an agent is allowed to perform.
|
|
36
|
+
|
|
37
|
+
**Current status:** Phase 1 (static rule checker) and Phase 2 (real-time OS enforcement) are complete. The tool evaluates actions against YAML-defined rules, returns allow/deny/warn verdicts, monitors the filesystem in real time, intercepts shell commands before execution, and kills offending agent processes.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## How It Works — Two-Layer Defense
|
|
42
|
+
|
|
43
|
+
The firewall protects your project through two independent layers that work together:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
Agent tries something destructive
|
|
47
|
+
│
|
|
48
|
+
┌───────▼────────┐
|
|
49
|
+
│ LAYER 1: │ Shell hooks intercept every command BEFORE it runs.
|
|
50
|
+
│ Prevention │ bash DEBUG trap / zsh preexec calls "agentfirewall check".
|
|
51
|
+
│ (hooks/) │ If denied → command never executes.
|
|
52
|
+
└───────┬────────┘
|
|
53
|
+
│ command allowed (or agent bypasses shell entirely,
|
|
54
|
+
│ e.g. Python os.remove(), Node fs.unlink())
|
|
55
|
+
┌───────▼────────┐
|
|
56
|
+
│ LAYER 2: │ Filesystem watcher (watchdog) monitors all file events.
|
|
57
|
+
│ Detection & │ Evaluates each event against engine rules.
|
|
58
|
+
│ Retaliation │ If denied → kills the agent process via psutil.
|
|
59
|
+
│ (watchers/) │
|
|
60
|
+
└───────┬────────┘
|
|
61
|
+
│
|
|
62
|
+
┌───────▼────────┐
|
|
63
|
+
│ Audit Log │ Both layers log every decision (allow/deny/warn)
|
|
64
|
+
│ (audit.py) │ as structured JSON to .agentfirewall/logs/firewall.log
|
|
65
|
+
└────────────────┘
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Why two layers?** Shell hooks can only intercept commands typed into a shell. If an LLM agent uses a programming language's file APIs directly (e.g., `os.remove()` in Python, `fs.unlinkSync()` in Node), the shell never sees it. The filesystem watcher catches those operations at the OS level.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Architecture
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
┌──────────────────────────────────────────────────────────┐
|
|
76
|
+
│ cli.py │
|
|
77
|
+
│ (Click commands: init, check, status, watch, hooks) │
|
|
78
|
+
└──┬────────┬────────┬──────────┬──────────┬───────────────┘
|
|
79
|
+
│ │ │ │ │
|
|
80
|
+
│ uses │ uses │ uses │ uses │ uses
|
|
81
|
+
│ │ │ │ │
|
|
82
|
+
▼ │ ▼ ▼ ▼
|
|
83
|
+
presets/ │ hooks/ watchers/ process.py
|
|
84
|
+
__init__.py │ shell.py filesystem (kill agents)
|
|
85
|
+
(rule sets) │ (bash/zsh .py │
|
|
86
|
+
│ hooks) (watchdog) │
|
|
87
|
+
│ │ ┌─────┘
|
|
88
|
+
▼ ▼ ▼
|
|
89
|
+
┌──────────────────────────────────┐
|
|
90
|
+
│ engine.py │
|
|
91
|
+
│ (Rule evaluation — verdicts) │
|
|
92
|
+
│ ▲ │ │
|
|
93
|
+
│ uses │ logs │ │
|
|
94
|
+
│ │ ▼ │
|
|
95
|
+
│ schema.py audit.py │
|
|
96
|
+
│ (YAML config) (JSON logger) │
|
|
97
|
+
└──────────────────────────────────┘
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Dependencies flow **one direction only** — no circular imports:
|
|
101
|
+
- `schema.py` → foundation (no internal imports)
|
|
102
|
+
- `engine.py` → imports `schema`
|
|
103
|
+
- `audit.py` → imports `engine` (for `RuleResult` type)
|
|
104
|
+
- `process.py` → standalone (uses `psutil`)
|
|
105
|
+
- `hooks/shell.py` → standalone (reads `$SHELL`, writes RC files)
|
|
106
|
+
- `watchers/filesystem.py` → imports `engine`, `schema`, `process`
|
|
107
|
+
- `cli.py` → imports everything above
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## `.agentfirewall/` Directory Structure
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
.agentfirewall/
|
|
115
|
+
├── config.yaml # Main configuration (rules, mode, sandbox settings)
|
|
116
|
+
├── rules/ # Custom rule files (reserved for future use)
|
|
117
|
+
├── logs/ # Audit logs (firewall.log lives here)
|
|
118
|
+
│ └── firewall.log # Structured JSON log — one entry per line
|
|
119
|
+
├── hooks/ # Shell hook scripts (reserved for future use)
|
|
120
|
+
└── plugins/ # Extension plugins (reserved for future use)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Created by `agentfirewall init`. The directory is discovered by walking up from the current working directory (like `.git/`).
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Core Files
|
|
128
|
+
|
|
129
|
+
### `schema.py` — Data Model
|
|
130
|
+
|
|
131
|
+
Defines all data structures and handles YAML config parsing/validation/serialization.
|
|
132
|
+
|
|
133
|
+
**Enums:**
|
|
134
|
+
- `FirewallMode` — `ENFORCE` | `AUDIT` | `OFF`
|
|
135
|
+
- ENFORCE: deny actions that violate rules
|
|
136
|
+
- AUDIT: log violations as warnings but allow them
|
|
137
|
+
- OFF: disable all checks
|
|
138
|
+
- `DenyOperation` — `DELETE` | `CHMOD` | `MOVE_OUTSIDE_SANDBOX` | `WRITE`
|
|
139
|
+
|
|
140
|
+
**Dataclasses (the config tree):**
|
|
141
|
+
- `FirewallConfig` — top-level container (version, mode, and all sub-configs)
|
|
142
|
+
- `SandboxConfig` — defines the allowed working directory (`root`) and whether escaping is permitted
|
|
143
|
+
- `FilesystemConfig` — protected path patterns (globs) and which operations are denied on them
|
|
144
|
+
- `CommandsConfig` — blocklist and allowlist of shell command patterns
|
|
145
|
+
- `NetworkConfig` — allowed hosts, denied egress targets, max upload size
|
|
146
|
+
- `LoggingConfig` — log file location and level
|
|
147
|
+
|
|
148
|
+
**Constants:**
|
|
149
|
+
- `DOTFILE_NAME = ".agentfirewall"` — directory name
|
|
150
|
+
- `CONFIG_FILENAME = "config.yaml"` — config file within the directory
|
|
151
|
+
- `SUBDIRS = ["rules", "logs", "hooks", "plugins"]` — subdirectories created on init
|
|
152
|
+
|
|
153
|
+
**Key Functions:**
|
|
154
|
+
- `find_config(start_dir)` — walks up from `start_dir` (default: CWD) looking for a `.agentfirewall/` directory containing `config.yaml`. Returns the path to `config.yaml` or `None`.
|
|
155
|
+
- `load_config(path)` — reads and validates a YAML file into a `FirewallConfig`. Uses `yaml.safe_load` for security. Raises `ConfigError` on invalid input.
|
|
156
|
+
- `default_config()` — returns a `FirewallConfig` with sensible defaults (15 blocklist patterns, 3 allowed hosts).
|
|
157
|
+
- `config_to_yaml(config)` — serializes a `FirewallConfig` back to YAML text.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
### `engine.py` — Rule Evaluation
|
|
162
|
+
|
|
163
|
+
The decision-making core. Takes a `FirewallConfig` and evaluates actions against it.
|
|
164
|
+
|
|
165
|
+
**Data Types:**
|
|
166
|
+
- `Verdict` enum — `ALLOW` | `DENY` | `WARN`
|
|
167
|
+
- `RuleResult` dataclass — contains `verdict`, `rule` (which rule triggered), `detail` (human-readable explanation), and a `blocked` property (True only for DENY)
|
|
168
|
+
|
|
169
|
+
**`Engine` class:**
|
|
170
|
+
|
|
171
|
+
Constructor takes a `FirewallConfig` and an optional `AuditLogger`. It pre-compiles all blocklist/allowlist patterns into regex objects for performance. When an `AuditLogger` is provided, every evaluation call automatically logs the decision.
|
|
172
|
+
|
|
173
|
+
Three evaluation methods:
|
|
174
|
+
|
|
175
|
+
1. **`evaluate_command(command)`** — checks a shell command string
|
|
176
|
+
- If mode is OFF → ALLOW
|
|
177
|
+
- If allowlist is non-empty and command doesn't match → DENY
|
|
178
|
+
- If command matches any blocklist pattern → DENY
|
|
179
|
+
- Otherwise → ALLOW
|
|
180
|
+
- Allowlist takes priority (checked first)
|
|
181
|
+
|
|
182
|
+
2. **`evaluate_file_operation(operation, path)`** — checks a filesystem operation
|
|
183
|
+
- If mode is OFF → ALLOW
|
|
184
|
+
- Sandbox boundary check: resolves the path against `sandbox.root` and verifies it stays within bounds. Relative paths are resolved against the sandbox root (not CWD) to prevent bypass.
|
|
185
|
+
- Protected paths check: if the operation is in `deny_operations` and the path matches any `protected_paths` glob → DENY
|
|
186
|
+
- Otherwise → ALLOW
|
|
187
|
+
|
|
188
|
+
3. **`evaluate_network(host)`** — checks an outbound connection
|
|
189
|
+
- If mode is OFF → ALLOW
|
|
190
|
+
- If host is in `deny_egress_to` → DENY
|
|
191
|
+
- If `allowed_hosts` is set and host isn't listed → DENY
|
|
192
|
+
- Otherwise → ALLOW
|
|
193
|
+
|
|
194
|
+
**Internal helpers:**
|
|
195
|
+
- `_make_result()` — in AUDIT mode, downgrades DENY → WARN (with `[AUDIT]` prefix in detail)
|
|
196
|
+
- `_compile_command_pattern()` — converts glob-style patterns (e.g., `rm -rf *`) to regex by escaping everything except `*`, then replacing `*` with `.*`
|
|
197
|
+
- `_path_matches()` — matches paths against glob patterns using both the full path and just the filename
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
### `cli.py` — Command-Line Interface
|
|
202
|
+
|
|
203
|
+
Click-based CLI with 10 commands:
|
|
204
|
+
|
|
205
|
+
| Command | Description | Exit Code |
|
|
206
|
+
|---------|-------------|-----------|
|
|
207
|
+
| `protect` | One-command setup: init + install hooks + start background watcher | 0 or 1 |
|
|
208
|
+
| `unprotect` | One-command teardown: stop watcher + remove hooks | 0 |
|
|
209
|
+
| `init` | Create `.agentfirewall/` with config and subdirectories | 0 or 1 |
|
|
210
|
+
| `check <command>` | Dry-run: check if a shell command would be allowed | 0=allow, 1=deny |
|
|
211
|
+
| `check-file <path>` | Dry-run: check if a file operation would be allowed | 0=allow, 1=deny |
|
|
212
|
+
| `check-network <host>` | Dry-run: check if a network connection would be allowed | 0=allow, 1=deny |
|
|
213
|
+
| `status` | Show current firewall config, watcher state, and hooks state | 0 |
|
|
214
|
+
| `watch` | Start real-time filesystem monitoring (Ctrl+C to stop) | 0 or 1 |
|
|
215
|
+
| `install-hooks` | Install shell preexec hooks for command interception | 0 |
|
|
216
|
+
| `uninstall-hooks` | Remove shell preexec hooks | 0 |
|
|
217
|
+
|
|
218
|
+
**`protect` command details:**
|
|
219
|
+
- Accepts `--preset` (standard/strict/permissive), `--shell` (bash/zsh), and `--force` flags
|
|
220
|
+
- Runs `init`, installs shell hooks, and starts `watch` as a background process
|
|
221
|
+
- Saves the watcher PID to `.agentfirewall/watcher.pid` so `unprotect` can stop it later
|
|
222
|
+
- Errors if `.agentfirewall/` already exists (unless `--force`)
|
|
223
|
+
|
|
224
|
+
**`unprotect` command details:**
|
|
225
|
+
- Reads `.agentfirewall/watcher.pid` and sends SIGTERM to stop the background watcher
|
|
226
|
+
- Removes shell hooks from `~/.bashrc` or `~/.zshrc`
|
|
227
|
+
- `--remove-config` flag also deletes the `.agentfirewall/` directory entirely
|
|
228
|
+
- Handles gracefully if watcher is already stopped or PID file is missing
|
|
229
|
+
|
|
230
|
+
**`init` command details:**
|
|
231
|
+
- Accepts `--preset` (standard/strict/permissive) and `--force` flags
|
|
232
|
+
- Creates the `.agentfirewall/` directory + all subdirectories (`rules/`, `logs/`, `hooks/`, `plugins/`)
|
|
233
|
+
- Writes `config.yaml` from the selected preset
|
|
234
|
+
- Errors if directory already exists (unless `--force`)
|
|
235
|
+
|
|
236
|
+
**`watch` command details:**
|
|
237
|
+
- Loads the nearest `.agentfirewall/` config
|
|
238
|
+
- Creates an `AuditLogger`, wires it into an `Engine`, creates a `ProcessKiller`
|
|
239
|
+
- Starts a `FirewallObserver` that monitors the sandbox directory
|
|
240
|
+
- Runs in the foreground until Ctrl+C
|
|
241
|
+
|
|
242
|
+
**`install-hooks` / `uninstall-hooks` details:**
|
|
243
|
+
- Accept `--shell bash|zsh` (auto-detected if omitted)
|
|
244
|
+
- Install appends a guard-marked hook block to `~/.bashrc` or `~/.zshrc`
|
|
245
|
+
- Uninstall removes only the guard-marked block, preserving all other content
|
|
246
|
+
|
|
247
|
+
**`_load_engine()`** — shared helper that calls `find_config()` then `load_config()` then creates an `Engine`. Returns `None` if no config found.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
### `audit.py` — Structured Audit Logging
|
|
252
|
+
|
|
253
|
+
Writes every firewall decision (allow, deny, warn) as a JSON log entry to `.agentfirewall/logs/firewall.log`.
|
|
254
|
+
|
|
255
|
+
**`AuditLogger` class:**
|
|
256
|
+
- Constructor: `AuditLogger(config: LoggingConfig, base_dir: Path)`
|
|
257
|
+
- Creates the log directory if it doesn't exist
|
|
258
|
+
- Uses Python's `RotatingFileHandler` (5 MB per file, 3 backup files)
|
|
259
|
+
- Uses a custom `_JsonFormatter` that outputs one JSON object per line
|
|
260
|
+
|
|
261
|
+
**Log entry format (one per line):**
|
|
262
|
+
```json
|
|
263
|
+
{"timestamp": "2026-03-27T14:30:00Z", "action_type": "command", "target": "rm -rf /", "verdict": "deny", "rule": "blocklist", "detail": "Matches blocklist pattern: rm -rf /"}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**Log level filtering:**
|
|
267
|
+
The `level` in `LoggingConfig` controls which verdicts are logged:
|
|
268
|
+
- `"info"` — logs everything (ALLOW + DENY + WARN)
|
|
269
|
+
- `"warn"` (default) — logs DENY and WARN only, skips ALLOW
|
|
270
|
+
- `"error"` — logs DENY only
|
|
271
|
+
|
|
272
|
+
If `logging.enabled` is `false`, the logger is created but writes nothing.
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
### `process.py` — Agent Process Killer
|
|
277
|
+
|
|
278
|
+
Identifies and terminates running LLM agent processes. Called by the filesystem watcher when a rule violation is detected.
|
|
279
|
+
|
|
280
|
+
**`ProcessKiller` class:**
|
|
281
|
+
- Constructor: `ProcessKiller(signatures: list[str] | None = None)` — custom signatures override defaults
|
|
282
|
+
- `find_agent_processes()` — scans all running processes via `psutil`
|
|
283
|
+
- `kill_agents()` — finds and terminates agent processes, returns count killed
|
|
284
|
+
|
|
285
|
+
**Agent identification — two tiers:**
|
|
286
|
+
|
|
287
|
+
*Exact signatures* (process name contains any of these):
|
|
288
|
+
`claude`, `cursor`, `copilot-agent`, `windsurf`, `aider`
|
|
289
|
+
|
|
290
|
+
*Broad signatures* (process name matches, but only if the full command line also contains an agent keyword):
|
|
291
|
+
`node`, `code` — these are too common on their own, so the killer checks the full command line for agent-related strings before killing.
|
|
292
|
+
|
|
293
|
+
**Safety guards:**
|
|
294
|
+
- Never kills PID 0 or PID 1 (init/kernel)
|
|
295
|
+
- Never kills its own process or its parent process
|
|
296
|
+
- Gracefully handles `NoSuchProcess` (process died between scan and kill)
|
|
297
|
+
- Gracefully handles `AccessDenied` (insufficient permissions)
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
### `watchers/filesystem.py` — Real-Time Filesystem Monitor
|
|
302
|
+
|
|
303
|
+
Uses the `watchdog` library to monitor directory trees for file events, then evaluates each event against the engine rules.
|
|
304
|
+
|
|
305
|
+
**`FirewallHandler(FileSystemEventHandler)` class:**
|
|
306
|
+
- Receives filesystem events from watchdog
|
|
307
|
+
- Maps events to engine operations:
|
|
308
|
+
|
|
309
|
+
| Watchdog Event | → | Engine DenyOperation |
|
|
310
|
+
|----------------|---|---------------------|
|
|
311
|
+
| `FileDeletedEvent` | → | `DELETE` |
|
|
312
|
+
| `FileModifiedEvent` | → | `WRITE` |
|
|
313
|
+
| `FileMovedEvent` | → | `MOVE_OUTSIDE_SANDBOX` |
|
|
314
|
+
|
|
315
|
+
- Converts absolute paths (from watchdog) to relative paths (for engine pattern matching) using the watch root directory
|
|
316
|
+
- Ignores all events inside `.agentfirewall/` itself (prevents self-triggering)
|
|
317
|
+
- Ignores directory-level events (only monitors files)
|
|
318
|
+
- On DENY verdict: prints a formatted violation alert to stderr (emoji, verdict, operation, path, rule, detail), calls `ProcessKiller.kill_agents()` to terminate the offending agent, and prints the kill count
|
|
319
|
+
- On ALLOW verdict: no output (silent pass-through)
|
|
320
|
+
|
|
321
|
+
**`FirewallObserver` class:**
|
|
322
|
+
- Wraps watchdog's `Observer` with agentfirewall-specific setup
|
|
323
|
+
- `start()` — begins watching the sandbox root recursively
|
|
324
|
+
- `stop()` — stops the observer
|
|
325
|
+
- `run_forever()` — starts and blocks until Ctrl+C (used by the `watch` CLI command)
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
### `hooks/shell.py` — Shell Preexec Hooks
|
|
330
|
+
|
|
331
|
+
Generates, installs, and removes shell hook scripts that intercept commands before they execute.
|
|
332
|
+
|
|
333
|
+
**How the hooks work:**
|
|
334
|
+
- **Bash:** Enables `extdebug` mode (`shopt -s extdebug`) and installs a `DEBUG` trap. Before every command, bash calls `agentfirewall check "$BASH_COMMAND"`. If the exit code is non-zero (denied), the hook prints `[agentfirewall] BLOCKED: <command>` to stderr and returns 1 — with `extdebug` enabled, a non-zero return from a DEBUG trap **prevents the command from executing**. Without `extdebug`, bash ignores the return value and runs the command anyway.
|
|
335
|
+
- **Zsh:** Uses `add-zsh-hook preexec`. Checks the command via `agentfirewall check "$1"`. If denied, prints the BLOCKED message and sends `kill -INT $$` (SIGINT to self) to abort the pending command. Zsh's `preexec` is a notification hook — `return 1` alone doesn't stop execution — so the self-interrupt is required.
|
|
336
|
+
|
|
337
|
+
**Guard markers:**
|
|
338
|
+
Hook blocks are wrapped in `# >>> agentfirewall >>>` and `# <<< agentfirewall <<<` markers (similar to how conda manages shell integration). This allows:
|
|
339
|
+
- **Idempotent install** — if markers already exist, `install_hook()` does nothing
|
|
340
|
+
- **Clean uninstall** — `uninstall_hook()` removes only the guarded block, preserving all other RC file content
|
|
341
|
+
|
|
342
|
+
**Functions:**
|
|
343
|
+
- `generate_bash_hook()` / `generate_zsh_hook()` — return the hook script strings
|
|
344
|
+
- `detect_shell()` — reads `$SHELL` env var, returns `"bash"` or `"zsh"`
|
|
345
|
+
- `install_hook(shell, rc_path)` — appends hook to `~/.bashrc` or `~/.zshrc`
|
|
346
|
+
- `uninstall_hook(shell, rc_path)` — removes the guard-marked block
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
### `presets/__init__.py` — Built-in Rule Sets
|
|
351
|
+
|
|
352
|
+
Three presets with increasing strictness:
|
|
353
|
+
|
|
354
|
+
**Common blocklist patterns (shared by all presets):**
|
|
355
|
+
`rm -rf /`, `rm -rf ~`, `rm -rf /*`, `dd if=*of=/dev/*`, `mkfs.*`, `:(){ :|:& };:`, `chmod -R 777`, `sudo rm*`, `git push --force`, `git push.*--force`, `git reset --hard`, `git clean -fd`, `kill -9`
|
|
356
|
+
|
|
357
|
+
| Feature | Standard | Strict | Permissive |
|
|
358
|
+
|---------|----------|--------|------------|
|
|
359
|
+
| Mode | ENFORCE | ENFORCE | AUDIT |
|
|
360
|
+
| Blocklist | 13 common patterns | 23 patterns (common + extra) | 13 common patterns |
|
|
361
|
+
| Protected paths | .git/**, .env, .ssh/** | + /etc/**, /boot/**, /usr/** | .git/**, .env, .ssh/** |
|
|
362
|
+
| Sandbox escape | Allowed | **Blocked** | Allowed |
|
|
363
|
+
| Deny operations | delete, move_outside | + chmod, write | delete, move_outside |
|
|
364
|
+
| Allowed hosts | github.com, OpenAI, Anthropic | Same | *(none — all allowed)* |
|
|
365
|
+
| Logging | Enabled (warn level) | Enabled (warn level) | Enabled (warn level) |
|
|
366
|
+
|
|
367
|
+
**Extra strict blocklist additions:** `curl*|*bash`, `wget*|*sh`, `eval *`, `exec *`, `python -c*`, `node -e*`, `nc *`, `ncat *`, `netcat *`, `telnet *`
|
|
368
|
+
|
|
369
|
+
**API:**
|
|
370
|
+
- `get_preset(name)` — returns a `FirewallConfig` for the named preset
|
|
371
|
+
- `list_presets()` — returns `["standard", "strict", "permissive"]`
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## Config Schema (YAML)
|
|
376
|
+
|
|
377
|
+
```yaml
|
|
378
|
+
version: 1
|
|
379
|
+
mode: enforce # enforce | audit | off
|
|
380
|
+
|
|
381
|
+
sandbox:
|
|
382
|
+
root: "." # Working directory boundary
|
|
383
|
+
allow_escape: false # Allow operations outside sandbox root
|
|
384
|
+
|
|
385
|
+
filesystem:
|
|
386
|
+
protected_paths: # Glob patterns for protected files
|
|
387
|
+
- ".git/**"
|
|
388
|
+
- ".env"
|
|
389
|
+
- ".ssh/**"
|
|
390
|
+
deny_operations: # Operations blocked on protected paths
|
|
391
|
+
- delete
|
|
392
|
+
- move_outside_sandbox
|
|
393
|
+
|
|
394
|
+
commands:
|
|
395
|
+
blocklist: # Shell command patterns to block (* = wildcard)
|
|
396
|
+
- "rm -rf /"
|
|
397
|
+
- "sudo rm*"
|
|
398
|
+
allowlist: [] # If non-empty, ONLY these commands are allowed
|
|
399
|
+
|
|
400
|
+
network:
|
|
401
|
+
allowed_hosts: # If non-empty, only these hosts permitted
|
|
402
|
+
- "github.com"
|
|
403
|
+
- "api.openai.com"
|
|
404
|
+
deny_egress_to: # Always blocked regardless of allowed_hosts
|
|
405
|
+
- "169.254.169.254"
|
|
406
|
+
- "metadata.google.internal"
|
|
407
|
+
max_upload_bytes: 10485760 # 10 MB (not yet enforced)
|
|
408
|
+
|
|
409
|
+
logging:
|
|
410
|
+
enabled: true
|
|
411
|
+
file: "logs/firewall.log"
|
|
412
|
+
level: warn
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## Decision Logic Summary
|
|
418
|
+
|
|
419
|
+
```
|
|
420
|
+
Command Check:
|
|
421
|
+
mode=OFF? → ALLOW
|
|
422
|
+
allowlist non-empty AND command not in allowlist? → DENY
|
|
423
|
+
command matches blocklist? → DENY
|
|
424
|
+
else → ALLOW
|
|
425
|
+
|
|
426
|
+
File Operation Check:
|
|
427
|
+
mode=OFF? → ALLOW
|
|
428
|
+
path escapes sandbox (and allow_escape=false)? → DENY
|
|
429
|
+
operation in deny_operations AND path matches protected_paths? → DENY
|
|
430
|
+
else → ALLOW
|
|
431
|
+
|
|
432
|
+
Network Check:
|
|
433
|
+
mode=OFF? → ALLOW
|
|
434
|
+
host in deny_egress_to? → DENY
|
|
435
|
+
allowed_hosts non-empty AND host not in allowed_hosts? → DENY
|
|
436
|
+
else → ALLOW
|
|
437
|
+
|
|
438
|
+
In all cases: if mode=AUDIT, DENY is downgraded to WARN.
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## Test Structure
|
|
444
|
+
|
|
445
|
+
109 tests across 10 files (all passing):
|
|
446
|
+
|
|
447
|
+
| File | Tests | What it covers |
|
|
448
|
+
|------|-------|----------------|
|
|
449
|
+
| `tests/test_schema.py` | 12 | Config loading, validation, YAML round-trip, find_config directory walking, error handling |
|
|
450
|
+
| `tests/test_engine.py` | 25 | Command evaluation (blocklist, allowlist, wildcards), file operations (sandbox boundary, protected paths), network checks, audit mode downgrade, mode=off bypass |
|
|
451
|
+
| `tests/test_cli.py` | 14 | CLI init (directory creation, presets, --force), check/check-file/check-network commands, status output, missing config errors |
|
|
452
|
+
| `tests/test_presets.py` | 5 | Preset content validation, list_presets, unknown preset error |
|
|
453
|
+
| `tests/test_audit.py` | 10 | Log file creation, JSON format, multiple entries, disabled logging, level filtering, timestamp format |
|
|
454
|
+
| `tests/test_process.py` | 14 | Agent detection (exact + broad signatures), PID safety guards, kill count, AccessDenied/NoSuchProcess handling |
|
|
455
|
+
| `tests/test_watcher.py` | 10 | Delete/modify/move event handling, protected path triggers, allow normal ops, process killer calls, .agentfirewall/ ignoring, directory event ignoring |
|
|
456
|
+
| `tests/test_hooks.py` | 14 | Hook generation (bash/zsh), shell detection, install to new/existing files, idempotency, uninstall with content preservation |
|
|
457
|
+
| `tests/test_cli_phase2.py` | 6 | watch command (starts observer, no-config error), install-hooks (bash/zsh), uninstall-hooks (remove/not-present) |
|
|
458
|
+
|
|
459
|
+
Run tests: `pytest -v` (from project root with venv activated)
|
|
460
|
+
|
|
461
|
+
---
|
|
462
|
+
|
|
463
|
+
## Quick Start
|
|
464
|
+
|
|
465
|
+
The fastest way to get everything set up (system deps + venv + Python packages):
|
|
466
|
+
|
|
467
|
+
```bash
|
|
468
|
+
git clone <repo-url> && cd llm-security
|
|
469
|
+
./setup.sh # Full setup including FUSE sandbox support
|
|
470
|
+
./setup.sh --no-fuse # Skip FUSE if you don't need the sandbox
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Or install manually:
|
|
474
|
+
|
|
475
|
+
```bash
|
|
476
|
+
# System dependencies (for FUSE sandbox — skip if not needed)
|
|
477
|
+
sudo apt install fuse3 libfuse-dev # Debian/Ubuntu
|
|
478
|
+
sudo dnf install fuse fuse-devel # Fedora/RHEL
|
|
479
|
+
|
|
480
|
+
# Install in development mode
|
|
481
|
+
pip install -e ".[dev]" # Core + tests
|
|
482
|
+
pip install -e ".[dev,sandbox]" # Core + tests + FUSE sandbox
|
|
483
|
+
|
|
484
|
+
# --- One-command setup (recommended) ---
|
|
485
|
+
|
|
486
|
+
# Protect your project (init + hooks + background watcher in one step)
|
|
487
|
+
cd /path/to/your/project
|
|
488
|
+
agentfirewall protect # standard preset
|
|
489
|
+
agentfirewall protect --preset strict # stricter rules
|
|
490
|
+
agentfirewall protect --preset permissive # warn-only mode
|
|
491
|
+
|
|
492
|
+
# Disable protection (stops watcher + removes hooks)
|
|
493
|
+
agentfirewall unprotect
|
|
494
|
+
agentfirewall unprotect --remove-config # also deletes .agentfirewall/
|
|
495
|
+
|
|
496
|
+
# View current status (shows mode, watcher state, hooks state)
|
|
497
|
+
agentfirewall status
|
|
498
|
+
|
|
499
|
+
# --- Manual setup (advanced) ---
|
|
500
|
+
|
|
501
|
+
# Initialize firewall in your project
|
|
502
|
+
agentfirewall init # standard preset
|
|
503
|
+
agentfirewall init --preset strict # stricter rules
|
|
504
|
+
|
|
505
|
+
# Check commands before executing
|
|
506
|
+
agentfirewall check "rm -rf /" # 🚫 DENY
|
|
507
|
+
agentfirewall check "ls -la" # ✅ ALLOW
|
|
508
|
+
|
|
509
|
+
# Check file operations
|
|
510
|
+
agentfirewall check-file .env -o delete # 🚫 DENY
|
|
511
|
+
agentfirewall check-file readme.md -o write # ✅ ALLOW
|
|
512
|
+
|
|
513
|
+
# Check network connections
|
|
514
|
+
agentfirewall check-network github.com # ✅ ALLOW
|
|
515
|
+
agentfirewall check-network 169.254.169.254 # 🚫 DENY
|
|
516
|
+
|
|
517
|
+
# Start real-time filesystem monitoring
|
|
518
|
+
agentfirewall watch # Ctrl+C to stop
|
|
519
|
+
|
|
520
|
+
# Install shell hooks (intercept commands before execution)
|
|
521
|
+
agentfirewall install-hooks # auto-detects bash/zsh
|
|
522
|
+
agentfirewall install-hooks --shell zsh
|
|
523
|
+
|
|
524
|
+
# Remove shell hooks
|
|
525
|
+
agentfirewall uninstall-hooks
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
---
|
|
529
|
+
|
|
530
|
+
## Dependencies
|
|
531
|
+
|
|
532
|
+
### System Requirements
|
|
533
|
+
|
|
534
|
+
| Package | Platform | Purpose | Required? |
|
|
535
|
+
|---------|----------|---------|----------|
|
|
536
|
+
| `libfuse-dev` | Debian/Ubuntu | Provides `libfuse.so.2` for FUSE sandbox (`agentfirewall sandbox`) | Only for sandbox feature |
|
|
537
|
+
| `fuse3` | Debian/Ubuntu | FUSE utilities (`fusermount`) | Only for sandbox feature |
|
|
538
|
+
| `fuse-devel` | Fedora/RHEL | Equivalent of `libfuse-dev` | Only for sandbox feature |
|
|
539
|
+
| `macfuse` | macOS | macOS FUSE support ([osxfuse.github.io](https://osxfuse.github.io/)) | Only for sandbox feature |
|
|
540
|
+
|
|
541
|
+
> **Note:** `fusepy` (the Python binding) requires FUSE 2 (`libfuse.so.2`), **not** FUSE 3. On systems with only `libfuse3`, install `libfuse-dev` to get the FUSE 2 library — both coexist safely.
|
|
542
|
+
|
|
543
|
+
### Python Packages
|
|
544
|
+
|
|
545
|
+
| Package | Version | Purpose |
|
|
546
|
+
| `pyyaml` | ≥ 6.0 | Parse `.agentfirewall/config.yaml` (uses `safe_load` for security) |
|
|
547
|
+
| `click` | ≥ 8.0 | CLI framework (commands, options, argument parsing) |
|
|
548
|
+
| `watchdog` | ≥ 3.0 | Cross-platform filesystem monitoring (abstracts inotify/fsevents/ReadDirectoryChanges) |
|
|
549
|
+
| `psutil` | ≥ 5.9 | Cross-platform process scanning and termination (agent killing) |
|
|
550
|
+
| `fusepy` | ≥ 3.0 | FUSE filesystem bindings for sandbox (`pip install agentfirewall[sandbox]`) — *optional* |
|
|
551
|
+
|
|
552
|
+
Dev dependencies: `pytest` ≥ 7.0, `pytest-cov` ≥ 4.0
|
|
553
|
+
|
|
554
|
+
---
|
|
555
|
+
|
|
556
|
+
## Developer Onboarding
|
|
557
|
+
|
|
558
|
+
### Reading Order
|
|
559
|
+
|
|
560
|
+
If you're new to the codebase, read the source files in this order — each one builds on the previous:
|
|
561
|
+
|
|
562
|
+
1. **`schema.py`** — Start here. This is the data model. Learn the config structure (`FirewallConfig` and its sub-configs) and how YAML is parsed. No internal dependencies.
|
|
563
|
+
2. **`engine.py`** — The decision maker. Takes a config and evaluates commands/files/network against it. Returns `RuleResult` with a verdict. Depends only on `schema.py`.
|
|
564
|
+
3. **`presets/__init__.py`** — Three built-in configs (standard/strict/permissive). Shows how configs are constructed in code. Depends on `schema.py`.
|
|
565
|
+
4. **`audit.py`** — JSON structured logging. Wraps Python's `logging` module. Depends on `engine.py` (for the `RuleResult` type).
|
|
566
|
+
5. **`process.py`** — Agent process identification and killing. Standalone module — only uses `psutil`.
|
|
567
|
+
6. **`watchers/filesystem.py`** — Filesystem monitoring. Connects watchdog events → engine evaluation → process killing. Depends on `engine.py`, `schema.py`, `process.py`.
|
|
568
|
+
7. **`hooks/shell.py`** — Shell hook generation and RC file management. Standalone module — only uses `os` and `pathlib`.
|
|
569
|
+
8. **`cli.py`** — The user interface. Ties everything together into Click commands. Read this last — it imports from all other modules.
|
|
570
|
+
|
|
571
|
+
### Module Dependency Graph
|
|
572
|
+
|
|
573
|
+
```
|
|
574
|
+
schema.py ◄──── engine.py ◄──── audit.py
|
|
575
|
+
▲ ▲ │
|
|
576
|
+
│ │ │ (logged by)
|
|
577
|
+
│ │ ▼
|
|
578
|
+
presets/ watchers/ .agentfirewall/
|
|
579
|
+
__init__.py filesystem.py logs/firewall.log
|
|
580
|
+
│
|
|
581
|
+
├──── process.py (standalone)
|
|
582
|
+
│
|
|
583
|
+
▼
|
|
584
|
+
hooks/shell.py (standalone)
|
|
585
|
+
|
|
586
|
+
▲
|
|
587
|
+
│ all wired together by
|
|
588
|
+
│
|
|
589
|
+
cli.py
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
Arrows point from **dependent → dependency** (e.g., engine imports schema).
|
|
593
|
+
|
|
594
|
+
### How to Add a New Rule Type
|
|
595
|
+
|
|
596
|
+
1. Add a new evaluation method to `Engine` in `engine.py` (e.g., `evaluate_dns(hostname)`)
|
|
597
|
+
2. If it needs new config fields, add a new dataclass to `schema.py` and wire it into `FirewallConfig` + the YAML parser
|
|
598
|
+
3. Add the corresponding `action_type` string to audit log calls (e.g., `self._log("dns", hostname, result)`)
|
|
599
|
+
4. Add a CLI command in `cli.py` (e.g., `check-dns`)
|
|
600
|
+
5. Add tests in `tests/test_engine.py` and `tests/test_cli.py`
|
|
601
|
+
|
|
602
|
+
### How to Add a New Watcher Event Type
|
|
603
|
+
|
|
604
|
+
1. Add the new `DenyOperation` variant to the enum in `schema.py`
|
|
605
|
+
2. Map the watchdog event to the new operation in `_EVENT_TO_OP` in `watchers/filesystem.py`
|
|
606
|
+
3. Handle the event in `FirewallHandler` (implement the `on_*` method)
|
|
607
|
+
4. Add tests in `tests/test_watcher.py`
|
|
608
|
+
|
|
609
|
+
### Running Tests
|
|
610
|
+
|
|
611
|
+
```bash
|
|
612
|
+
# Activate the virtual environment
|
|
613
|
+
source security-env/bin/activate
|
|
614
|
+
|
|
615
|
+
# Run all tests with verbose output
|
|
616
|
+
pytest -v
|
|
617
|
+
|
|
618
|
+
# Run a specific test file
|
|
619
|
+
pytest tests/test_engine.py -v
|
|
620
|
+
|
|
621
|
+
# Run with coverage
|
|
622
|
+
pytest --cov=agentfirewall --cov-report=term-missing
|
|
623
|
+
|
|
624
|
+
# Run only FUSE sandbox tests (requires libfuse-dev)
|
|
625
|
+
pytest tests/test_sandbox.py -v
|
|
626
|
+
|
|
627
|
+
# Run everything except FUSE sandbox tests
|
|
628
|
+
pytest -v --ignore=tests/test_sandbox.py
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
> **Note:** FUSE sandbox tests (`test_sandbox.py`) require `libfuse-dev` to be installed. If only `libfuse3` is present, these tests will fail with `OSError: Unable to find libfuse`. Run `sudo apt install libfuse-dev` to fix, or use `./setup.sh` which handles this automatically.
|
|
632
|
+
|
|
633
|
+
---
|
|
634
|
+
|
|
635
|
+
## Roadmap
|
|
636
|
+
|
|
637
|
+
- **Phase 1** ✅ — Static rule checker (schema, engine, CLI dry-run commands, presets)
|
|
638
|
+
- **Phase 2** ✅ — Real-time OS enforcement (audit logging, filesystem watcher, process killer, shell hooks)
|
|
639
|
+
- **Phase 3** — Network interception (eBPF/iptables egress filtering, HTTP inspection)
|
|
640
|
+
- **Phase 4** — Agent protocol integration (MCP middleware, LangChain callbacks)
|
|
641
|
+
- **Phase 5** — Advanced features (ML anomaly detection, multi-agent policies)
|
|
642
|
+
- **Phase 6** — Web UI dashboard (Flask, live log viewer, config editor)
|