TazPod: Proxmox LXC Runtime (CT106) Detail

Level 3 (Detail) — the tazpod-proxmox container: provisioning IaC, installed stack, and the operator surfaces it hosts.

Concept

CT106 (tazpod-proxmox, 192.168.1.206) is the Proxmox LXC incarnation of TazPod: the same operator tooling as the Docker flavour, but as an unprivileged container on the tazlab node with the pet volume local-lvm:vm-999-disk-2 mounted at /workspace. The AI agents run here — this container is the operator environment.

The DeepSeek Harness (dsh) Web UI it used to host was decommissioned on 2026-09-25 (see below). Paperclip ran here from 2026-09-25 to 2026-09-26, moved to CT108 and was removed on 2026-10-19 with its decommission. Multica ran here from 2026-09-27 to 2026-10-20 (multica-daemon.service, layers layer6-multica/layer7-multica-team, the Git-declared team) and was decommissioned on 2026-10-20. What CT106 hosts now is the operator environment and its tooling.

Provisioning (tazpod/runtimes/lxc)

PieceNotes
terraform/CT definition: unprivileged, 2 cores, memory_mb/swap_mb/rootfs_size_gb variables, nesting=1 (keyctl is added by the raw phase), mp0 = pet volume → /workspace; Ubuntu 24.04 template
terraform-pet/CT 999 (pet-storage) which owns the disk volume consumed as mp0
ansible/Role tazpod (layers below)
create.shOrchestrator, see phase list

Live resources (verified 2026-09-25): 2 cores, 8192 MB RAM / 1024 MB swap, 50 GB rootfs, features=keyctl=1,nesting=1, static 192.168.1.206/24 — note the RAM/disk are above the Terraform file’s defaults, so the container has been resized since provisioning.

create.sh phases: 0 pet ensure → 1 terraform → 2 raw LXC config (lxc.idmap uid-1000 passthrough, TUN device, cap drops, mp1 Samba bind) → 2b authorize admin keys (pct exec: desktop key installed for root and tazpod) → 3 wait SSH → 3b build the tazpod binary → 4 Ansible baseline → 5 transfer Tailscale OAuth secrets from gopass → 6 Tailscale up → 7 verify.

Where the run happens: create.sh must execute on the trusted orchestration host tazlab (192.168.1.200), not a workstation — the tazpod-provision key is pinned restrict,from="192.168.1.200" on this container (TAZ-12). See Trusted Orchestration Host for the decision, prerequisites and the TAZ-19 acceptance checks.

Ansible layers: layer0-system (packages, locale, user + admin key, sudoers, sshd), layer1-shell (Tailscale, Go, nvm/Node, starship, zoxide, hugo), layer2-aws, layer3-k8s, layer4-ai (pi, gemini-cli, opencode, bun, pi-autoresearch, omp, antigravity), layer5-paperclip (the Paperclip unit — disabled since the 2026-09-26 move — plus the pet-backed pi state directory), layer6-multica (the Multica daemon: binary, unit, env, token), layer7-multica-team (the Git-declared agent roster, reconciled onto the control plane), tazpod-deploy (dotfiles, KUBECONFIG, systemd units, workspace symlinks, legacy cleanup).

Idempotency: verified with ansible-core in a local venv against the live container — a clean second pass reports changed=0 failed=0. Rules learned the hard way:

  • never use /workspace/... as a controller path (that is the path inside the container): sources must be derived from tazpod_workspace ({{ playbook_dir }}/../../../..), which is valid both from a checkout and from inside TazPod;
  • creates: must match the real binary name (herdr, not hdr; Tailscale installs to /usr/bin, not /usr/sbin);
  • .ssh must stay a real directory — Ansible correctly refuses to convert a directory holding live keys into a symlink;
  • authorized_key is no longer in ansible-core ≥ 2.21 (moved to the ansible.posix collection): core file + lineinfile are used instead, with no extra dependencies.

Re-run assessment (2026-09-25): a check-mode run against the live container (-i 'tazpod,' -c local --check --diff) reports ok=71 changed=2 failed=0 skipped=18. The only tasks that would change anything are the TazPod binary copy (same artifact already in the workspace) and the KUBECONFIG copy (diff: a trailing newline). Nothing upgrades packages, reboots, or rewrites /etc/resolv.conf.

The pi state directory is pet-backed (~/.pi)

pi keeps its state under ~/.pi: one session transcript per agent run in ~/.pi/paperclips/<timestamp>-<agentId>.jsonl (the most readable record of what an agent actually did) and the synced agent skills under ~/.pi/agent. pi runs as tazpod, whose home is on the container rootfs, so a create.sh rebuild would destroy both. They are therefore mirrored onto the pet volume — the same durability the Paperclip run logs already get from paperclip_home. Introduced by commit d7dd4ff (tazzo/tazpod master, “lxc(paperclip): keep the pi session transcripts on the pet volume”, refs TAZ-11).

PathRole
/workspace (/dev/mapper/pve-vm--999--disk--2, ext4)the pet volume (mp0, host local-lvm:vm-999-disk-2)
/workspace/.paperclip/home/.picanonical pet-backed pi state directory (0700, owner tazpod)
/workspace/.tazpod/.piolder pet location, still populated on the current host and used as the seed
/home/tazpod/.piaccount path, a symlink into the pet volume — today /workspace/.tazpod/.pi, after the next rebuild /workspace/.paperclip/home/.pi

