Lightweight containerized prosody tooling + moved auth scripts + xmpp reconnect/auth stabilization

This commit is contained in:
2026-03-05 02:18:12 +00:00
parent 0718a06c19
commit 2140c5facf
69 changed files with 3767 additions and 144 deletions

122
core/tasks/chat_defaults.py Normal file
View File

@@ -0,0 +1,122 @@
from __future__ import annotations
import re
from core.models import ChatTaskSource, TaskProject
from core.tasks.codex_support import channel_variants
SAFE_TASK_FLAGS_DEFAULTS = {
"derive_enabled": True,
"match_mode": "strict",
"require_prefix": True,
"allowed_prefixes": ["task:", "todo:"],
"completion_enabled": True,
"ai_title_enabled": True,
"announce_task_id": False,
"min_chars": 3,
}
def normalize_channel_identifier(service: str, identifier: str) -> str:
service_key = str(service or "").strip().lower()
value = str(identifier or "").strip()
if not value:
return ""
if service_key == "whatsapp":
bare = value.split("@", 1)[0].strip()
if not bare:
return value
if value.endswith("@s.whatsapp.net"):
return f"{bare}@s.whatsapp.net"
return f"{bare}@g.us"
return value
def resolve_message_scope(message) -> tuple[str, str]:
source_service = str(getattr(message, "source_service", "") or "").strip().lower()
source_channel = str(getattr(message, "source_chat_id", "") or "").strip()
if source_service != "web":
return source_service, source_channel
identifier = getattr(getattr(message, "session", None), "identifier", None)
fallback_service = str(getattr(identifier, "service", "") or "").strip().lower()
fallback_identifier = str(getattr(identifier, "identifier", "") or "").strip()
if fallback_service and fallback_identifier and fallback_service != "web":
return fallback_service, fallback_identifier
return source_service, source_channel
def _project_name_candidate(service: str, channel_identifier: str, message=None) -> str:
person_name = ""
if message is not None:
identifier = getattr(getattr(message, "session", None), "identifier", None)
person = getattr(identifier, "person", None)
person_name = str(getattr(person, "name", "") or "").strip()
if person_name:
return person_name[:255]
raw = str(channel_identifier or "").strip()
if str(service or "").strip().lower() == "whatsapp":
raw = raw.split("@", 1)[0].strip()
cleaned = re.sub(r"\s+", " ", raw).strip()
if not cleaned:
cleaned = "Chat"
return f"Chat: {cleaned}"[:255]
def _ensure_unique_project_name(user, base_name: str) -> str:
base = str(base_name or "").strip() or "Chat"
if not TaskProject.objects.filter(user=user, name=base).exists():
return base
idx = 2
while idx < 10000:
candidate = f"{base} ({idx})"[:255]
if not TaskProject.objects.filter(user=user, name=candidate).exists():
return candidate
idx += 1
return f"{base} ({str(user.id)[:8]})"[:255]
def ensure_default_source_for_chat(
*,
user,
service: str,
channel_identifier: str,
message=None,
):
service_key = str(service or "").strip().lower()
normalized_identifier = normalize_channel_identifier(service_key, channel_identifier)
variants = channel_variants(service_key, normalized_identifier)
if not service_key or not variants:
return None
existing = (
ChatTaskSource.objects.filter(
user=user,
service=service_key,
channel_identifier__in=variants,
)
.select_related("project", "epic")
.order_by("-enabled", "-updated_at", "-created_at")
.first()
)
if existing is not None:
if not existing.enabled:
existing.enabled = True
existing.save(update_fields=["enabled", "updated_at"])
return existing
project_name = _ensure_unique_project_name(
user,
_project_name_candidate(service_key, normalized_identifier, message=message),
)
project = TaskProject.objects.create(
user=user,
name=project_name,
settings=dict(SAFE_TASK_FLAGS_DEFAULTS),
)
return ChatTaskSource.objects.create(
user=user,
service=service_key,
channel_identifier=normalized_identifier,
project=project,
epic=None,
enabled=True,
settings=dict(SAFE_TASK_FLAGS_DEFAULTS),
)

View File

