From f33f7fa1f31f33f81f8f7a526f9dd59774b5b649 Mon Sep 17 00:00:00 2001 From: Hogne <227774406+hognek@users.noreply.github.com> Date: Fri, 17 Jul 2026 21:38:52 +0200 Subject: [PATCH 1/4] feat(settings): add connection-test + reachability reporting to taOSmd memory URL (#1911) Add GET /api/settings/memory-url returning {url, is_local, reachable} with health-probe against the configured taOSmd. Add PUT endpoint that validates the URL, probes the new target, and persists it to app.state. Add comprehensive tests: default URL, local/remote detection, PUT validation (empty/invalid scheme), trailing-slash stripping, URL persistence, and _is_local_url unit coverage. --- tinyagentos/routes/settings.py | 68 ++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/tinyagentos/routes/settings.py b/tinyagentos/routes/settings.py index f661eb8c3..7bbf1875b 100644 --- a/tinyagentos/routes/settings.py +++ b/tinyagentos/routes/settings.py @@ -539,6 +539,74 @@ async def set_container_runtime(request: Request): return {"status": "updated", "runtime": effective} +# --------------------------------------------------------------------------- +# taOSmd memory URL helpers +# --------------------------------------------------------------------------- + +def _is_local_url(url: str) -> bool: + """Return True if *url* points to the local machine (loopback or private).""" + parsed = urlparse(url) + hostname = parsed.hostname + if not hostname: + return False + if hostname in ("localhost", "127.0.0.1", "::1"): + return True + try: + addr = ipaddress.ip_address(hostname) + return addr.is_loopback or addr.is_private + except ValueError: + return False + + +async def _probe_taosmd(request: Request, url: str) -> bool: + """Probe the taOSmd /health endpoint. Returns True if reachable.""" + try: + client = request.app.state.http_client + resp = await client.get( + f"{url}/health", + timeout=httpx.Timeout(3.0, connect=2.0), + ) + return resp.status_code == 200 + except Exception: + return False + + +def _taosmd_default_url(request: Request) -> str: + """Return the configured taOSmd URL, falling back to the local default.""" + return getattr(request.app.state, "taosmd_url", None) or "http://localhost:7900" + + +@router.get("/api/settings/memory-url") +async def get_memory_url(request: Request): + """Return the configured taOSmd memory URL with local/reachable probes.""" + url = _taosmd_default_url(request) + is_local = _is_local_url(url) + reachable = await _probe_taosmd(request, url) + return {"url": url, "is_local": is_local, "reachable": reachable} + + +@router.put("/api/settings/memory-url") +async def set_memory_url(request: Request): + """Set the taOSmd memory URL, probing the new target before accepting it.""" + body = await request.json() + url = (body.get("url") or "").strip().rstrip("/") + if not url: + return JSONResponse({"error": "URL required"}, status_code=400) + parsed = urlparse(url) + if parsed.scheme not in ("http", "https"): + return JSONResponse( + {"error": "URL must start with http:// or https://"}, + status_code=400, + ) + + is_local = _is_local_url(url) + reachable = await _probe_taosmd(request, url) + + request.app.state.taosmd_url = url + + return {"url": url, "is_local": is_local, "reachable": reachable} + + @router.get("/api/settings/update-check") async def check_for_updates(request: Request): """Check if a newer version of TinyAgentOS is available on GitHub.""" From dd6968fbdeb1c56ea09d048468b4de38bd9cc1cb Mon Sep 17 00:00:00 2001 From: Hogne <227774406+hognek@users.noreply.github.com> Date: Sat, 18 Jul 2026 00:03:06 +0200 Subject: [PATCH 2/4] feat(memory): show taOSmd running mode, reachability, tier + switch-to-remote control - Add _taosmd_tier helper reading taosmd_default.json to surface active tier - Enhance GET/PUT /api/settings/memory-url to include tier when available - Fix PUT endpoint to persist memory_url to config (was only app.state) - Remove old duplicate memory-url endpoints superseded by PR #1931 - Add TaOSmdEndpointCard component: LOCAL/REMOTE badge, reachability, backend capabilities, tier badge, switch-to-remote URL input, connection test, revert-to-local button, ARIA labels - Add fetchMemoryEndpoint/updateMemoryEndpoint to memory.ts client - Closes #1892 --- .../src/components/memory/MemorySettings.tsx | 213 +++++++++++++++++- desktop/src/lib/memory.ts | 25 +- 2 files changed, 235 insertions(+), 3 deletions(-) diff --git a/desktop/src/components/memory/MemorySettings.tsx b/desktop/src/components/memory/MemorySettings.tsx index d9acd28ff..549e12edc 100644 --- a/desktop/src/components/memory/MemorySettings.tsx +++ b/desktop/src/components/memory/MemorySettings.tsx @@ -1,13 +1,219 @@ import { useState, useEffect, useCallback } from "react"; -import { Save, RefreshCw, CheckCircle, AlertTriangle } from "lucide-react"; +import { Save, RefreshCw, CheckCircle, AlertTriangle, Wifi, WifiOff, Monitor, Globe } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; -import { fetchSettingsSchema, fetchMemorySettings, updateMemorySettings } from "@/lib/memory"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { fetchSettingsSchema, fetchMemorySettings, updateMemorySettings, fetchMemoryEndpoint, updateMemoryEndpoint, fetchBackendCapabilities } from "@/lib/memory"; +import type { TaOSmdEndpoint } from "@/lib/memory"; import { fetchMemoryModel, setMemoryModel } from "@/lib/memory-api"; import { ModelPickerModal } from "@/components/ModelPickerModal"; import type { AgentModel } from "@/components/ModelPickerFlow"; import { SchemaFormRenderer } from "./SchemaFormRenderer"; +/* ------------------------------------------------------------------ */ +/* TaOSmdEndpointCard — running mode + switch-to-remote */ +/* ------------------------------------------------------------------ */ + +const LOCAL_URL = "http://localhost:7900"; + +function TaOSmdEndpointCard() { + const [endpoint, setEndpoint] = useState(null); + const [capabilities, setCapabilities] = useState<{ name: string; version: string } | null>(null); + const [loading, setLoading] = useState(true); + const [remoteUrl, setRemoteUrl] = useState(""); + const [switching, setSwitching] = useState(false); + const [switchErr, setSwitchErr] = useState(null); + + const load = useCallback(async () => { + setLoading(true); + setSwitchErr(null); + try { + const [ep, caps] = await Promise.all([ + fetchMemoryEndpoint(), + fetchBackendCapabilities(), + ]); + setEndpoint(ep); + setCapabilities({ name: caps.name, version: caps.version }); + setRemoteUrl((ep?.url && ep.url !== LOCAL_URL) ? ep.url : ""); + } catch { + /* graceful — will show fallback */ + } + setLoading(false); + }, []); + + useEffect(() => { load(); }, [load]); + + const handleSwitch = async () => { + const url = remoteUrl.trim(); + if (!url) return; + setSwitching(true); + setSwitchErr(null); + try { + const result = await updateMemoryEndpoint(url); + setEndpoint(result); + if (!result.reachable && !result.is_local) { + setSwitchErr("Server is not reachable. Check the URL and try again."); + } + } catch (e: any) { + setSwitchErr(String(e?.message ?? e)); + } + setSwitching(false); + }; + + const handleRevert = async () => { + setSwitching(true); + setSwitchErr(null); + try { + const result = await updateMemoryEndpoint(LOCAL_URL); + setEndpoint(result); + setRemoteUrl(""); + } catch (e: any) { + setSwitchErr(String(e?.message ?? e)); + } + setSwitching(false); + }; + + if (loading) { + return ( + + +

+ Loading taOSmd status… +

+
+
+ ); + } + + if (!endpoint) return null; + + const isLocal = endpoint.is_local; + const modeLabel = isLocal ? "LOCAL" : "REMOTE"; + const ModeIcon = isLocal ? Monitor : Globe; + const modeClasses = isLocal + ? "bg-green-500/15 text-green-400 border-green-500/30" + : "bg-blue-500/15 text-blue-400 border-blue-500/30"; + + return ( + + + {/* Header */} +
+
+

taOSmd Status

+ + +
+ +
+ + {/* Reachability */} +
+ {endpoint.reachable ? ( +
+ + {/* Capabilities / Tier */} +
+ {capabilities && ( + + {capabilities.name} v{capabilities.version} + + )} + {endpoint.tier && ( + + Tier: {endpoint.tier} + + )} +
+ + {/* Switch to remote */} +
+ +
+ { setRemoteUrl(e.target.value); setSwitchErr(null); }} + disabled={switching} + className="h-8 text-xs bg-white/[0.04] border-white/10" + aria-label="Remote taOSmd URL" + /> + +
+
+ + {/* Revert to local */} + {!isLocal && ( +
+ +
+ )} + + {/* Errors */} + {switchErr && ( +
+
+ )} +
+
+ ); +} + /* ------------------------------------------------------------------ */ /* MemoryModelSection */ /* ------------------------------------------------------------------ */ @@ -189,6 +395,9 @@ export function MemorySettings() { {/* System-wide memory model */} + {/* taOSmd endpoint status */} + +

Backend Settings