Coverage for core / install_links.py: 87.1%

85 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-12 04:49 +0000

1""" 

2core.install_links — canonical install-link source of truth. 

3 

4Single home for the (target_device, locale) -> install URL mapping. 

5Used by: 

6 - integrations/channels/agent_tools.py::send_install_link (LLM tool) 

7 - docs/downloads.md (human docs) 

8 - landing-page download CTAs (via /api/social/install-links if added) 

9 

10Per CLAUDE.md Gate 2 (DRY): NEVER inline install URLs anywhere else. 

11Import from here. If a URL changes, change ONE line. 

12 

13History: 

14 - 2026-04-28: created. Replaces the legacy 

15 `HevolveAI_Agent_Companion_Setup.exe` ad-hoc string and the 

16 scattered `play.google.com/.../com.hertzai.hevolve` references. 

17 Canonical URLs sourced from HARTOS/docs/downloads.md. 

18 

19Security: 

20 - All URLs in CANONICAL_INSTALL_LINKS are checked into the repo 

21 and reviewed. The `send_install_link` agent tool accepts an 

22 `install_link` override ONLY if the host matches `ALLOWED_HOSTS` 

23 — this prevents prompt-injection from steering users to a 

24 typosquat / phishing URL. 

25""" 

26 

27from __future__ import annotations 

28 

29from typing import Dict, Optional, Tuple 

30from urllib.parse import urlparse 

31 

32 

33# ─── Target devices the agent can hand off to ────────────────────── 

34 

35SUPPORTED_DEVICES: Tuple[str, ...] = ( 

36 'android', 

37 'ios', 

38 'windows', 

39 'macos', 

40 'linux', 

41) 

42 

43# ─── Channels through which install links may be sent ────────────── 

44# 

45# These are the channel_type values accepted by the agent tool. 

46# Must be a subset of the registered channel adapters 

47# (integrations/channels/registry.py); validated at call time. 

48 

49SUPPORTED_INSTALL_CHANNELS: Tuple[str, ...] = ( 

50 'telegram', 

51 'discord', 

52 'whatsapp', 

53 'slack', 

54 'signal', 

55 'web', # crossbar/in-app push 

56 'email', 

57) 

58 

59 

60# ─── Canonical install URLs ───────────────────────────────────────── 

61# 

62# Keyed by (target_device, locale). Locale 'default' is the global 

63# fallback; per-locale entries override only when present (e.g. a 

64# China-mainland mirror, an India-specific Play Store URL). 

65 

66CANONICAL_INSTALL_LINKS: Dict[Tuple[str, str], str] = { 

67 # Windows — GitHub release, Azure-Trusted-Signing-signed 

68 ('windows', 'default'): 

69 'https://github.com/hertz-ai/Nunba/releases/latest/download/Nunba_Setup.exe', 

70 # macOS — notarized DMG 

71 ('macos', 'default'): 

72 'https://github.com/hertz-ai/Nunba/releases/latest/download/Nunba_Setup.dmg', 

73 # Linux — AppImage works on every distro 

74 ('linux', 'default'): 

75 'https://github.com/hertz-ai/Nunba/releases/latest/download/Nunba-x86_64.AppImage', 

76 # Android — Google Play (Hevolve Droid is the user-facing brand) 

77 ('android', 'default'): 

78 'https://play.google.com/store/apps/details?id=com.hertzai.hevolve', 

79 # iOS — TestFlight not yet public; provide a coming-soon page 

80 ('ios', 'default'): 

81 'https://hevolve.ai/ios-coming-soon', 

82} 

83 

84 

85# ─── Allowed hosts for the `install_link` override parameter ─────── 

86# 

87# When the agent / caller supplies a custom URL, it MUST resolve to 

88# one of these hosts. Anything else is rejected as a potential 

89# prompt-injection payload. 

90 

91ALLOWED_HOSTS: Tuple[str, ...] = ( 

92 'github.com', # release artifacts (raw + browser) 

93 'objects.githubusercontent.com', # GitHub's CDN for release assets 

94 'play.google.com', # Android 

95 'apps.apple.com', # iOS App Store (future) 

96 'hevolve.ai', # marketing landing 

97 'docs.hevolve.ai', # docs mirror 

98 'testflight.apple.com', # iOS TestFlight (future) 

99) 

100 

101 

102# ─── Public API ──────────────────────────────────────────────────── 

103 

104def get_install_link( 

105 target_device: str, 

106 locale: str = 'default', 

107) -> Optional[str]: 

108 """Return the canonical install URL for a target device + locale. 

109 

110 Args: 

111 target_device: one of SUPPORTED_DEVICES 

112 locale: BCP-47 language tag (e.g. 'en', 'hi', 'zh') or 'default' 

113 

114 Returns: 

115 URL string, or None if (device, locale) has no entry AND no 

116 'default' fallback exists. 

117 """ 

118 device = (target_device or '').lower().strip() 

