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
6 changes: 3 additions & 3 deletions examples/lark-kanban-control-plane-smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
sys.path.insert(0, str(REPO_ROOT))

from loopx.extensions.lark.presentation import issue_fix_surface # noqa: E402
from loopx.extensions.lark.cli_transport import run_command # noqa: E402
from loopx.extensions.lark.presentation.kanban import ( # noqa: E402
CLAIM_UNCLAIMED,
STATUS_CLAIMED,
Expand All @@ -23,7 +24,6 @@
STATUS_REVIEW,
STATUS_TODO,
LarkKanbanConfig,
_run_command,
_lark_record_from_todo_block,
build_create_board_plan,
default_lark_kanban_config_path,
Expand Down Expand Up @@ -66,7 +66,7 @@ def forbidden_runner(

for argument in ("--ak", "--sk", "--app-secret", "--access-token"):
try:
_run_command(
run_command(
["lark-cli", "base", "+record-list", argument, "fixture-secret"],
execute=True,
runner=forbidden_runner,
Expand All @@ -77,7 +77,7 @@ def forbidden_runner(
raise AssertionError(f"credential argument was accepted: {argument}")
assert runner_called is False

dry_run = _run_command(
dry_run = run_command(
[
"lark-cli",
"base",
Expand Down
5 changes: 1 addition & 4 deletions loopx/chat_lark_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,7 @@
rebind_lark_manager_session,
)
from .extensions.lark.goal_topic_batch import connect_lark_goal_topics
from .extensions.lark.presentation.kanban import (
CommandRunner,
default_subprocess_runner,
)
from .extensions.lark.cli_transport import CommandRunner, default_subprocess_runner
from .chat_agent import CodexChatAgentError
from .chat_manager import (
controller_runtime_root,
Expand Down
2 changes: 1 addition & 1 deletion loopx/chat_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
from .extensions.lark.goal_topic_connections import list_lark_apps
from .extensions.lark.goal_topic_runtime import LarkGoalTopicRuntimeService
from .extensions.lark.manager_routing import authorized_manager_goal_ids
from .extensions.lark.presentation.kanban import CommandRunner
from .extensions.lark.cli_transport import CommandRunner
from .extensions.runtime import (
default_extension_state_file,
resolve_extension_activation,
Expand Down
2 changes: 1 addition & 1 deletion loopx/cli_commands/explore_feishu_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
explore_feishu_sync_busy_packet,
explore_feishu_sync_singleflight,
)
from ..extensions.lark.presentation.kanban import DEFAULT_CLI_BIN
from ..extensions.lark.cli_transport import DEFAULT_CLI_BIN
from ..extensions.lark.presentation.sink_visibility import SINK_VISIBILITY_OWNER_ONLY, SINK_VISIBILITIES


Expand Down
2 changes: 1 addition & 1 deletion loopx/cli_commands/lark_kanban.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
from typing import Callable

from ..extensions.lark import LARK_EXTENSION_ID, LARK_PROJECTION_SINK_PERMISSION
from ..extensions.lark.cli_transport import DEFAULT_CLI_BIN
from ..extensions.lark.presentation.kanban import (
DEFAULT_AGENT_ID,
DEFAULT_CLI_BIN,
DEFAULT_STATUS_QUEUE_VIEW,
DEFAULT_TABLE_NAME,
LarkKanbanConfig,
Expand Down
2 changes: 1 addition & 1 deletion loopx/extensions/lark/card_callback.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
normalized_card_text,
)
from .goal_channel_transport import call, json_payload, lark_args
from .presentation.kanban import CommandRunner
from .cli_transport import CommandRunner


def callback_timestamp(value: object, *, subject: str) -> str:
Expand Down
4 changes: 3 additions & 1 deletion loopx/extensions/lark/cli_resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from pathlib import Path
from typing import Any

from .cli_transport import CommandRunner


_SEMVER_DIRECTORY = re.compile(r"^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$")
_VERSION_TEXT = re.compile(r"(?:lark-cli\s+)?v?(\d+(?:\.\d+){1,3}(?:[-+][A-Za-z0-9._-]+)?)")
Expand Down Expand Up @@ -250,7 +252,7 @@ def build_lark_command_runner(
*,
environ: Mapping[str, str] | None = None,
subprocess_run: Callable[..., Any] = subprocess.run,
) -> Callable[[list[str], Path | None, float | None], dict[str, Any]]:
) -> CommandRunner:
"""Build the dashboard runner with the resolved Node wrapper environment."""

def run(
Expand Down
165 changes: 165 additions & 0 deletions loopx/extensions/lark/cli_transport.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
"""Lark provider CLI execution and result decoding.

Presentation and conversation callers share this IO boundary. Callers still
own authority, retries, and provider readback; a successful process is not a
verified external effect. Executable discovery and Node wrapper environments
remain in cli_resolution.
"""

from __future__ import annotations

import json
import shlex
import subprocess
from collections.abc import Callable
from pathlib import Path
from typing import Any

DEFAULT_CLI_BIN = "lark-cli"
OUTPUT_LIMIT = 1800
CommandRunner = Callable[[list[str], Path | None, float | None], dict[str, Any]]

_LARK_CREDENTIAL_ARGUMENTS = frozenset(
{
"--access-key",
"--access-token",
"--ak",
"--app-secret",
"--authorization",
"--bearer-token",
"--client-secret",
"--secret-key",
"--sk",
"--tenant-access-token",
"--user-access-token",
}
)


def default_subprocess_runner(
args: list[str],
cwd: Path | None = None,
timeout: float | None = None,
) -> dict[str, Any]:
completed = subprocess.run(
args,
cwd=str(cwd) if cwd else None,
timeout=timeout,
capture_output=True,
text=True, encoding="utf-8", errors="replace",
)
return {
"returncode": completed.returncode,
"stdout": completed.stdout,
"stderr": completed.stderr,
"timed_out": False,
}


def run_command(
args: list[str],
*,
execute: bool,
runner: CommandRunner = default_subprocess_runner,
cwd: Path | None = None,
timeout_seconds: float | None = None,
) -> dict[str, Any]:
credential_argument = next(
(
argument.split("=", 1)[0].lower()
for argument in args
if argument.split("=", 1)[0].lower() in _LARK_CREDENTIAL_ARGUMENTS
),
None,
)
if credential_argument:
raise ValueError(
f"{credential_argument} must remain in lark-cli auth; "
"the Kanban adapter never accepts inline credentials"
)
command = shlex.join(args)
if not execute:
return {
"command": command,
"executed": False,
"ok": True,
"returncode": None,
"stdout": "",
"stderr": "",
"json": None,
}
try:
result = runner(args, cwd, timeout_seconds)
except subprocess.TimeoutExpired as exc:
return {
"command": command,
"executed": True,
"ok": False,
"returncode": None,
"stdout": str(exc.stdout or ""),
"stderr": str(exc.stderr or ""),
"timed_out": True,
"json": None,
}
except OSError as exc:
return {
"command": command,
"executed": True,
"ok": False,
"returncode": None,
"stdout": "",
"stderr": str(exc),
"json": None,
}
stdout = str(result.get("stdout") or "")
stderr = str(result.get("stderr") or "")
parsed = _parse_json(stdout)
ok = int(result.get("returncode") or 0) == 0 and _parsed_ok(parsed)
return {
"command": command,
"executed": True,
"ok": ok,
"returncode": result.get("returncode"),
"stdout": stdout,
"stderr": stderr,
"json": parsed,
}


def _parse_json(text: str) -> Any:
stripped = text.strip()
if not stripped:
return None
try:
return json.loads(stripped)
except json.JSONDecodeError:
return None


def _parsed_ok(parsed: Any) -> bool:
if parsed is None:
return True
if not isinstance(parsed, dict):
return True
if parsed.get("ok") is False:
return False
if parsed.get("code") not in (None, 0):
return False
return True


def command_error(command_result: dict[str, Any]) -> str:
parsed = command_result.get("json")
if isinstance(parsed, dict):
error = parsed.get("error")
if isinstance(error, dict):
return str(error.get("message") or error)
if error:
return str(error)
if parsed.get("msg") and parsed.get("code") not in (None, 0):
return str(parsed.get("msg"))
stderr = " ".join(str(command_result.get("stderr") or "").split())
if stderr:
return stderr[:OUTPUT_LIMIT]
stdout = " ".join(str(command_result.get("stdout") or "").split())
return stdout[:OUTPUT_LIMIT]
2 changes: 1 addition & 1 deletion loopx/extensions/lark/conversation_identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from .goal_channel_transport import APP_ID_PATTERN, call, json_payload, lark_args
from .identity_shapes import LARK_OPEN_ID_PATTERN as OPEN_ID_PATTERN
from .goal_topic_connections import _profile_ref
from .presentation.kanban import CommandRunner
from .cli_transport import CommandRunner


def identity_ref(*values: str) -> str:
Expand Down
2 changes: 1 addition & 1 deletion loopx/extensions/lark/goal_channel_blocked_notice.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
message_readback_verified,
verified_app_id,
)
from .presentation.kanban import (
from .cli_transport import (
DEFAULT_CLI_BIN,
CommandRunner,
default_subprocess_runner,
Expand Down
5 changes: 1 addition & 4 deletions loopx/extensions/lark/goal_channel_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,7 @@
goal_channel_target_for_name,
read_goal_channel_targets,
)
from .presentation.kanban import (
CommandRunner,
default_subprocess_runner,
)
from .cli_transport import CommandRunner, default_subprocess_runner

FailureStage = Literal[
"lifecycle",
Expand Down
2 changes: 1 addition & 1 deletion loopx/extensions/lark/goal_channel_message_delivery.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
lark_args,
verified_app_id,
)
from .presentation.kanban import CommandRunner
from .cli_transport import CommandRunner


class GoalChannelDeliveryStageError(ValueError):
Expand Down
2 changes: 1 addition & 1 deletion loopx/extensions/lark/goal_channel_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
resolve_bound_goal_channel,
)
from .identity_shapes import require_card_callback_identity
from .presentation.kanban import CommandRunner, default_subprocess_runner
from .cli_transport import CommandRunner, default_subprocess_runner


OPERATION_CARD_ACTION_SCHEMA_VERSION = "loopx_operation_card_action_v0"
Expand Down
6 changes: 4 additions & 2 deletions loopx/extensions/lark/goal_channel_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,15 @@
pin_readback_verified,
verified_app_id,
)
from .presentation.kanban import (
from .cli_transport import (
DEFAULT_CLI_BIN,
CommandRunner,
default_subprocess_runner,
)
from .presentation.kanban import (
LarkKanbanConfig,
build_record_list_command,
default_lark_kanban_config_path,
default_subprocess_runner,
lark_kanban_config_from_payload,
lark_kanban_doctor,
read_lark_kanban_local_config,
Expand Down
6 changes: 4 additions & 2 deletions loopx/extensions/lark/goal_channel_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,13 @@
pin_readback_verified,
verified_app_id,
)
from .presentation.kanban import (
from .cli_transport import (
DEFAULT_CLI_BIN,
CommandRunner,
default_lark_kanban_config_path,
default_subprocess_runner,
)
from .presentation.kanban import (
default_lark_kanban_config_path,
read_lark_kanban_local_config,
save_lark_kanban_board_config,
setup_lark_kanban_board,
Expand Down
2 changes: 1 addition & 1 deletion loopx/extensions/lark/goal_channel_targets.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from ...file_lock import exclusive_file_lock
from .goal_channel_contracts import operation_packet
from .goal_channel_transport import APP_ID_PATTERN, CHAT_ID_PATTERN, OPEN_ID_PATTERN
from .presentation.kanban import DEFAULT_CLI_BIN
from .cli_transport import DEFAULT_CLI_BIN
from .private_json import write_private_json_atomic

GOAL_CHANNEL_TARGETS_SCHEMA_VERSION = "loopx_goal_channel_provider_targets_v0"
Expand Down
2 changes: 1 addition & 1 deletion loopx/extensions/lark/goal_channel_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
LARK_MESSAGE_ID_SEARCH as MESSAGE_ID_PATTERN,
LARK_OPEN_ID_SEARCH as OPEN_ID_PATTERN,
)
from .presentation.kanban import CommandRunner
from .cli_transport import CommandRunner

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")
Expand Down
2 changes: 1 addition & 1 deletion loopx/extensions/lark/goal_topic_batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from ...control_plane.todos.contract import normalize_todo_claimed_by
from .goal_channel_contracts import operation_packet
from .goal_topic_connections import connect_lark_goal_topic
from .presentation.kanban import (
from .cli_transport import (
DEFAULT_CLI_BIN,
CommandRunner,
default_subprocess_runner,
Expand Down
2 changes: 1 addition & 1 deletion loopx/extensions/lark/goal_topic_connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
_connection_routing_modes,
decide_lark_topic_route_event,
)
from .presentation.kanban import (
from .cli_transport import (
DEFAULT_CLI_BIN,
CommandRunner,
default_subprocess_runner,
Expand Down
2 changes: 1 addition & 1 deletion loopx/extensions/lark/periodic_report_delivery.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
goal_channel_target_for_name,
read_goal_channel_targets,
)
from .presentation.kanban import CommandRunner, default_subprocess_runner
from .cli_transport import CommandRunner, default_subprocess_runner
from .presentation.periodic_report import periodic_report_lark_sink_adapter
from ...presentation.public_safety import redact_public_text

Expand Down
Loading
Loading