tradingcodex 0.1.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.
Files changed (254) hide show
  1. apps/__init__.py +1 -0
  2. apps/audit/__init__.py +1 -0
  3. apps/audit/admin.py +6 -0
  4. apps/audit/apps.py +8 -0
  5. apps/audit/migrations/0001_initial.py +35 -0
  6. apps/audit/migrations/__init__.py +0 -0
  7. apps/audit/models.py +22 -0
  8. apps/harness/__init__.py +1 -0
  9. apps/harness/admin.py +6 -0
  10. apps/harness/apps.py +8 -0
  11. apps/harness/migrations/0001_initial.py +35 -0
  12. apps/harness/migrations/__init__.py +0 -0
  13. apps/harness/models.py +22 -0
  14. apps/harness/templatetags/__init__.py +1 -0
  15. apps/integrations/__init__.py +1 -0
  16. apps/integrations/admin.py +6 -0
  17. apps/integrations/apps.py +8 -0
  18. apps/integrations/migrations/0001_initial.py +29 -0
  19. apps/integrations/migrations/__init__.py +0 -0
  20. apps/integrations/models.py +16 -0
  21. apps/integrations/services.py +31 -0
  22. apps/mcp/__init__.py +1 -0
  23. apps/mcp/admin.py +20 -0
  24. apps/mcp/apps.py +8 -0
  25. apps/mcp/migrations/0001_initial.py +168 -0
  26. apps/mcp/migrations/__init__.py +0 -0
  27. apps/mcp/models.py +154 -0
  28. apps/mcp/services.py +327 -0
  29. apps/orders/__init__.py +1 -0
  30. apps/orders/admin.py +6 -0
  31. apps/orders/apps.py +8 -0
  32. apps/orders/migrations/0001_initial.py +79 -0
  33. apps/orders/migrations/__init__.py +0 -0
  34. apps/orders/models.py +66 -0
  35. apps/orders/services.py +107 -0
  36. apps/policy/__init__.py +1 -0
  37. apps/policy/admin.py +6 -0
  38. apps/policy/apps.py +8 -0
  39. apps/policy/migrations/0001_initial.py +75 -0
  40. apps/policy/migrations/__init__.py +0 -0
  41. apps/policy/models.py +61 -0
  42. apps/policy/services.py +110 -0
  43. apps/portfolio/__init__.py +1 -0
  44. apps/portfolio/admin.py +6 -0
  45. apps/portfolio/apps.py +8 -0
  46. apps/portfolio/migrations/0001_initial.py +67 -0
  47. apps/portfolio/migrations/__init__.py +0 -0
  48. apps/portfolio/models.py +53 -0
  49. apps/research/__init__.py +1 -0
  50. apps/research/admin.py +1 -0
  51. apps/research/apps.py +8 -0
  52. apps/research/migrations/__init__.py +0 -0
  53. apps/research/models.py +1 -0
  54. apps/workflows/__init__.py +1 -0
  55. apps/workflows/admin.py +6 -0
  56. apps/workflows/apps.py +8 -0
  57. apps/workflows/migrations/0001_initial.py +51 -0
  58. apps/workflows/migrations/__init__.py +0 -0
  59. apps/workflows/models.py +44 -0
  60. tradingcodex-0.1.0.dist-info/METADATA +337 -0
  61. tradingcodex-0.1.0.dist-info/RECORD +254 -0
  62. tradingcodex-0.1.0.dist-info/WHEEL +5 -0
  63. tradingcodex-0.1.0.dist-info/entry_points.txt +2 -0
  64. tradingcodex-0.1.0.dist-info/licenses/LICENSE +202 -0
  65. tradingcodex-0.1.0.dist-info/licenses/NOTICE +24 -0
  66. tradingcodex-0.1.0.dist-info/top_level.txt +4 -0
  67. tradingcodex_cli/__init__.py +1 -0
  68. tradingcodex_cli/__main__.py +124 -0
  69. tradingcodex_cli/commands/__init__.py +2 -0
  70. tradingcodex_cli/commands/bootstrap.py +157 -0
  71. tradingcodex_cli/commands/db.py +36 -0
  72. tradingcodex_cli/commands/doctor.py +230 -0
  73. tradingcodex_cli/commands/mcp.py +186 -0
  74. tradingcodex_cli/commands/orders.py +89 -0
  75. tradingcodex_cli/commands/policy.py +21 -0
  76. tradingcodex_cli/commands/profile.py +110 -0
  77. tradingcodex_cli/commands/research.py +76 -0
  78. tradingcodex_cli/commands/skills.py +93 -0
  79. tradingcodex_cli/commands/strategies.py +67 -0
  80. tradingcodex_cli/commands/subagents.py +106 -0
  81. tradingcodex_cli/commands/utils.py +134 -0
  82. tradingcodex_cli/commands/workspaces.py +53 -0
  83. tradingcodex_cli/generator.py +234 -0
  84. tradingcodex_cli/mcp_stdio.py +26 -0
  85. tradingcodex_cli/service_autostart.py +127 -0
  86. tradingcodex_service/__init__.py +5 -0
  87. tradingcodex_service/admin.py +1 -0
  88. tradingcodex_service/api.py +486 -0
  89. tradingcodex_service/application/__init__.py +2 -0
  90. tradingcodex_service/application/agents.py +1470 -0
  91. tradingcodex_service/application/audit.py +71 -0
  92. tradingcodex_service/application/common.py +88 -0
  93. tradingcodex_service/application/components.py +299 -0
  94. tradingcodex_service/application/harness.py +747 -0
  95. tradingcodex_service/application/markdown_preview.py +179 -0
  96. tradingcodex_service/application/orders.py +404 -0
  97. tradingcodex_service/application/policy.py +150 -0
  98. tradingcodex_service/application/portfolio.py +166 -0
  99. tradingcodex_service/application/research.py +356 -0
  100. tradingcodex_service/application/runtime.py +321 -0
  101. tradingcodex_service/asgi.py +6 -0
  102. tradingcodex_service/mcp_http.py +59 -0
  103. tradingcodex_service/mcp_runtime.py +565 -0
  104. tradingcodex_service/settings.py +91 -0
  105. tradingcodex_service/templates/web/activity.html +24 -0
  106. tradingcodex_service/templates/web/agent_skills.html +111 -0
  107. tradingcodex_service/templates/web/agents.html +121 -0
  108. tradingcodex_service/templates/web/base.html +150 -0
  109. tradingcodex_service/templates/web/dashboard.html +109 -0
  110. tradingcodex_service/templates/web/fragments/role_inspector.html +81 -0
  111. tradingcodex_service/templates/web/fragments/starter_prompt.html +24 -0
  112. tradingcodex_service/templates/web/fragments/topology_canvas.html +85 -0
  113. tradingcodex_service/templates/web/harness.html +87 -0
  114. tradingcodex_service/templates/web/mcp_router.html +250 -0
  115. tradingcodex_service/templates/web/orders.html +68 -0
  116. tradingcodex_service/templates/web/policy.html +81 -0
  117. tradingcodex_service/templates/web/portfolio.html +52 -0
  118. tradingcodex_service/templates/web/research.html +73 -0
  119. tradingcodex_service/templates/web/starter_prompt.html +40 -0
  120. tradingcodex_service/templates/web/strategies.html +74 -0
  121. tradingcodex_service/urls.py +42 -0
  122. tradingcodex_service/version.py +1 -0
  123. tradingcodex_service/web.py +885 -0
  124. tradingcodex_service/wsgi.py +6 -0
  125. workspace_templates/__init__.py +1 -0
  126. workspace_templates/modules/audit/files/.tradingcodex/audit/README.md +5 -0
  127. workspace_templates/modules/audit/files/trading/audit/.gitkeep +1 -0
  128. workspace_templates/modules/audit/module.json +16 -0
  129. workspace_templates/modules/codex-base/files/.codex/config.toml +272 -0
  130. workspace_templates/modules/codex-base/files/.codex/hooks/tradingcodex_hook.py +173 -0
  131. workspace_templates/modules/codex-base/files/.codex/hooks.json +105 -0
  132. workspace_templates/modules/codex-base/files/.codex/prompts/base_instructions/head-manager.md +129 -0
  133. workspace_templates/modules/codex-base/files/.codex/rules/tradingcodex.rules +50 -0
  134. workspace_templates/modules/codex-base/files/.tradingcodex/capabilities.yaml +56 -0
  135. workspace_templates/modules/codex-base/files/.tradingcodex/cli.py +16 -0
  136. workspace_templates/modules/codex-base/files/.tradingcodex/config.yaml +53 -0
  137. workspace_templates/modules/codex-base/files/.tradingcodex/policies/policy-bindings.yaml +16 -0
  138. workspace_templates/modules/codex-base/files/.tradingcodex/policies/principals.yaml +17 -0
  139. workspace_templates/modules/codex-base/files/.tradingcodex/policies/roles.yaml +27 -0
  140. workspace_templates/modules/codex-base/files/AGENTS.md +56 -0
  141. workspace_templates/modules/codex-base/files/pyproject.toml +13 -0
  142. workspace_templates/modules/codex-base/files/tcx +35 -0
  143. workspace_templates/modules/codex-base/module.json +17 -0
  144. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/policies/access-policies.yaml +39 -0
  145. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/policies/restricted-list.yaml +6 -0
  146. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/approval_receipt.schema.json +14 -0
  147. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/audit_event.schema.json +11 -0
  148. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/evidence_pack.schema.json +16 -0
  149. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/execution_result.schema.json +12 -0
  150. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/fundamental_report.schema.json +15 -0
  151. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/news_report.schema.json +15 -0
  152. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/order_intent.schema.json +30 -0
  153. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/portfolio_review.schema.json +15 -0
  154. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/postmortem_report.schema.json +14 -0
  155. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/risk_report.schema.json +15 -0
  156. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/technical_report.schema.json +15 -0
  157. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/thesis.schema.json +15 -0
  158. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/valuation.schema.json +15 -0
  159. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/scripts/validate-order-intent.py +15 -0
  160. workspace_templates/modules/enforcement-guardrails/module.json +19 -0
  161. workspace_templates/modules/fixed-subagents/files/.codex/agents/execution-operator.toml +71 -0
  162. workspace_templates/modules/fixed-subagents/files/.codex/agents/fundamental-analyst.toml +62 -0
  163. workspace_templates/modules/fixed-subagents/files/.codex/agents/instrument-analyst.toml +64 -0
  164. workspace_templates/modules/fixed-subagents/files/.codex/agents/macro-analyst.toml +64 -0
  165. workspace_templates/modules/fixed-subagents/files/.codex/agents/news-analyst.toml +63 -0
  166. workspace_templates/modules/fixed-subagents/files/.codex/agents/portfolio-manager.toml +62 -0
  167. workspace_templates/modules/fixed-subagents/files/.codex/agents/risk-manager.toml +65 -0
  168. workspace_templates/modules/fixed-subagents/files/.codex/agents/technical-analyst.toml +63 -0
  169. workspace_templates/modules/fixed-subagents/files/.codex/agents/valuation-analyst.toml +59 -0
  170. workspace_templates/modules/fixed-subagents/files/.tradingcodex/mainagent/head-manager.yaml +66 -0
  171. workspace_templates/modules/fixed-subagents/files/.tradingcodex/mainagent/skill-change-proposals/.gitkeep +1 -0
  172. workspace_templates/modules/fixed-subagents/files/.tradingcodex/mainagent/subagent-registry.yaml +56 -0
  173. workspace_templates/modules/fixed-subagents/module.json +23 -0
  174. workspace_templates/modules/guidance-guardrails/files/.tradingcodex/guidance/guardrails.md +17 -0
  175. workspace_templates/modules/guidance-guardrails/files/.tradingcodex/guidance/task-quality-checklist.md +37 -0
  176. workspace_templates/modules/guidance-guardrails/module.json +18 -0
  177. workspace_templates/modules/information-barriers/files/.tradingcodex/policies/information-barriers.yaml +211 -0
  178. workspace_templates/modules/information-barriers/files/.tradingcodex/secrets.md +9 -0
  179. workspace_templates/modules/information-barriers/files/trading/approvals/.gitkeep +1 -0
  180. workspace_templates/modules/information-barriers/files/trading/market-data/.gitkeep +1 -0
  181. workspace_templates/modules/information-barriers/files/trading/orders/approved/.gitkeep +1 -0
  182. workspace_templates/modules/information-barriers/files/trading/orders/draft/.gitkeep +1 -0
  183. workspace_templates/modules/information-barriers/files/trading/orders/executed/.gitkeep +1 -0
  184. workspace_templates/modules/information-barriers/files/trading/orders/rejected/.gitkeep +1 -0
  185. workspace_templates/modules/information-barriers/files/trading/portfolio/.gitkeep +1 -0
  186. workspace_templates/modules/information-barriers/files/trading/reports/fundamental/.gitkeep +1 -0
  187. workspace_templates/modules/information-barriers/files/trading/reports/instrument/.gitkeep +1 -0
  188. workspace_templates/modules/information-barriers/files/trading/reports/macro/.gitkeep +1 -0
  189. workspace_templates/modules/information-barriers/files/trading/reports/news/.gitkeep +1 -0
  190. workspace_templates/modules/information-barriers/files/trading/reports/policy/.gitkeep +1 -0
  191. workspace_templates/modules/information-barriers/files/trading/reports/portfolio/.gitkeep +1 -0
  192. workspace_templates/modules/information-barriers/files/trading/reports/postmortem/.gitkeep +1 -0
  193. workspace_templates/modules/information-barriers/files/trading/reports/risk/.gitkeep +1 -0
  194. workspace_templates/modules/information-barriers/files/trading/reports/technical/.gitkeep +1 -0
  195. workspace_templates/modules/information-barriers/files/trading/reports/valuation/.gitkeep +1 -0
  196. workspace_templates/modules/information-barriers/files/trading/research/.gitkeep +1 -0
  197. workspace_templates/modules/information-barriers/module.json +19 -0
  198. workspace_templates/modules/paper-trading/files/.tradingcodex/mcp/adapters/paper-trading.py +4 -0
  199. workspace_templates/modules/paper-trading/module.json +16 -0
  200. workspace_templates/modules/postmortem/files/.tradingcodex/workflows/postmortem.yaml +12 -0
  201. workspace_templates/modules/postmortem/module.json +16 -0
  202. workspace_templates/modules/repo-skills/files/.agents/skills/investment-workflow-map/SKILL.md +106 -0
  203. workspace_templates/modules/repo-skills/files/.agents/skills/investment-workflow-map/agents/openai.yaml +6 -0
  204. workspace_templates/modules/repo-skills/files/.agents/skills/manage-optional-skills/SKILL.md +101 -0
  205. workspace_templates/modules/repo-skills/files/.agents/skills/manage-optional-skills/agents/openai.yaml +6 -0
  206. workspace_templates/modules/repo-skills/files/.agents/skills/manage-subagents/SKILL.md +140 -0
  207. workspace_templates/modules/repo-skills/files/.agents/skills/manage-subagents/agents/openai.yaml +6 -0
  208. workspace_templates/modules/repo-skills/files/.agents/skills/orchestrate-workflow/SKILL.md +140 -0
  209. workspace_templates/modules/repo-skills/files/.agents/skills/orchestrate-workflow/agents/openai.yaml +6 -0
  210. workspace_templates/modules/repo-skills/files/.agents/skills/postmortem/SKILL.md +31 -0
  211. workspace_templates/modules/repo-skills/files/.agents/skills/postmortem/agents/openai.yaml +6 -0
  212. workspace_templates/modules/repo-skills/files/.agents/skills/scenario-quality-gates/SKILL.md +138 -0
  213. workspace_templates/modules/repo-skills/files/.agents/skills/scenario-quality-gates/agents/openai.yaml +6 -0
  214. workspace_templates/modules/repo-skills/files/.agents/skills/strategy-creator/SKILL.md +109 -0
  215. workspace_templates/modules/repo-skills/files/.agents/skills/strategy-creator/agents/openai.yaml +6 -0
  216. workspace_templates/modules/repo-skills/files/.agents/skills/synthesize-decision/SKILL.md +54 -0
  217. workspace_templates/modules/repo-skills/files/.agents/skills/synthesize-decision/agents/openai.yaml +6 -0
  218. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/execution-operator/execute-paper-order/SKILL.md +35 -0
  219. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/execution-operator/execute-paper-order/agents/openai.yaml +6 -0
  220. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/fundamental-analyst/fundamental-analysis/SKILL.md +46 -0
  221. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/fundamental-analyst/fundamental-analysis/agents/openai.yaml +6 -0
  222. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/instrument-analyst/instrument-analysis/SKILL.md +40 -0
  223. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/instrument-analyst/instrument-analysis/agents/openai.yaml +6 -0
  224. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/macro-analyst/macro-analysis/SKILL.md +40 -0
  225. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/macro-analyst/macro-analysis/agents/openai.yaml +6 -0
  226. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/news-analyst/news-analysis/SKILL.md +43 -0
  227. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/news-analyst/news-analysis/agents/openai.yaml +6 -0
  228. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/portfolio-manager/create-order-intent/SKILL.md +46 -0
  229. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/portfolio-manager/create-order-intent/agents/openai.yaml +6 -0
  230. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/portfolio-manager/portfolio-review/SKILL.md +44 -0
  231. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/portfolio-manager/portfolio-review/agents/openai.yaml +6 -0
  232. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/approve-order/SKILL.md +38 -0
  233. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/approve-order/agents/openai.yaml +6 -0
  234. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/policy-review/SKILL.md +43 -0
  235. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/policy-review/agents/openai.yaml +6 -0
  236. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/review-risk/SKILL.md +45 -0
  237. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/review-risk/agents/openai.yaml +6 -0
  238. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/shared/collect-evidence/SKILL.md +46 -0
  239. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/shared/collect-evidence/agents/openai.yaml +6 -0
  240. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/shared/external-data-source-gate/SKILL.md +66 -0
  241. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/shared/external-data-source-gate/agents/openai.yaml +6 -0
  242. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/technical-analyst/technical-analysis/SKILL.md +43 -0
  243. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/technical-analyst/technical-analysis/agents/openai.yaml +6 -0
  244. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/valuation-analyst/valuation-review/SKILL.md +47 -0
  245. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/valuation-analyst/valuation-review/agents/openai.yaml +6 -0
  246. workspace_templates/modules/repo-skills/module.json +37 -0
  247. workspace_templates/modules/stub-execution/files/.tradingcodex/mcp/adapters/stub-execution.py +4 -0
  248. workspace_templates/modules/stub-execution/module.json +15 -0
  249. workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/adapters/live-adapter.contract.md +25 -0
  250. workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/enforcer/README.md +5 -0
  251. workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/gateway/README.md +8 -0
  252. workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/server.py +27 -0
  253. workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/smoke-call.py +18 -0
  254. workspace_templates/modules/tradingcodex-mcp/module.json +24 -0
