fix(connector): bound numeric env parsing

This commit is contained in:
rookiestar28
2026-04-20 06:09:56 +08:00
parent 68b7040ac7
commit cf74e0c0c0
5 changed files with 419 additions and 65 deletions
+257 -59
View File
@@ -3,12 +3,155 @@ Connector Configuration (F29).
Loads environment variables and validates allowlists.
"""
import logging
import os
import sys
from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, List, Optional, Set
logger = logging.getLogger("ComfyUI-OpenClaw.connector.config")
DEFAULT_DELIVERY_MAX_IMAGES = 4
MIN_DELIVERY_MAX_IMAGES = 1
MAX_DELIVERY_MAX_IMAGES = 16
DEFAULT_DELIVERY_MAX_BYTES = 10 * 1024 * 1024
MIN_DELIVERY_MAX_BYTES = 64 * 1024
MAX_DELIVERY_MAX_BYTES = 50 * 1024 * 1024
DEFAULT_DELIVERY_TIMEOUT_SEC = 600
MIN_DELIVERY_TIMEOUT_SEC = 30
MAX_DELIVERY_TIMEOUT_SEC = 3600
DEFAULT_LINE_BIND_PORT = 8099
DEFAULT_WHATSAPP_BIND_PORT = 8098
DEFAULT_WECHAT_BIND_PORT = 8097
DEFAULT_KAKAO_BIND_PORT = 8096
DEFAULT_SLACK_BIND_PORT = 8095
DEFAULT_FEISHU_BIND_PORT = 8094
MIN_BIND_PORT = 1
MAX_BIND_PORT = 65535
DEFAULT_SLACK_OAUTH_STATE_TTL_SEC = 600
MIN_SLACK_OAUTH_STATE_TTL_SEC = 60
MAX_SLACK_OAUTH_STATE_TTL_SEC = 3600
DEFAULT_RATE_LIMIT_USER_RPM = 10
DEFAULT_RATE_LIMIT_CHANNEL_RPM = 30
MIN_RATE_LIMIT_RPM = 1
MAX_RATE_LIMIT_RPM = 600
DEFAULT_MAX_COMMAND_LENGTH = 4096
MIN_MAX_COMMAND_LENGTH = 128
MAX_MAX_COMMAND_LENGTH = 32768
DEFAULT_MEDIA_TTL_SEC = 300
MIN_MEDIA_TTL_SEC = 60
MAX_MEDIA_TTL_SEC = 86400
DEFAULT_MEDIA_MAX_MB = 8
MIN_MEDIA_MAX_MB = 1
MAX_MEDIA_MAX_MB = 64
def _warn_default_env(
env_key: str, raw_value: str, *, default: int, reason: str
) -> None:
logger.warning(
"Connector env %s=%r %s; using default %s.",
env_key,
raw_value,
reason,
default,
)
def _warn_clamped_env(
env_key: str,
raw_value: str,
*,
bound_name: str,
bound_value: int,
resolved: int,
) -> None:
logger.warning(
(
"Connector env %s=%r is below %s %s; clamped to %s."
if bound_name == "minimum"
else "Connector env %s=%r is above %s %s; clamped to %s."
),
env_key,
raw_value,
bound_name,
bound_value,
resolved,
)
def _load_bounded_int_env(
env_key: str,
*,
default: int,
minimum: Optional[int] = None,
maximum: Optional[int] = None,
clamp: bool = True,
) -> int:
raw_value = os.environ.get(env_key)
if raw_value is None:
return default
raw_value = raw_value.strip()
if not raw_value:
return default
try:
value = int(raw_value)
except (TypeError, ValueError):
_warn_default_env(
env_key,
raw_value,
default=default,
reason="is not a valid integer",
)
return default
if minimum is not None and value < minimum:
if clamp:
_warn_clamped_env(
env_key,
raw_value,
bound_name="minimum",
bound_value=minimum,
resolved=minimum,
)
return minimum
_warn_default_env(
env_key,
raw_value,
default=default,
reason=f"is outside supported range {minimum}..{maximum or 'inf'}",
)
return default
if maximum is not None and value > maximum:
if clamp:
_warn_clamped_env(
env_key,
raw_value,
bound_name="maximum",
bound_value=maximum,
resolved=maximum,
)
return maximum
_warn_default_env(
env_key,
raw_value,
default=default,
reason=f"is outside supported range {minimum or '-inf'}..{maximum}",
)
return default
return value
class CommandClass(str, Enum):
PUBLIC = "public" # status, help, tools
@@ -40,9 +183,9 @@ class ConnectorConfig:
# Results Delivery
delivery_enabled: bool = True
delivery_max_images: int = 4
delivery_max_bytes: int = 10 * 1024 * 1024 # 10MB
delivery_timeout_sec: int = 600
delivery_max_images: int = DEFAULT_DELIVERY_MAX_IMAGES
delivery_max_bytes: int = DEFAULT_DELIVERY_MAX_BYTES
delivery_timeout_sec: int = DEFAULT_DELIVERY_TIMEOUT_SEC
# Telegram
telegram_bot_token: Optional[str] = None
@@ -60,7 +203,7 @@ class ConnectorConfig:
line_allowed_users: List[str] = field(default_factory=list)
line_allowed_groups: List[str] = field(default_factory=list)
line_bind_host: str = "127.0.0.1"
line_bind_port: int = 8099
line_bind_port: int = DEFAULT_LINE_BIND_PORT
line_webhook_path: str = "/line/webhook"
# WhatsApp
@@ -70,7 +213,7 @@ class ConnectorConfig:
whatsapp_phone_number_id: Optional[str] = None
whatsapp_allowed_users: List[str] = field(default_factory=list)
whatsapp_bind_host: str = "127.0.0.1"
whatsapp_bind_port: int = 8098
whatsapp_bind_port: int = DEFAULT_WHATSAPP_BIND_PORT
whatsapp_webhook_path: str = "/whatsapp/webhook"
# WeChat Official Account (R74/S31/F43)
@@ -80,13 +223,13 @@ class ConnectorConfig:
wechat_encoding_aes_key: Optional[str] = None # R82: AES encrypted mode
wechat_allowed_users: List[str] = field(default_factory=list)
wechat_bind_host: str = "127.0.0.1"
wechat_bind_port: int = 8097
wechat_bind_port: int = DEFAULT_WECHAT_BIND_PORT
wechat_webhook_path: str = "/wechat/webhook"
# KakaoTalk (F44 Phase A)
kakao_enabled: bool = False
kakao_bind_host: str = "127.0.0.1"
kakao_bind_port: int = 8096
kakao_bind_port: int = DEFAULT_KAKAO_BIND_PORT
kakao_webhook_path: str = "/kakao/webhook"
kakao_allowed_users: List[str] = field(default_factory=list)
@@ -96,7 +239,7 @@ class ConnectorConfig:
slack_allowed_users: List[str] = field(default_factory=list)
slack_allowed_channels: List[str] = field(default_factory=list)
slack_bind_host: str = "127.0.0.1"
slack_bind_port: int = 8095
slack_bind_port: int = DEFAULT_SLACK_BIND_PORT
slack_webhook_path: str = "/slack/events"
slack_require_mention: bool = True
slack_reply_in_thread: bool = True
@@ -118,7 +261,7 @@ class ConnectorConfig:
"mpim:history",
]
)
slack_oauth_state_ttl_sec: int = 600
slack_oauth_state_ttl_sec: int = DEFAULT_SLACK_OAUTH_STATE_TTL_SEC
# Feishu / Lark (F67)
feishu_app_id: Optional[str] = None
@@ -133,7 +276,7 @@ class ConnectorConfig:
feishu_allowed_users: List[str] = field(default_factory=list)
feishu_allowed_chats: List[str] = field(default_factory=list)
feishu_bind_host: str = "127.0.0.1"
feishu_bind_port: int = 8094
feishu_bind_port: int = DEFAULT_FEISHU_BIND_PORT
feishu_webhook_path: str = "/feishu/events"
feishu_callback_path: str = "/feishu/callback"
feishu_domain: str = "feishu" # feishu | lark
@@ -147,13 +290,19 @@ class ConnectorConfig:
# Media Host (F33)
public_base_url: Optional[str] = None
media_path: str = "/media"
media_ttl_sec: int = 300
media_max_mb: int = 8
media_ttl_sec: int = DEFAULT_MEDIA_TTL_SEC
media_max_mb: int = DEFAULT_MEDIA_MAX_MB
# Security (F32)
rate_limit_user_rpm: int = 10 # Requests per minute per user
rate_limit_channel_rpm: int = 30 # Requests per minute per channel
max_command_length: int = 4096 # Max characters in a single command
rate_limit_user_rpm: int = (
DEFAULT_RATE_LIMIT_USER_RPM # Requests per minute per user
)
rate_limit_channel_rpm: int = (
DEFAULT_RATE_LIMIT_CHANNEL_RPM # Requests per minute per channel
)
max_command_length: int = (
DEFAULT_MAX_COMMAND_LENGTH # Max characters in a single command
)
llm_max_tokens_per_request: int = 1024 # LLM token budget
# R80: Command Auth Policy
@@ -186,14 +335,23 @@ def load_config() -> ConnectorConfig:
cfg.state_path = os.environ.get("OPENCLAW_CONNECTOR_STATE_PATH")
# Delivery
cfg.delivery_max_images = int(
os.environ.get("OPENCLAW_CONNECTOR_DELIVERY_MAX_IMAGES", "4")
cfg.delivery_max_images = _load_bounded_int_env(
"OPENCLAW_CONNECTOR_DELIVERY_MAX_IMAGES",
default=DEFAULT_DELIVERY_MAX_IMAGES,
minimum=MIN_DELIVERY_MAX_IMAGES,
maximum=MAX_DELIVERY_MAX_IMAGES,
)
cfg.delivery_max_bytes = int(
os.environ.get("OPENCLAW_CONNECTOR_DELIVERY_MAX_BYTES", str(10 * 1024 * 1024))
cfg.delivery_max_bytes = _load_bounded_int_env(
"OPENCLAW_CONNECTOR_DELIVERY_MAX_BYTES",
default=DEFAULT_DELIVERY_MAX_BYTES,
minimum=MIN_DELIVERY_MAX_BYTES,
maximum=MAX_DELIVERY_MAX_BYTES,
)
cfg.delivery_timeout_sec = int(
os.environ.get("OPENCLAW_CONNECTOR_DELIVERY_TIMEOUT_SEC", "600")
cfg.delivery_timeout_sec = _load_bounded_int_env(
"OPENCLAW_CONNECTOR_DELIVERY_TIMEOUT_SEC",
default=DEFAULT_DELIVERY_TIMEOUT_SEC,
minimum=MIN_DELIVERY_TIMEOUT_SEC,
maximum=MAX_DELIVERY_TIMEOUT_SEC,
)
# Telegram
@@ -229,9 +387,13 @@ def load_config() -> ConnectorConfig:
cfg.line_allowed_groups = [u.strip() for u in l_groups.split(",") if u.strip()]
cfg.line_bind_host = os.environ.get("OPENCLAW_CONNECTOR_LINE_BIND", "127.0.0.1")
if l_port := os.environ.get("OPENCLAW_CONNECTOR_LINE_PORT"):
if l_port.isdigit():
cfg.line_bind_port = int(l_port)
cfg.line_bind_port = _load_bounded_int_env(
"OPENCLAW_CONNECTOR_LINE_PORT",
default=DEFAULT_LINE_BIND_PORT,
minimum=MIN_BIND_PORT,
maximum=MAX_BIND_PORT,
clamp=False,
)
cfg.line_webhook_path = os.environ.get(
"OPENCLAW_CONNECTOR_LINE_PATH", "/line/webhook"
)
@@ -254,9 +416,13 @@ def load_config() -> ConnectorConfig:
cfg.whatsapp_bind_host = os.environ.get(
"OPENCLAW_CONNECTOR_WHATSAPP_BIND", "127.0.0.1"
)
if wa_port := os.environ.get("OPENCLAW_CONNECTOR_WHATSAPP_PORT"):
if wa_port.isdigit():
cfg.whatsapp_bind_port = int(wa_port)
cfg.whatsapp_bind_port = _load_bounded_int_env(
"OPENCLAW_CONNECTOR_WHATSAPP_PORT",
default=DEFAULT_WHATSAPP_BIND_PORT,
minimum=MIN_BIND_PORT,
maximum=MAX_BIND_PORT,
clamp=False,
)
cfg.whatsapp_webhook_path = os.environ.get(
"OPENCLAW_CONNECTOR_WHATSAPP_PATH", "/whatsapp/webhook"
)
@@ -271,9 +437,13 @@ def load_config() -> ConnectorConfig:
if wc_users := os.environ.get("OPENCLAW_CONNECTOR_WECHAT_ALLOWED_USERS"):
cfg.wechat_allowed_users = [u.strip() for u in wc_users.split(",") if u.strip()]
cfg.wechat_bind_host = os.environ.get("OPENCLAW_CONNECTOR_WECHAT_BIND", "127.0.0.1")
if wc_port := os.environ.get("OPENCLAW_CONNECTOR_WECHAT_PORT"):
if wc_port.isdigit():
cfg.wechat_bind_port = int(wc_port)
cfg.wechat_bind_port = _load_bounded_int_env(
"OPENCLAW_CONNECTOR_WECHAT_PORT",
default=DEFAULT_WECHAT_BIND_PORT,
minimum=MIN_BIND_PORT,
maximum=MAX_BIND_PORT,
clamp=False,
)
cfg.wechat_webhook_path = os.environ.get(
"OPENCLAW_CONNECTOR_WECHAT_PATH", "/wechat/webhook"
)
@@ -283,9 +453,13 @@ def load_config() -> ConnectorConfig:
cfg.kakao_enabled = True
cfg.kakao_bind_host = os.environ.get("OPENCLAW_CONNECTOR_KAKAO_BIND", "127.0.0.1")
if kp := os.environ.get("OPENCLAW_CONNECTOR_KAKAO_PORT"):
if kp.isdigit():
cfg.kakao_bind_port = int(kp)
cfg.kakao_bind_port = _load_bounded_int_env(
"OPENCLAW_CONNECTOR_KAKAO_PORT",
default=DEFAULT_KAKAO_BIND_PORT,
minimum=MIN_BIND_PORT,
maximum=MAX_BIND_PORT,
clamp=False,
)
cfg.kakao_webhook_path = os.environ.get(
"OPENCLAW_CONNECTOR_KAKAO_PATH", "/kakao/webhook"
)
@@ -300,9 +474,13 @@ def load_config() -> ConnectorConfig:
if sc := os.environ.get("OPENCLAW_CONNECTOR_SLACK_ALLOWED_CHANNELS"):
cfg.slack_allowed_channels = [u.strip() for u in sc.split(",") if u.strip()]
cfg.slack_bind_host = os.environ.get("OPENCLAW_CONNECTOR_SLACK_BIND", "127.0.0.1")
if sp := os.environ.get("OPENCLAW_CONNECTOR_SLACK_PORT"):
if sp.isdigit():
cfg.slack_bind_port = int(sp)
cfg.slack_bind_port = _load_bounded_int_env(
"OPENCLAW_CONNECTOR_SLACK_PORT",
default=DEFAULT_SLACK_BIND_PORT,
minimum=MIN_BIND_PORT,
maximum=MAX_BIND_PORT,
clamp=False,
)
cfg.slack_webhook_path = os.environ.get(
"OPENCLAW_CONNECTOR_SLACK_PATH", "/slack/events"
)
@@ -335,11 +513,12 @@ def load_config() -> ConnectorConfig:
]
if parsed_scopes:
cfg.slack_oauth_scopes = parsed_scopes
if slack_oauth_ttl := os.environ.get(
"OPENCLAW_CONNECTOR_SLACK_OAUTH_STATE_TTL_SEC"
):
if slack_oauth_ttl.isdigit():
cfg.slack_oauth_state_ttl_sec = max(60, int(slack_oauth_ttl))
cfg.slack_oauth_state_ttl_sec = _load_bounded_int_env(
"OPENCLAW_CONNECTOR_SLACK_OAUTH_STATE_TTL_SEC",
default=DEFAULT_SLACK_OAUTH_STATE_TTL_SEC,
minimum=MIN_SLACK_OAUTH_STATE_TTL_SEC,
maximum=MAX_SLACK_OAUTH_STATE_TTL_SEC,
)
# Feishu / Lark (F67)
cfg.feishu_app_id = os.environ.get("OPENCLAW_CONNECTOR_FEISHU_APP_ID")
@@ -362,9 +541,13 @@ def load_config() -> ConnectorConfig:
if fc := os.environ.get("OPENCLAW_CONNECTOR_FEISHU_ALLOWED_CHATS"):
cfg.feishu_allowed_chats = [u.strip() for u in fc.split(",") if u.strip()]
cfg.feishu_bind_host = os.environ.get("OPENCLAW_CONNECTOR_FEISHU_BIND", "127.0.0.1")
if fp := os.environ.get("OPENCLAW_CONNECTOR_FEISHU_PORT"):
if fp.isdigit():
cfg.feishu_bind_port = int(fp)
cfg.feishu_bind_port = _load_bounded_int_env(
"OPENCLAW_CONNECTOR_FEISHU_PORT",
default=DEFAULT_FEISHU_BIND_PORT,
minimum=MIN_BIND_PORT,
maximum=MAX_BIND_PORT,
clamp=False,
)
cfg.feishu_webhook_path = os.environ.get(
"OPENCLAW_CONNECTOR_FEISHU_PATH", "/feishu/events"
)
@@ -393,25 +576,40 @@ def load_config() -> ConnectorConfig:
cfg.admin_users = [u.strip() for u in admins.split(",") if u.strip()]
# Security (F32)
if rpm := os.environ.get("OPENCLAW_CONNECTOR_RATE_LIMIT_USER_RPM"):
if rpm.isdigit():
cfg.rate_limit_user_rpm = int(rpm)
if rpm := os.environ.get("OPENCLAW_CONNECTOR_RATE_LIMIT_CHANNEL_RPM"):
if rpm.isdigit():
cfg.rate_limit_channel_rpm = int(rpm)
if max_len := os.environ.get("OPENCLAW_CONNECTOR_MAX_COMMAND_LENGTH"):
if max_len.isdigit():
cfg.max_command_length = int(max_len)
cfg.rate_limit_user_rpm = _load_bounded_int_env(
"OPENCLAW_CONNECTOR_RATE_LIMIT_USER_RPM",
default=DEFAULT_RATE_LIMIT_USER_RPM,
minimum=MIN_RATE_LIMIT_RPM,
maximum=MAX_RATE_LIMIT_RPM,
)
cfg.rate_limit_channel_rpm = _load_bounded_int_env(
"OPENCLAW_CONNECTOR_RATE_LIMIT_CHANNEL_RPM",
default=DEFAULT_RATE_LIMIT_CHANNEL_RPM,
minimum=MIN_RATE_LIMIT_RPM,
maximum=MAX_RATE_LIMIT_RPM,
)
cfg.max_command_length = _load_bounded_int_env(
"OPENCLAW_CONNECTOR_MAX_COMMAND_LENGTH",
default=DEFAULT_MAX_COMMAND_LENGTH,
minimum=MIN_MAX_COMMAND_LENGTH,
maximum=MAX_MAX_COMMAND_LENGTH,
)
# Media Host (F33)
cfg.public_base_url = os.environ.get("OPENCLAW_CONNECTOR_PUBLIC_BASE_URL")
cfg.media_path = os.environ.get("OPENCLAW_CONNECTOR_MEDIA_PATH", "/media")
if ttl := os.environ.get("OPENCLAW_CONNECTOR_MEDIA_TTL_SEC"):
if ttl.isdigit():
cfg.media_ttl_sec = int(ttl)
if mb := os.environ.get("OPENCLAW_CONNECTOR_MEDIA_MAX_MB"):
if mb.isdigit():
cfg.media_max_mb = int(mb)
cfg.media_ttl_sec = _load_bounded_int_env(
"OPENCLAW_CONNECTOR_MEDIA_TTL_SEC",
default=DEFAULT_MEDIA_TTL_SEC,
minimum=MIN_MEDIA_TTL_SEC,
maximum=MAX_MEDIA_TTL_SEC,
)
cfg.media_max_mb = _load_bounded_int_env(
"OPENCLAW_CONNECTOR_MEDIA_MAX_MB",
default=DEFAULT_MEDIA_MAX_MB,
minimum=MIN_MEDIA_MAX_MB,
maximum=MAX_MEDIA_MAX_MB,
)
# R80: Command Auth Policy
import json
+6 -3
View File
@@ -171,7 +171,7 @@ Set the following environment variables (or put them in a `.env` file if you use
- `OPENCLAW_CONNECTOR_SLACK_OAUTH_INSTALL_PATH`: Local install route (default `/slack/install`).
- `OPENCLAW_CONNECTOR_SLACK_OAUTH_CALLBACK_PATH`: Local callback route (default `/slack/oauth/callback`).
- `OPENCLAW_CONNECTOR_SLACK_OAUTH_SCOPES`: Comma-separated bot scopes used for install URL generation.
- `OPENCLAW_CONNECTOR_SLACK_OAUTH_STATE_TTL_SEC`: TTL in seconds for single-use OAuth state tokens (default `600`).
- `OPENCLAW_CONNECTOR_SLACK_OAUTH_STATE_TTL_SEC`: TTL in seconds for single-use OAuth state tokens (default `600`, clamped to `60..3600`).
- `OPENCLAW_CONNECTOR_SLACK_ALLOWED_USERS`: Comma-separated user IDs (e.g. `U12345, U67890`).
- `OPENCLAW_CONNECTOR_SLACK_ALLOWED_CHANNELS`: Comma-separated channel IDs (e.g. `C12345`).
- `OPENCLAW_CONNECTOR_SLACK_BIND`: Host to bind (default `127.0.0.1`).
@@ -206,10 +206,13 @@ Set the following environment variables (or put them in a `.env` file if you use
**Image Delivery:**
- `OPENCLAW_CONNECTOR_DELIVERY_MAX_IMAGES`: Max completed images delivered per job (default `4`, clamped to `1..16`).
- `OPENCLAW_CONNECTOR_DELIVERY_MAX_BYTES`: Per-image delivery cap in bytes (default `10485760`, clamped to `65536..52428800`).
- `OPENCLAW_CONNECTOR_DELIVERY_TIMEOUT_SEC`: Result delivery timeout in seconds (default `600`, clamped to `30..3600`).
- `OPENCLAW_CONNECTOR_PUBLIC_BASE_URL`: Public HTTPS URL of your connector (e.g. `https://your-tunnel.example.com`). Required for sending images.
- `OPENCLAW_CONNECTOR_MEDIA_PATH`: URL path for serving temporary media (default `/media`).
- `OPENCLAW_CONNECTOR_MEDIA_TTL_SEC`: Image expiry in seconds (default `300`).
- `OPENCLAW_CONNECTOR_MEDIA_MAX_MB`: Max image size in MB (default `8`).
- `OPENCLAW_CONNECTOR_MEDIA_TTL_SEC`: Image expiry in seconds (default `300`, clamped to `60..86400`).
- `OPENCLAW_CONNECTOR_MEDIA_MAX_MB`: Max image size in MB (default `8`, clamped to `1..64`).
> **Note:** Media URLs are signed with a secret derived from `OPENCLAW_CONNECTOR_ADMIN_TOKEN` or a random key.
> To ensure URLs remain valid after connector restarts, **you must set `OPENCLAW_CONNECTOR_ADMIN_TOKEN`**.
+6 -2
View File
@@ -140,7 +140,7 @@ Controls the `connector` sidecar process and outbound delivery.
| `OPENCLAW_CONNECTOR_SLACK_OAUTH_INSTALL_PATH` | Slack | Local install route path (default `/slack/install`). |
| `OPENCLAW_CONNECTOR_SLACK_OAUTH_CALLBACK_PATH` | Slack | Local OAuth callback route path (default `/slack/oauth/callback`). |
| `OPENCLAW_CONNECTOR_SLACK_OAUTH_SCOPES` | Slack | Comma-separated bot scopes for install URL generation. |
| `OPENCLAW_CONNECTOR_SLACK_OAUTH_STATE_TTL_SEC` | Slack | TTL for single-use OAuth state tokens (default `600`). |
| `OPENCLAW_CONNECTOR_SLACK_OAUTH_STATE_TTL_SEC` | Slack | TTL for single-use OAuth state tokens (default `600`, clamped to `60..3600`). |
| `OPENCLAW_CONNECTOR_SLACK_ALLOWED_USERS` | Slack | Comma-separated trusted user IDs. |
| `OPENCLAW_CONNECTOR_SLACK_ALLOWED_CHANNELS` | Slack | Comma-separated trusted channel IDs. |
| `OPENCLAW_CONNECTOR_FEISHU_APP_ID` | Feishu/Lark | App ID for the default binding. |
@@ -168,9 +168,13 @@ Connector posture rules:
| Variable | Description |
| :--- | :--- |
| `OPENCLAW_CONNECTOR_DELIVERY_TIMEOUT_SEC` | Timeout (sec) for delivering results to chat (default: `600`). |
| `OPENCLAW_CONNECTOR_DELIVERY_TIMEOUT_SEC` | Timeout (sec) for delivering results to chat (default `600`, clamped to `30..3600`). |
| `OPENCLAW_CONNECTOR_PUBLIC_BASE_URL` | Public base URL for serving images to LINE/Webhooks. |
| `OPENCLAW_CONNECTOR_MEDIA_PATH` | Local directory for staging media files. |
| `OPENCLAW_CONNECTOR_DELIVERY_MAX_IMAGES` | Max completed images delivered per job (default `4`, clamped to `1..16`). |
| `OPENCLAW_CONNECTOR_DELIVERY_MAX_BYTES` | Per-image delivery cap in bytes (default `10485760`, clamped to `65536..52428800`). |
| `OPENCLAW_CONNECTOR_MEDIA_TTL_SEC` | Media expiry in seconds (default `300`, clamped to `60..86400`). |
| `OPENCLAW_CONNECTOR_MEDIA_MAX_MB` | Max staged media size in MB (default `8`, clamped to `1..64`). |
### 2.5 Execution Budgets & Limits
+3 -1
View File
@@ -14,7 +14,9 @@ ROOT = Path(__file__).resolve().parents[1]
class TestR176ImportSafeConfig(unittest.TestCase):
def _run_python(self, script: str, *, env: dict[str, str]) -> subprocess.CompletedProcess:
def _run_python(
self, script: str, *, env: dict[str, str]
) -> subprocess.CompletedProcess:
return subprocess.run(
[sys.executable, "-c", script],
capture_output=True,
+147
View File
@@ -0,0 +1,147 @@
import os
import unittest
from unittest.mock import patch
from connector import config as connector_config
class TestR177ConnectorConfigEnv(unittest.TestCase):
def test_invalid_delivery_envs_fall_back_to_defaults_with_warnings(self):
with patch.dict(
os.environ,
{
"OPENCLAW_CONNECTOR_DELIVERY_MAX_IMAGES": "NaN",
"OPENCLAW_CONNECTOR_DELIVERY_MAX_BYTES": "10mb",
"OPENCLAW_CONNECTOR_DELIVERY_TIMEOUT_SEC": "forever",
},
clear=False,
):
with self.assertLogs(
"ComfyUI-OpenClaw.connector.config", level="WARNING"
) as logs:
cfg = connector_config.load_config()
self.assertEqual(
cfg.delivery_max_images,
connector_config.DEFAULT_DELIVERY_MAX_IMAGES,
)
self.assertEqual(
cfg.delivery_max_bytes,
connector_config.DEFAULT_DELIVERY_MAX_BYTES,
)
self.assertEqual(
cfg.delivery_timeout_sec,
connector_config.DEFAULT_DELIVERY_TIMEOUT_SEC,
)
rendered = "\n".join(logs.output)
self.assertIn("OPENCLAW_CONNECTOR_DELIVERY_MAX_IMAGES", rendered)
self.assertIn("OPENCLAW_CONNECTOR_DELIVERY_MAX_BYTES", rendered)
self.assertIn("OPENCLAW_CONNECTOR_DELIVERY_TIMEOUT_SEC", rendered)
self.assertIn("using default", rendered)
def test_delivery_bounds_are_clamped(self):
with patch.dict(
os.environ,
{
"OPENCLAW_CONNECTOR_DELIVERY_MAX_IMAGES": "0",
"OPENCLAW_CONNECTOR_DELIVERY_MAX_BYTES": str(
connector_config.MAX_DELIVERY_MAX_BYTES + 1
),
"OPENCLAW_CONNECTOR_DELIVERY_TIMEOUT_SEC": "-5",
},
clear=False,
):
with self.assertLogs(
"ComfyUI-OpenClaw.connector.config", level="WARNING"
) as logs:
cfg = connector_config.load_config()
self.assertEqual(
cfg.delivery_max_images,
connector_config.MIN_DELIVERY_MAX_IMAGES,
)
self.assertEqual(
cfg.delivery_max_bytes,
connector_config.MAX_DELIVERY_MAX_BYTES,
)
self.assertEqual(
cfg.delivery_timeout_sec,
connector_config.MIN_DELIVERY_TIMEOUT_SEC,
)
rendered = "\n".join(logs.output)
self.assertIn("clamped", rendered)
self.assertIn("OPENCLAW_CONNECTOR_DELIVERY_MAX_IMAGES", rendered)
self.assertIn("OPENCLAW_CONNECTOR_DELIVERY_MAX_BYTES", rendered)
self.assertIn("OPENCLAW_CONNECTOR_DELIVERY_TIMEOUT_SEC", rendered)
def test_invalid_ports_fall_back_to_defaults(self):
with patch.dict(
os.environ,
{
"OPENCLAW_CONNECTOR_LINE_PORT": "0",
"OPENCLAW_CONNECTOR_SLACK_PORT": "99999",
"OPENCLAW_CONNECTOR_FEISHU_PORT": "not-a-port",
},
clear=False,
):
with self.assertLogs(
"ComfyUI-OpenClaw.connector.config", level="WARNING"
) as logs:
cfg = connector_config.load_config()
self.assertEqual(cfg.line_bind_port, connector_config.DEFAULT_LINE_BIND_PORT)
self.assertEqual(cfg.slack_bind_port, connector_config.DEFAULT_SLACK_BIND_PORT)
self.assertEqual(
cfg.feishu_bind_port,
connector_config.DEFAULT_FEISHU_BIND_PORT,
)
rendered = "\n".join(logs.output)
self.assertIn("OPENCLAW_CONNECTOR_LINE_PORT", rendered)
self.assertIn("OPENCLAW_CONNECTOR_SLACK_PORT", rendered)
self.assertIn("OPENCLAW_CONNECTOR_FEISHU_PORT", rendered)
self.assertIn("using default", rendered)
def test_timeout_and_media_limits_are_bounded(self):
with patch.dict(
os.environ,
{
"OPENCLAW_CONNECTOR_SLACK_OAUTH_STATE_TTL_SEC": "10",
"OPENCLAW_CONNECTOR_MEDIA_TTL_SEC": "999999",
"OPENCLAW_CONNECTOR_MEDIA_MAX_MB": "0",
"OPENCLAW_CONNECTOR_RATE_LIMIT_USER_RPM": "-1",
"OPENCLAW_CONNECTOR_RATE_LIMIT_CHANNEL_RPM": "10000",
"OPENCLAW_CONNECTOR_MAX_COMMAND_LENGTH": "64",
},
clear=False,
):
with self.assertLogs(
"ComfyUI-OpenClaw.connector.config", level="WARNING"
) as logs:
cfg = connector_config.load_config()
self.assertEqual(
cfg.slack_oauth_state_ttl_sec,
connector_config.MIN_SLACK_OAUTH_STATE_TTL_SEC,
)
self.assertEqual(cfg.media_ttl_sec, connector_config.MAX_MEDIA_TTL_SEC)
self.assertEqual(cfg.media_max_mb, connector_config.MIN_MEDIA_MAX_MB)
self.assertEqual(cfg.rate_limit_user_rpm, connector_config.MIN_RATE_LIMIT_RPM)
self.assertEqual(
cfg.rate_limit_channel_rpm,
connector_config.MAX_RATE_LIMIT_RPM,
)
self.assertEqual(
cfg.max_command_length,
connector_config.MIN_MAX_COMMAND_LENGTH,
)
rendered = "\n".join(logs.output)
self.assertIn("OPENCLAW_CONNECTOR_SLACK_OAUTH_STATE_TTL_SEC", rendered)
self.assertIn("OPENCLAW_CONNECTOR_MEDIA_TTL_SEC", rendered)
self.assertIn("OPENCLAW_CONNECTOR_MEDIA_MAX_MB", rendered)
self.assertIn("OPENCLAW_CONNECTOR_RATE_LIMIT_USER_RPM", rendered)
self.assertIn("OPENCLAW_CONNECTOR_RATE_LIMIT_CHANNEL_RPM", rendered)
self.assertIn("OPENCLAW_CONNECTOR_MAX_COMMAND_LENGTH", rendered)
if __name__ == "__main__":
unittest.main()