put rootless docker back, behind the developer gate

Uncommented at all three sites: the call and import in provisionOsAccount, the
~/.local/dockers bind-mount directory in confineUserTree, and the DOCKER_HOST block in
the member zshrc.

Not restored unconditionally, which is how it was before. It now sits behind the same
Developer check as the Postgres role — the gate that prompted disabling it in the first
place. So rolePermitsDatabase is renamed rolePermitsDevTools: it gates two things now
and a name saying "database" while deciding whether you get containers is the kind of
comment that goes stale silently.

~/.local/dockers is created for EVERY account rather than only Developers. It is two
install calls, and confineUserTree is the function that places the layout, not the one
that knows who is a Developer — so a member promoted later finds it already correct.

Postgres role work is untouched and still in place.

Verified: transpiles.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-13 12:49:32 +00:00
co-authored by Claude Opus 5
parent d6f01862fe
commit fb0286e1a9
5 changed files with 70 additions and 75 deletions
+34 -42
View File
@@ -369,17 +369,11 @@ export async function confineUserTree(params: {
}
}
// ── One directory where container bind mounts can live: DISABLED 2026-08-13, code kept ──
// ── One directory where container bind mounts can live ──
//
// `~/.local/dockers` existed only for rootless Docker, and members no longer get a daemon — see the
// commented-out step in api/users/provision-os.ts. Creating a blessed bind-mount directory for a daemon
// that is not there is a promise about container storage the platform no longer keeps.
//
// `.local` itself STAYS. It is not Docker's: the `claude` installer targets `~/.local/bin`, and the
// paragraph below is the record of that directory being created root-owned and blocking it.
//
// The reasoning is kept in full because turning containers back on means uncommenting the block, not
// rediscovering why 711 and why the ACLs come off.
// Created for every account, not only the ones that get a daemon: it is two `install` calls, and
// `confineUserTree` is the function that places the whole layout rather than the one that knows who is
// a Developer. A member promoted later finds it already correct.
//
// The default ACLs above are inherited by everything created in the home afterwards, including
// `default:other::---`. A rootless container's INNER uid is neither the service user nor the member —
@@ -426,39 +420,37 @@ export async function confineUserTree(params: {
]);
if (!madeLocalDir.ok) return { ok: false, error: `could not create ${localDir}: ${madeLocalDir.out}` };
// const composeDir = join(home, '.local', 'dockers');
// const madeComposeDir = await run([
// 'sudo',
// '-n',
// 'install',
// '-d',
// '-o',
// String(params.uid),
// '-g',
// String(params.gid),
// // 711, not 700, and this is the whole point of the directory. A container's inner uid is `other`, so
// // it needs `x` HERE to reach a bind source inside — 700 blocks the path before any ACL matters. `r`
// // stays off, so nothing can list it. Safe because the home above is 700: no other account can
// // traverse this far in the first place, and the only uids that get here are the member's own
// // containers.
// '-m',
// '711',
// composeDir,
// ]);
// if (!madeComposeDir.ok) return { ok: false, error: `could not create ${composeDir}: ${madeComposeDir.out}` };
const composeDir = join(home, '.local', 'dockers');
const madeComposeDir = await run([
'sudo',
'-n',
'install',
'-d',
'-o',
String(params.uid),
'-g',
String(params.gid),
// 711, not 700, and this is the whole point of the directory. A container's inner uid is `other`, so it
// needs `x` HERE to reach a bind source inside — 700 blocks the path before any ACL matters. `r` stays
// off, so nothing can list it. Safe because the home above is 700: no other account can traverse this
// far in the first place, and the only uids that get here are the member's own containers.
'-m',
'711',
composeDir,
]);
if (!madeComposeDir.ok) return { ok: false, error: `could not create ${composeDir}: ${madeComposeDir.out}` };
// `-b`, not `-k`: remove ACCESS entries as well as defaults, leaving plain POSIX modes.
//
// // `-b`, not `-k`: remove ACCESS entries as well as defaults, leaving plain POSIX modes.
// //
// // `-k` alone left `mask::---` behind — inherited named entries with every permission masked off,
// // reading as `user:pastilhas:rwx #effective:---`. An ACL that says one thing and means another is worse
// // than no ACL, and container storage is the one place in the home that wants ordinary mode bits and
// // nothing else.
// //
// // AFTER the recursive grant above, or what it just set is re-inherited here.
// const stripped = await run(['sudo', '-n', 'setfacl', '-R', '-b', composeDir]);
// if (!stripped.ok) {
// return { ok: false, error: `could not clear inherited ACLs from ${composeDir}: ${stripped.out}` };
// }
// `-k` alone left `mask::---` behind — inherited named entries with every permission masked off, reading
// as `user:pastilhas:rwx #effective:---`. An ACL that says one thing and means another is worse than no
// ACL, and container storage is the one place in the home that wants ordinary mode bits and nothing else.
//
// AFTER the recursive grant above, or what it just set is re-inherited here.
const stripped = await run(['sudo', '-n', 'setfacl', '-R', '-b', composeDir]);
if (!stripped.ok) {
return { ok: false, error: `could not clear inherited ACLs from ${composeDir}: ${stripped.out}` };
}
return { ok: true };
} catch (ex) {