Kin / Maintainers

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.

Read as Markdown

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.

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 + 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:

./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 (scripts/install.sh), which clones the repo itself and skips the tmux step — see 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):

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.

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 worktrees 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; for the Outpost container itself, see Outpost.

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

~/.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:

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:

# 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.