Rebase onto upstream (a4d95fd) #12

Closed
wylab wants to merge 144 commits from rebase-onto-upstream into main
4 changed files with 18 additions and 40 deletions
Showing only changes of commit 38f33f51a8 - Show all commits
+2 -2
View File
@@ -374,12 +374,12 @@ def gateway(
hooks_config = config.hooks if hasattr(config, 'hooks') else None
hooks_server = None
if hooks_config and hooks_config.enabled and hooks_config.has_tokens:
if hooks_config and hooks_config.enabled:
# Register hook channel
hook_channel = HookChannel(bus)
channels.register_channel("hook", hook_channel)
# Create hooks server
# Create hooks server (checks has_tokens internally)
hooks_server = HooksServer(
host=config.gateway.host,
port=config.gateway.port,
+1 -6
View File
@@ -313,26 +313,21 @@ class GatewayConfig(Base):
class HooksConfig(Base):
"""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)
return bool(self.tokens)
class WebSearchConfig(Base):
+9 -12
View File
@@ -60,19 +60,15 @@ class HooksServer:
Validate auth and return token name if valid, None otherwise.
Checks Authorization: Bearer <token> and X-Hook-Token headers.
"""
# Try Authorization: Bearer <token>
auth = request.headers.get("Authorization", "")
if auth.startswith("Bearer "):
name = self.config.resolve_token(auth[7:])
if name:
return name
token = auth[7:]
else:
# Try X-Hook-Token header
token = request.headers.get("X-Hook-Token", "")
token = request.headers.get("X-Hook-Token", "")
if token:
name = self.config.resolve_token(token)
if name:
return name
return None
return self.config.resolve_token(token) if token else None
async def _handle_health(self, request: web.Request) -> web.Response:
"""Health check endpoint — no auth required."""
@@ -131,12 +127,13 @@ class HooksServer:
response = await asyncio.wait_for(future, timeout=timeout)
return web.json_response({"ok": True, "response": response})
except asyncio.TimeoutError:
self.bus.cancel_correlation(correlation_id)
return web.json_response(
{"ok": False, "error": f"agent did not respond within {timeout}s"},
status=504,
)
except Exception as e:
self.bus.cancel_correlation(correlation_id)
logger.error(f"Hook processing error: {e}")
return web.json_response({"error": "internal error"}, status=500)
finally:
# Clean up correlation on any failure
self.bus.cancel_correlation(correlation_id)
+6 -20
View File
@@ -3,36 +3,22 @@
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."""
def test_hooks_config_resolve_token():
"""resolve_token should return token name for a matching 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
def test_hooks_config_has_tokens():
"""has_tokens should be True if tokens dict is non-empty."""
assert HooksConfig(enabled=True, tokens={"webhook": "secret"}).has_tokens
assert not HooksConfig(enabled=True).has_tokens
assert not HooksConfig(enabled=True, tokens={}).has_tokens