Repo docs

Per-repo documentation — each repo's docs/ and README, ingested and associated with the repo. Rendered through the <<<RepoDocs>>> tag.

← gitlab__testmonkeyalpha-group__git-surface-mcp docs · docs/workstation-fabric-host.md

The workstation fabric host — build, auth, runbook

The git-surface fabric (fabric_up / fabric_launch / fabric_leases / fabric_instances / fabric_attach) does not run on the Mac. It lives inside a single Linux VM — the workstation libvirt domain (qemu:///session, reachable at ssh -p 2225 [email protected]). Inside that one VM, OVS is native, so every macOS limitation (no kernel OVS, no tap devices, no <virtualport>, no privilege) simply doesn't apply. Clients are Incus instances with a bridged NIC on the OVS bridge fabric; they DHCP a 10.1.0.x from the fabric's own dnsmasq.

Mac (macOS/arm64/HVF)
└── libvirt domain "workstation"  (Ubuntu noble cloud image)
    ├── OVS bridge  fabric   10.1.0.1/24   (gateway)
    ├── dnsmasq     DHCP 10.1.0.50–200, NAT (MASQUERADE) out enp0s30 (the SLIRP uplink)
    └── Incus instances  eth0 = bridged nic, parent=fabric  → 10.1.0.x

Build it from the cloud image + seed — NOT a desktop ISO

The host is built from the Ubuntu cloud image overlaid + cloud-init-seeded. This is the single most important lesson: earlier attempts booted the Ubuntu desktop ISO (casper / autoinstall) and never provisioned — wrong base entirely. The cloud image + NoCloud seed is the correct, fully automated path.

Artifacts (all under ~/bin/config/vm/): - noble-arm64-base.img — Ubuntu noble arm64 cloud image (the seedable base) - workstation.qcow2 — 20 GiB qcow2 overlay on the base (qemu-img create -f qcow2 -F qcow2 -b) - workstation-seed.iso — NoCloud CIDATA seed built from ~/code/vm/workstation/seed/ (hdiutil makehybrid -iso -joliet -default-volume-name CIDATA) - workstation-vars.fd — per-domain edk2 UEFI nvram (raw copy of edk2-arm-vars.fd) - workstation.xml — the domain: edk2 UEFI (raw loader + raw nvram), vda=overlay, sda=seed cdrom (virtio-scsi), a virtio-9p codeshare~/code mount, VNC, and a user-net uplink with hostfwd=tcp::2225-:22 on an explicit high PCI slot (bus=pcie.0,addr=0x1e, to dodge the libvirt pcie-root-port slot-1 collision).

The seed (~/code/vm/workstation/seed/user-data, cloud-config): user steve (sudo, incus-admin), packages openvswitch-switch incus git python3 bridge-utils uidmap, enables openvswitch-switch, mounts the 9p codeshare at /mnt/code. meta-data carries instance-idbump it to force cloud-init to re-run on a fresh overlay.

Auth: the fix for "fabric host unreachable" (was BUG-0003)

The MCP sshes with BatchMode=yes (no IdentityFile) — so it offers only default/agent identities and cannot fall back to a password. The original seed set a password for steve but no authorized key, so the MCP could never authenticate — the host read as unreachable. Fixed at the root, three parts:

1. A dedicated key ~/.ssh/workstation (ed25519, no passphrase) — for [email protected] only. 2. Its pubkey is authorized in the seed (steve.ssh_authorized_keys). 3. A scoped ~/.ssh/config stanza so the MCP's keyless ssh offers exactly that key:

   Match user steve host 127.0.0.1
     IdentityFile ~/.ssh/workstation
     IdentitiesOnly yes

The key is not a secret worth guarding like the wallet seed, but it is host-scoped and passphrase-less by design (unattended MCP access to a loopback-only VM).

Runbook (verified 2026-07-12)

# 1. build (once) — overlay, nvram, seed
cd ~/bin/config/vm
qemu-img create -f qcow2 -F qcow2 -b noble-arm64-base.img workstation.qcow2 20G
cp -f /opt/homebrew/share/qemu/edk2-arm-vars.fd workstation-vars.fd
hdiutil makehybrid -quiet -iso -joliet -default-volume-name CIDATA \
  -o workstation-seed.iso ~/code/vm/workstation/seed/

# 2. define + boot; cloud-init takes several minutes (installs OVS + Incus over the uplink)
virsh -c qemu:///session define ~/bin/config/vm/workstation.xml
virsh -c qemu:///session start workstation
ssh -p 2225 -i ~/.ssh/workstation [email protected] 'cloud-init status --wait'

# 3. one-time, inside the VM: incus needs a storage pool; forwarding for NAT
ssh -p 2225 -i ~/.ssh/workstation [email protected] \
  'incus admin init --minimal && \
   echo net.ipv4.ip_forward=1 | sudo tee /etc/sysctl.d/99-fabric.conf && \
   sudo sysctl -w net.ipv4.ip_forward=1'

# 4. drive the fabric via the MCP: fabric_up -> fabric_launch <name> -> fabric_leases

Verified state after fabric_up: {bridge: fabric, gateway_10.1.0.1: true, dhcp_running: true, nat_out: true, healthy: true}.

Gotchas

- incus not initializedfabric_launch's incus init fails with no storage pool. Run incus admin init --minimal once (dir pool, no incus-managed network — we bridge to OVS manually). - ip_forward off → the MCP's fabric_up sets the MASQUERADE rule but not forwarding; instances get a lease but can't route out. Set net.ipv4.ip_forward=1 (persisted above). - 9p may not mount on some qemu/macOS builds; the seed uses nofail, and the MCP works over ssh regardless — /mnt/code is a convenience (the reference fabric_up.sh), not a dependency. - fabric_launch pulls an image (default images:debian/12) over the uplink on first use — minutes, and non-deterministic. Pre-seed a local Incus image, or keep a warm base instance, for fast/repeatable spins (see network-replay.md).