feat(hh): /pw command, RAM-only direnv autostart, robust lets-hack; coven→clergy

- add /pw (alias /password): reveal this room's password locally (never
  broadcast); surfaced in the F1 help overlay and the join hint
- direnv-autostart/: cd-to-launch a single real-user session via direnv;
  password is minted in memory at launch (never written to disk, matching the
  RAM-only model) and scoped to the child process. setup.sh installs direnv,
  hooks bash/zsh, and `direnv allow`s the dir
- lets-hack.sh: boot a FRESH server by default (replacing any live one) with a
  --reuse opt-out; add -h/--help/-help; guard against killing the tmux session
  you're attached to; switch-client into the coven when run inside tmux
- rename coven→clergy across rust/python/scripts; tests/test_coven.py→test_clergy.py
- snapshots in-progress hack-house client work (sandbox, themes, net, ui)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-05-31 22:29:17 -07:00
parent 8e6365a649
commit 5de493e895
21 changed files with 1058 additions and 91 deletions
+77 -18
View File
@@ -2,16 +2,46 @@
//!
//! The broker (owner's client) spawns a sandbox shell inside a PTY. Output bytes
//! are pumped out of a reader thread onto an mpsc channel; the broker encrypts
//! them with the room key and relays them to the coven as `sbx pty_data` frames.
//! them with the room key and relays them to the clergy as `sbx pty_data` frames.
//! Input frames (`sbx pty_input`) are written back into the PTY. The server only
//! ever sees ciphertext — identical trust model to chat/file transfer.
use anyhow::{Context, Result};
use portable_pty::{native_pty_system, Child, CommandBuilder, MasterPty, PtySize};
use std::io::{Read, Write};
use std::process::Command;
use std::process::{Command, Stdio};
use std::sync::mpsc;
/// Helper that ensures the Docker daemon is running (ships beside this source).
const ENSURE_DOCKER: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/ensure-docker.sh");
/// Is the Docker daemon accepting connections? (`docker info` succeeds.)
pub fn docker_daemon_up() -> bool {
Command::new("docker")
.arg("info")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
/// Start the Docker daemon via `ensure-docker.sh --yes`, waiting until it's
/// ready. Returns the script's last error line on failure (e.g. needs sudo).
fn start_docker_daemon() -> Result<()> {
let out = Command::new("bash")
.arg(ENSURE_DOCKER)
.arg("--yes")
.output()
.context("running ensure-docker.sh")?;
if !out.status.success() {
let err = String::from_utf8_lossy(&out.stderr);
let last = err.lines().last().unwrap_or("could not start the docker daemon");
anyhow::bail!("{last}");
}
Ok(())
}
/// Which sandbox to summon. Multipass = strong isolation (default for real use),
/// Docker = fast, Local = no isolation (dev/testing only).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -50,7 +80,7 @@ impl Backend {
/// One-time setup before the PTY shell is spawned. Blocking — run off the UI
/// thread (Multipass boots a real VM, ~20-30s). Idempotent: reuses an instance
/// that already exists.
pub fn prepare(backend: Backend, name: &str, image: &str) -> Result<()> {
pub fn prepare(backend: Backend, name: &str, image: &str, start_daemon: bool) -> Result<()> {
match backend {
Backend::Local => Ok(()),
Backend::Multipass => {
@@ -60,24 +90,48 @@ pub fn prepare(backend: Backend, name: &str, image: &str) -> Result<()> {
.map(|o| o.status.success())
.unwrap_or(false);
if !exists {
let st = Command::new("multipass")
// Capture output so it can't bleed onto the TUI surface; surface
// the failure reason through the returned error instead.
let out = Command::new("multipass")
.args(["launch", "--name", name, "--cpus", "1", "--memory", "1G", "--disk", "5G", image])
.status()
.output()
.context("multipass launch (is multipass installed?)")?;
anyhow::ensure!(st.success(), "multipass launch failed");
if !out.status.success() {
let err = String::from_utf8_lossy(&out.stderr);
anyhow::bail!("multipass launch failed: {}", err.lines().last().unwrap_or("").trim());
}
} else {
let _ = Command::new("multipass").args(["start", name]).status();
let _ = Command::new("multipass").args(["start", name])
.stdout(Stdio::null()).stderr(Stdio::null()).status();
}
Ok(())
}
Backend::Docker => {
// The daemon must be up before any `docker` call. Rather than fail
// with a raw connection error, start it (the caller confirmed via
// `/sbx launch docker --start`).
if !docker_daemon_up() {
if start_daemon {
start_docker_daemon().context("starting docker daemon")?;
} else {
anyhow::bail!(
"docker daemon is not running — retry with `/sbx launch docker --start`"
);
}
}
// Persistent container so we can exec in to provision users + shells.
let _ = Command::new("docker").args(["rm", "-f", name]).status();
let st = Command::new("docker")
let _ = Command::new("docker").args(["rm", "-f", name])
.stdout(Stdio::null()).stderr(Stdio::null()).status();
// Capture output so a failure can't paint over the TUI; the reason is
// surfaced through the returned error (shown in the error popup).
let out = Command::new("docker")
.args(["run", "-d", "--name", name, "--hostname", name, "-w", "/root", image, "sleep", "infinity"])
.status()
.output()
.context("docker run (is docker installed?)")?;
anyhow::ensure!(st.success(), "docker run failed");
if !out.status.success() {
let err = String::from_utf8_lossy(&out.stderr);
anyhow::bail!("docker run failed: {}", err.lines().last().unwrap_or("").trim());
}
Ok(())
}
}
@@ -88,10 +142,12 @@ pub fn prepare(backend: Backend, name: &str, image: &str) -> Result<()> {
pub fn teardown(backend: Backend, name: &str) {
match backend {
Backend::Multipass => {
let _ = Command::new("multipass").args(["delete", name, "--purge"]).status();
let _ = Command::new("multipass").args(["delete", name, "--purge"])
.stdout(Stdio::null()).stderr(Stdio::null()).status();
}
Backend::Docker => {
let _ = Command::new("docker").args(["rm", "-f", name]).status();
let _ = Command::new("docker").args(["rm", "-f", name])
.stdout(Stdio::null()).stderr(Stdio::null()).status();
}
Backend::Local => {}
}
@@ -125,7 +181,7 @@ fn command_for(backend: Backend, name: &str, run_user: &str) -> CommandBuilder {
}
}
/// Sanitize a coven display name into a safe unix username.
/// Sanitize a clergy display name into a safe unix username.
pub fn unix_name(name: &str) -> String {
let s: String = name
.to_lowercase()
@@ -139,12 +195,15 @@ pub fn unix_name(name: &str) -> String {
fn mp(name: &str, args: &[&str]) {
let mut a = vec!["exec", name, "--"];
a.extend_from_slice(args);
let _ = Command::new("multipass").args(a).status();
// Null stdio so provisioning chatter never bleeds onto the TUI surface.
let _ = Command::new("multipass").args(a)
.stdout(Stdio::null()).stderr(Stdio::null()).status();
}
fn dk(name: &str, args: &[&str]) {
let mut a = vec!["exec", name];
a.extend_from_slice(args);
let _ = Command::new("docker").args(a).status();
let _ = Command::new("docker").args(a)
.stdout(Stdio::null()).stderr(Stdio::null()).status();
}
/// Grant a Multipass user real *passwordless* sudo (group + sudoers.d drop-in)
@@ -160,7 +219,7 @@ fn mp_revoke_sudo(name: &str, u: &str) {
mp(name, &["sudo", "bash", "-c", &script]);
}
/// Provision a real unix account per coven member inside the VM/container and
/// Provision a real unix account per clergy member inside the VM/container and
/// make the owner a superuser (sudoer). Returns the unix user the shared shell
/// should run as. Blocking — call off the UI thread.
pub fn provision(backend: Backend, name: &str, owner: &str, members: &[String]) -> String {
@@ -219,7 +278,7 @@ pub struct Sandbox {
impl Sandbox {
/// Spawn the backend in a PTY. A reader thread pushes raw output bytes onto
/// `out`; the caller relays them (encrypted) to the coven.
/// `out`; the caller relays them (encrypted) to the clergy.
pub fn launch(
backend: Backend,
name: &str,