mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
fix(buzz): treat a scalar allowed_users entry as one member pubkey (#6811)
* fix(buzz): treat a scalar allowed_users entry as one member pubkey
BuzzChannel built its allowlist by iterating config.get("allowed_users") directly. A scalar string -- what an allowed_users: $BUZZ_OWNER_PUBKEY reference resolves to -- was iterated character by character, feeding each character to parse_pubkey, whose first non-hex character raised and aborted channel construction; a numeric scalar raised TypeError the same way. The sibling QQ (#6730) and Discord (#6776) connectors already treat a scalar as a single entry.
Parse the value with the shared parse_allowed_users helper via a parse_pubkey coercer: a scalar is one entry, list entries keep their per-entry validation, unusable entries (bad hex, numeric refs, mappings) are dropped with the shared warning instead of crashing, and the deny-by-default contract is unchanged -- an empty or blank value still yields an empty allowlist with start()'s existing deny-all warning.
config.example.yaml states the accepted shape next to the key, matching the mention_free_channels note added by #6776.
* docs(config): split buzz allowed_users note into aligned comment lines
The two-line note on the buzz example entry was emitted with a literal
`/n` where a newline was intended, so the second note never became its
own comment line and the `#`-column alignment shared with the
DENY-BY-DEFAULT lines below was broken. Split it to match the
mention_free_channels sibling shape (review comment on #6811).
This commit is contained in:
parent
bf40985fa8
commit
eecfebd419
@ -49,7 +49,7 @@ from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from app.channels import buzz_nostr
|
||||
from app.channels.allowed_users import parse_channel_ids
|
||||
from app.channels.allowed_users import parse_allowed_users, parse_channel_ids
|
||||
from app.channels.base import Channel
|
||||
from app.channels.buzz_seen_events import BuzzSeenEventStore
|
||||
from app.channels.commands import is_known_channel_command
|
||||
@ -250,6 +250,19 @@ def _is_transient_close(reason: str) -> bool:
|
||||
return not any(marker in text for marker in _PERMANENT_CLOSE_MARKERS)
|
||||
|
||||
|
||||
def _parse_member_pubkey(value: Any) -> str | None:
|
||||
"""Return the member's hex pubkey, or None when the entry is not one.
|
||||
|
||||
Shared parse_allowed_users coerces config entries through this, so a
|
||||
scalar pubkey is one member and an unusable entry is dropped with a
|
||||
warning instead of aborting channel construction.
|
||||
"""
|
||||
try:
|
||||
return buzz_nostr.parse_pubkey(str(value))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _parse_mention_free_channels(mention_free_channels: Any) -> frozenset[str]:
|
||||
"""Parse Buzz mention exemptions without splitting scalar channel IDs."""
|
||||
return parse_channel_ids(
|
||||
@ -277,7 +290,18 @@ class BuzzChannel(Channel):
|
||||
self._workspace_id = urlparse(self._relay_url).netloc
|
||||
self._private_key_raw = str(config.get("private_key", ""))
|
||||
self._keys: buzz_nostr.NostrKeys | None = None # parsed in start() so coincurve stays lazy
|
||||
self._allowed_users = {buzz_nostr.parse_pubkey(v) for v in config.get("allowed_users", []) or []}
|
||||
self._allowed_users = (
|
||||
parse_allowed_users(
|
||||
config.get("allowed_users"),
|
||||
parse_user_id=_parse_member_pubkey,
|
||||
logger=logger,
|
||||
channel_name="buzz",
|
||||
expected_id="a 64-hex or npub1... member pubkey",
|
||||
valid_id_name="member pubkey",
|
||||
warn_on_scalar_separators=True,
|
||||
)
|
||||
or frozenset()
|
||||
)
|
||||
self._require_mention = bool(config.get("require_mention", True))
|
||||
self._mention_free: frozenset[str] = _parse_mention_free_channels(config.get("mention_free_channels"))
|
||||
self._channel_meta: dict[str, dict[str, Any]] = {}
|
||||
|
||||
@ -98,6 +98,43 @@ def test_config_mention_free_channel_scalar_with_separators_warns(caplog):
|
||||
assert "treating it as one literal channel id" in caplog.text
|
||||
|
||||
|
||||
def test_config_allowed_users_accepts_a_single_scalar_pubkey():
|
||||
"""One pubkey scalar is one member entry -- not the characters of it.
|
||||
|
||||
An ``allowed_users: $BUZZ_OWNER_PUBKEY`` style reference resolves to a
|
||||
scalar string. Iterating it character by character fed each character to
|
||||
``parse_pubkey``, whose first non-hex character raised and aborted channel
|
||||
construction. Sibling channels (QQ #6730, Discord) already treat a scalar
|
||||
as a single entry.
|
||||
"""
|
||||
assert _channel(allowed_users=OWNER)._allowed_users == frozenset({OWNER})
|
||||
assert _channel(allowed_users=f" {OWNER} ")._allowed_users == frozenset({OWNER})
|
||||
|
||||
|
||||
def test_config_allowed_users_accepts_a_non_string_scalar_without_crashing():
|
||||
"""An unquoted scalar YAML typed as a number must not abort channel setup.
|
||||
|
||||
An int is not iterable, so constructing the channel raised
|
||||
``TypeError: 'int' object is not iterable``; the value is not a usable
|
||||
pubkey, so the deny-by-default allowlist stays empty.
|
||||
"""
|
||||
assert _channel(allowed_users=123)._allowed_users == frozenset()
|
||||
|
||||
|
||||
def test_config_allowed_users_drops_invalid_entries_with_a_warning(caplog):
|
||||
"""A mix of valid and unusable entries keeps the valid ones, fail-closed."""
|
||||
with caplog.at_level(logging.WARNING, logger="app.channels.buzz"):
|
||||
ch = _channel(allowed_users=[OWNER, "not-a-pubkey", 42])
|
||||
assert ch._allowed_users == frozenset({OWNER})
|
||||
assert "Ignoring allowed_users entry" in caplog.text
|
||||
|
||||
|
||||
def test_config_allowed_users_empty_shapes_still_deny_by_default():
|
||||
"""Unset, blank and empty shapes keep the empty allowlist (deny all)."""
|
||||
for value in (None, "", " ", [], frozenset()):
|
||||
assert _channel(allowed_users=value)._allowed_users == frozenset()
|
||||
|
||||
|
||||
def test_config_rejects_non_websocket_relay_url():
|
||||
with pytest.raises(ValueError):
|
||||
_channel(relay_url="https://buzz.example.com")
|
||||
|
||||
@ -3207,7 +3207,8 @@ run_ownership:
|
||||
# enabled: false
|
||||
# relay_url: wss://buzz.example.com
|
||||
# private_key: $BUZZ_PRIVATE_KEY # hex or nsec1…
|
||||
# allowed_users: [] # pubkeys (hex or npub) allowed to trigger runs.
|
||||
# allowed_users: [] # pubkeys (hex or npub) allowed to trigger runs:
|
||||
# # one pubkey (hex or npub) or a YAML list.
|
||||
# # DENY-BY-DEFAULT (unlike other channels): empty
|
||||
# # means nobody, and DeerFlow logs a startup warning.
|
||||
# require_mention: true # @mention needed in channels
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user