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