@@ -0,0 +1,91 @@
from __future__ import annotations
import hashlib
from asgiref.sync import async_to_sync
from core.clients.transport import send_message_raw
from core.models import CodexPermissionRequest, ExternalSyncEvent, TaskProviderConfig
def _deterministic_approval_key(idempotency_key: str) -> str:
digest = hashlib.sha1(str(idempotency_key or "").encode("utf-8")).hexdigest()[:12]
return f"pre-{digest}"
def queue_codex_event_with_pre_approval(
*,
user,
run,
task,
task_event,
action: str,
provider_payload: dict,
idempotency_key: str,
) -> tuple[ExternalSyncEvent, CodexPermissionRequest]:
approval_key = _deterministic_approval_key(idempotency_key)
waiting_event, _ = ExternalSyncEvent.objects.update_or_create(
idempotency_key=f"codex_waiting:{idempotency_key}",
defaults={
"user": user,
"task": task,
"task_event": task_event,
"provider": "codex_cli",
"status": "waiting_approval",
"payload": {
"action": str(action or "append_update"),
"provider_payload": dict(provider_payload or {}),
},
"error": "",
},
)
run.status = "waiting_approval"
run.error = ""
run.save(update_fields=["status", "error", "updated_at"])
request, _ = CodexPermissionRequest.objects.update_or_create(
approval_key=approval_key,
defaults={
"user": user,
"codex_run": run,
"external_sync_event": waiting_event,
"summary": "Pre-submit approval required before sending to Codex",
"requested_permissions": {
"type": "pre_submit",
"provider": "codex_cli",
"action": str(action or "append_update"),
},
"resume_payload": {
"gate_type": "pre_submit",
"action": str(action or "append_update"),
"provider_payload": dict(provider_payload or {}),
"idempotency_key": str(idempotency_key or ""),
},
"status": "pending",
"resolved_at": None,
"resolved_by_identifier": "",
"resolution_note": "",
},
)
cfg = TaskProviderConfig.objects.filter(user=user, provider="codex_cli", enabled=True).first()
settings_payload = dict(getattr(cfg, "settings", {}) or {})
approver_service = str(settings_payload.get("approver_service") or "").strip().lower()
approver_identifier = str(settings_payload.get("approver_identifier") or "").strip()
if approver_service and approver_identifier:
try:
async_to_sync(send_message_raw)(
approver_service,
approver_identifier,
text=(
f"[codex approval] key={approval_key}\n"
"summary=Pre-submit approval required before sending to Codex\n"
"requested=pre_submit\n"
f"use: .codex approve {approval_key} or .codex deny {approval_key}"
),
attachments=[],
metadata={"origin_tag": f"codex-pre-approval:{approval_key}"},
)
except Exception:
pass
return waiting_event, request

View File

