lambda-erp 0.8.28__tar.gz → 0.8.29__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.
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/PKG-INFO +1 -1
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/chat.py +14 -5
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/mcp.py +5 -28
- lambda_erp-0.8.29/api/tool_permissions.py +52 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/pyproject.toml +1 -1
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/.gitignore +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/LICENSE +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/README.md +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/__init__.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/attachments.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/auth.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/bootstrap.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/demo_limits.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/deps.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/errors.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/list_values.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/main.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/oauth.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/pdf.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/providers.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/remarks_md.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/__init__.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/accounting.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/actions.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/admin.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/analytics.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/availability.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/bank_statements.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/chat_api.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/documents.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/masters.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/proposals.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/reports.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/setup.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/services.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/templates/document.html +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/templates/proposal.html +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/docs/agents/README.md +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/frontend/LICENSE +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/frontend/README.md +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/frontend/src/api/client.ts +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/bank_account.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/bank_reconciliation.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/bank_statement_import.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/bank_transaction.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/camt.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/chart_of_accounts.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/__init__.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/engine.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/pack.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/packs/ch.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/packs/generic.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/profiles.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/spine.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/assets/__init__.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/assets/asset.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/assets/reservation.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/database.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/model.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/selling/proposal.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/utils.py +0 -0
- {lambda_erp-0.8.28 → lambda_erp-0.8.29}/terraform/README.md +0 -0
|
@@ -27,6 +27,7 @@ from fastapi import APIRouter, Depends as _Depends, HTTPException, WebSocket, We
|
|
|
27
27
|
from openai import OpenAI
|
|
28
28
|
|
|
29
29
|
from api import services
|
|
30
|
+
from api.tool_permissions import tool_allowed, tool_permission_error
|
|
30
31
|
from api.demo_limits import (
|
|
31
32
|
demo_call_reserve_usd,
|
|
32
33
|
demo_max_completion_tokens,
|
|
@@ -1438,10 +1439,12 @@ def build_tools(user_info: dict | None = None):
|
|
|
1438
1439
|
role = user_info.get("role")
|
|
1439
1440
|
action_tools = [
|
|
1440
1441
|
tool for tool in action_tools
|
|
1441
|
-
if
|
|
1442
|
+
if tool_allowed(tool["function"]["name"], role)
|
|
1442
1443
|
]
|
|
1443
|
-
|
|
1444
|
-
|
|
1444
|
+
hidden_tools = {
|
|
1445
|
+
tool["function"]["name"] for tool in TOOLS
|
|
1446
|
+
if not tool_allowed(tool["function"]["name"], role)
|
|
1447
|
+
}
|
|
1445
1448
|
if not extra_docs and not extra_masters and not action_tools and not hidden_tools:
|
|
1446
1449
|
return TOOLS
|
|
1447
1450
|
tools = copy.deepcopy(TOOLS)
|
|
@@ -3556,7 +3559,8 @@ async def run_thinking_loop(
|
|
|
3556
3559
|
# If GPT is delegating report code-gen to the Anthropic specialist,
|
|
3557
3560
|
# surface the handoff in the UI.
|
|
3558
3561
|
will_delegate_to_code_specialist = (
|
|
3559
|
-
fn_name
|
|
3562
|
+
tool_allowed(fn_name, user_role)
|
|
3563
|
+
and fn_name in ("create_custom_analytics_report", "update_custom_analytics_report")
|
|
3560
3564
|
and not fn_args.get("transform_js")
|
|
3561
3565
|
and not fn_args.get("data_requests")
|
|
3562
3566
|
)
|
|
@@ -3569,7 +3573,12 @@ async def run_thinking_loop(
|
|
|
3569
3573
|
})
|
|
3570
3574
|
|
|
3571
3575
|
handler = tool_handlers.get(fn_name)
|
|
3572
|
-
|
|
3576
|
+
# The model's tool list and prompt are not authorization controls.
|
|
3577
|
+
# Check the caller at execution even for invented or replayed calls.
|
|
3578
|
+
if not tool_allowed(fn_name, user_role):
|
|
3579
|
+
result = tool_permission_error(fn_name, user_role)
|
|
3580
|
+
success = False
|
|
3581
|
+
elif not handler:
|
|
3573
3582
|
result = {"error": f"Unknown tool: {fn_name}"}
|
|
3574
3583
|
success = False
|
|
3575
3584
|
else:
|
|
@@ -25,6 +25,7 @@ from api.auth import get_current_user
|
|
|
25
25
|
from api import services
|
|
26
26
|
from api import chat as chat_mod
|
|
27
27
|
from api.chat import TOOL_HANDLERS, build_tools
|
|
28
|
+
from api.tool_permissions import tool_allowed
|
|
28
29
|
|
|
29
30
|
router = APIRouter(tags=["mcp"])
|
|
30
31
|
|
|
@@ -42,37 +43,10 @@ _EXCLUDE = {
|
|
|
42
43
|
"plan_company_setup",
|
|
43
44
|
"apply_company_setup",
|
|
44
45
|
}
|
|
45
|
-
# Mirror the REST permission model: reads are viewer+, writes are manager+,
|
|
46
|
-
# delete_master is admin-only (the handler also re-checks).
|
|
47
|
-
_WRITE = {
|
|
48
|
-
"create_document", "update_document", "batch_update_documents",
|
|
49
|
-
"submit_document", "cancel_document",
|
|
50
|
-
"discard_document", "convert_document", "create_master", "update_master",
|
|
51
|
-
"reconcile_bank_transaction", "undo_bank_reconciliation",
|
|
52
|
-
}
|
|
53
|
-
_ADMIN = {"delete_master"}
|
|
54
|
-
_MANAGER_ONLY = {
|
|
55
|
-
"list_bank_reconciliation_queue", "suggest_bank_reconciliation",
|
|
56
|
-
"reconcile_bank_transaction", "undo_bank_reconciliation",
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def _can_write(role) -> bool:
|
|
61
|
-
return role in ("manager", "admin", "public_manager")
|
|
62
46
|
|
|
63
47
|
|
|
64
48
|
def _allowed(name: str, role) -> bool:
|
|
65
|
-
|
|
66
|
-
return False
|
|
67
|
-
if name in services.REGISTERED_ACTIONS:
|
|
68
|
-
return services.registered_action_allowed(name, role)
|
|
69
|
-
if name in _ADMIN:
|
|
70
|
-
return role == "admin"
|
|
71
|
-
if name in _MANAGER_ONLY:
|
|
72
|
-
return role in ("manager", "admin")
|
|
73
|
-
if name in _WRITE:
|
|
74
|
-
return _can_write(role)
|
|
75
|
-
return True
|
|
49
|
+
return name not in _EXCLUDE and tool_allowed(name, role)
|
|
76
50
|
|
|
77
51
|
|
|
78
52
|
def _require_caller(request: Request) -> dict:
|
|
@@ -99,6 +73,9 @@ def _tools(role) -> list:
|
|
|
99
73
|
|
|
100
74
|
|
|
101
75
|
def _call(name: str, args: dict, user: dict):
|
|
76
|
+
# Preserve MCP's unknown-tool error without dispatching an unclassified tool.
|
|
77
|
+
if name not in TOOL_HANDLERS and name != "delete_master" and name not in services.REGISTERED_ACTIONS:
|
|
78
|
+
raise KeyError(name)
|
|
102
79
|
role = user.get("role")
|
|
103
80
|
if not _allowed(name, role):
|
|
104
81
|
return {"error": f"'{name}' is not available to a {role or 'viewer'} key."}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Shared execution policy for ERP tools exposed through chat and MCP.
|
|
2
|
+
|
|
3
|
+
Every built-in tool must be classified here. Unknown tools and unknown callers
|
|
4
|
+
are denied; registered extension actions carry their own explicit policy.
|
|
5
|
+
Session ownership and other resource checks still belong to their handlers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from api import services
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_READERS = frozenset({"viewer", "manager", "admin", "public_manager"})
|
|
12
|
+
_DOCUMENT_WRITERS = frozenset({"manager", "admin", "public_manager"})
|
|
13
|
+
_MANAGERS = frozenset({"manager", "admin"})
|
|
14
|
+
_ADMINS = frozenset({"admin"})
|
|
15
|
+
|
|
16
|
+
TOOL_ROLES = {
|
|
17
|
+
**dict.fromkeys((
|
|
18
|
+
"list_documents", "get_document_fields", "get_document",
|
|
19
|
+
"get_master_fields", "search_masters", "get_report", "get_current_time",
|
|
20
|
+
"retrieve_chat_history", "list_chat_attachments", "retrieve_chat_attachment",
|
|
21
|
+
"preview_bank_statement_attachments", "query_dataset",
|
|
22
|
+
# Personal report drafts follow the viewer-accessible reporting API.
|
|
23
|
+
# Their handlers enforce ownership; these do not post ERP transactions.
|
|
24
|
+
"create_custom_analytics_report", "get_custom_analytics_report",
|
|
25
|
+
"update_custom_analytics_report",
|
|
26
|
+
), _READERS),
|
|
27
|
+
**dict.fromkeys((
|
|
28
|
+
"create_document", "update_document", "batch_update_documents",
|
|
29
|
+
"submit_document", "cancel_document", "discard_document", "convert_document",
|
|
30
|
+
), _DOCUMENT_WRITERS),
|
|
31
|
+
**dict.fromkeys((
|
|
32
|
+
"create_master", "update_master", "revalue_currencies",
|
|
33
|
+
"import_bank_statement_attachments", "list_bank_reconciliation_queue",
|
|
34
|
+
"suggest_bank_reconciliation", "reconcile_bank_transaction",
|
|
35
|
+
"undo_bank_reconciliation",
|
|
36
|
+
), _MANAGERS),
|
|
37
|
+
**dict.fromkeys((
|
|
38
|
+
"delete_master", "plan_company_setup", "apply_company_setup",
|
|
39
|
+
), _ADMINS),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def tool_allowed(name: str, role: str | None) -> bool:
|
|
44
|
+
if role not in _READERS:
|
|
45
|
+
return False
|
|
46
|
+
if name in TOOL_ROLES:
|
|
47
|
+
return role in TOOL_ROLES[name]
|
|
48
|
+
return services.registered_action_allowed(name, role)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def tool_permission_error(name: str, role: str | None) -> dict:
|
|
52
|
+
return {"error": f"Tool '{name}' is not available to role '{role or 'unauthenticated'}'."}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|