devpy-cli 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,330 @@
1
+ Metadata-Version: 2.4
2
+ Name: devpy-cli
3
+ Version: 1.0.0
4
+ Summary: AI-powered DevOps CLI Assistant for local and remote Docker management
5
+ Author-email: Eddy Ortega <atrox390@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/your-username/devpy-cli
8
+ Project-URL: Bug Tracker, https://github.com/your-username/devpy-cli/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Topic :: System :: Systems Administration
13
+ Classifier: Topic :: Software Development :: Build Tools
14
+ Requires-Python: >=3.11
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: docker>=7.0.0
17
+ Requires-Dist: paramiko>=3.4.0
18
+ Requires-Dist: cryptography>=42.0.0
19
+ Requires-Dist: rich>=13.7.0
20
+ Requires-Dist: langchain>=0.1.0
21
+ Requires-Dist: langchain-openai>=0.0.5
22
+ Requires-Dist: langgraph>=0.0.10
23
+ Requires-Dist: python-dotenv>=1.0.0
24
+ Requires-Dist: psutil>=5.9.0
25
+
26
+ # DevPy CLI
27
+
28
+ An intelligent command-line assistant powered by LLM (DeepSeek/OpenAI) to manage Docker environments, both local and remote via SSH. Designed to simplify DevOps tasks with natural language, ensuring security and control.
29
+
30
+ ## Key Features
31
+
32
+ * **Natural Language Interaction**: "Restart the nginx container", "Show database logs", "Monitor memory usage".
33
+ * **Local and Remote Docker Management**: Connect to your local machine or remote servers via SSH transparently.
34
+ * **Secure SSH Key Management**: Encrypted storage (AES-256) of SSH private keys. Import from `~/.ssh`.
35
+ * **Granular Permission System**:
36
+ * Interactive confirmation for critical operations (write/delete).
37
+ * Configurable whitelists.
38
+ * Persistent permission rules with hot-reload.
39
+ * "Dry-Run" mode to simulate executions.
40
+ * **Logging and Auditing**: Detailed logging of all operations and permission decisions in `logs/permissions.log`.
41
+
42
+ ## System Requirements
43
+
44
+ * Python 3.9 or higher.
45
+ * Docker client installed (local) or SSH access to a server with Docker.
46
+ * Operating System: Windows, macOS, Linux.
47
+
48
+ ## Installation
49
+
50
+ 1. **Clone the repository:**
51
+
52
+ ```bash
53
+ git clone <repo-url>
54
+ cd devpy-cli
55
+ ```
56
+
57
+ 2. **Create virtual environment (recommended):**
58
+
59
+ ```bash
60
+ python -m venv venv
61
+ # Windows
62
+ .\venv\Scripts\activate
63
+ # Linux/Mac
64
+ source venv/bin/activate
65
+ ```
66
+
67
+ 3. **Install dependencies:**
68
+
69
+ ```bash
70
+ pip install -e .
71
+ ```
72
+
73
+ 4. **Configure environment:**
74
+ Create a `.env` file in the root (you can copy the example if it exists) with your LLM API key:
75
+
76
+ ```ini
77
+ DEEPSEEK_API_KEY=your_api_key_here
78
+ # Optional: LLM=chatgpt and OPENAI_API_KEY=...
79
+ ```
80
+
81
+ ## Usage Guide
82
+
83
+ ### Start the CLI
84
+
85
+ ```bash
86
+ # From the repository
87
+ python app.py
88
+
89
+ # Or if installed in editable mode
90
+ devpy-cli
91
+ ```
92
+
93
+ On first run, if no `.env` file exists, an interactive setup wizard will guide you through:
94
+ - Choosing your LLM provider.
95
+ - Entering the API key.
96
+ - Optionally setting a custom base URL.
97
+
98
+ After setup, the CLI banner appears and you are asked whether to enable dry-run mode.
99
+
100
+ ---
101
+
102
+ ### CLI Mode (Local Docker)
103
+
104
+ Use this mode when you want to manage containers running on the same machine where DevPy CLI is installed.
105
+
106
+ - **Requirements**
107
+ - Docker is installed and the daemon is running locally.
108
+ - Your user can talk to the Docker socket (e.g., `docker ps` works from your shell).
109
+
110
+ - **Step-by-step**
111
+ 1. Start the CLI (see above).
112
+ 2. When prompted, choose whether to enable dry-run mode.
113
+ 3. Ensure the mode is set to `local` (this is the default):
114
+ ```bash
115
+ config mode local
116
+ ```
117
+ 4. Type natural language instructions, for example:
118
+ - `What containers are running?`
119
+ - `Restart the nginx container and show me its latest logs`
120
+ - `Create a redis container called cache`
121
+ 5. When an action is potentially destructive (creating/stopping/removing containers, starting monitors, etc.), DevPy will:
122
+ - Show a preview of the Docker command.
123
+ - Ask for confirmation (once, for the command, or for the whole session).
124
+
125
+ - **Typical local use cases**
126
+ - Quickly inspecting and restarting local services from the terminal.
127
+ - Checking logs of a misbehaving container.
128
+ - Spinning up utility containers (e.g., Redis, Postgres) by name and image.
129
+
130
+ ---
131
+
132
+ ### SSH Mode (Remote Docker over SSH)
133
+
134
+ Use this mode to manage containers on a remote host over SSH, while still talking to the CLI locally.
135
+
136
+ - **Prerequisites**
137
+ - The remote server:
138
+ - Has Docker installed and running.
139
+ - Is reachable via SSH (e.g., `ssh user@host` works).
140
+ - You have an SSH private key that can authenticate to that server.
141
+
142
+ - **Step 1: Store your SSH key (encrypted)**
143
+
144
+ You can import keys from `~/.ssh` or add a specific file:
145
+
146
+ ```bash
147
+ # Scan ~/.ssh for potential keys and import one
148
+ keys scan
149
+
150
+ # Or add a specific key path
151
+ keys add my-remote /path/to/id_rsa
152
+
153
+ # List stored keys
154
+ keys list
155
+ ```
156
+
157
+ During `keys scan` or `keys add`, you are asked for a **passphrase for encryption**.
158
+ This passphrase is used to derive a key that encrypts your private key on disk (AES-256 via `cryptography.Fernet`).
159
+
160
+ - **Step 2: Configure SSH connection**
161
+
162
+ In the CLI, run:
163
+
164
+ ```bash
165
+ config ssh
166
+ ```
167
+
168
+ You will be prompted for:
169
+ - **SSH Host** (e.g., `myserver.example.com` or `192.168.1.100`)
170
+ - **SSH User** (e.g., `ubuntu`, `root`, `deploy`)
171
+ - **SSH Key Name** (one of the names returned by `keys list`)
172
+
173
+ This information is stored in `config.json`.
174
+
175
+ - **Step 3: Switch to SSH mode**
176
+
177
+ ```bash
178
+ config mode ssh
179
+ ```
180
+
181
+ From now on, Docker operations happen against the remote host using the stored SSH configuration.
182
+
183
+ - **Step 4: Authenticate with your key**
184
+
185
+ When the backend needs to connect to the remote Docker daemon, it:
186
+ - Prompts for the passphrase you used when storing the key, **or**
187
+ - Uses the `DOCKER_SSH_PASSPHRASE` environment variable if it is set.
188
+
189
+ This decrypted key is written to a temporary file (with restricted permissions) and used only for the SSH connection.
190
+
191
+ - **Typical SSH use cases**
192
+ - Managing a remote Docker host from your laptop without logging in manually.
193
+ - Checking logs and restarting containers in staging/production environments.
194
+ - Monitoring memory usage of remote containers and triggering alerts.
195
+
196
+ ---
197
+
198
+ ### Command Reference
199
+
200
+ #### Configuration Commands
201
+
202
+ Use these to configure how the CLI connects and which LLM it uses:
203
+
204
+ ```bash
205
+ # Show or set connection mode
206
+ config mode # shows current mode (local or ssh)
207
+ config mode local # use local Docker
208
+ config mode ssh # use remote Docker over SSH
209
+
210
+ # Configure SSH details (host, user, key)
211
+ config ssh
212
+
213
+ # Re-run the LLM setup wizard and regenerate .env
214
+ config llm
215
+ ```
216
+
217
+ #### SSH Key Management Commands
218
+
219
+ ```bash
220
+ # Import keys from ~/.ssh (interactive)
221
+ keys scan
222
+
223
+ # Add a key manually
224
+ keys add <name> <path_to_private_key>
225
+
226
+ # List saved keys
227
+ keys list
228
+
229
+ # Delete a stored key
230
+ keys delete <name>
231
+ ```
232
+
233
+ #### Permission Management Commands
234
+
235
+ Control what the agent is allowed to do:
236
+
237
+ ```bash
238
+ # View current rules
239
+ permissions list
240
+
241
+ # Block container restarts permanently
242
+ permissions add restart_container deny
243
+
244
+ # Allow container creation (with optional parameters)
245
+ permissions add create_container allow
246
+
247
+ # Reset all persistent permission rules
248
+ permissions reset
249
+ ```
250
+
251
+ During interactive confirmations, you can choose:
252
+ - `y` – allow once.
253
+ - `yc` – always allow this exact command during the session.
254
+ - `ys` – always allow this operation type during the session.
255
+ - `n` – deny.
256
+
257
+ ---
258
+
259
+ ### Interaction Examples with the Agent
260
+
261
+ Once configured, simply type what you need:
262
+
263
+ - *"What containers are running?"*
264
+ - *"Restart the 'web-app' container and show me its latest logs"*
265
+ - *"Create a redis container named 'my-redis'"*
266
+ - *"Alert me if memory usage of container 'api' exceeds 80%"*
267
+
268
+ The agent plans and executes one or more Docker operations, asking for permission when necessary.
269
+
270
+ ---
271
+
272
+ ### Dry-Run Mode
273
+
274
+ You can enable dry-run mode in two ways:
275
+
276
+ - At startup, when the CLI asks:
277
+ - Answer `y` to run in dry-run mode for the session.
278
+ - Via environment variable:
279
+ - Set `DRY_RUN=1` before starting the app.
280
+
281
+ In this mode, the agent **simulates** write actions (creating, deleting, restarting containers, starting monitors, etc.) without actually executing them.
282
+ The permission log still records what *would* have been executed.
283
+
284
+ ---
285
+
286
+ ## Authentication and Security
287
+
288
+ - **LLM API Authentication**
289
+ - The `.env` file created by the setup wizard stores:
290
+ - `LLM` – which provider/adapter to use.
291
+ - `<PROVIDER>_API_KEY` – the API key for that provider.
292
+ - Optionally `LLM_BASE_URL` – custom base URL for compatible providers.
293
+ - You can re-run the wizard at any time with:
294
+ ```bash
295
+ config llm
296
+ ```
297
+
298
+ - **SSH Key Encryption**
299
+ - Stored SSH keys live in `ssh_keys.enc`.
300
+ - Each key is encrypted using a passphrase-derived key (PBKDF2 + AES-256).
301
+ - The file permissions are hardened to allow read/write only for the current user.
302
+
303
+ - **Runtime Environment Variables**
304
+ - `DRY_RUN` – if set to `1`, `true`, `yes`, or `y`, forces dry-run mode.
305
+ - `DOCKER_SSH_PASSPHRASE` – optional; if set, avoids interactive passphrase prompts for SSH keys.
306
+ - `DOCKER_SAFE_COMMANDS` – comma-separated list of operations that never prompt for confirmation.
307
+ - `DOCKER_CLI_USER` – overrides the username recorded in permission logs.
308
+
309
+ - **Logging and Auditing**
310
+ - All operations go through a permission and logging layer.
311
+ - Logs are written as JSON lines to `logs/permissions.log`.
312
+ - Each entry includes timestamp, user, operation, arguments, decision, and optional command preview.
313
+
314
+ ## Project Structure
315
+
316
+ * `app.py`: Entry point.
317
+ * `frontend_cli.py`: User interface and CLI command handling.
318
+ * `backend.py`: Agent logic, integration with LangChain/LangGraph and Docker tools.
319
+ * `permissions_manager.py`: Access control and auditing system.
320
+ * `ssh_key_manager.py`: Encryption and key management.
321
+ * `config_manager.py`: Configuration persistence (mode, ssh host).
322
+ * `logs/`: Audit log files.
323
+
324
+ ## License
325
+
326
+ MIT License. See `LICENSE` file for more details.
327
+
328
+ ## Author
329
+
330
+ Developed by [Your Name/Organization].
@@ -0,0 +1,12 @@
1
+ app.py,sha256=iRPS0o8Ylp_PJiaPjmMkRvi0d2DGjgKotqZgCnVf0lA,383
2
+ backend.py,sha256=yChg_BQAEU6OpPnL-4DDYFCCqkDEJuQiTVpjog2KHbc,12887
3
+ config_manager.py,sha256=Nrgw0fQjLHl6sHb2Ww5ilKaYcvLmRhWgXdyXeyyyqTk,1345
4
+ frontend_cli.py,sha256=OF9rs1NKpMhJXSJzxbEu9c3AhvXdwLWyCeOtHIKfZSQ,6452
5
+ permissions_config_manager.py,sha256=w2nmRw54lZrrWa18uf18VedeK-gLbaBuSxr3Hy94E8E,3501
6
+ permissions_manager.py,sha256=-blv36TGq0xkterLk8Nzt9Iaaw9RBv2yuEz3DBDJXNs,5646
7
+ ssh_key_manager.py,sha256=49-23P0oo8ipmkjVfGLM9HFvVa-UZ6RqWR7uDxlRqgU,2423
8
+ devpy_cli-1.0.0.dist-info/METADATA,sha256=9gRE3oEdgfSHDkaf6Nsfur1QqSDg1bzAJ49t651PoN0,10204
9
+ devpy_cli-1.0.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
10
+ devpy_cli-1.0.0.dist-info/entry_points.txt,sha256=ww9qvpAxiE5vIb7huMHna206tHrkeTRAf2RWSGd5TDM,42
11
+ devpy_cli-1.0.0.dist-info/top_level.txt,sha256=-yDfRnyYYfuyhetW2c6LJEg74FoesmjoPu8o9DHtPQw,103
12
+ devpy_cli-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ devpy-cli = app:run_cli
@@ -0,0 +1,7 @@
1
+ app
2
+ backend
3
+ config_manager
4
+ frontend_cli
5
+ permissions_config_manager
6
+ permissions_manager
7
+ ssh_key_manager
frontend_cli.py ADDED
@@ -0,0 +1,205 @@
1
+ from rich.console import Console
2
+ from rich.prompt import Prompt
3
+ from rich.markdown import Markdown
4
+ from backend import run_agent_flow, config_manager, ssh_key_manager, reset_docker_client, permission_manager
5
+ import os
6
+ from pathlib import Path
7
+ from setup_wizard import run_setup
8
+
9
+ console = Console()
10
+
11
+
12
+ def handle_config_command(user_input):
13
+ parts = user_input.split()
14
+ if len(parts) < 2:
15
+ console.print('[yellow]Usage: config [mode|ssh|llm][/yellow]')
16
+ return
17
+
18
+ cmd = parts[1]
19
+ if cmd == 'mode':
20
+ if len(parts) == 3:
21
+ new_mode = parts[2]
22
+ if new_mode in ['local', 'ssh']:
23
+ config_manager.set_mode(new_mode)
24
+ reset_docker_client()
25
+ console.print(f'[green]Mode set to {new_mode}[/green]')
26
+ else:
27
+ console.print("[red]Invalid mode. Use 'local' or 'ssh'[/red]")
28
+ else:
29
+ console.print(f'Current mode: {config_manager.get_mode()}')
30
+ elif cmd == 'ssh':
31
+ host = Prompt.ask('SSH Host')
32
+ user = Prompt.ask('SSH User')
33
+ keys = ssh_key_manager.list_keys()
34
+ if not keys:
35
+ console.print("[red]No SSH keys found. Add one with 'keys add' first.[/red]")
36
+ return
37
+ key_name = Prompt.ask('SSH Key Name', choices=keys)
38
+ config_manager.set_ssh_config(host, user, key_name)
39
+ reset_docker_client()
40
+ console.print('[green]SSH Configuration saved.[/green]')
41
+ elif cmd == 'llm':
42
+ console.print('[bold]Reconfiguring LLM Settings...[/bold]')
43
+ run_setup(force=True)
44
+ console.print('[yellow]Please restart the application for changes to take effect.[/yellow]')
45
+
46
+
47
+ def handle_keys_command(user_input):
48
+ parts = user_input.split()
49
+ if len(parts) < 2:
50
+ console.print('[yellow]Usage: keys [list|add|delete|scan][/yellow]')
51
+ return
52
+
53
+ cmd = parts[1]
54
+ if cmd == 'list':
55
+ keys = ssh_key_manager.list_keys()
56
+ if keys:
57
+ console.print('Stored SSH Keys:')
58
+ for k in keys:
59
+ console.print(f'- {k}')
60
+ else:
61
+ console.print('No keys stored.')
62
+ elif cmd == 'scan':
63
+ # List keys in ~/.ssh
64
+ ssh_dir = Path.home() / '.ssh'
65
+ if not ssh_dir.exists():
66
+ console.print(f'[red]Directory {ssh_dir} not found.[/red]')
67
+ return
68
+
69
+ potential_keys = []
70
+ for f in ssh_dir.iterdir():
71
+ if (
72
+ f.is_file()
73
+ and not f.name.endswith('.pub')
74
+ and not f.name.startswith('known_hosts')
75
+ and not f.name.startswith('config')
76
+ ):
77
+ potential_keys.append(f)
78
+
79
+ if not potential_keys:
80
+ console.print(f'[yellow]No potential keys found in {ssh_dir}[/yellow]')
81
+ return
82
+
83
+ console.print(f'[bold]Found keys in {ssh_dir}:[/bold]')
84
+ choices = [k.name for k in potential_keys]
85
+ choices.append('Cancel')
86
+
87
+ selected = Prompt.ask('Select key to import', choices=choices, default='Cancel')
88
+ if selected == 'Cancel':
89
+ return
90
+
91
+ path = ssh_dir / selected
92
+ name = Prompt.ask('Enter name for this key', default=selected)
93
+ passphrase = Prompt.ask('Enter Passphrase for encryption', password=True)
94
+
95
+ try:
96
+ ssh_key_manager.add_key(name, str(path), passphrase)
97
+ console.print(f"[green]Key '{name}' imported successfully.[/green]")
98
+ except Exception as e:
99
+ console.print(f'[red]Error adding key: {e}[/red]')
100
+
101
+ elif cmd == 'add':
102
+ if len(parts) < 4:
103
+ console.print('[yellow]Usage: keys add <name> <path>[/yellow]')
104
+ return
105
+ name = parts[2]
106
+ path = parts[3]
107
+ passphrase = Prompt.ask('Enter Passphrase for encryption', password=True)
108
+ try:
109
+ ssh_key_manager.add_key(name, path, passphrase)
110
+ console.print(f"[green]Key '{name}' added successfully.[/green]")
111
+ except Exception as e:
112
+ console.print(f'[red]Error adding key: {e}[/red]')
113
+ elif cmd == 'delete':
114
+ if len(parts) < 3:
115
+ console.print('[yellow]Usage: keys delete <name>[/yellow]')
116
+ return
117
+ name = parts[2]
118
+ if ssh_key_manager.delete_key(name):
119
+ console.print(f"[green]Key '{name}' deleted.[/green]")
120
+ else:
121
+ console.print(f"[red]Key '{name}' not found.[/red]")
122
+
123
+
124
+ def handle_permissions_command(user_input):
125
+ parts = user_input.split()
126
+ if len(parts) < 2:
127
+ console.print('[yellow]Usage: permissions [list|add|reset][/yellow]')
128
+ return
129
+
130
+ cmd = parts[1]
131
+ manager = permission_manager.config_manager
132
+
133
+ if cmd == 'list':
134
+ rules = manager.list_rules()
135
+ if not rules:
136
+ console.print('No permission rules configured.')
137
+ else:
138
+ console.print('[bold]Permission Rules:[/bold]')
139
+ for rule in rules:
140
+ console.print(f'- {rule["operation"]} -> {rule["decision"]} (Params: {rule.get("params")})')
141
+
142
+ elif cmd == 'add':
143
+ # Interactive add
144
+ # permissions add <operation> <decision> [param=value]
145
+ if len(parts) < 4:
146
+ console.print('[yellow]Usage: permissions add <operation> <allow|deny> [param=value...][/yellow]')
147
+ return
148
+
149
+ operation = parts[2]
150
+ decision = parts[3]
151
+ if decision not in ['allow', 'deny']:
152
+ console.print("[red]Decision must be 'allow' or 'deny'[/red]")
153
+ return
154
+
155
+ params = {}
156
+ if len(parts) > 4:
157
+ for p in parts[4:]:
158
+ if '=' in p:
159
+ k, v = p.split('=', 1)
160
+ params[k] = v
161
+
162
+ manager.add_rule(operation, decision, params=params)
163
+ console.print(f'[green]Rule added for {operation} -> {decision}[/green]')
164
+
165
+ elif cmd == 'reset':
166
+ if Prompt.ask('Are you sure you want to reset all permission rules?', choices=['y', 'n']) == 'y':
167
+ manager.reset_config()
168
+ console.print('[green]Permissions configuration reset.[/green]')
169
+
170
+
171
+ def run_cli():
172
+ console.print(Markdown('# DevPy CLI'))
173
+ console.print('[dim]Version 1.0.0[/dim]\n')
174
+ dry_run_answer = Prompt.ask(
175
+ '\n[bold]Enable dry-run mode?[/bold]',
176
+ choices=['y', 'n'],
177
+ default='n',
178
+ )
179
+ if dry_run_answer == 'y':
180
+ os.environ['DRY_RUN'] = '1'
181
+ while True:
182
+ try:
183
+ user_input = Prompt.ask('\n[bold]Enter a command[/bold]')
184
+ if user_input.lower() in ['exit', 'quit', 'bye']:
185
+ console.print('\n[bold green]Goodbye[/bold green]')
186
+ break
187
+ if user_input.strip() == '':
188
+ continue
189
+
190
+ if user_input.startswith('config'):
191
+ handle_config_command(user_input)
192
+ continue
193
+
194
+ if user_input.startswith('keys'):
195
+ handle_keys_command(user_input)
196
+ continue
197
+
198
+ if user_input.startswith('permissions'):
199
+ handle_permissions_command(user_input)
200
+ continue
201
+
202
+ run_agent_flow(user_input)
203
+ except KeyboardInterrupt:
204
+ console.print('\n[bold green]Goodbye[/bold green]')
205
+ break
@@ -0,0 +1,92 @@
1
+ import json
2
+ import os
3
+ import threading
4
+ import time
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+
8
+ class PermissionConfigManager:
9
+ def __init__(self, config_file='permissions_config.json'):
10
+ self.config_file = config_file
11
+ self.config = self._load_config()
12
+ self._last_mtime = self._get_mtime()
13
+ self._lock = threading.Lock()
14
+ self._start_watcher()
15
+
16
+ def _get_mtime(self):
17
+ try:
18
+ return os.path.getmtime(self.config_file)
19
+ except OSError:
20
+ return 0
21
+
22
+ def _load_config(self):
23
+ if not os.path.exists(self.config_file):
24
+ return {
25
+ "version": "1.0",
26
+ "rules": []
27
+ }
28
+ try:
29
+ with open(self.config_file, 'r', encoding='utf-8') as f:
30
+ return json.load(f)
31
+ except json.JSONDecodeError:
32
+ return {"version": "1.0", "rules": []}
33
+
34
+ def _save_config(self):
35
+ with self._lock:
36
+ with open(self.config_file, 'w', encoding='utf-8') as f:
37
+ json.dump(self.config, f, indent=2, ensure_ascii=False)
38
+
39
+ def _start_watcher(self):
40
+ def watcher():
41
+ while True:
42
+ time.sleep(2)
43
+ current_mtime = self._get_mtime()
44
+ if current_mtime > self._last_mtime:
45
+ self._last_mtime = current_mtime
46
+ with self._lock:
47
+ print(f"[PermissionConfigManager] Reloading configuration from {self.config_file}")
48
+ self.config = self._load_config()
49
+
50
+ t = threading.Thread(target=watcher, daemon=True)
51
+ t.start()
52
+
53
+ def add_rule(self, operation, decision, context=None, params=None):
54
+ rule = {
55
+ "operation": operation,
56
+ "decision": decision, # 'allow', 'deny', 'ask'
57
+ "created_at": datetime.utcnow().isoformat() + "Z",
58
+ "context": context or "",
59
+ "params": params or {}
60
+ }
61
+ with self._lock:
62
+ # Remove existing rules for same operation to avoid conflicts (simple priority: last wins)
63
+ # Or we can implement a more complex priority system.
64
+ # For now, let's append and filter during evaluation or replace.
65
+ # Strategy: Replace if exact match on operation and params?
66
+ # Let's just append for history, but get_decision will pick the latest relevant one.
67
+ self.config["rules"].insert(0, rule) # Insert at beginning for higher priority
68
+ self._save_config()
69
+ return rule
70
+
71
+ def get_decision(self, operation, params=None):
72
+ with self._lock:
73
+ for rule in self.config.get("rules", []):
74
+ if rule.get("operation") == operation:
75
+ # Check params match if specified in rule
76
+ rule_params = rule.get("params", {})
77
+ if not rule_params:
78
+ return rule.get("decision")
79
+
80
+ # If rule has params, all must match provided params
81
+ if params and all(params.get(k) == v for k, v in rule_params.items()):
82
+ return rule.get("decision")
83
+ return None # No explicit rule found
84
+
85
+ def list_rules(self):
86
+ with self._lock:
87
+ return self.config.get("rules", [])
88
+
89
+ def reset_config(self):
90
+ with self._lock:
91
+ self.config = {"version": "1.0", "rules": []}
92
+ self._save_config()