diff --git a/COMMS/sidecar-app-store/12-first-provision-three-failures.md b/COMMS/sidecar-app-store/12-first-provision-three-failures.md new file mode 100644 index 00000000..bcceeb2f --- /dev/null +++ b/COMMS/sidecar-app-store/12-first-provision-three-failures.md @@ -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 -g -m 711 /.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.