119 if device not in SUPPORTED_DEVICES: 

120 return None 

121 

122 # Try locale-specific first, then fall back to 'default' 

123 url = CANONICAL_INSTALL_LINKS.get((device, locale)) 

124 if url: 

125 return url 

126 return CANONICAL_INSTALL_LINKS.get((device, 'default')) 

127 

128 

129def is_allowed_install_link(url: str) -> bool: 

130 """Verify a candidate install URL resolves to an allowed host. 

131 

132 Used to gate the `install_link` override on the agent tool — a 

133 misbehaving or prompt-injected agent MUST NOT be able to send an 

134 arbitrary URL to a user's Telegram/Discord/etc. 

135 

136 Args: 

137 url: candidate URL string 

138 

139 Returns: 

140 True iff the parsed netloc matches one of ALLOWED_HOSTS 

141 (exact match or proper subdomain). 

142 """ 

143 if not isinstance(url, str) or not url: 

144 return False 

145 try: 

146 parsed = urlparse(url) 

147 except Exception: 

148 return False 

149 if parsed.scheme not in ('http', 'https'): 

150 return False 

151 host = (parsed.netloc or '').lower() 

152 # strip port if present 

153 host = host.split(':', 1)[0] 

154 if not host: 

155 return False 

156 for allowed in ALLOWED_HOSTS: 

157 if host == allowed or host.endswith('.' + allowed): 

158 return True 

159 return False 

160 

161 

162def is_supported_device(device: str) -> bool: 

163 """True iff `device` is one of SUPPORTED_DEVICES (case-insensitive).""" 

164 return (device or '').lower().strip() in SUPPORTED_DEVICES 

165 

166 

167def is_supported_install_channel(channel_type: str) -> bool: 

168 """True iff `channel_type` is one of SUPPORTED_INSTALL_CHANNELS.""" 

169 return (channel_type or '').lower().strip() in SUPPORTED_INSTALL_CHANNELS 

170 

171 

172# ─── Custom-scheme deep links (UNIF-G4) ───────────────────────────── 

173# 

174# Distinct from `is_allowed_install_link` (HTTPS-only) above — these 

175# helpers cover the OS-registered custom schemes used by the Nunba 

176# desktop / Android / iOS protocol handlers to receive invite-accept, 

177# meet-join, and group-join intents from external apps (browser, 

178# Telegram, Discord, OS file manager, etc.). 

179# 

180# Schemes (all three accepted on the receive side; builders default 

181# to ``hevolveai`` for the canonical cross-platform link): 

182# - hevolveai:// Nunba desktop registers this since 2024; 

183# canonical for agent-emitted links. 

184# - nunba:// UNIF-G4 brand-canon scheme; iOS + desktop accept. 

185# - hevolve:// Legacy mobile scheme; Android + iOS Info.plist 

186# have registered it since the early Hevolve_RN 

187# builds (referrals, share, campaign, channel). 

188# Recognized here so a single ``invite_link()`` URL 

189# can be valid on every surface — the desktop 

190# protocol handler, Android intent filter, and iOS 

191# URL types all accept the same input. 

192# 

193# Verbs (path[0]): 

194# - invite → /invite/<invite_code> 

195# - meet → /meet/<platform>/<room_id> 

196# - group → /group/<platform>/<group_id> 

197# 

198# Mobile clients use these deep links to route an inbound share-link 

199# tap straight into the matching agent tool (Invite_Friend.accept or 

200# Join_External_Room) without going through a web redirect first. 

201 

202DEEPLINK_SCHEMES: Tuple[str, ...] = ('hevolveai', 'nunba', 'hevolve') 

203DEEPLINK_VERBS: Tuple[str, ...] = ('invite', 'meet', 'group') 

204 

205 

206def is_allowed_deeplink_uri(uri: str) -> bool: 

207 """Validate a custom-scheme deep-link URI. 

208 

209 True iff scheme ∈ DEEPLINK_SCHEMES and the leading path segment is 

210 one of DEEPLINK_VERBS with at least one trailing segment. 

211 """ 

212 if not isinstance(uri, str) or not uri: 

213 return False 

214 try: 

215 parsed = urlparse(uri) 

216 except Exception: 

217 return False 

218 scheme = (parsed.scheme or '').lower() 

219 if scheme not in DEEPLINK_SCHEMES: 

220 return False 

221 # urlparse treats `nunba://invite/X` netloc='invite', path='/X' 

222 # and `nunba:invite/X` path='invite/X'. Normalize. 

223 raw = (parsed.netloc + parsed.path) if parsed.netloc else parsed.path 

224 segments = [s for s in (raw or '').split('/') if s] 

225 if not segments: 

226 return False 

227 verb = segments[0].lower() 

228 if verb not in DEEPLINK_VERBS: 

229 return False 

230 # Per-verb arity: 

231 # invite needs 1 trailing segment (code) → total ≥ 2 

232 # meet / group need 2 trailing segments 

