feat(hh): real VM unix users + sudo delegation ⛧

Linux-style user permissions inside the sandbox (the original superuser ask):
- Backends are now persistent (docker run -d + exec; multipass instance) so the
  broker can provision accounts and run the shell as a chosen user.
- sbx::provision(): create a real unix account per coven member at launch; the
  OWNER becomes a passwordless superuser (sudo group + /etc/sudoers.d NOPASSWD
  drop-in on multipass). The shared shell runs as the owner's account.
- /sudo <user> and /unsudo <user> (owner-only): real usermod + sudoers.d in the
  VM — delegate/withdraw superuser. ACL frame carries sudoers; roster shows
  ⛧ owner ·  sudoer · ◆ driver · • member.

Verified live on a real Multipass VM: shell runs as owner@vm with
'sudo -n whoami' == root; '/sudo member' gives member 'NOPASSWD: ALL';
teardown purges the instance. Docker provisions accounts + persistent
container (shell as root; sudo pkg absent so drive-grant is the delegation).

Tests: 7 cargo tests pass; clean build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-05-30 19:51:40 -07:00
parent 1dfc614cc5
commit 0e269afce7
4 changed files with 210 additions and 49 deletions
+10 -7
View File
@@ -192,15 +192,18 @@ fn parse_perm(text: &str) -> Option<Net> {
if v["_perm"].as_str()? != "acl" {
return None;
}
let drivers = v["drivers"]
.as_array()
.into_iter()
.flatten()
.filter_map(|d| d.as_str().map(str::to_string))
.collect();
let list = |key: &str| {
v[key]
.as_array()
.into_iter()
.flatten()
.filter_map(|d| d.as_str().map(str::to_string))
.collect::<Vec<_>>()
};
Some(Net::Perm {
owner: v["owner"].as_str().unwrap_or("").to_string(),
drivers,
drivers: list("drivers"),
sudoers: list("sudoers"),
})
}