navette-browser 1.7.0

The browser for agents — one tiny binary driving the WebView your OS already ships.
name: ci

on:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  macos:
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - run: cargo build --release
      - name: smoke
        shell: bash
        run: |
          RUST_BACKTRACE=1 sh -c './target/release/navette serve --port 8765; exit 0' > serve.log 2>&1 &
          sleep 3
          cat serve.log
          python3 -m http.server 8901 -d test &
          for i in $(seq 1 15); do
            curl -sf --max-time 2 http://127.0.0.1:8901/smoke-page.html >/dev/null && break
            sleep 1
          done
          curl -sf http://127.0.0.1:8765/health | grep -q '"ok"'
          RESP=$(curl -sf -X POST http://127.0.0.1:8765/navigate -H 'Content-Type: application/json' \
            -d '{"url":"http://127.0.0.1:8901/smoke-page.html","with_content":true}') || RESP="CURL_FAILED"
          echo "$RESP" | grep -q 'Example Domain' || { echo "NAV RESPONSE: $RESP"; echo "--- serve.log ---"; cat serve.log; exit 1; }
          curl -sf --max-time 30 -X POST http://127.0.0.1:8765/screenshot -o shot.png || { echo "SCREENSHOT REQUEST FAILED"; cat serve.log; exit 1; }
          head -c 4 shot.png | od -An -tx1 | tr -s " " | grep -q "89 50 4e 47" || { echo "SCREENSHOT NOT PNG"; ls -la shot.png; cat serve.log; exit 1; }
          echo "screenshot: $(wc -c < shot.png) bytes"
          curl -sf --max-time 30 -X POST http://127.0.0.1:8765/sessions/load -H 'Content-Type: application/json' \
            -d '{"cookies":[{"name":"navette_smoke","value":"1","domain":"127.0.0.1","path":"/"}]}' >/dev/null
          curl -sf --max-time 30 -X POST http://127.0.0.1:8765/sessions/state | grep -q navette_smoke || { echo "COOKIE ROUND-TRIP FAILED"; cat serve.log; exit 1; }
          echo "cookie round-trip OK"
          curl -sf --max-time 30 -X POST http://127.0.0.1:8765/upload -H 'Content-Type: application/json' \
            -d '{"selector":"#f","filename":"up.txt","mime":"text/plain","content_base64":"aGVsbG8="}' >/dev/null
          curl -sf --max-time 30 -X POST http://127.0.0.1:8765/click -H 'Content-Type: application/json' \
            -d '{"selector":"#go"}' >/dev/null
          RESP2=$(curl -sf --max-time 30 -X POST http://127.0.0.1:8765/evaluate -H 'Content-Type: application/json' \
            -d '{"js":"document.getElementById(\"t\").value"}')
          echo "$RESP2" | grep -q 'up.txt:5' || { echo "UPLOAD ROUND-TRIP FAILED: $RESP2"; cat serve.log; exit 1; }
          echo "upload round-trip OK"
          curl -sf --max-time 30 -X POST http://127.0.0.1:8765/scroll -H 'Content-Type: application/json' \
            -d '{"y":50}' | grep -q '"ok"' || { echo "SCROLL FAILED"; cat serve.log; exit 1; }
          echo "scroll OK"
          echo "macOS smoke OK"

  windows:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - name: ensure WebView2 runtime
        shell: pwsh
        run: |
          $present = Test-Path "C:\Program Files (x86)\Microsoft\EdgeWebView\Application"
          if (-not $present) {
            Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/p/?LinkId=2124703" -OutFile webview2bootstrapper.exe
            Start-Process -FilePath "./webview2bootstrapper.exe" -ArgumentList "/silent","/install" -Wait
          } else { Write-Host "WebView2 runtime present" }
      - run: cargo build --release
      - name: smoke
        shell: bash
        run: |
          # Launch navette from bash with redirected stdout so its breadcrumbs
          # land in serve.log (Start-Process detached it into the void).
          RUST_BACKTRACE=1 ./target/release/navette.exe serve --port 8765 > serve.log 2>&1 &
          sleep 5
          cat serve.log
          python -m http.server 8901 -d test &
          for i in $(seq 1 15); do
            curl -sf --max-time 2 http://127.0.0.1:8901/smoke-page.html >/dev/null && break
            sleep 1
          done
          curl -sf --max-time 10 http://127.0.0.1:8765/health | grep -q '"ok"' || { echo "HEALTH FAILED"; cat serve.log; exit 1; }
          # http:// loopback like the other platforms — the file:// URL stalls
          # ~45 s in WebView2 on the GPU-less CI VM (fold arrives 100 ms after
          # the timeout); loopback loads complete in milliseconds.
          RESP=$(curl -sfS --max-time 50 -X POST http://127.0.0.1:8765/navigate -H 'Content-Type: application/json' \
            -d '{"url":"http://127.0.0.1:8901/smoke-page.html","with_content":true}') || RESP="CURL_FAILED"
          echo "$RESP" | grep -q 'Example Domain' || { echo "NAV RESPONSE: $RESP"; echo "--- serve.log ---"; cat serve.log; exit 1; }
          curl -sf --max-time 30 -X POST http://127.0.0.1:8765/screenshot -o shot.png || { echo "SCREENSHOT REQUEST FAILED"; cat serve.log; exit 1; }
          head -c 4 shot.png | od -An -tx1 | tr -s " " | grep -q "89 50 4e 47" || { echo "SCREENSHOT NOT PNG"; ls -la shot.png; cat serve.log; exit 1; }
          echo "screenshot: $(wc -c < shot.png) bytes"
          curl -sf --max-time 30 -X POST http://127.0.0.1:8765/sessions/load -H 'Content-Type: application/json' \
            -d '{"cookies":[{"name":"navette_smoke","value":"1","domain":"127.0.0.1","path":"/"}]}' >/dev/null
          curl -sf --max-time 30 -X POST http://127.0.0.1:8765/sessions/state | grep -q navette_smoke || { echo "COOKIE ROUND-TRIP FAILED"; cat serve.log; exit 1; }
          echo "cookie round-trip OK"
          curl -sf --max-time 30 -X POST http://127.0.0.1:8765/upload -H 'Content-Type: application/json' \
            -d '{"selector":"#f","filename":"up.txt","mime":"text/plain","content_base64":"aGVsbG8="}' >/dev/null
          curl -sf --max-time 30 -X POST http://127.0.0.1:8765/click -H 'Content-Type: application/json' \
            -d '{"selector":"#go"}' >/dev/null
          RESP2=$(curl -sf --max-time 30 -X POST http://127.0.0.1:8765/evaluate -H 'Content-Type: application/json' \
            -d '{"js":"document.getElementById(\"t\").value"}')
          echo "$RESP2" | grep -q 'up.txt:5' || { echo "UPLOAD ROUND-TRIP FAILED: $RESP2"; cat serve.log; exit 1; }
          echo "upload round-trip OK"
          curl -sf --max-time 30 -X POST http://127.0.0.1:8765/scroll -H 'Content-Type: application/json' \
            -d '{"y":50}' | grep -q '"ok"' || { echo "SCROLL FAILED"; cat serve.log; exit 1; }
          echo "scroll OK"
          taskkill /IM navette.exe || true
          taskkill /IM msedgewebview2.exe || true
          echo "Windows smoke OK"
          exit 0

  linux:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
      - run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev glib-networking xvfb curl dbus
      - uses: dtolnay/rust-toolchain@stable
      - run: cargo build --release
      - name: smoke
        shell: bash
        run: |
          # dbus-run-session: gtk_init blocks on the session bus without one
          # (g_application_register -> g_dbus_proxy_new_sync, gdb-proven).
          RUST_BACKTRACE=1 WEBKIT_FORCE_SANDBOX=0 WEBKIT_DISABLE_DMABUF_RENDERER=1 GDK_PLATFORM=x11 setsid xvfb-run -a -s "-screen 0 1280x800x24" dbus-run-session -- sh -c './target/release/navette serve --port 8765; exit 0' > serve.log 2>&1 &
          sleep 5
          cat serve.log
          python -m http.server 8901 -d test &
          for i in $(seq 1 15); do
            curl -sf --max-time 2 http://127.0.0.1:8901/smoke-page.html >/dev/null && break
            sleep 1
          done
          curl -sf --max-time 10 http://127.0.0.1:8765/health | grep -q '"ok"' || { echo "HEALTH FAILED"; cat serve.log; exit 1; }
          for i in $(seq 1 30); do
            S=$(curl -sf --max-time 5 http://127.0.0.1:8765/sessions || true)
            echo "$S" | grep -q '"url"' && { echo "PRE-WARMED after ${i} tries: $S"; break; }
            sleep 2
          done
          RESP=$(curl -sfS --max-time 50 -X POST http://127.0.0.1:8765/navigate -H 'Content-Type: application/json' \
            -d '{"url":"http://127.0.0.1:8901/smoke-page.html","with_content":true}') || RESP="CURL_FAILED"
          echo "$RESP" | grep -q 'Example Domain' || { echo "NAV RESPONSE: $RESP"; echo "--- serve.log ---"; cat serve.log; exit 1; }
          curl -sf --max-time 30 -X POST http://127.0.0.1:8765/screenshot -o shot.png || { echo "SCREENSHOT REQUEST FAILED"; cat serve.log; exit 1; }
          head -c 4 shot.png | od -An -tx1 | tr -s " " | grep -q "89 50 4e 47" || { echo "SCREENSHOT NOT PNG"; ls -la shot.png; cat serve.log; exit 1; }
          echo "screenshot: $(wc -c < shot.png) bytes"
          curl -sf --max-time 30 -X POST http://127.0.0.1:8765/sessions/load -H 'Content-Type: application/json' \
            -d '{"cookies":[{"name":"navette_smoke","value":"1","domain":"127.0.0.1","path":"/"}]}' >/dev/null
          curl -sf --max-time 30 -X POST http://127.0.0.1:8765/sessions/state | grep -q navette_smoke || { echo "COOKIE ROUND-TRIP FAILED"; cat serve.log; exit 1; }
          echo "cookie round-trip OK"
          KEYR=$(curl -sf --max-time 30 -X POST http://127.0.0.1:8765/key -H 'Content-Type: application/json' \
            -d '{"key":"Tab"}')
          echo "$KEYR" | grep -q '"ok"' || { echo "KEY FAILED"; cat serve.log; exit 1; }
          echo "key OK — mode: $(echo "$KEYR" | grep -o '"mode": "[a-z]*"')"
          curl -sf --max-time 30 -X POST http://127.0.0.1:8765/upload -H 'Content-Type: application/json' \
            -d '{"selector":"#f","filename":"up.txt","mime":"text/plain","content_base64":"aGVsbG8="}' >/dev/null
          curl -sf --max-time 30 -X POST http://127.0.0.1:8765/click -H 'Content-Type: application/json' \
            -d '{"selector":"#go"}' >/dev/null
          RESP2=$(curl -sf --max-time 30 -X POST http://127.0.0.1:8765/evaluate -H 'Content-Type: application/json' \
            -d '{"js":"document.getElementById(\"t\").value"}')
          echo "$RESP2" | grep -q 'up.txt:5' || { echo "UPLOAD ROUND-TRIP FAILED: $RESP2"; cat serve.log; exit 1; }
          echo "upload round-trip OK"
          curl -sf --max-time 30 -X POST http://127.0.0.1:8765/scroll -H 'Content-Type: application/json' \
            -d '{"y":50}' | grep -q '"ok"' || { echo "SCROLL FAILED"; cat serve.log; exit 1; }
          echo "scroll OK"
          pkill -x navette || true
          pkill -f WebKit || true
          pkill -x Xvfb || true
          echo "Linux smoke OK"
          exit 0