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,75 @@
|
|
|
1
|
+
# Generated by Django 5.2.5 on 2026-06-12 16:32
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
initial = True
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.CreateModel(
|
|
16
|
+
name='PolicyDecision',
|
|
17
|
+
fields=[
|
|
18
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
19
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
20
|
+
('principal_id', models.CharField(max_length=128)),
|
|
21
|
+
('action', models.CharField(max_length=160)),
|
|
22
|
+
('resource', models.CharField(blank=True, max_length=255)),
|
|
23
|
+
('decision', models.CharField(max_length=16)),
|
|
24
|
+
('reasons', models.JSONField(blank=True, default=list)),
|
|
25
|
+
('workspace_context', models.JSONField(blank=True, default=dict)),
|
|
26
|
+
],
|
|
27
|
+
options={
|
|
28
|
+
'verbose_name': 'Policy decision',
|
|
29
|
+
'verbose_name_plural': 'Policy decisions',
|
|
30
|
+
'ordering': ['-created_at', '-id'],
|
|
31
|
+
},
|
|
32
|
+
),
|
|
33
|
+
migrations.CreateModel(
|
|
34
|
+
name='Principal',
|
|
35
|
+
fields=[
|
|
36
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
37
|
+
('principal_id', models.CharField(max_length=128, unique=True)),
|
|
38
|
+
('role', models.CharField(max_length=128)),
|
|
39
|
+
('active', models.BooleanField(default=True)),
|
|
40
|
+
],
|
|
41
|
+
options={
|
|
42
|
+
'verbose_name': 'Principal',
|
|
43
|
+
'verbose_name_plural': 'Principals',
|
|
44
|
+
},
|
|
45
|
+
),
|
|
46
|
+
migrations.CreateModel(
|
|
47
|
+
name='RestrictedSymbol',
|
|
48
|
+
fields=[
|
|
49
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
50
|
+
('symbol', models.CharField(max_length=64, unique=True)),
|
|
51
|
+
('reason', models.TextField(blank=True)),
|
|
52
|
+
('active', models.BooleanField(default=True)),
|
|
53
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
54
|
+
],
|
|
55
|
+
options={
|
|
56
|
+
'verbose_name': 'Restricted symbol',
|
|
57
|
+
'verbose_name_plural': 'Restricted symbols',
|
|
58
|
+
},
|
|
59
|
+
),
|
|
60
|
+
migrations.CreateModel(
|
|
61
|
+
name='Capability',
|
|
62
|
+
fields=[
|
|
63
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
64
|
+
('action', models.CharField(max_length=160)),
|
|
65
|
+
('resource_pattern', models.CharField(default='*', max_length=255)),
|
|
66
|
+
('effect', models.CharField(choices=[('allow', 'Allow'), ('deny', 'Deny')], default='allow', max_length=16)),
|
|
67
|
+
('principal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='capabilities', to='policy.principal')),
|
|
68
|
+
],
|
|
69
|
+
options={
|
|
70
|
+
'verbose_name': 'Capability',
|
|
71
|
+
'verbose_name_plural': 'Capabilities',
|
|
72
|
+
'unique_together': {('principal', 'action', 'resource_pattern')},
|
|
73
|
+
},
|
|
74
|
+
),
|
|
75
|
+
]
|
|
File without changes
|
apps/policy/models.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Principal(models.Model):
|
|
5
|
+
principal_id = models.CharField(max_length=128, unique=True)
|
|
6
|
+
role = models.CharField(max_length=128)
|
|
7
|
+
active = models.BooleanField(default=True)
|
|
8
|
+
|
|
9
|
+
class Meta:
|
|
10
|
+
verbose_name = "Principal"
|
|
11
|
+
verbose_name_plural = "Principals"
|
|
12
|
+
|
|
13
|
+
def __str__(self) -> str:
|
|
14
|
+
return self.principal_id
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Capability(models.Model):
|
|
18
|
+
principal = models.ForeignKey(Principal, on_delete=models.CASCADE, related_name="capabilities")
|
|
19
|
+
action = models.CharField(max_length=160)
|
|
20
|
+
resource_pattern = models.CharField(max_length=255, default="*")
|
|
21
|
+
effect = models.CharField(max_length=16, choices=[("allow", "Allow"), ("deny", "Deny")], default="allow")
|
|
22
|
+
|
|
23
|
+
class Meta:
|
|
24
|
+
unique_together = [("principal", "action", "resource_pattern")]
|
|
25
|
+
verbose_name = "Capability"
|
|
26
|
+
verbose_name_plural = "Capabilities"
|
|
27
|
+
|
|
28
|
+
def __str__(self) -> str:
|
|
29
|
+
return f"{self.principal_id if hasattr(self, 'principal_id') else self.principal} {self.effect} {self.action}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RestrictedSymbol(models.Model):
|
|
33
|
+
symbol = models.CharField(max_length=64, unique=True)
|
|
34
|
+
reason = models.TextField(blank=True)
|
|
35
|
+
active = models.BooleanField(default=True)
|
|
36
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
37
|
+
|
|
38
|
+
class Meta:
|
|
39
|
+
verbose_name = "Restricted symbol"
|
|
40
|
+
verbose_name_plural = "Restricted symbols"
|
|
41
|
+
|
|
42
|
+
def __str__(self) -> str:
|
|
43
|
+
return self.symbol
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class PolicyDecision(models.Model):
|
|
47
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
48
|
+
principal_id = models.CharField(max_length=128)
|
|
49
|
+
action = models.CharField(max_length=160)
|
|
50
|
+
resource = models.CharField(max_length=255, blank=True)
|
|
51
|
+
decision = models.CharField(max_length=16)
|
|
52
|
+
reasons = models.JSONField(default=list, blank=True)
|
|
53
|
+
workspace_context = models.JSONField(default=dict, blank=True)
|
|
54
|
+
|
|
55
|
+
class Meta:
|
|
56
|
+
ordering = ["-created_at", "-id"]
|
|
57
|
+
verbose_name = "Policy decision"
|
|
58
|
+
verbose_name_plural = "Policy decisions"
|
|
59
|
+
|
|
60
|
+
def __str__(self) -> str:
|
|
61
|
+
return f"{self.decision}: {self.principal_id} {self.action}"
|
apps/policy/services.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from fnmatch import fnmatch
|
|
4
|
+
from typing import Any, Iterable
|
|
5
|
+
|
|
6
|
+
from django.db.models import QuerySet
|
|
7
|
+
|
|
8
|
+
from apps.policy.models import Capability, Principal, RestrictedSymbol
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
BUILTIN_ROLE_IDS = {
|
|
12
|
+
"head-manager",
|
|
13
|
+
"fundamental-analyst",
|
|
14
|
+
"technical-analyst",
|
|
15
|
+
"news-analyst",
|
|
16
|
+
"macro-analyst",
|
|
17
|
+
"instrument-analyst",
|
|
18
|
+
"valuation-analyst",
|
|
19
|
+
"portfolio-manager",
|
|
20
|
+
"risk-manager",
|
|
21
|
+
"execution-operator",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def tool_capability_action(tool_name: str, capability_required: str = "") -> str:
|
|
26
|
+
return capability_required or f"mcp.tradingcodex.{tool_name}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def sync_builtin_principals_and_capabilities(tool_specs: Iterable[Any] | None = None) -> None:
|
|
30
|
+
if tool_specs is None:
|
|
31
|
+
from tradingcodex_service.mcp_runtime import TOOL_SPECS
|
|
32
|
+
|
|
33
|
+
tool_specs = TOOL_SPECS
|
|
34
|
+
|
|
35
|
+
for role in sorted(BUILTIN_ROLE_IDS):
|
|
36
|
+
Principal.objects.get_or_create(principal_id=role, defaults={"role": role, "active": True})
|
|
37
|
+
|
|
38
|
+
for tool in tool_specs:
|
|
39
|
+
action = tool_capability_action(tool.name, tool.capability_required)
|
|
40
|
+
for role in tool.allowed_roles:
|
|
41
|
+
principal, _ = Principal.objects.get_or_create(principal_id=role, defaults={"role": role, "active": True})
|
|
42
|
+
if principal.role != role:
|
|
43
|
+
principal.role = role
|
|
44
|
+
principal.save(update_fields=["role"])
|
|
45
|
+
Capability.objects.get_or_create(
|
|
46
|
+
principal=principal,
|
|
47
|
+
action=action,
|
|
48
|
+
resource_pattern="*",
|
|
49
|
+
defaults={"effect": "allow"},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def role_for_principal_id(principal_id: str) -> str:
|
|
54
|
+
principal = Principal.objects.filter(principal_id=principal_id).first()
|
|
55
|
+
if principal is not None:
|
|
56
|
+
return principal.role if principal.active else ""
|
|
57
|
+
if principal_id in BUILTIN_ROLE_IDS:
|
|
58
|
+
return principal_id
|
|
59
|
+
return principal_id or "unknown"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def capability_check(principal_id: str, action: str, resource: str | None = None) -> tuple[bool, list[str]]:
|
|
63
|
+
principal = Principal.objects.filter(principal_id=principal_id).first()
|
|
64
|
+
if principal is None:
|
|
65
|
+
return False, [f"principal is unknown: {principal_id}"]
|
|
66
|
+
if not principal.active:
|
|
67
|
+
return False, [f"principal is inactive: {principal_id}"]
|
|
68
|
+
|
|
69
|
+
resource_value = str(resource or "*")
|
|
70
|
+
candidates = [
|
|
71
|
+
capability
|
|
72
|
+
for capability in Capability.objects.filter(principal=principal, action=action)
|
|
73
|
+
if resource_matches(capability.resource_pattern, resource_value)
|
|
74
|
+
]
|
|
75
|
+
if any(capability.effect == "deny" for capability in candidates):
|
|
76
|
+
return False, [f"capability denied: {principal_id} {action} {resource_value}"]
|
|
77
|
+
if any(capability.effect == "allow" for capability in candidates):
|
|
78
|
+
return True, []
|
|
79
|
+
return False, [f"principal lacks capability: {principal_id} {action} {resource_value}"]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def resource_matches(pattern: str, resource: str) -> bool:
|
|
83
|
+
normalized = pattern or "*"
|
|
84
|
+
return normalized == "*" or fnmatch(resource, normalized)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def set_principal_active(queryset: QuerySet[Principal], active: bool, actor: str = "admin") -> int:
|
|
88
|
+
count = queryset.update(active=active)
|
|
89
|
+
_audit("principal.activated" if active else "principal.deactivated", {"count": count}, actor)
|
|
90
|
+
return count
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def set_capability_effect(queryset: QuerySet[Capability], effect: str, actor: str = "admin") -> int:
|
|
94
|
+
if effect not in {"allow", "deny"}:
|
|
95
|
+
raise ValueError("capability effect must be allow or deny")
|
|
96
|
+
count = queryset.update(effect=effect)
|
|
97
|
+
_audit("capability.allowed" if effect == "allow" else "capability.denied", {"count": count}, actor)
|
|
98
|
+
return count
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def set_restricted_symbols_active(queryset: QuerySet[RestrictedSymbol], active: bool, actor: str = "admin") -> int:
|
|
102
|
+
count = queryset.update(active=active)
|
|
103
|
+
_audit("restricted_symbol.activated" if active else "restricted_symbol.deactivated", {"count": count}, actor)
|
|
104
|
+
return count
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _audit(action: str, payload: dict[str, Any], actor: str) -> None:
|
|
108
|
+
from tradingcodex_service.application.audit import write_audit_event_if_available
|
|
109
|
+
|
|
110
|
+
write_audit_event_if_available(None, actor, "admin", {"type": action, "payload": payload})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
apps/portfolio/admin.py
ADDED
apps/portfolio/apps.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Generated by Django 5.2.5 on 2026-06-12 16:32
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
initial = True
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.CreateModel(
|
|
16
|
+
name='PortfolioSnapshot',
|
|
17
|
+
fields=[
|
|
18
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
19
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
20
|
+
('source', models.CharField(default='paper-trading', max_length=64)),
|
|
21
|
+
('portfolio_id', models.CharField(default='default-paper', max_length=120)),
|
|
22
|
+
('account_id', models.CharField(default='local-paper', max_length=120)),
|
|
23
|
+
('strategy_id', models.CharField(default='default-strategy', max_length=120)),
|
|
24
|
+
('workspace_context', models.JSONField(blank=True, default=dict)),
|
|
25
|
+
('payload', models.JSONField(blank=True, default=dict)),
|
|
26
|
+
],
|
|
27
|
+
options={
|
|
28
|
+
'verbose_name': 'Portfolio snapshot',
|
|
29
|
+
'verbose_name_plural': 'Portfolio snapshots',
|
|
30
|
+
'ordering': ['-created_at', '-id'],
|
|
31
|
+
},
|
|
32
|
+
),
|
|
33
|
+
migrations.CreateModel(
|
|
34
|
+
name='CashBalance',
|
|
35
|
+
fields=[
|
|
36
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
37
|
+
('currency', models.CharField(default='KRW', max_length=16)),
|
|
38
|
+
('amount', models.DecimalField(decimal_places=2, max_digits=24)),
|
|
39
|
+
('portfolio_id', models.CharField(default='default-paper', max_length=120)),
|
|
40
|
+
('account_id', models.CharField(default='local-paper', max_length=120)),
|
|
41
|
+
('strategy_id', models.CharField(default='default-strategy', max_length=120)),
|
|
42
|
+
('snapshot', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cash_balances', to='portfolio.portfoliosnapshot')),
|
|
43
|
+
],
|
|
44
|
+
options={
|
|
45
|
+
'verbose_name': 'Cash balance',
|
|
46
|
+
'verbose_name_plural': 'Cash balances',
|
|
47
|
+
},
|
|
48
|
+
),
|
|
49
|
+
migrations.CreateModel(
|
|
50
|
+
name='Position',
|
|
51
|
+
fields=[
|
|
52
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
53
|
+
('symbol', models.CharField(max_length=64)),
|
|
54
|
+
('quantity', models.DecimalField(decimal_places=6, max_digits=20)),
|
|
55
|
+
('average_price', models.DecimalField(decimal_places=6, max_digits=20)),
|
|
56
|
+
('currency', models.CharField(default='KRW', max_length=16)),
|
|
57
|
+
('portfolio_id', models.CharField(default='default-paper', max_length=120)),
|
|
58
|
+
('account_id', models.CharField(default='local-paper', max_length=120)),
|
|
59
|
+
('strategy_id', models.CharField(default='default-strategy', max_length=120)),
|
|
60
|
+
('snapshot', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='positions', to='portfolio.portfoliosnapshot')),
|
|
61
|
+
],
|
|
62
|
+
options={
|
|
63
|
+
'verbose_name': 'Position',
|
|
64
|
+
'verbose_name_plural': 'Positions',
|
|
65
|
+
},
|
|
66
|
+
),
|
|
67
|
+
]
|
|
File without changes
|
apps/portfolio/models.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class PortfolioSnapshot(models.Model):
|
|
5
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
6
|
+
source = models.CharField(max_length=64, default="paper-trading")
|
|
7
|
+
portfolio_id = models.CharField(max_length=120, default="default-paper")
|
|
8
|
+
account_id = models.CharField(max_length=120, default="local-paper")
|
|
9
|
+
strategy_id = models.CharField(max_length=120, default="default-strategy")
|
|
10
|
+
workspace_context = models.JSONField(default=dict, blank=True)
|
|
11
|
+
payload = models.JSONField(default=dict, blank=True)
|
|
12
|
+
|
|
13
|
+
class Meta:
|
|
14
|
+
ordering = ["-created_at", "-id"]
|
|
15
|
+
verbose_name = "Portfolio snapshot"
|
|
16
|
+
verbose_name_plural = "Portfolio snapshots"
|
|
17
|
+
|
|
18
|
+
def __str__(self) -> str:
|
|
19
|
+
return f"{self.source} {self.created_at:%Y-%m-%d %H:%M:%S}"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Position(models.Model):
|
|
23
|
+
snapshot = models.ForeignKey(PortfolioSnapshot, on_delete=models.CASCADE, related_name="positions")
|
|
24
|
+
symbol = models.CharField(max_length=64)
|
|
25
|
+
quantity = models.DecimalField(max_digits=20, decimal_places=6)
|
|
26
|
+
average_price = models.DecimalField(max_digits=20, decimal_places=6)
|
|
27
|
+
currency = models.CharField(max_length=16, default="KRW")
|
|
28
|
+
portfolio_id = models.CharField(max_length=120, default="default-paper")
|
|
29
|
+
account_id = models.CharField(max_length=120, default="local-paper")
|
|
30
|
+
strategy_id = models.CharField(max_length=120, default="default-strategy")
|
|
31
|
+
|
|
32
|
+
class Meta:
|
|
33
|
+
verbose_name = "Position"
|
|
34
|
+
verbose_name_plural = "Positions"
|
|
35
|
+
|
|
36
|
+
def __str__(self) -> str:
|
|
37
|
+
return f"{self.symbol} {self.quantity}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CashBalance(models.Model):
|
|
41
|
+
snapshot = models.ForeignKey(PortfolioSnapshot, on_delete=models.CASCADE, related_name="cash_balances")
|
|
42
|
+
currency = models.CharField(max_length=16, default="KRW")
|
|
43
|
+
amount = models.DecimalField(max_digits=24, decimal_places=2)
|
|
44
|
+
portfolio_id = models.CharField(max_length=120, default="default-paper")
|
|
45
|
+
account_id = models.CharField(max_length=120, default="local-paper")
|
|
46
|
+
strategy_id = models.CharField(max_length=120, default="default-strategy")
|
|
47
|
+
|
|
48
|
+
class Meta:
|
|
49
|
+
verbose_name = "Cash balance"
|
|
50
|
+
verbose_name_plural = "Cash balances"
|
|
51
|
+
|
|
52
|
+
def __str__(self) -> str:
|
|
53
|
+
return f"{self.currency} {self.amount}"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
apps/research/admin.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Research artifacts are workspace files, not Django Admin models."""
|
apps/research/apps.py
ADDED
|
File without changes
|
apps/research/models.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Research is file-native; this Django app intentionally owns no DB models."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
apps/workflows/admin.py
ADDED
apps/workflows/apps.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Generated by Django 5.2.5 on 2026-06-12 16:32
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
initial = True
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.CreateModel(
|
|
16
|
+
name='WorkflowRun',
|
|
17
|
+
fields=[
|
|
18
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
19
|
+
('run_id', models.CharField(max_length=180, unique=True)),
|
|
20
|
+
('lane', models.CharField(max_length=80)),
|
|
21
|
+
('universe', models.CharField(default='public_equity', max_length=80)),
|
|
22
|
+
('readiness_label', models.CharField(default='factual-baseline', max_length=80)),
|
|
23
|
+
('status', models.CharField(default='open', max_length=32)),
|
|
24
|
+
('original_request', models.TextField(blank=True)),
|
|
25
|
+
('workspace_context', models.JSONField(blank=True, default=dict)),
|
|
26
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
27
|
+
('updated_at', models.DateTimeField(auto_now=True)),
|
|
28
|
+
],
|
|
29
|
+
options={
|
|
30
|
+
'verbose_name': 'Workflow run',
|
|
31
|
+
'verbose_name_plural': 'Workflow runs',
|
|
32
|
+
},
|
|
33
|
+
),
|
|
34
|
+
migrations.CreateModel(
|
|
35
|
+
name='ArtifactRef',
|
|
36
|
+
fields=[
|
|
37
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
38
|
+
('path', models.CharField(max_length=512)),
|
|
39
|
+
('artifact_type', models.CharField(max_length=80)),
|
|
40
|
+
('role', models.CharField(blank=True, max_length=128)),
|
|
41
|
+
('handoff_state', models.CharField(choices=[('accepted', 'accepted'), ('revise', 'revise'), ('blocked', 'blocked'), ('waiting', 'waiting')], default='waiting', max_length=32)),
|
|
42
|
+
('hero', models.BooleanField(default=False)),
|
|
43
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
44
|
+
('workflow', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='artifacts', to='workflows.workflowrun')),
|
|
45
|
+
],
|
|
46
|
+
options={
|
|
47
|
+
'verbose_name': 'Artifact reference',
|
|
48
|
+
'verbose_name_plural': 'Artifact references',
|
|
49
|
+
},
|
|
50
|
+
),
|
|
51
|
+
]
|
|
File without changes
|
apps/workflows/models.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
|
|
3
|
+
HANDOFF_STATE_CHOICES = [
|
|
4
|
+
("accepted", "accepted"),
|
|
5
|
+
("revise", "revise"),
|
|
6
|
+
("blocked", "blocked"),
|
|
7
|
+
("waiting", "waiting"),
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class WorkflowRun(models.Model):
|
|
12
|
+
run_id = models.CharField(max_length=180, unique=True)
|
|
13
|
+
lane = models.CharField(max_length=80)
|
|
14
|
+
universe = models.CharField(max_length=80, default="public_equity")
|
|
15
|
+
readiness_label = models.CharField(max_length=80, default="factual-baseline")
|
|
16
|
+
status = models.CharField(max_length=32, default="open")
|
|
17
|
+
original_request = models.TextField(blank=True)
|
|
18
|
+
workspace_context = models.JSONField(default=dict, blank=True)
|
|
19
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
20
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
21
|
+
|
|
22
|
+
class Meta:
|
|
23
|
+
verbose_name = "Workflow run"
|
|
24
|
+
verbose_name_plural = "Workflow runs"
|
|
25
|
+
|
|
26
|
+
def __str__(self) -> str:
|
|
27
|
+
return self.run_id
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ArtifactRef(models.Model):
|
|
31
|
+
workflow = models.ForeignKey(WorkflowRun, on_delete=models.CASCADE, related_name="artifacts")
|
|
32
|
+
path = models.CharField(max_length=512)
|
|
33
|
+
artifact_type = models.CharField(max_length=80)
|
|
34
|
+
role = models.CharField(max_length=128, blank=True)
|
|
35
|
+
handoff_state = models.CharField(max_length=32, choices=HANDOFF_STATE_CHOICES, default="waiting")
|
|
36
|
+
hero = models.BooleanField(default=False)
|
|
37
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
38
|
+
|
|
39
|
+
class Meta:
|
|
40
|
+
verbose_name = "Artifact reference"
|
|
41
|
+
verbose_name_plural = "Artifact references"
|
|
42
|
+
|
|
43
|
+
def __str__(self) -> str:
|
|
44
|
+
return self.path
|