fix(smolmachines): docker push fails on Docker Desktop — daemon-side route differs from host loopback #74

Merged
didericis-claude merged 13 commits from fix-local-registry-docker-desktop into main 2026-05-27 16:10:46 -04:00
Showing only changes of commit 35edf50f21 - Show all commits
+40 -22
View File
@@ -9,20 +9,36 @@ context-manager contract is satisfied.
User context: `smolvm machine exec` runs commands as root in the User context: `smolvm machine exec` runs commands as root in the
VM, but the agent image's USER is `node` and claude-code refuses VM, but the agent image's USER is `node` and claude-code refuses
to run as root with `--dangerously-skip-permissions`. Both to run as root with `--dangerously-skip-permissions`. Both
`exec_claude` and `exec` wrap commands in `runuser -l node -c` `exec_claude` and `exec` switch to the requested user (default
so they execute as the node user (and pick up node's $HOME / `node`) via `runuser -u <user> --` and set `HOME` / `USER`
$USER from the login shell) — matches the docker backend's through `smolvm -e` — avoiding `runuser -l`'s login-shell wiring
default-USER behavior.""" (PAM session setup, /etc/profile sourcing) which can hang on a
minimal Debian VM with no PAM session config."""
from __future__ import annotations from __future__ import annotations
import shlex
import subprocess import subprocess
from .. import Bottle, ExecResult from .. import Bottle, ExecResult
from . import smolvm as _smolvm from . import smolvm as _smolvm
# Per-user env the agent image's USER (node) expects. claude
# reads ~/.claude.json + writes session state under ~/.claude/;
# bare `runuser -u` inherits root's HOME=/root, which claude
# can't write to. Set HOME / USER explicitly through smolvm -e
# so the child process sees them.
_HOME_FOR = {
"node": "/home/node",
"root": "/root",
}
def _env_flags_for(user: str) -> list[str]:
home = _HOME_FOR.get(user, f"/home/{user}")
return ["-e", f"HOME={home}", "-e", f"USER={user}"]
class SmolmachinesBottle(Bottle): class SmolmachinesBottle(Bottle):
"""Handle returned by `SmolmachinesBottleBackend.launch`. The """Handle returned by `SmolmachinesBottleBackend.launch`. The
underlying VM lifecycle (create / start / stop / delete) lives underlying VM lifecycle (create / start / stop / delete) lives
@@ -47,21 +63,18 @@ class SmolmachinesBottle(Bottle):
scripted exec, wrong for an interactive shell. Drop down scripted exec, wrong for an interactive shell. Drop down
to `subprocess.run` with the TTY inherited. to `subprocess.run` with the TTY inherited.
`runuser -l node -c` runs the inner command under node's UID switches via `runuser -u node --` (not `-l`) so we
login shell so $HOME / $USER are set. Without that switch avoid login-shell wiring. HOME / USER come from `smolvm
claude bails on `--dangerously-skip-permissions cannot be -e` instead, which sets them on the process env."""
used with root/sudo privileges`."""
flags = ["smolvm", "machine", "exec", "--name", self.name] flags = ["smolvm", "machine", "exec", "--name", self.name]
if tty: if tty:
flags += ["-i", "-t"] flags += ["-i", "-t"]
flags += _env_flags_for("node")
claude_argv = ["claude"] claude_argv = ["claude"]
if self._prompt_path: if self._prompt_path:
claude_argv += ["--append-system-prompt-file", self._prompt_path] claude_argv += ["--append-system-prompt-file", self._prompt_path]
claude_argv += argv claude_argv += argv
# shlex-quote each piece so flags / paths with shell-special flags += ["--", "runuser", "-u", "node", "--", *claude_argv]
# chars survive the runuser -c shell parse.
inner = " ".join(shlex.quote(p) for p in claude_argv)
flags += ["--", "runuser", "-l", "node", "-c", f"exec {inner}"]
result = subprocess.run(flags, check=False) result = subprocess.run(flags, check=False)
return result.returncode return result.returncode
@@ -73,18 +86,23 @@ class SmolmachinesBottle(Bottle):
on both backends. Pass `user="root"` for tests that need on both backends. Pass `user="root"` for tests that need
root. root.
`smolvm machine exec` runs commands as root in the VM, so `runuser -u <user> -- /bin/sh -c <script>` switches UID
we always need to switch user (even when the caller asked without invoking a login shell; HOME / USER are set via
for root, switching to root is a cheap no-op via `smolvm -e` (see `_env_flags_for`)."""
`runuser -l root`).""" argv = (
r = _smolvm.machine_exec( _env_flags_for(user)
self.name, + ["--", "runuser", "-u", user, "--", "/bin/sh", "-c", script]
["runuser", "-l", user, "-c", script], )
# _smolvm.machine_exec expects argv (the bit after `--`);
# the -e flags go before, so call smolvm directly.
r = subprocess.run(
["smolvm", "machine", "exec", "--name", self.name] + argv,
capture_output=True, text=True, check=False,
) )
return ExecResult( return ExecResult(
returncode=r.returncode, returncode=r.returncode,
stdout=r.stdout, stdout=r.stdout or "",
stderr=r.stderr, stderr=r.stderr or "",
) )
def cp_in(self, host_path: str, container_path: str) -> None: def cp_in(self, host_path: str, container_path: str) -> None: