# Multi-machine dev setup (MacOS ↔ kloud)

> The repo is designed so you can switch between MacOS (laptop) and kloud (Docker host) without losing state. The same commands work on either side; each machine owns a role and the seam between them is just git + SSH.

The repo is designed so you can switch between MacOS (laptop) and kloud
(Docker host) without losing state. The same commands work on either side;
each machine owns a role and the seam between them is just `git` + SSH.

<!-- SOURCE: Taskfile.yml, container/compose.yaml, scripts/setup-dev.sh, scripts/init-get-publish-root.sh, scripts/publish-kin-get.sh, examples/settings.toml.shared-template.toml, container/README.md, docs/getting-started/install.md -->

## Roles

| Machine | Primary role | Why |
|---|---|---|
| **MacOS** | Harness + TUI iteration. The day-to-day dev loop. | Best TUI rendering, all local tools, fastest iteration on `src/kin/harness/` + `src/kin/tui/`. |
| **kloud** | [Outpost](/docs/kin/guide/outpost/) + container + live-verifies. The Docker-host seat. | `task outpost:build`/`task outpost:up`; DS4/Qwen live gates use the same stable Tailnet contracts from either machine. |

Either machine can do the other's job — `task run` works fine over SSH + tmux,
`task verify` is fast on both — but each is *best* at its lane.

## Bootstrap a fresh machine

From inside the checkout:

```bash
./scripts/setup-dev.sh     # idempotent — safe to re-run
```

`setup-dev.sh` is the **dev-machine** bootstrap — it assumes you already
have a checkout and want the tmux/`.bashrc` dev-loop plumbing. It's a
different script from the public one-liner at
[get.kinra.ai](https://get.kinra.ai) (`scripts/install.sh`), which
clones the repo itself and skips the tmux step — see
[Install](/docs/kin/getting-started/install/) for that consumer-device path.

Does four things, none of which need sudo:

1. `uv sync` — creates `.venv`, installs all deps from `uv.lock`.
2. `uv tool install --editable .` — installs the `kin` command on `$PATH`.
3. Copies `examples/settings.toml.shared-template.toml` → `~/.kin/settings.toml`
   (only if not already present). Secrets stay out — enter via `kin` → `/providers`.
4. Appends a one-liner to `~/.bashrc` that auto-attaches (or creates) a tmux
   session named `kin` on every SSH login.

The verify gate ([details](/docs/kin/internals/testing/)):

```bash
task verify    # thousands of headless asserts, no model/terminal needed
```

`kin doctor` is a complementary, lighter-weight check for a freshly
bootstrapped machine — it verifies the install itself (uv/kin/git on
PATH), that a provider actually resolves (mirroring `make_backend`'s
config chain), and that the resolved endpoint is reachable, without
needing a checkout or `task` at all. See [Install](/docs/kin/getting-started/install/).

Re-running the script on a machine that's already set up is a no-op —
each step checks for its precondition.

## The build-worktree split (deploy safety)

The kloud checkout has dedicated build worktrees — separate `git worktree`s
of the same repo, checked out at the **deploy SHA**. The deploy tasks operate
there; your dev working tree at `~/kin/` never gets `reset --hard`. The
worktrees share git history (one `.git/`) but have independent file state.

```
~/kin/                  ← dev tree, on branch dev, your WIP commits live here
~/srv/web/kin-outpost/  ← outpost deploys: detached HEAD, reset each deploy
~/srv/net/rig-vm/       ← rig/rig-vm deploys: detached HEAD, reset each deploy
```

The deploy tasks (`task outpost:deploy-dev`, `task rig:vm-deploy-dev`, …)
create their build worktree on first run, then
`fetch + reset --hard origin/<branch>` it each subsequent run. Your dev tree
is untouched — the silent-wipe footgun is gone.

The rig worktree runs ONLY the rig services and carries two untracked,
reset-surviving deployment files under `container/`: a
`compose.override.yaml` that splits the compose project off the tracked
`name: outpost` (keeping the pre-split volume names and joining the outpost
network so the dashboard still reaches `rig`), and a `.env` pinning
`OUTPOST_BIND_IP` to the tailnet address (net tier). The rig deploy tasks
therefore run `docker compose` with auto-discovery from that directory —
never `-f` — so the override participates, and they only fall back to their
`BIND_IP` variable when `.env` does not set the bind address.

Container secrets (`oauth-client-id`, `oauth-client-secret`, `oauth-cookie-secret`,
`kin-settings`, `kin-credentials`) live in the **dev tree** under `container/secrets/` —
gitignored fail-closed, mode 600, only you drop them. The deploy script
symlinks `container/secrets/` from the dev tree into the build worktree
before building, so `docker compose` resolves the same paths regardless of
which worktree owns the compose file.

## The publish profile (get.kinra.ai + docs.kinra.ai)

This section documents Blake's own reference deployment (`kloud` +
`kloud-vps`) as a worked example — substitute your own host/SSH-alias/DNS
if you're standing up a similar pair of public docroots elsewhere. For the
consumer-facing side of this (what a visitor to get.kinra.ai actually
runs), see [Install](/docs/kin/getting-started/install/); for the Outpost
container itself, see [Outpost](/docs/kin/guide/outpost/).

The public `get` docroot is durable host state, independent of the disposable
build worktree:

```text
~/.local/share/kinra-publish/get/   # nginx source by default
~/srv/web/kin-outpost/container/installer-www/ # frozen legacy emergency mount
```

