bun run doctor, and provisioning checks its own work

the tenth install in a row with something new wrong, and the pattern behind
them is not the individual faults. every step in provisionOsAccount is
deliberately non-fatal — a member whose account provisioned fine should not be
rolled back because their shell config failed. that is right, and it means
"nothing reported an error" is a far weaker statement than it reads as.

the acl-mask bug was the sharp version: a clean provisioning run, every named
entry correct, and an account that could reach nothing. it was found by a
person three days later, through a file browser message three steps removed
from the cause.

so provisioning now VERIFIES rather than assuming, and it verifies by making
the same readdir the file browser makes. deliberately not by inspecting the
acl: reading getfacl and reasoning about masks is exactly how this was missed,
because the acl looked right. a check that tests something adjacent to the
failure is how you get a green report and a broken server.

a failed check is reported FIRST, ahead of keys and shells — an account that
cannot read its own home is broken in a way that working ssh does not make up
for.

and `bun run doctor` for the nine servers that already exist: same check
across every account, plus the one that is invisible by construction — a
confined grant held by an account with no linux user, which sits in the table,
shows as ticked in the UI, and resolves to nothing.

read-only, so it is safe on a production box. exits 1, so it can gate the end
of a setup run. same posture as assertPermissionTotality, except this reports
instead of refusing to boot: one broken member must not stop the platform
serving everyone else.
This commit is contained in:
2026-08-19 23:07:47 +00:00
parent 069dd4b23f
commit bf57201a11
4 changed files with 192 additions and 11 deletions
+26 -11
View File
@@ -1,6 +1,7 @@
import type { UserRole } from 'officerdb';
import { updateUser } from 'officerdb';
import { ensureOsUser, hardenOwnerHome, osUserHome, reassertHomeMask } from '@@/os-user';
import { checkOsAccount } from '@@/os-user-verify';
import { provisionSshAccess } from '@@/os-user-ssh';
import { seedShellConfig } from '@@/os-user-shell';
import { provisionClaudeCli } from '@@/os-user-claude';
@@ -140,6 +141,18 @@ export async function provisionOsAccount(params: {
const mask = await reassertHomeMask(osUserHome(params.email));
if (!mask.ok) console.warn(`[users] ${params.email}: ${mask.error}`);
// And then CHECK it, rather than trusting that no step reported failure.
//
// Every step above is non-fatal by design, so "nothing returned an error" is a much weaker statement
// than it looks — the ACL-mask bug produced a clean run and an account that could reach nothing. This
// makes the same call the file browser makes, which is the only version of the question that cannot be
// satisfied by something merely looking correct.
//
// Reported first when it fails, ahead of keys and shells: an account that cannot read its own home is
// broken in a way that no amount of working SSH makes up for.
const verified = await checkOsAccount(params.email, account.osUser);
if (!verified.ok) console.warn(`[provision] ${params.email}: ${verified.problem}`);
// The Linux account is recorded either way: it exists, it is confined, and a member's terminal can run as
// it. Only the keys are missing, and that is what the error says.
const sshPublicKey = ssh.ok ? ssh.publicKey : null;
@@ -151,16 +164,18 @@ export async function provisionOsAccount(params: {
for (const step of [claude, shell, postgres, docker]) {
if (step && !step.ok) console.warn(`[users] ${params.email}: ${step.error}`);
}
const error = !ssh.ok
? ssh.error
: !claude.ok
? claude.error
: !shell.ok
? shell.error
: postgres && !postgres.ok
? postgres.error
: docker && !docker.ok
? docker.error
: null;
const error = !verified.ok
? verified.problem
: !ssh.ok
? ssh.error
: !claude.ok
? claude.error
: !shell.ok
? shell.error
: postgres && !postgres.ok
? postgres.error
: docker && !docker.ok
? docker.error
: null;
return { osUser: account.osUser, sshPublicKey, error };
}