Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions loopx/extensions/lark/event_collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
inspect_lark_event_inbox,
load_lark_event_inbox_config,
)
from .identity_shapes import LARK_CHAT_ID_PATTERN

CONFIG_SCHEMA_VERSION_V0 = "lark_event_collector_config_v0"
CONFIG_SCHEMA_VERSION = "lark_event_collector_config_v1"
Expand All @@ -29,7 +30,6 @@
STATUS_SCHEMA_VERSION = "lark_event_collector_status_v0"
INSTALL_SCHEMA_VERSION = "lark_event_collector_install_v0"
SERVICE_RE = re.compile(r"^loopx-[a-z0-9][a-z0-9._-]{1,73}$")
CHAT_RE = re.compile(r"^oc_[A-Za-z0-9_-]+$")
TIMEOUT_RE = re.compile(r"^[1-9][0-9]*(?:s|m|h)$")
SUPPORTED_SUPERVISORS = {"launchd", "systemd"}
SUPPORTED_EVENT_KEY = "im.message.receive_v1"
Expand Down Expand Up @@ -231,7 +231,7 @@ def load_lark_event_collector_config(
"public-safe token"
)
chat_id = str(raw_route.get("chat_id") or "").strip()
if not CHAT_RE.fullmatch(chat_id):
if not LARK_CHAT_ID_PATTERN.fullmatch(chat_id):
raise ValueError(
f"collector route {index + 1} chat_id must be a Lark oc_ chat id"
)
Expand Down
4 changes: 2 additions & 2 deletions loopx/extensions/lark/event_collector_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

from ...file_lock import exclusive_file_lock
from .event_collector import (
CHAT_RE,
CONFIG_SCHEMA_VERSION,
MAX_ROUTE_COUNT,
_project_config_path,
Expand All @@ -23,6 +22,7 @@
ROUTE_KEY_PATTERN,
load_lark_event_inbox_config,
)
from .identity_shapes import LARK_CHAT_ID_PATTERN

ROUTE_RECONCILE_SCHEMA_VERSION = "lark_event_collector_route_reconcile_v0"

