Metadata-Version: 2.4
Name: alyxruntime
Version: 0.1.0
Summary: Use any remote machine's CPU/GPU as a Jupyter runtime over a Cloudflare tunnel, Colab-style, from the command line or Python.
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.28
Requires-Dist: websocket-client>=1.4
Provides-Extra: host
Requires-Dist: jupyter_server>=2.0; extra == "host"
Requires-Dist: ipykernel>=6.0; extra == "host"
Provides-Extra: lab
Requires-Dist: jupyter_server>=2.0; extra == "lab"
Requires-Dist: ipykernel>=6.0; extra == "lab"
Requires-Dist: jupyterlab>=4.0; extra == "lab"

# alyxruntime

Use any other computer's CPU/GPU as your Jupyter runtime - like Colab's "connect to local runtime",
but for *any* machine you control, from the command line or from Python.

```
 your laptop        Cloudflare edge            GPU box (host) - no open ports, outbound only
 ┌────────────┐ https ┌───────────────┐ tunnel ┌─────────────────────────────────────────┐
 │ alyxruntime│ ────► │ *.trycloudflare│ ◄──────│ cloudflared ─► IP-allowlist gate        │
 │  connect   │ +token│     .com       │        │                  └─► Jupyter (loopback) │
 └────────────┘       └───────────────┘        │                        └─► kernel (GPU) │
                                                └─────────────────────────────────────────┘
```

The host talks *out* to Cloudflare through `cloudflared`, so there is **no port forwarding, no public IP,
and HTTPS is included** - it works from behind home routers, NAT and most firewalls.

## Install

```bash
pip install "alyxruntime[host]"   # on the machine that SHARES its GPU/CPU
pip install alyxruntime           # on the machine that CONNECTS (lighter)
```

## Usage

On the **host** (the machine with the GPU):

```bash
alyxruntime stream --allowed_ip 203.0.113.7                # your client's public IP
alyxruntime stream --allowed_ip 203.0.113.7 10.0.0.0/24    # several IPs / CIDR ranges
alyxruntime stream --allowed_ip all                        # any IP - the token is the only gate
```

It starts a Cloudflare quick tunnel (if `cloudflared` is missing it offers to download the official
binary, or pass `--install-cloudflared`) and prints a ready-to-paste command with a random access token:

```bash
alyxruntime connect 'https://random-words.trycloudflare.com/?token=...'
```

Not sure what your public IP is? Run `alyxruntime connect` from the client anyway: the host replies
`access denied for IP x.x.x.x. Ask the host to add it with --allowed_ip x.x.x.x`.

On the **client**:

```bash
alyxruntime connect https://my-host:8765/?token=XXXX   # verifies, shows remote CPU/RAM/GPU, saves session
alyxruntime run train.py            # run a script   (also .ipynb, `-c "code"`, or stdin with `-`)
alyxruntime shell                   # interactive prompt; variables persist between commands
alyxruntime info                    # remote CPU / RAM / GPU
alyxruntime upload data.csv         # copy files to the host's working dir
alyxruntime download model.pt
alyxruntime restart                 # fresh kernel
alyxruntime disconnect --kill-kernel
```

Plots (`plt.show()`) are saved locally to `./alyx_outputs/`.
VS Code / JupyterLab also work: *Select Kernel -> Existing Jupyter Server* -> paste the same URL.

### From Python

```python
from alyxruntime import Runtime

rt = Runtime.connect("https://my-host:8765/?token=XXXX")
rt.run("import torch; print(torch.cuda.is_available())")
res = rt.run("2 + 2", echo=False);  print(res.results)     # ['4']
rt.upload("data.csv"); rt.download("out.pt")
```

Host side from Python: `from alyxruntime import serve; serve(allowed_ip=["203.0.113.7"])`.

### Stable URL (named tunnel)

Quick tunnels get a new random URL every run. For a fixed hostname, create a tunnel in the Cloudflare
dashboard (Zero Trust -> Networks -> Tunnels), point a public hostname at `http://localhost:9000`, then:

```bash
alyxruntime stream --tunnel-token <TOKEN> --public-url https://gpu.example.com --port 9000 \
                   --allowed_ip 203.0.113.7
```

## `stream` options

| flag | meaning |
|---|---|
| `--allowed_ip IP [IP...]` | public IPs / CIDR ranges (v4 or v6) allowed to connect, as seen by Cloudflare, or the word `all` (same as `--allow-any`). **Required** in tunnel mode. |
| `--allow-any` | accept any IP (the token is then your only protection) |
| `--no-tunnel` | skip Cloudflare; listen directly on `--host:--port` (you handle NAT; loopback is always allowed in this mode) |
| `--install-cloudflared` | download `cloudflared` automatically if it's missing |
| `--tunnel-token` / `--public-url` / `--port` | use a named tunnel with a stable hostname (see above) |
| `--token` | fixed access token instead of a random one |
| `--root-dir` | directory the client can see (default: current dir) |
| `--tls-cert` / `--tls-key` | HTTPS for `--no-tunnel` mode (not needed with a tunnel) |
| `--public-host` | hostname/IP to print in the connect URL with `--no-tunnel` |

## Security - please read

A runtime is **remote code execution by design**: whoever connects runs arbitrary code as the host user.

* **Two independent gates:** the IP allowlist *and* a random token.
* **How the allowlist works with a tunnel:** `cloudflared` reaches the host from `127.0.0.1`, so the
  socket address is useless for filtering. The gate instead reads the `CF-Connecting-IP` header, which
  Cloudflare's edge sets itself, and checks **every request** against `--allowed_ip`. Requests with no
  such header (anything not coming through the tunnel) are refused, loopback gets no special pass, and
  each request is forced to `Connection: close` so one vetted connection can't be reused by someone else.
* The quick-tunnel URL is public: anyone who learns it can *reach* the gate (and be refused). Treat the
  token like a password, and don't paste the connect line in public places.
* Traffic passes through Cloudflare's network (TLS to Cloudflare, then Cloudflare -> your host over the
  tunnel); Cloudflare can technically see it. If that's unacceptable, use `--no-tunnel` with your own VPN.
* IP allowlisting trusts that Cloudflare reports the true client IP. It won't protect against someone
  sharing your NAT/VPN exit IP, and mobile/CGNAT or rotating-IP connections will need a CIDR range.
* Local users on the host can reach the gate on its loopback port and are trusted like the host user.
* Don't run `stream` as root unless you mean it. The client's saved token lives in `~/.alyxruntime/session.json` (0600).
* Cloudflare's free quick tunnels are meant for development: no uptime guarantee, and you should read
  Cloudflare's terms before using them for anything heavy or long-lived (use a named tunnel for that).