apps/mcp/services.py ADDED
@@ -0,0 +1,327 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from typing import Any
6
+
7
+ from django.utils import timezone
8
+ from django.db.models import QuerySet
9
+
10
+ from apps.mcp.models import (
11
+ McpExternalTool,
12
+ McpExternalToolCall,
13
+ McpExternalToolPermission,
14
+ McpRouter,
15
+ McpToolDefinition,
16
+ )
17
+ from apps.policy.services import role_for_principal_id
18
+ from tradingcodex_service.application.common import stable_hash
19
+ from tradingcodex_service.application.runtime import workspace_context_payload
20
+
21
+
22
+ READ_ONLY_PROXY_MODES = {"read_only", "summary_only"}
23
+ SERVICE_PROXY_MODES = {"service_adapter", "service_path"}
24
+ RESEARCH_ROLES = {
25
+ "head-manager",
26
+ "fundamental-analyst",
27
+ "technical-analyst",
28
+ "news-analyst",
29
+ "macro-analyst",
30
+ "instrument-analyst",
31
+ "valuation-analyst",
32
+ }
33
+ ACCOUNT_READ_ROLES = {"head-manager", "portfolio-manager", "risk-manager", "execution-operator"}
34
+ PORTFOLIO_STATE_ROLES = {"portfolio-manager", "risk-manager"}
35
+
36
+
37
+ def set_mcp_tools_enabled(queryset: QuerySet[McpToolDefinition], enabled: bool, actor: str = "admin") -> int:
38
+ count = queryset.update(enabled=enabled)
39
+ _audit("mcp_tool.enabled" if enabled else "mcp_tool.disabled", {"count": count}, actor)
40
+ return count
41
+
42
+
43
+ def sync_builtin_mcp_registry(actor: str = "admin") -> None:
44
+ from tradingcodex_service.mcp_runtime import sync_mcp_tool_definitions
45
+
46
+ sync_mcp_tool_definitions()
47
+ _audit("mcp_tool_registry.synced", {"source": "builtin"}, actor)
48
+
49
+
50
+ def create_or_update_router(
51
+ *,
52
+ name: str,
53
+ label: str = "",
54
+ transport: str = "stdio",
55
+ command: str = "",
56
+ url: str = "",
57
+ credential_ref: str = "",
58
+ enabled: bool = False,
59
+ actor: str = "web",
60
+ ) -> McpRouter:
61
+ if not name:
62
+ raise ValueError("router name is required")
63
+ router, created = McpRouter.objects.update_or_create(
64
+ name=name,
65
+ defaults={
66
+ "label": label,
67
+ "transport": transport or "stdio",
68
+ "command": command,
69
+ "url": url,
70
+ "credential_ref": credential_ref,
71
+ "enabled": bool(enabled),
72
+ },
73
+ )
74
+ _audit("external_mcp_router.created" if created else "external_mcp_router.updated", {"router": router.name, "enabled": router.enabled}, actor)
75
+ return router
76
+
77
+
78
+ def import_external_mcp_discovery(router: McpRouter, discovery_payload: str | dict[str, Any], actor: str = "web") -> dict[str, Any]:
79
+ payload = _coerce_payload(discovery_payload)
80
+ imported: list[McpExternalTool] = []
81
+ for primitive, item in _iter_discovered_primitives(payload):
82
+ imported.append(upsert_external_mcp_tool(router, primitive, item))
83
+ router.last_status = "ok"
84
+ router.last_error = ""
85
+ router.last_checked_at = timezone.now()
86
+ router.save(update_fields=["last_status", "last_error", "last_checked_at", "updated_at"])
87
+ _audit("external_mcp.discovery_imported", {"router": router.name, "count": len(imported)}, actor)
88
+ return {"router": router.name, "imported": len(imported), "tool_ids": [tool.id for tool in imported]}
89
+
90
+
91
+ def upsert_external_mcp_tool(router: McpRouter, primitive: str, item: dict[str, Any]) -> McpExternalTool:
92
+ external_name = str(item.get("name") or item.get("uri") or item.get("id") or "").strip()
93
+ if not external_name:
94
+ raise ValueError("external MCP item is missing name, uri, or id")
95
+ description = str(item.get("description") or item.get("title") or "")
96
+ input_schema = item.get("inputSchema") or item.get("input_schema") or item.get("schema") or {}
97
+ output_schema = item.get("outputSchema") or item.get("output_schema") or {}
98
+ schema_hash = stable_hash({"primitive": primitive, "name": external_name, "description": description, "input_schema": input_schema, "output_schema": output_schema})
99
+ classification = classify_external_mcp_item(external_name, description, input_schema, primitive=primitive)
100
+ tool, created = McpExternalTool.objects.get_or_create(
101
+ router=router,
102
+ primitive=primitive,
103
+ external_name=external_name,
104
+ defaults={
105
+ "description": description,
106
+ "input_schema": input_schema if isinstance(input_schema, dict) else {},
107
+ "output_schema": output_schema if isinstance(output_schema, dict) else {},
108
+ "schema_hash": schema_hash,
109
+ **classification,
110
+ "last_seen_at": timezone.now(),
111
+ },
112
+ )
113
+ if created:
114
+ return tool
115
+ changed = bool(tool.schema_hash and tool.schema_hash != schema_hash)
116
+ tool.description = description
117
+ tool.input_schema = input_schema if isinstance(input_schema, dict) else {}
118
+ tool.output_schema = output_schema if isinstance(output_schema, dict) else {}
119
+ tool.schema_hash = schema_hash
120
+ tool.last_seen_at = timezone.now()
121
+ if changed:
122
+ tool.enabled = False
123
+ tool.drift_detected = True
124
+ tool.review_status = "schema_changed"
125
+ else:
126
+ for field, value in classification.items():
127
+ if tool.review_status in {"review_required", "auto_classified"} or field in {"category", "risk_level", "sensitivity", "canonical_capability"}:
128
+ setattr(tool, field, value)
129
+ tool.save()
130
+ return tool
131
+
132
+
133
+ def classify_external_mcp_item(name: str, description: str = "", schema: dict[str, Any] | None = None, *, primitive: str = "tool") -> dict[str, Any]:
134
+ text = " ".join([name, description, json.dumps(schema or {}, sort_keys=True, default=str)]).lower()
135
+ if primitive != "tool":
136
+ return {
137
+ "category": "market_data" if primitive == "resource" else "workflow_prompt",
138
+ "risk_level": "read",
139
+ "sensitivity": "public",
140
+ "canonical_capability": "market_data.read" if primitive == "resource" else "workflow.prompt.read",
141
+ "proxy_mode": "read_only",
142
+ "allowed_roles": sorted(RESEARCH_ROLES),
143
+ "conditions": {"as_of_required": primitive == "resource"},
144
+ "review_status": "auto_classified",
145
+ }
146
+ if _matches(text, r"secret|credential|password|api[_\s-]?key|token|\.env"):
147
+ return _classification("secret", "blocked", "secret", "secret.read", "blocked", [])
148
+ if _matches(text, r"transfer|withdraw|wire|ach|deposit"):
149
+ return _classification("execution", "execution", "private", "cash.transfer", "service_path", [])
150
+ if _matches(text, r"place[_\s-]?order|submit[_\s-]?order|create[_\s-]?order|replace[_\s-]?order|cancel[_\s-]?order|trade|execute"):
151
+ capability = "order.cancel" if "cancel" in text else "order.submit"
152
+ return _classification("execution", "execution", "private", capability, "service_adapter", [])
153
+ if _matches(text, r"policy|permission|principal|capability|allowlist|admin|enable[_\s-]?tool|disable[_\s-]?tool"):
154
+ return _classification("policy_admin", "write", "canonical_state", "policy.config.write", "blocked", [])
155
+ if _matches(text, r"position|positions|balance|balances|account|buying[_\s-]?power|portfolio|orders|fills|holdings"):
156
+ return _classification("account_read", "read", "private", "account.positions.read", "summary_only", sorted(ACCOUNT_READ_ROLES))
157
+ if _matches(text, r"quote|quotes|candles|bars|ohlcv|price|market[_\s-]?data|ticker|tickers|news|filing|fundamental|financial|earnings"):
158
+ return _classification("market_data", "read", "public", "market_data.read", "read_only", sorted(RESEARCH_ROLES | PORTFOLIO_STATE_ROLES))
159
+ if _matches(text, r"snapshot|source|artifact|research|dataset|import"):
160
+ return _classification("research_write", "write", "research", "research.snapshot.write", "service_path", sorted(RESEARCH_ROLES))
161
+ return _classification("unknown", "unknown", "unknown", "mcp.external.unknown", "blocked", [])
162
+
163
+
164
+ def set_external_tool_policy(
165
+ tool: McpExternalTool,
166
+ *,
167
+ category: str | None = None,
168
+ risk_level: str | None = None,
169
+ sensitivity: str | None = None,
170
+ canonical_capability: str | None = None,
171
+ proxy_mode: str | None = None,
172
+ allowed_roles: list[str] | None = None,
173
+ enabled: bool | None = None,
174
+ review_status: str = "reviewed",
175
+ actor: str = "web",
176
+ ) -> McpExternalTool:
177
+ if category is not None:
178
+ tool.category = category or "unknown"
179
+ if risk_level is not None:
180
+ tool.risk_level = risk_level or "unknown"
181
+ if sensitivity is not None:
182
+ tool.sensitivity = sensitivity or "unknown"
183
+ if canonical_capability is not None:
184
+ tool.canonical_capability = canonical_capability
185
+ if proxy_mode is not None:
186
+ tool.proxy_mode = proxy_mode or "blocked"
187
+ if allowed_roles is not None:
188
+ tool.allowed_roles = [role for role in allowed_roles if role]
189
+ if enabled is not None:
190
+ if enabled:
191
+ _validate_external_tool_can_enable(tool)
192
+ tool.enabled = bool(enabled)
193
+ tool.review_status = review_status or "reviewed"
194
+ if tool.review_status == "reviewed":
195
+ tool.drift_detected = False
196
+ tool.save()
197
+ _audit("external_mcp_tool.policy_updated", {"tool": str(tool), "enabled": tool.enabled, "proxy_mode": tool.proxy_mode}, actor)
198
+ return tool
199
+
200
+
201
+ def evaluate_external_mcp_proxy_call(
202
+ workspace_root: Any,
203
+ tool: McpExternalTool,
204
+ *,
205
+ principal_id: str,
206
+ arguments: dict[str, Any] | None = None,
207
+ actor: str = "mcp-proxy",
208
+ ) -> dict[str, Any]:
209
+ reasons = external_tool_denial_reasons(tool, principal_id)
210
+ decision = "allow" if not reasons else "deny"
211
+ result = {
212
+ "decision": decision,
213
+ "reasons": reasons,
214
+ "router": tool.router.name,
215
+ "external_name": tool.external_name,
216
+ "proxy_mode": tool.proxy_mode,
217
+ "category": tool.category,
218
+ "risk_level": tool.risk_level,
219
+ "canonical_capability": tool.canonical_capability,
220
+ "adapter_call_allowed": decision == "allow" and tool.proxy_mode in SERVICE_PROXY_MODES,
221
+ "direct_proxy_allowed": decision == "allow" and tool.proxy_mode in READ_ONLY_PROXY_MODES,
222
+ "db_canonical": True,
223
+ "workspace_context": workspace_context_payload(workspace_root),
224
+ }
225
+ McpExternalToolCall.objects.create(
226
+ external_tool=tool,
227
+ router_name=tool.router.name,
228
+ external_name=tool.external_name,
229
+ principal_id=principal_id,
230
+ proxy_mode=tool.proxy_mode,
231
+ decision=decision,
232
+ reasons=reasons,
233
+ request=arguments or {},
234
+ response=result,
235
+ request_hash=stable_hash(arguments or {}),
236
+ result_hash=stable_hash(result),
237
+ workspace_context=result["workspace_context"],
238
+ )
239
+ _audit("external_mcp.proxy_allowed" if decision == "allow" else "external_mcp.proxy_denied", {"tool": str(tool), "reasons": reasons}, actor)
240
+ return result
241
+
242
+
243
+ def external_tool_denial_reasons(tool: McpExternalTool, principal_id: str) -> list[str]:
244
+ reasons: list[str] = []
245
+ role = role_for_principal_id(principal_id)
246
+ if not tool.router.enabled:
247
+ reasons.append(f"router is disabled: {tool.router.name}")
248
+ if not tool.enabled:
249
+ reasons.append(f"external tool is disabled: {tool.external_name}")
250
+ if tool.drift_detected:
251
+ reasons.append("schema drift requires review")
252
+ if tool.review_status not in {"reviewed", "approved"}:
253
+ reasons.append(f"tool review is not complete: {tool.review_status}")
254
+ if tool.category in {"secret", "policy_admin"}:
255
+ reasons.append(f"category is not proxyable: {tool.category}")
256
+ if tool.category == "execution" and tool.proxy_mode not in SERVICE_PROXY_MODES:
257
+ reasons.append("execution tools must map to a TradingCodex service adapter path")
258
+ if tool.category == "unknown":
259
+ reasons.append("unknown tools require classification before proxy")
260
+ allowed = set(tool.allowed_roles or [])
261
+ if not _permission_allows(tool, principal_id, role, allowed):
262
+ reasons.append(f"principal is not allowed for external tool: {principal_id}")
263
+ return list(dict.fromkeys(reasons))
264
+
265
+
266
+ def _validate_external_tool_can_enable(tool: McpExternalTool) -> None:
267
+ if tool.drift_detected:
268
+ raise ValueError("schema drift requires review before enabling")
269
+ if tool.proxy_mode == "direct":
270
+ raise ValueError("direct raw proxy mode is not allowed")
271
+ if tool.category in {"secret", "policy_admin", "unknown"}:
272
+ raise ValueError(f"{tool.category} tools cannot be enabled for proxy")
273
+ if tool.category == "execution" and tool.proxy_mode not in SERVICE_PROXY_MODES:
274
+ raise ValueError("execution tools must use service_adapter or service_path proxy mode")
275
+
276
+
277
+ def _permission_allows(tool: McpExternalTool, principal_id: str, role: str, allowed_roles: set[str]) -> bool:
278
+ if principal_id in allowed_roles or role in allowed_roles:
279
+ return True
280
+ permissions = McpExternalToolPermission.objects.filter(external_tool=tool, enabled=True)
281
+ if permissions.filter(decision="deny", principal_or_role__in={principal_id, role}).exists():
282
+ return False
283
+ return permissions.filter(decision="allow", principal_or_role__in={principal_id, role}).exists()
284
+
285
+
286
+ def _classification(category: str, risk_level: str, sensitivity: str, capability: str, proxy_mode: str, roles: list[str]) -> dict[str, Any]:
287
+ return {
288
+ "category": category,
289
+ "risk_level": risk_level,
290
+ "sensitivity": sensitivity,
291
+ "canonical_capability": capability,
292
+ "proxy_mode": proxy_mode,
293
+ "allowed_roles": roles,
294
+ "conditions": {},
295
+ "review_status": "auto_classified",
296
+ }
297
+
298
+
299
+ def _matches(text: str, pattern: str) -> bool:
300
+ return bool(re.search(pattern, text, flags=re.I))
301
+
302
+
303
+ def _coerce_payload(payload: str | dict[str, Any]) -> dict[str, Any]:
304
+ if isinstance(payload, dict):
305
+ return payload
306
+ if not str(payload).strip():
307
+ return {}
308
+ parsed = json.loads(payload)
309
+ if not isinstance(parsed, dict):
310
+ raise ValueError("MCP discovery payload must be a JSON object")
311
+ return parsed
312
+
313
+
314
+ def _iter_discovered_primitives(payload: dict[str, Any]) -> list[tuple[str, dict[str, Any]]]:
315
+ body = payload.get("result") if isinstance(payload.get("result"), dict) else payload
316
+ primitives: list[tuple[str, dict[str, Any]]] = []
317
+ for key, primitive in [("tools", "tool"), ("resources", "resource"), ("prompts", "prompt")]:
318
+ items = body.get(key) if isinstance(body, dict) else None
319
+ if isinstance(items, list):
320
+ primitives.extend((primitive, item) for item in items if isinstance(item, dict))
321
+ return primitives
322
+
323
+
324
+ def _audit(action: str, payload: dict[str, Any], actor: str) -> None:
325
+ from tradingcodex_service.application.audit import write_audit_event_if_available
326
+
327
+ write_audit_event_if_available(None, actor, "admin", {"type": action, "payload": payload})
@@ -0,0 +1 @@
1
+
apps/orders/admin.py ADDED
@@ -0,0 +1,6 @@
1
+ from django.contrib import admin
2
+
3
+ from apps.orders.models import ApprovalReceipt, ExecutionResult, OrderIntent
4
+
5
+
6
+ admin.site.register([ApprovalReceipt, ExecutionResult, OrderIntent])
apps/orders/apps.py ADDED
@@ -0,0 +1,8 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class OrdersConfig(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "apps.orders"
7
+ label = "orders"
8
+ verbose_name = "Orders And Approvals"
@@ -0,0 +1,79 @@
1
+ # Generated by Django 5.2.5 on 2026-06-12 16:32
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ initial = True
9
+
10
+ dependencies = [
11
+ ]
12
+
13
+ operations = [
14
+ migrations.CreateModel(
15
+ name='ApprovalReceipt',
16
+ fields=[
17
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18
+ ('receipt_id', models.CharField(max_length=160, unique=True)),
19
+ ('order_intent_id', models.CharField(max_length=160)),
20
+ ('approved_by', models.CharField(max_length=128)),
21
+ ('valid', models.BooleanField(default=True)),
22
+ ('created_at', models.DateTimeField(auto_now_add=True)),
23
+ ('expires_at', models.DateTimeField()),
24
+ ('workspace_context', models.JSONField(blank=True, default=dict)),
25
+ ('payload', models.JSONField(blank=True, default=dict)),
26
+ ],
27
+ options={
28
+ 'verbose_name': 'Approval receipt',
29
+ 'verbose_name_plural': 'Approval receipts',
30
+ },
31
+ ),
32
+ migrations.CreateModel(
33
+ name='ExecutionResult',
34
+ fields=[
35
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
36
+ ('order_intent_id', models.CharField(max_length=160)),
37
+ ('approval_receipt_id', models.CharField(blank=True, max_length=160)),
38
+ ('idempotency_key', models.CharField(blank=True, max_length=220, null=True, unique=True)),
39
+ ('adapter', models.CharField(max_length=64)),
40
+ ('status', models.CharField(max_length=32)),
41
+ ('created_at', models.DateTimeField(auto_now_add=True)),
42
+ ('portfolio_id', models.CharField(default='default-paper', max_length=120)),
43
+ ('account_id', models.CharField(default='local-paper', max_length=120)),
44
+ ('strategy_id', models.CharField(default='default-strategy', max_length=120)),
45
+ ('workspace_context', models.JSONField(blank=True, default=dict)),
46
+ ('payload', models.JSONField(blank=True, default=dict)),
47
+ ],
48
+ options={
49
+ 'verbose_name': 'Execution result',
50
+ 'verbose_name_plural': 'Execution results',
51
+ 'ordering': ['-created_at', '-id'],
52
+ },
53
+ ),
54
+ migrations.CreateModel(
55
+ name='OrderIntent',
56
+ fields=[
57
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
58
+ ('intent_id', models.CharField(max_length=160, unique=True)),
59
+ ('symbol', models.CharField(max_length=64)),
60
+ ('side', models.CharField(max_length=8)),
61
+ ('quantity', models.DecimalField(decimal_places=6, max_digits=20)),
62
+ ('limit_price', models.DecimalField(decimal_places=6, max_digits=20)),
63
+ ('currency', models.CharField(default='KRW', max_length=16)),
64
+ ('broker', models.CharField(max_length=64)),
65
+ ('estimated_notional_krw', models.DecimalField(decimal_places=2, max_digits=24)),
66
+ ('created_by', models.CharField(max_length=128)),
67
+ ('created_at', models.DateTimeField()),
68
+ ('portfolio_id', models.CharField(default='default-paper', max_length=120)),
69
+ ('account_id', models.CharField(default='local-paper', max_length=120)),
70
+ ('strategy_id', models.CharField(default='default-strategy', max_length=120)),
71
+ ('workspace_context', models.JSONField(blank=True, default=dict)),
72
+ ('payload', models.JSONField(blank=True, default=dict)),
73
+ ],
74
+ options={
75
+ 'verbose_name': 'Order intent',
76
+ 'verbose_name_plural': 'Order intents',
77
+ },
78
+ ),
79
+ ]
File without changes
apps/orders/models.py ADDED
@@ -0,0 +1,66 @@
1
+ from django.db import models
2
+
3
+
4
+ class OrderIntent(models.Model):
5
+ intent_id = models.CharField(max_length=160, unique=True)
6
+ symbol = models.CharField(max_length=64)
7
+ side = models.CharField(max_length=8)
8
+ quantity = models.DecimalField(max_digits=20, decimal_places=6)
9
+ limit_price = models.DecimalField(max_digits=20, decimal_places=6)
10
+ currency = models.CharField(max_length=16, default="KRW")
11
+ broker = models.CharField(max_length=64)
12
+ estimated_notional_krw = models.DecimalField(max_digits=24, decimal_places=2)
13
+ created_by = models.CharField(max_length=128)
14
+ created_at = models.DateTimeField()
15
+ portfolio_id = models.CharField(max_length=120, default="default-paper")
16
+ account_id = models.CharField(max_length=120, default="local-paper")
17
+ strategy_id = models.CharField(max_length=120, default="default-strategy")
18
+ workspace_context = models.JSONField(default=dict, blank=True)
19
+ payload = models.JSONField(default=dict, blank=True)
20
+
21
+ class Meta:
22
+ verbose_name = "Order intent"
23
+ verbose_name_plural = "Order intents"
24
+
25
+ def __str__(self) -> str:
26
+ return self.intent_id
27
+
28
+
29
+ class ApprovalReceipt(models.Model):
30
+ receipt_id = models.CharField(max_length=160, unique=True)
31
+ order_intent_id = models.CharField(max_length=160)
32
+ approved_by = models.CharField(max_length=128)
33
+ valid = models.BooleanField(default=True)
34
+ created_at = models.DateTimeField(auto_now_add=True)
35
+ expires_at = models.DateTimeField()
36
+ workspace_context = models.JSONField(default=dict, blank=True)
37
+ payload = models.JSONField(default=dict, blank=True)
38
+
39
+ class Meta:
40
+ verbose_name = "Approval receipt"
41
+ verbose_name_plural = "Approval receipts"
42
+
43
+ def __str__(self) -> str:
44
+ return self.receipt_id
45
+
46
+
47
+ class ExecutionResult(models.Model):
48
+ order_intent_id = models.CharField(max_length=160)
49
+ approval_receipt_id = models.CharField(max_length=160, blank=True)
50
+ idempotency_key = models.CharField(max_length=220, unique=True, null=True, blank=True)
51
+ adapter = models.CharField(max_length=64)
52
+ status = models.CharField(max_length=32)
53
+ created_at = models.DateTimeField(auto_now_add=True)
54
+ portfolio_id = models.CharField(max_length=120, default="default-paper")
55
+ account_id = models.CharField(max_length=120, default="local-paper")
56
+ strategy_id = models.CharField(max_length=120, default="default-strategy")
57
+ workspace_context = models.JSONField(default=dict, blank=True)
58
+ payload = models.JSONField(default=dict, blank=True)
59
+
60
+ class Meta:
61
+ ordering = ["-created_at", "-id"]
62
+ verbose_name = "Execution result"
63
+ verbose_name_plural = "Execution results"
64
+
65
+ def __str__(self) -> str:
66
+ return f"{self.status}: {self.order_intent_id}"
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ from django.db import IntegrityError, transaction
9
+
10
+ from apps.orders.models import ExecutionResult
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class ExecutionReservation:
15
+ created: bool
16
+ execution: ExecutionResult
17
+ idempotency_key: str
18
+
19
+
20
+ def execution_idempotency_key(
21
+ order: dict[str, Any],
22
+ receipt: dict[str, Any] | None = None,
23
+ portfolio_id: str = "",
24
+ account_id: str = "",
25
+ strategy_id: str = "",
26
+ ) -> str:
27
+ explicit = order.get("idempotency_key") or (receipt or {}).get("idempotency_key")
28
+ if explicit:
29
+ return str(explicit)
30
+ payload = {
31
+ "order_intent_id": order.get("id"),
32
+ "portfolio_id": portfolio_id or order.get("portfolio_id", ""),
33
+ "account_id": account_id or order.get("account_id", ""),
34
+ "strategy_id": strategy_id or order.get("strategy_id", ""),
35
+ "execution_boundary": "submit_approved_order",
36
+ }
37
+ return "submit:" + hashlib.sha256(json.dumps(payload, sort_keys=True).encode("utf-8")).hexdigest()
38
+
39
+
40
+ def existing_execution_for_order(
41
+ order_id: str,
42
+ idempotency_key: str,
43
+ portfolio_id: str,
44
+ account_id: str,
45
+ strategy_id: str,
46
+ ) -> ExecutionResult | None:
47
+ return (
48
+ ExecutionResult.objects.filter(idempotency_key=idempotency_key).order_by("-created_at", "-id").first()
49
+ or ExecutionResult.objects.filter(
50
+ order_intent_id=order_id,
51
+ portfolio_id=portfolio_id,
52
+ account_id=account_id,
53
+ strategy_id=strategy_id,
54
+ ).order_by("-created_at", "-id").first()
55
+ )
56
+
57
+
58
+ def reserve_execution(
59
+ *,
60
+ order: dict[str, Any],
61
+ receipt: dict[str, Any],
62
+ adapter: str,
63
+ portfolio_id: str,
64
+ account_id: str,
65
+ strategy_id: str,
66
+ workspace_context: dict[str, Any],
67
+ principal_id: str,
68
+ ) -> ExecutionReservation:
69
+ key = execution_idempotency_key(order, receipt, portfolio_id, account_id, strategy_id)
70
+ existing = existing_execution_for_order(str(order.get("id", "")), key, portfolio_id, account_id, strategy_id)
71
+ if existing is not None:
72
+ return ExecutionReservation(False, existing, key)
73
+
74
+ payload = {
75
+ "status": "pending",
76
+ "order_intent_id": order.get("id"),
77
+ "approval_receipt_id": receipt.get("id", ""),
78
+ "principal_id": principal_id,
79
+ "idempotency_key": key,
80
+ }
81
+ try:
82
+ with transaction.atomic():
83
+ execution = ExecutionResult.objects.create(
84
+ order_intent_id=order["id"],
85
+ approval_receipt_id=receipt.get("id", ""),
86
+ adapter=adapter,
87
+ status="pending",
88
+ portfolio_id=portfolio_id,
89
+ account_id=account_id,
90
+ strategy_id=strategy_id,
91
+ workspace_context=workspace_context,
92
+ payload=payload,
93
+ idempotency_key=key,
94
+ )
95
+ except IntegrityError:
96
+ execution = existing_execution_for_order(str(order.get("id", "")), key, portfolio_id, account_id, strategy_id)
97
+ if execution is None:
98
+ raise
99
+ return ExecutionReservation(False, execution, key)
100
+ return ExecutionReservation(True, execution, key)
101
+
102
+
103
+ def finalize_execution_reservation(execution: ExecutionResult, result: dict[str, Any]) -> None:
104
+ execution.status = str(result.get("status") or "recorded")
105
+ execution.adapter = str(result.get("adapter") or execution.adapter)
106
+ execution.payload = result
107
+ execution.save(update_fields=["status", "adapter", "payload"])
@@ -0,0 +1 @@
1
+
apps/policy/admin.py ADDED
@@ -0,0 +1,6 @@
1
+ from django.contrib import admin
2
+
3
+ from apps.policy.models import Capability, PolicyDecision, Principal, RestrictedSymbol
4
+
5
+
6
+ admin.site.register([Capability, PolicyDecision, Principal, RestrictedSymbol])
apps/policy/apps.py ADDED
@@ -0,0 +1,8 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class PolicyConfig(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "apps.policy"
7
+ label = "policy"
8
+ verbose_name = "Policy And Limits"