Findings were being relayed through the owner by hand, from memory, at the end of long sessions. A file survives a context window and carries its reasoning; a message does not. The untracked COMMS/ at the workspace root stays what it is — state about one machine at one moment. This one is in the repo because any clone should carry it. First handoff covers what I would otherwise have asked the owner to pass on: the bind-mount container test I could not run here and how to retrofit green, the setup-dockers.sh PG18 layout left deliberately alone, the terminal replay bug and the deprovision/uid-reuse hole with a proposed fix, the shared-home question I cannot answer without the passwd and users rows, and the four things most likely to surprise a reader — bootstrap-only default grants, chat grantable but refused, Bun.spawn ignoring uid, and members never getting the owner's anthropic proxy. The README states the convention: dated files, verified separated from assumed, name lines, reply in a new file rather than editing someone else's, and delete a handoff when it is spent. Durable reasoning goes in docs/ or next to the code — this directory is for coordination, not for the record. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7.0 KiB
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:
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:
- Delete a member. Their home stays (correct — we never delete data), owned by uid 1001.
- Create another.
useraddhands out 1001 again. - 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-userthendisable-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 stayschown -Rtheir 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
writefor 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.tsrefuses non-owners wholesale and the socket is refused inserver.tsx. The permission and route exist; the agent still runs as the owner. Both guards say so, andregistry.test.tsnames them so an edit cannot move one without the other. Bun.spawnsilently ignoresuid/gid. Verified on 1.3.10. Every privilege drop goes throughsudo -n setpriv;os-user.test.tspins 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
- Confirm the bind mount on your host (§1) — cheapest, and it closes the loop on your report.
- The terminal replay bug (§3) — visible to a member every day.
deprovisionOsAccount(§3) — quietly the most dangerous item here.- Per-user Claude: install the CLIs into the member's home, detect login by reading
~/.claudeas 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.