Improve security
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
import time
|
||||
|
||||
from django.http import JsonResponse
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpResponseRedirect, JsonResponse
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
from django.views import View
|
||||
|
||||
from core.clients import transport
|
||||
from core.models import (
|
||||
AdapterHealthEvent,
|
||||
AIRequest,
|
||||
@@ -28,6 +30,9 @@ from core.models import (
|
||||
Persona,
|
||||
PersonIdentifier,
|
||||
QueuedMessage,
|
||||
CommandSecurityPolicy,
|
||||
UserXmppOmemoState,
|
||||
UserXmppSecuritySettings,
|
||||
WorkspaceConversation,
|
||||
WorkspaceMetricSnapshot,
|
||||
)
|
||||
@@ -459,3 +464,357 @@ class MemorySearchQueryAPI(SuperUserRequiredMixin, View):
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _parse_xmpp_jid(jid_str: str) -> dict:
|
||||
"""Split a full JID (localpart@domain/resource) into components."""
|
||||
raw = str(jid_str or "").strip()
|
||||
bare, _, resource = raw.partition("/")
|
||||
localpart, _, domain = bare.partition("@")
|
||||
return {"full": raw, "bare": bare, "localpart": localpart, "domain": domain, "resource": resource}
|
||||
|
||||
|
||||
def _to_bool(value, default=False):
|
||||
if value is None:
|
||||
return bool(default)
|
||||
text = str(value).strip().lower()
|
||||
if text in {"1", "true", "yes", "on", "y"}:
|
||||
return True
|
||||
if text in {"0", "false", "no", "off", "n"}:
|
||||
return False
|
||||
return bool(default)
|
||||
|
||||
|
||||
class SecurityPage(LoginRequiredMixin, View):
|
||||
"""Security settings page for OMEMO and command-scope policy controls."""
|
||||
|
||||
template_name = "pages/security.html"
|
||||
GLOBAL_SCOPE_KEY = "global.override"
|
||||
# Allowed Services list used by both Global Scope Override and local scopes.
|
||||
# Keep this in sync with the UI text on the Security page.
|
||||
POLICY_SERVICES = ["xmpp", "whatsapp", "signal", "instagram", "web"]
|
||||
# Override mode names as shown in the interface:
|
||||
# - per_scope: local scope controls remain editable
|
||||
# - on/off: global override forces each local scope value
|
||||
OVERRIDE_OPTIONS = ("per_scope", "on", "off")
|
||||
GLOBAL_OVERRIDE_FIELDS = (
|
||||
"scope_enabled",
|
||||
"require_omemo",
|
||||
"require_trusted_fingerprint",
|
||||
)
|
||||
POLICY_SCOPES = [
|
||||
("gateway.tasks", "Gateway .tasks commands", "Handles .tasks list/show/complete/undo over gateway channels."),
|
||||
("gateway.approval", "Gateway approval commands", "Handles .approval/.codex/.claude approve/deny over gateway channels."),
|
||||
("gateway.totp", "Gateway TOTP enrollment", "Controls TOTP enrollment/status commands over gateway channels."),
|
||||
("tasks.submit", "Task submissions from chat", "Controls automatic task creation from inbound messages."),
|
||||
("tasks.commands", "Task command verbs (.task/.undo/.epic)", "Controls explicit task command verbs."),
|
||||
("command.bp", "Business plan command", "Controls Business Plan command execution."),
|
||||
("command.codex", "Codex command", "Controls Codex command execution."),
|
||||
("command.claude", "Claude command", "Controls Claude command execution."),
|
||||
]
|
||||
POLICY_GROUP_LABELS = {
|
||||
"gateway": "Gateway",
|
||||
"tasks": "Tasks",
|
||||
"command": "Commands",
|
||||
"agentic": "Agentic",
|
||||
"other": "Other",
|
||||
}
|
||||
|
||||
def _security_settings(self, request):
|
||||
row, _ = UserXmppSecuritySettings.objects.get_or_create(user=request.user)
|
||||
return row
|
||||
|
||||
def _parse_override_value(self, value):
|
||||
option = str(value or "").strip().lower()
|
||||
if option == "inherit":
|
||||
# Backward-compat for existing persisted values.
|
||||
option = "per_scope"
|
||||
if option in self.OVERRIDE_OPTIONS:
|
||||
return option
|
||||
return "per_scope"
|
||||
|
||||
def _global_override_payload(self, request):
|
||||
row, _ = CommandSecurityPolicy.objects.get_or_create(
|
||||
user=request.user,
|
||||
scope_key=self.GLOBAL_SCOPE_KEY,
|
||||
defaults={
|
||||
"enabled": True,
|
||||
"allowed_services": [],
|
||||
"allowed_channels": {},
|
||||
"settings": {},
|
||||
},
|
||||
)
|
||||
settings_payload = dict(row.settings or {})
|
||||
values = {
|
||||
"scope_enabled": self._parse_override_value(
|
||||
settings_payload.get("scope_enabled")
|
||||
),
|
||||
"require_omemo": self._parse_override_value(
|
||||
settings_payload.get("require_omemo")
|
||||
),
|
||||
"require_trusted_fingerprint": self._parse_override_value(
|
||||
settings_payload.get("require_trusted_fingerprint")
|
||||
),
|
||||
}
|
||||
allowed_services = [
|
||||
str(value or "").strip().lower()
|
||||
for value in (row.allowed_services or [])
|
||||
if str(value or "").strip()
|
||||
]
|
||||
channel_rules = self._channel_rules_from_map(dict(row.allowed_channels or {}))
|
||||
if not channel_rules:
|
||||
channel_rules = [{"service": "xmpp", "pattern": ""}]
|
||||
return {
|
||||
"row": row,
|
||||
"values": values,
|
||||
"allowed_services": allowed_services,
|
||||
"channel_rules": channel_rules,
|
||||
}
|
||||
|
||||
def _apply_global_override(self, current_value: bool, option: str) -> bool:
|
||||
normalized = self._parse_override_value(option)
|
||||
if normalized == "on":
|
||||
return True
|
||||
if normalized == "off":
|
||||
return False
|
||||
return bool(current_value)
|
||||
|
||||
def _channel_rules_from_map(self, source_map):
|
||||
rows = []
|
||||
raw = dict(source_map or {})
|
||||
for service_key, patterns in raw.items():
|
||||
service_name = str(service_key or "").strip().lower()
|
||||
if not service_name:
|
||||
continue
|
||||
if isinstance(patterns, list):
|
||||
for pattern in patterns:
|
||||
pattern_text = str(pattern or "").strip()
|
||||
if pattern_text:
|
||||
rows.append({
|
||||
"service": service_name,
|
||||
"pattern": pattern_text,
|
||||
})
|
||||
return rows
|
||||
|
||||
def _channels_map_from_post(self, request):
|
||||
channel_services = request.POST.getlist("allowed_channel_service")
|
||||
channel_patterns = request.POST.getlist("allowed_channel_pattern")
|
||||
allowed_channels: dict[str, list[str]] = {}
|
||||
for idx, raw_pattern in enumerate(channel_patterns):
|
||||
pattern = str(raw_pattern or "").strip()
|
||||
if not pattern:
|
||||
continue
|
||||
service_name = str(
|
||||
channel_services[idx] if idx < len(channel_services) else ""
|
||||
).strip().lower()
|
||||
if not service_name:
|
||||
service_name = "*"
|
||||
allowed_channels.setdefault(service_name, [])
|
||||
if pattern not in allowed_channels[service_name]:
|
||||
allowed_channels[service_name].append(pattern)
|
||||
return allowed_channels
|
||||
|
||||
def _scope_rows(self, request):
|
||||
global_overrides = self._global_override_payload(request)["values"]
|
||||
rows = {
|
||||
str(item.scope_key or "").strip().lower(): item
|
||||
for item in CommandSecurityPolicy.objects.filter(user=request.user).exclude(
|
||||
scope_key=self.GLOBAL_SCOPE_KEY
|
||||
)
|
||||
}
|
||||
payload = []
|
||||
for scope_key, label, description in self.POLICY_SCOPES:
|
||||
key = str(scope_key or "").strip().lower()
|
||||
item = rows.get(key)
|
||||
raw_allowed_services = [
|
||||
str(value or "").strip().lower()
|
||||
for value in (getattr(item, "allowed_services", []) or [])
|
||||
if str(value or "").strip()
|
||||
]
|
||||
channel_rules = self._channel_rules_from_map(
|
||||
dict(getattr(item, "allowed_channels", {}) or {})
|
||||
)
|
||||
if not channel_rules:
|
||||
channel_rules = [{"service": "xmpp", "pattern": ""}]
|
||||
enabled_locked = global_overrides["scope_enabled"] != "per_scope"
|
||||
require_omemo_locked = global_overrides["require_omemo"] != "per_scope"
|
||||
require_trusted_locked = (
|
||||
global_overrides["require_trusted_fingerprint"] != "per_scope"
|
||||
)
|
||||
payload.append({
|
||||
"scope_key": key,
|
||||
"label": label,
|
||||
"description": description,
|
||||
"enabled": self._apply_global_override(
|
||||
bool(getattr(item, "enabled", True)),
|
||||
global_overrides["scope_enabled"],
|
||||
),
|
||||
"require_omemo": self._apply_global_override(
|
||||
bool(getattr(item, "require_omemo", False)),
|
||||
global_overrides["require_omemo"],
|
||||
),
|
||||
"require_trusted_fingerprint": self._apply_global_override(
|
||||
bool(getattr(item, "require_trusted_omemo_fingerprint", False)),
|
||||
global_overrides["require_trusted_fingerprint"],
|
||||
),
|
||||
"enabled_locked": enabled_locked,
|
||||
"require_omemo_locked": require_omemo_locked,
|
||||
"require_trusted_fingerprint_locked": require_trusted_locked,
|
||||
"lock_help": "Set this field to 'Per Scope' in Global Scope Override to edit it here.",
|
||||
"allowed_services": raw_allowed_services,
|
||||
"channel_rules": channel_rules,
|
||||
})
|
||||
return payload
|
||||
|
||||
def _scope_group_key(self, scope_key: str) -> str:
|
||||
key = str(scope_key or "").strip().lower()
|
||||
if key in {"command.codex", "command.claude"}:
|
||||
return "agentic"
|
||||
if key.startswith("gateway."):
|
||||
return "command"
|
||||
if key.startswith("tasks."):
|
||||
if key == "tasks.submit":
|
||||
return "tasks"
|
||||
return "command"
|
||||
if key.startswith("command."):
|
||||
return "command"
|
||||
if ".commands" in key:
|
||||
return "command"
|
||||
if ".approval" in key:
|
||||
return "command"
|
||||
if ".totp" in key:
|
||||
return "command"
|
||||
if ".task" in key:
|
||||
return "tasks"
|
||||
return "other"
|
||||
|
||||
def _grouped_scope_rows(self, request):
|
||||
rows = self._scope_rows(request)
|
||||
grouped: dict[str, list[dict]] = {key: [] for key in self.POLICY_GROUP_LABELS}
|
||||
for row in rows:
|
||||
group_key = self._scope_group_key(row.get("scope_key"))
|
||||
grouped.setdefault(group_key, [])
|
||||
grouped[group_key].append(row)
|
||||
payload = []
|
||||
for group_key in ("tasks", "command", "agentic", "other"):
|
||||
items = grouped.get(group_key) or []
|
||||
if not items:
|
||||
continue
|
||||
payload.append({
|
||||
"key": group_key,
|
||||
"label": self.POLICY_GROUP_LABELS.get(group_key, group_key.title()),
|
||||
"rows": items,
|
||||
})
|
||||
return payload
|
||||
|
||||
def post(self, request):
|
||||
row = self._security_settings(request)
|
||||
if "require_omemo" in request.POST:
|
||||
row.require_omemo = _to_bool(request.POST.get("require_omemo"), False)
|
||||
row.save(update_fields=["require_omemo", "updated_at"])
|
||||
redirect_to = HttpResponseRedirect(reverse("security_settings"))
|
||||
scope_key = str(request.POST.get("scope_key") or "").strip().lower()
|
||||
if scope_key == self.GLOBAL_SCOPE_KEY:
|
||||
global_row = self._global_override_payload(request)["row"]
|
||||
settings_payload = dict(global_row.settings or {})
|
||||
for field in self.GLOBAL_OVERRIDE_FIELDS:
|
||||
settings_payload[field] = self._parse_override_value(
|
||||
request.POST.get(f"global_{field}")
|
||||
)
|
||||
global_row.allowed_services = [
|
||||
str(item or "").strip().lower()
|
||||
for item in request.POST.getlist("allowed_services")
|
||||
if str(item or "").strip()
|
||||
]
|
||||
global_row.allowed_channels = self._channels_map_from_post(request)
|
||||
global_row.settings = settings_payload
|
||||
global_row.save(
|
||||
update_fields=[
|
||||
"settings",
|
||||
"allowed_services",
|
||||
"allowed_channels",
|
||||
"updated_at",
|
||||
]
|
||||
)
|
||||
return redirect_to
|
||||
|
||||
if scope_key:
|
||||
if str(request.POST.get("scope_change_mode") or "").strip() != "1":
|
||||
return redirect_to
|
||||
global_overrides = self._global_override_payload(request)["values"]
|
||||
allowed_services = [
|
||||
str(item or "").strip().lower()
|
||||
for item in request.POST.getlist("allowed_services")
|
||||
if str(item or "").strip()
|
||||
]
|
||||
allowed_channels = self._channels_map_from_post(request)
|
||||
policy, _ = CommandSecurityPolicy.objects.get_or_create(
|
||||
user=request.user,
|
||||
scope_key=scope_key,
|
||||
)
|
||||
policy.allowed_services = allowed_services
|
||||
policy.allowed_channels = allowed_channels
|
||||
if global_overrides["scope_enabled"] == "per_scope":
|
||||
policy.enabled = _to_bool(request.POST.get("policy_enabled"), True)
|
||||
if global_overrides["require_omemo"] == "per_scope":
|
||||
policy.require_omemo = _to_bool(
|
||||
request.POST.get("policy_require_omemo"), False
|
||||
)
|
||||
if global_overrides["require_trusted_fingerprint"] == "per_scope":
|
||||
policy.require_trusted_omemo_fingerprint = _to_bool(
|
||||
request.POST.get("policy_require_trusted_fingerprint"),
|
||||
False,
|
||||
)
|
||||
policy.save(
|
||||
update_fields=[
|
||||
"enabled",
|
||||
"require_omemo",
|
||||
"require_trusted_omemo_fingerprint",
|
||||
"allowed_services",
|
||||
"allowed_channels",
|
||||
"updated_at",
|
||||
]
|
||||
)
|
||||
return redirect_to
|
||||
|
||||
def get(self, request):
|
||||
xmpp_state = transport.get_runtime_state("xmpp")
|
||||
try:
|
||||
omemo_row = UserXmppOmemoState.objects.get(user=request.user)
|
||||
except UserXmppOmemoState.DoesNotExist:
|
||||
omemo_row = None
|
||||
security_settings = self._security_settings(request)
|
||||
sender_jid = _parse_xmpp_jid(getattr(omemo_row, "last_sender_jid", "") or "")
|
||||
omemo_plan = [
|
||||
{
|
||||
"label": "Component OMEMO active",
|
||||
"done": bool(xmpp_state.get("omemo_enabled")),
|
||||
"hint": "The gateway's OMEMO plugin must be loaded and initialised.",
|
||||
},
|
||||
{
|
||||
"label": "OMEMO observed from your client",
|
||||
"done": omemo_row is not None and omemo_row.status == "detected",
|
||||
"hint": "Send any message with OMEMO enabled in your XMPP client.",
|
||||
},
|
||||
{
|
||||
"label": "Client key on file",
|
||||
"done": bool(getattr(omemo_row, "latest_client_key", "")),
|
||||
"hint": "A device key (sid/rid) must be recorded from your client.",
|
||||
},
|
||||
{
|
||||
"label": "Encryption required",
|
||||
"done": security_settings.require_omemo,
|
||||
"hint": "Enable 'Require OMEMO encryption' in Security Policy above to enforce this policy.",
|
||||
},
|
||||
]
|
||||
return render(request, self.template_name, {
|
||||
"xmpp_state": xmpp_state,
|
||||
"omemo_row": omemo_row,
|
||||
"security_settings": security_settings,
|
||||
"global_override": self._global_override_payload(request),
|
||||
"policy_services": self.POLICY_SERVICES,
|
||||
"policy_rows": self._scope_rows(request),
|
||||
"policy_groups": self._grouped_scope_rows(request),
|
||||
"sender_jid": sender_jid,
|
||||
"omemo_plan": omemo_plan,
|
||||
})
|
||||
|
||||
@@ -338,6 +338,23 @@ def _codex_settings_with_defaults(raw: dict | None) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _claude_settings_with_defaults(raw: dict | None) -> dict:
|
||||
row = dict(raw or {})
|
||||
timeout_raw = str(row.get("timeout_seconds") or "60").strip()
|
||||
try:
|
||||
timeout_seconds = max(1, int(timeout_raw))
|
||||
except Exception:
|
||||
timeout_seconds = 60
|
||||
return {
|
||||
"command": str(row.get("command") or "claude").strip() or "claude",
|
||||
"workspace_root": str(row.get("workspace_root") or "").strip(),
|
||||
"default_profile": str(row.get("default_profile") or "").strip(),
|
||||
"timeout_seconds": timeout_seconds,
|
||||
"approver_service": str(row.get("approver_service") or "").strip().lower(),
|
||||
"approver_identifier": str(row.get("approver_identifier") or "").strip(),
|
||||
}
|
||||
|
||||
|
||||
def _enqueue_codex_task_submission(
|
||||
*,
|
||||
user,
|
||||
@@ -347,10 +364,12 @@ def _enqueue_codex_task_submission(
|
||||
mode: str = "default",
|
||||
command_text: str = "",
|
||||
source_message=None,
|
||||
provider: str = "codex_cli",
|
||||
) -> CodexRun:
|
||||
provider = str(provider or "codex_cli").strip() or "codex_cli"
|
||||
external_chat_id = resolve_external_chat_id(
|
||||
user=user,
|
||||
provider="codex_cli",
|
||||
provider=provider,
|
||||
service=source_service,
|
||||
channel=source_channel,
|
||||
)
|
||||
@@ -398,6 +417,7 @@ def _enqueue_codex_task_submission(
|
||||
action="append_update",
|
||||
provider_payload=dict(provider_payload),
|
||||
idempotency_key=idempotency_key,
|
||||
provider=provider,
|
||||
)
|
||||
return run
|
||||
|
||||
@@ -703,6 +723,12 @@ class TasksHub(LoginRequiredMixin, View):
|
||||
"mapped": mapped,
|
||||
}
|
||||
)
|
||||
enabled_providers = list(
|
||||
TaskProviderConfig.objects.filter(user=request.user, enabled=True)
|
||||
.exclude(provider="mock")
|
||||
.values_list("provider", flat=True)
|
||||
.order_by("provider")
|
||||
)
|
||||
return {
|
||||
"projects": projects,
|
||||
"project_choices": all_projects,
|
||||
@@ -711,6 +737,7 @@ class TasksHub(LoginRequiredMixin, View):
|
||||
"person_identifier_rows": person_identifier_rows,
|
||||
"selected_project": selected_project,
|
||||
"show_empty_projects": show_empty,
|
||||
"enabled_providers": enabled_providers,
|
||||
}
|
||||
|
||||
def get(self, request):
|
||||
@@ -1152,9 +1179,13 @@ class TaskSettings(LoginRequiredMixin, View):
|
||||
provider_map = _provider_row_map(request.user)
|
||||
codex_cfg = provider_map.get("codex_cli")
|
||||
codex_settings = _codex_settings_with_defaults(dict(getattr(codex_cfg, "settings", {}) or {}))
|
||||
claude_cfg = provider_map.get("claude_cli")
|
||||
claude_settings = _claude_settings_with_defaults(dict(getattr(claude_cfg, "settings", {}) or {}))
|
||||
mock_cfg = provider_map.get("mock")
|
||||
codex_provider = get_provider("codex_cli")
|
||||
claude_provider = get_provider("claude_cli")
|
||||
codex_healthcheck = codex_provider.healthcheck(codex_settings) if codex_cfg else None
|
||||
claude_healthcheck = claude_provider.healthcheck(claude_settings) if claude_cfg else None
|
||||
codex_queue_counts = {
|
||||
"pending": ExternalSyncEvent.objects.filter(
|
||||
user=request.user, provider="codex_cli", status="pending"
|
||||
@@ -1169,11 +1200,25 @@ class TaskSettings(LoginRequiredMixin, View):
|
||||
user=request.user, provider="codex_cli", status="ok"
|
||||
).count(),
|
||||
}
|
||||
claude_queue_counts = {
|
||||
"pending": ExternalSyncEvent.objects.filter(
|
||||
user=request.user, provider="claude_cli", status="pending"
|
||||
).count(),
|
||||
"waiting_approval": ExternalSyncEvent.objects.filter(
|
||||
user=request.user, provider="claude_cli", status="waiting_approval"
|
||||
).count(),
|
||||
"failed": ExternalSyncEvent.objects.filter(
|
||||
user=request.user, provider="claude_cli", status="failed"
|
||||
).count(),
|
||||
"ok": ExternalSyncEvent.objects.filter(
|
||||
user=request.user, provider="claude_cli", status="ok"
|
||||
).count(),
|
||||
}
|
||||
codex_recent_runs = CodexRun.objects.filter(user=request.user).order_by("-created_at")[:10]
|
||||
latest_worker_event = (
|
||||
ExternalSyncEvent.objects.filter(
|
||||
user=request.user,
|
||||
provider="codex_cli",
|
||||
provider__in=["codex_cli", "claude_cli"],
|
||||
)
|
||||
.filter(status__in=["ok", "failed", "waiting_approval", "retrying"])
|
||||
.order_by("-updated_at")
|
||||
@@ -1233,6 +1278,21 @@ class TaskSettings(LoginRequiredMixin, View):
|
||||
"queue_counts": codex_queue_counts,
|
||||
"recent_runs": codex_recent_runs,
|
||||
},
|
||||
"claude_provider_config": claude_cfg,
|
||||
"claude_provider_settings": {
|
||||
"command": str(claude_settings.get("command") or "claude"),
|
||||
"workspace_root": str(claude_settings.get("workspace_root") or ""),
|
||||
"default_profile": str(claude_settings.get("default_profile") or ""),
|
||||
"timeout_seconds": int(claude_settings.get("timeout_seconds") or 60),
|
||||
"approver_service": str(claude_settings.get("approver_service") or ""),
|
||||
"approver_identifier": str(claude_settings.get("approver_identifier") or ""),
|
||||
},
|
||||
"claude_compact_summary": {
|
||||
"healthcheck_ok": bool(getattr(claude_healthcheck, "ok", False)),
|
||||
"healthcheck_error": str(getattr(claude_healthcheck, "error", "") or ""),
|
||||
"healthcheck_payload": dict(getattr(claude_healthcheck, "payload", {}) or {}),
|
||||
"queue_counts": claude_queue_counts,
|
||||
},
|
||||
"person_identifiers": person_identifiers,
|
||||
"external_link_person_identifiers": external_link_person_identifiers,
|
||||
"external_link_scoped": external_link_scoped,
|
||||
@@ -1376,6 +1436,17 @@ class TaskSettings(LoginRequiredMixin, View):
|
||||
"approver_mode": "channel",
|
||||
}
|
||||
)
|
||||
elif provider == "claude_cli":
|
||||
settings_payload = _claude_settings_with_defaults(
|
||||
{
|
||||
"command": request.POST.get("command"),
|
||||
"workspace_root": request.POST.get("workspace_root"),
|
||||
"default_profile": request.POST.get("default_profile"),
|
||||
"timeout_seconds": request.POST.get("timeout_seconds"),
|
||||
"approver_service": request.POST.get("approver_service"),
|
||||
"approver_identifier": request.POST.get("approver_identifier"),
|
||||
}
|
||||
)
|
||||
row.settings = settings_payload
|
||||
row.save(update_fields=["enabled", "settings", "updated_at"])
|
||||
return _settings_redirect(request)
|
||||
@@ -1460,10 +1531,16 @@ class TaskSettings(LoginRequiredMixin, View):
|
||||
return _settings_redirect(request)
|
||||
|
||||
|
||||
_ALLOWED_SUBMIT_PROVIDERS = {"codex_cli", "claude_cli"}
|
||||
|
||||
|
||||
class TaskCodexSubmit(LoginRequiredMixin, View):
|
||||
def post(self, request):
|
||||
task_id = str(request.POST.get("task_id") or "").strip()
|
||||
next_url = str(request.POST.get("next") or reverse("tasks_hub")).strip()
|
||||
provider = str(request.POST.get("provider") or "codex_cli").strip().lower()
|
||||
if provider not in _ALLOWED_SUBMIT_PROVIDERS:
|
||||
provider = "codex_cli"
|
||||
task = get_object_or_404(
|
||||
DerivedTask.objects.select_related("project", "epic", "origin_message"),
|
||||
id=task_id,
|
||||
@@ -1471,13 +1548,14 @@ class TaskCodexSubmit(LoginRequiredMixin, View):
|
||||
)
|
||||
cfg = TaskProviderConfig.objects.filter(
|
||||
user=request.user,
|
||||
provider="codex_cli",
|
||||
provider=provider,
|
||||
enabled=True,
|
||||
).first()
|
||||
provider_label = "Claude" if provider == "claude_cli" else "Codex"
|
||||
if cfg is None:
|
||||
messages.error(
|
||||
request,
|
||||
"Codex provider is disabled. Enable it in Task Settings first.",
|
||||
f"{provider_label} provider is disabled. Enable it in Task Settings first.",
|
||||
)
|
||||
return redirect(next_url)
|
||||
run = _enqueue_codex_task_submission(
|
||||
@@ -1487,10 +1565,11 @@ class TaskCodexSubmit(LoginRequiredMixin, View):
|
||||
source_channel=str(task.source_channel or ""),
|
||||
mode="default",
|
||||
source_message=getattr(task, "origin_message", None),
|
||||
provider=provider,
|
||||
)
|
||||
messages.success(
|
||||
request,
|
||||
f"Queued approval for task #{task.reference_code} before Codex run {run.id}.",
|
||||
f"Queued approval for task #{task.reference_code} before {provider_label} run {run.id}.",
|
||||
)
|
||||
return redirect(next_url)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user