feat(worker): box-user script builders + username sanitizer
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
// The per-box Linux user. Terminal sessions and agent turns exec as this user
|
||||
// instead of root (Claude Code refuses some operations as root, and a real
|
||||
// machine has a named user); sudo is passwordless until the user stores a
|
||||
// password, after which wheel membership makes sudo prompt for it.
|
||||
|
||||
export const BOX_UID = 1000;
|
||||
|
||||
const shellQuote = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;
|
||||
|
||||
// Linux-safe username derived from the Spoon username. The home path keeps
|
||||
// the Spoon username everywhere; only the passwd entry uses this.
|
||||
export const linuxUsername = (spoonUsername: string): string => {
|
||||
const cleaned = spoonUsername
|
||||
.toLowerCase()
|
||||
.replaceAll(/[^a-z0-9_-]/g, '-')
|
||||
.slice(0, 32);
|
||||
if (!cleaned) return 'spoon';
|
||||
return /^[a-z_]/.test(cleaned) ? cleaned : `u${cleaned}`.slice(0, 32);
|
||||
};
|
||||
|
||||
// Idempotent root init: create the user at the (already mounted) home, grant
|
||||
// wheel, and chown the home ONCE (marker-guarded — later host-side writes are
|
||||
// repaired by targeted chowns, so a big home is never re-walked). `-o` allows
|
||||
// the uid even if the image ever gains a uid-1000 user; `-M` skips skeleton
|
||||
// copy since the mounted home persists across containers.
|
||||
export const buildBoxInitScript = (args: {
|
||||
username: string;
|
||||
home: string;
|
||||
}): string => {
|
||||
const user = shellQuote(args.username);
|
||||
const home = shellQuote(args.home);
|
||||
const marker = shellQuote(`${args.home}/.spoon/chown-v1`);
|
||||
return [
|
||||
'set -e',
|
||||
`if ! id -u ${user} >/dev/null 2>&1; then`,
|
||||
` useradd -u ${BOX_UID} -o -M -d ${home} -s /bin/bash ${user}`,
|
||||
'fi',
|
||||
`usermod -aG wheel ${user}`,
|
||||
`if [ ! -f ${marker} ]; then`,
|
||||
` chown -R ${BOX_UID}:${BOX_UID} ${home}`,
|
||||
` mkdir -p "$(dirname ${marker})" && touch ${marker} && chown ${BOX_UID}:${BOX_UID} ${marker}`,
|
||||
'fi',
|
||||
].join('\n');
|
||||
};
|
||||
|
||||
// Sudo policy: NOPASSWD drop-in only while no password is stored; with a
|
||||
// password, wheel membership makes sudo prompt for it. The drop-in is staged
|
||||
// to a temp file and checked with `visudo -c` before install so a bad write
|
||||
// can never brick sudo.
|
||||
export const buildSudoPolicyScript = (args: {
|
||||
username: string;
|
||||
passwordSet: boolean;
|
||||
}): string => {
|
||||
if (args.passwordSet) {
|
||||
return 'rm -f /etc/sudoers.d/spoon-box';
|
||||
}
|
||||
const line = `${args.username} ALL=(ALL) NOPASSWD:ALL`;
|
||||
return [
|
||||
'set -e',
|
||||
`printf '%s\\n' ${shellQuote(line)} > /etc/sudoers.d/spoon-box.tmp`,
|
||||
'visudo -c -f /etc/sudoers.d/spoon-box.tmp',
|
||||
'chmod 0440 /etc/sudoers.d/spoon-box.tmp',
|
||||
'mv /etc/sudoers.d/spoon-box.tmp /etc/sudoers.d/spoon-box',
|
||||
].join('\n');
|
||||
};
|
||||
|
||||
// `chpasswd` reads `user:password` from stdin, so the password never appears
|
||||
// in argv (visible in `ps`) or in a shell script body.
|
||||
export const CHPASSWD_COMMAND = ['chpasswd'];
|
||||
|
||||
export const buildClearPasswordCommand = (username: string): string[] => [
|
||||
'passwd',
|
||||
'-d',
|
||||
username,
|
||||
];
|
||||
Reference in New Issue
Block a user