# VastLc0UciBridge

Expose GPU-backed `lc0` running inside a Vast.ai container to your local AIChessGM over private Tailscale TCP, using plain UCI text (no protocol changes). The bridge now also supports legacy-compatible management commands (`scan`, `query`) on TCP `53350`.

New to Go/Rust/Docker? Start with the beginner guide: [docs/00-start-here.md](/Users/james/LeelaOnVastAI/docs/00-start-here.md)
Need help choosing Lc0 on Vast? See [docs/09-lc0-version-and-build-selection.md](/Users/james/LeelaOnVastAI/docs/09-lc0-version-and-build-selection.md)
Need a full end-to-end new-user guide for Vast.ai, Tailscale, macOS, Windows, and home Linux? See [docs/14-leela-vastai-setup-guide.md](/Users/james/LeelaOnVastAI/docs/14-leela-vastai-setup-guide.md)
Cloud rollout execution checklist: [docs/13-cloud-rollout-checklist.md](/Users/james/LeelaOnVastAI/docs/13-cloud-rollout-checklist.md)

## What this includes

- `cmd/uci-bridge/main.go`: TCP-to-child-process UCI bridge.
- `entrypoint.sh`: boots Tailscale in userspace mode, joins tailnet, optionally publishes TCP 5555 and TCP 53350 via `tailscale serve`, then starts bridge.
- `Dockerfile`: CUDA runtime + tailscale + compiled bridge.

## Runtime model

- Each engine keeps one persistent `lc0` process warm and reuses it across client sessions.
- Client sessions are serialized per engine so a new session starts from a clean `readyok` boundary.
- If the engine receives no further UCI input while idle, the bridge can stop the child process after a configurable timeout.
- Optional auth line before UCI traffic when token is configured:
  - first line must be `AUTH <token>`
- UCI is forwarded as newline-delimited plain text over TCP.
- Optional legacy-compatible management listener can return engine discovery/info:
  - `scan` -> `JRE_...` lines + `scanisdone`
  - `query` -> table text ending with `querydone`

## Bridge flags

`uci-bridge` supports:

- `--listen` (default `127.0.0.1:5555`)
- `--profiles` (default from `UCI_ENGINE_PROFILES`; when set, load legacy-style multi-engine catalog and listen on one port per profile)
- `--mgmt-listen` (default from `UCI_MGMT_LISTEN`, else `127.0.0.1:53350`; empty disables management listener)
- `--mgmt-advertise-host` (default `MGMT_ADVERTISE_HOST`, else `TS_HOSTNAME`; used in `scan` response)
- `--rewrite-id-name` (default from `UCI_REWRITE_ID_NAME`, else `false`; rewrites `id name` to legacy-style identity)
- `--id-name-separator` (default from `UCI_ID_NAME_SEPARATOR`, else `##`)
- `--engine-name` (default from `UCI_ENGINE_NAME`, else `lc0-cloud`)
- `--lc0` (default `/usr/local/bin/lc0`)
- `--weights` (default `/data/weights.pb.gz`; use empty or `<autodiscover>` to scan `/data`, `/workspace`, `.`)
- `--backend` (default `cuda-fp16`)
- `--backend-opts` (default empty)
- `--threads` (default `2`)
- `--nncache` (default `200000`)
- `--minibatch-size` (default `0`; let lc0 choose automatically)
- `--max-prefetch` (default `32`)
- `--engine-idle-timeout` (default `1h`; set `0` to keep the lc0 child alive indefinitely)
- `--auth-token-env` (default `UCI_AUTH_TOKEN`)

Extra env:

- `LC0_SHUTDOWN_TIMEOUT` (default `3s`, supports duration like `5s` or integer seconds)

## Entrypoint env vars

Required:

- `TS_AUTHKEY`: Tailscale auth key.

Optional:

