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.
- apps/__init__.py +1 -0
- apps/audit/__init__.py +1 -0
- apps/audit/admin.py +6 -0
- apps/audit/apps.py +8 -0
- apps/audit/migrations/0001_initial.py +35 -0
- apps/audit/migrations/__init__.py +0 -0
- apps/audit/models.py +22 -0
- apps/harness/__init__.py +1 -0
- apps/harness/admin.py +6 -0
- apps/harness/apps.py +8 -0
- apps/harness/migrations/0001_initial.py +35 -0
- apps/harness/migrations/__init__.py +0 -0
- apps/harness/models.py +22 -0
- apps/harness/templatetags/__init__.py +1 -0
- apps/integrations/__init__.py +1 -0
- apps/integrations/admin.py +6 -0
- apps/integrations/apps.py +8 -0
- apps/integrations/migrations/0001_initial.py +29 -0
- apps/integrations/migrations/__init__.py +0 -0
- apps/integrations/models.py +16 -0
- apps/integrations/services.py +31 -0
- apps/mcp/__init__.py +1 -0
- apps/mcp/admin.py +20 -0
- apps/mcp/apps.py +8 -0
- apps/mcp/migrations/0001_initial.py +168 -0
- apps/mcp/migrations/__init__.py +0 -0
- apps/mcp/models.py +154 -0
- apps/mcp/services.py +327 -0
- apps/orders/__init__.py +1 -0
- apps/orders/admin.py +6 -0
- apps/orders/apps.py +8 -0
- apps/orders/migrations/0001_initial.py +79 -0
- apps/orders/migrations/__init__.py +0 -0
- apps/orders/models.py +66 -0
- apps/orders/services.py +107 -0
- apps/policy/__init__.py +1 -0
- apps/policy/admin.py +6 -0
- apps/policy/apps.py +8 -0
- apps/policy/migrations/0001_initial.py +75 -0
- apps/policy/migrations/__init__.py +0 -0
- apps/policy/models.py +61 -0
- apps/policy/services.py +110 -0
- apps/portfolio/__init__.py +1 -0
- apps/portfolio/admin.py +6 -0
- apps/portfolio/apps.py +8 -0
- apps/portfolio/migrations/0001_initial.py +67 -0
- apps/portfolio/migrations/__init__.py +0 -0
- apps/portfolio/models.py +53 -0
- apps/research/__init__.py +1 -0
- apps/research/admin.py +1 -0
- apps/research/apps.py +8 -0
- apps/research/migrations/__init__.py +0 -0
- apps/research/models.py +1 -0
- apps/workflows/__init__.py +1 -0
- apps/workflows/admin.py +6 -0
- apps/workflows/apps.py +8 -0
- apps/workflows/migrations/0001_initial.py +51 -0
- apps/workflows/migrations/__init__.py +0 -0
- apps/workflows/models.py +44 -0
- tradingcodex-0.1.0.dist-info/METADATA +337 -0
- tradingcodex-0.1.0.dist-info/RECORD +254 -0
- tradingcodex-0.1.0.dist-info/WHEEL +5 -0
- tradingcodex-0.1.0.dist-info/entry_points.txt +2 -0
- tradingcodex-0.1.0.dist-info/licenses/LICENSE +202 -0
- tradingcodex-0.1.0.dist-info/licenses/NOTICE +24 -0
- tradingcodex-0.1.0.dist-info/top_level.txt +4 -0
- tradingcodex_cli/__init__.py +1 -0
- tradingcodex_cli/__main__.py +124 -0
- tradingcodex_cli/commands/__init__.py +2 -0
- tradingcodex_cli/commands/bootstrap.py +157 -0
- tradingcodex_cli/commands/db.py +36 -0
- tradingcodex_cli/commands/doctor.py +230 -0
- tradingcodex_cli/commands/mcp.py +186 -0
- tradingcodex_cli/commands/orders.py +89 -0
- tradingcodex_cli/commands/policy.py +21 -0
- tradingcodex_cli/commands/profile.py +110 -0
- tradingcodex_cli/commands/research.py +76 -0
- tradingcodex_cli/commands/skills.py +93 -0
- tradingcodex_cli/commands/strategies.py +67 -0
- tradingcodex_cli/commands/subagents.py +106 -0
- tradingcodex_cli/commands/utils.py +134 -0
- tradingcodex_cli/commands/workspaces.py +53 -0
- tradingcodex_cli/generator.py +234 -0
- tradingcodex_cli/mcp_stdio.py +26 -0
- tradingcodex_cli/service_autostart.py +127 -0
- tradingcodex_service/__init__.py +5 -0
- tradingcodex_service/admin.py +1 -0
- tradingcodex_service/api.py +486 -0
- tradingcodex_service/application/__init__.py +2 -0
- tradingcodex_service/application/agents.py +1470 -0
- tradingcodex_service/application/audit.py +71 -0
- tradingcodex_service/application/common.py +88 -0
- tradingcodex_service/application/components.py +299 -0
- tradingcodex_service/application/harness.py +747 -0
- tradingcodex_service/application/markdown_preview.py +179 -0
- tradingcodex_service/application/orders.py +404 -0
- tradingcodex_service/application/policy.py +150 -0
- tradingcodex_service/application/portfolio.py +166 -0
- tradingcodex_service/application/research.py +356 -0
- tradingcodex_service/application/runtime.py +321 -0
- tradingcodex_service/asgi.py +6 -0
- tradingcodex_service/mcp_http.py +59 -0
- tradingcodex_service/mcp_runtime.py +565 -0
- tradingcodex_service/settings.py +91 -0
- tradingcodex_service/templates/web/activity.html +24 -0
- tradingcodex_service/templates/web/agent_skills.html +111 -0
- tradingcodex_service/templates/web/agents.html +121 -0
- tradingcodex_service/templates/web/base.html +150 -0
- tradingcodex_service/templates/web/dashboard.html +109 -0
- tradingcodex_service/templates/web/fragments/role_inspector.html +81 -0
- tradingcodex_service/templates/web/fragments/starter_prompt.html +24 -0
- tradingcodex_service/templates/web/fragments/topology_canvas.html +85 -0
- tradingcodex_service/templates/web/harness.html +87 -0
- tradingcodex_service/templates/web/mcp_router.html +250 -0
- tradingcodex_service/templates/web/orders.html +68 -0
- tradingcodex_service/templates/web/policy.html +81 -0
- tradingcodex_service/templates/web/portfolio.html +52 -0
- tradingcodex_service/templates/web/research.html +73 -0
- tradingcodex_service/templates/web/starter_prompt.html +40 -0
- tradingcodex_service/templates/web/strategies.html +74 -0
- tradingcodex_service/urls.py +42 -0
- tradingcodex_service/version.py +1 -0
- tradingcodex_service/web.py +885 -0
- tradingcodex_service/wsgi.py +6 -0
- workspace_templates/__init__.py +1 -0
- workspace_templates/modules/audit/files/.tradingcodex/audit/README.md +5 -0
- workspace_templates/modules/audit/files/trading/audit/.gitkeep +1 -0
- workspace_templates/modules/audit/module.json +16 -0
- workspace_templates/modules/codex-base/files/.codex/config.toml +272 -0
- workspace_templates/modules/codex-base/files/.codex/hooks/tradingcodex_hook.py +173 -0
- workspace_templates/modules/codex-base/files/.codex/hooks.json +105 -0
- workspace_templates/modules/codex-base/files/.codex/prompts/base_instructions/head-manager.md +129 -0
- workspace_templates/modules/codex-base/files/.codex/rules/tradingcodex.rules +50 -0
- workspace_templates/modules/codex-base/files/.tradingcodex/capabilities.yaml +56 -0
- workspace_templates/modules/codex-base/files/.tradingcodex/cli.py +16 -0
- workspace_templates/modules/codex-base/files/.tradingcodex/config.yaml +53 -0
- workspace_templates/modules/codex-base/files/.tradingcodex/policies/policy-bindings.yaml +16 -0
- workspace_templates/modules/codex-base/files/.tradingcodex/policies/principals.yaml +17 -0
- workspace_templates/modules/codex-base/files/.tradingcodex/policies/roles.yaml +27 -0
- workspace_templates/modules/codex-base/files/AGENTS.md +56 -0
- workspace_templates/modules/codex-base/files/pyproject.toml +13 -0
- workspace_templates/modules/codex-base/files/tcx +35 -0
- workspace_templates/modules/codex-base/module.json +17 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/policies/access-policies.yaml +39 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/policies/restricted-list.yaml +6 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/approval_receipt.schema.json +14 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/audit_event.schema.json +11 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/evidence_pack.schema.json +16 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/execution_result.schema.json +12 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/fundamental_report.schema.json +15 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/news_report.schema.json +15 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/order_intent.schema.json +30 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/portfolio_review.schema.json +15 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/postmortem_report.schema.json +14 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/risk_report.schema.json +15 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/technical_report.schema.json +15 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/thesis.schema.json +15 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/valuation.schema.json +15 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/scripts/validate-order-intent.py +15 -0
- workspace_templates/modules/enforcement-guardrails/module.json +19 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/execution-operator.toml +71 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/fundamental-analyst.toml +62 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/instrument-analyst.toml +64 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/macro-analyst.toml +64 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/news-analyst.toml +63 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/portfolio-manager.toml +62 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/risk-manager.toml +65 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/technical-analyst.toml +63 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/valuation-analyst.toml +59 -0
- workspace_templates/modules/fixed-subagents/files/.tradingcodex/mainagent/head-manager.yaml +66 -0
- workspace_templates/modules/fixed-subagents/files/.tradingcodex/mainagent/skill-change-proposals/.gitkeep +1 -0
- workspace_templates/modules/fixed-subagents/files/.tradingcodex/mainagent/subagent-registry.yaml +56 -0
- workspace_templates/modules/fixed-subagents/module.json +23 -0
- workspace_templates/modules/guidance-guardrails/files/.tradingcodex/guidance/guardrails.md +17 -0
- workspace_templates/modules/guidance-guardrails/files/.tradingcodex/guidance/task-quality-checklist.md +37 -0
- workspace_templates/modules/guidance-guardrails/module.json +18 -0
- workspace_templates/modules/information-barriers/files/.tradingcodex/policies/information-barriers.yaml +211 -0
- workspace_templates/modules/information-barriers/files/.tradingcodex/secrets.md +9 -0
- workspace_templates/modules/information-barriers/files/trading/approvals/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/market-data/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/orders/approved/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/orders/draft/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/orders/executed/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/orders/rejected/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/portfolio/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/fundamental/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/instrument/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/macro/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/news/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/policy/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/portfolio/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/postmortem/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/risk/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/technical/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/valuation/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/research/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/module.json +19 -0
- workspace_templates/modules/paper-trading/files/.tradingcodex/mcp/adapters/paper-trading.py +4 -0
- workspace_templates/modules/paper-trading/module.json +16 -0
- workspace_templates/modules/postmortem/files/.tradingcodex/workflows/postmortem.yaml +12 -0
- workspace_templates/modules/postmortem/module.json +16 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/investment-workflow-map/SKILL.md +106 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/investment-workflow-map/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/manage-optional-skills/SKILL.md +101 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/manage-optional-skills/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/manage-subagents/SKILL.md +140 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/manage-subagents/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/orchestrate-workflow/SKILL.md +140 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/orchestrate-workflow/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/postmortem/SKILL.md +31 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/postmortem/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/scenario-quality-gates/SKILL.md +138 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/scenario-quality-gates/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/strategy-creator/SKILL.md +109 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/strategy-creator/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/synthesize-decision/SKILL.md +54 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/synthesize-decision/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/execution-operator/execute-paper-order/SKILL.md +35 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/execution-operator/execute-paper-order/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/fundamental-analyst/fundamental-analysis/SKILL.md +46 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/fundamental-analyst/fundamental-analysis/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/instrument-analyst/instrument-analysis/SKILL.md +40 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/instrument-analyst/instrument-analysis/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/macro-analyst/macro-analysis/SKILL.md +40 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/macro-analyst/macro-analysis/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/news-analyst/news-analysis/SKILL.md +43 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/news-analyst/news-analysis/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/portfolio-manager/create-order-intent/SKILL.md +46 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/portfolio-manager/create-order-intent/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/portfolio-manager/portfolio-review/SKILL.md +44 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/portfolio-manager/portfolio-review/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/approve-order/SKILL.md +38 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/approve-order/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/policy-review/SKILL.md +43 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/policy-review/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/review-risk/SKILL.md +45 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/review-risk/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/shared/collect-evidence/SKILL.md +46 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/shared/collect-evidence/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/shared/external-data-source-gate/SKILL.md +66 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/shared/external-data-source-gate/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/technical-analyst/technical-analysis/SKILL.md +43 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/technical-analyst/technical-analysis/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/valuation-analyst/valuation-review/SKILL.md +47 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/valuation-analyst/valuation-review/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/module.json +37 -0
- workspace_templates/modules/stub-execution/files/.tradingcodex/mcp/adapters/stub-execution.py +4 -0
- workspace_templates/modules/stub-execution/module.json +15 -0
- workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/adapters/live-adapter.contract.md +25 -0
- workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/enforcer/README.md +5 -0
- workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/gateway/README.md +8 -0
- workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/server.py +27 -0
- workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/smoke-call.py +18 -0
- 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
|
+
}
|