The mechanism is Ansible, not a manual step: runtimes/lxc/ansible/roles/tazpod/tasks/paperclip-pi-home.yml, imported at the end of layer5-paperclip.yml (tag pi-home) and run by create.sh. On a rebuild layer4-ai first installs the pi extensions into a real ~/.pi; the Paperclip task then:

  1. creates /workspace/.paperclip/home/.pi (0700, tazpod);
  2. seeds it from the older pet location /workspace/.tazpod/.pi with cp -a -n (no clobber), so the transcripts already collected there are not orphaned;
  3. copies any remaining rootfs ~/.pi content into it and removes the real directory;
  4. replaces ~/.pi with a symlink to the pet copy.

The whole block is guarded on “not already a symlink”, so on the steady state — and on today’s host, where ~/.pi already points into the pet volume — it is a no-op and a --check re-run stays clean. tazpod-deploy.yml no longer symlinks .pi (its list entry and symlink loop item were removed), because that task raced layer4-ai’s install into a real ~/.pi.

Caveat — the interactive .bashrc is still a second writer. The dotfiles deployed to CT106 by tazpod-deploy.yml (tazpod/dotfiles/.bashrc, live at /home/tazpod/.bashrc) keep .pi in their AI-tool symlink loop and force it to /workspace/.tazpod/.pi whenever readlink ~/.pi differs from that target:

for _tool in .pi .omp .gemini .claude .antigravity .antigravitycli; do
    _target="/workspace/.tazpod/$_tool"; _link="$HOME/$_tool"
    if [ ! -L "$_link" ] || [ "$(readlink "$_link")" != "$_target" ]; then
        rm -rf "$_link" && ln -sf "$_target" "$_link"
    fi
done

.profile sources .bashrc, so the first interactive/login shell after the Ansible apply repoints ~/.pi back at the legacy path, and /workspace/.paperclip/home/.pi then stops receiving transcripts. The durability guarantee still holds — both locations are on the same pet volume — but the canonical target is not stable until .pi is removed from (or aligned with) that loop. Routed to the Platform Engineer, the tazpod single writer.

Verified live on 2026-09-26 (before the pending apply):

readlink -f /home/tazpod/.pi                              # → /workspace/.tazpod/.pi
findmnt -no SOURCE,TARGET,FSTYPE /workspace               # → /dev/mapper/pve-vm--999--disk--2 /workspace ext4
ls -d /workspace/.paperclip/home                          # → No such file or directory (created on the next rebuild)

Removed: dsh Web UI (decommissioned 2026-09-25)

The DeepSeek Harness Web UI was retired because it is no longer used. What was removed:

RemovedDetail
Ansible layerlayer5-dsh.yml, templates/dsh.service.j2, the restart dsh handler, the dsh_version/dsh_web_port defaults, the layer import, and the dsh checks in create.sh
systemd unit/etc/systemd/system/dsh.service — stopped, disabled, deleted, daemon-reload
Package@deepseek-ai/dsh@0.1.5-rc.1 from the tazpod user’s nvm prefix (npm rm -g, 520 packages removed)
Log/var/log/dsh-web.log
ToolingSKILLS/tools/dsh-web-access/ (tunnel helper, TOOL.md, research note) and its row in SKILLS/tools/SKILL.md

~/.dsh/ (41 MB — profiles, settings.yaml, sessions, backups, the skills symlink) was deleted afterwards on explicit operator instruction. The container is clean: no unit, no package, no binary, nothing listening on 3081, and no ~/.dsh under /home/tazpod or /root.

Historical context worth keeping: dsh pinned privileged RPCs to the loopback origin, so remote access was always an SSH port forward (ssh -N -L 127.0.0.1:3081:…) — never a reverse proxy. That is why the container’s only exposed surface was loopback, and why the nginx reverse proxy from the earlier era was removed in 2026-09-10.

Legacy removed (2026-09-10, residue cleared 2026-09-14)

  • nginx reverse proxy on :3080 (Host-header spoofing, crypto.randomUUID polyfill, local TLS certs) — stopped, disabled, vhost removed, package purged, client polyfill plugin deleted. Residue cleared 2026-09-14: the nginx-era /etc/dsh-tls/ key material was deleted from CT106 and the now-unused infra/dsh-tls/* secrets were removed from the store (TD-065).
  • LAN isLoopback vendor patch in the dsh client bundle (… || pageLocation.hostname === "192.168.1.206") — reverted 2026-09-14 by reinstalling the clean package; verified in the source and in the bundle actually served to the browser. Two helper scripts from the same era were handled: ~/.dsh/scripts/patch-remote-settings.sh deleted, ~/.dsh/scripts/web-login-url.sh rewritten to print the loopback tunnel URL instead of the dead :3080 origin.
  • tazpod-sync.service (S3 sync daemon from the pre-gopass era) — was restart-looping against a nonexistent binary (10,117 restarts); unit removed.

Verification

# the decommissioned surface must be absent
systemctl status dsh            # → Unit dsh.service could not be found
command -v dsh                  # → nothing
ss -tln | grep 3081             # → nothing listens

See Also

  • Parent hub: tazpod
  • Provisioning host: Trusted Orchestration Host (TAZ-19) — tazlab owns /root/.ssh/tazpod-provision, so create.sh runs there
  • Related: lxc-storage-infra (CT 107, different container), Hindsight
  • CRISP: paperclip-lxc-deployment — the application replacing the removed UI on this container