feat(config): named tokens for hooks
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -310,7 +310,32 @@ class GatewayConfig(Base):
|
||||
heartbeat: HeartbeatConfig = Field(default_factory=HeartbeatConfig)
|
||||
|
||||
|
||||
class WebSearchConfig(Base):
|
||||
class HooksConfig(BaseModel):
|
||||
"""Webhook endpoint configuration."""
|
||||
enabled: bool = False
|
||||
token: str = "" # Single bearer token (backward compat)
|
||||
tokens: dict[str, str] = Field(default_factory=dict) # Named tokens: {name: secret}
|
||||
path: str = "/hooks" # URL path for the endpoint
|
||||
timeout_seconds: int = 120 # Max time to wait for agent response
|
||||
|
||||
def resolve_token(self, provided: str) -> str | None:
|
||||
"""Return token name if provided secret matches, else None."""
|
||||
# Check named tokens first
|
||||
for name, secret in self.tokens.items():
|
||||
if secret == provided:
|
||||
return name
|
||||
# Fall back to single token
|
||||
if self.token and provided == self.token:
|
||||
return "hook"
|
||||
return None
|
||||
|
||||
@property
|
||||
def has_tokens(self) -> bool:
|
||||
"""True if at least one token is configured."""
|
||||
return bool(self.token) or bool(self.tokens)
|
||||
|
||||
|
||||
class WebSearchConfig(BaseModel):
|
||||
"""Web search tool configuration."""
|
||||
|
||||
api_key: str = "" # Brave Search API key
|
||||
@@ -357,6 +382,7 @@ class Config(BaseSettings):
|
||||
channels: ChannelsConfig = Field(default_factory=ChannelsConfig)
|
||||
providers: ProvidersConfig = Field(default_factory=ProvidersConfig)
|
||||
gateway: GatewayConfig = Field(default_factory=GatewayConfig)
|
||||
hooks: HooksConfig = Field(default_factory=HooksConfig)
|
||||
tools: ToolsConfig = Field(default_factory=ToolsConfig)
|
||||
|
||||
@property
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Tests for HooksConfig with named tokens."""
|
||||
|
||||
from nanobot.config.schema import HooksConfig
|
||||
|
||||
|
||||
def test_hooks_config_single_token_backward_compat():
|
||||
"""Single token string should still work."""
|
||||
config = HooksConfig(enabled=True, token="my-secret")
|
||||
assert config.token == "my-secret"
|
||||
assert config.tokens == {}
|
||||
|
||||
|
||||
def test_hooks_config_named_tokens():
|
||||
"""Named tokens dict should work."""
|
||||
config = HooksConfig(enabled=True, tokens={"gitea": "secret1", "ha": "secret2"})
|
||||
assert config.tokens == {"gitea": "secret1", "ha": "secret2"}
|
||||
|
||||
|
||||
def test_hooks_config_resolve_token_from_named():
|
||||
"""resolve_token should return token name for a matching named token."""
|
||||
config = HooksConfig(enabled=True, tokens={"gitea": "secret1", "ha": "secret2"})
|
||||
assert config.resolve_token("secret1") == "gitea"
|
||||
assert config.resolve_token("secret2") == "ha"
|
||||
assert config.resolve_token("unknown") is None
|
||||
|
||||
|
||||
def test_hooks_config_resolve_token_from_single():
|
||||
"""resolve_token should return 'hook' for the single token."""
|
||||
config = HooksConfig(enabled=True, token="my-secret")
|
||||
assert config.resolve_token("my-secret") == "hook"
|
||||
assert config.resolve_token("wrong") is None
|
||||
|
||||
|
||||
def test_hooks_config_any_token_set():
|
||||
"""has_tokens should be True if either token or tokens is set."""
|
||||
assert HooksConfig(enabled=True, token="x").has_tokens
|
||||
assert HooksConfig(enabled=True, tokens={"a": "b"}).has_tokens
|
||||
assert not HooksConfig(enabled=True).has_tokens
|
||||
Reference in New Issue
Block a user