- `TS_HOSTNAME` (default `vast-lc0`)
- `TS_ACCEPT_DNS` (default `false`)
- `ENABLE_TS_SERVE` (default `1`)
- `ENABLE_MGMT` (default `1`; set `0` to disable management listener)
- `ENABLE_TS_SERVE_MGMT` (default `1`; when `1`, publish management TCP `53350` through `tailscale serve`)
- `UCI_ENGINE_PROFILES` (default empty; path to `engine_profiles.conf`-style profile file)
- `UCI_MGMT_LISTEN` (default `127.0.0.1:53350`)
- `MGMT_ADVERTISE_HOST` (default `TS_HOSTNAME`; host/IP announced in management scan response)
- `UCI_REWRITE_ID_NAME` (default `0`; set `1` to rewrite engine `id name` responses to legacy-style identity)
- `UCI_ID_NAME_SEPARATOR` (default `##`)
- `UCI_ENGINE_NAME` (default `lc0-cloud`; engine name announced in management responses)
- `LC0_PATH` (default `/usr/local/bin/lc0`)
- `LC0_WEIGHTS` (default `/data/weights.pb.gz`)
- `LC0_BACKEND` (default `cuda-fp16`)
- `LC0_BACKEND_OPTS` (default empty)
- `LC0_THREADS` (default `2`)
- `LC0_NNCACHE` (default `200000`)
- `LC0_MINIBATCH_SIZE` (default `0`; set `768` for the RTX 5090 BT4 throughput profile)
- `LC0_MAX_PREFETCH` (default `32`; set `512` for the RTX 5090 BT4 throughput profile)
- `LC0_ENGINE_IDLE_TIMEOUT` (default `1h`; shuts down the persistent lc0 child after idle time with no new UCI input)
- `UCI_IDLE_EXIT_TIMEOUT` (default `0`, disabled; set like `20m` to auto-exit when idle)
- `UCI_AUTH_TOKEN` (optional app-layer token)
- `UCI_AUTH_TOKEN_ENV` (default `UCI_AUTH_TOKEN`)

## Vast.ai template notes

- Use an NVIDIA CUDA runtime image and enable GPU passthrough.
- Mount persistent storage to `/data` and place weights there.
- You do not need public port mapping for this design.
- Outbound internet is required so container can reach Tailscale control plane.

If you must use Vast public ports for debugging, Vast supports host-to-container mappings, but this design keeps service private in tailnet.

## Build image

```bash
docker build -t vast-lc0-uci-bridge .
```

Optional `lc0` binary install at build time:

```bash
docker build --build-arg LC0_DOWNLOAD_URL="https://example.com/lc0-linux" -t vast-lc0-uci-bridge .
```

## Run container (example)

```bash
docker run --rm --gpus all \
  -e TS_AUTHKEY="tskey-..." \
  -e TS_HOSTNAME="vast-lc0" \
  -e ENABLE_TS_SERVE=1 \
  -e ENABLE_MGMT=1 \
  -e LC0_ENGINE_IDLE_TIMEOUT="1h" \
  -e UCI_IDLE_EXIT_TIMEOUT="20m" \
  -e UCI_AUTH_TOKEN="change-me" \
  -v /path/to/weights:/data \
  vast-lc0-uci-bridge
```

## Connect from local AIChessGM

Use tailnet hostname or IP:

- `vast-lc0:5555` (MagicDNS)
- `100.x.y.z:5555`

If auth enabled, send `AUTH <token>` as first line, then normal UCI:

```text
AUTH change-me
uci
isready
```

Expected responses include `id name ...`, `uciok`, `readyok`.

## legacy-compatible management (scan/query)

Use tailnet hostname or IP on management port:

- `vast-lc0:53350` (MagicDNS)
- `100.x.y.z:53350`

Examples:

```bash
printf 'scan' | nc vast-lc0 53350
```

Expected output shape:

```text
JRE_X64LNX_vast-lc0_5555_lc0-cloud
scanisdone
```

When `UCI_ENGINE_PROFILES` is set, `scan` returns one `JRE_...` line per configured profile/port.

```bash
printf 'query' | nc vast-lc0 53350
```

Expected output ends with:

```text
================================<<<querydone
```

## Local acceptance tests

### 1) No-GPU bridge smoke test

Create a tiny fake UCI engine:

```bash
cat >/tmp/fake-uci.sh <<'EOF'
#!/usr/bin/env bash
while IFS= read -r line; do
  case "$line" in
    uci)
      printf 'id name fake-uci\nuciok\n'
      ;;
    isready)
      printf 'readyok\n'
      ;;
    quit)
      exit 0
      ;;
  esac
done
EOF
chmod +x /tmp/fake-uci.sh
go run ./cmd/uci-bridge --listen 127.0.0.1:5555 --lc0 /tmp/fake-uci.sh --weights '<autodiscover>' --engine-idle-timeout 0
```

