# Agent sandbox profile — a bare `yokai` user for running coding agents under a # separate UID, isolated from the machine owner's files and secrets. yokai has # no privileged group memberships and its own $HOME. # # Opt-in per machine: import this profile and set `sandbox.owner` to the human # user allowed to drop into the sandbox (enzo: hwebs, kusanagi: henz). The # profile stays username-agnostic; only that one option differs per machine. # # Model: LOCAL CLONES, not a shared working tree. yokai owns its own clone of # each repo under /srv/git; the owner 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 the owner runs `git` *inside* — a planted # hook/config would run as the owner 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: the owner 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 the # owner and yokai (both in ikai) can each drop clones without sudo and new files # inherit the ikai group. It sits outside ~ so /home/ 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 the owner reviewed+vouches, so the # owner 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 the owner (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 the owner'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 # (owner) git -C ~/git/ fetch /srv/git/x.bundle main:sandbox/main # # Run the agent as yokai: sudo -u yokai claude { config, lib, pkgs, ... }: let owner = config.sandbox.owner; in { options.sandbox.owner = lib.mkOption { type = lib.types.str; example = "hwebs"; description = '' Human user permitted to `sudo -u yokai` into the agent sandbox, and joined to the `ikai` group so they share the /srv/git drop dir with yokai. Set per machine (enzo: hwebs, kusanagi: henz). ''; }; config = { # The owner joins ikai so they can drop clones into /srv/git without sudo. users.users.${owner}.extraGroups = [ "ikai" ]; # Let the owner 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 = [ owner ]; 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 the # owner and yokai can each place clones here without sudo; world sees # nothing. Sits outside ~ so /home/ stays a sealed 700. Trailing "-" # = no age-cleaning. Each clone's own ownership is set when seeded (above). systemd.tmpfiles.rules = [ "d /srv/git 2770 root ikai -" ]; }; }