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>
5.6 KiB
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.