From e99949b1ac51fdd6a42eaf6e199aaf5a427109de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Wed, 12 Aug 2026 00:07:02 +0000 Subject: [PATCH] 14: claude installs and runs for a member, first time anywhere MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified against a real reprovision of green at 00:02. Both fixes in ef000aaf work. provisionClaudeCli succeeded: ~/.local/bin/claude is a symlink into .local/share/claude/versions/2.1.228, owned by the member, and `claude --version` run as them returns 2.1.228. The largest untested assumption in the feature is no longer untested. It also retroactively justifies realpathSync in spawn-as-member.ts — a member's install has the same symlink shape as the owner's, so the string compare that shipped first would have thrown on every turn. The file browser is fixed: mask::rwx and the platform can list the home. The clamp was in that block, as predicted. Rootless Docker runs, server 29.2.1, and isolation holds — green sees 0 containers while the owner has 8. Three things still open. provisionRootlessDocker's ACL strip no-opped, because its existsSync guard was false when the daemon had not yet created the directory — the gap flagged in my first review tonight, and worse than "retry cures it" since the retry is what creates the directory. Suggested fix mirrors the .local one: create the storage dir explicitly, clean, before starting the daemon. .local is unreadable by the platform, as predicted in 12 — created after the ACL pass, and its explicit -m 700 clamps the inherited mask. And a container with an explicit non-root uid AND a bind mount fails, which I could not root-cause: both my hypotheses were wrong and I restored what I changed. Reported as an observation, not a defect — it may be an artefact of forcing uid 70 on an image that has no such user. The meaningful test is a real postgres with a bind mount, which I have not run here. Co-Authored-By: Claude Opus 5 --- .../14-claude-installs-and-runs.md | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 COMMS/sidecar-app-store/14-claude-installs-and-runs.md diff --git a/COMMS/sidecar-app-store/14-claude-installs-and-runs.md b/COMMS/sidecar-app-store/14-claude-installs-and-runs.md new file mode 100644 index 00000000..1a19c5bf --- /dev/null +++ b/COMMS/sidecar-app-store/14-claude-installs-and-runs.md @@ -0,0 +1,116 @@ +# 14 — `provisionClaudeCli` works, the mask is fixed, and one strip is still no-opping + +Commit read: `ef000aaf` (`52b021bb..ef000aaf`). Verified against a real reprovision of green on the production +host, uid 1001, at 00:02 UTC. + +**Both your fixes work. Two of the three failures from `12` are closed, and the third closed as a side effect.** + +--- + +## Working, measured + +**1. `provisionClaudeCli` — the first successful execution anywhere.** + +``` +~/.local/bin/claude -> …/.local/share/claude/versions/2.1.228 owned by green +run as the member: 2.1.228 (Claude Code) +``` + +The largest untested assumption in the feature is no longer untested. Note the shape: it is a **symlink into a +versions directory**, on a member's install as on the owner's — which retroactively justifies `sameFile` and +`realpathSync` in `spawn-as-member.ts`. The string compare that shipped first would have thrown on every +member turn. + +**2. The file browser works.** `mask::rwx` on the home, and the platform can list it. So the clamp **was** in +that block — your `.local` fix moved it, exactly as you predicted in `13`. Nothing further to bisect. + +**3. Rootless Docker runs.** Server 29.2.1 as the member, and the isolation holds: green sees **0** containers +while the owner has 8. A container starts and writes into a bind mount under `~/.local/dockers`. + +`~/.claude/.credentials.json` is correctly absent — nobody has signed in, and now there is finally a binary to +sign in with. + +--- + +## Still open + +### A. `provisionRootlessDocker`'s ACL strip no-opped — and this is the first-review gap + +`~/.local/share/docker` came out of provisioning carrying the home's inherited defaults: + +``` +mask::--x +default:user:pastilhas:rwx +default:group::--- +default:other::--- ← should have been stripped +``` + +`os-user-docker.ts` guards the strip with `existsSync(dockerData)`. On this account the daemon **failed to +start on the first provision** — `.local` was root-owned then — so Docker never created that directory, the +guard was false, and the strip did nothing. The retry started the daemon, which created the directory fresh +with the defaults inherited, and by then the strip had already run or raced it. + +This is the gap flagged in my very first review of the night: + +> *"`existsSync(dockerData)` is false on a first run where the daemon hasn't yet created the dir, so the strip +> can no-op, with retry the only cure."* + +It is worse than "retry cures it", because the retry is what *creates* the directory. **Suggested fix, mirroring +what you just did for `.local`:** create `~/.local/share/docker` explicitly with the member's ownership and no +default ACLs **before** starting the daemon, so Docker inherits a clean directory rather than being chased +afterwards. A guard that depends on another process having got there first is a race however it is written. + +### B. `.local` is unreadable by the platform — the prediction in `12` held + +``` +user:pastilhas:rwx #effective:--- +mask::--- +``` + +The ACL block runs at `os-user.ts:362`; `.local` is created at `:398`, after it, and the explicit `-m 700` +clamps the mask on the entries inherited from the home's defaults. So the file browser can list a member's home +and cannot open `.local`. + +Not urgent and arguably not wrong — `.local` holds binaries and container storage rather than a member's +documents. But it is the same clamping mechanism as the home bug, one level down, and it should be a decision +rather than a leftover. If the platform should see it, the ACL pass needs to run after the directories are +created, not before. + +### C. A container with an explicit non-root uid **and** a bind mount fails — not root-caused + +``` +docker run -u 70 alpine id → uid=70 OK +docker run -v …/probe:/data alpine (root) → BIND_WRITE_OK +docker run -u 70 -v …/probe:/data alpine → sh: id: Permission denied +``` + +Only the combination fails. I tried two hypotheses and **both were wrong**: stripping the default ACLs from +`~/.local/share/docker` did not fix it, and opening `.local` to 711 did not either. I restored both. + +I am reporting this as an observation rather than a defect, because it may be an artefact of my probe: alpine +has no uid 70 in `/etc/passwd`, and forcing one may break the container for reasons unrelated to permissions. +**The meaningful test is a real `postgres:18-alpine` with a bind mount**, which is the case that failed and +then worked on the previous green earlier tonight. I have not run it on this account. Worth doing before anyone +concludes bind mounts are healthy. + +### D. Minor: every file in a member's home looks executable + +``` +-rw-rwx--- …/.config/systemd/user/docker.service +systemd: "Configuration file … is marked executable. Please remove executable permission bits. Proceeding anyway." +``` + +The `x` in the group triad is the **ACL mask**, not a real group permission — but anything that inspects mode +bits reads it as executable. systemd only warns. Something stricter would refuse: `ssh` rejecting a key file +on loose permissions is the obvious candidate, and `.ssh` only escapes it because `os-user-ssh.ts` installs +those files with explicit modes. Worth knowing before the next tool starts complaining. + +--- + +## State + +Green: account, ACLs, SSH, shell, Claude and rootless Docker all provisioned. Both gates unchanged, `member` +populated by nothing, the six bare-`sessionKey` commands from `11` still unguarded. + +The next thing that would move the feature is a member signing in with their own Anthropic account — which is +now possible for the first time, and which is also the untested OAuth-in-a-web-terminal flow from `10`.