Paliadin: per-user HOME profile for tmux/claude isolation #21

Open
opened 2026-05-08 18:57:46 +00:00 by mAi · 0 comments
Collaborator

Context

The Paliadin PoC currently runs the per-user claude panes (paliad-paliadin-<userid8>) under m's normal HOME=/home/m. That means every claude session — regardless of which Paliad user fired it — inherits:

  • the entire ~/.claude/skills/ directory (every mai-*, lex-*, youpc-* skill, plus all the mcp__* tool surfaces these wire up)
  • m's project-scoped .mcp.json (today: paliad/Supabase only — but no enforcement that it stays minimal)
  • m's ~/.netrc, ~/.config/age, ~/.dotfiles, etc. via filesystem reachability under the m user
  • m's mai-memory MCP namespace, m's CalDAV creds, m's Gitea credentials

For the single-tenant PoC this is fine — services.PaliadinOwnerEmail gates /paliadin to m only. The moment a second account is allowed in, every privileged surface above leaks across users.

Direct prompt-pane access (tmux attach -t paliad-paliadin-<userid8>) for debugging is non-negotiable. Container-on-mRiver is overkill and breaks that workflow.

Proposal

Stage A — Custom HOME per user (this issue)

LocalPaliadinService.startSession(userID) sets HOME=<profileRoot>/<userID> on the spawned tmux session. A new ensureProfile(userID) helper bootstraps the profile lazily:

<profileRoot>/<userID>/
  .claude/
    skills/
      paliadin/        # symlink or copy of the canonical skill
  .mcp.json            # curated: only the Supabase tool, nothing else

What this buys:

  • claude only sees the paliadin skill — no mai-memory, no mai-mail, no lex-*.
  • No accidental access to m's .netrc / dotfiles / other repos via skill-defaults that read from $HOME.
  • Per-user memory MCP state if/when we wire memory back in (each profile gets its own DB pointer).
  • tmux attach -t paliad-paliadin-<userid> still works exactly as today — same machine, same m user, just a different HOME env.

What it does not buy:

  • Kernel-level FS sandboxing. Process can still cat /home/m/.netrc if it actively tries — protection here is "agent doesn't see it, won't ask for it", not "agent cannot reach it".

Default profileRoot: /var/lib/paliadin/profiles (override via PALIADIN_PROFILE_ROOT). Falls back to <repo>/.paliadin-profiles/ for local dev where m doesn't want to touch /var/lib.

Stage B — bwrap wrap (separate follow-up issue, do not block A)

When a non-trusted second user arrives, wrap the tmux launch in bwrap with:

  • --ro-bind /usr /usr --ro-bind /etc /etc
  • --bind <profileRoot>/<userID> <profileRoot>/<userID>
  • --bind /home/m/dev/paliad /home/m/dev/paliad (read-only? TBD)
  • --bind /tmp/paliadin /tmp/paliadin
  • --unshare-pid --unshare-net=disable (or net filter for Supabase only)

Kernel-enforced isolation. tmux attach still possible via nsenter from the host — small friction but acceptable.

Not in scope for this issue. File when needed.

Stage C — Container-per-user (skip)

Docker / nspawn buys multi-tenancy but breaks the tmux attach debugging flow we explicitly want to keep. Skip unless / until paliad goes hosted with multiple paying tenants.

Implementation sketch (Stage A)

  • internal/services/paliadin_profile.go (new): ensureProfile(userID uuid.UUID) (homeDir string, err error). Creates the dir tree on first call, idempotent thereafter. Symlinks ~/.claude/skills/paliadin into the profile so skill updates from the canonical source still flow through.
  • internal/services/paliadin.go: when running tmux new-session, prepend env HOME=<profile> ... to the spawn command (or use tmux set-environment HOME <profile> before new-session).
  • cmd/server/main.go: read PALIADIN_PROFILE_ROOT, default sensibly, log it on boot next to paliadin: local tmux mode.
  • Test: processLateFile and friends keep working (the response dir is unchanged, only HOME changes).

Risks

  • The curated .mcp.json must not omit Supabase by accident — would silently break every SQL recipe in the skill. Add a smoke-test: spawn a fresh profile, run a turn, assert used_tools includes mcp__supabase__execute_sql for a known data question.
  • Symlink to ~/.claude/skills/paliadin couples profiles to m's home — fine for laptop PoC, but if/when this runs on a server without /home/m, the install path needs to change. Use an env var (PALIADIN_SKILL_SOURCE) that defaults to ~/.claude/skills/paliadin but can point elsewhere.

Out of scope

  • Stage B (bwrap).
  • Multi-user auth model (separate "production v1" track per docs/design-paliadin-2026-05-07.md §2).
  • Per-user memory partitioning beyond the HOME-scoped MCP data files.