Before the first deploy carrying this topology, copy the currently live legacy
root without exposing the new directory:

```bash
task outpost:publish-init
```

The command refuses an existing destination, validates the old docroot, copies
it atomically, and does not touch nginx. This explicit step preserves every
live Kin URL before Paddock is staged or the bind source changes. If neither
root exists, a dev deploy refuses to create a public site and directs the
operator to deploy verified `main`; verified main may bootstrap a genuinely
new host.

On `task outpost:deploy-main`, before `compose up`:

1. `uv run --group docs mkdocs build` refreshes the build-worktree `site/`
   served as **docs.kinra.ai**.
2. `scripts/publish-kin-get.sh` atomically replaces only Kin's literal
   allowlist in the durable root: `/`, `/kin/`, `/paddock/index.html`,
   `/assets/`, the Kin/Outpost installers and their checksums. It never deletes
   or rewrites Paddock's installer, manifest, assets, or immutable releases.
3. Only when HEAD carries the release tag matching `pyproject.toml`
   (`v<version>`), `uv build --wheel` publishes the Kin release channel: the
   real PEP 427 wheel, stable `kin-latest.whl` alias and checksum, static PEP
   503 index, and `version.json` last. The newest five wheels remain available
   for rollback. An untagged main deploy leaves that channel pointer unchanged.

Paddock publishes independently into the same durable root. Its publisher owns
only `/paddock/install.sh`, its checksum, `/paddock/version.json`,
`/paddock/assets/`, and `/paddock/releases/`; it verifies immutable paths and
activates `version.json` last. Neither product release requires the other
repository to release or become public.

`compose up --profile publish` adds two `nginx:alpine` services:
`get` on `${OUTPOST_BIND_IP}:7018` and `docs` on `:7019`. The get bind source
defaults to the durable path above and is configurable with
`KINRA_GET_PUBLISH_ROOT`. For an emergency rollback, set that variable to the
absolute legacy worktree path and redeploy; Kin recognizes that mount as frozen
and skips publication. Remove the override to return to the durable root.

The kloud-vps Traefik routes `get.kinra.ai` / `docs.kinra.ai` to those ports
over WireGuard (routers `kin-get` / `kin-docs` in
`~/kloud/traefik/dynamic/routes.yml`, mirroring `kin-outpost`). A stranger's
`outpost-install.sh` runs plain `compose up` with no profile, so these publishers
never start on a non-kloud host. Dev deploys do not mutate either public
docroot; a missing docs root also fails with verified-main recovery guidance.

## Per-host overrides

The shared template is the *baseline* — the non-secret defaults both
machines start from. Per-host overrides happen three ways, in precedence
order:

1. **CLI flag** (`kin --base-url ... --model ...`) — one-off, for a single launch.
2. **Env var** (`KIN_BASE_URL=... KIN_MODEL=...`) — set in shell for a session.
3. **`~/.kin/settings.toml`** — durable per-machine config.

The checked-in shared template now describes the current private topology:
DS4 Responses at `https://kinra-inference-001.mist-hake.ts.net:8001/v1` and one-slot Qwen
vision/utility at `http://100.87.84.40:18081/v1`. Both machines can use it
unchanged on the Tailnet. The hosted `https://api.kinra.ai/v1` profile is the
authenticated client/Outpost route and should be installed with `kin connect`
so its bearer lands in `credentials.toml`, never this shared file.

## The Taskfile verbs

All under the existing `OUTPOST_SSH` (default `kloud`) + `OUTPOST_DIR`
(default `~/kin`) vars — so they pick up the same overrides as
the deploy task:

| Task | What it does |
|---|---|
| `task kloud:setup` | Run `scripts/setup-dev.sh` on kloud over SSH (bootstrap a fresh box). |
| `task kloud:dev` | SSH into kloud, attach (or create) the `kin` tmux session. |
| `task kloud:verify` | Run `task verify` on kloud — headless suites against the real Docker host. |
| `task kloud:status` | One-shot summary: git state, active tmux sessions, `kin` tool install. |

## What stays out of this repo

This page is the boundary. Per-host concerns don't belong here:

| Concern | Where it goes |
|---|---|
| `~/.ssh/config` (aliases, multiplexing, keys) | Per-host shell/dotfiles |
| `~/.zshrc`, aliases, completions | Per-host shell/dotfiles |
| `~/.gitconfig` `includeIf host` | Per-host git config |
| mosh, Tailscale SSH, devcontainers | Per-host preference |
| VSCode / Neovim configs | Per-host IDE |
| `BRAVE_API_KEY`, provider secrets | `~/.kin/credentials.toml` via Kin's key prompts, or env vars |

If you want any of those synced across machines, they go in a separate
`dotfiles` repo — keep the dev-workflow concerns here, the personal-config
concerns there.

## Flipping between machines

A typical day:

```bash
# Morning on MacOS — harness work
task run
task verify
git push origin dev

# Afternoon on kloud — Outpost-specific work
task kloud:dev            # ssh + tmux auto-attach
cd ~/kin          # dev tree, safe from deploys
task outpost:build        # real Docker host image build
task outpost:up           # smoke-test the image locally
git push origin dev

# Back to MacOS — pick up the kloud commits
git pull --ff-only
task run
```

The repo state stays identical because both machines are symmetric peers
of `origin/dev`. Whoever makes the commit pushes; whoever pulls next
gets it.
