feat: Satelliten — ARIAs Augen & Haende in fremden Netzen (Info-/Gateway-Aussenposten)

Neuer eigenstaendiger Container satellite/ (RVS-Client, network_mode host), den
man in einem beliebigen Netz (Buero etc.) deployt. Gibt ARIA Zugriff auf dieses
Netz ohne Haupt-Stack davor.

- satellite/satellite.py: RVS-Client + Discovery (mDNS/Zeroconf, SSDP/UPnP+DIAL,
  ARP) + Steuerung (dial.launch fuer YouTube-auf-FireTV, wol, http) mit Guards
  (CONTROL_ENABLED + Allowlist + Logging, token-gated, keine offenen Ports).
  Periodisches Re-Announce (sat_hello im Heartbeat) fuer spaet joinende Bridge.
- satellite/: Dockerfile, requirements, docker-compose (host-net), .env.example
  (SATELLITE_LOCATION als Adresse), README.
- rvs: sat_hello/discover/devices/command/result whitelisted.
- bridge: Satelliten-Registry (sat_hello) + Future-Relay (_satellite_request) +
  /internal/satellite + /internal/satellite-list (Muster wie flux).
- brain: Tools satellite_list/devices/command + _dispatch_satellite + Seed-Regel.

Alle py_compile + node -c gruen. Kein APK-Rebuild noetig.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 23:22:30 +02:00
co-authored by Claude Opus 4.8
parent 28361ea97a
commit 071e38f464
12 changed files with 975 additions and 0 deletions
+122
View File
@@ -1105,6 +1105,62 @@ META_TOOLS = [
},
},
},
{
"type": "function",
"function": {
"name": "satellite_list",
"description": (
"Zeigt welche ARIA-Satelliten (Aussenposten-Container in FREMDEN Netzen, "
"z.B. 'Buero') gerade ONLINE sind und was sie koennen (discover / "
"dial.launch / wol / http). Nutze das ZUERST, wenn Stefan etwas 'im "
"Buero' / 'im Netz X' / 'auf dem <Geraet> dort' machen will — so weisst "
"Du welche Netze erreichbar sind."
),
"parameters": {"type": "object", "properties": {}},
},
},
{
"type": "function",
"function": {
"name": "satellite_devices",
"description": (
"Listet die Geraete im Netz eines Satelliten (Fire TV, Chromecast, "
"Smart-TVs, Drucker, NAS, Hosts ...). Nutze es um herauszufinden welches "
"Geraet gemeint ist, BEVOR Du satellite_command aufrufst."
),
"parameters": {
"type": "object",
"properties": {
"satellite": {"type": "string", "description": "Satelliten-Name/Location/ID (z.B. 'Buero')."},
},
"required": ["satellite"],
},
},
},
{
"type": "function",
"function": {
"name": "satellite_command",
"description": (
"Fuehrt eine Aktion auf einem Geraet im Netz eines Satelliten aus. "
"Beispiel YouTube-Video auf Fire TV: action='dial.launch', "
"device='Fire TV', params={'app':'YouTube','v':'<videoId>'}. Weitere "
"Aktionen: 'wol' (params={'mac':'...'}) zum Aufwecken, "
"'http.get'/'http.post' (params={'url':'...'}) fuer lokale Webhooks. "
"Geht nur, wenn der Satellit Steuerung erlaubt (siehe satellite_list)."
),
"parameters": {
"type": "object",
"properties": {
"satellite": {"type": "string", "description": "Satelliten-Name/Location/ID."},
"device": {"type": "string", "description": "Geraet (Name/ID/IP) aus satellite_devices."},
"action": {"type": "string", "description": "z.B. 'dial.launch', 'wol', 'http.get'."},
"params": {"type": "object", "description": "Aktions-Parameter, z.B. {'app':'YouTube','v':'<id>'}."},
},
"required": ["satellite", "action"],
},
},
},
]
@@ -1938,6 +1994,70 @@ class Agent:
return []
return out[-limit:]
def _dispatch_satellite(self, name: str, arguments: dict) -> str:
"""satellite_list / satellite_devices / satellite_command — geht via
Bridge (/internal/satellite*) → RVS → Satellit. Muster wie flux_generate."""
def _post(path: str, body: dict, timeout: float) -> dict:
data = json.dumps(body).encode("utf-8")
req = urllib.request.Request(f"{BRIDGE_URL}{path}", data=data, method="POST",
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode("utf-8", "ignore"))
try:
if name == "satellite_list":
result = _post("/internal/satellite-list", {}, 10)
sats = result.get("satellites") or []
if not sats:
return ("Gerade ist kein Satellit online. (Ein Satellit ist ein "
"Aussenposten-Container in einem fremden Netz, der sich mit "
"RVS verbindet und Dir dort Geraete zeigt + steuerbar macht.)")
lines = []
for s in sats:
status = "online" if s.get("online") else "offline"
caps = ", ".join(s.get("caps") or []) or "nur beobachten"
lines.append(f"- {s.get('location')} (id={s.get('id')}, {status}; kann: {caps})")
return "Satelliten / erreichbare Netze:\n" + "\n".join(lines)
if name == "satellite_devices":
sat = (arguments.get("satellite") or "").strip()
if not sat:
return "FEHLER: satellite ist Pflicht."
result = _post("/internal/satellite", {"op": "discover", "satellite": sat}, 30)
if result.get("error"):
return f"FEHLER: {result['error']}"
devices = result.get("devices") or []
if not devices:
return f"Im Netz von '{sat}' wurden keine Geraete gefunden (oder der Satellit ist offline)."
lines = []
for d in devices[:40]:
tags = []
if d.get("model"):
tags.append(d["model"])
if d.get("dialAppUrl"):
tags.append("DIAL/App-Launch")
if d.get("mac"):
tags.append(d["mac"])
extra = f" ({'; '.join(tags)})" if tags else ""
lines.append(f"- {d.get('name')} [{d.get('type')}] {d.get('ip', '')}{extra} id={d.get('id')}")
return f"Geraete im Netz '{result.get('location', sat)}':\n" + "\n".join(lines)
# satellite_command
sat = (arguments.get("satellite") or "").strip()
action = (arguments.get("action") or "").strip()
if not sat or not action:
return "FEHLER: satellite und action sind Pflicht."
params = arguments.get("params") if isinstance(arguments.get("params"), dict) else {}
result = _post("/internal/satellite", {
"op": "command", "satellite": sat,
"device": arguments.get("device") or "", "action": action, "params": params,
}, 30)
if result.get("ok"):
r = result.get("result")
return f"OK — {r}" if r else "OK — Aktion ausgefuehrt."
return f"FEHLER: {result.get('error') or 'Aktion fehlgeschlagen'}"
except Exception as exc:
return f"FEHLER: Satellit/Bridge nicht erreichbar: {exc}"
def _dispatch_tool(self, name: str, arguments: dict, project_id: str = "") -> str:
"""Fuehrt einen Tool-Call aus und gibt ein kurzes Text-Resultat zurueck.
Niemals werfen — Fehler werden als Text-Resultat reportet damit Claude
@@ -2675,6 +2795,8 @@ class Agent:
f"Editor + Desktop erscheinen in der App. Code-Dateien unter "
f"/shared/projects/{active_id}/ schreiben, damit sie im Editor auftauchen.")
return f"OK — Projekt '{updated['name']}' ist wieder ein normaler Chat."
if name in ("satellite_list", "satellite_devices", "satellite_command"):
return self._dispatch_satellite(name, arguments)
return f"Unbekanntes Tool: {name}"
except Exception as exc:
logger.exception("Tool '%s' fehlgeschlagen", name)
+31
View File
@@ -437,6 +437,37 @@ SEED_RULES: List[dict] = [
"wie gewohnt einfach machen, nicht moralisieren."
),
},
{
"migration_key": "seed/architecture/satellites-remote-networks",
"type": "rule",
"title": "Satelliten: Augen + Haende in fremden Netzen (Buero etc.)",
"category": "architektur",
"content": (
"Ein SATELLIT ist ein eigenstaendiger Container in einem FREMDEN Netz "
"(z.B. Buero, Werkstatt), der sich mit RVS verbindet und Dir dort Geraete "
"zeigt UND steuerbar macht. So erreichst Du Netze, in denen der Haupt-Stack "
"NICHT steht.\n"
"\n"
"Wenn Stefan etwas 'im Buero' / 'im Netz X' / 'auf dem <Geraet> dort' will:\n"
" 1. `satellite_list` — welche Satelliten/Netze sind online + was koennen sie.\n"
" 2. `satellite_devices(satellite='Buero')` — welche Geraete gibt es dort "
"(Fire TV, Chromecast, Smart-TVs, Drucker, NAS ...). Nutze es um das "
"gemeinte Geraet zu finden, BEVOR Du steuerst.\n"
" 3. `satellite_command(...)` — Aktion ausfuehren.\n"
"\n"
"Beispiel 'spiel YouTube-Video auf dem Buero-Stick':\n"
" satellite_command(satellite='Buero', device='Fire TV', "
"action='dial.launch', params={'app':'YouTube','v':'<videoId>'})\n"
"Die YouTube-Video-ID (v=) ziehst Du aus dem Link/Titel (ggf. web_search). "
"Weitere Aktionen: 'wol' (params={'mac':'...'}) zum Aufwecken, "
"'http.get'/'http.post' (params={'url':'...'}) fuer lokale Webhooks.\n"
"\n"
"Adressierung ueber Location/Name des Satelliten ('Buero'), NICHT ueber die "
"Geraete — die identifizieren sich selbst. Steuerung geht nur, wenn der "
"Satellit sie erlaubt (steht in satellite_list als caps). Ist keiner online: "
"sag das ehrlich, statt zu raten."
),
},
{
"migration_key": "seed/architecture/brain-tools-xml-tag",
"type": "rule",