# library-tunnel

Durable, reboot-surviving SSH tunnel manager for the remote-library ingest
(`@stravica-ai/remote-library`). Ships as the estate's onboarding pattern —
same three-word command whether the host is macbook, smarthome01, wsd, or
dex.

Loopback-only ingest is a transport ruling (Dave, 2026-08-21): the service
listens on `127.0.0.1:4004` on `ops-01` and every consumer reaches it through
an ssh port-forward. This tool manages that forward as a first-class daemon.

## Verbs

```
library-tunnel install   [--host <alias>] [--local-port <n>] [--remote <host:port>]
                         [--status-port <n>] [--ssh-bin <path>]
library-tunnel uninstall [--host <alias>]
library-tunnel status    [--host <alias>] [--json]
library-tunnel check     [--host <alias>] [--json]
```

Defaults: `--host ops-01 --local-port 4004 --remote 127.0.0.1:4004
--ssh-bin /usr/bin/ssh`. `--status-port` is unset by default; pass e.g.
`--status-port 4005` to add a second forward for the `/status` endpoint.

## Onboarding a fresh host (macbook, dex, wsd, smarthome01)

```sh
# 1. Prerequisite: ~/.ssh/config has a `Host <alias>` block with a
#    BatchMode-safe key (no passphrase prompt at runtime).

# 2. Install the LaunchAgent + persist the state file.
node operator/extensions/tools/library-tunnel/bin/library-tunnel install --host ops-01

# 3. Verify the tunnel end-to-end (TCP + HTTP HEAD through the forward).
node operator/extensions/tools/library-tunnel/bin/library-tunnel check --host ops-01
#   [ingest] tunnel ok 127.0.0.1:4004 — tcp Xms · http 404 (Yms)

# 4. Verify the library write path (once library.mode=remote is set in
#    operator/config/preferences[.local].json on this host).
node operator/extensions/tools/library-cli/bin/library status
#   service ok (http <n>) — lag=... events / ...s
```

## macOS install layout

- **LaunchAgent plist:** `~/Library/LaunchAgents/ai.stravica.library-tunnel.<host>.plist`
- **State file:** `<repoRoot>/.cache/library-tunnel/<host>.json` (per-machine;
  install parameters remembered so `check` / `status` / `uninstall` need no
  re-typing)
- **Log files:** `<repoRoot>/.cache/library-tunnel/<host>.log` (stdout,
  usually empty) and `<host>.err.log` (ssh diagnostics). Kept across
  uninstall for post-mortems.

The plist runs `ssh -N` in the foreground under launchd (no `-f`) so the
supervisor owns the child pid. `KeepAlive=true`, `RunAtLoad=true`, and
`ThrottleInterval=30` give autossh-equivalent semantics without adding a
dependency. `-o ExitOnForwardFailure=yes` makes ssh die fast when the forward
cannot bind, so launchd relaunches instead of sitting on a broken tunnel;
`-o ServerAliveInterval=30 -o ServerAliveCountMax=3` closes dead links within
~90s; `-o BatchMode=yes` refuses interactive prompts.

## Linux (systemd) — not yet wired

`buildTunnelSystemdUnit` in `src/plist.mjs` generates the equivalent user
unit. It is intentionally *not* invoked from the CLI yet; the install path
will grow a systemd branch when the first Linux host (smarthome01 or dex)
onboards, at which point the CLI surface stays identical.

## Exit codes

`0` ok · `1` usage / config error · `2` operational failure · `3` transport
failure (probes failed, or `status` sees plist-installed-but-no-pid — the
actionable degraded state).

## Tests

```sh
node --test operator/extensions/tools/library-tunnel/test/*.test.mjs
```

Zero npm dependencies (Node built-ins only). Tests cover argv parsing, plist
generation, install/uninstall flow (mocked launchctl + tmp state dir), and
TCP + HTTP probes against ephemeral sockets.
