- Go 72.5%
- Nix 27.5%
Make a fresh sandbox more welcoming: - fish as the login shell (programs.fish.enable; the provisioner now creates trainees with -s fish, and the vestigial client account matches); - a global MOTD printed once per login shell (the broker enters via su - <user>), guiding usage: the box name + http://<name>:<port> reachability, claude, the tooling, and how to leave/destroy; - friendlier defaults: micro, bat, eza, tree, fzf, htop. Also drops the deprecated pkgs.system alias in the image. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| cmd/firsthouse | ||
| images | ||
| internal | ||
| nix | ||
| .gitignore | ||
| DESIGN.md | ||
| flake.lock | ||
| flake.nix | ||
| go.mod | ||
| go.sum | ||
| README.md | ||
firsthouse
Self-service Incus sandbox portal, gated on Tailscale identity. Trainees
ssh firsthouse; the portal learns who they are from their verified Tailscale
identity (tsnet + LocalClient.WhoIs) and gives them a tiny TUI to provision
/ enter / destroy their one sandbox. Sandboxes are Incus containers that
join the sandbox tailnet but run no SSH — the only shell is back through
firsthouse, which bridges the session via incus exec.
This is the application half of Phase 6. The deployment (systemd unit,
hardening, Incus socket access, sops-wired OAuth secret + tag:firsthouse key)
lives in the consuming NixOS config (nixcfg, hosts/oink/), which pins this
repo as a flake input. See DESIGN.md for the full picture.
Layout
cmd/firsthouse/ entrypoint — tsnet node + wish SSH server, WhoIs adapter
internal/identity/ verified Tailscale identity → owner key (the security core)
internal/box/ sandbox name validation (charset + uniqueness backends)
internal/store/ persistent one-box-per-identity ownership map (JSON)
internal/portal/ resolve identity, dispatch subcommands (status/new/enter/destroy)
internal/provision/ the lifecycle recipe: mint → launch → user → join → record
internal/incus/ CLI-backed container manager (launch/exec/delete + shell cmd)
internal/tailnet/ tailnet-B control client (mint key / list / delete device)
internal/broker/ local-PTY ↔ SSH-session bridge for `incus exec -t`
The owner check ("the only thing keeping tenants apart") and the provisioning orchestration are unit-tested against fakes, in isolation from the SSH / Incus / Tailscale plumbing.
Interface
Non-interactive subcommands carried as the SSH command (identity is from the verified tailnet connection, not keys):
ssh firsthouse # status: who you are + your sandbox (if any)
ssh firsthouse new [name] # provision your one sandbox (default name from identity)
ssh -t firsthouse enter # shell into it (brokered via incus exec; needs a tty)
ssh firsthouse destroy yes # tear it down + free the name
Status
The connect path, provisioning/destroy orchestration, and name rules are
implemented and unit-tested; the binary + sandbox image + NixOS module all build
under Nix. The Incus/Tailscale calls compile and mirror the validated Phase-5
scripts, but the end-to-end provision/enter/destroy flow is verified live on the
host (Incus daemon + a real tag:sandbox key), not in CI. An interactive TUI is
a later UX nicety.
Develop
nix develop # go_1_26 + gopls + gotools
go test ./...
nix build # builds the firsthouse binary