Expand All @@ -38,7 +38,7 @@ def _route_reconcile_candidate(
raise ValueError("collector route reconcile requires a v1 collector config")
if not ROUTE_KEY_PATTERN.fullmatch(route_key):
raise ValueError("route_key must be a lowercase public-safe token")
if not CHAT_RE.fullmatch(chat_id):
if not LARK_CHAT_ID_PATTERN.fullmatch(chat_id):
raise ValueError("chat_id must be a Lark oc_ chat id")
root = Path(config["project"])
inbox_ref, inbox_config_path = _relative_project_path(
Expand Down
6 changes: 4 additions & 2 deletions loopx/extensions/lark/event_inbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,17 @@
OPEN_ID_PATTERN,
lark_provider_mention_identities,
)
from .identity_shapes import ( # noqa: F401
LARK_CHAT_ID_PATTERN as CHAT_ID_PATTERN,
LARK_MESSAGE_ID_PATTERN as MESSAGE_ID_PATTERN,
)

EVENT_SCHEMA_VERSION = "lark_event_inbox_event_v0"
CONFIG_SCHEMA_VERSION = "lark_event_inbox_config_v0"
PROCESSED_SCHEMA_VERSION = "lark_event_inbox_processed_v0"
MATERIAL_REVIEW_LEDGER_SCHEMA_VERSION = "lark_material_review_ledger_v0"
MESSAGE_ID_PATTERN = re.compile(r"om_[A-Za-z0-9_-]+")
EVENT_ID_PATTERN = re.compile(r"[A-Za-z0-9:_-]{1,200}")
SAFE_PROFILE_PATTERN = re.compile(r"[A-Za-z0-9._-]{1,100}")
CHAT_ID_PATTERN = re.compile(r"oc_[A-Za-z0-9_-]+")
REACTION_EMOJI_PATTERN = re.compile(r"[A-Za-z0-9_]{1,64}")
REPLY_PLACEMENT_POLICIES = {"source_thread", "source_context"}
REPLY_EDITORIAL_STYLES = {"concise", "bullet_points_preferred"}
Expand Down
4 changes: 2 additions & 2 deletions loopx/extensions/lark/goal_channel_delivery_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
from collections.abc import Callable, Mapping
from typing import Any

from .identity_shapes import LARK_CHAT_ID_PATTERN

_GOAL_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$")
_LARK_CHAT_ID_RE = re.compile(r"^oc_[A-Za-z0-9_-]+$")
_LARK_APP_ID_RE = re.compile(r"^cli_[A-Za-z0-9_-]+$")
_LARK_PROFILE_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$")

Expand Down Expand Up @@ -42,7 +42,7 @@ def goal_channel_delivery_route(
if (
identity.get("mode") != "project_bot"
or sender_identity != "bot"
or not _LARK_CHAT_ID_RE.fullmatch(chat_id)
or not LARK_CHAT_ID_PATTERN.fullmatch(chat_id)
or not _LARK_PROFILE_RE.fullmatch(sender_profile)
or sender_profile.lower() == "default"
or not _LARK_APP_ID_RE.fullmatch(bot_app_id)
Expand Down
15 changes: 2 additions & 13 deletions loopx/extensions/lark/goal_channel_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import hashlib
import html
import json
import re
from collections.abc import Callable, Mapping
from pathlib import Path
from typing import Any
Expand Down Expand Up @@ -36,16 +35,13 @@
GoalChannelMessageDeliverySession,
resolve_bound_goal_channel,
)
from .identity_shapes import require_card_callback_identity
from .presentation.kanban import CommandRunner, default_subprocess_runner


OPERATION_CARD_ACTION_SCHEMA_VERSION = "loopx_operation_card_action_v0"
OPERATION_CALLBACK_RECEIPT_SCHEMA_VERSION = "lark_operation_callback_receipt_v0"
OPERATION_EXECUTOR_CAPABILITY_ID = "human-confirmed-operation-executor"
_EVENT_ID = re.compile(r"^[A-Za-z0-9._:-]{1,240}$")
_MESSAGE_ID = re.compile(r"^om_[A-Za-z0-9_-]+$")
_CHAT_ID = re.compile(r"^oc_[A-Za-z0-9_-]+$")
_OPEN_ID = re.compile(r"^ou_[A-Za-z0-9_-]+$")


def _digest(value: object) -> str:
Expand Down Expand Up @@ -990,14 +986,7 @@ def handle_goal_channel_operation_callback(
or any(ord(character) < 32 for character in callback_token)
):
raise ValueError("operation callback update token is invalid")
for field, pattern in (
("event_id", _EVENT_ID),
("message_id", _MESSAGE_ID),
("chat_id", _CHAT_ID),
("operator_id", _OPEN_ID),
):
if not pattern.fullmatch(str(event.get(field) or "")):
raise ValueError(f"operation callback {field} is invalid")
require_card_callback_identity(event, error_prefix="operation callback")
if str(event.get("host") or "") != "im_message":
raise ValueError("operation callback host is unsupported")
store = ChatActionStore(action_store_root)
Expand Down
10 changes: 7 additions & 3 deletions loopx/extensions/lark/goal_channel_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@
from enum import Enum
from typing import Any

# Re-exported for the existing callers of this module; the shapes themselves are
# decided once, in ``identity_shapes``.
from .identity_shapes import ( # noqa: F401
LARK_CHAT_ID_SEARCH as CHAT_ID_PATTERN,
LARK_MESSAGE_ID_SEARCH as MESSAGE_ID_PATTERN,
LARK_OPEN_ID_SEARCH as OPEN_ID_PATTERN,
)
from .presentation.kanban import CommandRunner

CHAT_ID_PATTERN = re.compile(r"oc_[A-Za-z0-9_-]+")
MESSAGE_ID_PATTERN = re.compile(r"om_[A-Za-z0-9_-]+")
APP_ID_PATTERN = re.compile(r"cli_[A-Za-z0-9_-]+")
OPEN_ID_PATTERN = re.compile(r"ou_[A-Za-z0-9_-]+")
SAFE_PROFILE_PATTERN = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,99}")
REQUIRED_GOAL_TOPIC_SCOPES = ("im:message", "im:message:readonly")
REQUIRED_BOT_GROUP_HISTORY_SCOPES = (
Expand Down
49 changes: 49 additions & 0 deletions loopx/extensions/lark/identity_shapes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""One owner for the whole-value shapes of Lark platform identifiers.

A card callback names four identifiers: the event that triggered it, the message
the card lives in, the chat that message belongs to, and the operator who
clicked. Two callback validators each decided independently what a valid one
looks like, so a shape fix in one of them silently leaves the other behind.

Searching for an identifier inside larger text is the same decision about a
different question, so it is stated here too and nowhere else:
``goal_channel_transport`` and ``event_inbox`` each compiled the unanchored
spelling themselves while fifteen and thirteen modules respectively imported it
from them, which is how a shape fix could land in one and be missed in the
other. The unanchored forms are not interchangeable with the anchored ones --
``goal_channel_contracts`` and ``goal_channel_notification`` ``search()`` for an
id inside payload text, where ``^`` and ``$`` would change the answer -- so both
spellings stay distinct and one module decides both.
"""

from __future__ import annotations

import re
from collections.abc import Mapping
from typing import Any

LARK_EVENT_ID_PATTERN = re.compile(r"^[A-Za-z0-9._:-]{1,240}$")
LARK_MESSAGE_ID_PATTERN = re.compile(r"^om_[A-Za-z0-9_-]+$")
LARK_CHAT_ID_PATTERN = re.compile(r"^oc_[A-Za-z0-9_-]+$")
LARK_OPEN_ID_PATTERN = re.compile(r"^ou_[A-Za-z0-9_-]+$")

LARK_MESSAGE_ID_SEARCH = re.compile(r"om_[A-Za-z0-9_-]+")
LARK_CHAT_ID_SEARCH = re.compile(r"oc_[A-Za-z0-9_-]+")
LARK_OPEN_ID_SEARCH = re.compile(r"ou_[A-Za-z0-9_-]+")

CARD_CALLBACK_IDENTITY_SHAPES = (
("event_id", LARK_EVENT_ID_PATTERN),
("message_id", LARK_MESSAGE_ID_PATTERN),
("chat_id", LARK_CHAT_ID_PATTERN),
("operator_id", LARK_OPEN_ID_PATTERN),
)


def require_card_callback_identity(
event: Mapping[str, Any], *, error_prefix: str
) -> None:
"""Reject a card callback whose identity fields are not whole Lark ids."""

for field, pattern in CARD_CALLBACK_IDENTITY_SHAPES:
if not pattern.fullmatch(str(event.get(field) or "")):
raise ValueError(f"{error_prefix} {field} is invalid")
16 changes: 9 additions & 7 deletions loopx/extensions/lark/reviewer_notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
reviewer_notification_idempotency_key,
)
from ...control_plane.runtime.public_safety import public_safe_compact_text
from .identity_shapes import (
LARK_CHAT_ID_PATTERN,
LARK_MESSAGE_ID_PATTERN,
LARK_OPEN_ID_PATTERN,
)
from .outbound import (
LarkMention,
build_lark_mention_prefix,
Expand All @@ -30,9 +35,6 @@
r"search:message|missing\s+scope[^\n]*(?:message|search)",
re.IGNORECASE,
)
LARK_DESTINATION_PATTERN = re.compile(r"oc_[A-Za-z0-9_-]+")
LARK_MEMBER_PATTERN = re.compile(r"ou_[A-Za-z0-9_-]+")
LARK_MESSAGE_PATTERN = re.compile(r"om_[A-Za-z0-9_-]+")


def _normalise_handle(value: Any) -> str | None:
Expand All @@ -43,7 +45,7 @@ def _normalise_handle(value: Any) -> str | None:
def _find_message_id(value: Any) -> str | None:
if isinstance(value, Mapping):
candidate = value.get("message_id")
if isinstance(candidate, str) and LARK_MESSAGE_PATTERN.fullmatch(candidate):
if isinstance(candidate, str) and LARK_MESSAGE_ID_PATTERN.fullmatch(candidate):
return candidate
for nested in value.values():
found = _find_message_id(nested)
Expand Down Expand Up @@ -99,7 +101,7 @@ def visit(node: Any) -> None:
for key, child in node.items():
if key in {"member_id", "open_id"} and isinstance(child, str):
member_id = child.strip()
if LARK_MEMBER_PATTERN.fullmatch(member_id):
if LARK_OPEN_ID_PATTERN.fullmatch(member_id):
member_ids.add(member_id)
else:
visit(child)
Expand Down Expand Up @@ -179,7 +181,7 @@ def lark_reviewer_notification_sink(
external_write_authority_asserted=execute,
blocker="dedicated_bot_identity_required",
)
if not LARK_DESTINATION_PATTERN.fullmatch(destination_id):
if not LARK_CHAT_ID_PATTERN.fullmatch(destination_id):
return build_reviewer_notification_sink_result(
sink_kind=sink_kind,
reviewer_handles=[],
Expand Down Expand Up @@ -207,7 +209,7 @@ def lark_reviewer_notification_sink(
identity.get("display_name") or handle,
limit=80,
)
if not LARK_MEMBER_PATTERN.fullmatch(member_id):
if not LARK_OPEN_ID_PATTERN.fullmatch(member_id):
return build_reviewer_notification_sink_result(
sink_kind=sink_kind,
reviewer_handles=[],
Expand Down
24 changes: 9 additions & 15 deletions loopx/extensions/lark/team_plan_confirmation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import hashlib
import json
import logging
import re
import subprocess
import threading
from collections.abc import Callable, Mapping, Sequence
Expand Down Expand Up @@ -46,6 +45,11 @@
goal_channel_target_for_name,
read_goal_channel_targets,
)
from .identity_shapes import (
LARK_CHAT_ID_PATTERN,
LARK_MESSAGE_ID_PATTERN,
require_card_callback_identity,
)
from .manager_reply_delivery import (
TEAM_PLAN_DELIVERY_RECEIPT_SCHEMA_VERSION,
validate_team_plan_delivery_receipt,
Expand All @@ -60,10 +64,6 @@


TEAM_PLAN_CALLBACK_RECEIPT_SCHEMA_VERSION = "lark_team_plan_callback_receipt_v0"
_EVENT_ID = re.compile(r"^[A-Za-z0-9._:-]{1,240}$")
_MESSAGE_ID = re.compile(r"^om_[A-Za-z0-9_-]+$")
_CHAT_ID = re.compile(r"^oc_[A-Za-z0-9_-]+$")
_OPEN_ID = re.compile(r"^ou_[A-Za-z0-9_-]+$")
_EVENT_DIAGNOSTIC_PREFIX = "[event] "

ProcessFactory = Callable[[list[str]], Any]
Expand Down Expand Up @@ -141,7 +141,7 @@ def active_profile_chat_ids(snapshot: Mapping[str, Any], profile: str) -> list[s
same_app = bool(app_ids) and str(identity.get("bot_app_id") or "") in app_ids
if (
same_app or str(identity.get("sender_profile") or "") == profile
) and _CHAT_ID.fullmatch(chat_id):
) and LARK_CHAT_ID_PATTERN.fullmatch(chat_id):
chats.add(chat_id)
return sorted(chats)

Expand Down Expand Up @@ -459,7 +459,8 @@ def _deliver_one(
"submitted_card": card,
"authorized_principal": authorized_principal,
}
if not _MESSAGE_ID.fullmatch(str(recorded.get("message_id") or "")) or any(
recorded_message_id = str(recorded.get("message_id") or "")
if not LARK_MESSAGE_ID_PATTERN.fullmatch(recorded_message_id) or any(
recorded.get(key) != value for key, value in expected.items()
):
raise ActionConflictError(
Expand Down Expand Up @@ -691,14 +692,7 @@ def handle_team_plan_review_callback(
or any(ord(character) < 32 for character in callback_token)
):
raise ValueError("team plan callback update token is invalid")
for field, pattern in (
("event_id", _EVENT_ID),
("message_id", _MESSAGE_ID),
("chat_id", _CHAT_ID),
("operator_id", _OPEN_ID),
):
if not pattern.fullmatch(str(event.get(field) or "")):
raise ValueError(f"team plan callback {field} is invalid")
require_card_callback_identity(event, error_prefix="team plan callback")
if str(event.get("host") or "") != "im_message":
raise ValueError("team plan callback host is unsupported")
store = ChatActionStore(action_store_root)
Expand Down
Loading
Loading