@@ -20,6 +20,8 @@ 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
@@ -355,6 +357,17 @@ async def _emit_sync_event(task: DerivedTask, event: DerivedTaskEvent, action: s
# Worker-backed providers are queued and executed by `manage.py codex_worker`.
if bool(getattr(provider, "run_in_worker", False)):
if provider_name == "codex_cli":
await sync_to_async(queue_codex_event_with_pre_approval)(
user=task.user,
run=codex_run,
task=task,
task_event=event,
action=action,
provider_payload=dict(request_payload),
idempotency_key=idempotency_key,
)
return
await sync_to_async(ExternalSyncEvent.objects.update_or_create)(
idempotency_key=idempotency_key,
defaults={
@@ -526,6 +539,15 @@ async def _handle_epic_create_command(message: Message, sources: list[ChatTaskSo
return True
def _is_task_command_candidate(text: str) -> bool:
body = str(text or "").strip()
if not body:
return False
if _LIST_TASKS_RE.match(body) or _UNDO_TASK_RE.match(body) or _EPIC_CREATE_RE.match(body):
return True
return _has_task_prefix(body.lower(), ["task:", "todo:"])
async def process_inbound_task_intelligence(message: Message) -> None:
if message is None:
return
@@ -537,7 +559,20 @@ async def process_inbound_task_intelligence(message: Message) -> None:
sources = await _resolve_source_mappings(message)
if not sources:
return
if not _is_task_command_candidate(text):
return
service, channel = resolve_message_scope(message)
if not service or not channel:
return
seeded = await sync_to_async(ensure_default_source_for_chat)(
user=message.user,
service=service,
channel_identifier=channel,
message=message,
)
if seeded is None:
return
sources = [seeded]
if await _handle_scope_task_commands(message, sources, text):
return
if await _handle_epic_create_command(message, sources, text):

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
import json
import subprocess
from hashlib import sha1
from .base import ProviderResult, TaskProvider
@@ -25,27 +26,106 @@ class CodexCLITaskProvider(TaskProvider):
def _profile(self, config: dict) -> str:
return str(config.get("default_profile") or "").strip()
def _is_task_sync_contract_mismatch(self, stderr: str) -> bool:
text = str(stderr or "").lower()
if "unexpected argument '--op'" in text:
return True
if "unexpected argument 'create'" in text and "usage: codex" in text:
return True
if "unexpected argument 'append_update'" in text and "usage: codex" in text:
return True
if "unexpected argument 'mark_complete'" in text and "usage: codex" in text:
return True
if "unexpected argument 'link_task'" in text and "usage: codex" in text:
return True
if "unrecognized subcommand 'create'" in text and "usage: codex" in text:
return True
if "unrecognized subcommand 'append_update'" in text and "usage: codex" in text:
return True
if "unrecognized subcommand 'mark_complete'" in text and "usage: codex" in text:
return True
return False
def _builtin_stub_result(self, op: str, payload: dict, stderr: str) -> ProviderResult:
mode = str(payload.get("mode") or "default").strip().lower()
external_key = (
str(payload.get("external_key") or "").strip()
or str(payload.get("task_id") or "").strip()
)
if mode == "approval_response":
return ProviderResult(
ok=True,
external_key=external_key,
payload={
"op": op,
"status": "ok",
"summary": "approval acknowledged; resumed by builtin codex stub",
"requires_approval": False,
"output": "",
"fallback_mode": "builtin_task_sync_stub",
"fallback_reason": str(stderr or "")[:4000],
},
)
task_id = str(payload.get("task_id") or "").strip()
key_basis = f"{op}:{task_id}:{payload.get('trigger_message_id') or payload.get('origin_message_id') or ''}"
approval_key = sha1(key_basis.encode("utf-8")).hexdigest()[:12]
summary = "Codex approval required (builtin stub fallback)"
return ProviderResult(
ok=True,
external_key=external_key,
payload={
"op": op,
"status": "requires_approval",
"requires_approval": True,
"summary": summary,
"approval_key": approval_key,
"permission_request": {
"summary": summary,
"requested_permissions": ["workspace_write"],
},
"resume_payload": {
"task_id": task_id,
"op": op,
},
"fallback_mode": "builtin_task_sync_stub",
"fallback_reason": str(stderr or "")[:4000],
},
)
def _run(self, config: dict, op: str, payload: dict) -> ProviderResult:
cmd = [self._command(config), "task-sync", "--op", str(op)]
base_cmd = [self._command(config), "task-sync"]
workspace = self._workspace(config)
if workspace:
cmd.extend(["--workspace", workspace])
profile = self._profile(config)
if profile:
cmd.extend(["--profile", profile])
command_timeout = self._timeout(config)
data = json.dumps(dict(payload or {}), separators=(",", ":"))
cmd.extend(["--payload-json", data])
common_args: list[str] = []
if workspace:
common_args.extend(["--workspace", workspace])
if profile:
common_args.extend(["--profile", profile])
primary_cmd = [*base_cmd, "--op", str(op), *common_args, "--payload-json", data]
fallback_cmd = [*base_cmd, str(op), *common_args, "--payload-json", data]
try:
completed = subprocess.run(
cmd,
primary_cmd,
capture_output=True,
text=True,
timeout=command_timeout,
check=False,
cwd=workspace if workspace else None,
)
stderr_probe = str(completed.stderr or "").lower()
if completed.returncode != 0 and "unexpected argument '--op'" in stderr_probe:
completed = subprocess.run(
fallback_cmd,
capture_output=True,
text=True,
timeout=command_timeout,
check=False,
cwd=workspace if workspace else None,
)
except subprocess.TimeoutExpired:
return ProviderResult(
ok=False,
@@ -90,6 +170,8 @@ class CodexCLITaskProvider(TaskProvider):
"requires_approval": requires_approval,
}
out_payload.update(parsed)
if (not ok) and self._is_task_sync_contract_mismatch(stderr):
return self._builtin_stub_result(op, dict(payload or {}), stderr)
return ProviderResult(ok=ok, external_key=ext, error=("" if ok else stderr[:4000]), payload=out_payload)
def healthcheck(self, config: dict) -> ProviderResult: