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 <noreply@anthropic.com>
117 lines
5.6 KiB
Markdown
117 lines
5.6 KiB
Markdown
# 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`.
|