## Context The Paliadin PoC currently runs the per-user `claude` panes (`paliad-paliadin-<userid8>`) under m's normal `HOME=/home/m`. That means every claude session — regardless of which Paliad user fired it — inherits: - the entire `~/.claude/skills/` directory (every `mai-*`, `lex-*`, `youpc-*` skill, plus all the `mcp__*` tool surfaces these wire up) - m's project-scoped `.mcp.json` (today: paliad/Supabase only — but no enforcement that it stays minimal) - m's `~/.netrc`, `~/.config/age`, `~/.dotfiles`, etc. via filesystem reachability under the `m` user - m's mai-memory MCP namespace, m's CalDAV creds, m's Gitea credentials For the single-tenant PoC this is fine — `services.PaliadinOwnerEmail` gates `/paliadin` to m only. The moment a second account is allowed in, every privileged surface above leaks across users. Direct prompt-pane access (`tmux attach -t paliad-paliadin-<userid8>`) for debugging is non-negotiable. Container-on-mRiver is overkill and breaks that workflow. ## Proposal ### Stage A — Custom HOME per user (this issue) `LocalPaliadinService.startSession(userID)` sets `HOME=<profileRoot>/<userID>` on the spawned tmux session. A new `ensureProfile(userID)` helper bootstraps the profile lazily: ``` <profileRoot>/<userID>/ .claude/ skills/ paliadin/ # symlink or copy of the canonical skill .mcp.json # curated: only the Supabase tool, nothing else ``` What this buys: - claude only sees the paliadin skill — no `mai-memory`, no `mai-mail`, no `lex-*`. - No accidental access to m's `.netrc` / dotfiles / other repos via skill-defaults that read from `$HOME`. - Per-user memory MCP state if/when we wire memory back in (each profile gets its own DB pointer). - `tmux attach -t paliad-paliadin-<userid>` still works exactly as today — same machine, same `m` user, just a different `HOME` env. What it does **not** buy: - Kernel-level FS sandboxing. Process can still `cat /home/m/.netrc` if it actively tries — protection here is "agent doesn't see it, won't ask for it", not "agent cannot reach it". Default `profileRoot`: `/var/lib/paliadin/profiles` (override via `PALIADIN_PROFILE_ROOT`). Falls back to `<repo>/.paliadin-profiles/` for local dev where m doesn't want to touch `/var/lib`. ### Stage B — `bwrap` wrap (separate follow-up issue, do not block A) When a non-trusted second user arrives, wrap the tmux launch in `bwrap` with: - `--ro-bind /usr /usr --ro-bind /etc /etc` - `--bind <profileRoot>/<userID> <profileRoot>/<userID>` - `--bind /home/m/dev/paliad /home/m/dev/paliad` (read-only? TBD) - `--bind /tmp/paliadin /tmp/paliadin` - `--unshare-pid --unshare-net=disable` (or net filter for Supabase only) Kernel-enforced isolation. `tmux attach` still possible via `nsenter` from the host — small friction but acceptable. Not in scope for this issue. File when needed. ### Stage C — Container-per-user (skip) Docker / nspawn buys multi-tenancy but breaks the `tmux attach` debugging flow we explicitly want to keep. Skip unless / until paliad goes hosted with multiple paying tenants. ## Implementation sketch (Stage A) - `internal/services/paliadin_profile.go` (new): `ensureProfile(userID uuid.UUID) (homeDir string, err error)`. Creates the dir tree on first call, idempotent thereafter. Symlinks `~/.claude/skills/paliadin` into the profile so skill updates from the canonical source still flow through. - `internal/services/paliadin.go`: when running `tmux new-session`, prepend `env HOME=<profile> ...` to the spawn command (or use `tmux set-environment HOME <profile>` before `new-session`). - `cmd/server/main.go`: read `PALIADIN_PROFILE_ROOT`, default sensibly, log it on boot next to `paliadin: local tmux mode`. - Test: `processLateFile` and friends keep working (the response dir is unchanged, only `HOME` changes). ## Risks - The curated `.mcp.json` must not omit Supabase by accident — would silently break every SQL recipe in the skill. Add a smoke-test: spawn a fresh profile, run a turn, assert `used_tools` includes `mcp__supabase__execute_sql` for a known data question. - Symlink to `~/.claude/skills/paliadin` couples profiles to m's home — fine for laptop PoC, but if/when this runs on a server without `/home/m`, the install path needs to change. Use an env var (`PALIADIN_SKILL_SOURCE`) that defaults to `~/.claude/skills/paliadin` but can point elsewhere. ## Out of scope - Stage B (`bwrap`). - Multi-user auth model (separate "production v1" track per `docs/design-paliadin-2026-05-07.md §2`). - Per-user memory partitioning beyond the `HOME`-scoped MCP data files.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: m/paliad#21
No description provided.