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>
This commit is contained in:
@@ -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/<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.
|
||||||
@@ -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-<subject>.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.
|
||||||
Reference in New Issue
Block a user