[[
wikihub
]]
Search
⌘K
Explore
People
For Agents
Sign in
Explore
People
For Agents
Sign in
@jacobcole / picortex — planning / docs/specs/001-workspace-isolation-linux-users.md
Suggest edit
Cancel
Submit suggestion
Title
Name
Note
--- visibility: public --- # Spec 001 — Workspace isolation via Linux users **Status:** Draft **Related:** [PRD FR-5..FR-8](../prd/001-picortex-v1.md#workspace-isolation), [ADR-0002](../adrs/0002-linux-users-over-docker.md) ## Goal Each chat (1:1 or group) has its own Unix user, home directory, and filesystem. No chat can read another chat's files. The picortex backend runs as a dedicated service user and enters each chat's context via `runuser`/`sudo -u`. ## Identifiers - **Chat ID** — from Linq's `chat.id` (stable for the lifetime of the chat). Stored in SQLite `chats.id`. - **Chat user** — Linux username `chat-<n>` where `<n>` is the first 8 hex chars of `sha256(chat_id)`. Collisions detected at creation; collisions get `chat-<n>-1`, `-2` etc. - **Home directory** — `$CHAT_WORKSPACE_ROOT/<chat_id>` (default `/srv/picortex/chats/<chat_id>`). Symlinked from `/home/chat-<n>` for standard shell behavior. ## Provisioning Triggered on first inbound message to an unknown chat. Backend (as root via sudoers) runs: ```bash useradd --create-home \ --home-dir "$CHAT_WORKSPACE_ROOT/$CHAT_ID" \ --shell /bin/bash \ --user-group \ "chat-$HEX" chmod 0700 "$CHAT_WORKSPACE_ROOT/$CHAT_ID" chown "chat-$HEX:chat-$HEX" "$CHAT_WORKSPACE_ROOT/$CHAT_ID" # Seed files sudo -u "chat-$HEX" -H bash -c ' mkdir -p .picortex/prompts cp /usr/local/share/picortex/discriminator.default.md .picortex/prompts/discriminator.md git init -q git add -A && git commit -q -m "init" ' # Per-chat cgroup (cgroups v2) mkdir -p "/sys/fs/cgroup/picortex/$CHAT_ID" echo "256M" > "/sys/fs/cgroup/picortex/$CHAT_ID/memory.max" echo "200" > "/sys/fs/cgroup/picortex/$CHAT_ID/pids.max" ``` All steps idempotent (check existence first). Failures roll back via a single `userdel -rf`. ## Teardown Soft: `tmux kill-session -t picortex:$CHAT_ID` + archive home dir to `$CHAT_WORKSPACE_ROOT/_archive/$CHAT_ID.tar.zst` + `userdel -r chat-$HEX`. Hard: `scripts/destroy-chat.sh <CHAT_ID>` removes user, home, archive, cgroup. Logged to SQLite `events` table. ## Sudoers A single drop-in at `/etc/sudoers.d/picortex`: ``` picortex ALL=(%picortex-chats) NOPASSWD: /usr/bin/tmux, /usr/bin/runuser, /usr/bin/bash picortex ALL=(root) NOPASSWD: /usr/sbin/useradd, /usr/sbin/userdel, /usr/bin/chown, /usr/bin/chmod, /bin/mkdir ``` The `picortex-chats` group auto-includes every `chat-*` user (added at `useradd` time). ## Security invariants 1. Backend can read any chat's home (runs as root *via sudoers*, not ambient). Chat users cannot read other chat users' homes (`0700`). 2. Chat users have no sudo, no passwordless privilege escalation. 3. Chat users' `PATH` contains `/usr/local/bin/picortex-shims` + system defaults — the shims directory holds allowlisted wrappers (e.g. `claude`, `git`, `rg`, `jq`) and denies everything else via a restrictive `bash --restricted` profile when using the web terminal. 4. `/tmp` is per-user (via `pam_namespace`). 5. Network egress is firewalled; see [Spec 008](008-observability.md) for allowlist. ## Testing - **Unit:** provisioning pure functions (user-name derivation, path composition). - **Integration:** spin up, spin down, assert `0700`; assert chat-A can't `cat` chat-B's `~/.picortex/prompts/discriminator.md`. - **Stress:** 50 concurrent provisions; no collisions, no orphan users. - **Chaos:** kill the backend mid-provision; assert cleanup on next start. ## Open questions - OQ1: Should we tighten with `bubblewrap` in v1 or wait for D2? — Leaning wait. - OQ2: How to handle Linq chat-name changes? (Chat ID is stable, so probably ignore.) - OQ3: Per-chat cgroups require cgroups v2; does our deploy target support? — Hetzner does; HMA macOS does not. D1 question.