Increase security and reformat
This commit is contained in:
@@ -22,16 +22,23 @@ from core.models import (
|
||||
TaskEpic,
|
||||
TaskProviderConfig,
|
||||
)
|
||||
from core.tasks.chat_defaults import ensure_default_source_for_chat, resolve_message_scope
|
||||
from core.tasks.codex_approval import queue_codex_event_with_pre_approval
|
||||
from core.tasks.providers import get_provider
|
||||
from core.tasks.codex_support import resolve_external_chat_id
|
||||
from core.security.command_policy import CommandSecurityContext, evaluate_command_policy
|
||||
from core.tasks.chat_defaults import (
|
||||
ensure_default_source_for_chat,
|
||||
resolve_message_scope,
|
||||
)
|
||||
from core.tasks.codex_approval import queue_codex_event_with_pre_approval
|
||||
from core.tasks.codex_support import resolve_external_chat_id
|
||||
from core.tasks.providers import get_provider
|
||||
|
||||
_TASK_HINT_RE = re.compile(r"\b(todo|task|action|need to|please)\b", re.IGNORECASE)
|
||||
_COMPLETION_RE = re.compile(r"\b(done|completed|fixed)\s*#([A-Za-z0-9_-]+)\b", re.IGNORECASE)
|
||||
_COMPLETION_RE = re.compile(
|
||||
r"\b(done|completed|fixed)\s*#([A-Za-z0-9_-]+)\b", re.IGNORECASE
|
||||
)
|
||||
_BALANCED_HINT_RE = re.compile(r"\b(todo|task|action item|action)\b", re.IGNORECASE)
|
||||
_BROAD_HINT_RE = re.compile(r"\b(todo|task|action|need to|please|reminder)\b", re.IGNORECASE)
|
||||
_BROAD_HINT_RE = re.compile(
|
||||
r"\b(todo|task|action|need to|please|reminder)\b", re.IGNORECASE
|
||||
)
|
||||
_PREFIX_HEAD_TRIM = " \t\r\n`'\"([{<*#-–—_>.,:;!/?\\|"
|
||||
_LIST_TASKS_RE = re.compile(
|
||||
r"^\s*(?:\.l(?:\s+list(?:\s+tasks?)?)?|\.list(?:\s+tasks?)?)\s*$",
|
||||
@@ -151,15 +158,23 @@ async def _resolve_source_mappings(message: Message) -> list[ChatTaskSource]:
|
||||
lookup_service = str(message.source_service or "").strip().lower()
|
||||
variants = _channel_variants(lookup_service, message.source_chat_id or "")
|
||||
session_identifier = getattr(getattr(message, "session", None), "identifier", None)
|
||||
canonical_service = str(getattr(session_identifier, "service", "") or "").strip().lower()
|
||||
canonical_identifier = str(getattr(session_identifier, "identifier", "") or "").strip()
|
||||
canonical_service = (
|
||||
str(getattr(session_identifier, "service", "") or "").strip().lower()
|
||||
)
|
||||
canonical_identifier = str(
|
||||
getattr(session_identifier, "identifier", "") or ""
|
||||
).strip()
|
||||
if lookup_service == "web" and canonical_service and canonical_service != "web":
|
||||
lookup_service = canonical_service
|
||||
variants = _channel_variants(lookup_service, message.source_chat_id or "")
|
||||
for expanded in _channel_variants(lookup_service, canonical_identifier):
|
||||
if expanded and expanded not in variants:
|
||||
variants.append(expanded)
|
||||
elif canonical_service and canonical_identifier and canonical_service == lookup_service:
|
||||
elif (
|
||||
canonical_service
|
||||
and canonical_identifier
|
||||
and canonical_service == lookup_service
|
||||
):
|
||||
for expanded in _channel_variants(canonical_service, canonical_identifier):
|
||||
if expanded and expanded not in variants:
|
||||
variants.append(expanded)
|
||||
@@ -170,10 +185,14 @@ async def _resolve_source_mappings(message: Message) -> list[ChatTaskSource]:
|
||||
if not signal_value:
|
||||
continue
|
||||
companions += await sync_to_async(list)(
|
||||
Chat.objects.filter(source_uuid=signal_value).values_list("source_number", flat=True)
|
||||
Chat.objects.filter(source_uuid=signal_value).values_list(
|
||||
"source_number", flat=True
|
||||
)
|
||||
)
|
||||
companions += await sync_to_async(list)(
|
||||
Chat.objects.filter(source_number=signal_value).values_list("source_uuid", flat=True)
|
||||
Chat.objects.filter(source_number=signal_value).values_list(
|
||||
"source_uuid", flat=True
|
||||
)
|
||||
)
|
||||
for candidate in companions:
|
||||
for expanded in _channel_variants("signal", str(candidate or "").strip()):
|
||||
@@ -271,7 +290,8 @@ def _normalize_flags(raw: dict | None) -> dict:
|
||||
row = dict(raw or {})
|
||||
return {
|
||||
"derive_enabled": _to_bool(row.get("derive_enabled"), True),
|
||||
"match_mode": str(row.get("match_mode") or "balanced").strip().lower() or "balanced",
|
||||
"match_mode": str(row.get("match_mode") or "balanced").strip().lower()
|
||||
or "balanced",
|
||||
"require_prefix": _to_bool(row.get("require_prefix"), False),
|
||||
"allowed_prefixes": _parse_prefixes(row.get("allowed_prefixes")),
|
||||
"completion_enabled": _to_bool(row.get("completion_enabled"), True),
|
||||
@@ -287,7 +307,9 @@ def _normalize_partial_flags(raw: dict | None) -> dict:
|
||||
if "derive_enabled" in row:
|
||||
out["derive_enabled"] = _to_bool(row.get("derive_enabled"), True)
|
||||
if "match_mode" in row:
|
||||
out["match_mode"] = str(row.get("match_mode") or "balanced").strip().lower() or "balanced"
|
||||
out["match_mode"] = (
|
||||
str(row.get("match_mode") or "balanced").strip().lower() or "balanced"
|
||||
)
|
||||
if "require_prefix" in row:
|
||||
out["require_prefix"] = _to_bool(row.get("require_prefix"), False)
|
||||
if "allowed_prefixes" in row:
|
||||
@@ -304,7 +326,9 @@ def _normalize_partial_flags(raw: dict | None) -> dict:
|
||||
|
||||
|
||||
def _effective_flags(source: ChatTaskSource) -> dict:
|
||||
project_flags = _normalize_flags(getattr(getattr(source, "project", None), "settings", {}) or {})
|
||||
project_flags = _normalize_flags(
|
||||
getattr(getattr(source, "project", None), "settings", {}) or {}
|
||||
)
|
||||
source_flags = _normalize_partial_flags(getattr(source, "settings", {}) or {})
|
||||
merged = dict(project_flags)
|
||||
merged.update(source_flags)
|
||||
@@ -360,7 +384,10 @@ async def _derive_title(message: Message) -> str:
|
||||
{"role": "user", "content": text[:2000]},
|
||||
]
|
||||
try:
|
||||
title = str(await ai_runner.run_prompt(prompt, ai_obj, operation="task_derive_title") or "").strip()
|
||||
title = str(
|
||||
await ai_runner.run_prompt(prompt, ai_obj, operation="task_derive_title")
|
||||
or ""
|
||||
).strip()
|
||||
except Exception:
|
||||
title = ""
|
||||
return (title or text)[:255]
|
||||
@@ -376,9 +403,13 @@ async def _derive_title_with_flags(message: Message, flags: dict) -> str:
|
||||
return (cleaned or title or "Untitled task")[:255]
|
||||
|
||||
|
||||
async def _emit_sync_event(task: DerivedTask, event: DerivedTaskEvent, action: str) -> None:
|
||||
async def _emit_sync_event(
|
||||
task: DerivedTask, event: DerivedTaskEvent, action: str
|
||||
) -> None:
|
||||
cfg = await sync_to_async(
|
||||
lambda: TaskProviderConfig.objects.filter(user=task.user, enabled=True).order_by("provider").first()
|
||||
lambda: TaskProviderConfig.objects.filter(user=task.user, enabled=True)
|
||||
.order_by("provider")
|
||||
.first()
|
||||
)()
|
||||
provider_name = str(getattr(cfg, "provider", "mock") or "mock")
|
||||
provider_settings = dict(getattr(cfg, "settings", {}) or {})
|
||||
@@ -416,7 +447,11 @@ async def _emit_sync_event(task: DerivedTask, event: DerivedTaskEvent, action: s
|
||||
"source_channel": str(task.source_channel or ""),
|
||||
"external_chat_id": external_chat_id,
|
||||
"origin_message_id": str(getattr(task, "origin_message_id", "") or ""),
|
||||
"trigger_message_id": str(getattr(event, "source_message_id", "") or getattr(task, "origin_message_id", "") or ""),
|
||||
"trigger_message_id": str(
|
||||
getattr(event, "source_message_id", "")
|
||||
or getattr(task, "origin_message_id", "")
|
||||
or ""
|
||||
),
|
||||
"mode": "default",
|
||||
"payload": event.payload,
|
||||
"memory_context": memory_context,
|
||||
@@ -495,7 +530,9 @@ async def _emit_sync_event(task: DerivedTask, event: DerivedTaskEvent, action: s
|
||||
codex_run.status = status
|
||||
codex_run.result_payload = dict(result.payload or {})
|
||||
codex_run.error = str(result.error or "")
|
||||
await sync_to_async(codex_run.save)(update_fields=["status", "result_payload", "error", "updated_at"])
|
||||
await sync_to_async(codex_run.save)(
|
||||
update_fields=["status", "result_payload", "error", "updated_at"]
|
||||
)
|
||||
if result.ok and result.external_key and not task.external_key:
|
||||
task.external_key = str(result.external_key)
|
||||
await sync_to_async(task.save)(update_fields=["external_key"])
|
||||
@@ -503,15 +540,28 @@ async def _emit_sync_event(task: DerivedTask, event: DerivedTaskEvent, action: s
|
||||
|
||||
async def _completion_regex(message: Message) -> re.Pattern:
|
||||
patterns = await sync_to_async(list)(
|
||||
TaskCompletionPattern.objects.filter(user=message.user, enabled=True).order_by("position", "created_at")
|
||||
TaskCompletionPattern.objects.filter(user=message.user, enabled=True).order_by(
|
||||
"position", "created_at"
|
||||
)
|
||||
)
|
||||
phrases = [str(row.phrase or "").strip() for row in patterns if str(row.phrase or "").strip()]
|
||||
phrases = [
|
||||
str(row.phrase or "").strip()
|
||||
for row in patterns
|
||||
if str(row.phrase or "").strip()
|
||||
]
|
||||
if not phrases:
|
||||
phrases = ["done", "completed", "fixed"]
|
||||
return re.compile(r"\\b(?:" + "|".join(re.escape(p) for p in phrases) + r")\\s*#([A-Za-z0-9_-]+)\\b", re.IGNORECASE)
|
||||
return re.compile(
|
||||
r"\\b(?:"
|
||||
+ "|".join(re.escape(p) for p in phrases)
|
||||
+ r")\\s*#([A-Za-z0-9_-]+)\\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
async def _send_scope_message(source: ChatTaskSource, message: Message, text: str) -> None:
|
||||
async def _send_scope_message(
|
||||
source: ChatTaskSource, message: Message, text: str
|
||||
) -> None:
|
||||
await send_message_raw(
|
||||
source.service or message.source_service or "web",
|
||||
source.channel_identifier or message.source_chat_id or "",
|
||||
@@ -521,7 +571,9 @@ async def _send_scope_message(source: ChatTaskSource, message: Message, text: st
|
||||
)
|
||||
|
||||
|
||||
async def _handle_scope_task_commands(message: Message, sources: list[ChatTaskSource], text: str) -> bool:
|
||||
async def _handle_scope_task_commands(
|
||||
message: Message, sources: list[ChatTaskSource], text: str
|
||||
) -> bool:
|
||||
if not sources:
|
||||
return False
|
||||
body = str(text or "").strip()
|
||||
@@ -538,7 +590,9 @@ async def _handle_scope_task_commands(message: Message, sources: list[ChatTaskSo
|
||||
.order_by("-created_at")[:20]
|
||||
)
|
||||
if not open_rows:
|
||||
await _send_scope_message(source, message, "[task] no open tasks in this chat.")
|
||||
await _send_scope_message(
|
||||
source, message, "[task] no open tasks in this chat."
|
||||
)
|
||||
return True
|
||||
lines = ["[task] open tasks:"]
|
||||
for row in open_rows:
|
||||
@@ -573,7 +627,9 @@ async def _handle_scope_task_commands(message: Message, sources: list[ChatTaskSo
|
||||
.first()
|
||||
)()
|
||||
if task is None:
|
||||
await _send_scope_message(source, message, "[task] nothing to undo in this chat.")
|
||||
await _send_scope_message(
|
||||
source, message, "[task] nothing to undo in this chat."
|
||||
)
|
||||
return True
|
||||
ref = str(task.reference_code or "")
|
||||
title = str(task.title or "")
|
||||
@@ -596,10 +652,16 @@ async def _handle_scope_task_commands(message: Message, sources: list[ChatTaskSo
|
||||
.first()
|
||||
)()
|
||||
if task is None:
|
||||
await _send_scope_message(source, message, f"[task] #{reference} not found.")
|
||||
await _send_scope_message(
|
||||
source, message, f"[task] #{reference} not found."
|
||||
)
|
||||
return True
|
||||
due_str = f"\ndue: {task.due_date}" if task.due_date else ""
|
||||
assignee_str = f"\nassignee: {task.assignee_identifier}" if task.assignee_identifier else ""
|
||||
assignee_str = (
|
||||
f"\nassignee: {task.assignee_identifier}"
|
||||
if task.assignee_identifier
|
||||
else ""
|
||||
)
|
||||
detail = (
|
||||
f"[task] #{task.reference_code}: {task.title}"
|
||||
f"\nstatus: {task.status_snapshot}"
|
||||
@@ -624,7 +686,9 @@ async def _handle_scope_task_commands(message: Message, sources: list[ChatTaskSo
|
||||
.first()
|
||||
)()
|
||||
if task is None:
|
||||
await _send_scope_message(source, message, f"[task] #{reference} not found.")
|
||||
await _send_scope_message(
|
||||
source, message, f"[task] #{reference} not found."
|
||||
)
|
||||
return True
|
||||
task.status_snapshot = "completed"
|
||||
await sync_to_async(task.save)(update_fields=["status_snapshot"])
|
||||
@@ -633,10 +697,16 @@ async def _handle_scope_task_commands(message: Message, sources: list[ChatTaskSo
|
||||
event_type="completion_marked",
|
||||
actor_identifier=str(message.sender_uuid or ""),
|
||||
source_message=message,
|
||||
payload={"marker": reference, "command": ".task complete", "via": "chat_command"},
|
||||
payload={
|
||||
"marker": reference,
|
||||
"command": ".task complete",
|
||||
"via": "chat_command",
|
||||
},
|
||||
)
|
||||
await _emit_sync_event(task, event, "complete")
|
||||
await _send_scope_message(source, message, f"[task] completed #{task.reference_code}: {task.title}")
|
||||
await _send_scope_message(
|
||||
source, message, f"[task] completed #{task.reference_code}: {task.title}"
|
||||
)
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -656,7 +726,9 @@ def _strip_epic_token(text: str) -> str:
|
||||
return re.sub(r"\s{2,}", " ", cleaned).strip()
|
||||
|
||||
|
||||
async def _handle_epic_create_command(message: Message, sources: list[ChatTaskSource], text: str) -> bool:
|
||||
async def _handle_epic_create_command(
|
||||
message: Message, sources: list[ChatTaskSource], text: str
|
||||
) -> bool:
|
||||
match = _EPIC_CREATE_RE.match(str(text or ""))
|
||||
if not match or not sources:
|
||||
return False
|
||||
@@ -766,13 +838,21 @@ async def process_inbound_task_intelligence(message: Message) -> None:
|
||||
if not submit_decision.allowed:
|
||||
return
|
||||
|
||||
completion_allowed = any(bool(_effective_flags(source).get("completion_enabled")) for source in sources)
|
||||
completion_allowed = any(
|
||||
bool(_effective_flags(source).get("completion_enabled")) for source in sources
|
||||
)
|
||||
completion_rx = await _completion_regex(message) if completion_allowed else None
|
||||
marker_match = (completion_rx.search(text) if completion_rx else None) or (_COMPLETION_RE.search(text) if completion_allowed else None)
|
||||
marker_match = (completion_rx.search(text) if completion_rx else None) or (
|
||||
_COMPLETION_RE.search(text) if completion_allowed else None
|
||||
)
|
||||
if marker_match:
|
||||
ref_code = str(marker_match.group(marker_match.lastindex or 1) or "").strip()
|
||||
task = await sync_to_async(
|
||||
lambda: DerivedTask.objects.filter(user=message.user, reference_code=ref_code).order_by("-created_at").first()
|
||||
lambda: DerivedTask.objects.filter(
|
||||
user=message.user, reference_code=ref_code
|
||||
)
|
||||
.order_by("-created_at")
|
||||
.first()
|
||||
)()
|
||||
if not task:
|
||||
# parser warning event attached to a newly derived placeholder in mapped project
|
||||
@@ -848,7 +928,11 @@ async def process_inbound_task_intelligence(message: Message) -> None:
|
||||
status_snapshot="open",
|
||||
due_date=parsed_due_date,
|
||||
assignee_identifier=parsed_assignee,
|
||||
immutable_payload={"origin_text": text, "task_text": task_text, "flags": flags},
|
||||
immutable_payload={
|
||||
"origin_text": text,
|
||||
"task_text": task_text,
|
||||
"flags": flags,
|
||||
},
|
||||
)
|
||||
event = await sync_to_async(DerivedTaskEvent.objects.create)(
|
||||
task=task,
|
||||
|
||||
Reference in New Issue
Block a user