personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

pairing: LAN-IP auto-detect with localhost fallback

Teach pairing host URL resolution to prefer an explicit override, then a detected LAN IPv4 when network access is enabled, and otherwise fall back to localhost. This gives pairing and status surfaces a stable effective URL without requiring a manual host URL on the common path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+58 -1
+35 -1
tests/test_pairing_config.py
··· 5 5 6 6 import json 7 7 from pathlib import Path 8 + from unittest.mock import Mock, patch 8 9 9 10 from think.pairing import config 10 11 ··· 40 41 assert config.get_host_url() == "https://example.test/base" 41 42 42 43 43 - def test_pairing_host_url_synthesizes_recorded_convey_port(journal_copy): 44 + def test_pairing_host_url_uses_detected_lan_ip_when_network_access_enabled( 45 + journal_copy, 46 + ): 44 47 payload = _read_config(journal_copy) 45 48 payload["pairing"] = {"host_url": None} 49 + payload["convey"]["allow_network_access"] = True 50 + _write_config(journal_copy, payload) 51 + health_dir = journal_copy / "health" 52 + health_dir.mkdir(parents=True, exist_ok=True) 53 + (health_dir / "convey.port").write_text("6123", encoding="utf-8") 54 + 55 + with patch("think.pairing.config._detect_lan_ipv4", return_value="192.168.1.44"): 56 + assert config.get_host_url() == "http://192.168.1.44:6123" 57 + 58 + 59 + def test_pairing_host_url_uses_localhost_when_network_access_disabled(journal_copy): 60 + payload = _read_config(journal_copy) 61 + payload["pairing"] = {"host_url": None} 62 + payload["convey"]["allow_network_access"] = False 46 63 _write_config(journal_copy, payload) 47 64 health_dir = journal_copy / "health" 48 65 health_dir.mkdir(parents=True, exist_ok=True) 49 66 (health_dir / "convey.port").write_text("6123", encoding="utf-8") 50 67 51 68 assert config.get_host_url() == "http://localhost:6123" 69 + 70 + 71 + def test_pairing_host_url_falls_back_to_localhost_when_lan_detect_fails(journal_copy): 72 + payload = _read_config(journal_copy) 73 + payload["pairing"] = {"host_url": None} 74 + payload["convey"]["allow_network_access"] = True 75 + _write_config(journal_copy, payload) 76 + health_dir = journal_copy / "health" 77 + health_dir.mkdir(parents=True, exist_ok=True) 78 + (health_dir / "convey.port").write_text("6123", encoding="utf-8") 79 + 80 + mock_socket = Mock() 81 + mock_socket.__enter__ = Mock(return_value=mock_socket) 82 + mock_socket.__exit__ = Mock(return_value=None) 83 + mock_socket.connect.side_effect = OSError("boom") 84 + with patch("think.pairing.config.socket.socket", return_value=mock_socket): 85 + assert config.get_host_url() == "http://localhost:6123" 52 86 53 87 54 88 def test_pairing_token_ttl_clamps(journal_copy):
+23
think/pairing/config.py
··· 5 5 6 6 from __future__ import annotations 7 7 8 + import socket 8 9 from typing import Any 9 10 10 11 from think.service import DEFAULT_SERVICE_PORT ··· 28 29 return cleaned or None 29 30 30 31 32 + def _detect_lan_ipv4() -> str | None: 33 + """Return this host's outward-facing IPv4, or None on failure. 34 + 35 + Uses a UDP socket connect to a routable address so the kernel resolves the 36 + outbound interface without sending any packets. 37 + """ 38 + 39 + try: 40 + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: 41 + sock.connect(("8.8.8.8", 80)) 42 + return sock.getsockname()[0] 43 + except OSError: 44 + return None 45 + 46 + 31 47 def get_host_url() -> str: 32 48 configured = _clean_str(_pairing_config().get("host_url")) 33 49 if configured is not None: 34 50 return configured 51 + convey_config = get_config().get("convey", {}) 52 + if convey_config.get("allow_network_access", False): 53 + lan_ipv4 = _detect_lan_ipv4() 54 + if lan_ipv4 is not None: 55 + convey_port = read_service_port("convey") or DEFAULT_SERVICE_PORT 56 + return f"http://{lan_ipv4}:{convey_port}" 35 57 convey_port = read_service_port("convey") or DEFAULT_SERVICE_PORT 36 58 return f"http://localhost:{convey_port}" 37 59 ··· 61 83 "DEFAULT_TOKEN_TTL_SECONDS", 62 84 "MAX_TOKEN_TTL_SECONDS", 63 85 "MIN_TOKEN_TTL_SECONDS", 86 + "_detect_lan_ipv4", 64 87 "get_host_url", 65 88 "get_owner_identity", 66 89 "get_token_ttl_seconds",