feat(sandbox): podman backend + goose AI harness + noVNC GUI sandbox
Add Podman as a rootless/daemonless sandbox backend alongside Docker, Multipass and Local, and wire Goose in as the default agentic harness for the granted `!task` path (bridge execs `<engine> exec <name> goose run` and streams output to chat; auto-degrades to the simple one-shot injector when goose is absent). Add an optional GUI sandbox track (XFCE + TigerVNC + websockify/noVNC on :6080) summoned via `/sbx <engine> gui`, plus container-side provisioning in sandbox-bootstrap.sh and a host-side ensure-podman.sh prereq helper. Refresh the in-app command help to the backend-led `/sbx <engine> [gui]` grammar and minor ui tweaks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+174
-60
@@ -16,6 +16,8 @@ use std::sync::mpsc;
|
||||
const ENSURE_DOCKER: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/scripts/ensure-docker.sh");
|
||||
/// Detect-first Multipass installer (ships in hh/scripts/).
|
||||
const ENSURE_MULTIPASS: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/scripts/ensure-multipass.sh");
|
||||
/// Detect-first Podman installer (apt; rootless preflight). Ships in hh/scripts/.
|
||||
const ENSURE_PODMAN: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/scripts/ensure-podman.sh");
|
||||
/// Detect-first VirtualBox installer (ships in hh/scripts/).
|
||||
const ENSURE_VBOX: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/scripts/ensure-vbox.sh");
|
||||
/// Baseline dev-toolchain installer run inside Docker sandboxes (hh/scripts/).
|
||||
@@ -25,6 +27,11 @@ const SBX_TOOLS_JSON: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/scripts/sandb
|
||||
/// Creates a fresh, cloud-init-provisioned Ubuntu VirtualBox VM (hh/scripts/).
|
||||
const VBOX_NEW: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/scripts/vbox-new.sh");
|
||||
|
||||
/// Port a GUI sandbox serves noVNC on, both inside the container and on the host
|
||||
/// loopback (`-p 127.0.0.1:6080:6080`). The bootstrap bridges the container's VNC
|
||||
/// server (:1 / 5901) to this port via websockify; the owner opens it in a browser.
|
||||
pub const GUI_PORT: u16 = 6080;
|
||||
|
||||
/// Is the `docker` binary installed? (`docker --version` succeeds.) This is a
|
||||
/// weaker check than `docker_daemon_up`: the CLI can be present while the daemon
|
||||
/// is down. We need both before a Docker sandbox can launch.
|
||||
@@ -49,6 +56,19 @@ pub fn docker_daemon_up() -> bool {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Is the `podman` binary installed? (`podman --version` succeeds.) Podman is
|
||||
/// daemonless, so unlike Docker there is no separate daemon-up check: if the CLI
|
||||
/// is present a rootless container can launch immediately (no daemon, no sudo).
|
||||
pub fn podman_installed() -> bool {
|
||||
Command::new("podman")
|
||||
.arg("--version")
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Is this a Docker Desktop (Linux) install? Its engine runs in a per-user VM
|
||||
/// started by the *user* unit `docker-desktop.service` — there's no root
|
||||
/// `docker.service`, so starting the daemon needs **no sudo**. Detect it so the
|
||||
@@ -146,6 +166,24 @@ pub fn ensure_docker_install(password: Option<String>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Install Podman via `ensure-podman.sh --yes` (apt + a rootless subuid/subgid
|
||||
/// preflight). Consent is the caller's job (they passed `install`); the script is
|
||||
/// idempotent if Podman is already present. Daemonless — nothing to start after.
|
||||
/// Returns the script's last error line on failure.
|
||||
pub fn ensure_podman_install() -> Result<()> {
|
||||
let out = Command::new("bash")
|
||||
.arg(ENSURE_PODMAN)
|
||||
.arg("--yes")
|
||||
.output()
|
||||
.context("running ensure-podman.sh")?;
|
||||
if !out.status.success() {
|
||||
let err = String::from_utf8_lossy(&out.stderr);
|
||||
let last = err.lines().last().unwrap_or("could not install Podman");
|
||||
anyhow::bail!("{last}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Is Multipass installed? (`multipass version` succeeds.)
|
||||
pub fn multipass_installed() -> bool {
|
||||
Command::new("multipass")
|
||||
@@ -485,11 +523,13 @@ pub fn stop_vtx_holder(h: &VtxHolder) -> Result<()> {
|
||||
}
|
||||
|
||||
/// Which sandbox to summon. Multipass = strong isolation (default for real use),
|
||||
/// Docker = fast, Local = no isolation (dev/testing only).
|
||||
/// Podman = daemonless/rootless containers (no sudo), Docker = fast, Local = no
|
||||
/// isolation (dev/testing only).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Backend {
|
||||
Local,
|
||||
Docker,
|
||||
Podman,
|
||||
Multipass,
|
||||
}
|
||||
|
||||
@@ -498,6 +538,7 @@ impl Backend {
|
||||
match s {
|
||||
"local" => Some(Backend::Local),
|
||||
"docker" => Some(Backend::Docker),
|
||||
"podman" => Some(Backend::Podman),
|
||||
"multipass" => Some(Backend::Multipass),
|
||||
_ => None,
|
||||
}
|
||||
@@ -506,6 +547,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Local => "local-shell",
|
||||
Backend::Docker => "docker",
|
||||
Backend::Podman => "podman",
|
||||
Backend::Multipass => "multipass",
|
||||
}
|
||||
}
|
||||
@@ -513,10 +555,43 @@ impl Backend {
|
||||
pub fn default_image(self) -> &'static str {
|
||||
match self {
|
||||
Backend::Multipass => "24.04",
|
||||
Backend::Docker => "ubuntu:24.04",
|
||||
// Docker defaults to Parrot OS (Security). Debian/apt-based, so the
|
||||
// apt-only sandbox-bootstrap.sh path works unchanged. `parrotsec/core`
|
||||
// is the minimal base (bootstrap fills the toolchain); swap for
|
||||
// `parrotsec/security` per-launch if you want the full pentest set.
|
||||
Backend::Docker => "parrotsec/core",
|
||||
// Podman defaults to Kali (rolling). Still Debian/apt-based, so the
|
||||
// apt-only sandbox-bootstrap.sh path works unchanged; base is minimal
|
||||
// (no pentest metapackages pulled by default — keep first launch fast).
|
||||
Backend::Podman => "kalilinux/kali-rolling",
|
||||
Backend::Local => "",
|
||||
}
|
||||
}
|
||||
/// The exec family a co-located agent uses to run a command *inside* this
|
||||
/// sandbox — `docker`/`podman exec`, `multipass exec`, or host `local`.
|
||||
/// Advertised in the `_sbx:status` frame (distinct from `label`, whose Local
|
||||
/// value is the cosmetic "local-shell") so the bridge can build the right
|
||||
/// `goose run` invocation for the backend that holds the sandbox.
|
||||
pub fn engine(self) -> &'static str {
|
||||
match self {
|
||||
Backend::Local => "local",
|
||||
Backend::Docker => "docker",
|
||||
Backend::Podman => "podman",
|
||||
Backend::Multipass => "multipass",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The container-engine binary for an OCI backend. Docker and Podman share the
|
||||
/// same CLI grammar (`run/exec/commit/save/rm/images`), so the container code
|
||||
/// path is written once and parameterised by this. Non-container backends have
|
||||
/// no engine binary; calling this on them is a logic error (they never reach the
|
||||
/// container arms), so we default to "docker" rather than panic.
|
||||
fn engine_bin(backend: Backend) -> &'static str {
|
||||
match backend {
|
||||
Backend::Podman => "podman",
|
||||
_ => "docker",
|
||||
}
|
||||
}
|
||||
|
||||
/// One-time setup before the PTY shell is spawned. Blocking — run off the UI
|
||||
@@ -528,6 +603,11 @@ pub fn prepare(
|
||||
image: &str,
|
||||
start_daemon: bool,
|
||||
password: Option<String>,
|
||||
// GUI sandbox: publish the in-container noVNC port (6080) on the host loopback
|
||||
// so a browser can reach the desktop. Containers are headless, so this is the
|
||||
// only way out for a GUI — bound to 127.0.0.1 (never 0.0.0.0) to keep the
|
||||
// surface local. Only meaningful for Docker/Podman; ignored otherwise.
|
||||
gui: bool,
|
||||
) -> Result<()> {
|
||||
match backend {
|
||||
Backend::Local => Ok(()),
|
||||
@@ -563,11 +643,13 @@ pub fn prepare(
|
||||
}
|
||||
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() {
|
||||
Backend::Docker | Backend::Podman => {
|
||||
let engine = engine_bin(backend);
|
||||
// Docker needs a running daemon before any call; rather than fail with
|
||||
// a raw connection error, start it (the caller confirmed via
|
||||
// `/sbx launch docker --start`). Podman is daemonless — skip the check
|
||||
// and the sudo modal entirely; a rootless container launches as-is.
|
||||
if backend == Backend::Docker && !docker_daemon_up() {
|
||||
if start_daemon {
|
||||
start_docker_daemon(password).context("starting docker daemon")?;
|
||||
} else {
|
||||
@@ -577,35 +659,42 @@ pub fn prepare(
|
||||
}
|
||||
}
|
||||
// Persistent container so we can exec in to provision users + shells.
|
||||
let _ = Command::new("docker")
|
||||
let _ = Command::new(engine)
|
||||
.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",
|
||||
])
|
||||
let mut run = Command::new(engine);
|
||||
run.args(["run", "-d", "--name", name, "--hostname", name, "-w", "/root"]);
|
||||
// Goose (and any in-container tool) reaches the host Ollama via a
|
||||
// gateway. Docker maps `host.docker.internal` to the host gateway IP.
|
||||
// For rootless Podman the native `host.containers.internal` resolves to
|
||||
// the host's *LAN* interface, which can't reach an Ollama bound to
|
||||
// 127.0.0.1 (the safe default) — so request slirp4netns host-loopback
|
||||
// forwarding and point the in-container OLLAMA_HOST at the slirp gateway
|
||||
// 10.0.2.2 (see dk_bootstrap). Without this the granted `!task` path dies
|
||||
// with "Could not connect to host.containers.internal:11434".
|
||||
if backend == Backend::Docker {
|
||||
run.arg("--add-host=host.docker.internal:host-gateway");
|
||||
} else if backend == Backend::Podman {
|
||||
run.arg("--network=slirp4netns:allow_host_loopback=true");
|
||||
}
|
||||
// GUI sandbox: publish noVNC (container 6080 → host 127.0.0.1:6080).
|
||||
// Loopback-only so the desktop is reachable from this machine's browser
|
||||
// but not the LAN. The desktop+VNC stack itself is installed by the
|
||||
// bootstrap when HH_SBX_GUI=1 (see dk_bootstrap / sandbox-bootstrap.sh).
|
||||
if gui {
|
||||
run.args(["-p", &format!("127.0.0.1:{GUI_PORT}:{GUI_PORT}")]);
|
||||
}
|
||||
run.args([image, "sleep", "infinity"]);
|
||||
let out = run
|
||||
.output()
|
||||
.context("docker run (is docker installed?)")?;
|
||||
.with_context(|| format!("{engine} run (is {engine} installed?)"))?;
|
||||
if !out.status.success() {
|
||||
let err = String::from_utf8_lossy(&out.stderr);
|
||||
anyhow::bail!(
|
||||
"docker run failed: {}",
|
||||
err.lines().last().unwrap_or("").trim()
|
||||
);
|
||||
anyhow::bail!("{engine} run failed: {}", err.lines().last().unwrap_or("").trim());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -636,8 +725,8 @@ pub fn teardown(backend: Backend, name: &str) {
|
||||
.stderr(Stdio::null())
|
||||
.status();
|
||||
}
|
||||
Backend::Docker => {
|
||||
let _ = Command::new("docker")
|
||||
Backend::Docker | Backend::Podman => {
|
||||
let _ = Command::new(engine_bin(backend))
|
||||
.args(["rm", "-f", name])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
@@ -685,26 +774,24 @@ fn snap_dir() -> Result<std::path::PathBuf> {
|
||||
/// Returns a short human description of what was written on success.
|
||||
pub fn save_state(backend: Backend, name: &str, label: &str, local: bool) -> Result<String> {
|
||||
match backend {
|
||||
Backend::Docker => {
|
||||
Backend::Docker | Backend::Podman => {
|
||||
let engine = engine_bin(backend);
|
||||
let tag = format!("{SNAP_REPO}:{label}");
|
||||
let out = Command::new("docker")
|
||||
let out = Command::new(engine)
|
||||
.args(["commit", name, &tag])
|
||||
.output()
|
||||
.context("docker commit (is docker installed?)")?;
|
||||
.with_context(|| format!("{engine} commit (is {engine} installed?)"))?;
|
||||
if !out.status.success() {
|
||||
let err = String::from_utf8_lossy(&out.stderr);
|
||||
anyhow::bail!(
|
||||
"docker commit failed: {}",
|
||||
err.lines().last().unwrap_or("").trim()
|
||||
);
|
||||
anyhow::bail!("{engine} commit failed: {}", err.lines().last().unwrap_or("").trim());
|
||||
}
|
||||
if local {
|
||||
let path = snap_dir()?.join(format!("hh-snap-{label}.tar"));
|
||||
let out = Command::new("docker")
|
||||
let out = Command::new(engine)
|
||||
.args(["save", &tag, "-o"])
|
||||
.arg(&path)
|
||||
.output()
|
||||
.context("docker save")?;
|
||||
.with_context(|| format!("{engine} save"))?;
|
||||
if !out.status.success() {
|
||||
let err = String::from_utf8_lossy(&out.stderr);
|
||||
anyhow::bail!(
|
||||
@@ -881,11 +968,12 @@ pub fn vm_snapshots(vm: &str) -> Result<Vec<String>> {
|
||||
/// `hh-snap`, or multipass snapshots of the instance). Blocking.
|
||||
pub fn list_snapshots(backend: Backend, name: &str) -> Result<Vec<String>> {
|
||||
match backend {
|
||||
Backend::Docker => {
|
||||
let out = Command::new("docker")
|
||||
Backend::Docker | Backend::Podman => {
|
||||
let engine = engine_bin(backend);
|
||||
let out = Command::new(engine)
|
||||
.args(["images", SNAP_REPO, "--format", "{{.Tag}}"])
|
||||
.output()
|
||||
.context("docker images")?;
|
||||
.with_context(|| format!("{engine} images"))?;
|
||||
Ok(String::from_utf8_lossy(&out.stdout)
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
@@ -920,15 +1008,16 @@ pub fn list_snapshots(backend: Backend, name: &str) -> Result<Vec<String>> {
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SnapKind {
|
||||
Docker,
|
||||
Podman,
|
||||
Multipass,
|
||||
None,
|
||||
}
|
||||
|
||||
/// Resolve a snapshot label to the backend that holds it. Probes multipass first
|
||||
/// (its snapshots are instance-scoped and rarer), then docker's `hh-snap` repo.
|
||||
/// Blocking — run off the UI thread. A backend whose CLI is absent simply
|
||||
/// reports no match rather than erroring, so a missing multipass doesn't block a
|
||||
/// docker load and vice-versa.
|
||||
/// (its snapshots are instance-scoped and rarer), then the OCI engines' `hh-snap`
|
||||
/// repo (docker, then podman). Blocking — run off the UI thread. A backend whose
|
||||
/// CLI is absent simply reports no match rather than erroring, so a missing
|
||||
/// multipass doesn't block a docker load and vice-versa.
|
||||
pub fn locate_snapshot(name: &str, label: &str) -> SnapKind {
|
||||
if list_snapshots(Backend::Multipass, name)
|
||||
.map(|s| s.iter().any(|l| l == label))
|
||||
@@ -942,6 +1031,13 @@ pub fn locate_snapshot(name: &str, label: &str) -> SnapKind {
|
||||
{
|
||||
return SnapKind::Docker;
|
||||
}
|
||||
if podman_installed()
|
||||
&& list_snapshots(Backend::Podman, name)
|
||||
.map(|s| s.iter().any(|l| l == label))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return SnapKind::Podman;
|
||||
}
|
||||
SnapKind::None
|
||||
}
|
||||
|
||||
@@ -954,13 +1050,13 @@ fn command_for(backend: Backend, name: &str, run_user: &str) -> CommandBuilder {
|
||||
c.arg("-i");
|
||||
c
|
||||
}
|
||||
Backend::Docker => {
|
||||
Backend::Docker | Backend::Podman => {
|
||||
let user = if run_user.is_empty() {
|
||||
"root"
|
||||
} else {
|
||||
run_user
|
||||
};
|
||||
let mut c = CommandBuilder::new("docker");
|
||||
let mut c = CommandBuilder::new(engine_bin(backend));
|
||||
c.args(["exec", "-it", "-u", user, name, "bash", "-il"]);
|
||||
c
|
||||
}
|
||||
@@ -998,10 +1094,10 @@ fn mp(name: &str, args: &[&str]) {
|
||||
.stderr(Stdio::null())
|
||||
.status();
|
||||
}
|
||||
fn dk(name: &str, args: &[&str]) {
|
||||
fn dk(engine: &str, name: &str, args: &[&str]) {
|
||||
let mut a = vec!["exec", name];
|
||||
a.extend_from_slice(args);
|
||||
let _ = Command::new("docker")
|
||||
let _ = Command::new(engine)
|
||||
.args(a)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
@@ -1041,15 +1137,32 @@ fn sandbox_pkgs() -> String {
|
||||
/// Run the sandbox bootstrap inside the container, piping the script to `bash -s`
|
||||
/// on stdin (keeps it out of argv) with the resolved package list in the env.
|
||||
/// Blocking — provision() is already off the UI thread.
|
||||
fn dk_bootstrap(name: &str) {
|
||||
fn dk_bootstrap(engine: &str, name: &str, gui: bool) {
|
||||
let script = std::fs::read_to_string(SBX_BOOTSTRAP).unwrap_or_else(|_| {
|
||||
// Degraded fallback if the script file is missing: still refresh the
|
||||
// index and install whatever the env carries (at minimum vim+curl).
|
||||
"apt-get update -qq && apt-get install -y --no-install-recommends $HH_SBX_PKGS".into()
|
||||
});
|
||||
let env = format!("HH_SBX_PKGS={}", sandbox_pkgs());
|
||||
let child = Command::new("docker")
|
||||
.args(["exec", "-i", "-e", &env, name, "bash", "-s"])
|
||||
let pkgs_env = format!("HH_SBX_PKGS={}", sandbox_pkgs());
|
||||
// The in-container Goose config points OLLAMA_HOST at the host gateway, whose
|
||||
// address depends on the engine. Docker resolves `host.docker.internal` via the
|
||||
// `--add-host` we add at run time. Rootless Podman's `host.containers.internal`
|
||||
// resolves to the host LAN IP and can't reach a loopback-bound Ollama, so we
|
||||
// launch the container with slirp4netns:allow_host_loopback=true (see launch())
|
||||
// and target the slirp host-loopback gateway 10.0.2.2 instead. Pass it through
|
||||
// so sandbox-bootstrap.sh can bake the right URL.
|
||||
let gateway = if engine == "podman" {
|
||||
"HH_OLLAMA_HOST=http://10.0.2.2:11434"
|
||||
} else {
|
||||
"HH_OLLAMA_HOST=http://host.docker.internal:11434"
|
||||
};
|
||||
// HH_SBX_GUI=1 tells the bootstrap to also install + start the XFCE/noVNC
|
||||
// desktop stack (heavy — only when the launcher asked for a GUI sandbox).
|
||||
let gui_env = format!("HH_SBX_GUI={}", if gui { "1" } else { "0" });
|
||||
let child = Command::new(engine)
|
||||
.args([
|
||||
"exec", "-i", "-e", &pkgs_env, "-e", gateway, "-e", &gui_env, name, "bash", "-s",
|
||||
])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
@@ -1092,7 +1205,7 @@ fn mp_revoke_sudo(name: &str, u: &str) {
|
||||
/// 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 {
|
||||
pub fn provision(backend: Backend, name: &str, owner: &str, members: &[String], gui: bool) -> String {
|
||||
let run = unix_name(owner);
|
||||
match backend {
|
||||
Backend::Multipass => {
|
||||
@@ -1108,17 +1221,18 @@ pub fn provision(backend: Backend, name: &str, owner: &str, members: &[String])
|
||||
mp_bootstrap(name); // same baseline dev toolchain as the docker sandbox
|
||||
run
|
||||
}
|
||||
Backend::Docker => {
|
||||
Backend::Docker | Backend::Podman => {
|
||||
let engine = engine_bin(backend);
|
||||
// Install the baseline dev toolchain (editable list in
|
||||
// scripts/sandbox-tools.json; vim+curl guaranteed) so a fresh
|
||||
// sandbox comes up usable instead of bare. The script also refreshes
|
||||
// the apt index (base images ship without /var/lib/apt/lists) and is
|
||||
// idempotent + sentinel-guarded, so re-provisions stay fast.
|
||||
dk_bootstrap(name);
|
||||
dk_bootstrap(engine, name, gui);
|
||||
for m in members {
|
||||
let u = unix_name(m);
|
||||
if !u.is_empty() {
|
||||
dk(name, &["useradd", "-m", "-s", "/bin/bash", &u]);
|
||||
dk(engine, name, &["useradd", "-m", "-s", "/bin/bash", &u]);
|
||||
}
|
||||
}
|
||||
// Base images usually lack the sudo package; the shared shell runs as
|
||||
@@ -1150,7 +1264,7 @@ pub fn set_sudo(backend: Backend, name: &str, user: &str, enable: bool) {
|
||||
pub fn run_user_for(backend: Backend, owner: &str) -> String {
|
||||
match backend {
|
||||
Backend::Multipass => unix_name(owner),
|
||||
Backend::Docker => "root".to_string(),
|
||||
Backend::Docker | Backend::Podman => "root".to_string(),
|
||||
Backend::Local => String::new(),
|
||||
}
|
||||
}
|
||||
@@ -1173,9 +1287,9 @@ pub fn push(
|
||||
"local sandbox shares the host filesystem — {} is already reachable",
|
||||
local.display()
|
||||
),
|
||||
Backend::Docker => {
|
||||
Backend::Docker | Backend::Podman => {
|
||||
// Shell runs as root with $HOME=/root (see `prepare`'s `-w /root`).
|
||||
let mut c = Command::new("docker");
|
||||
let mut c = Command::new(engine_bin(backend));
|
||||
c.args(["exec", "-i", name, "tar", "-C", "/root", "-xf", "-"]);
|
||||
extract_tar(c, &tar)?;
|
||||
Ok(format!("/root/{base}"))
|
||||
|
||||
Reference in New Issue
Block a user