diff --git a/.gitignore b/.gitignore index 45cb80a..b528b7d 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,7 @@ VectorSnapshots/ .DS_Store Thumbs.db desktop.ini + +# ---Docker Configs and Volumes --- +vector-data/ + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..26ac536 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,126 @@ +FROM ubuntu:22.04 + +# Prevent interactive prompts during package installation +ENV DEBIAN_FRONTEND=noninteractive + +# 1. Install system dependencies +RUN apt-get update && apt-get install -y \ + curl \ + git \ + python3 \ + python3-venv \ + python3-pip \ + unzip \ + build-essential \ + cmake \ + avahi-daemon \ + dbus \ + sudo \ + zstd \ + pkg-config \ + libopus-dev \ + libopusfile-dev \ + && rm -rf /var/lib/apt/lists/* + +# 2. Install Go 1.22.4 +ENV GO_VERSION=1.22.4 +RUN curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" -o /tmp/go.tar.gz && \ + tar -C /usr/local -xzf /tmp/go.tar.gz && \ + rm /tmp/go.tar.gz +ENV PATH="/usr/local/go/bin:${PATH}" + +# 3. Install Ollama +RUN curl -fsSL https://ollama.com/install.sh | sh + +# 4. Set up the working directory and copy project files +WORKDIR /root/vector-pod +COPY . /root/vector-intelligence + +# 5. Build Wire-Pod (Chipper) +ENV WIREPOD_REPO="https://github.com/kercre123/wire-pod" +ENV WIREPOD_DIR="/root/vector-pod/wire-pod" +ENV WIREPOD_COMMIT="11e7b22095166ed35765e88a8a10ed3a6ce49d5c" +ENV WHISPER_COMMIT="60cd96acff3a72895cb9ae9cbabe9de21b1e9125" +ENV SDK_COMMIT="62168f3595d67ae0bf24103a9fe1fc5f2eb9b85c" + +RUN git clone "$WIREPOD_REPO" "$WIREPOD_DIR" && \ + cd "$WIREPOD_DIR" && git checkout -q "$WIREPOD_COMMIT" + +# Apply Python-based patches to Wire-Pod +RUN cp /root/vector-intelligence/shared/config/wirepod-intents-en-US.json $WIREPOD_DIR/chipper/intent-data/en-US.json && \ + sed -i -E 's|inactiveNumMax := (23\|150\|100\|75)[^\r\n]*|inactiveNumMax := 75 // 1.5s of silence|' $WIREPOD_DIR/chipper/pkg/wirepod/speechrequest/speechrequest.go && \ + python3 /root/vector-intelligence/shared/patches/expand-animations.py $WIREPOD_DIR/chipper/pkg/wirepod/ttr/kgsim_cmds.go && \ + python3 /root/vector-intelligence/shared/patches/wake-word-grace-period.py $WIREPOD_DIR/chipper/pkg/wirepod/ttr/kgsim_interrupt.go && \ + python3 /root/vector-intelligence/shared/patches/add-button-interrupt.py $WIREPOD_DIR/chipper/pkg/wirepod/ttr/kgsim_interrupt.go && \ + python3 /root/vector-intelligence/shared/patches/wake-word-mute-during-getimage.py $WIREPOD_DIR && \ + python3 /root/vector-intelligence/shared/patches/add-ondemand-face.py $WIREPOD_DIR/chipper/pkg/wirepod/ttr/kgsim_interrupt.go && \ + python3 /root/vector-intelligence/shared/patches/remove-photo-countdown.py $WIREPOD_DIR/chipper/pkg/wirepod/ttr/kgsim_cmds.go && \ + python3 /root/vector-intelligence/shared/patches/use-builtin-behaviors.py $WIREPOD_DIR/chipper/pkg/wirepod/ttr/kgsim_cmds.go && \ + python3 /root/vector-intelligence/shared/patches/prelim-lookatme-then-llm.py $WIREPOD_DIR/chipper/pkg/wirepod/preqs/intent_graph.go && \ + python3 /root/vector-intelligence/shared/patches/slow-tts.py $WIREPOD_DIR/chipper/pkg/wirepod/ttr/kgsim_cmds.go && \ + python3 /root/vector-intelligence/shared/patches/add-eye-color-cmd.py $WIREPOD_DIR/chipper/pkg/wirepod/ttr/kgsim_cmds.go && \ + python3 /root/vector-intelligence/shared/patches/add-sensor-reactions.py $WIREPOD_DIR && \ + python3 /root/vector-intelligence/shared/patches/fix-connection-leak.py $WIREPOD_DIR/chipper/pkg/wirepod/ttr/kgsim.go && \ + python3 /root/vector-intelligence/shared/patches/fix-saytext-stream-leak.py $WIREPOD_DIR/chipper/pkg/wirepod/ttr/bcontrol.go && \ + python3 /root/vector-intelligence/shared/patches/fix-name-extraction.py $WIREPOD_DIR/chipper/pkg/wirepod/ttr/intentparam.go && \ + python3 /root/vector-intelligence/shared/patches/add-face-probe.py $WIREPOD_DIR && \ + python3 /root/vector-intelligence/shared/patches/add-ambient-loop.py $WIREPOD_DIR && \ + sed -i 's/vars.APIConfig.Server.EPConfig && //g' $WIREPOD_DIR/chipper/pkg/initwirepod/startserver.go + +# Patched vector-go-sdk +RUN git clone "https://github.com/fforchino/vector-go-sdk" "$WIREPOD_DIR/chipper/third_party/vector-go-sdk" && \ + cd "$WIREPOD_DIR/chipper/third_party/vector-go-sdk" && git checkout -q "$SDK_COMMIT" && \ + python3 /root/vector-intelligence/shared/patches/add-sdk-close.py "$WIREPOD_DIR/chipper/third_party/vector-go-sdk/pkg/vector/vector.go" && \ + cd "$WIREPOD_DIR/chipper" && echo "\nreplace github.com/fforchino/vector-go-sdk => ./third_party/vector-go-sdk\n" >> go.mod + +# Build Whisper.cpp and chipper-whisper +ARG WHISPER_MODEL="base.en" +ENV WHISPER_MODEL="${WHISPER_MODEL}" +ENV WHISPER_REPO="$WIREPOD_DIR/whisper.cpp" +RUN git clone https://github.com/kercre123/whisper.cpp.git "$WHISPER_REPO" && \ + cd "$WHISPER_REPO" && git checkout -q "$WHISPER_COMMIT" && \ + cmake -B build_go -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DWHISPER_BUILD_EXAMPLES=OFF -DWHISPER_BUILD_TESTS=OFF && \ + cmake --build build_go --config Release -j 4 + +RUN curl -L -o "$WHISPER_REPO/models/ggml-${WHISPER_MODEL}.bin" "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-${WHISPER_MODEL}.bin" + +# Install libwhisper and ggml globally so the executable can find it at runtime without LD_LIBRARY_PATH +RUN cp $WHISPER_REPO/build_go/src/libwhisper.so /usr/local/lib/ && \ + cp $WHISPER_REPO/build_go/ggml/src/libggml*.so /usr/local/lib/ && \ + ldconfig + +RUN cd "$WIREPOD_DIR/chipper" && \ + CGO_CFLAGS="-I$WHISPER_REPO/include -I$WHISPER_REPO/ggml/include" \ + CGO_LDFLAGS="-L/usr/local/lib -lwhisper -lggml" \ + go build -o chipper-whisper ./cmd/experimental/whisper.cpp + +# 5b. Cross-compile the mDNS reflector for Windows +# This small .exe runs natively on Windows to bridge mDNS (multicast) +# between the Docker container and the physical LAN — working around +# WSL2's inability to forward multicast traffic. +RUN cd "$WIREPOD_DIR/chipper" && \ + CGO_ENABLED=0 GOOS=windows GOARCH=amd64 \ + go build -o /root/vector-pod/windows-mdns.exe \ + /root/vector-intelligence/shared/mdns-reflector.go + + +# 6. Set up vector-ai python environment +ENV VECTORAI_DIR="/root/vector-pod/vector-ai" +RUN mkdir -p "$VECTORAI_DIR" && \ + cp /root/vector-intelligence/shared/vector-ai/service.py "$VECTORAI_DIR/service.py" && \ + cp /root/vector-intelligence/shared/vector-ai/memory.py "$VECTORAI_DIR/memory.py" && \ + cp /root/vector-intelligence/shared/vector-ai/requirements.txt "$VECTORAI_DIR/requirements.txt" && \ + cp /root/vector-intelligence/shared/supervisor.py "/root/vector-pod/supervisor.py" && \ + cp /root/vector-intelligence/shared/vector-ai/.env "$VECTORAI_DIR/.env" && \ + cp /root/vector-intelligence/shared/vector-ai/persona.txt "$VECTORAI_DIR/persona.txt" + +RUN python3 -m venv "$VECTORAI_DIR/venv" && \ + "$VECTORAI_DIR/venv/bin/pip" install --upgrade pip && \ + "$VECTORAI_DIR/venv/bin/pip" install -r "$VECTORAI_DIR/requirements.txt" + +# Provide entrypoint +COPY docker-entrypoint.sh /root/docker-entrypoint.sh +RUN chmod +x /root/docker-entrypoint.sh + +# The entrypoint will start dbus/avahi and then supervisor.py +ENTRYPOINT ["/root/docker-entrypoint.sh"] diff --git a/README.md b/README.md index 18db4ad..4b26acb 100644 --- a/README.md +++ b/README.md @@ -227,6 +227,61 @@ bash apply-wirepod-config.sh # apply the AI config # pair Vector via the Robots tab ``` +### Docker (Windows & Linux) + +You can run the entire stack (Wire-Pod, vector-ai, and Ollama) using Docker via the included `docker-compose.yml`. + +#### Windows Setup (Docker Desktop) +The most reliable way to run this stack on Windows is using **Docker Desktop** with the WSL2 backend. + +**Important:** Docker on Windows (WSL2) cannot forward mDNS multicast traffic +to the physical LAN. The build automatically cross-compiles a tiny +`windows-mdns.exe` that runs natively on Windows and bridges this gap — Vector +discovers the server through it. + +1. Install [Docker Desktop](https://www.docker.com/products/docker-desktop/) (ensure the WSL2 backend is selected). +2. Build and start: + ```bash + docker compose build + docker compose up -d + ``` +3. On the **very first start**, the container exports `windows-mdns.exe` into + your `./vector-data/` folder. Run it (double-click or from a terminal): + ```powershell + .\vector-data\windows-mdns.exe + ``` + Keep this window open — it advertises `escapepod.local` on your LAN so + Vector can find the Docker container. It also auto-detects Vector's IP + changes and updates `botSdkInfo.json`. +4. **Initial Bot Setup & Authentication Note (Windows Docker):** + Performing initial bot pairing and authentication wizard directly through the web UI in Docker Desktop on Windows has known limitations. The recommended onboarding path on Windows is: + - Download the official native [Wire-Pod for Windows](https://github.com/kercre123/wire-pod). + - Run native Wire-Pod once to complete initial setup and authenticate/pair Vector. + - Copy the generated folders `session-certs`, `certs`, and `jdocs` from `C:\Users\\AppData\Roaming\wire-pod` directly into the `./vector-data/` folder of this Docker project. +5. **Cleaning Windows Firewall Rules (Optional):** + On first execution, `windows-mdns.exe` prompts for Windows Defender Firewall permission so it can answer mDNS queries on UDP 5353. If you ever uninstall the project and want to clean up those firewall rules, run the provided script in PowerShell (as Administrator): + ```powershell + .\shared\scripts\remove-windows-mdns-rule.ps1 + ``` + Or execute: `Remove-NetFirewallRule -DisplayName "windows-mdns"` + +#### Linux Setup +On Linux, Docker can access the host network directly — no mDNS reflector binary needed. +1. In `docker-compose.yml`, uncomment `network_mode: "host"` and comment out the `ports:` block. +2. In host network mode, Docker shares the host's physical network adapter (`eth0`/`wlan0`) directly. The internal `zeroconf` service inside the container advertises `escapepod.local` across your physical LAN natively. +3. Ensure the NVIDIA Container Toolkit is installed so Docker can access your GPU: + - [Official NVIDIA Container Toolkit install guide](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html). + +#### Customizing the Whisper Speech Model +By default, the Docker image builds with the English base model (`base.en`). You can change the model size (e.g. `tiny.en`, `base.en`, `small.en`, `medium.en`) when building or running Docker: + +- Pass `WHISPER_MODEL` when building or running `docker compose`: + ```bash + WHISPER_MODEL=small.en docker compose build --no-cache + WHISPER_MODEL=small.en docker compose up -d + ``` + + --- ## Daily operation diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3ba5bae --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,34 @@ +services: + vector-intelligence: + build: + context: . + args: + WHISPER_MODEL: ${WHISPER_MODEL:-base.en} + container_name: vector-intelligence + ports: + - "80:80" + - "443:443" + - "8080:8080" + - "8084:8084" + - "8090:8090" + - "11434:11434" + + environment: + - WHISPER_MODEL=${WHISPER_MODEL:-base.en} + - OLLAMA_MAIN_MODEL=${OLLAMA_MAIN_MODEL:-gemma4:e4b} + - OLLAMA_MODEL=${OLLAMA_MAIN_MODEL:-gemma4:e4b} + - OLLAMA_SUMMARY_MODEL=${OLLAMA_SUMMARY_MODEL:-gemma4:e2b} + volumes: + - ollama_data:/root/.ollama + - ./vector-data:/user-data + restart: unless-stopped + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: 1 + capabilities: [gpu] + +volumes: + ollama_data: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..d112c82 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,106 @@ +#!/bin/bash +set -e + +echo "Starting dbus and avahi-daemon for mDNS..." +mkdir -p /var/run/dbus + +# Disable the supervisor's Python-level mDNS — on Docker/Windows, the native +# windows-mdns.exe handles LAN mDNS. Without this, the supervisor would +# advertise the container's internal 172.x IP, confusing Vector. +export DISABLE_SUPERVISOR_MDNS="true" +export WHISPER_MODEL="${WHISPER_MODEL:-base.en}" + +# Remove old PID if restarting container +rm -f /run/dbus/pid +dbus-daemon --system +# avahi-daemon disabled — on Docker/Windows, windows-mdns.exe handles mDNS. +# avahi would broadcast the container's 172.x IP, confusing Vector. +# avahi-daemon -D + +# Safely manage user data in the bind mount +mkdir -p /user-data/jdocs + +# Provide the Windows mDNS reflector .exe on first run +# (pre-compiled during Docker build for Windows users) +if [ -f /root/vector-pod/windows-mdns.exe ] && [ ! -f /user-data/windows-mdns.exe ]; then + cp /root/vector-pod/windows-mdns.exe /user-data/windows-mdns.exe + echo "Exported windows-mdns.exe to /user-data/ (run it on Windows to bridge mDNS)" +fi + + +# Vector-AI persona +if [ ! -f /user-data/persona.txt ]; then + cp /root/vector-pod/vector-ai/persona.txt /user-data/persona.txt +fi +rm -f /root/vector-pod/vector-ai/persona.txt +ln -s /user-data/persona.txt /root/vector-pod/vector-ai/persona.txt + +# Wire-Pod botSdkInfo.json +if [ -f /root/vector-pod/wire-pod/chipper/jdocs/botSdkInfo.json ] && [ ! -f /user-data/jdocs/botSdkInfo.json ]; then + cp /root/vector-pod/wire-pod/chipper/jdocs/botSdkInfo.json /user-data/jdocs/botSdkInfo.json +fi +rm -rf /root/vector-pod/wire-pod/chipper/jdocs +ln -s /user-data/jdocs /root/vector-pod/wire-pod/chipper/jdocs + +# Import certs if provided +if [ -d /user-data/certs ]; then + rm -rf /root/vector-pod/wire-pod/certs /root/vector-pod/wire-pod/chipper/certs + ln -s /user-data/certs /root/vector-pod/wire-pod/certs + ln -s /user-data/certs /root/vector-pod/wire-pod/chipper/certs +fi + +# Import old session-certs if provided +if [ -d /user-data/session-certs ]; then + rm -rf /root/vector-pod/wire-pod/chipper/session-certs + ln -s /user-data/session-certs /root/vector-pod/wire-pod/chipper/session-certs +fi + +# Import old botConfig.txt if provided +if [ -f /user-data/botConfig.txt ]; then + rm -f /root/vector-pod/wire-pod/chipper/botConfig.txt + ln -s /user-data/botConfig.txt /root/vector-pod/wire-pod/chipper/botConfig.txt +fi + +# Persist apiConfig.json (Wire-Pod's main config with setup flags) +if [ ! -f /user-data/apiConfig.json ]; then + if [ -f /root/vector-intelligence/shared/config/wirepod-apiConfig.json ]; then + cp /root/vector-intelligence/shared/config/wirepod-apiConfig.json /user-data/apiConfig.json + elif [ -f /root/vector-pod/wire-pod/chipper/apiConfig.json ]; then + cp /root/vector-pod/wire-pod/chipper/apiConfig.json /user-data/apiConfig.json + fi +fi +rm -f /root/vector-pod/wire-pod/chipper/apiConfig.json +ln -s /user-data/apiConfig.json /root/vector-pod/wire-pod/chipper/apiConfig.json + +# Vector-AI memory db +if [ ! -f /user-data/memory.db ]; then + touch /user-data/memory.db +fi +ln -sf /user-data/memory.db /root/vector-pod/vector-ai/memory.db + +# Set default values if empty +OLLAMA_MAIN_MODEL=${OLLAMA_MAIN_MODEL:-"gemma3:12b"} +OLLAMA_SUMMARY_MODEL=${OLLAMA_SUMMARY_MODEL:-"llama3.2:3b"} + + +echo "Ensuring Ollama models ($OLLAMA_MAIN_MODEL, $OLLAMA_SUMMARY_MODEL) are downloaded..." +# Start Ollama temporarily to pull models +ollama serve > /dev/null 2>&1 & +OLLAMA_PID=$! + +echo "Waiting for Ollama to start..." +until curl -s http://127.0.0.1:11434 > /dev/null; do + sleep 1 +done + +echo "Pulling models..." +ollama pull "$OLLAMA_MAIN_MODEL" +ollama pull "$OLLAMA_SUMMARY_MODEL" + +# Stop temporary Ollama process so supervisor can manage it +kill $OLLAMA_PID +wait $OLLAMA_PID 2>/dev/null || true + +echo "Starting Vector Pod Supervisor..." +cd /root/vector-pod +exec /root/vector-pod/vector-ai/venv/bin/python supervisor.py diff --git a/shared/config/wirepod-apiConfig.json b/shared/config/wirepod-apiConfig.json index 32ecb3b..1f4b025 100644 --- a/shared/config/wirepod-apiConfig.json +++ b/shared/config/wirepod-apiConfig.json @@ -23,11 +23,13 @@ "temp": 1.2 }, "STT": { - "provider": "vosk", + "provider": "whisper.cpp", "language": "en-US" }, "server": { - "epconfig": true, + "epconfig": false, "port": "443" - } + }, + "hasreadfromenv": false, + "pastinitialsetup": true } diff --git a/shared/mdns-reflector.go b/shared/mdns-reflector.go new file mode 100644 index 0000000..99e615e --- /dev/null +++ b/shared/mdns-reflector.go @@ -0,0 +1,254 @@ +// mdns-reflector: standalone mDNS bridge for Windows hosts running +// VectorIntelligence in Docker. It replicates the exact mDNS behaviour of +// Wire-Pod's built-in mdnshandler (register/shutdown loop every 30 s, +// reactive broadcast when Vector appears) but runs natively on the Windows +// network stack, bypassing WSL2's multicast limitations. +// +// Build (cross-compile inside Docker): +// GOOS=windows GOARCH=amd64 go build -o /output/windows-mdns.exe ./shared/mdns-reflector.go +// +// Usage: +// windows-mdns.exe (auto-detect IP) +// windows-mdns.exe -botinfo path/to/botSdkInfo.json + +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net" + "os" + "os/signal" + "path/filepath" + "strings" + "sync" + "syscall" + "time" + + "github.com/kercre123/zeroconf" +) + +// ── Configuration ──────────────────────────────────────────────────────────── + +const ( + // How often the mDNS registration is destroyed and recreated (matches + // wire-pod's mdnshandler.go loop timer). + reRegisterInterval = 30 * time.Second + + // Service parameters — must match wire-pod exactly. + instanceName = "escapepod" + serviceType = "_app-proto._tcp" + domain = "local." + servicePort = 8084 + + // The mDNS service type that Vector himself broadcasts when he boots. + vectorServiceType = "_ankivector._tcp" +) + +var txtRecords = []string{"txtv=0", "lo=1", "la=2"} + +// ── IP detection (same algorithm as wire-pod's vars.GetOutboundIP) ─────────── + +func getOutboundIP() string { + conn, err := net.Dial("udp", "8.8.8.8:80") + if err != nil { + log.Println("[mdns-reflector] WARN: not connected to a network:", err) + return "0.0.0.0" + } + defer conn.Close() + localAddr := conn.LocalAddr().(*net.UDPAddr) + return localAddr.IP.String() +} + +// ── botSdkInfo.json updater ────────────────────────────────────────────────── + +// BotInfo mirrors the minimal structure of botSdkInfo.json. +type BotInfo struct { + Robots []struct { + Esn string `json:"esn"` + IPAddress string `json:"ip_address"` + // keep other fields intact + } `json:"robots"` +} + +func updateBotInfo(path string, vectorIP string) { + raw, err := os.ReadFile(path) + if err != nil { + return // file doesn't exist yet — nothing to update + } + + // Parse into a generic map so we preserve ALL fields, not just the ones + // we model in BotInfo. + var data map[string]interface{} + if err := json.Unmarshal(raw, &data); err != nil { + log.Printf("[mdns-reflector] WARN: failed to parse %s: %v\n", path, err) + return + } + + robots, ok := data["robots"].([]interface{}) + if !ok || len(robots) == 0 { + return + } + first, ok := robots[0].(map[string]interface{}) + if !ok { + return + } + + oldIP, _ := first["ip_address"].(string) + if oldIP == vectorIP { + return // nothing changed + } + + first["ip_address"] = vectorIP + out, err := json.Marshal(data) + if err != nil { + return + } + if err := os.WriteFile(path, out, 0644); err != nil { + log.Printf("[mdns-reflector] WARN: failed to write %s: %v\n", path, err) + return + } + log.Printf("[mdns-reflector] Vector IP updated: %s -> %s\n", oldIP, vectorIP) +} + +// ── Vector discovery (listen for _ankivector._tcp) ─────────────────────────── + +func watchForVector(botInfoPath string, triggerReRegister chan<- struct{}) { + for { + resolver, err := zeroconf.NewResolver(nil) + if err != nil { + log.Printf("[mdns-reflector] WARN: resolver error: %v\n", err) + time.Sleep(5 * time.Second) + continue + } + + entries := make(chan *zeroconf.ServiceEntry) + ctx, cancel := context.WithTimeout(context.Background(), 80*time.Second) + + if err := resolver.Browse(ctx, vectorServiceType, domain, entries); err != nil { + log.Printf("[mdns-reflector] WARN: browse error: %v\n", err) + cancel() + time.Sleep(5 * time.Second) + continue + } + + for entry := range entries { + if strings.Contains(entry.Service, "ankivector") { + vectorIP := "" + if len(entry.AddrIPv4) > 0 { + vectorIP = entry.AddrIPv4[0].String() + } + log.Printf("[mdns-reflector] Vector discovered on network: %s (%s)\n", + entry.HostName, vectorIP) + + // Update botSdkInfo.json with Vector's current IP + if vectorIP != "" && botInfoPath != "" { + updateBotInfo(botInfoPath, vectorIP) + } + + // Trigger immediate mDNS re-registration (like PostmDNSNow) + select { + case triggerReRegister <- struct{}{}: + default: + } + } + } + cancel() + } +} + +// ── Main mDNS registration loop ───────────────────────────────────────────── + +func main() { + log.SetFlags(log.Ldate | log.Ltime) + log.Println("[mdns-reflector] Starting mDNS reflector for VectorIntelligence...") + + // Determine botSdkInfo.json path + botInfoPath := "" + for i, arg := range os.Args { + if arg == "-botinfo" && i+1 < len(os.Args) { + botInfoPath = os.Args[i+1] + } + } + if botInfoPath == "" { + // Default: look in ./vector-data/jdocs/ (relative to .exe location) + exePath, _ := os.Executable() + exeDir := filepath.Dir(exePath) + candidate := filepath.Join(exeDir, "vector-data", "jdocs", "botSdkInfo.json") + if _, err := os.Stat(candidate); err == nil { + botInfoPath = candidate + } + } + if botInfoPath != "" { + log.Printf("[mdns-reflector] Will update botSdkInfo.json at: %s\n", botInfoPath) + } + + // Channel to trigger immediate re-registration + triggerReRegister := make(chan struct{}, 1) + + // Start Vector discovery in background + go watchForVector(botInfoPath, triggerReRegister) + + // Handle graceful shutdown + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + + var server *zeroconf.Server + var mu sync.Mutex + + registerMDNS := func() *zeroconf.Server { + ip := getOutboundIP() + s, err := zeroconf.RegisterProxy( + instanceName, + serviceType, + domain, + servicePort, + instanceName, // hostname + []string{ip}, // IPs + txtRecords, // TXT records + nil, // interfaces (all) + ) + if err != nil { + log.Printf("[mdns-reflector] WARN: failed to register mDNS: %v\n", err) + return nil + } + log.Printf("[mdns-reflector] Broadcasting escapepod.local -> %s\n", ip) + return s + } + + shutdownServer := func() { + mu.Lock() + defer mu.Unlock() + if server != nil { + server.Shutdown() + server = nil + } + } + + // Main loop: register, wait 30s (or trigger), shutdown, repeat. + go func() { + for { + mu.Lock() + server = registerMDNS() + mu.Unlock() + + // Wait for either the timer or a forced re-registration trigger + select { + case <-time.After(reRegisterInterval): + case <-triggerReRegister: + log.Println("[mdns-reflector] Immediate re-registration triggered (Vector detected)") + } + + shutdownServer() + time.Sleep(333 * time.Millisecond) // brief pause before re-register + } + }() + + // Block until SIGINT/SIGTERM + sig := <-sigCh + fmt.Printf("\n[mdns-reflector] Received %v, shutting down...\n", sig) + shutdownServer() + log.Println("[mdns-reflector] Goodbye!") +} diff --git a/shared/scripts/remove-windows-mdns-rule.ps1 b/shared/scripts/remove-windows-mdns-rule.ps1 new file mode 100644 index 0000000..3ce4b54 --- /dev/null +++ b/shared/scripts/remove-windows-mdns-rule.ps1 @@ -0,0 +1,3 @@ +# PowerShell script to cleanly remove windows-mdns firewall rules if uninstalled +Remove-NetFirewallRule -DisplayName "windows-mdns" -ErrorAction SilentlyContinue +Write-Host "windows-mdns firewall rules removed successfully!" -ForegroundColor Green diff --git a/shared/supervisor.py b/shared/supervisor.py index ee080e0..c62bb36 100644 --- a/shared/supervisor.py +++ b/shared/supervisor.py @@ -355,6 +355,15 @@ def __init__(self): self.zc = None self.info = None self._advertised_ip: str | None = None + # When running inside Docker on Windows, the native windows-mdns.exe + # handles mDNS on the real LAN. The supervisor must NOT advertise + # the container's private 172.x IP, so all mDNS operations become + # harmless no-ops. + self._disabled = os.environ.get("DISABLE_SUPERVISOR_MDNS") == "true" + if self._disabled: + log("mDNS disabled (DISABLE_SUPERVISOR_MDNS) — " + "relying on external mDNS reflector") + def start(self, prefer_target: str = None, wait: float = 30.0) -> str: """Advertise escapepod.local. Returns the IP actually published. @@ -362,6 +371,10 @@ def start(self, prefer_target: str = None, wait: float = 30.0) -> str: prefer_target is Vector's known LAN IP — passed to local_ip() so we probe routing to Vector directly and pick the interface that talks to him, regardless of whether Tailscale or another VPN is present.""" + if self._disabled: + ip = local_ip(wait=wait, prefer_target=prefer_target) + self._advertised_ip = ip + return ip from zeroconf import IPVersion, ServiceInfo, Zeroconf ip = local_ip(wait=wait, prefer_target=prefer_target) self._advertised_ip = ip @@ -380,6 +393,8 @@ def start(self, prefer_target: str = None, wait: float = 30.0) -> str: def refresh(self, prefer_target: str = None, wait: float = 30.0) -> str: """Re-register with the current LAN IP — after sleep or IP change.""" + if self._disabled: + return self.start(prefer_target=prefer_target, wait=wait) try: self.stop() except Exception: @@ -397,6 +412,8 @@ def reannounce(self): restart" failure). Announcements in the PC->robot direction do get through, so re-announcing inside the TTL window keeps his cache permanently warm and removes the dependency on his queries entirely.""" + if self._disabled: + return if self.zc and self.info: try: self.zc.update_service(self.info) @@ -405,6 +422,8 @@ def reannounce(self): def check_and_update(self, prefer_target: str = None) -> bool: """Re-advertise if the LAN IP has drifted. Returns True if refreshed.""" + if self._disabled: + return False current = _detect_local_ip(prefer_target) if current and current != self._advertised_ip: log(f"mDNS: IP drift {self._advertised_ip} -> {current} — re-advertising")