Increase security and reformat
This commit is contained in:
@@ -31,7 +31,9 @@ def chunk_for_transport(text: str, limit: int = 3000) -> list[str]:
|
||||
return [part for part in parts if part]
|
||||
|
||||
|
||||
async def post_status_in_source(trigger_message: Message, text: str, origin_tag: str) -> bool:
|
||||
async def post_status_in_source(
|
||||
trigger_message: Message, text: str, origin_tag: str
|
||||
) -> bool:
|
||||
service = str(trigger_message.source_service or "").strip().lower()
|
||||
if service not in STATUS_VISIBLE_SOURCE_SERVICES:
|
||||
return False
|
||||
@@ -76,9 +78,10 @@ async def post_to_channel_binding(
|
||||
channel_identifier = str(binding_channel_identifier or "").strip()
|
||||
if service == "web":
|
||||
session = None
|
||||
if channel_identifier and channel_identifier == str(
|
||||
trigger_message.source_chat_id or ""
|
||||
).strip():
|
||||
if (
|
||||
channel_identifier
|
||||
and channel_identifier == str(trigger_message.source_chat_id or "").strip()
|
||||
):
|
||||
session = trigger_message.session
|
||||
if session is None and channel_identifier:
|
||||
session = await sync_to_async(
|
||||
@@ -99,7 +102,8 @@ async def post_to_channel_binding(
|
||||
ts=int(time.time() * 1000),
|
||||
custom_author="BOT",
|
||||
source_service="web",
|
||||
source_chat_id=channel_identifier or str(trigger_message.source_chat_id or ""),
|
||||
source_chat_id=channel_identifier
|
||||
or str(trigger_message.source_chat_id or ""),
|
||||
message_meta={"origin_tag": origin_tag},
|
||||
)
|
||||
return True
|
||||
|
||||
@@ -58,9 +58,15 @@ def _effective_bootstrap_scope(
|
||||
identifier = str(ctx.channel_identifier or "").strip()
|
||||
if service != "web":
|
||||
return service, identifier
|
||||
session_identifier = getattr(getattr(trigger_message, "session", None), "identifier", None)
|
||||
fallback_service = str(getattr(session_identifier, "service", "") or "").strip().lower()
|
||||
fallback_identifier = str(getattr(session_identifier, "identifier", "") or "").strip()
|
||||
session_identifier = getattr(
|
||||
getattr(trigger_message, "session", None), "identifier", None
|
||||
)
|
||||
fallback_service = (
|
||||
str(getattr(session_identifier, "service", "") or "").strip().lower()
|
||||
)
|
||||
fallback_identifier = str(
|
||||
getattr(session_identifier, "identifier", "") or ""
|
||||
).strip()
|
||||
if fallback_service and fallback_identifier and fallback_service != "web":
|
||||
return fallback_service, fallback_identifier
|
||||
return service, identifier
|
||||
@@ -89,7 +95,11 @@ def _ensure_bp_profile(user_id: int) -> CommandProfile:
|
||||
if str(profile.trigger_token or "").strip() != ".bp":
|
||||
profile.trigger_token = ".bp"
|
||||
profile.save(update_fields=["trigger_token", "updated_at"])
|
||||
for action_type, position in (("extract_bp", 0), ("save_document", 1), ("post_result", 2)):
|
||||
for action_type, position in (
|
||||
("extract_bp", 0),
|
||||
("save_document", 1),
|
||||
("post_result", 2),
|
||||
):
|
||||
action, created = CommandAction.objects.get_or_create(
|
||||
profile=profile,
|
||||
action_type=action_type,
|
||||
@@ -327,7 +337,9 @@ async def process_inbound_message(ctx: CommandContext) -> list[CommandResult]:
|
||||
return []
|
||||
if is_mirrored_origin(trigger_message.message_meta):
|
||||
return []
|
||||
effective_service, effective_channel = _effective_bootstrap_scope(ctx, trigger_message)
|
||||
effective_service, effective_channel = _effective_bootstrap_scope(
|
||||
ctx, trigger_message
|
||||
)
|
||||
security_context = CommandSecurityContext(
|
||||
service=effective_service,
|
||||
channel_identifier=effective_channel,
|
||||
@@ -394,7 +406,9 @@ async def process_inbound_message(ctx: CommandContext) -> list[CommandResult]:
|
||||
result = await handler.execute(ctx)
|
||||
results.append(result)
|
||||
except Exception as exc:
|
||||
log.exception("command execution failed for profile=%s: %s", profile.slug, exc)
|
||||
log.exception(
|
||||
"command execution failed for profile=%s: %s", profile.slug, exc
|
||||
)
|
||||
results.append(
|
||||
CommandResult(
|
||||
ok=False,
|
||||
|
||||
@@ -45,14 +45,15 @@ class BPParsedCommand(dict):
|
||||
return str(self.get("remainder_text") or "")
|
||||
|
||||
|
||||
|
||||
def parse_bp_subcommand(text: str) -> BPParsedCommand:
|
||||
body = str(text or "")
|
||||
if _BP_SET_RANGE_RE.match(body):
|
||||
return BPParsedCommand(command="set_range", remainder_text="")
|
||||
match = _BP_SET_RE.match(body)
|
||||
if match:
|
||||
return BPParsedCommand(command="set", remainder_text=str(match.group("rest") or "").strip())
|
||||
return BPParsedCommand(
|
||||
command="set", remainder_text=str(match.group("rest") or "").strip()
|
||||
)
|
||||
return BPParsedCommand(command=None, remainder_text="")
|
||||
|
||||
|
||||
@@ -63,7 +64,9 @@ def bp_subcommands_enabled() -> bool:
|
||||
return bool(raw)
|
||||
|
||||
|
||||
def bp_trigger_matches(message_text: str, trigger_token: str, exact_match_only: bool) -> bool:
|
||||
def bp_trigger_matches(
|
||||
message_text: str, trigger_token: str, exact_match_only: bool
|
||||
) -> bool:
|
||||
body = str(message_text or "").strip()
|
||||
trigger = str(trigger_token or "").strip()
|
||||
parsed = parse_bp_subcommand(body)
|
||||
@@ -144,7 +147,8 @@ class BPCommandHandler(CommandHandler):
|
||||
"enabled": True,
|
||||
"generation_mode": "ai" if variant_key == "bp" else "verbatim",
|
||||
"send_plan_to_egress": "post_result" in action_types,
|
||||
"send_status_to_source": str(profile.visibility_mode or "") == "status_in_source",
|
||||
"send_status_to_source": str(profile.visibility_mode or "")
|
||||
== "status_in_source",
|
||||
"send_status_to_egress": False,
|
||||
"store_document": True,
|
||||
}
|
||||
@@ -224,10 +228,14 @@ class BPCommandHandler(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"
|
||||
)
|
||||
)
|
||||
|
||||
def _annotation(self, mode: str, message_count: int, has_addendum: bool = False) -> str:
|
||||
def _annotation(
|
||||
self, mode: str, message_count: int, has_addendum: bool = False
|
||||
) -> str:
|
||||
if mode == "set" and has_addendum:
|
||||
return "Generated from 1 message + 1 addendum."
|
||||
if message_count == 1:
|
||||
@@ -291,21 +299,29 @@ class BPCommandHandler(CommandHandler):
|
||||
if anchor is None:
|
||||
run.status = "failed"
|
||||
run.error = "bp_set_range_requires_reply_target"
|
||||
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
|
||||
await sync_to_async(run.save)(
|
||||
update_fields=["status", "error", "updated_at"]
|
||||
)
|
||||
return CommandResult(ok=False, status="failed", error=run.error)
|
||||
rows = await self._load_window(trigger, anchor)
|
||||
deterministic_content = plain_text_blob(rows)
|
||||
if not deterministic_content.strip():
|
||||
run.status = "failed"
|
||||
run.error = "bp_set_range_empty_content"
|
||||
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
|
||||
await sync_to_async(run.save)(
|
||||
update_fields=["status", "error", "updated_at"]
|
||||
)
|
||||
return CommandResult(ok=False, status="failed", error=run.error)
|
||||
if str(policy.get("generation_mode") or "verbatim") == "ai":
|
||||
ai_obj = await sync_to_async(lambda: AI.objects.filter(user=trigger.user).first())()
|
||||
ai_obj = await sync_to_async(
|
||||
lambda: AI.objects.filter(user=trigger.user).first()
|
||||
)()
|
||||
if ai_obj is None:
|
||||
run.status = "failed"
|
||||
run.error = "ai_not_configured"
|
||||
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
|
||||
await sync_to_async(run.save)(
|
||||
update_fields=["status", "error", "updated_at"]
|
||||
)
|
||||
return CommandResult(ok=False, status="failed", error=run.error)
|
||||
prompt = [
|
||||
{
|
||||
@@ -329,12 +345,16 @@ class BPCommandHandler(CommandHandler):
|
||||
except Exception as exc:
|
||||
run.status = "failed"
|
||||
run.error = f"bp_ai_failed:{exc}"
|
||||
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
|
||||
await sync_to_async(run.save)(
|
||||
update_fields=["status", "error", "updated_at"]
|
||||
)
|
||||
return CommandResult(ok=False, status="failed", error=run.error)
|
||||
if not content:
|
||||
run.status = "failed"
|
||||
run.error = "empty_ai_response"
|
||||
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
|
||||
await sync_to_async(run.save)(
|
||||
update_fields=["status", "error", "updated_at"]
|
||||
)
|
||||
return CommandResult(ok=False, status="failed", error=run.error)
|
||||
else:
|
||||
content = deterministic_content
|
||||
@@ -360,9 +380,7 @@ class BPCommandHandler(CommandHandler):
|
||||
elif anchor is not None and remainder:
|
||||
base = str(anchor.text or "").strip() or "(no text)"
|
||||
content = (
|
||||
f"{base}\n"
|
||||
"--- Addendum (newer message text) ---\n"
|
||||
f"{remainder}"
|
||||
f"{base}\n" "--- Addendum (newer message text) ---\n" f"{remainder}"
|
||||
)
|
||||
source_ids.extend([str(anchor.id), str(trigger.id)])
|
||||
has_addendum = True
|
||||
@@ -373,15 +391,21 @@ class BPCommandHandler(CommandHandler):
|
||||
else:
|
||||
run.status = "failed"
|
||||
run.error = "bp_set_empty_content"
|
||||
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
|
||||
await sync_to_async(run.save)(
|
||||
update_fields=["status", "error", "updated_at"]
|
||||
)
|
||||
return CommandResult(ok=False, status="failed", error=run.error)
|
||||
|
||||
if str(policy.get("generation_mode") or "verbatim") == "ai":
|
||||
ai_obj = await sync_to_async(lambda: AI.objects.filter(user=trigger.user).first())()
|
||||
ai_obj = await sync_to_async(
|
||||
lambda: AI.objects.filter(user=trigger.user).first()
|
||||
)()
|
||||
if ai_obj is None:
|
||||
run.status = "failed"
|
||||
run.error = "ai_not_configured"
|
||||
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
|
||||
await sync_to_async(run.save)(
|
||||
update_fields=["status", "error", "updated_at"]
|
||||
)
|
||||
return CommandResult(ok=False, status="failed", error=run.error)
|
||||
prompt = [
|
||||
{
|
||||
@@ -405,16 +429,22 @@ class BPCommandHandler(CommandHandler):
|
||||
except Exception as exc:
|
||||
run.status = "failed"
|
||||
run.error = f"bp_ai_failed:{exc}"
|
||||
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
|
||||
await sync_to_async(run.save)(
|
||||
update_fields=["status", "error", "updated_at"]
|
||||
)
|
||||
return CommandResult(ok=False, status="failed", error=run.error)
|
||||
if not ai_content:
|
||||
run.status = "failed"
|
||||
run.error = "empty_ai_response"
|
||||
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
|
||||
await sync_to_async(run.save)(
|
||||
update_fields=["status", "error", "updated_at"]
|
||||
)
|
||||
return CommandResult(ok=False, status="failed", error=run.error)
|
||||
content = ai_content
|
||||
|
||||
annotation = self._annotation("set", 1 if not has_addendum else 2, has_addendum)
|
||||
annotation = self._annotation(
|
||||
"set", 1 if not has_addendum else 2, has_addendum
|
||||
)
|
||||
doc = None
|
||||
if bool(policy.get("store_document", True)):
|
||||
doc = await self._persist_document(
|
||||
@@ -430,7 +460,9 @@ class BPCommandHandler(CommandHandler):
|
||||
else:
|
||||
run.status = "failed"
|
||||
run.error = "bp_unknown_subcommand"
|
||||
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
|
||||
await sync_to_async(run.save)(
|
||||
update_fields=["status", "error", "updated_at"]
|
||||
)
|
||||
return CommandResult(ok=False, status="failed", error=run.error)
|
||||
|
||||
fanout_stats = {"sent_bindings": 0, "failed_bindings": 0}
|
||||
@@ -479,7 +511,9 @@ class BPCommandHandler(CommandHandler):
|
||||
if trigger.reply_to_id is None:
|
||||
run.status = "failed"
|
||||
run.error = "bp_requires_reply_target"
|
||||
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
|
||||
await sync_to_async(run.save)(
|
||||
update_fields=["status", "error", "updated_at"]
|
||||
)
|
||||
return CommandResult(ok=False, status="failed", error=run.error)
|
||||
|
||||
anchor = trigger.reply_to
|
||||
@@ -488,7 +522,9 @@ class BPCommandHandler(CommandHandler):
|
||||
rows,
|
||||
author_rewrites={"USER": "Operator", "BOT": "Assistant"},
|
||||
)
|
||||
max_transcript_chars = int(getattr(settings, "BP_MAX_TRANSCRIPT_CHARS", 12000) or 12000)
|
||||
max_transcript_chars = int(
|
||||
getattr(settings, "BP_MAX_TRANSCRIPT_CHARS", 12000) or 12000
|
||||
)
|
||||
transcript = _clamp_transcript(transcript, max_transcript_chars)
|
||||
default_template = (
|
||||
"Business Plan:\n"
|
||||
@@ -499,7 +535,9 @@ class BPCommandHandler(CommandHandler):
|
||||
"- Risks"
|
||||
)
|
||||
template_text = profile.template_text or default_template
|
||||
max_template_chars = int(getattr(settings, "BP_MAX_TEMPLATE_CHARS", 5000) or 5000)
|
||||
max_template_chars = int(
|
||||
getattr(settings, "BP_MAX_TEMPLATE_CHARS", 5000) or 5000
|
||||
)
|
||||
template_text = str(template_text or "")[:max_template_chars]
|
||||
generation_mode = str(policy.get("generation_mode") or "ai")
|
||||
if generation_mode == "verbatim":
|
||||
@@ -507,14 +545,20 @@ class BPCommandHandler(CommandHandler):
|
||||
if not summary.strip():
|
||||
run.status = "failed"
|
||||
run.error = "bp_verbatim_empty_content"
|
||||
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
|
||||
await sync_to_async(run.save)(
|
||||
update_fields=["status", "error", "updated_at"]
|
||||
)
|
||||
return CommandResult(ok=False, status="failed", error=run.error)
|
||||
else:
|
||||
ai_obj = await sync_to_async(lambda: AI.objects.filter(user=trigger.user).first())()
|
||||
ai_obj = await sync_to_async(
|
||||
lambda: AI.objects.filter(user=trigger.user).first()
|
||||
)()
|
||||
if ai_obj is None:
|
||||
run.status = "failed"
|
||||
run.error = "ai_not_configured"
|
||||
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
|
||||
await sync_to_async(run.save)(
|
||||
update_fields=["status", "error", "updated_at"]
|
||||
)
|
||||
return CommandResult(ok=False, status="failed", error=run.error)
|
||||
|
||||
prompt = [
|
||||
@@ -530,13 +574,20 @@ class BPCommandHandler(CommandHandler):
|
||||
},
|
||||
]
|
||||
try:
|
||||
summary = str(await ai_runner.run_prompt(prompt, ai_obj, operation="command_bp_extract") or "").strip()
|
||||
summary = str(
|
||||
await ai_runner.run_prompt(
|
||||
prompt, ai_obj, operation="command_bp_extract"
|
||||
)
|
||||
or ""
|
||||
).strip()
|
||||
if not summary:
|
||||
raise RuntimeError("empty_ai_response")
|
||||
except Exception as exc:
|
||||
run.status = "failed"
|
||||
run.error = f"bp_ai_failed:{exc}"
|
||||
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
|
||||
await sync_to_async(run.save)(
|
||||
update_fields=["status", "error", "updated_at"]
|
||||
)
|
||||
return CommandResult(ok=False, status="failed", error=run.error)
|
||||
|
||||
annotation = self._annotation("legacy", len(rows))
|
||||
@@ -588,23 +639,31 @@ class BPCommandHandler(CommandHandler):
|
||||
|
||||
async def execute(self, ctx: CommandContext) -> CommandResult:
|
||||
trigger = await sync_to_async(
|
||||
lambda: Message.objects.select_related("user", "session").filter(id=ctx.message_id).first()
|
||||
lambda: Message.objects.select_related("user", "session")
|
||||
.filter(id=ctx.message_id)
|
||||
.first()
|
||||
)()
|
||||
if trigger is None:
|
||||
return CommandResult(ok=False, status="failed", error="trigger_not_found")
|
||||
|
||||
profile = await sync_to_async(
|
||||
lambda: trigger.user.commandprofile_set.filter(slug=self.slug, enabled=True).first()
|
||||
lambda: trigger.user.commandprofile_set.filter(
|
||||
slug=self.slug, enabled=True
|
||||
).first()
|
||||
)()
|
||||
if profile is None:
|
||||
return CommandResult(ok=False, status="skipped", error="profile_missing")
|
||||
|
||||
actions = await sync_to_async(list)(
|
||||
CommandAction.objects.filter(profile=profile, enabled=True).order_by("position", "id")
|
||||
CommandAction.objects.filter(profile=profile, enabled=True).order_by(
|
||||
"position", "id"
|
||||
)
|
||||
)
|
||||
action_types = {row.action_type for row in actions}
|
||||
if "extract_bp" not in action_types:
|
||||
return CommandResult(ok=False, status="skipped", error="extract_bp_disabled")
|
||||
return CommandResult(
|
||||
ok=False, status="skipped", error="extract_bp_disabled"
|
||||
)
|
||||
|
||||
run, created = await sync_to_async(CommandRun.objects.get_or_create)(
|
||||
profile=profile,
|
||||
@@ -612,7 +671,11 @@ class BPCommandHandler(CommandHandler):
|
||||
defaults={"user": trigger.user, "status": "running"},
|
||||
)
|
||||
if not created and run.status in {"ok", "running"}:
|
||||
return CommandResult(ok=True, status="ok", payload={"document_id": str(run.result_ref_id or "")})
|
||||
return CommandResult(
|
||||
ok=True,
|
||||
status="ok",
|
||||
payload={"document_id": str(run.result_ref_id or "")},
|
||||
)
|
||||
|
||||
run.status = "running"
|
||||
run.error = ""
|
||||
@@ -627,7 +690,9 @@ class BPCommandHandler(CommandHandler):
|
||||
if not bool(policy.get("enabled")):
|
||||
run.status = "skipped"
|
||||
run.error = f"variant_disabled:{variant_key}"
|
||||
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
|
||||
await sync_to_async(run.save)(
|
||||
update_fields=["status", "error", "updated_at"]
|
||||
)
|
||||
return CommandResult(ok=False, status="skipped", error=run.error)
|
||||
|
||||
parsed = parse_bp_subcommand(ctx.message_text)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
_CODEX_DEFAULT_RE = re.compile(
|
||||
r"^\s*(?:\.codex\b|#codex#?)(?P<body>.*)$",
|
||||
@@ -31,7 +31,9 @@ _CODEX_PLAN_RE = re.compile(
|
||||
r"^\s*(?:\.codex\s+plan\b|#codex\s+plan#?)(?P<body>.*)$",
|
||||
re.IGNORECASE | re.DOTALL,
|
||||
)
|
||||
_CODEX_STATUS_RE = re.compile(r"^\s*(?:\.codex\s+status\b|#codex\s+status#?)\s*$", re.IGNORECASE)
|
||||
_CODEX_STATUS_RE = re.compile(
|
||||
r"^\s*(?:\.codex\s+status\b|#codex\s+status#?)\s*$", re.IGNORECASE
|
||||
)
|
||||
_CODEX_APPROVE_DENY_RE = re.compile(
|
||||
r"^\s*(?:\.codex|#codex)\s+(?P<action>approve|deny)\s+(?P<approval_key>[A-Za-z0-9._:-]+)#?\s*$",
|
||||
re.IGNORECASE,
|
||||
@@ -55,7 +57,6 @@ class CodexParsedCommand(dict):
|
||||
return str(self.get("approval_key") or "")
|
||||
|
||||
|
||||
|
||||
def parse_codex_command(text: str) -> CodexParsedCommand:
|
||||
body = str(text or "")
|
||||
m = _CODEX_APPROVE_DENY_RE.match(body)
|
||||
@@ -84,7 +85,9 @@ def parse_codex_command(text: str) -> CodexParsedCommand:
|
||||
return CodexParsedCommand(command=None, body_text="", approval_key="")
|
||||
|
||||
|
||||
def codex_trigger_matches(message_text: str, trigger_token: str, exact_match_only: bool) -> bool:
|
||||
def codex_trigger_matches(
|
||||
message_text: str, trigger_token: str, exact_match_only: bool
|
||||
) -> bool:
|
||||
body = str(message_text or "").strip()
|
||||
parsed = parse_codex_command(body)
|
||||
if parsed.command:
|
||||
@@ -102,7 +105,9 @@ class CodexCommandHandler(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()
|
||||
)()
|
||||
@@ -113,11 +118,18 @@ class CodexCommandHandler(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 []
|
||||
@@ -130,7 +142,9 @@ class CodexCommandHandler(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(
|
||||
@@ -142,7 +156,9 @@ class CodexCommandHandler(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()
|
||||
@@ -163,10 +179,14 @@ class CodexCommandHandler(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()
|
||||
@@ -189,7 +209,9 @@ class CodexCommandHandler(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, ""
|
||||
@@ -198,20 +220,31 @@ class CodexCommandHandler(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"codex-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:
|
||||
@@ -224,7 +257,9 @@ class CodexCommandHandler(CommandHandler):
|
||||
|
||||
runs = await sync_to_async(_load_runs)()
|
||||
if not runs:
|
||||
await self._post_source_status(trigger, "[codex] no recent runs for this scope.", "empty")
|
||||
await self._post_source_status(
|
||||
trigger, "[codex] no recent runs for this scope.", "empty"
|
||||
)
|
||||
return CommandResult(ok=True, status="ok", payload={"count": 0})
|
||||
lines = ["[codex] recent runs:"]
|
||||
for row in runs:
|
||||
@@ -243,27 +278,43 @@ class CodexCommandHandler(CommandHandler):
|
||||
current_channel: str,
|
||||
) -> CommandResult:
|
||||
cfg = await sync_to_async(
|
||||
lambda: TaskProviderConfig.objects.filter(user=trigger.user, provider="codex_cli").first()
|
||||
lambda: TaskProviderConfig.objects.filter(
|
||||
user=trigger.user, provider="codex_cli"
|
||||
).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":
|
||||
@@ -280,14 +331,20 @@ class CodexCommandHandler(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 {})
|
||||
@@ -299,14 +356,18 @@ class CodexCommandHandler(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"codex_approval:{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",
|
||||
@@ -334,17 +395,30 @@ class CodexCommandHandler(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 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",
|
||||
)
|
||||
@@ -371,7 +445,11 @@ class CodexCommandHandler(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,
|
||||
@@ -383,10 +461,14 @@ class CodexCommandHandler(CommandHandler):
|
||||
project: TaskProject,
|
||||
) -> CommandResult:
|
||||
cfg = await sync_to_async(
|
||||
lambda: TaskProviderConfig.objects.filter(user=trigger.user, provider="codex_cli", enabled=True).first()
|
||||
lambda: TaskProviderConfig.objects.filter(
|
||||
user=trigger.user, provider="codex_cli", enabled=True
|
||||
).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)(
|
||||
@@ -413,7 +495,9 @@ class CodexCommandHandler(CommandHandler):
|
||||
if mode == "plan":
|
||||
anchor = trigger.reply_to
|
||||
if anchor is None:
|
||||
return CommandResult(ok=False, status="failed", error="reply_required_for_codex_plan")
|
||||
return CommandResult(
|
||||
ok=False, status="failed", error="reply_required_for_codex_plan"
|
||||
)
|
||||
rows = await sync_to_async(list)(
|
||||
Message.objects.filter(
|
||||
user=trigger.user,
|
||||
@@ -422,7 +506,9 @@ class CodexCommandHandler(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),
|
||||
@@ -441,12 +527,18 @@ class CodexCommandHandler(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"codex_cmd:{trigger.id}:{mode}:{task.id}:{hashlib.sha1(str(body_text or '').encode('utf-8')).hexdigest()[:12]}"
|
||||
@@ -471,20 +563,26 @@ class CodexCommandHandler(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_codex_command(ctx.message_text)
|
||||
if not parsed.command:
|
||||
return CommandResult(ok=False, status="skipped", error="codex_command_not_matched")
|
||||
return CommandResult(
|
||||
ok=False, status="skipped", error="codex_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)
|
||||
@@ -502,7 +600,9 @@ class CodexCommandHandler(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,
|
||||
@@ -513,7 +613,9 @@ class CodexCommandHandler(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(
|
||||
|
||||
@@ -32,7 +32,8 @@ def _legacy_defaults(profile: CommandProfile, post_result_enabled: bool) -> dict
|
||||
"enabled": True,
|
||||
"generation_mode": "ai",
|
||||
"send_plan_to_egress": bool(post_result_enabled),
|
||||
"send_status_to_source": str(profile.visibility_mode or "") == "status_in_source",
|
||||
"send_status_to_source": str(profile.visibility_mode or "")
|
||||
== "status_in_source",
|
||||
"send_status_to_egress": False,
|
||||
"store_document": True,
|
||||
}
|
||||
@@ -56,7 +57,9 @@ def ensure_variant_policies_for_profile(
|
||||
*,
|
||||
action_rows: Iterable[CommandAction] | None = None,
|
||||
) -> dict[str, CommandVariantPolicy]:
|
||||
actions = list(action_rows) if action_rows is not None else list(profile.actions.all())
|
||||
actions = (
|
||||
list(action_rows) if action_rows is not None else list(profile.actions.all())
|
||||
)
|
||||
post_result_enabled = any(
|
||||
row.action_type == "post_result" and bool(row.enabled) for row in actions
|
||||
)
|
||||
@@ -91,7 +94,9 @@ def ensure_variant_policies_for_profile(
|
||||
return result
|
||||
|
||||
|
||||
def load_variant_policy(profile: CommandProfile, variant_key: str) -> CommandVariantPolicy | None:
|
||||
def load_variant_policy(
|
||||
profile: CommandProfile, variant_key: str
|
||||
) -> CommandVariantPolicy | None:
|
||||
key = str(variant_key or "").strip()
|
||||
if not key:
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user