Increase security and reformat
This commit is contained in:
@@ -20,8 +20,8 @@ from core.models import (
|
||||
TaskProject,
|
||||
TaskProviderConfig,
|
||||
)
|
||||
from core.tasks.codex_support import channel_variants, resolve_external_chat_id
|
||||
from core.tasks.codex_approval import queue_codex_event_with_pre_approval
|
||||
from core.tasks.codex_support import channel_variants, resolve_external_chat_id
|
||||
|
||||
_CLAUDE_DEFAULT_RE = re.compile(
|
||||
r"^\s*(?:\.claude\b|#claude#?)(?P<body>.*)$",
|
||||
@@ -31,7 +31,9 @@ _CLAUDE_PLAN_RE = re.compile(
|
||||
r"^\s*(?:\.claude\s+plan\b|#claude\s+plan#?)(?P<body>.*)$",
|
||||
re.IGNORECASE | re.DOTALL,
|
||||
)
|
||||
_CLAUDE_STATUS_RE = re.compile(r"^\s*(?:\.claude\s+status\b|#claude\s+status#?)\s*$", re.IGNORECASE)
|
||||
_CLAUDE_STATUS_RE = re.compile(
|
||||
r"^\s*(?:\.claude\s+status\b|#claude\s+status#?)\s*$", re.IGNORECASE
|
||||
)
|
||||
_CLAUDE_APPROVE_DENY_RE = re.compile(
|
||||
r"^\s*(?:\.claude|#claude)\s+(?P<action>approve|deny)\s+(?P<approval_key>[A-Za-z0-9._:-]+)#?\s*$",
|
||||
re.IGNORECASE,
|
||||
@@ -83,7 +85,9 @@ def parse_claude_command(text: str) -> ClaudeParsedCommand:
|
||||
return ClaudeParsedCommand(command=None, body_text="", approval_key="")
|
||||
|
||||
|
||||
def claude_trigger_matches(message_text: str, trigger_token: str, exact_match_only: bool) -> bool:
|
||||
def claude_trigger_matches(
|
||||
message_text: str, trigger_token: str, exact_match_only: bool
|
||||
) -> bool:
|
||||
body = str(message_text or "").strip()
|
||||
parsed = parse_claude_command(body)
|
||||
if parsed.command:
|
||||
@@ -103,7 +107,9 @@ class ClaudeCommandHandler(CommandHandler):
|
||||
|
||||
async def _load_trigger(self, message_id: str) -> Message | None:
|
||||
return await sync_to_async(
|
||||
lambda: Message.objects.select_related("user", "session", "session__identifier", "reply_to")
|
||||
lambda: Message.objects.select_related(
|
||||
"user", "session", "session__identifier", "reply_to"
|
||||
)
|
||||
.filter(id=message_id)
|
||||
.first()
|
||||
)()
|
||||
@@ -114,11 +120,18 @@ class ClaudeCommandHandler(CommandHandler):
|
||||
identifier = getattr(getattr(trigger, "session", None), "identifier", None)
|
||||
fallback_service = str(getattr(identifier, "service", "") or "").strip().lower()
|
||||
fallback_identifier = str(getattr(identifier, "identifier", "") or "").strip()
|
||||
if service == "web" and fallback_service and fallback_identifier and fallback_service != "web":
|
||||
if (
|
||||
service == "web"
|
||||
and fallback_service
|
||||
and fallback_identifier
|
||||
and fallback_service != "web"
|
||||
):
|
||||
return fallback_service, fallback_identifier
|
||||
return service or "web", channel
|
||||
|
||||
async def _mapped_sources(self, user, service: str, channel: str) -> list[ChatTaskSource]:
|
||||
async def _mapped_sources(
|
||||
self, user, service: str, channel: str
|
||||
) -> list[ChatTaskSource]:
|
||||
variants = channel_variants(service, channel)
|
||||
if not variants:
|
||||
return []
|
||||
@@ -131,7 +144,9 @@ class ClaudeCommandHandler(CommandHandler):
|
||||
).select_related("project", "epic")
|
||||
)
|
||||
|
||||
async def _linked_task_from_reply(self, user, reply_to: Message | None) -> DerivedTask | None:
|
||||
async def _linked_task_from_reply(
|
||||
self, user, reply_to: Message | None
|
||||
) -> DerivedTask | None:
|
||||
if reply_to is None:
|
||||
return None
|
||||
by_origin = await sync_to_async(
|
||||
@@ -143,7 +158,9 @@ class ClaudeCommandHandler(CommandHandler):
|
||||
if by_origin is not None:
|
||||
return by_origin
|
||||
return await sync_to_async(
|
||||
lambda: DerivedTask.objects.filter(user=user, events__source_message=reply_to)
|
||||
lambda: DerivedTask.objects.filter(
|
||||
user=user, events__source_message=reply_to
|
||||
)
|
||||
.select_related("project", "epic")
|
||||
.order_by("-created_at")
|
||||
.first()
|
||||
@@ -164,10 +181,14 @@ class ClaudeCommandHandler(CommandHandler):
|
||||
return ""
|
||||
return str(m.group(1) or "").strip()
|
||||
|
||||
async def _resolve_task(self, user, reference_code: str, reply_task: DerivedTask | None) -> DerivedTask | None:
|
||||
async def _resolve_task(
|
||||
self, user, reference_code: str, reply_task: DerivedTask | None
|
||||
) -> DerivedTask | None:
|
||||
if reference_code:
|
||||
return await sync_to_async(
|
||||
lambda: DerivedTask.objects.filter(user=user, reference_code=reference_code)
|
||||
lambda: DerivedTask.objects.filter(
|
||||
user=user, reference_code=reference_code
|
||||
)
|
||||
.select_related("project", "epic")
|
||||
.order_by("-created_at")
|
||||
.first()
|
||||
@@ -190,7 +211,9 @@ class ClaudeCommandHandler(CommandHandler):
|
||||
return reply_task.project, ""
|
||||
if project_token:
|
||||
project = await sync_to_async(
|
||||
lambda: TaskProject.objects.filter(user=user, name__iexact=project_token).first()
|
||||
lambda: TaskProject.objects.filter(
|
||||
user=user, name__iexact=project_token
|
||||
).first()
|
||||
)()
|
||||
if project is not None:
|
||||
return project, ""
|
||||
@@ -199,20 +222,31 @@ class ClaudeCommandHandler(CommandHandler):
|
||||
mapped = await self._mapped_sources(user, service, channel)
|
||||
project_ids = sorted({str(row.project_id) for row in mapped if row.project_id})
|
||||
if len(project_ids) == 1:
|
||||
project = next((row.project for row in mapped if str(row.project_id) == project_ids[0]), None)
|
||||
project = next(
|
||||
(
|
||||
row.project
|
||||
for row in mapped
|
||||
if str(row.project_id) == project_ids[0]
|
||||
),
|
||||
None,
|
||||
)
|
||||
return project, ""
|
||||
if len(project_ids) > 1:
|
||||
return None, "project_required:[project:Name]"
|
||||
return None, "project_unresolved"
|
||||
|
||||
async def _post_source_status(self, trigger: Message, text: str, suffix: str) -> None:
|
||||
async def _post_source_status(
|
||||
self, trigger: Message, text: str, suffix: str
|
||||
) -> None:
|
||||
await post_status_in_source(
|
||||
trigger_message=trigger,
|
||||
text=text,
|
||||
origin_tag=f"claude-status:{suffix}",
|
||||
)
|
||||
|
||||
async def _run_status(self, trigger: Message, service: str, channel: str, project: TaskProject | None) -> CommandResult:
|
||||
async def _run_status(
|
||||
self, trigger: Message, service: str, channel: str, project: TaskProject | None
|
||||
) -> CommandResult:
|
||||
def _load_runs():
|
||||
qs = CodexRun.objects.filter(user=trigger.user)
|
||||
if service:
|
||||
@@ -225,7 +259,9 @@ class ClaudeCommandHandler(CommandHandler):
|
||||
|
||||
runs = await sync_to_async(_load_runs)()
|
||||
if not runs:
|
||||
await self._post_source_status(trigger, "[claude] no recent runs for this scope.", "empty")
|
||||
await self._post_source_status(
|
||||
trigger, "[claude] no recent runs for this scope.", "empty"
|
||||
)
|
||||
return CommandResult(ok=True, status="ok", payload={"count": 0})
|
||||
lines = ["[claude] recent runs:"]
|
||||
for row in runs:
|
||||
@@ -249,24 +285,38 @@ class ClaudeCommandHandler(CommandHandler):
|
||||
).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()
|
||||
approver_service = (
|
||||
str(settings_payload.get("approver_service") or "").strip().lower()
|
||||
)
|
||||
approver_identifier = str(
|
||||
settings_payload.get("approver_identifier") or ""
|
||||
).strip()
|
||||
if not approver_service or not approver_identifier:
|
||||
return CommandResult(ok=False, status="failed", error="approver_channel_not_configured")
|
||||
return CommandResult(
|
||||
ok=False, status="failed", error="approver_channel_not_configured"
|
||||
)
|
||||
|
||||
if str(current_service or "").strip().lower() != approver_service or str(current_channel or "").strip() not in set(
|
||||
channel_variants(approver_service, approver_identifier)
|
||||
):
|
||||
return CommandResult(ok=False, status="failed", error="approval_command_not_allowed_in_this_channel")
|
||||
if str(current_service or "").strip().lower() != approver_service or str(
|
||||
current_channel or ""
|
||||
).strip() not in set(channel_variants(approver_service, approver_identifier)):
|
||||
return CommandResult(
|
||||
ok=False,
|
||||
status="failed",
|
||||
error="approval_command_not_allowed_in_this_channel",
|
||||
)
|
||||
|
||||
approval_key = parsed.approval_key
|
||||
request = await sync_to_async(
|
||||
lambda: CodexPermissionRequest.objects.select_related("codex_run", "external_sync_event")
|
||||
lambda: CodexPermissionRequest.objects.select_related(
|
||||
"codex_run", "external_sync_event"
|
||||
)
|
||||
.filter(user=trigger.user, approval_key=approval_key)
|
||||
.first()
|
||||
)()
|
||||
if request is None:
|
||||
return CommandResult(ok=False, status="failed", error="approval_key_not_found")
|
||||
return CommandResult(
|
||||
ok=False, status="failed", error="approval_key_not_found"
|
||||
)
|
||||
|
||||
now = timezone.now()
|
||||
if parsed.command == "approve":
|
||||
@@ -283,14 +333,20 @@ class ClaudeCommandHandler(CommandHandler):
|
||||
]
|
||||
)
|
||||
if request.external_sync_event_id:
|
||||
await sync_to_async(ExternalSyncEvent.objects.filter(id=request.external_sync_event_id).update)(
|
||||
await sync_to_async(
|
||||
ExternalSyncEvent.objects.filter(
|
||||
id=request.external_sync_event_id
|
||||
).update
|
||||
)(
|
||||
status="ok",
|
||||
error="",
|
||||
)
|
||||
run = request.codex_run
|
||||
run.status = "approved_waiting_resume"
|
||||
run.error = ""
|
||||
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
|
||||
await sync_to_async(run.save)(
|
||||
update_fields=["status", "error", "updated_at"]
|
||||
)
|
||||
source_service = str(run.source_service or "")
|
||||
source_channel = str(run.source_channel or "")
|
||||
resume_payload = dict(request.resume_payload or {})
|
||||
@@ -302,14 +358,18 @@ class ClaudeCommandHandler(CommandHandler):
|
||||
provider_payload["source_service"] = source_service
|
||||
provider_payload["source_channel"] = source_channel
|
||||
event_action = resume_action
|
||||
resume_idempotency_key = str(resume_payload.get("idempotency_key") or "").strip()
|
||||
resume_idempotency_key = str(
|
||||
resume_payload.get("idempotency_key") or ""
|
||||
).strip()
|
||||
resume_event_key = (
|
||||
resume_idempotency_key
|
||||
if resume_idempotency_key
|
||||
else f"{self._approval_prefix}:{approval_key}:approved"
|
||||
)
|
||||
else:
|
||||
provider_payload = dict(run.request_payload.get("provider_payload") or {})
|
||||
provider_payload = dict(
|
||||
run.request_payload.get("provider_payload") or {}
|
||||
)
|
||||
provider_payload.update(
|
||||
{
|
||||
"mode": "approval_response",
|
||||
@@ -337,17 +397,30 @@ class ClaudeCommandHandler(CommandHandler):
|
||||
"error": "",
|
||||
},
|
||||
)
|
||||
return CommandResult(ok=True, status="ok", payload={"approval_key": approval_key, "resolution": "approved"})
|
||||
return CommandResult(
|
||||
ok=True,
|
||||
status="ok",
|
||||
payload={"approval_key": approval_key, "resolution": "approved"},
|
||||
)
|
||||
|
||||
request.status = "denied"
|
||||
request.resolved_at = now
|
||||
request.resolved_by_identifier = current_channel
|
||||
request.resolution_note = "denied via claude command"
|
||||
await sync_to_async(request.save)(
|
||||
update_fields=["status", "resolved_at", "resolved_by_identifier", "resolution_note"]
|
||||
update_fields=[
|
||||
"status",
|
||||
"resolved_at",
|
||||
"resolved_by_identifier",
|
||||
"resolution_note",
|
||||
]
|
||||
)
|
||||
if request.external_sync_event_id:
|
||||
await sync_to_async(ExternalSyncEvent.objects.filter(id=request.external_sync_event_id).update)(
|
||||
await sync_to_async(
|
||||
ExternalSyncEvent.objects.filter(
|
||||
id=request.external_sync_event_id
|
||||
).update
|
||||
)(
|
||||
status="failed",
|
||||
error="approval_denied",
|
||||
)
|
||||
@@ -374,7 +447,11 @@ class ClaudeCommandHandler(CommandHandler):
|
||||
"error": "approval_denied",
|
||||
},
|
||||
)
|
||||
return CommandResult(ok=True, status="ok", payload={"approval_key": approval_key, "resolution": "denied"})
|
||||
return CommandResult(
|
||||
ok=True,
|
||||
status="ok",
|
||||
payload={"approval_key": approval_key, "resolution": "denied"},
|
||||
)
|
||||
|
||||
async def _create_submission(
|
||||
self,
|
||||
@@ -391,7 +468,9 @@ class ClaudeCommandHandler(CommandHandler):
|
||||
).first()
|
||||
)()
|
||||
if cfg is None:
|
||||
return CommandResult(ok=False, status="failed", error="provider_disabled_or_missing")
|
||||
return CommandResult(
|
||||
ok=False, status="failed", error="provider_disabled_or_missing"
|
||||
)
|
||||
|
||||
service, channel = self._effective_scope(trigger)
|
||||
external_chat_id = await sync_to_async(resolve_external_chat_id)(
|
||||
@@ -418,7 +497,9 @@ class ClaudeCommandHandler(CommandHandler):
|
||||
if mode == "plan":
|
||||
anchor = trigger.reply_to
|
||||
if anchor is None:
|
||||
return CommandResult(ok=False, status="failed", error="reply_required_for_claude_plan")
|
||||
return CommandResult(
|
||||
ok=False, status="failed", error="reply_required_for_claude_plan"
|
||||
)
|
||||
rows = await sync_to_async(list)(
|
||||
Message.objects.filter(
|
||||
user=trigger.user,
|
||||
@@ -427,7 +508,9 @@ class ClaudeCommandHandler(CommandHandler):
|
||||
ts__lte=int(trigger.ts or 0),
|
||||
)
|
||||
.order_by("ts")
|
||||
.select_related("session", "session__identifier", "session__identifier__person")
|
||||
.select_related(
|
||||
"session", "session__identifier", "session__identifier__person"
|
||||
)
|
||||
)
|
||||
payload["reply_context"] = {
|
||||
"anchor_message_id": str(anchor.id),
|
||||
@@ -446,12 +529,18 @@ class ClaudeCommandHandler(CommandHandler):
|
||||
source_channel=channel,
|
||||
external_chat_id=external_chat_id,
|
||||
status="waiting_approval",
|
||||
request_payload={"action": "append_update", "provider_payload": dict(payload)},
|
||||
request_payload={
|
||||
"action": "append_update",
|
||||
"provider_payload": dict(payload),
|
||||
},
|
||||
result_payload={},
|
||||
error="",
|
||||
)
|
||||
payload["codex_run_id"] = str(run.id)
|
||||
run.request_payload = {"action": "append_update", "provider_payload": dict(payload)}
|
||||
run.request_payload = {
|
||||
"action": "append_update",
|
||||
"provider_payload": dict(payload),
|
||||
}
|
||||
await sync_to_async(run.save)(update_fields=["request_payload", "updated_at"])
|
||||
|
||||
idempotency_key = f"claude_cmd:{trigger.id}:{mode}:{task.id}:{hashlib.sha1(str(body_text or '').encode('utf-8')).hexdigest()[:12]}"
|
||||
@@ -476,20 +565,26 @@ class ClaudeCommandHandler(CommandHandler):
|
||||
return CommandResult(ok=False, status="failed", error="trigger_not_found")
|
||||
|
||||
profile = await sync_to_async(
|
||||
lambda: CommandProfile.objects.filter(user=trigger.user, slug=self.slug, enabled=True).first()
|
||||
lambda: CommandProfile.objects.filter(
|
||||
user=trigger.user, slug=self.slug, enabled=True
|
||||
).first()
|
||||
)()
|
||||
if profile is None:
|
||||
return CommandResult(ok=False, status="skipped", error="profile_missing")
|
||||
|
||||
parsed = parse_claude_command(ctx.message_text)
|
||||
if not parsed.command:
|
||||
return CommandResult(ok=False, status="skipped", error="claude_command_not_matched")
|
||||
return CommandResult(
|
||||
ok=False, status="skipped", error="claude_command_not_matched"
|
||||
)
|
||||
|
||||
service, channel = self._effective_scope(trigger)
|
||||
|
||||
if parsed.command == "status":
|
||||
project = None
|
||||
reply_task = await self._linked_task_from_reply(trigger.user, trigger.reply_to)
|
||||
reply_task = await self._linked_task_from_reply(
|
||||
trigger.user, trigger.reply_to
|
||||
)
|
||||
if reply_task is not None:
|
||||
project = reply_task.project
|
||||
return await self._run_status(trigger, service, channel, project)
|
||||
@@ -507,7 +602,9 @@ class ClaudeCommandHandler(CommandHandler):
|
||||
reply_task = await self._linked_task_from_reply(trigger.user, trigger.reply_to)
|
||||
task = await self._resolve_task(trigger.user, reference_code, reply_task)
|
||||
if task is None:
|
||||
return CommandResult(ok=False, status="failed", error="task_target_required")
|
||||
return CommandResult(
|
||||
ok=False, status="failed", error="task_target_required"
|
||||
)
|
||||
|
||||
project, project_error = await self._resolve_project(
|
||||
user=trigger.user,
|
||||
@@ -518,7 +615,9 @@ class ClaudeCommandHandler(CommandHandler):
|
||||
project_token=project_token,
|
||||
)
|
||||
if project is None:
|
||||
return CommandResult(ok=False, status="failed", error=project_error or "project_unresolved")
|
||||
return CommandResult(
|
||||
ok=False, status="failed", error=project_error or "project_unresolved"
|
||||
)
|
||||
|
||||
mode = "plan" if parsed.command == "plan" else "default"
|
||||
return await self._create_submission(
|
||||
|
||||
Reference in New Issue
Block a user