diff --git a/COMMS/sidecar-app-store/2026-08-11-per-user-accounts-handoff.md b/COMMS/sidecar-app-store/2026-08-11-per-user-accounts-handoff.md new file mode 100644 index 00000000..f0ef60bd --- /dev/null +++ b/COMMS/sidecar-app-store/2026-08-11-per-user-accounts-handoff.md @@ -0,0 +1,131 @@ +# 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//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. diff --git a/COMMS/sidecar-app-store/README.md b/COMMS/sidecar-app-store/README.md new file mode 100644 index 00000000..9ccc6aab --- /dev/null +++ b/COMMS/sidecar-app-store/README.md @@ -0,0 +1,27 @@ +# COMMS/sidecar-app-store + +A tracked channel between the agents working on the sidecar app store and per-user Linux accounts. + +## Why it is in the repo + +Because the alternative was the owner relaying findings between us by hand, from memory, at the end of long +sessions. A file survives a context window; a message in a chat does not, and neither does the reasoning +behind it. The untracked `COMMS/` at the workspace root is for state about one machine at one moment. This +one is for things any clone should carry. + +## How to use it + +- **One file per handoff**, named `YYYY-MM-DD-.md`. Dated, because "which of these is current" is the + first question a reader has. +- **Write what you verified and what you assumed**, separately and explicitly. A handoff that reads as + confident about something untested is worse than no handoff — the reader will build on it. +- **Name files and lines.** `os-user.ts:362` costs nothing to write and saves the reader a search. +- **Reply in a new file rather than editing someone else's.** An edited handoff loses the record of what was + believed when a decision was made, which is usually the thing that explains the decision. +- **Delete a handoff when it is spent**, the way `platform/CLAUDE.md` says to delete finished checklists. + Something still open belongs here; something done belongs in a commit message or a doc. + +## Where the durable reasoning lives instead + +Handoffs are for coordination — what is broken, what is unproven, what needs deciding. Anything that will +still be true in a month belongs in `docs/per-user-linux-accounts.md` or next to the code, not here.