# Handoff — per-user Linux accounts, 2026-08-11

Branch `sidecar-app-store` @ `401dcb7`. Everything below is on that branch; master is untouched.

Written for whoever picks this up next, including the agent on the production host who reported the
postgres bind-mount failure. Thank you for that report — the diagnosis was better than mine and it corrected
a commit conclusion of mine that was wrong.

---

## 1. Needs verifying on the live server (I could not)

**A container starting from a bind mount inside `~/.local/dockers`.**

`401dcb7` adds that directory at provision time, mode `711`, all ACLs removed (`setfacl -R -b`). It is the
answer to your postgres report: the inner uid (70 → 231141 through the subuid range) is `other`, and `other`
needs `x` on every directory on the path. `700` denies it before any ACL is consulted, and `-k` alone left
`mask::---` behind so named entries read `rwx #effective:---`.

Verified here: the directory comes out `user::rwx group::--- other::--x`, no ACL, no defaults.

**Not verified here**, and this is the ask: an actual container. My host recycles uid 1001 across probe
accounts, and the stale `/run/user/1001` is a systemd runtime *mount* that survives `rm`, so a fresh probe
account gets "Failed to connect to bus" and rootless Docker never starts. Your host has green at uid 1002
with no recycling.

To retrofit green: press the terminal icon on his row in Settings → User management. `ensureOsUser` is
idempotent and will create and strip the directory. Then:

```bash
sudo getfacl -p ~/officerdev/data/<his-email>/home/.local/dockers
# expect exactly: user::rwx / group::--- / other::--x   and NO default: lines
```

If his directory predates `401dcb7` it will have inherited ACLs; the strip should clear them. Then a
bind-mounted `postgres:18-alpine` under it is the real test — a **named volume passes with the bug present**,
which is exactly how my first fix looked complete.

Note the fix is bounded on purpose: a bind mount from elsewhere in the home still hits the denial. This is
the place that works, not a promise about everywhere. Reasoning for rejecting the alternatives (extend the
strip / `d:other::--x` on the whole home) is in `docs/per-user-linux-accounts.md`.

---

## 2. Yours if you want it — I left it alone deliberately

**`scripts/setup-dockers.sh:129,131` — the pre-18 Postgres layout.**

Your diagnosis is right and I agree with the fix (mount `/var/lib/postgresql`, drop the explicit `PGDATA`,
align the `18.3-alpine` / `18-alpine` drift). I did not do it because that file provisions the platform's own
Postgres and I had no runway left to test it.

One constraint if you take it: **template only, never an existing compose file.** Live installs have data in
the old layout; changing a running one is a migration, not a fix.

---

## 3. Known broken, not yet fixed

**The terminal replays terminal QUERIES, which get typed into the shell.**

Symptom, seen on green: `$y2026;…$y2048;…1;2c…rgb:1a1a/1a1a/2e2e` accumulating on the prompt line while
idle, with the cursor at the end of it. Those are DECRPM mode reports, a DA1 reply and an OSC colour reply —
*answers*, arriving as keystrokes.

Mechanism: `src/servers/sidecar/pty/sessions.mjs` sends the whole scrollback as `{type:'replay'}` on attach.
If the buffer contains query sequences, replaying them **re-asks the questions**; xterm.js answers; the
answers arrive as input, get echoed into the buffer, and the next attach replays a longer version.

Fix: strip query sequences in `appendBuffer` so what we store and replay is output only. A replay must
reproduce output, never re-issue requests. Ctrl-C clears the line meanwhile.

**Deleting a member leaves their entire Linux side, and a recycled uid inherits it.**

`deleteUserHandler` removes the row and cascades the database. `userdel` never runs. So the account, home,
keys and rootless Docker daemon all survive, and then:

1. Delete a member. Their home stays (correct — we never delete data), owned by uid 1001.
2. Create another. `useradd` hands out 1001 again.
3. The new member owns the previous member's home, keys and Docker storage. By uid, not by any decision.

Proposed `deprovisionOsAccount`, called from the delete handler:

- `loginctl terminate-user` then `disable-linger` — stops their Docker and clears the stale user manager
  (this is what broke my test rig above, so it is not hypothetical)
- `userdel`, never `-r`, so the data stays
- **`chown -R` their tree to the service user** — the part that actually closes it. Data preserved, uid link
  severed, so a recycled uid inherits nothing.

Better than a monotonic uid allocator: simpler, and it fixes the orphaned-files half too.

---

## 4. Open question I cannot answer

**The stale `officer_jg` / `green` shared-home report.** That predates anything I have seen. If two platform
accounts point at one home, it is more serious than either bug above — it would mean `ensureOsUser`'s
adoption rule was bypassed, and that rule is what makes an un-prefixed username safe (it refuses to adopt an
account whose passwd home is not the one we are about to confine, and refuses any uid below 1000).

What would settle it: the `getent passwd` lines for both, and their `users` rows (`id, email, username,
os_user`). I would rather not guess at a mechanism from a symptom here.

---

## 5. Things that will surprise you

- **Default grants seed at bootstrap only.** Terminal, Chat and Files at `write` for every role, inserted
  when the owner account is created. An install past bootstrap — yours — needs them set once by hand. This is
  deliberate: seeding repeatedly would resurrect a revocation.
- **Chat is grantable and deliberately refused.** `api/chat/chat.ts` refuses non-owners wholesale and the
  socket is refused in `server.tsx`. The permission and route exist; the agent still runs as the owner. Both
  guards say so, and `registry.test.ts` names them so an edit cannot move one without the other.
- **`Bun.spawn` silently ignores `uid`/`gid`.** Verified on 1.3.10. Every privilege drop goes through
  `sudo -n setpriv`; `os-user.test.ts` pins Bun's behaviour so we find out if it ever changes.
- **Members must never get `ANTHROPIC_BASE_URL`.** The owner's proxy holds the owner's credential. Per-user
  Claude means their own login in their own home — I suggested the proxy earlier in the session and it was
  wrong.

---

## 6. Next, in the order I would do it

1. Confirm the bind mount on your host (§1) — cheapest, and it closes the loop on your report.
2. The terminal replay bug (§3) — visible to a member every day.
3. `deprovisionOsAccount` (§3) — quietly the most dangerous item here.
4. Per-user Claude: install the CLIs into the member's home, detect login by reading `~/.claude` as root,
   render a terminal with instructions when absent, then move the history layer
   (`claude-sessions.ts`, the pwd picker, the session list) onto per-caller resolution the way the file
   browser was moved. opencode is installer-only for now — master moved it to a single shared serve, which is
   at odds with per-user and needs its own think.
