Skip to content
Closed
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
1 change: 1 addition & 0 deletions hermes_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ def _warn_config_parse_failure(config_path: Path, exc: Exception) -> None:
"SIGNAL_HOME_CHANNEL", "SIGNAL_HOME_CHANNEL_NAME",
"SMS_HOME_CHANNEL", "SMS_HOME_CHANNEL_NAME",
"DINGTALK_CLIENT_ID", "DINGTALK_CLIENT_SECRET",
"DINGTALK_ALLOWED_USERS", "DINGTALK_ALLOW_ALL_USERS",
"DINGTALK_HOME_CHANNEL", "DINGTALK_HOME_CHANNEL_NAME",
"FEISHU_APP_ID", "FEISHU_APP_SECRET", "FEISHU_ENCRYPT_KEY", "FEISHU_VERIFICATION_TOKEN",
"FEISHU_HOME_CHANNEL", "FEISHU_HOME_CHANNEL_NAME",
Expand Down
46 changes: 42 additions & 4 deletions hermes_cli/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
is_managed,
managed_error,
read_raw_config,
remove_env_value,
save_env_value,
)
# display_hermes_home is imported lazily at call sites to avoid ImportError
Expand Down Expand Up @@ -3487,6 +3488,9 @@ def _atexit_hook() -> None:
"help": "The AppKey from your DingTalk application credentials."},
{"name": "DINGTALK_CLIENT_SECRET", "prompt": "AppSecret (Client Secret)", "password": True,
"help": "The AppSecret from your DingTalk application credentials."},
{"name": "DINGTALK_ALLOWED_USERS", "prompt": "Allowed DingTalk user IDs (comma-separated staff_id or sender_id)", "password": False,
"is_allowlist": True,
"help": "Only messages from these DingTalk users will be processed. Use staff_id or sender_id values."},
],
},
{
Expand Down Expand Up @@ -3932,6 +3936,43 @@ def _setup_sms():
_setup_standard_platform(sms_platform)


def _configure_dingtalk_access() -> None:
"""Prompt for DingTalk access control after credentials are configured."""
print()
print_info(" The gateway DENIES all users by default for security.")
print_info(" Enter DingTalk staff_id or sender_id values to create an allowlist,")
print_info(" or leave empty and choose DM pairing or explicit open access next.")
allowed_users = prompt(
" Allowed DingTalk user IDs (comma-separated staff_id or sender_id)",
password=False,
).replace(" ", "")
if allowed_users:
save_env_value("DINGTALK_ALLOWED_USERS", allowed_users)
remove_env_value("DINGTALK_ALLOW_ALL_USERS")
print_success(" Saved — only these DingTalk users can interact with the bot.")
return

access_idx = prompt_choice(
" How should unauthorized DingTalk users be handled?",
[
"Enable open access (any DingTalk user who can reach the bot)",
"Use DM pairing (unknown users request access, you approve with 'hermes pairing approve')",
"Skip for now (bot will deny all users until configured)",
],
1,
)
if access_idx == 0:
save_env_value("DINGTALK_ALLOW_ALL_USERS", "true")
print_warning(" Open access enabled — any DingTalk user who can reach the bot can use it!")
else:
remove_env_value("DINGTALK_ALLOW_ALL_USERS")
if access_idx == 1:
print_success(" DM pairing mode — users will receive a code to request access.")
print_info(" Approve with: hermes pairing approve dingtalk <code>")
else:
print_info(" Skipped — configure later with 'hermes gateway setup'")


def _setup_dingtalk():
"""Configure DingTalk — QR scan (recommended) or manual credential entry."""
from hermes_cli.setup import (
Expand Down Expand Up @@ -3980,15 +4021,12 @@ def _setup_dingtalk():
client_id, client_secret = result
save_env_value("DINGTALK_CLIENT_ID", client_id)
save_env_value("DINGTALK_CLIENT_SECRET", client_secret)
save_env_value("DINGTALK_ALLOW_ALL_USERS", "true")
_configure_dingtalk_access()
print()
print_success(f"{emoji} {label} configured via QR scan!")
else:
# ── Manual entry ──
_setup_standard_platform(dingtalk_platform)
# Also enable allow-all by default for convenience
if get_env_value("DINGTALK_CLIENT_ID"):
save_env_value("DINGTALK_ALLOW_ALL_USERS", "true")


def _setup_wecom():
Expand Down
42 changes: 42 additions & 0 deletions tests/hermes_cli/test_dingtalk_setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import ast
from pathlib import Path


GATEWAY_CLI = Path(__file__).resolve().parents[2] / "hermes_cli" / "gateway.py"


def _function_calls(function_name: str):
tree = ast.parse(GATEWAY_CLI.read_text(encoding="utf-8"))
for node in tree.body:
if isinstance(node, ast.FunctionDef) and node.name == function_name:
return [child for child in ast.walk(node) if isinstance(child, ast.Call)]
raise AssertionError(f"Function not found: {function_name}")


def test_dingtalk_qr_setup_does_not_silently_enable_allow_all():
calls = _function_calls("_setup_dingtalk")

silent_allow_all_writes = []
for call in calls:
if not isinstance(call.func, ast.Name) or call.func.id != "save_env_value":
continue
if len(call.args) < 2:
continue
key, value = call.args[:2]
if (
isinstance(key, ast.Constant)
and key.value == "DINGTALK_ALLOW_ALL_USERS"
and isinstance(value, ast.Constant)
and value.value == "true"
):
silent_allow_all_writes.append(call.lineno)

assert silent_allow_all_writes == []


def test_dingtalk_setup_includes_access_control_prompting():
source = GATEWAY_CLI.read_text(encoding="utf-8")

assert '"name": "DINGTALK_ALLOWED_USERS"' in source
assert '"is_allowlist": True' in source
assert "_configure_dingtalk_access()" in source
2 changes: 1 addition & 1 deletion website/docs/user-guide/messaging/dingtalk.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ hermes gateway setup

Select **DingTalk** when prompted. The setup wizard can authorize via one of two paths:

- **QR-code device flow (recommended).** Scan the QR that prints in your terminal with the DingTalk mobile app — your Client ID and Client Secret are returned automatically and written to `~/.hermes/.env`. No developer-console trip needed.
- **QR-code device flow (recommended).** Scan the QR that prints in your terminal with the DingTalk mobile app — your Client ID and Client Secret are returned automatically and written to `~/.hermes/.env`. No developer-console trip needed. After credentials are saved, the wizard asks you to configure `DINGTALK_ALLOWED_USERS`, DM pairing, or explicit open access; it does not silently enable allow-all access.
- **Manual paste.** If you already have credentials (or QR scanning isn't convenient), paste your Client ID, Client Secret, and allowed user IDs when prompted.

:::note openClaw branding disclosure
Expand Down
Loading