Merge branch 'main' into pr-1399
This commit is contained in:
18
README.md
18
README.md
@@ -347,7 +347,7 @@ pip install nanobot-ai[matrix]
|
|||||||
"accessToken": "syt_xxx",
|
"accessToken": "syt_xxx",
|
||||||
"deviceId": "NANOBOT01",
|
"deviceId": "NANOBOT01",
|
||||||
"e2eeEnabled": true,
|
"e2eeEnabled": true,
|
||||||
"allowFrom": [],
|
"allowFrom": ["@your_user:matrix.org"],
|
||||||
"groupPolicy": "open",
|
"groupPolicy": "open",
|
||||||
"groupAllowFrom": [],
|
"groupAllowFrom": [],
|
||||||
"allowRoomMentions": false,
|
"allowRoomMentions": false,
|
||||||
@@ -441,14 +441,14 @@ Uses **WebSocket** long connection — no public IP required.
|
|||||||
"appSecret": "xxx",
|
"appSecret": "xxx",
|
||||||
"encryptKey": "",
|
"encryptKey": "",
|
||||||
"verificationToken": "",
|
"verificationToken": "",
|
||||||
"allowFrom": []
|
"allowFrom": ["ou_YOUR_OPEN_ID"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> `encryptKey` and `verificationToken` are optional for Long Connection mode.
|
> `encryptKey` and `verificationToken` are optional for Long Connection mode.
|
||||||
> `allowFrom`: Leave empty to allow all users, or add `["ou_xxx"]` to restrict access.
|
> `allowFrom`: Add your open_id (find it in nanobot logs when you message the bot). Use `["*"]` to allow all users.
|
||||||
|
|
||||||
**3. Run**
|
**3. Run**
|
||||||
|
|
||||||
@@ -478,7 +478,7 @@ Uses **botpy SDK** with WebSocket — no public IP required. Currently supports
|
|||||||
|
|
||||||
**3. Configure**
|
**3. Configure**
|
||||||
|
|
||||||
> - `allowFrom`: Leave empty for public access, or add user openids to restrict. You can find openids in the nanobot logs when a user messages the bot.
|
> - `allowFrom`: Add your openid (find it in nanobot logs when you message the bot). Use `["*"]` for public access.
|
||||||
> - For production: submit a review in the bot console and publish. See [QQ Bot Docs](https://bot.q.qq.com/wiki/) for the full publishing flow.
|
> - For production: submit a review in the bot console and publish. See [QQ Bot Docs](https://bot.q.qq.com/wiki/) for the full publishing flow.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -488,7 +488,7 @@ Uses **botpy SDK** with WebSocket — no public IP required. Currently supports
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"appId": "YOUR_APP_ID",
|
"appId": "YOUR_APP_ID",
|
||||||
"secret": "YOUR_APP_SECRET",
|
"secret": "YOUR_APP_SECRET",
|
||||||
"allowFrom": []
|
"allowFrom": ["YOUR_OPENID"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -527,13 +527,13 @@ Uses **Stream Mode** — no public IP required.
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"clientId": "YOUR_APP_KEY",
|
"clientId": "YOUR_APP_KEY",
|
||||||
"clientSecret": "YOUR_APP_SECRET",
|
"clientSecret": "YOUR_APP_SECRET",
|
||||||
"allowFrom": []
|
"allowFrom": ["YOUR_STAFF_ID"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> `allowFrom`: Leave empty to allow all users, or add `["staffId"]` to restrict access.
|
> `allowFrom`: Add your staff ID. Use `["*"]` to allow all users.
|
||||||
|
|
||||||
**3. Run**
|
**3. Run**
|
||||||
|
|
||||||
@@ -568,6 +568,7 @@ Uses **Socket Mode** — no public URL required.
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"botToken": "xoxb-...",
|
"botToken": "xoxb-...",
|
||||||
"appToken": "xapp-...",
|
"appToken": "xapp-...",
|
||||||
|
"allowFrom": ["YOUR_SLACK_USER_ID"],
|
||||||
"groupPolicy": "mention"
|
"groupPolicy": "mention"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -601,7 +602,7 @@ Give nanobot its own email account. It polls **IMAP** for incoming mail and repl
|
|||||||
**2. Configure**
|
**2. Configure**
|
||||||
|
|
||||||
> - `consentGranted` must be `true` to allow mailbox access. This is a safety gate — set `false` to fully disable.
|
> - `consentGranted` must be `true` to allow mailbox access. This is a safety gate — set `false` to fully disable.
|
||||||
> - `allowFrom`: Leave empty to accept emails from anyone, or restrict to specific senders.
|
> - `allowFrom`: Add your email address. Use `["*"]` to accept emails from anyone.
|
||||||
> - `smtpUseTls` and `smtpUseSsl` default to `true` / `false` respectively, which is correct for Gmail (port 587 + STARTTLS). No need to set them explicitly.
|
> - `smtpUseTls` and `smtpUseSsl` default to `true` / `false` respectively, which is correct for Gmail (port 587 + STARTTLS). No need to set them explicitly.
|
||||||
> - Set `"autoReplyEnabled": false` if you only want to read/analyze emails without sending automatic replies.
|
> - Set `"autoReplyEnabled": false` if you only want to read/analyze emails without sending automatic replies.
|
||||||
|
|
||||||
@@ -874,6 +875,7 @@ MCP tools are automatically discovered and registered on startup. The LLM can us
|
|||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> For production deployments, set `"restrictToWorkspace": true` in your config to sandbox the agent.
|
> For production deployments, set `"restrictToWorkspace": true` in your config to sandbox the agent.
|
||||||
|
> **Change in source / post-`v0.1.4.post3`:** In `v0.1.4.post3` and earlier, an empty `allowFrom` means "allow all senders". In newer versions (including building from source), **empty `allowFrom` denies all access by default**. To allow all senders, set `"allowFrom": ["*"]`.
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
|--------|---------|-------------|
|
|--------|---------|-------------|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ chmod 600 ~/.nanobot/config.json
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Security Notes:**
|
**Security Notes:**
|
||||||
- Empty `allowFrom` list will **ALLOW ALL** users (open by default for personal use)
|
- In `v0.1.4.post3` and earlier, an empty `allowFrom` allows all users. In newer versions (including source builds), **empty `allowFrom` denies all access** — set `["*"]` to explicitly allow everyone.
|
||||||
- Get your Telegram user ID from `@userinfobot`
|
- Get your Telegram user ID from `@userinfobot`
|
||||||
- Use full phone numbers with country code for WhatsApp
|
- Use full phone numbers with country code for WhatsApp
|
||||||
- Review access logs regularly for unauthorized access attempts
|
- Review access logs regularly for unauthorized access attempts
|
||||||
@@ -212,9 +212,8 @@ If you suspect a security breach:
|
|||||||
- Input length limits on HTTP requests
|
- Input length limits on HTTP requests
|
||||||
|
|
||||||
✅ **Authentication**
|
✅ **Authentication**
|
||||||
- Allow-list based access control
|
- Allow-list based access control — in `v0.1.4.post3` and earlier empty means allow all; in newer versions empty means deny all (`["*"]` to explicitly allow all)
|
||||||
- Failed authentication attempt logging
|
- Failed authentication attempt logging
|
||||||
- Open by default (configure allowFrom for production use)
|
|
||||||
|
|
||||||
✅ **Resource Protection**
|
✅ **Resource Protection**
|
||||||
- Command execution timeouts (60s default)
|
- Command execution timeouts (60s default)
|
||||||
|
|||||||
@@ -59,29 +59,17 @@ class BaseChannel(ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def is_allowed(self, sender_id: str) -> bool:
|
def is_allowed(self, sender_id: str) -> bool:
|
||||||
"""
|
"""Check if *sender_id* is permitted. Empty list → deny all; ``"*"`` → allow all."""
|
||||||
Check if a sender is allowed to use this bot.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
sender_id: The sender's identifier.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if allowed, False otherwise.
|
|
||||||
"""
|
|
||||||
allow_list = getattr(self.config, "allow_from", [])
|
allow_list = getattr(self.config, "allow_from", [])
|
||||||
|
|
||||||
# If no allow list, allow everyone
|
|
||||||
if not allow_list:
|
if not allow_list:
|
||||||
|
logger.warning("{}: allow_from is empty — all access denied", self.name)
|
||||||
|
return False
|
||||||
|
if "*" in allow_list:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
sender_str = str(sender_id)
|
sender_str = str(sender_id)
|
||||||
if sender_str in allow_list:
|
return sender_str in allow_list or any(
|
||||||
return True
|
p in allow_list for p in sender_str.split("|") if p
|
||||||
if "|" in sender_str:
|
)
|
||||||
for part in sender_str.split("|"):
|
|
||||||
if part and part in allow_list:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def _handle_message(
|
async def _handle_message(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -149,6 +149,16 @@ class ChannelManager:
|
|||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
logger.warning("Matrix channel not available: {}", e)
|
logger.warning("Matrix channel not available: {}", e)
|
||||||
|
|
||||||
|
self._validate_allow_from()
|
||||||
|
|
||||||
|
def _validate_allow_from(self) -> None:
|
||||||
|
for name, ch in self.channels.items():
|
||||||
|
if getattr(ch.config, "allow_from", None) == []:
|
||||||
|
raise SystemExit(
|
||||||
|
f'Error: "{name}" has empty allowFrom (denies all). '
|
||||||
|
f'Set ["*"] to allow everyone, or add specific user IDs.'
|
||||||
|
)
|
||||||
|
|
||||||
async def _start_channel(self, name: str, channel: BaseChannel) -> None:
|
async def _start_channel(self, name: str, channel: BaseChannel) -> None:
|
||||||
"""Start a channel and log any exceptions."""
|
"""Start a channel and log any exceptions."""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -362,7 +362,11 @@ class MatrixChannel(BaseChannel):
|
|||||||
limit_bytes = await self._effective_media_limit_bytes()
|
limit_bytes = await self._effective_media_limit_bytes()
|
||||||
for path in candidates:
|
for path in candidates:
|
||||||
if fail := await self._upload_and_send_attachment(
|
if fail := await self._upload_and_send_attachment(
|
||||||
msg.chat_id, path, limit_bytes, relates_to):
|
room_id=msg.chat_id,
|
||||||
|
path=path,
|
||||||
|
limit_bytes=limit_bytes,
|
||||||
|
relates_to=relates_to,
|
||||||
|
):
|
||||||
failures.append(fail)
|
failures.append(fail)
|
||||||
if failures:
|
if failures:
|
||||||
text = f"{text.rstrip()}\n{chr(10).join(failures)}" if text.strip() else "\n".join(failures)
|
text = f"{text.rstrip()}\n{chr(10).join(failures)}" if text.strip() else "\n".join(failures)
|
||||||
@@ -450,8 +454,7 @@ class MatrixChannel(BaseChannel):
|
|||||||
await asyncio.sleep(2)
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
async def _on_room_invite(self, room: MatrixRoom, event: InviteEvent) -> None:
|
async def _on_room_invite(self, room: MatrixRoom, event: InviteEvent) -> None:
|
||||||
allow_from = self.config.allow_from or []
|
if self.is_allowed(event.sender):
|
||||||
if not allow_from or event.sender in allow_from:
|
|
||||||
await self.client.join(room.room_id)
|
await self.client.join(room.room_id)
|
||||||
|
|
||||||
def _is_direct_room(self, room: MatrixRoom) -> bool:
|
def _is_direct_room(self, room: MatrixRoom) -> bool:
|
||||||
@@ -676,11 +679,13 @@ class MatrixChannel(BaseChannel):
|
|||||||
parts: list[str] = []
|
parts: list[str] = []
|
||||||
if isinstance(body := getattr(event, "body", None), str) and body.strip():
|
if isinstance(body := getattr(event, "body", None), str) and body.strip():
|
||||||
parts.append(body.strip())
|
parts.append(body.strip())
|
||||||
parts.append(marker)
|
if marker:
|
||||||
|
parts.append(marker)
|
||||||
|
|
||||||
await self._start_typing_keepalive(room.room_id)
|
await self._start_typing_keepalive(room.room_id)
|
||||||
try:
|
try:
|
||||||
meta = self._base_metadata(room, event)
|
meta = self._base_metadata(room, event)
|
||||||
|
meta["attachments"] = []
|
||||||
if attachment:
|
if attachment:
|
||||||
meta["attachments"] = [attachment]
|
meta["attachments"] = [attachment]
|
||||||
await self._handle_message(
|
await self._handle_message(
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ class SlackConfig(Base):
|
|||||||
user_token_read_only: bool = True
|
user_token_read_only: bool = True
|
||||||
reply_in_thread: bool = True
|
reply_in_thread: bool = True
|
||||||
react_emoji: str = "eyes"
|
react_emoji: str = "eyes"
|
||||||
|
allow_from: list[str] = Field(default_factory=list) # Allowed Slack user IDs (sender-level)
|
||||||
group_policy: str = "mention" # "mention", "open", "allowlist"
|
group_policy: str = "mention" # "mention", "open", "allowlist"
|
||||||
group_allow_from: list[str] = Field(default_factory=list) # Allowed channel IDs if allowlist
|
group_allow_from: list[str] = Field(default_factory=list) # Allowed channel IDs if allowlist
|
||||||
dm: SlackDMConfig = Field(default_factory=SlackDMConfig)
|
dm: SlackDMConfig = Field(default_factory=SlackDMConfig)
|
||||||
|
|||||||
Reference in New Issue
Block a user