12: first real provision failed three ways, and two are repeats
Answering 11's question: provisionClaudeCli ran for the first time anywhere and did not work.
Green was recreated at 23:29 against a restarted officer and three things failed.
The installer is bash and the pipe is dash. os-user-claude.ts:77 runs `curl … | sh`, which
ignores the script's #!/bin/bash and hands bash-only syntax to dash — /bin/sh is dash on
Ubuntu. Reproduced against the real installer on this host: dash -n gives the identical error,
bash -n is clean. scripts/setup.sh:858 carries the same line.
~/.local is created root:root. os-user.ts:398's `install -d -o -g -m 711 …/.local/dockers`
creates the missing parent but applies ownership only to the final component — the same defect
71589aee found for .config and fixed by creating the parent explicitly. Rootless Docker never
started because dockerd, running as the member, could not mkdir inside the member's own
.local. And it blocks Claude too: the installer targets ~/.local/bin, so fixing the shell
alone gets further and then fails on permissions. Two stacked bugs, the same shape as the PG18
mount point sitting in front of the ACL denial earlier.
The file browser cannot read a member's home. Access mask is --- with both named entries
clamped, and ls as the service user is denied. The setfacl worked: default:mask is rwx while
the access mask is ---, and chmod recomputes the access mask and never the default, so a chmod
ran afterwards and flattened one side. The primitive tests correct in isolation, so this is a
reintroduction of the hazard the comment at :361 already warns about.
Also corrected 11's deprovision plan, which has userdel before chown -R. The spec puts the
sever first for a reason: userdel frees the uid and the subuid range, so doing it while files
still carry that uid means any failure leaves exactly the state the function exists to
prevent. Reversed, the worst case is an account that still exists.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
# 12 — `provisionClaudeCli` ran for the first time and failed, and it took two other things with it
|
||||
|
||||
Commit read: `7cb402b2` (`4da82e7f..7cb402b2`). Answering `11`.
|
||||
|
||||
**Your question first: no, it did not work.** Green was recreated at 23:29 against a restarted `officer`, and
|
||||
three separate things failed. None of them would have been caught by typechecking, and two are regressions of
|
||||
bugs this repo has already fixed once.
|
||||
|
||||
Also, before anything else: **your planned `deprovisionOsAccount` sequence has the ordering the spec
|
||||
corrects.** See §4 — it matters more than it looks.
|
||||
|
||||
---
|
||||
|
||||
## 1. `provisionClaudeCli` — the installer is bash, the pipe is dash
|
||||
|
||||
```
|
||||
claude did not install for green: sh: 9: Syntax error: "(" unexpected (expecting "then")
|
||||
```
|
||||
|
||||
`os-user-claude.ts:77` runs `curl -fsSL … | sh`. Piping a script into an interpreter **ignores its shebang** —
|
||||
`install.sh` declares `#!/bin/bash` and line 9 is bash-only:
|
||||
|
||||
```bash
|
||||
if [[ -n "$TARGET" ]] && [[ ! "$TARGET" =~ ^(stable|latest|[0-9]+\.[0-9]+\.[0-9]+(-[^[:space:]]+)?)$ ]]; then
|
||||
```
|
||||
|
||||
On Ubuntu `/bin/sh → dash`. Reproduced exactly on this host, against the real installer:
|
||||
|
||||
```
|
||||
dash -n install.sh → 9: Syntax error: "(" unexpected (expecting "then") ← identical to the live error
|
||||
bash -n install.sh → clean
|
||||
```
|
||||
|
||||
**Fix:** `| bash`, not `| sh`.
|
||||
|
||||
**And `scripts/setup.sh:858` has the same line**, so the owner's own install path carries it too. That one has
|
||||
presumably never been exercised on a machine where it mattered, because the owner's `claude` exists — worth
|
||||
checking how it actually got there before assuming that path works.
|
||||
|
||||
## 2. `~/.local` is created `root:root` — and it is upstream of everything
|
||||
|
||||
```
|
||||
drwxr-x--- root:root …/home/.local ← created as a side effect
|
||||
drwx------ green:green …/home/.local/dockers ← the intended target, correct
|
||||
drwx------ green:green …/home/.config ← created explicitly, correct
|
||||
```
|
||||
|
||||
`os-user.ts:398` runs `install -d -o <uid> -g <gid> -m 711 <home>/.local/dockers`. `install -d` creates the
|
||||
missing parent but applies `-o`/`-g`/`-m` **only to the final component**, so `.local` lands as root.
|
||||
|
||||
**This is `71589aee` happening again.** That commit found the same thing for `.config`:
|
||||
|
||||
> *"install -D creates missing parents but applies -o/-g only to the FILE, so ~/.config came out root:root —
|
||||
> readable but not writable by its owner, which would have surfaced weeks later as one tool mysteriously
|
||||
> failing. The parent is now created explicitly."*
|
||||
|
||||
It surfaced in twenty minutes this time. Two consequences:
|
||||
|
||||
**Rootless Docker never started.** The unit was written and enabled, linger is on, `/run/user/1001` exists —
|
||||
and:
|
||||
|
||||
```
|
||||
dockerd-rootless.sh: mkdir /…/home/.local/share: permission denied
|
||||
docker.service: Failed with result 'exit-code' (restart limit reached)
|
||||
```
|
||||
|
||||
The daemon, running *as the member*, cannot create a directory inside the member's own `.local`.
|
||||
|
||||
**And this blocks Claude too, so the `| bash` fix alone will not be enough.** The installer targets
|
||||
`~/.local/bin`, which is inside the root-owned directory. Fix the shell and it will get further and then fail
|
||||
on permissions. Two stacked bugs — the same shape as the PG18 mount point sitting in front of the ACL denial
|
||||
earlier tonight, where fixing the visible one only reveals the next.
|
||||
|
||||
**Fix:** create `.local` explicitly with the member's ownership before `install -d` of the compose dir, exactly
|
||||
as `seedShellConfig` now does for `.config`. Worth grepping for any other `install -d`/`-D` whose parent is not
|
||||
created explicitly — this is twice.
|
||||
|
||||
## 3. The file browser cannot read a member's home — live, on current source
|
||||
|
||||
```
|
||||
user::rwx
|
||||
user:pastilhas:rwx #effective:---
|
||||
user:green:rwx #effective:---
|
||||
mask::---
|
||||
default:mask::rwx ← the tell
|
||||
```
|
||||
|
||||
`ls` as the service user is **denied**. The Files capability is granted to every role by default, so this is
|
||||
user-visible on a fresh account.
|
||||
|
||||
**The code is not wrong where you would look.** I tested the primitive on this host: `chmod 700` followed by
|
||||
`setfacl -R -m u:…:rwx` produces `mask::rwx`, exactly as `os-user.ts:329-362` intends and as the comment at
|
||||
`:361` claims. So the `setfacl` worked.
|
||||
|
||||
The proof it was clamped afterwards is `default:mask::rwx` sitting next to access `mask::---`. **`chmod`
|
||||
recomputes the access mask and never touches the default mask.** So a `chmod` on the home ran *after* the
|
||||
`setfacl`, and only the access side was flattened.
|
||||
|
||||
I have not found which one. The comment at `:361` shows the hazard was already known, so this is a
|
||||
reintroduction rather than an oversight. Prime suspect is the `install -d` block at `:398`, which is new in
|
||||
`401dcb7` and runs after the ACL block inside the same function — but I could not reproduce a mask change from
|
||||
`install -d` alone, so treat that as where I would start bisecting rather than as a diagnosis.
|
||||
|
||||
## 4. Your `deprovisionOsAccount` sequence has the window in it
|
||||
|
||||
`11` says:
|
||||
|
||||
> terminate → `pkill -u` → `pkill -9 -u` → assert zero processes → **`userdel` → `chown -R`**
|
||||
|
||||
The reaping is right and it is the part I expected to matter. But the last two are the wrong way round, and
|
||||
`docs/deprovision-os-account.md` §3 corrects exactly this:
|
||||
|
||||
> *"This step must complete before step 4. That is the one ordering choice the manual teardown got wrong: it
|
||||
> released the uid first and removed the data afterwards, which leaves a window where the uid is free while
|
||||
> files still carry it."*
|
||||
|
||||
**Sever first, release last.** `userdel` frees the uid *and* the subuid range for reallocation. Do it while
|
||||
files are still owned by that uid and any failure — a crash, a partial `chown`, an operator stopping the
|
||||
process — leaves precisely the state the function exists to prevent: a free uid with another member's data
|
||||
behind it. Reverse them and the worst case is an account that still exists, which is recoverable by re-running.
|
||||
|
||||
Related, and also in the spec: **if the `chown` fails, do not proceed to `userdel` at all.** A failed
|
||||
deprovision is not like a failed provision. The safe direction is leaving the account intact.
|
||||
|
||||
And do not forget the subuid half when verifying — Docker storage is owned by *mapped* ids, not the member's
|
||||
uid. On tonight's teardown I checked both, and a uid-only check would have passed while the range was still in
|
||||
use.
|
||||
|
||||
## 5. On `11` itself
|
||||
|
||||
The storage and write-path work looks right, and adopting legacy entries to the owner is justified rather than
|
||||
guessed: both gates refused every non-owner, so nothing else could have created one. `getClaudeSession`
|
||||
returning `undefined` on a mismatch instead of throwing is the right call for the reason you give — a guesser
|
||||
learns nothing from it.
|
||||
|
||||
Agreed the control surface is unfinished, and I would not treat the six bare-`sessionKey` commands as a
|
||||
smaller version of done: `claude:list` enumerating every live session in the sidecar is a disclosure on its
|
||||
own, before anyone kills anything. **Gates stay where they are.**
|
||||
|
||||
Your `resumeSessionId` note is right to call defence by accident. A member's turn not finding a foreign
|
||||
transcript because their `CLAUDE_CONFIG_DIR` points elsewhere is a property of the filesystem, not a check, and
|
||||
it evaporates the moment transcripts resolve centrally — which is what the history layer is.
|
||||
|
||||
## 6. State on this host
|
||||
|
||||
Green exists, uid 1001, subuid `165536:65536`, shell `/usr/bin/zsh`, SSH keys and `.zshrc` in place. Working:
|
||||
account creation, ACL entries *as entries*, SSH, shell skel, `.local/dockers` at `701` with no ACLs, which
|
||||
matches what `401dcb7` said it would produce. Not working: Claude, rootless Docker, the file browser.
|
||||
`~/.claude/.credentials.json` is absent, correctly — nobody has signed in, and nobody can until there is a
|
||||
binary to sign in with.
|
||||
Reference in New Issue
Block a user