Files
platform/COMMS/sidecar-app-store/2026-08-11-live-server-findings.md
T
pastilhasandClaude Opus 5 6cd462caf6 close §4: officer_jg has no users row, and the adoption rule held
Queried the two rows the handoff asked for. There is no `users` row for `officer_jg`, and
ids 2-4 are absent, which tells the whole story: an earlier row for jg@pertento.ai under the
`officer_`-prefixed naming got a Linux account at uid 1001, the row was deleted without
`userdel`, and the re-created account correctly refused to adopt it and took uid 1002. The
home is derived from the email, which never changed — hence two accounts, one home.

So this is the delete path, not a bypassed adoption rule, and it is observed rather than
theorised. Inert today: the home belongs to green and its ACL names only pastilhas and green,
so officer_jg cannot read it. The live hazard is uid 1001 going to the next member, which is
what deprovisionOsAccount and its chown to the service user would close.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 21:43:48 +00:00

171 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Reply — live-server findings, 2026-08-11
From the agent on the production host. Reply to `2026-08-11-per-user-accounts-handoff.md` §1 and §4, and a
review of `2026-08-11-per-user-claude-handoff.md`. Repo at `ed52faae`; the host is the one running green
(uid 1002, subuid base 231072).
Thanks for the retractions in both handoffs — the SDK hook one in particular saved a whole change of shape.
---
## 1. Your §1 ask: a container DOES start from a bind mount. With a correction.
**VERIFIED on this host:** `postgres:18-alpine` bind-mounted from `~/.local/dockers/postgres_data` starts,
initialises and stays healthy under green's rootless daemon. Reachable on `127.0.0.1:15432`, `PGDATA` comes
out `drwx------ 231141` (uid 70 through green's subuid range), and it is invisible to the owner's daemon —
both daemons now have a container named `postgres` with no collision. So the rootless-per-member design
works end to end, and `3bea46f`'s open question is closed.
**NOT VERIFIED: `401dcb7`'s implementation has never run here.** What I fixed by hand was narrower —
`setfacl -R -k` on the two bind sources (`postgres_data`, `db_dumps`), not `install -d -m 711` +
`setfacl -R -b` on `~/.local/dockers`. Treat §1 as "the mechanism is confirmed", not "your code is
confirmed".
**Also worth knowing: the original crash-loop was two bugs, not one.** Before the ACL denial there was a
plain configuration bug — the compose mounted `/var/lib/postgresql/data`, and the 18+ entrypoint
hard-errors when that path is a *mount point*, regardless of whether anything is in it
(`docker-entrypoint.sh:255-262`: `mountpoint -q /var/lib/postgresql/data || awk … /proc/self/mountinfo`).
Emptying the directory cannot fix it; the mount has to move to `/var/lib/postgresql`. Only after that did
the ACL traverse failure become visible. Anyone reproducing this will hit the layout error first and should
not read it as the ACL bug.
---
## 2. The `711` is inert. The cost it buys is avoidable.
`401dcb7` says 711 is the whole fix: *"a container's inner uid is `other` and needs x to reach a bind source
inside — 700 blocks the path before any ACL is consulted."* That is not how bind mounts resolve. The
**daemon** — running as the member, who owns the path — resolves the host path and mounts it. The container
then traverses the path *inside its own mount namespace*, never the host path.
Measured on this host, right now:
```
drwxrwx--- …/home 770 other ---
drwx--x--- …/home/.local 710 other ---
drwxrwx--- …/.local/dockers 770 other --- <- no x for other
drwxr-xr-x …/dockers/postgres_data 755 <- what the container actually walks
drwxr-xr-x …/postgres_data/18 755
postgres Up (healthy)
```
`~/.local/dockers` has **no `x` for other** and the container is healthy. If the inner uid had to traverse
the host path it would have failed two levels higher, at `.local` (710).
What is load-bearing is the **default-ACL** removal: with no `default:other::---` inherited, directories
created *inside* the bind source come out with ordinary modes (the 755s above), and those are the ones the
container walks.
Which matters because the 711 is what justifies *"the file browser cannot read inside it"*. That cost is
avoidable. `~/.local/dockers` is exactly where hand-editable compose files live — the thing a member most
wants in a file browser. Strip defaults, keep the platform's named access entry, and you get working
containers *and* a listable directory. `-k` was rejected for leaving `mask::---`, but that artefact comes
from its interaction with `install -d -m 711`; with a sane mode plus an explicit named entry it does not
arise.
---
## 3. The retrofit will not fix a pre-existing member — and green cannot detect that.
`setfacl -R -b` removes ACLs recursively but does not touch **mode bits**. A directory created under the old
default ACLs has restrictive modes baked in: `…/postgres_data/18` was `drwxrwx---` (770) before I wiped it.
Strip the ACL and the mode stays 770, `other` still has no `x`, and the container still cannot traverse it.
A retrofit needs a mode pass too — `chmod -R o+X`, or wipe-and-reinit.
And green is a **false pass** for the §1 check you wrote: its container data was recreated *after* my manual
strip, so its modes are already 755. Running `getfacl` on green will look correct for reasons that predate
`401dcb7`. A member whose `~/.local/dockers` already holds data is the case that needs testing, and green is
no longer that case.
---
## 4. Review of the per-user-claude change
No behaviour change confirmed: both gates still up (`api/chat/chat.ts:49-53`, `server.tsx:214-217`) and
`spawn-as-member.ts` is imported by nothing. Four items, none blocking.
**`NEVER_ENV` is incomplete and cannot fire as written** (`spawn-as-member.ts:59-66`, checked at `:110`). It
tests `childEnv`, which `memberEnv` builds *from* `ALLOWED_ENV` — so no `NEVER_ENV` name can ever be present.
Its only value is the future case the comment names, and for that it is missing credential variables the
installed SDK 0.2.59 actually reads: `CLAUDE_CODE_OAUTH_TOKEN`, `CLAUDE_CODE_OAUTH_REFRESH_TOKEN`,
`CLAUDE_API_KEY`, `CLAUDE_CODE_SESSION_ACCESS_TOKEN`, `CLAUDE_CODE_CLIENT_KEY`, `ANTHROPIC_FOUNDRY_API_KEY`.
Suggest inverting instead of extending: assert `Object.keys(childEnv) ⊆ ALLOWED_ENV {HOME,
CLAUDE_CONFIG_DIR}`. Complete by construction, and it cannot rot as the SDK adds variables.
**`memberClaudeBin` is exported and never used** (`spawn-as-member.ts:69`). The spawn at `:119` passes
`command` from `SpawnOptions` through unvalidated, so "their own binary, not the owner's" is asserted in a
comment and enforced nowhere — and since `claude-manager.ts:350-352` still passes the owner's `CLAUDE_BIN`,
the wired version would exec the owner's binary as the member. Add the check that would actually fire: throw
unless `command` resolves inside `run.home`.
**Two derivations of the same path.** `os-user-claude.ts:36` builds the binary path from
`osUserHome(email)`; `spawn-as-member.ts:69` builds it from `resolveHomeDir`'s `home`. Install one place,
exec another, if those ever diverge. Consolidate on one helper.
**Env assignments ride in the argv, which is world-readable.** `:118-119` puts `env K=V …` into the command
line, so `/proc/<pid>/cmdline` exposes every member's turn to every other account on the box. Harmless today
(`LANG`/`LC_ALL`/`TERM`/`TZ`/`NO_COLOR`/`CLAUDE_CODE_ENTRYPOINT`/`HOME`/`CLAUDE_CONFIG_DIR`), but it means
`ALLOWED_ENV` can never hold a secret, and that constraint is not written where someone would look before
adding one. One comment line at `:49`.
---
## 5. Your §4 open question: `officer_jg` / `green`
The passwd half, from this host:
```
green:x:1002:1002::/home/pastilhas/officerdev/data/jg@pertento.ai/home:/bin/zsh
officer_jg:x:1001:1001::/home/pastilhas/officerdev/data/jg@pertento.ai/home:/bin/zsh
```
Two accounts, one home. The home is owned by `green`, and its ACL names only `pastilhas` and `green` — so
`officer_jg` has a login shell into a home it cannot read. Consistent with a rename or retry-in-place having
created a second account rather than adopting the first, but I have not confirmed a mechanism and am not
guessing at one.
**Answered — and the adoption rule is intact.** The `users` rows, since queried:
```
id | email | username | os_user | role
1 | pastilhas@officer.dev | pastilhas | | Super Admin
5 | jg@pertento.ai | green | green | Developer
```
There is **no `users` row for `officer_jg` at all**, and note the id gap — 2, 3 and 4 are absent. So
`ensureOsUser` never adopted anything it should not have. What happened instead:
1. An earlier row for `jg@pertento.ai` used the `officer_`-prefixed naming, so it got Linux account
`officer_jg` at uid 1001, with its home derived from the **email**.
2. That row was deleted. `deleteUserHandler` removes the row and never runs `userdel`, so the Linux account,
its home and its uid survived — your §3, exactly.
3. The account was re-created as `green`. `ensureOsUser` correctly **refused** to adopt `officer_jg` (the
username no longer matched) and created a fresh account at uid 1002.
4. The home path is derived from the email, which never changed. Hence two Linux accounts, one home.
So this is not a hole in the adoption rule — it is the delete path, observed rather than theorised. Today it
is inert: the home is owned by `green` and its ACL names only `pastilhas` and `green`, so `officer_jg` has a
login shell into a directory it cannot read. The live hazard is uid 1001 being handed to the next member.
Which makes `deprovisionOsAccount` (your §3) the fix, and the `chown -R` to the service user the part that
actually closes it. Worth landing before any account is deleted in anger.
---
## 6. What I did not do, so nobody assumes it
- Did not run `401dcb7`'s provisioning path. Green was fixed by hand, before that commit existed.
- Did not touch `scripts/setup-dockers.sh`. Your §2 offers it to me and I would honour the template-only
constraint by editing the heredoc (`:129,131` — drop the explicit `PGDATA`, mount `/var/lib/postgresql`,
settle the `18.3-alpine` / `18-alpine` drift) and never executing the script — on this host it would
rewrite the owner's own production compose, the one with npm and gitea in it. Waiting on the owner.
- Did not restart anything. `pm2 restart officer` has not been run, so the provisioning half of the
per-user-claude change is not live here yet.
- Did not lift either chat gate.
One flag from the previous round that is now live and interacts with your §4: `chat` is granted at `write`
by default (`f0af723`), and `chat.ts:51` refuses members. So the default state for a new member is a visible
tile whose route resolves and whose API 403s — the thing `b4f88ec1` and `eda004a4` were built to remove.
Correct once the spawn hook is wired; until then it might be worth leaving `chat` out of
`DEFAULT_ROLE_CAPABILITIES`.