odoo-safe-mcp 2.0.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.
- odoo_safe_mcp-2.0.0/LICENSE +21 -0
- odoo_safe_mcp-2.0.0/PKG-INFO +132 -0
- odoo_safe_mcp-2.0.0/README.md +95 -0
- odoo_safe_mcp-2.0.0/odoo_safe_mcp/__init__.py +7 -0
- odoo_safe_mcp-2.0.0/odoo_safe_mcp/rules.py +343 -0
- odoo_safe_mcp-2.0.0/odoo_safe_mcp/server.py +386 -0
- odoo_safe_mcp-2.0.0/odoo_safe_mcp.egg-info/PKG-INFO +132 -0
- odoo_safe_mcp-2.0.0/odoo_safe_mcp.egg-info/SOURCES.txt +15 -0
- odoo_safe_mcp-2.0.0/odoo_safe_mcp.egg-info/dependency_links.txt +1 -0
- odoo_safe_mcp-2.0.0/odoo_safe_mcp.egg-info/entry_points.txt +2 -0
- odoo_safe_mcp-2.0.0/odoo_safe_mcp.egg-info/requires.txt +4 -0
- odoo_safe_mcp-2.0.0/odoo_safe_mcp.egg-info/top_level.txt +2 -0
- odoo_safe_mcp-2.0.0/pyproject.toml +61 -0
- odoo_safe_mcp-2.0.0/setup.cfg +4 -0
- odoo_safe_mcp-2.0.0/setup.py +50 -0
- odoo_safe_mcp-2.0.0/tests/test_rules.py +130 -0
- odoo_safe_mcp-2.0.0/tests/test_server.py +138 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Odoo Safe MCP Contributors
|
|
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.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: odoo-safe-mcp
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Deterministic AI Firewall and Model Context Protocol (MCP) Server for Odoo ERP with Human-in-the-Loop approvals.
|
|
5
|
+
Home-page: https://odoosafemcp.stream
|
|
6
|
+
Author: Odoo Safe MCP
|
|
7
|
+
Author-email: Odoo Safe MCP <soporte@odoosafemcp.stream>
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://odoosafemcp.stream
|
|
10
|
+
Project-URL: Documentation, https://odoosafemcp.stream/#how
|
|
11
|
+
Project-URL: Repository, https://github.com/makelele29/odoo-safe-mcp
|
|
12
|
+
Project-URL: Bug Tracker, https://github.com/makelele29/odoo-safe-mcp/issues
|
|
13
|
+
Project-URL: Get Pro API Key, https://odoosafemcp.stream/#pricing
|
|
14
|
+
Keywords: odoo,erp,mcp,model-context-protocol,ai-agents,claude,cursor,security,human-in-the-loop
|
|
15
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Intended Audience :: Information Technology
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Topic :: Office/Business :: Financial :: Accounting
|
|
24
|
+
Classifier: Topic :: Security
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: mcp>=1.2.0
|
|
30
|
+
Requires-Dist: httpx>=0.27.0
|
|
31
|
+
Requires-Dist: pydantic>=2.6.0
|
|
32
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
33
|
+
Dynamic: author
|
|
34
|
+
Dynamic: home-page
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
Dynamic: requires-python
|
|
37
|
+
|
|
38
|
+
# 🛡️ Odoo Safe MCP — The AI Firewall for Odoo ERP
|
|
39
|
+
|
|
40
|
+
Connect AI Agents (Claude Desktop, Cursor, n8n, LangChain) to **Odoo ERP (v14 to v19+)** without risking accounting disasters, accidental record deletions (`unlink`), or unauthorized invoice validations (`action_post`).
|
|
41
|
+
|
|
42
|
+
[](https://pypi.org/project/odoo-safe-mcp/)
|
|
43
|
+
[](https://www.odoo.com)
|
|
44
|
+
[](https://opensource.org/licenses/MIT)
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## ⚠️ Why you should NEVER connect a raw MCP server to Odoo
|
|
49
|
+
|
|
50
|
+
Odoo's native permissions were designed for humans, not LLMs. If you give an AI agent an API key for a standard sales or accounting user so it can read data or draft quotes, **that same key can execute `unlink` (delete records) or `action_post` (legally post invoices to tax authorities)**.
|
|
51
|
+
|
|
52
|
+
One LLM hallucination can corrupt your `account.move` ledger or wipe out `res.partner` records. **Odoo Safe MCP** acts as a deterministic firewall between your AI agent and Odoo's JSON-RPC/XML-RPC API.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 🚦 How the Security Traffic Light Works
|
|
57
|
+
|
|
58
|
+
| Risk Level | Odoo Methods | Action Taken | Tier |
|
|
59
|
+
| :--- | :--- | :--- | :--- |
|
|
60
|
+
| **🟢 GREEN (Safe Read)** | `search`, `read`, `search_read`, `fields_get`, `read_group`, `name_search`, `name_get`, `search_count`, `default_get` | Instant execution via local JSON-RPC. | **Free (Open Source)** |
|
|
61
|
+
| **🟡 YELLOW (Guarded Drafts)** | `create` on `crm.lead`, `sale.order`, `purchase.order` | Enforces `state='draft'` and injects an AI audit note into the Odoo Chatter (`message_post`). | **Pro Guard** |
|
|
62
|
+
| **🔴 RED (Critical / Destructive)** | `unlink` (any model), `action_post`, `action_confirm`, `button_validate`, `write` on `account.move` | **Pauses execution.** Sends an interactive **Approve / Reject** button to Slack or MS Teams. | **Pro Guard** |
|
|
63
|
+
| **⛔ BLACKLIST (Zero-Day Protection)** | Mutations on `res.users`, `res.groups`, `ir.rule`, `ir.model.access` | **Blocked unconditionally** (even with Pro). Prevents privilege escalation. | **Hard Block** |
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 🚀 Quickstart (2 Minutes)
|
|
68
|
+
|
|
69
|
+
### 1. Install via `pip` or run directly with `uvx`
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install "odoo-safe-mcp==2.0.0"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 2. Add to your Claude Desktop or Cursor configuration
|
|
76
|
+
|
|
77
|
+
Edit your `claude_desktop_config.json` (or `.cursor/mcp.json`):
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"mcpServers": {
|
|
82
|
+
"odoo-erp": {
|
|
83
|
+
"command": "uvx",
|
|
84
|
+
"args": ["odoo-safe-mcp@2.0.0"],
|
|
85
|
+
"env": {
|
|
86
|
+
"ODOO_URL": "https://your-company.odoo.com",
|
|
87
|
+
"ODOO_DB": "your-database-name",
|
|
88
|
+
"ODOO_USERNAME": "ai-bot@your-company.com",
|
|
89
|
+
"ODOO_API_KEY": "your_odoo_user_api_key",
|
|
90
|
+
"ODOO_DRY_RUN": "false",
|
|
91
|
+
"ODOO_SAFE_GUARD_KEY": "optional_pro_license_key_for_write_access"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
> ⚠️ **Production Best Practice (Version Pinning):** Always pin the exact version in `uvx` (`odoo-safe-mcp@2.0.0`). Running unpinned `uvx odoo-safe-mcp` downloads upstream changes on every launch, which could lead to breaking changes or unexpected behavior in automated production pipelines.
|
|
99
|
+
>
|
|
100
|
+
> **Note:** Without `ODOO_SAFE_GUARD_KEY`, the server runs in **Strict Read-Only Mode (🟢 Green Level)**. Set `"ODOO_DRY_RUN": "true"` to test and simulate `YELLOW` and `RED` policies locally with zero risk to your database.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 🔓 Unlock Guarded Write Access, AI Defender & Slack Approvals (Pro & Partner Tiers)
|
|
105
|
+
|
|
106
|
+
Need your agents to create draft quotes, prepare vendor bills, audit payloads with **Llama 3 8B AI Defender**, or request atomic human approval on Slack before posting invoices?
|
|
107
|
+
|
|
108
|
+
| Plan | Price | Databases | Key Features |
|
|
109
|
+
| :--- | :--- | :--- | :--- |
|
|
110
|
+
| **Community** | **€0 / forever** | 1 Local DB | Strict Read-Only Mode (🟢 Green), Zero-Risk Dry-Run Simulation (`ODOO_DRY_RUN=true`), Credential Sanitization |
|
|
111
|
+
| **Pro Guard** | **€49 / month** | Up to 5 DBs | Guarded Drafts (🟡 Yellow), Slack Interactive Approvals (🔴 Red), D1 Audit Log, **Llama 3 8B AI Defender Included** |
|
|
112
|
+
| **Partner Agency** | **€149 / month** | Up to 25 DBs | Multi-tenant Agency Isolation, Custom Slack Signing Secret, MS Teams Alerts, Llama 3 AI Defender |
|
|
113
|
+
|
|
114
|
+
1. Get your **License Key** at **[odoosafemcp.stream/#pricing](https://odoosafemcp.stream/#pricing)**.
|
|
115
|
+
2. Connect your Slack webhook in 1 click via the Cloudflare D1 Activation Portal.
|
|
116
|
+
3. Add `ODOO_SAFE_GUARD_KEY` to your environment variables.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 📄 License & Terms of Service
|
|
121
|
+
|
|
122
|
+
Distributed under the **MIT License** — Free forever for read-only local execution and Dry-Run simulation.
|
|
123
|
+
|
|
124
|
+
### Limitation of Liability, GDPR Privacy & Trademark Disclaimer
|
|
125
|
+
> **IMPORTANT NOTICE:** `odoo-safe-mcp` and its associated Enterprise Guard backend are provided on an **"AS IS" and "AS AVAILABLE"** basis without warranties of any kind, either express or implied, including but not limited to implied warranties of merchantability, fitness for a particular purpose, and non-infringement.
|
|
126
|
+
>
|
|
127
|
+
> 1. **Auxiliary Control Layer & Human-in-the-Loop Responsibility:** This software operates strictly as an auxiliary deterministic filter and does not replace native database security, accounting audits, or operational vigilance. The human supervisor who clicks **"Approve"** in Slack or Microsoft Teams is the sole legal and accounting authorizer of the resulting ERP mutation.
|
|
128
|
+
> 2. **Customer Responsibilities:** The user and customer retain sole and exclusive responsibility for configuring least-privilege permissions on the Odoo user account linked to `ODOO_API_KEY`, monitoring AI agent behavior, and maintaining independent, verified daily database backups.
|
|
129
|
+
> 3. **Liability Cap:** To the maximum extent permitted by applicable law, the cumulative aggregate liability of the authors, contributors, and operators for any direct, indirect, incidental, special, or consequential damages (including data loss, accounting discrepancy, lost revenue, or business interruption) shall be strictly capped at the total fee actually paid by the customer for the software during the **one (1) month immediately preceding the event** giving rise to liability (maximum €49 for Pro Guard, €149 for Partner Agency, or €0 for Community Tier).
|
|
130
|
+
> 4. **GDPR / RGPD Zero-Data-Retention Architecture:** Read-only (`GREEN`) queries execute locally on the user's machine and never transit through the Enterprise Guard cloud. Paused (`RED`) payloads are encrypted in transit and at rest using `AES-GCM-256` and are **purged immediately (`PURGED_AFTER_RESOLUTION`)** upon approval, rejection, or 24-hour TTL expiration. Audit logs store only cryptographic `SHA-256` payload hashes and technical metadata.
|
|
131
|
+
> 5. **Trademark & Non-Affiliation Disclaimer:** This project is an independent software product and is **not affiliated with, sponsored by, or endorsed by Odoo S.A.** `Odoo` is a registered trademark of Odoo S.A. `Slack` is a registered trademark of Slack Technologies, LLC. `Microsoft Teams` is a registered trademark of Microsoft Corporation.
|
|
132
|
+
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# 🛡️ Odoo Safe MCP — The AI Firewall for Odoo ERP
|
|
2
|
+
|
|
3
|
+
Connect AI Agents (Claude Desktop, Cursor, n8n, LangChain) to **Odoo ERP (v14 to v19+)** without risking accounting disasters, accidental record deletions (`unlink`), or unauthorized invoice validations (`action_post`).
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/odoo-safe-mcp/)
|
|
6
|
+
[](https://www.odoo.com)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## ⚠️ Why you should NEVER connect a raw MCP server to Odoo
|
|
12
|
+
|
|
13
|
+
Odoo's native permissions were designed for humans, not LLMs. If you give an AI agent an API key for a standard sales or accounting user so it can read data or draft quotes, **that same key can execute `unlink` (delete records) or `action_post` (legally post invoices to tax authorities)**.
|
|
14
|
+
|
|
15
|
+
One LLM hallucination can corrupt your `account.move` ledger or wipe out `res.partner` records. **Odoo Safe MCP** acts as a deterministic firewall between your AI agent and Odoo's JSON-RPC/XML-RPC API.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 🚦 How the Security Traffic Light Works
|
|
20
|
+
|
|
21
|
+
| Risk Level | Odoo Methods | Action Taken | Tier |
|
|
22
|
+
| :--- | :--- | :--- | :--- |
|
|
23
|
+
| **🟢 GREEN (Safe Read)** | `search`, `read`, `search_read`, `fields_get`, `read_group`, `name_search`, `name_get`, `search_count`, `default_get` | Instant execution via local JSON-RPC. | **Free (Open Source)** |
|
|
24
|
+
| **🟡 YELLOW (Guarded Drafts)** | `create` on `crm.lead`, `sale.order`, `purchase.order` | Enforces `state='draft'` and injects an AI audit note into the Odoo Chatter (`message_post`). | **Pro Guard** |
|
|
25
|
+
| **🔴 RED (Critical / Destructive)** | `unlink` (any model), `action_post`, `action_confirm`, `button_validate`, `write` on `account.move` | **Pauses execution.** Sends an interactive **Approve / Reject** button to Slack or MS Teams. | **Pro Guard** |
|
|
26
|
+
| **⛔ BLACKLIST (Zero-Day Protection)** | Mutations on `res.users`, `res.groups`, `ir.rule`, `ir.model.access` | **Blocked unconditionally** (even with Pro). Prevents privilege escalation. | **Hard Block** |
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 🚀 Quickstart (2 Minutes)
|
|
31
|
+
|
|
32
|
+
### 1. Install via `pip` or run directly with `uvx`
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install "odoo-safe-mcp==2.0.0"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2. Add to your Claude Desktop or Cursor configuration
|
|
39
|
+
|
|
40
|
+
Edit your `claude_desktop_config.json` (or `.cursor/mcp.json`):
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"mcpServers": {
|
|
45
|
+
"odoo-erp": {
|
|
46
|
+
"command": "uvx",
|
|
47
|
+
"args": ["odoo-safe-mcp@2.0.0"],
|
|
48
|
+
"env": {
|
|
49
|
+
"ODOO_URL": "https://your-company.odoo.com",
|
|
50
|
+
"ODOO_DB": "your-database-name",
|
|
51
|
+
"ODOO_USERNAME": "ai-bot@your-company.com",
|
|
52
|
+
"ODOO_API_KEY": "your_odoo_user_api_key",
|
|
53
|
+
"ODOO_DRY_RUN": "false",
|
|
54
|
+
"ODOO_SAFE_GUARD_KEY": "optional_pro_license_key_for_write_access"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
> ⚠️ **Production Best Practice (Version Pinning):** Always pin the exact version in `uvx` (`odoo-safe-mcp@2.0.0`). Running unpinned `uvx odoo-safe-mcp` downloads upstream changes on every launch, which could lead to breaking changes or unexpected behavior in automated production pipelines.
|
|
62
|
+
>
|
|
63
|
+
> **Note:** Without `ODOO_SAFE_GUARD_KEY`, the server runs in **Strict Read-Only Mode (🟢 Green Level)**. Set `"ODOO_DRY_RUN": "true"` to test and simulate `YELLOW` and `RED` policies locally with zero risk to your database.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 🔓 Unlock Guarded Write Access, AI Defender & Slack Approvals (Pro & Partner Tiers)
|
|
68
|
+
|
|
69
|
+
Need your agents to create draft quotes, prepare vendor bills, audit payloads with **Llama 3 8B AI Defender**, or request atomic human approval on Slack before posting invoices?
|
|
70
|
+
|
|
71
|
+
| Plan | Price | Databases | Key Features |
|
|
72
|
+
| :--- | :--- | :--- | :--- |
|
|
73
|
+
| **Community** | **€0 / forever** | 1 Local DB | Strict Read-Only Mode (🟢 Green), Zero-Risk Dry-Run Simulation (`ODOO_DRY_RUN=true`), Credential Sanitization |
|
|
74
|
+
| **Pro Guard** | **€49 / month** | Up to 5 DBs | Guarded Drafts (🟡 Yellow), Slack Interactive Approvals (🔴 Red), D1 Audit Log, **Llama 3 8B AI Defender Included** |
|
|
75
|
+
| **Partner Agency** | **€149 / month** | Up to 25 DBs | Multi-tenant Agency Isolation, Custom Slack Signing Secret, MS Teams Alerts, Llama 3 AI Defender |
|
|
76
|
+
|
|
77
|
+
1. Get your **License Key** at **[odoosafemcp.stream/#pricing](https://odoosafemcp.stream/#pricing)**.
|
|
78
|
+
2. Connect your Slack webhook in 1 click via the Cloudflare D1 Activation Portal.
|
|
79
|
+
3. Add `ODOO_SAFE_GUARD_KEY` to your environment variables.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 📄 License & Terms of Service
|
|
84
|
+
|
|
85
|
+
Distributed under the **MIT License** — Free forever for read-only local execution and Dry-Run simulation.
|
|
86
|
+
|
|
87
|
+
### Limitation of Liability, GDPR Privacy & Trademark Disclaimer
|
|
88
|
+
> **IMPORTANT NOTICE:** `odoo-safe-mcp` and its associated Enterprise Guard backend are provided on an **"AS IS" and "AS AVAILABLE"** basis without warranties of any kind, either express or implied, including but not limited to implied warranties of merchantability, fitness for a particular purpose, and non-infringement.
|
|
89
|
+
>
|
|
90
|
+
> 1. **Auxiliary Control Layer & Human-in-the-Loop Responsibility:** This software operates strictly as an auxiliary deterministic filter and does not replace native database security, accounting audits, or operational vigilance. The human supervisor who clicks **"Approve"** in Slack or Microsoft Teams is the sole legal and accounting authorizer of the resulting ERP mutation.
|
|
91
|
+
> 2. **Customer Responsibilities:** The user and customer retain sole and exclusive responsibility for configuring least-privilege permissions on the Odoo user account linked to `ODOO_API_KEY`, monitoring AI agent behavior, and maintaining independent, verified daily database backups.
|
|
92
|
+
> 3. **Liability Cap:** To the maximum extent permitted by applicable law, the cumulative aggregate liability of the authors, contributors, and operators for any direct, indirect, incidental, special, or consequential damages (including data loss, accounting discrepancy, lost revenue, or business interruption) shall be strictly capped at the total fee actually paid by the customer for the software during the **one (1) month immediately preceding the event** giving rise to liability (maximum €49 for Pro Guard, €149 for Partner Agency, or €0 for Community Tier).
|
|
93
|
+
> 4. **GDPR / RGPD Zero-Data-Retention Architecture:** Read-only (`GREEN`) queries execute locally on the user's machine and never transit through the Enterprise Guard cloud. Paused (`RED`) payloads are encrypted in transit and at rest using `AES-GCM-256` and are **purged immediately (`PURGED_AFTER_RESOLUTION`)** upon approval, rejection, or 24-hour TTL expiration. Audit logs store only cryptographic `SHA-256` payload hashes and technical metadata.
|
|
94
|
+
> 5. **Trademark & Non-Affiliation Disclaimer:** This project is an independent software product and is **not affiliated with, sponsored by, or endorsed by Odoo S.A.** `Odoo` is a registered trademark of Odoo S.A. `Slack` is a registered trademark of Slack Technologies, LLC. `Microsoft Teams` is a registered trademark of Microsoft Corporation.
|
|
95
|
+
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rules Engine & Traffic Light System for Odoo Safe MCP.
|
|
3
|
+
|
|
4
|
+
Classifies Odoo RPC calls into:
|
|
5
|
+
- GREEN: Safe Read (Free Open Source, instant local JSON-RPC execution)
|
|
6
|
+
- YELLOW: Guarded Drafts (Forces state='draft' and Chatter audit note)
|
|
7
|
+
- RED: Critical / Destructive / Unknown / Wizards (Requires human Slack approval)
|
|
8
|
+
- FORBIDDEN / BLOCKED: Permanent lockdown for sensitive models (res.users, ir.config_parameter, etc.)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import copy
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RiskLevel(str, Enum):
|
|
20
|
+
GREEN = "GREEN" # Safe read
|
|
21
|
+
YELLOW = "YELLOW" # Guarded draft creation
|
|
22
|
+
RED = "RED" # Critical / destructive / state change / wizards / unknown
|
|
23
|
+
BLOCKED = "BLOCKED" # Privilege escalation / security violation (alias)
|
|
24
|
+
FORBIDDEN = "FORBIDDEN" # Forbidden operation (res.users, security models)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Methods safe for read-only consumption on non-sensitive business models
|
|
28
|
+
GREEN_METHODS = {
|
|
29
|
+
"search",
|
|
30
|
+
"read",
|
|
31
|
+
"search_read",
|
|
32
|
+
"fields_get",
|
|
33
|
+
"read_group",
|
|
34
|
+
"name_search",
|
|
35
|
+
"name_get",
|
|
36
|
+
"search_count",
|
|
37
|
+
"default_get",
|
|
38
|
+
"check_access_rights",
|
|
39
|
+
"fields_view_get",
|
|
40
|
+
"get_views",
|
|
41
|
+
"load_views",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Models where ANY access (even READ) is strictly forbidden to prevent credential/secret exfiltration
|
|
45
|
+
READ_AND_WRITE_FORBIDDEN_MODELS = {
|
|
46
|
+
"res.users",
|
|
47
|
+
"res.apikeys",
|
|
48
|
+
"ir.config_parameter",
|
|
49
|
+
"ir.mail_server",
|
|
50
|
+
"fetchmail.server",
|
|
51
|
+
"payment.provider",
|
|
52
|
+
"payment.acquirer",
|
|
53
|
+
"payment.token",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Critical system models where mutations (create/write/unlink/custom) are strictly forbidden
|
|
57
|
+
PROTECTED_SYSTEM_MODELS = READ_AND_WRITE_FORBIDDEN_MODELS | {
|
|
58
|
+
"res.groups",
|
|
59
|
+
"ir.model.access",
|
|
60
|
+
"ir.rule",
|
|
61
|
+
"ir.module.module",
|
|
62
|
+
"ir.actions.server",
|
|
63
|
+
"ir.cron",
|
|
64
|
+
"base.automation",
|
|
65
|
+
"ir.model",
|
|
66
|
+
"ir.model.fields",
|
|
67
|
+
"ir.ui.view",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# Backward compatibility alias
|
|
71
|
+
BLACKLIST_MODELS = PROTECTED_SYSTEM_MODELS
|
|
72
|
+
|
|
73
|
+
# Sensitive field names that must never be read via fields list
|
|
74
|
+
FORBIDDEN_SENSITIVE_FIELDS = {
|
|
75
|
+
"password",
|
|
76
|
+
"password_crypt",
|
|
77
|
+
"api_key",
|
|
78
|
+
"totp_secret",
|
|
79
|
+
"oauth_access_token",
|
|
80
|
+
"smtp_pass",
|
|
81
|
+
"stripe_secret_key",
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Strict Whitelist: Only these models can be created under YELLOW (Guarded Drafts)
|
|
85
|
+
YELLOW_CREATE_MODELS = {
|
|
86
|
+
"sale.order",
|
|
87
|
+
"purchase.order",
|
|
88
|
+
"crm.lead",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Critical methods that always trigger RED level alerts
|
|
92
|
+
CRITICAL_RED_METHODS = {
|
|
93
|
+
"unlink",
|
|
94
|
+
"action_post",
|
|
95
|
+
"action_confirm",
|
|
96
|
+
"button_validate",
|
|
97
|
+
"action_invoice_open",
|
|
98
|
+
"action_invoice_paid",
|
|
99
|
+
"action_cancel",
|
|
100
|
+
"button_immediate_transfer",
|
|
101
|
+
"button_approve",
|
|
102
|
+
"copy",
|
|
103
|
+
"web_save",
|
|
104
|
+
"name_create",
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
UPGRADE_URL = "https://odoosafemcp.stream/#pricing"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class RuleEvaluationResult:
|
|
112
|
+
level: RiskLevel
|
|
113
|
+
allowed: bool
|
|
114
|
+
requires_guard_proxy: bool
|
|
115
|
+
reason: str
|
|
116
|
+
sanitized_args: List[Any] = field(default_factory=list)
|
|
117
|
+
sanitized_kwargs: Dict[str, Any] = field(default_factory=dict)
|
|
118
|
+
audit_note: Optional[str] = None
|
|
119
|
+
inject_chatter_note: bool = False
|
|
120
|
+
|
|
121
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
122
|
+
return {
|
|
123
|
+
"level": self.level.value,
|
|
124
|
+
"allowed": self.allowed,
|
|
125
|
+
"requires_guard_proxy": self.requires_guard_proxy,
|
|
126
|
+
"reason": self.reason,
|
|
127
|
+
"sanitized_args": self.sanitized_args,
|
|
128
|
+
"sanitized_kwargs": self.sanitized_kwargs,
|
|
129
|
+
"audit_note": self.audit_note,
|
|
130
|
+
"inject_chatter_note": self.inject_chatter_note,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _enforce_draft_on_dict(item: Dict[str, Any], model: str) -> None:
|
|
135
|
+
if model in {"sale.order", "purchase.order"}:
|
|
136
|
+
item["state"] = "draft"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _sanitize_yellow_payload(
|
|
140
|
+
model: str, method: str, args: List[Any], kwargs: Dict[str, Any]
|
|
141
|
+
) -> Tuple[List[Any], Dict[str, Any], str]:
|
|
142
|
+
"""
|
|
143
|
+
Enforces state='draft' for sales and purchase orders across positional args,
|
|
144
|
+
keyword vals/vals_list, and context default_state to prevent bypasses.
|
|
145
|
+
"""
|
|
146
|
+
new_args = copy.deepcopy(args)
|
|
147
|
+
new_kwargs = copy.deepcopy(kwargs)
|
|
148
|
+
audit_note = f"[Odoo Safe MCP] Created via AI Agent ({model}.{method}) under Guarded Drafts policy."
|
|
149
|
+
|
|
150
|
+
if method == "create":
|
|
151
|
+
# 1. Sanitize positional args[0]
|
|
152
|
+
if new_args:
|
|
153
|
+
first_arg = new_args[0]
|
|
154
|
+
if isinstance(first_arg, dict):
|
|
155
|
+
_enforce_draft_on_dict(first_arg, model)
|
|
156
|
+
new_args[0] = first_arg
|
|
157
|
+
elif isinstance(first_arg, list):
|
|
158
|
+
for item in first_arg:
|
|
159
|
+
if isinstance(item, dict):
|
|
160
|
+
_enforce_draft_on_dict(item, model)
|
|
161
|
+
new_args[0] = first_arg
|
|
162
|
+
|
|
163
|
+
# 2. Sanitize kwargs['vals'] or kwargs['vals_list'] (prevents kwargs bypass)
|
|
164
|
+
if isinstance(new_kwargs.get("vals"), dict):
|
|
165
|
+
_enforce_draft_on_dict(new_kwargs["vals"], model)
|
|
166
|
+
elif isinstance(new_kwargs.get("vals"), list):
|
|
167
|
+
for item in new_kwargs["vals"]:
|
|
168
|
+
if isinstance(item, dict):
|
|
169
|
+
_enforce_draft_on_dict(item, model)
|
|
170
|
+
|
|
171
|
+
if isinstance(new_kwargs.get("vals_list"), list):
|
|
172
|
+
for item in new_kwargs["vals_list"]:
|
|
173
|
+
if isinstance(item, dict):
|
|
174
|
+
_enforce_draft_on_dict(item, model)
|
|
175
|
+
|
|
176
|
+
# 3. Sanitize kwargs['context']['default_state']
|
|
177
|
+
if isinstance(new_kwargs.get("context"), dict) and model in {"sale.order", "purchase.order"}:
|
|
178
|
+
if "default_state" in new_kwargs["context"]:
|
|
179
|
+
new_kwargs["context"]["default_state"] = "draft"
|
|
180
|
+
|
|
181
|
+
return new_args, new_kwargs, audit_note
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _sanitize_green_read_kwargs(kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
|
185
|
+
"""Strip any forbidden credential fields from 'fields' parameter in read/search_read."""
|
|
186
|
+
new_kwargs = copy.deepcopy(kwargs)
|
|
187
|
+
fields_param = new_kwargs.get("fields")
|
|
188
|
+
if isinstance(fields_param, list):
|
|
189
|
+
new_kwargs["fields"] = [
|
|
190
|
+
f for f in fields_param
|
|
191
|
+
if isinstance(f, str) and f.strip().lower() not in FORBIDDEN_SENSITIVE_FIELDS
|
|
192
|
+
]
|
|
193
|
+
return new_kwargs
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def evaluate_odoo_call(
|
|
197
|
+
model: str,
|
|
198
|
+
method: str,
|
|
199
|
+
args: Optional[List[Any]] = None,
|
|
200
|
+
kwargs: Optional[Dict[str, Any]] = None,
|
|
201
|
+
) -> RuleEvaluationResult:
|
|
202
|
+
"""
|
|
203
|
+
Evaluates whether an Odoo execute_kw call is permitted, its intrinsic risk tier,
|
|
204
|
+
and applies automated safety sanitization.
|
|
205
|
+
|
|
206
|
+
Returns a RuleEvaluationResult with level: GREEN, YELLOW, RED, or FORBIDDEN.
|
|
207
|
+
"""
|
|
208
|
+
args = list(args) if args is not None else []
|
|
209
|
+
kwargs = dict(kwargs) if kwargs is not None else {}
|
|
210
|
+
norm_model = (model or "").strip().lower()
|
|
211
|
+
norm_method = (method or "").strip().lower()
|
|
212
|
+
|
|
213
|
+
# 0. Block empty or private/dunder method calls (e.g. '_check_credentials', '__import__')
|
|
214
|
+
if not norm_model or not norm_method or norm_method.startswith("_"):
|
|
215
|
+
return RuleEvaluationResult(
|
|
216
|
+
level=RiskLevel.FORBIDDEN,
|
|
217
|
+
allowed=False,
|
|
218
|
+
requires_guard_proxy=False,
|
|
219
|
+
reason=(
|
|
220
|
+
f"⛔ SECURITY ALERT: Calling private or invalid method '{method}' on '{model}' "
|
|
221
|
+
"is permanently FORBIDDEN."
|
|
222
|
+
),
|
|
223
|
+
sanitized_args=args,
|
|
224
|
+
sanitized_kwargs=kwargs,
|
|
225
|
+
inject_chatter_note=False,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# 1. FORBIDDEN: Block reading OR writing secret/credential models (res.users, ir.config_parameter, etc.)
|
|
229
|
+
if norm_model in READ_AND_WRITE_FORBIDDEN_MODELS:
|
|
230
|
+
return RuleEvaluationResult(
|
|
231
|
+
level=RiskLevel.FORBIDDEN,
|
|
232
|
+
allowed=False,
|
|
233
|
+
requires_guard_proxy=False,
|
|
234
|
+
reason=(
|
|
235
|
+
f"⛔ SECURITY ALERT: Direct access to sensitive model '{norm_model}' is permanently FORBIDDEN "
|
|
236
|
+
"by Odoo Safe MCP to prevent credential/token exfiltration or privilege escalation."
|
|
237
|
+
),
|
|
238
|
+
sanitized_args=args,
|
|
239
|
+
sanitized_kwargs=kwargs,
|
|
240
|
+
inject_chatter_note=False,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# 2. FORBIDDEN: Prevent mutation of access control, groups, modules, views, automations and crons
|
|
244
|
+
if norm_model in PROTECTED_SYSTEM_MODELS:
|
|
245
|
+
if norm_method not in GREEN_METHODS:
|
|
246
|
+
return RuleEvaluationResult(
|
|
247
|
+
level=RiskLevel.FORBIDDEN,
|
|
248
|
+
allowed=False,
|
|
249
|
+
requires_guard_proxy=False,
|
|
250
|
+
reason=(
|
|
251
|
+
f"⛔ SECURITY ALERT: Direct mutations on sensitive system model '{model}' "
|
|
252
|
+
f"(method: '{method}') are permanently FORBIDDEN by Odoo Safe MCP to prevent "
|
|
253
|
+
"privilege escalation, even with a Pro license."
|
|
254
|
+
),
|
|
255
|
+
sanitized_args=args,
|
|
256
|
+
sanitized_kwargs=kwargs,
|
|
257
|
+
inject_chatter_note=False,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# 3. YELLOW TIER: Strict Whitelist - Only guarded draft creation on sales, purchases, leads
|
|
261
|
+
if (norm_model in YELLOW_CREATE_MODELS) and (norm_method == "create"):
|
|
262
|
+
san_args, san_kwargs, note = _sanitize_yellow_payload(
|
|
263
|
+
norm_model, norm_method, args, kwargs
|
|
264
|
+
)
|
|
265
|
+
return RuleEvaluationResult(
|
|
266
|
+
level=RiskLevel.YELLOW,
|
|
267
|
+
allowed=True,
|
|
268
|
+
requires_guard_proxy=True,
|
|
269
|
+
reason="Guarded Draft Creation: enforced state='draft' and AI audit note.",
|
|
270
|
+
sanitized_args=san_args,
|
|
271
|
+
sanitized_kwargs=san_kwargs,
|
|
272
|
+
audit_note=note,
|
|
273
|
+
inject_chatter_note=True,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# 4. GREEN TIER: Safe Read Operations on standard business models
|
|
277
|
+
if norm_method in GREEN_METHODS:
|
|
278
|
+
san_kwargs = _sanitize_green_read_kwargs(kwargs)
|
|
279
|
+
return RuleEvaluationResult(
|
|
280
|
+
level=RiskLevel.GREEN,
|
|
281
|
+
allowed=True,
|
|
282
|
+
requires_guard_proxy=False,
|
|
283
|
+
reason="Safe read-only operation. Executed instantly via local JSON-RPC.",
|
|
284
|
+
sanitized_args=args,
|
|
285
|
+
sanitized_kwargs=san_kwargs,
|
|
286
|
+
inject_chatter_note=False,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# 5. RED TIER: All other mutations, accounting, deletions, wizards, or unknown methods (Fail-safe to RED)
|
|
290
|
+
is_critical = norm_method in CRITICAL_RED_METHODS or norm_model.startswith("account.")
|
|
291
|
+
reason = (
|
|
292
|
+
f"Critical operation ('{norm_model}.{norm_method}'): Human-in-the-Loop approval required via Slack."
|
|
293
|
+
if is_critical
|
|
294
|
+
else f"Unguarded mutation or wizard ('{norm_model}.{norm_method}'): Routed through Pro Guard Proxy."
|
|
295
|
+
)
|
|
296
|
+
return RuleEvaluationResult(
|
|
297
|
+
level=RiskLevel.RED,
|
|
298
|
+
allowed=True,
|
|
299
|
+
requires_guard_proxy=True,
|
|
300
|
+
reason=reason,
|
|
301
|
+
sanitized_args=args,
|
|
302
|
+
sanitized_kwargs=kwargs,
|
|
303
|
+
inject_chatter_note=True,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def evaluate_rule(
|
|
308
|
+
model: str,
|
|
309
|
+
method: str,
|
|
310
|
+
args: Optional[List[Any]] = None,
|
|
311
|
+
kwargs: Optional[Dict[str, Any]] = None,
|
|
312
|
+
has_pro_license: bool = False,
|
|
313
|
+
) -> RuleEvaluationResult:
|
|
314
|
+
"""
|
|
315
|
+
Evaluates whether an Odoo execute_kw call is permitted in the MCP server context,
|
|
316
|
+
taking into account the client's Pro License status.
|
|
317
|
+
"""
|
|
318
|
+
decision = evaluate_odoo_call(model=model, method=method, args=args, kwargs=kwargs)
|
|
319
|
+
|
|
320
|
+
if decision.level in (RiskLevel.FORBIDDEN, RiskLevel.BLOCKED):
|
|
321
|
+
return decision
|
|
322
|
+
|
|
323
|
+
if decision.level == RiskLevel.GREEN:
|
|
324
|
+
return decision
|
|
325
|
+
|
|
326
|
+
# If mutation is YELLOW or RED but user has no Pro license -> Strict Read-Only Mode
|
|
327
|
+
if not has_pro_license:
|
|
328
|
+
return RuleEvaluationResult(
|
|
329
|
+
level=decision.level,
|
|
330
|
+
allowed=False,
|
|
331
|
+
requires_guard_proxy=False,
|
|
332
|
+
reason=(
|
|
333
|
+
f"🔒 Odoo Safe MCP (Free Tier) operates in Strict Read-Only Mode. "
|
|
334
|
+
f"Method '{method}' on model '{model}' is classified as {decision.level.value}.\n"
|
|
335
|
+
f"To enable guarded write access and Human-in-the-Loop Slack approvals, "
|
|
336
|
+
f"get a Pro API Key at {UPGRADE_URL} and configure ODOO_SAFE_GUARD_KEY."
|
|
337
|
+
),
|
|
338
|
+
sanitized_args=decision.sanitized_args,
|
|
339
|
+
sanitized_kwargs=decision.sanitized_kwargs,
|
|
340
|
+
inject_chatter_note=False,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
return decision
|