233 # (platform + room/group_id) → total ≥ 3 

234 required_total = 2 if verb == 'invite' else 3 

235 if len(segments) < required_total: 

236 return False 

237 return True 

238 

239 

240def invite_link(invite_code: str, scheme: str = 'hevolveai') -> str: 

241 """Build a custom-scheme deep link for accepting a friend invite.""" 

242 if not invite_code: 

243 raise ValueError("invite_link requires a non-empty invite_code") 

244 s = (scheme or 'hevolveai').lower() 

245 if s not in DEEPLINK_SCHEMES: 

246 raise ValueError(f"unsupported scheme {scheme!r}") 

247 return f"{s}://invite/{invite_code}" 

248 

249 

250def meet_link(platform: str, room_id: str, 

251 scheme: str = 'hevolveai') -> str: 

252 """Build a custom-scheme deep link for joining an external meet.""" 

253 if not platform or not room_id: 

254 raise ValueError("meet_link requires platform + room_id") 

255 s = (scheme or 'hevolveai').lower() 

256 if s not in DEEPLINK_SCHEMES: 

257 raise ValueError(f"unsupported scheme {scheme!r}") 

258 return f"{s}://meet/{platform.lower()}/{room_id}" 

259 

260 

261def group_link(platform: str, group_id: str, 

262 scheme: str = 'hevolveai') -> str: 

263 """Build a custom-scheme deep link for joining an external group.""" 

264 if not platform or not group_id: 

265 raise ValueError("group_link requires platform + group_id") 

266 s = (scheme or 'hevolveai').lower() 

267 if s not in DEEPLINK_SCHEMES: 

268 raise ValueError(f"unsupported scheme {scheme!r}") 

269 return f"{s}://group/{platform.lower()}/{group_id}" 

270 

271 

272# ─── Public OG / share URL builder ──────────────────────────────── 

273# 

274# F1 of memory/feedback_unification_reuse_contract.md — the SINGLE 

275# canonical builder for HTTPS share URLs that crawlers (Discord / 

276# Slack / WhatsApp Web / Twitter / Telegram / LinkedIn / FB) fetch 

277# to render unfurl previews. Every emit site (InviteService, 

278# DistributionService, adapter share-card builders, OG endpoint) 

279# must call ``og_url`` — never inline a string. 

280# 

281# Distinct from the custom-scheme deep-links above: 

282# * ``invite_link / meet_link / group_link`` → ``hevolveai://...`` — 

283# consumed by the OS protocol handler, opens Nunba app directly. 

284# * ``og_url`` → ``https://...`` — consumed by web bots for OG 

285# preview rendering; redirects to SPA / app from the resulting 

286# OG endpoint. 

287# 

288# These are NOT parallel paths: they serve different consumers 

289# (protocol handler vs HTTP bot fetcher). A single emit site 

290# typically uses BOTH (custom-scheme as the click-through CTA inside 

291# an HTTPS preview card). 

292 

293OG_RESOURCE_TYPES: Tuple[str, ...] = ('i', 'c', 'p', 'u') 

294 

295 

296def og_url(resource_type: str, identifier: str, 

297 base_url: Optional[str] = None) -> str: 

298 """Build canonical public share URL for a resource. 

299 

300 Args: 

301 resource_type: one of OG_RESOURCE_TYPES — ``i`` (invite), 

302 ``c`` (community), ``p`` (post), ``u`` (user/agent). 

303 Future expansion: ``m`` (meet), ``g`` (group), ``ch`` 

304 (channel) — these require canonical Meet / Group / 

305 ChannelBinding models first (F8 in the contract). 

306 identifier: the canonical ID for the resource (UUID, slug, 

307 invite_code, or handle). Must be non-empty. 

308 base_url: override. Defaults to 

309 ``HEVOLVE_PUBLIC_BASE_URL`` env or 

310 ``https://hevolve.ai``. 

311 

312 Returns: 

313 ``<base>/<resource_type>/<identifier>``. Example: 

314 ``https://hevolve.ai/i/A1B2C3``. 

315 

316 Raises: 

317 ValueError on unknown ``resource_type`` or empty 

318 ``identifier``. 

319 

320 Single-writer contract: every share-URL emit site in HARTOS + 

321 Nunba calls this function. No parallel implementations. See 

322 ``memory/feedback_unification_reuse_contract.md``. 

323 """ 

324 if resource_type not in OG_RESOURCE_TYPES: 

325 raise ValueError( 

326 f"unsupported resource_type {resource_type!r}; " 

327 f"known: {OG_RESOURCE_TYPES}" 

328 ) 

329 if not identifier: 

330 raise ValueError("og_url requires non-empty identifier") 

331 import os as _os 

332 base = base_url or _os.environ.get( 

333 'HEVOLVE_PUBLIC_BASE_URL', 'https://hevolve.ai' 

334 ) 

335 return f"{base.rstrip('/')}/{resource_type}/{identifier}"