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.
Files changed (96) hide show
  1. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/PKG-INFO +1 -1
  2. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/chat.py +14 -5
  3. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/mcp.py +5 -28
  4. lambda_erp-0.8.29/api/tool_permissions.py +52 -0
  5. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/pyproject.toml +1 -1
  6. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/.gitignore +0 -0
  7. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/LICENSE +0 -0
  8. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/README.md +0 -0
  9. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/__init__.py +0 -0
  10. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/attachments.py +0 -0
  11. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/auth.py +0 -0
  12. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/bootstrap.py +0 -0
  13. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/demo_limits.py +0 -0
  14. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/deps.py +0 -0
  15. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/errors.py +0 -0
  16. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/list_values.py +0 -0
  17. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/main.py +0 -0
  18. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/oauth.py +0 -0
  19. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/pdf.py +0 -0
  20. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/providers.py +0 -0
  21. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/remarks_md.py +0 -0
  22. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/__init__.py +0 -0
  23. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/accounting.py +0 -0
  24. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/actions.py +0 -0
  25. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/admin.py +0 -0
  26. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/analytics.py +0 -0
  27. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/availability.py +0 -0
  28. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/bank_reconciliation.py +0 -0
  29. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/bank_statements.py +0 -0
  30. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/chat_api.py +0 -0
  31. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/documents.py +0 -0
  32. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/masters.py +0 -0
  33. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/proposals.py +0 -0
  34. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/reports.py +0 -0
  35. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/routers/setup.py +0 -0
  36. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/services.py +0 -0
  37. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/templates/document.html +0 -0
  38. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/api/templates/proposal.html +0 -0
  39. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/docs/agents/README.md +0 -0
  40. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/frontend/LICENSE +0 -0
  41. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/frontend/README.md +0 -0
  42. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/frontend/src/api/client.ts +0 -0
  43. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/__init__.py +0 -0
  44. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/__init__.py +0 -0
  45. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/bank_account.py +0 -0
  46. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/bank_reconciliation.py +0 -0
  47. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/bank_statement_import.py +0 -0
  48. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/bank_transaction.py +0 -0
  49. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/budget.py +0 -0
  50. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/camt.py +0 -0
  51. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/chart_of_accounts.py +0 -0
  52. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/general_ledger.py +0 -0
  53. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/journal_entry.py +0 -0
  54. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/payment_entry.py +0 -0
  55. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/pos_invoice.py +0 -0
  56. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/purchase_invoice.py +0 -0
  57. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/revaluation.py +0 -0
  58. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/sales_invoice.py +0 -0
  59. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/__init__.py +0 -0
  60. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/engine.py +0 -0
  61. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/pack.py +0 -0
  62. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
  63. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/packs/ch.py +0 -0
  64. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
  65. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
  66. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
  67. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/packs/generic.py +0 -0
  68. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/profiles.py +0 -0
  69. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/spine.py +0 -0
  70. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/accounting/subscription.py +0 -0
  71. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/assets/__init__.py +0 -0
  72. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/assets/asset.py +0 -0
  73. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/assets/reservation.py +0 -0
  74. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/buying/__init__.py +0 -0
  75. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/buying/purchase_order.py +0 -0
  76. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/controllers/__init__.py +0 -0
  77. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/controllers/currency.py +0 -0
  78. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/controllers/defaults.py +0 -0
  79. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/controllers/pricing_rule.py +0 -0
  80. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/controllers/taxes_and_totals.py +0 -0
  81. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/database.py +0 -0
  82. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/exceptions.py +0 -0
  83. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/hooks.py +0 -0
  84. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/model.py +0 -0
  85. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/selling/__init__.py +0 -0
  86. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/selling/proposal.py +0 -0
  87. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/selling/quotation.py +0 -0
  88. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/selling/sales_order.py +0 -0
  89. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/simulation.py +0 -0
  90. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/stock/__init__.py +0 -0
  91. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/stock/delivery_note.py +0 -0
  92. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/stock/purchase_receipt.py +0 -0
  93. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/stock/stock_entry.py +0 -0
  94. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/stock/stock_ledger.py +0 -0
  95. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/lambda_erp/utils.py +0 -0
  96. {lambda_erp-0.8.28 → lambda_erp-0.8.29}/terraform/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lambda-erp
3
- Version: 0.8.28
3
+ Version: 0.8.29
4
4
  Summary: Core ERP logic - accounting, sales, purchasing, inventory
5
5
  Author: TORUS INVESTMENTS AG
6
6
  License-Expression: Apache-2.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 services.registered_action_allowed(tool["function"]["name"], role)
1442
+ if tool_allowed(tool["function"]["name"], role)
1442
1443
  ]
1443
- if role not in {"manager", "admin"}:
1444
- hidden_tools.add("import_bank_statement_attachments")
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 in ("create_custom_analytics_report", "update_custom_analytics_report")
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
- if not handler:
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
- if name in _EXCLUDE:
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'}'."}
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lambda-erp"
3
- version = "0.8.28"
3
+ version = "0.8.29"
4
4
  description = "Core ERP logic - accounting, sales, purchasing, inventory"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
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