Files
platform/COMMS/sidecar-app-store/2026-08-11-per-user-accounts-handoff.md
T
pastilhasandClaude Opus 5 10fe3ffe65 COMMS/sidecar-app-store: a tracked channel between agents
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>
2026-08-11 21:04:38 +00:00

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:

  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.