In another shell:

```bash
printf 'uci\nisready\nquit\n' | nc 127.0.0.1 5555
```

Expected: `id name fake-uci`, `uciok`, and `readyok`.

### 2) Auth handshake check

```bash
UCI_AUTH_TOKEN=secret go run ./cmd/uci-bridge --listen 127.0.0.1:5555 --lc0 /tmp/fake-uci.sh --weights '<autodiscover>' --engine-idle-timeout 0
printf 'AUTH bad\nuci\n' | nc 127.0.0.1 5555
```

Expected: `unauthorized` and disconnect.

### 3) Real lc0 integration

```bash
nc vast-lc0 5555
```

Then type:

```text
uci
isready
```

Expected: `uciok` and `readyok`.

### 4) Management compatibility check

```bash
printf 'scan' | nc 127.0.0.1 53350
printf 'query' | nc 127.0.0.1 53350
```

Expected:
- `scan` result includes one `JRE_...` line and `scanisdone`
- `query` result includes `querydone`

### 5) Multi-engine profile mode (legacy config format)

Create a profile file (example):

```text
lc0-air-cow    53352   lc0.exe         --backend=cuda-auto:--weights=weights.pb
lc0-mini-cow   53355   lc0.exe         --backend=cuda-auto:--weights=weights.pb
sf-avx2        53375   stockfish.exe
```

Run bridge with profiles:

```bash
UCI_ENGINE_PROFILES=/path/to/engine_profiles.conf \
go run ./cmd/uci-bridge --listen 127.0.0.1:5555 --mgmt-listen 127.0.0.1:53350
```

Behavior:
- Bridge starts one UCI listener per profile port (using host part from `--listen`).
- `scan` advertises all profile entries.
- `query` shows each profile and connected clients per profile.

### 6) Optional legacy-style `id name` rewrite

Enable for legacy clients that expect rewritten identity:

```bash
UCI_REWRITE_ID_NAME=1 go run ./cmd/uci-bridge --listen 127.0.0.1:5555
```

With rewrite on, an engine line like:

```text
id name Lc0 v0.31.2
```

is forwarded as:

```text
id name JRE_X64LNX_<host>_<port>_<engine-name>##Lc0 v0.31.2
```

### 7) One-command cloud smoke check

```bash
./scripts/cloud-smoke-test.sh --host vast-lc0 --auth-token '<YOUR_AUTH_TOKEN>'
```

### 8) Convert Windows legacy config to cloud profiles

```bash
./scripts/migrate-legacy-conf.sh \
  --in ./data/engine_profiles.conf \
  --out ./data/engine_profiles.cloud.conf \
  --weights /data/BT4-332.pb
```

### 9) Validate cloud profile before deploy

```bash
./scripts/validate-cloud-profile.sh --profile ./data/engine_profiles.cloud.conf --warn-only
```


### 10) Build Spark Cursor handoff package

```bash
./scripts/build-spark-cursor-package.sh
```

Output:
- `dist/LeelaSparkCursorPackage-<timestamp>.tar.gz`

Copy that tarball to Spark, extract it, open in Cursor, then use `CURSOR_TASK_PROMPT.md`.


## Troubleshooting

- `weights not found`: set `LC0_WEIGHTS` to exact file path or mount weights under `/data`.
- `failed to start lc0`: verify `LC0_PATH` points to executable binary in container.
- CUDA/cuDNN init failures: inspect bridge logs; `lc0` stderr lines are emitted.
- Tailscale not reachable: verify `TS_AUTHKEY`, outbound connectivity, and tailnet ACL rules.
- Cannot connect to `vast-lc0:5555`: confirm `ENABLE_TS_SERVE=1` and run `tailscale status` inside container.
- Cannot connect to `vast-lc0:53350`: confirm `ENABLE_MGMT=1` and `ENABLE_TS_SERVE_MGMT=1`.
- `scan` advertises wrong host/IP: set `MGMT_ADVERTISE_HOST` explicitly.

## ACL guidance

Restrict access in Tailscale ACLs so only allowed devices/users can connect to destination `vast-lc0:5555` and, if enabled, `vast-lc0:53350`.
