From e9b9671d238bb3de779c0dbf4a518b41280be778 Mon Sep 17 00:00:00 2001 From: "Henry J. Webster" Date: Mon, 3 Aug 2026 19:44:07 -0500 Subject: enzo: add yokai sandbox user for coding agents Add a bare `yokai` user (primary group `ikai`) for running coding agents under a separate UID, isolated from hwebs's files and secrets. yokai has no privileged group memberships and its own $HOME. - systemd.tmpfiles creates /srv/git (2770 root:ikai) as a drop dir for the agent's local clones, outside ~ so /home/hwebs stays a sealed 700. - security.sudo lets hwebs drop to yokai without a password (de-escalation only, scoped to runAs=yokai; root stays gated). - hwebs joins the ikai group. Extensive comments document the local-clones model: yokai owns its clone, hwebs never runs git inside it, and commits are exchanged by fetching (or via bundles) and landed on main with squash/rebase/interactive + signing. Assisted-by: claude-code:claude-opus-4-8 --- machines/enzo/default.nix | 103 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) (limited to 'machines') diff --git a/machines/enzo/default.nix b/machines/enzo/default.nix index f72522c..d14dacd 100644 --- a/machines/enzo/default.nix +++ b/machines/enzo/default.nix @@ -28,6 +28,7 @@ extraGroups = [ "networkmanager" "wheel" + "ikai" ]; packages = with pkgs; [ git @@ -45,6 +46,108 @@ ]; }; + # --- Sandbox account (yokai) --- + # Model: LOCAL CLONES, not a shared working tree. yokai owns its own clone of + # each repo under /srv/git; hwebs keeps the canonical repo in ~. Work moves + # between the two over git remotes — never by sharing one tree. + # + # Why not one shared tree: git executes hooks and config out of .git, so a + # tree yokai can write must never be one hwebs runs `git` *inside* — a planted + # hook/config would run as hwebs and escape the sandbox. Git's "dubious + # ownership" warning is exactly that guard; don't defeat it with safe.directory + # on a yokai-writable repo. + # + # CARDINAL RULE: hwebs never runs git with CWD inside /srv/git/. Review + # the agent's work by FETCHING its commits into your own repo and reading them + # there. + # + # /srv/git is created below as 2770 root:ikai — group-writable + setgid, so + # hwebs and yokai (both in ikai) can each drop clones without sudo and new + # files inherit the ikai group. It sits outside ~ so /home/hwebs stays 700 + # and yokai never needs to traverse into home. (Tradeoff: yokai has write on + # the /srv/git dir itself, i.e. its own play area — fine for a sandbox.) + # + # Seed the agent's clone (hand a fresh clone to yokai): + # git clone ~/git/ /srv/git/ + # sudo chown -R yokai:ikai /srv/git/ # yokai now owns its tree + # TODO: wrap seed (clone + chown) in a one-shot helper (shell fn / just). + # + # Pull the agent's work back into YOUR repo (stay in your trusted tree): + # git -C ~/git/ remote add sandbox /srv/git/ + # git -C ~/git/ fetch sandbox + # git -C ~/git/ log --oneline sandbox/main # review, then merge + # + # Land + SIGN on main (signatures attest that hwebs reviewed+vouches, so hwebs + # signs — never put a signing key in yokai's home, and never give yokai yours). + # Squash the agent's WIP into one commit you author + sign; satisfies "require + # signed commits" branch protection trivially and keeps history clean: + # git -C ~/git/ checkout main + # git -C ~/git/ merge --squash sandbox/main # stage, don't commit + # git -C ~/git/ commit -S -m "...\n\nAssisted-by: claude-code:" + # git -C ~/git/ push origin main + # Alt — keep granular commits instead of squashing: rebase re-commits each + # one signed by hwebs (also all-signed, but review commit-by-commit): + # git -C ~/git/ checkout -b land sandbox/main # local branch at tip + # git -C ~/git/ rebase -S main # replay onto main, sign each + # git -C ~/git/ checkout main + # git -C ~/git/ merge --ff-only land && git -C ~/git/ branch -d land + # git -C ~/git/ push origin main + # Alt — INTERACTIVE, for exploring/curating the agent's history by hand: opens + # a todo list (pick/squash/fixup/reword/drop/reorder per commit), then the + # message editor. Good when you want to hand-pick what lands. Run it in a real + # terminal — interactive git can't be driven through the agent harness: + # git -C ~/git/ checkout -b land sandbox/main + # git -C ~/git/ rebase -i -S main # -S signs each resulting commit + # # ...then merge --ff-only land into main + push, as above. + # (Signing config lives in hwebs's dotfiles, not here: commit.gpgsign + + # user.signingkey.) + # + # Airtight variant — exchange via bundles (inert data, no hooks/config run): + # (yokai) git -C /srv/git/ bundle create /srv/git/x.bundle main + # (hwebs) git -C ~/git/ fetch /srv/git/x.bundle main:sandbox/main + # + # Run the agent as yokai: sudo -u yokai claude + # + # Let hwebs drop to yokai without a password. This grants NO new privilege — + # it's a de-escalation to a weaker account — so NOPASSWD here is low-risk, + # unlike a run-as-root rule. Scoped to runAs=yokai only; root stays gated. + security.sudo.extraRules = [ + { + users = [ "hwebs" ]; + runAs = "yokai"; + commands = [ + { + command = "ALL"; + options = [ + "NOPASSWD" + "SETENV" + ]; + } + ]; + } + ]; + + users.groups.ikai = { }; + users.users.yokai = { + isNormalUser = true; + description = "Sandbox account for coding agents"; + group = "ikai"; + packages = with pkgs; [ + git + claude-code + pi-coding-agent + ]; + }; + + # Drop dir for the agent's local clones. Created at activation as 2770 + # root:ikai — setgid (new entries inherit ikai) + group-writable so hwebs and + # yokai can each place clones here without sudo; world sees nothing. Sits + # outside ~ so /home/hwebs stays a sealed 700. Trailing "-" = no age-cleaning. + # Each clone's own ownership is set when seeded (see the notes above). + systemd.tmpfiles.rules = [ + "d /srv/git 2770 root ikai -" + ]; + nix.settings.trusted-users = [ "root" "hwebs" -- cgit v1.3