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
@@ -0,0 +1,885 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import subprocess
6
+ from pathlib import Path
7
+ from typing import Any, Callable
8
+ from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
9
+
10
+ from django.http import HttpRequest, HttpResponse, HttpResponseForbidden
11
+ from django.shortcuts import redirect, render
12
+ from django.views.decorators.http import require_GET, require_POST
13
+
14
+ from tradingcodex_cli.generator import bootstrap_workspace
15
+ from tradingcodex_service.application.agents import (
16
+ AGENT_SPECS,
17
+ EXPECTED_SUBAGENTS,
18
+ SKILL_SPECS,
19
+ build_projection_state,
20
+ create_or_update_optional_skill,
21
+ delete_optional_skill,
22
+ read_agent_additional_instructions,
23
+ read_strategy_skill_records,
24
+ set_optional_skill_status,
25
+ write_agent_additional_instructions,
26
+ )
27
+ from tradingcodex_service.application.harness import (
28
+ build_subagent_starter_prompt,
29
+ get_harness_health,
30
+ get_harness_topology,
31
+ get_role_detail,
32
+ list_policy_overview,
33
+ list_recent_activity,
34
+ )
35
+ from tradingcodex_service.application.markdown_preview import (
36
+ MarkdownPreview,
37
+ read_markdown_preview,
38
+ render_markdown_preview,
39
+ )
40
+ from tradingcodex_service.application.research import list_workspace_research_artifacts
41
+ from tradingcodex_service.application.portfolio import (
42
+ DEFAULT_ACCOUNT_ID,
43
+ DEFAULT_PORTFOLIO_ID,
44
+ DEFAULT_STRATEGY_ID,
45
+ default_paper_portfolio_state,
46
+ )
47
+ from tradingcodex_service.application.runtime import (
48
+ WORKSPACE_MANIFEST_REL,
49
+ ensure_runtime_database,
50
+ persist_workspace_context_if_available,
51
+ workspace_context_payload,
52
+ )
53
+ from apps.mcp.services import (
54
+ create_or_update_router,
55
+ evaluate_external_mcp_proxy_call,
56
+ import_external_mcp_discovery,
57
+ set_external_tool_policy,
58
+ )
59
+
60
+
61
+ PRODUCT_NAV = [
62
+ {"label": "Agents", "href": "/harness/agents/", "key": "agents"},
63
+ {"label": "Strategies", "href": "/harness/strategies/", "key": "strategies"},
64
+ {"label": "Research", "href": "/research/", "key": "research"},
65
+ {"label": "MCP Router", "href": "/integrations/mcp/", "key": "mcp-router"},
66
+ ]
67
+ WORKSPACE_SESSION_KEY = "tradingcodex_selected_workspace_id"
68
+ WORKSPACE_NOTICE_SESSION_KEY = "tradingcodex_workspace_notice"
69
+ WORKSPACE_ERROR_SESSION_KEY = "tradingcodex_workspace_error"
70
+ SKILL_TEMPLATE_ROOT = Path(__file__).resolve().parents[1] / "workspace_templates" / "modules" / "repo-skills" / "files" / ".agents" / "skills"
71
+ SUBAGENT_SKILL_TEMPLATE_ROOT = Path(__file__).resolve().parents[1] / "workspace_templates" / "modules" / "repo-skills" / "files" / ".tradingcodex" / "subagents" / "skills"
72
+
73
+
74
+ def require_local_or_staff(view: Callable[..., HttpResponse]) -> Callable[..., HttpResponse]:
75
+ def wrapped(request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
76
+ remote_addr = request.META.get("REMOTE_ADDR", "")
77
+ if remote_addr in {"127.0.0.1", "::1", ""}:
78
+ return view(request, *args, **kwargs)
79
+ if getattr(request, "user", None) and request.user.is_staff:
80
+ return view(request, *args, **kwargs)
81
+ return HttpResponseForbidden("TradingCodex web is local or staff only.")
82
+
83
+ return wrapped
84
+
85
+
86
+ def default_workspace_root() -> Path:
87
+ return Path(os.environ.get("TRADINGCODEX_WORKSPACE_ROOT", os.getcwd())).expanduser().resolve()
88
+
89
+
90
+ def workspace_root(request: HttpRequest | None = None) -> Path:
91
+ fallback = default_workspace_root()
92
+ if request is None or not hasattr(request, "session"):
93
+ return fallback
94
+
95
+ query_workspace_id = str(request.GET.get("workspace") or "").strip()
96
+ if query_workspace_id:
97
+ selected = _workspace_option_by_id(query_workspace_id)
98
+ if selected:
99
+ request.session[WORKSPACE_SESSION_KEY] = selected["workspace_id"]
100
+ request.session.modified = True
101
+ return Path(selected["path"]).expanduser().resolve()
102
+ request.session.pop(WORKSPACE_SESSION_KEY, None)
103
+ request.session.modified = True
104
+ return fallback
105
+
106
+ session_workspace_id = request.session.get(WORKSPACE_SESSION_KEY)
107
+ if isinstance(session_workspace_id, str) and session_workspace_id:
108
+ selected = _workspace_option_by_id(session_workspace_id)
109
+ if selected:
110
+ return Path(selected["path"]).expanduser().resolve()
111
+ request.session.pop(WORKSPACE_SESSION_KEY, None)
112
+ request.session.modified = True
113
+
114
+ return fallback
115
+
116
+
117
+ def base_context(request: HttpRequest, active: str) -> dict[str, Any]:
118
+ root = workspace_root(request)
119
+ context = workspace_context_payload(root)
120
+ options = workspace_options(root)
121
+ return {
122
+ "active": active,
123
+ "nav_items": PRODUCT_NAV,
124
+ "workspace_context": context,
125
+ "workspace_options": options,
126
+ "selected_workspace_id": context["workspace_id"],
127
+ "workspace_notice": _pop_session_message(request, WORKSPACE_NOTICE_SESSION_KEY),
128
+ "workspace_error": _pop_session_message(request, WORKSPACE_ERROR_SESSION_KEY),
129
+ }
130
+
131
+
132
+ @require_GET
133
+ @require_local_or_staff
134
+ def dashboard(request: HttpRequest) -> HttpResponse:
135
+ return redirect("web-agents")
136
+
137
+
138
+ @require_GET
139
+ @require_local_or_staff
140
+ def harness(request: HttpRequest) -> HttpResponse:
141
+ return redirect("web-agents")
142
+
143
+
144
+ @require_GET
145
+ @require_local_or_staff
146
+ def role_inspector(request: HttpRequest, role: str) -> HttpResponse:
147
+ return render(
148
+ request,
149
+ "web/fragments/role_inspector.html",
150
+ {"selected_role": get_role_detail(role, workspace_root(request))},
151
+ )
152
+
153
+
154
+ @require_GET
155
+ @require_local_or_staff
156
+ def agents_index(request: HttpRequest) -> HttpResponse:
157
+ return _render_agents(request)
158
+
159
+
160
+ @require_GET
161
+ @require_local_or_staff
162
+ def strategies_index(request: HttpRequest) -> HttpResponse:
163
+ root = workspace_root(request)
164
+ strategies = _strategy_web_records(root, read_strategy_skill_records(root))
165
+ selected_name = str(request.GET.get("name") or "").strip()
166
+ selected_strategy = next((strategy for strategy in strategies if strategy.get("name") == selected_name), None) if selected_name else None
167
+ if selected_strategy:
168
+ strategy_mode = "detail"
169
+ else:
170
+ strategy_mode = "list"
171
+ selected_name = ""
172
+ state = build_projection_state(root)
173
+ preview = _skill_markdown_preview(root, state, selected_name) if selected_strategy else None
174
+ return render(
175
+ request,
176
+ "web/strategies.html",
177
+ {
178
+ **base_context(request, "strategies"),
179
+ "strategies": strategies,
180
+ "strategy_mode": strategy_mode,
181
+ "selected_strategy": selected_strategy,
182
+ "selected_strategy_name": selected_name,
183
+ "strategy_preview": preview,
184
+ },
185
+ )
186
+
187
+
188
+ @require_POST
189
+ @require_local_or_staff
190
+ def optional_skill_create(request: HttpRequest, role: str) -> HttpResponse:
191
+ return _mutating_redirect(
192
+ request,
193
+ f"/harness/agents/?role={role}",
194
+ lambda root: create_or_update_optional_skill(
195
+ root,
196
+ role,
197
+ _post(request, "name"),
198
+ description=_post(request, "description"),
199
+ body=_post(request, "body"),
200
+ status=_post(request, "status") or "draft",
201
+ actor="web",
202
+ ),
203
+ )
204
+
205
+
206
+ @require_POST
207
+ @require_local_or_staff
208
+ def optional_skill_update(request: HttpRequest, role: str, name: str) -> HttpResponse:
209
+ return _mutating_redirect(
210
+ request,
211
+ f"/harness/agents/?role={role}&skill={name}",
212
+ lambda root: create_or_update_optional_skill(
213
+ root,
214
+ role,
215
+ name,
216
+ description=_post(request, "description"),
217
+ body=_post(request, "body"),
218
+ status=_post(request, "status") or "draft",
219
+ actor="web",
220
+ ),
221
+ )
222
+
223
+
224
+ @require_POST
225
+ @require_local_or_staff
226
+ def optional_skill_activate(request: HttpRequest, role: str, name: str) -> HttpResponse:
227
+ return _mutating_redirect(request, f"/harness/agents/?role={role}&skill={name}", lambda root: set_optional_skill_status(root, role, name, "active", actor="web"))
228
+
229
+
230
+ @require_POST
231
+ @require_local_or_staff
232
+ def optional_skill_archive(request: HttpRequest, role: str, name: str) -> HttpResponse:
233
+ return _mutating_redirect(request, f"/harness/agents/?role={role}&skill={name}", lambda root: set_optional_skill_status(root, role, name, "archived", actor="web"))
234
+
235
+
236
+ @require_POST
237
+ @require_local_or_staff
238
+ def optional_skill_delete(request: HttpRequest, role: str, name: str) -> HttpResponse:
239
+ return _mutating_redirect(request, f"/harness/agents/?role={role}", lambda root: delete_optional_skill(root, role, name, force=_post(request, "force") == "true", actor="web"))
240
+
241
+
242
+ @require_POST
243
+ @require_local_or_staff
244
+ def agent_instruction_update(request: HttpRequest, role: str) -> HttpResponse:
245
+ return _mutating_redirect(
246
+ request,
247
+ f"/harness/agents/?role={role}",
248
+ lambda root: write_agent_additional_instructions(root, role, _post_preserve_newlines(request, "body"), actor="web"),
249
+ )
250
+
251
+
252
+ @require_POST
253
+ @require_local_or_staff
254
+ def workspace_open(request: HttpRequest) -> HttpResponse:
255
+ next_url = _safe_next_url(str(request.POST.get("next") or request.META.get("HTTP_REFERER") or "/research/"))
256
+ raw_path = str(request.POST.get("workspace_path") or request.POST.get("path") or "").strip()
257
+ if not raw_path:
258
+ request.session[WORKSPACE_ERROR_SESSION_KEY] = "Workspace path is required."
259
+ request.session.modified = True
260
+ return redirect(next_url)
261
+ return _open_workspace_path(request, Path(raw_path).expanduser().resolve(), next_url)
262
+
263
+
264
+ @require_POST
265
+ @require_local_or_staff
266
+ def workspace_browse(request: HttpRequest) -> HttpResponse:
267
+ next_url = _safe_next_url(str(request.POST.get("next") or request.META.get("HTTP_REFERER") or "/research/"))
268
+ try:
269
+ target = _choose_workspace_directory()
270
+ except Exception as exc:
271
+ request.session[WORKSPACE_ERROR_SESSION_KEY] = f"Could not choose workspace folder: {exc}"
272
+ request.session.modified = True
273
+ return redirect(next_url)
274
+ return _open_workspace_path(request, target, next_url)
275
+
276
+
277
+ @require_POST
278
+ @require_local_or_staff
279
+ def workspace_remove(request: HttpRequest, workspace_id: str) -> HttpResponse:
280
+ next_url = _safe_next_url(str(request.POST.get("next") or request.META.get("HTTP_REFERER") or "/research/"))
281
+ try:
282
+ ensure_runtime_database(None)
283
+ from apps.harness.models import WorkspaceContext
284
+
285
+ WorkspaceContext.objects.filter(workspace_id=workspace_id).delete()
286
+ if request.session.get(WORKSPACE_SESSION_KEY) == workspace_id:
287
+ request.session.pop(WORKSPACE_SESSION_KEY, None)
288
+ request.session[WORKSPACE_NOTICE_SESSION_KEY] = "Workspace reference removed. Files were not touched."
289
+ request.session.modified = True
290
+ except Exception as exc:
291
+ request.session[WORKSPACE_ERROR_SESSION_KEY] = f"Could not remove workspace reference: {exc}"
292
+ request.session.modified = True
293
+ return redirect(_url_without_workspace(next_url))
294
+
295
+
296
+ def _open_workspace_path(request: HttpRequest, target: Path, next_url: str) -> HttpResponse:
297
+ try:
298
+ bootstrapped = not (target / WORKSPACE_MANIFEST_REL).exists()
299
+ if bootstrapped:
300
+ bootstrap_workspace(target, force=True)
301
+ ensure_runtime_database(target)
302
+ context = persist_workspace_context_if_available(target)
303
+ request.session[WORKSPACE_SESSION_KEY] = context["workspace_id"]
304
+ request.session[WORKSPACE_NOTICE_SESSION_KEY] = "Workspace bootstrapped and opened." if bootstrapped else "Workspace opened."
305
+ request.session.modified = True
306
+ return redirect(_url_with_workspace(next_url, context["workspace_id"]))
307
+ except Exception as exc:
308
+ request.session[WORKSPACE_ERROR_SESSION_KEY] = f"Could not open workspace: {exc}"
309
+ request.session.modified = True
310
+ return redirect(next_url)
311
+
312
+
313
+ def _choose_workspace_directory() -> Path:
314
+ if os.name != "posix":
315
+ raise RuntimeError("native folder picker is only available on this local desktop platform")
316
+ script = 'POSIX path of (choose folder with prompt "Open TradingCodex workspace")'
317
+ result = subprocess.run(
318
+ ["osascript", "-e", script],
319
+ capture_output=True,
320
+ text=True,
321
+ timeout=300,
322
+ check=False,
323
+ )
324
+ if result.returncode != 0:
325
+ message = result.stderr.strip() or "folder selection cancelled"
326
+ raise RuntimeError(message)
327
+ selected = result.stdout.strip()
328
+ if not selected:
329
+ raise RuntimeError("folder selection cancelled")
330
+ return Path(selected).expanduser().resolve()
331
+
332
+
333
+ def _render_agents(request: HttpRequest, selected_role: str | None = None) -> HttpResponse:
334
+ root = workspace_root(request)
335
+ state = build_projection_state(root)
336
+ role = selected_role or request.GET.get("role") or "head-manager"
337
+ if role not in state["agents"]:
338
+ role = "head-manager"
339
+ agent = state["agents"].get(role)
340
+ if not agent:
341
+ return HttpResponse("Unknown agent role.", status=404)
342
+ required_skills = [_skill_preview_item(root, state, skill_id, "required") for skill_id in agent.get("builtin_skills", [])]
343
+ optional_skills = [
344
+ _skill_preview_item(root, state, str(record.get("name") or ""), "optional", record=record)
345
+ for record in agent.get("optional_skills", [])
346
+ if record.get("status") == "active"
347
+ ]
348
+ if role == "head-manager":
349
+ optional_skills.extend(
350
+ _skill_preview_item(root, state, skill_id, "strategy")
351
+ for skill_id, skill in sorted(state.get("skills", {}).items())
352
+ if skill.get("source") == "strategy" and skill.get("active")
353
+ )
354
+ skill_id = request.GET.get("skill") or (required_skills[0]["id"] if required_skills else optional_skills[0]["id"] if optional_skills else "")
355
+ skill_preview = _skill_markdown_preview(root, state, skill_id) if skill_id else render_markdown_preview("_No skill selected._")
356
+ for item in [*required_skills, *optional_skills]:
357
+ item["selected"] = item["id"] == skill_id
358
+ context = {
359
+ **base_context(request, "agents"),
360
+ "state": state,
361
+ "head_manager": state["agents"]["head-manager"],
362
+ "agents": [state["agents"][agent_role] for agent_role in EXPECTED_SUBAGENTS],
363
+ "role_cards": [state["agents"]["head-manager"], *[state["agents"][agent_role] for agent_role in EXPECTED_SUBAGENTS]],
364
+ "selected_agent": agent,
365
+ "required_skills": required_skills,
366
+ "optional_skills": optional_skills,
367
+ "selected_skill_id": skill_id,
368
+ "skill_preview": skill_preview,
369
+ "agent": agent,
370
+ "additional_instructions": read_agent_additional_instructions(root, role),
371
+ }
372
+ return render(request, "web/agents.html", context)
373
+
374
+
375
+ @require_GET
376
+ @require_local_or_staff
377
+ def research(request: HttpRequest) -> HttpResponse:
378
+ root = workspace_root(request)
379
+ artifacts = list_workspace_research_artifacts(root)
380
+ selected_artifact_id = request.GET.get("artifact") or ""
381
+ selected_artifact: dict[str, Any] | None = None
382
+ artifact_preview: MarkdownPreview | None = None
383
+ if selected_artifact_id:
384
+ selected_artifact = next((artifact for artifact in artifacts if artifact.get("artifact_id") == selected_artifact_id or artifact.get("path") == selected_artifact_id), None)
385
+ if selected_artifact:
386
+ artifact_preview = read_markdown_preview(
387
+ root / str(selected_artifact["path"]),
388
+ source_file=str(selected_artifact["path"]),
389
+ source_label="workspace research file",
390
+ )
391
+ else:
392
+ artifact_preview = render_markdown_preview("_Research file is unavailable._", source_label="workspace research file")
393
+ context = {
394
+ **base_context(request, "research"),
395
+ "artifacts": artifacts,
396
+ "selected_artifact": selected_artifact,
397
+ "selected_artifact_id": selected_artifact_id,
398
+ "artifact_preview": artifact_preview,
399
+ "research": research_overview(root),
400
+ }
401
+ return render(request, "web/research.html", context)
402
+
403
+
404
+ def _strategy_web_records(root: Path, records: list[dict[str, Any]]) -> list[dict[str, Any]]:
405
+ web_records: list[dict[str, Any]] = []
406
+ for record in records:
407
+ if record.get("name") == "strategy-creator":
408
+ continue
409
+ source_file = str(record.get("source_file") or "")
410
+ preview = read_markdown_preview(root / source_file, source_file=source_file, source_label="strategy skill")
411
+ fields = dict(record.get("frontmatter") or {})
412
+ web_records.append(
413
+ {
414
+ **record,
415
+ "heading": preview.heading or record.get("name"),
416
+ "description": fields.get("description") or record.get("description") or "",
417
+ }
418
+ )
419
+ return web_records
420
+
421
+
422
+ @require_GET
423
+ @require_local_or_staff
424
+ def portfolio(request: HttpRequest) -> HttpResponse:
425
+ context = {**base_context(request, "portfolio"), "portfolio": portfolio_overview()}
426
+ return render(request, "web/portfolio.html", context)
427
+
428
+
429
+ @require_GET
430
+ @require_local_or_staff
431
+ def orders(request: HttpRequest) -> HttpResponse:
432
+ context = {**base_context(request, "orders"), **orders_overview()}
433
+ return render(request, "web/orders.html", context)
434
+
435
+
436
+ @require_GET
437
+ @require_local_or_staff
438
+ def policy(request: HttpRequest) -> HttpResponse:
439
+ context = {**base_context(request, "policy"), "policy": list_policy_overview(workspace_root(request))}
440
+ return render(request, "web/policy.html", context)
441
+
442
+
443
+ @require_GET
444
+ @require_local_or_staff
445
+ def activity(request: HttpRequest) -> HttpResponse:
446
+ context = {**base_context(request, "activity"), "activity": list_recent_activity(workspace_root(request), limit=50)}
447
+ return render(request, "web/activity.html", context)
448
+
449
+
450
+ @require_GET
451
+ @require_local_or_staff
452
+ def mcp_router(request: HttpRequest) -> HttpResponse:
453
+ context = {**base_context(request, "mcp-router"), **mcp_router_overview()}
454
+ return render(request, "web/mcp_router.html", context)
455
+
456
+
457
+ @require_POST
458
+ @require_local_or_staff
459
+ def mcp_router_create(request: HttpRequest) -> HttpResponse:
460
+ return _service_redirect(
461
+ request,
462
+ "/integrations/mcp/",
463
+ lambda: create_or_update_router(
464
+ name=_post(request, "name"),
465
+ label=_post(request, "label"),
466
+ transport=_post(request, "transport") or "stdio",
467
+ command=_post(request, "command"),
468
+ url=_post(request, "url"),
469
+ credential_ref=_post(request, "credential_ref"),
470
+ enabled=_post(request, "enabled") == "true",
471
+ actor="web",
472
+ ),
473
+ )
474
+
475
+
476
+ @require_POST
477
+ @require_local_or_staff
478
+ def mcp_router_import(request: HttpRequest, router_id: int) -> HttpResponse:
479
+ def operation() -> Any:
480
+ from apps.mcp.models import McpRouter
481
+
482
+ router = McpRouter.objects.get(pk=router_id)
483
+ return import_external_mcp_discovery(router, _post(request, "discovery_payload"), actor="web")
484
+
485
+ return _service_redirect(request, f"/integrations/mcp/#router-{router_id}", operation)
486
+
487
+
488
+ @require_POST
489
+ @require_local_or_staff
490
+ def mcp_external_tool_update(request: HttpRequest, tool_id: int) -> HttpResponse:
491
+ def operation() -> Any:
492
+ from apps.mcp.models import McpExternalTool
493
+
494
+ tool = McpExternalTool.objects.get(pk=tool_id)
495
+ return set_external_tool_policy(
496
+ tool,
497
+ category=_post(request, "category"),
498
+ risk_level=_post(request, "risk_level"),
499
+ sensitivity=_post(request, "sensitivity"),
500
+ canonical_capability=_post(request, "canonical_capability"),
501
+ proxy_mode=_post(request, "proxy_mode"),
502
+ allowed_roles=_split_csv(_post(request, "allowed_roles")),
503
+ enabled=_post(request, "enabled") == "true",
504
+ review_status="reviewed",
505
+ actor="web",
506
+ )
507
+
508
+ return _service_redirect(request, f"/integrations/mcp/#tool-{tool_id}", operation)
509
+
510
+
511
+ @require_POST
512
+ @require_local_or_staff
513
+ def mcp_external_tool_check(request: HttpRequest, tool_id: int) -> HttpResponse:
514
+ def operation() -> Any:
515
+ from apps.mcp.models import McpExternalTool
516
+
517
+ tool = McpExternalTool.objects.get(pk=tool_id)
518
+ raw_arguments = _post(request, "arguments")
519
+ arguments = json.loads(raw_arguments) if raw_arguments else {}
520
+ return evaluate_external_mcp_proxy_call(
521
+ workspace_root(request),
522
+ tool,
523
+ principal_id=_post(request, "principal_id") or "head-manager",
524
+ arguments=arguments if isinstance(arguments, dict) else {},
525
+ actor="web",
526
+ )
527
+
528
+ return _service_redirect(request, f"/integrations/mcp/#tool-{tool_id}", operation)
529
+
530
+
531
+ @require_GET
532
+ @require_local_or_staff
533
+ def starter_prompt(request: HttpRequest) -> HttpResponse:
534
+ query = request.GET.get("q", "")
535
+ context = {
536
+ **base_context(request, "workflow"),
537
+ "query": query,
538
+ "starter_prompt": build_subagent_starter_prompt(query) if query.strip() else "",
539
+ }
540
+ return render(request, "web/starter_prompt.html", context)
541
+
542
+
543
+ @require_GET
544
+ @require_local_or_staff
545
+ def starter_prompt_fragment(request: HttpRequest) -> HttpResponse:
546
+ query = request.GET.get("q", "")
547
+ return render(
548
+ request,
549
+ "web/fragments/starter_prompt.html",
550
+ {"query": query, "starter_prompt": build_subagent_starter_prompt(query) if query.strip() else ""},
551
+ )
552
+
553
+
554
+ def _post(request: HttpRequest, name: str) -> str:
555
+ return str(request.POST.get(name) or "").strip()
556
+
557
+
558
+ def _post_preserve_newlines(request: HttpRequest, name: str) -> str:
559
+ return str(request.POST.get(name) or "")
560
+
561
+
562
+ def _mutating_redirect(request: HttpRequest, fallback_url: str, operation: Callable[[Path], Any]) -> HttpResponse:
563
+ next_url = _safe_next_url(str(request.POST.get("next") or fallback_url))
564
+ try:
565
+ operation(workspace_root(request))
566
+ request.session[WORKSPACE_NOTICE_SESSION_KEY] = "Workspace files updated."
567
+ except Exception as exc:
568
+ request.session[WORKSPACE_ERROR_SESSION_KEY] = f"Could not update workspace files: {exc}"
569
+ request.session.modified = True
570
+ return redirect(next_url)
571
+
572
+
573
+ def _service_redirect(request: HttpRequest, fallback_url: str, operation: Callable[[], Any]) -> HttpResponse:
574
+ next_url = _safe_next_url(str(request.POST.get("next") or fallback_url))
575
+ try:
576
+ operation()
577
+ request.session[WORKSPACE_NOTICE_SESSION_KEY] = "MCP settings updated."
578
+ except Exception as exc:
579
+ request.session[WORKSPACE_ERROR_SESSION_KEY] = f"Could not update MCP settings: {exc}"
580
+ request.session.modified = True
581
+ return redirect(next_url)
582
+
583
+
584
+ def _split_csv(value: str) -> list[str]:
585
+ return [item.strip() for item in value.split(",") if item.strip()]
586
+
587
+
588
+ def research_overview(root: Path) -> dict[str, Any]:
589
+ artifacts = list_workspace_research_artifacts(root)[:5]
590
+ universes = sorted({artifact.get("universe") for artifact in artifacts if artifact.get("universe")})
591
+ return {"count": len(artifacts), "recent": artifacts, "universes": universes}
592
+
593
+
594
+ def _skill_preview_item(
595
+ root: Path,
596
+ state: dict[str, Any],
597
+ skill_id: str,
598
+ kind: str,
599
+ *,
600
+ record: dict[str, Any] | None = None,
601
+ ) -> dict[str, Any]:
602
+ skill = state.get("skills", {}).get(skill_id, {})
603
+ spec = SKILL_SPECS.get(skill_id)
604
+ source_file, source_label = _skill_source(root, skill_id, skill=skill, record=record)
605
+ return {
606
+ "id": skill_id,
607
+ "label": str(skill.get("label") or (spec.label if spec else "") or _skill_heading(root, skill_id, skill=skill, record=record) or skill_id),
608
+ "kind": kind,
609
+ "status": str((record or {}).get("status") or skill.get("status") or "active"),
610
+ "validation_status": str((record or {}).get("validation_status") or skill.get("validation_status") or "valid"),
611
+ "risk_tags": list((record or {}).get("risk_tags") or skill.get("risk_tags") or []),
612
+ "source_file": source_file,
613
+ "source_label": source_label,
614
+ "selected": False,
615
+ }
616
+
617
+
618
+ def _skill_markdown_preview(root: Path, state: dict[str, Any], skill_id: str) -> MarkdownPreview:
619
+ skill = state.get("skills", {}).get(skill_id, {})
620
+ source_file, source_label = _skill_source(root, skill_id, skill=skill)
621
+ path = _skill_markdown_path(root, skill_id, skill=skill)
622
+ return read_markdown_preview(path, source_file=source_file, source_label=source_label)
623
+
624
+
625
+ def _skill_heading(
626
+ root: Path,
627
+ skill_id: str,
628
+ *,
629
+ skill: dict[str, Any] | None = None,
630
+ record: dict[str, Any] | None = None,
631
+ ) -> str:
632
+ path = _skill_markdown_path(root, skill_id, skill=skill, record=record)
633
+ return read_markdown_preview(path).heading
634
+
635
+
636
+ def _skill_source(
637
+ root: Path,
638
+ skill_id: str,
639
+ *,
640
+ skill: dict[str, Any] | None = None,
641
+ record: dict[str, Any] | None = None,
642
+ ) -> tuple[str, str]:
643
+ path = _skill_markdown_path(root, skill_id, skill=skill, record=record)
644
+ try:
645
+ return path.relative_to(root).as_posix(), "workspace skill"
646
+ except ValueError:
647
+ try:
648
+ return path.relative_to(Path(__file__).resolve().parents[1]).as_posix(), "repo template"
649
+ except ValueError:
650
+ return str(path), "markdown file"
651
+
652
+
653
+ def _skill_markdown_path(
654
+ root: Path,
655
+ skill_id: str,
656
+ *,
657
+ skill: dict[str, Any] | None = None,
658
+ record: dict[str, Any] | None = None,
659
+ ) -> Path:
660
+ for raw in [record.get("source_file") if record else "", skill.get("source_file") if skill else ""]:
661
+ if raw:
662
+ candidate = Path(raw)
663
+ path = candidate if candidate.is_absolute() else root / candidate
664
+ if path.exists():
665
+ return path
666
+ workspace_path = root / ".agents" / "skills" / skill_id / "SKILL.md"
667
+ if workspace_path.exists():
668
+ return workspace_path
669
+ spec = SKILL_SPECS.get(skill_id)
670
+ if spec and spec.scope == "subagent_shared":
671
+ return SUBAGENT_SKILL_TEMPLATE_ROOT / "shared" / skill_id / "SKILL.md"
672
+ if spec and spec.scope == "subagent_role":
673
+ role = spec.owner_roles[0] if spec.owner_roles else ""
674
+ return SUBAGENT_SKILL_TEMPLATE_ROOT / role / skill_id / "SKILL.md"
675
+ return SKILL_TEMPLATE_ROOT / skill_id / "SKILL.md"
676
+
677
+
678
+ def workspace_options(selected_root: Path) -> list[dict[str, Any]]:
679
+ selected_context = workspace_context_payload(selected_root)
680
+ options: list[dict[str, Any]] = []
681
+ try:
682
+ ensure_runtime_database(None)
683
+ persist_workspace_context_if_available(selected_root)
684
+ from apps.harness.models import WorkspaceContext
685
+
686
+ for workspace in WorkspaceContext.objects.order_by("-last_seen_at", "project_name", "id")[:20]:
687
+ options.append(_workspace_option_from_model(workspace))
688
+ except Exception:
689
+ options = []
690
+
691
+ selected_option = _workspace_option_from_context(selected_context)
692
+ if not any(option["workspace_id"] == selected_option["workspace_id"] for option in options):
693
+ options.insert(0, selected_option)
694
+ for option in options:
695
+ option["selected"] = option["workspace_id"] == selected_context["workspace_id"]
696
+ return options[:20]
697
+
698
+
699
+ def _workspace_option_by_id(workspace_id: str) -> dict[str, Any] | None:
700
+ if not workspace_id:
701
+ return None
702
+ try:
703
+ ensure_runtime_database(None)
704
+ from apps.harness.models import WorkspaceContext
705
+
706
+ workspace = WorkspaceContext.objects.filter(workspace_id=workspace_id).first()
707
+ if not workspace:
708
+ return None
709
+ option = _workspace_option_from_model(workspace)
710
+ if not Path(option["path"]).expanduser().exists():
711
+ return None
712
+ return option
713
+ except Exception:
714
+ return None
715
+
716
+
717
+ def _workspace_option_from_model(workspace: Any) -> dict[str, Any]:
718
+ active_profile = workspace.active_profile if isinstance(workspace.active_profile, dict) else {}
719
+ path = Path(workspace.path).expanduser()
720
+ exists = path.exists()
721
+ bootstrapped = (path / WORKSPACE_MANIFEST_REL).exists()
722
+ return {
723
+ "workspace_id": workspace.workspace_id,
724
+ "project_name": workspace.project_name,
725
+ "path": workspace.path,
726
+ "git_branch": workspace.git_branch,
727
+ "active_profile": active_profile,
728
+ "active_profile_label": str(active_profile.get("label") or active_profile.get("profile_id") or "default-paper"),
729
+ "last_seen_at": workspace.last_seen_at,
730
+ "exists": exists,
731
+ "bootstrapped": bootstrapped,
732
+ "status_label": "Ready" if exists and bootstrapped else "Needs bootstrap" if exists else "Missing",
733
+ "selected": False,
734
+ }
735
+
736
+
737
+ def _workspace_option_from_context(context: dict[str, Any]) -> dict[str, Any]:
738
+ active_profile = context.get("active_profile") if isinstance(context.get("active_profile"), dict) else {}
739
+ path = Path(str(context["path"])).expanduser()
740
+ exists = path.exists()
741
+ bootstrapped = (path / WORKSPACE_MANIFEST_REL).exists()
742
+ return {
743
+ "workspace_id": context["workspace_id"],
744
+ "project_name": context["project_name"],
745
+ "path": context["path"],
746
+ "git_branch": context.get("git_branch", ""),
747
+ "active_profile": active_profile,
748
+ "active_profile_label": str(active_profile.get("label") or active_profile.get("profile_id") or "default-paper"),
749
+ "last_seen_at": None,
750
+ "exists": exists,
751
+ "bootstrapped": bootstrapped,
752
+ "status_label": "Ready" if exists and bootstrapped else "Needs bootstrap" if exists else "Missing",
753
+ "selected": False,
754
+ }
755
+
756
+
757
+ def _pop_session_message(request: HttpRequest, key: str) -> str:
758
+ if not hasattr(request, "session"):
759
+ return ""
760
+ value = request.session.pop(key, "")
761
+ if value:
762
+ request.session.modified = True
763
+ return str(value)
764
+
765
+
766
+ def _safe_next_url(raw_url: str) -> str:
767
+ if not raw_url.startswith("/") or raw_url.startswith("//"):
768
+ return "/research/"
769
+ return raw_url
770
+
771
+
772
+ def _url_with_workspace(raw_url: str, workspace_id: str) -> str:
773
+ split = urlsplit(_safe_next_url(raw_url))
774
+ query = [(key, value) for key, value in parse_qsl(split.query, keep_blank_values=True) if key != "workspace"]
775
+ query.insert(0, ("workspace", workspace_id))
776
+ return urlunsplit((split.scheme, split.netloc, split.path or "/research/", urlencode(query), split.fragment))
777
+
778
+
779
+ def _url_without_workspace(raw_url: str) -> str:
780
+ split = urlsplit(_safe_next_url(raw_url))
781
+ query = [(key, value) for key, value in parse_qsl(split.query, keep_blank_values=True) if key != "workspace"]
782
+ return urlunsplit((split.scheme, split.netloc, split.path or "/research/", urlencode(query), split.fragment))
783
+
784
+
785
+ def portfolio_overview() -> dict[str, Any]:
786
+ try:
787
+ from apps.portfolio.models import PortfolioSnapshot
788
+
789
+ latest = PortfolioSnapshot.objects.order_by("-created_at", "-id").first()
790
+ if latest and isinstance(latest.payload, dict):
791
+ state = dict(latest.payload)
792
+ state.setdefault("updated_at", latest.created_at.isoformat())
793
+ else:
794
+ state = default_paper_portfolio_state(DEFAULT_PORTFOLIO_ID, DEFAULT_ACCOUNT_ID, DEFAULT_STRATEGY_ID)
795
+ positions = state.get("positions") if isinstance(state.get("positions"), dict) else {}
796
+ return {
797
+ "cash_krw": state.get("cash_krw", 0),
798
+ "positions": sorted(
799
+ [
800
+ {
801
+ "symbol": symbol,
802
+ "quantity": position.get("quantity", 0),
803
+ "average_price": position.get("average_price", 0),
804
+ "currency": position.get("currency", "KRW"),
805
+ }
806
+ for symbol, position in positions.items()
807
+ ],
808
+ key=lambda item: item["symbol"],
809
+ ),
810
+ "positions_count": len(positions),
811
+ "updated_at": state.get("updated_at", ""),
812
+ "portfolio_id": state.get("portfolio_id", DEFAULT_PORTFOLIO_ID),
813
+ "account_id": state.get("account_id", DEFAULT_ACCOUNT_ID),
814
+ "strategy_id": state.get("strategy_id", DEFAULT_STRATEGY_ID),
815
+ }
816
+ except Exception:
817
+ state = default_paper_portfolio_state(DEFAULT_PORTFOLIO_ID, DEFAULT_ACCOUNT_ID, DEFAULT_STRATEGY_ID)
818
+ return {
819
+ "cash_krw": state["cash_krw"],
820
+ "positions": [],
821
+ "positions_count": 0,
822
+ "updated_at": state["updated_at"],
823
+ "portfolio_id": DEFAULT_PORTFOLIO_ID,
824
+ "account_id": DEFAULT_ACCOUNT_ID,
825
+ "strategy_id": DEFAULT_STRATEGY_ID,
826
+ }
827
+
828
+
829
+ def orders_overview() -> dict[str, Any]:
830
+ try:
831
+ from apps.orders.models import ApprovalReceipt, ExecutionResult, OrderIntent
832
+
833
+ return {
834
+ "order_intents": OrderIntent.objects.order_by("-created_at", "-id")[:30],
835
+ "approval_receipts": ApprovalReceipt.objects.order_by("-created_at", "-id")[:30],
836
+ "execution_results": ExecutionResult.objects.order_by("-created_at", "-id")[:30],
837
+ "order_count": OrderIntent.objects.count(),
838
+ "approval_count": ApprovalReceipt.objects.count(),
839
+ "execution_count": ExecutionResult.objects.count(),
840
+ }
841
+ except Exception:
842
+ return {
843
+ "order_intents": [],
844
+ "approval_receipts": [],
845
+ "execution_results": [],
846
+ "order_count": 0,
847
+ "approval_count": 0,
848
+ "execution_count": 0,
849
+ }
850
+
851
+
852
+ def mcp_router_overview() -> dict[str, Any]:
853
+ try:
854
+ ensure_runtime_database(None)
855
+ from apps.mcp.models import McpExternalTool, McpExternalToolCall, McpRouter
856
+
857
+ routers = list(McpRouter.objects.prefetch_related("external_tools").all())
858
+ tools = list(McpExternalTool.objects.select_related("router").all())
859
+ return {
860
+ "routers": routers,
861
+ "external_tools": tools,
862
+ "recent_external_calls": McpExternalToolCall.objects.select_related("external_tool")[:15],
863
+ "router_count": len(routers),
864
+ "external_tool_count": len(tools),
865
+ "enabled_external_tool_count": sum(1 for tool in tools if tool.enabled),
866
+ "review_required_count": sum(1 for tool in tools if tool.review_status != "reviewed" or tool.drift_detected),
867
+ "category_options": ["market_data", "account_read", "research_write", "portfolio_state", "policy_admin", "execution", "secret", "workflow_prompt", "unknown"],
868
+ "risk_options": ["read", "write", "approval", "execution", "blocked", "unknown"],
869
+ "sensitivity_options": ["public", "private", "research", "canonical_state", "secret", "unknown"],
870
+ "proxy_mode_options": ["blocked", "read_only", "summary_only", "service_path", "service_adapter"],
871
+ }
872
+ except Exception:
873
+ return {
874
+ "routers": [],
875
+ "external_tools": [],
876
+ "recent_external_calls": [],
877
+ "router_count": 0,
878
+ "external_tool_count": 0,
879
+ "enabled_external_tool_count": 0,
880
+ "review_required_count": 0,
881
+ "category_options": [],
882
+ "risk_options": [],
883
+ "sensitivity_options": [],
884
+ "proxy_mode_options": [],
885
+ }