Skip to content

Commit 419faa3

Browse files
committed
Apply fixes for user mapping
1 parent abdc8b4 commit 419faa3

3 files changed

Lines changed: 184 additions & 8 deletions

File tree

src/vibepod/core/docker.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,27 @@ def _prepare_volume_dir(self, path: Path) -> None:
116116
except OSError:
117117
pass
118118

119+
def _resolved_userns_mode(self, userns_mode: str | None) -> str | None:
120+
"""Normalize user namespace modes per runtime.
121+
122+
Podman supports modes like ``keep-id`` that Docker rejects, so only
123+
forward values that make sense for the selected runtime.
124+
"""
125+
if userns_mode is None:
126+
return None
127+
128+
normalized = userns_mode.strip().lower()
129+
if not normalized:
130+
return None
131+
132+
if self.runtime == RUNTIME_PODMAN:
133+
return normalized if normalized in {"auto", "keep-id", "nomap"} else None
134+
135+
if normalized in {"auto", "keep-id", "nomap"}:
136+
return None
137+
138+
return normalized
139+
119140
def _run_container(self, **kwargs: Any) -> Any:
120141
"""Create and start a container via the high-level SDK."""
121142
return self.client.containers.run(**kwargs)
@@ -223,6 +244,7 @@ def run_agent(
223244
volumes.extend(f"{host}:{bind}:{mode}" for host, bind, mode in extra_volumes)
224245

225246
try:
247+
resolved_userns_mode = self._resolved_userns_mode(userns_mode)
226248
run_kwargs: dict[str, Any] = {
227249
"image": image,
228250
"name": container_name,
@@ -243,8 +265,8 @@ def run_agent(
243265
run_kwargs["entrypoint"] = entrypoint
244266
if user:
245267
run_kwargs["user"] = user
246-
if userns_mode:
247-
run_kwargs["userns_mode"] = userns_mode
268+
if resolved_userns_mode:
269+
run_kwargs["userns_mode"] = resolved_userns_mode
248270

249271
return self._run_container(**run_kwargs)
250272
except APIError as exc:
@@ -315,6 +337,7 @@ def ensure_datasette(
315337
logs_db_container_path = f"/mount/logs/{logs_db_path.name}"
316338
proxy_db_container_path = f"/mount/proxy/{proxy_db_path.name}"
317339

340+
resolved_userns_mode = self._resolved_userns_mode(userns_mode)
318341
run_kwargs: dict[str, Any] = {
319342
"image": image,
320343
"name": "vibepod-datasette",
@@ -328,8 +351,8 @@ def ensure_datasette(
328351
"volumes": volumes,
329352
"ports": {"8001/tcp": port},
330353
}
331-
if userns_mode:
332-
run_kwargs["userns_mode"] = userns_mode
354+
if resolved_userns_mode:
355+
run_kwargs["userns_mode"] = resolved_userns_mode
333356

334357
return self._run_container(**run_kwargs)
335358

@@ -382,13 +405,14 @@ def ensure_proxy(
382405
"network": network,
383406
}
384407

408+
resolved_userns_mode = self._resolved_userns_mode(userns_mode)
385409
if self.runtime != RUNTIME_PODMAN:
386410
getuid = getattr(os, "getuid", None)
387411
getgid = getattr(os, "getgid", None)
388412
if callable(getuid) and callable(getgid):
389413
run_kwargs["user"] = f"{getuid()}:{getgid()}"
390-
if userns_mode:
391-
run_kwargs["userns_mode"] = userns_mode
414+
if resolved_userns_mode:
415+
run_kwargs["userns_mode"] = resolved_userns_mode
392416

393417
container = self._run_container(**run_kwargs)
394418
try:

tests/test_proxy_permissions.py

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def _fail_replace(src: str, dst: str) -> None:
5151
assert updated is False
5252

5353

54-
def test_ensure_proxy_runs_container_as_current_user_and_forwards_userns_mode(
54+
def test_ensure_proxy_runs_container_as_current_user_and_omits_podman_userns_mode_on_docker(
5555
tmp_path: Path, monkeypatch
5656
) -> None:
5757
class _FakeContainers:
@@ -87,7 +87,7 @@ def __init__(self) -> None:
8787
run_kwargs = manager.client.containers.run_kwargs # type: ignore[union-attr]
8888
assert run_kwargs is not None
8989
assert run_kwargs["user"] == "1234:2345"
90-
assert run_kwargs["userns_mode"] == "keep-id"
90+
assert "userns_mode" not in run_kwargs
9191
assert "ports" not in run_kwargs
9292
assert db_path.parent.exists()
9393
assert ca_dir.exists()
@@ -127,6 +127,74 @@ def __init__(self) -> None:
127127
assert run_kwargs["userns_mode"] == "keep-id"
128128

129129

130+
def test_ensure_datasette_omits_podman_userns_mode_on_docker(tmp_path: Path, monkeypatch) -> None:
131+
class _FakeContainers:
132+
def __init__(self) -> None:
133+
self.run_kwargs: dict | None = None
134+
135+
def run(self, **kwargs):
136+
self.run_kwargs = kwargs
137+
return {"id": "datasette"}
138+
139+
class _FakeClient:
140+
def __init__(self) -> None:
141+
self.containers = _FakeContainers()
142+
143+
manager = object.__new__(DockerManager)
144+
manager.client = _FakeClient() # type: ignore[assignment]
145+
manager.runtime = "docker"
146+
147+
monkeypatch.setattr(DockerManager, "find_datasette", lambda self: None)
148+
149+
logs_db_path = tmp_path / "logs" / "logs.db"
150+
proxy_db_path = tmp_path / "proxy" / "proxy.db"
151+
manager.ensure_datasette(
152+
image="vibepod/datasette:latest",
153+
logs_db_path=logs_db_path,
154+
proxy_db_path=proxy_db_path,
155+
port=8001,
156+
userns_mode="keep-id",
157+
)
158+
159+
run_kwargs = manager.client.containers.run_kwargs # type: ignore[union-attr]
160+
assert run_kwargs is not None
161+
assert "userns_mode" not in run_kwargs
162+
163+
164+
def test_ensure_proxy_forwards_podman_userns_mode(tmp_path: Path, monkeypatch) -> None:
165+
class _FakeContainers:
166+
def __init__(self) -> None:
167+
self.run_kwargs: dict | None = None
168+
169+
def run(self, **kwargs):
170+
self.run_kwargs = kwargs
171+
return {"id": "proxy"}
172+
173+
class _FakeClient:
174+
def __init__(self) -> None:
175+
self.containers = _FakeContainers()
176+
177+
manager = object.__new__(DockerManager)
178+
manager.client = _FakeClient() # type: ignore[assignment]
179+
manager.runtime = "podman"
180+
181+
monkeypatch.setattr(DockerManager, "find_proxy", lambda self: None)
182+
183+
db_path = tmp_path / "proxy" / "proxy.db"
184+
ca_dir = tmp_path / "proxy" / "mitmproxy"
185+
manager.ensure_proxy(
186+
image="vibepod/proxy:latest",
187+
db_path=db_path,
188+
ca_dir=ca_dir,
189+
network="vibepod-network",
190+
userns_mode="keep-id",
191+
)
192+
193+
run_kwargs = manager.client.containers.run_kwargs # type: ignore[union-attr]
194+
assert run_kwargs is not None
195+
assert run_kwargs["userns_mode"] == "keep-id"
196+
197+
130198
def test_ensure_proxy_connects_existing_proxy_to_requested_network(monkeypatch) -> None:
131199
connected: list[tuple[object, str]] = []
132200

tests/test_run.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,90 @@ def __init__(self) -> None:
174174
assert run_kwargs["userns_mode"] == "keep-id"
175175

176176

177+
def test_run_agent_omits_podman_userns_mode_for_docker(tmp_path: Path) -> None:
178+
class _FakeContainers:
179+
def __init__(self) -> None:
180+
self.run_kwargs: dict | None = None
181+
182+
def run(self, **kwargs):
183+
self.run_kwargs = kwargs
184+
return {"id": "agent"}
185+
186+
class _FakeClient:
187+
def __init__(self) -> None:
188+
self.containers = _FakeContainers()
189+
190+
manager = object.__new__(DockerManager)
191+
manager.client = _FakeClient() # type: ignore[assignment]
192+
manager.runtime = "docker"
193+
194+
workspace = tmp_path / "workspace"
195+
config_dir = tmp_path / "agents" / "claude"
196+
workspace.mkdir(parents=True, exist_ok=True)
197+
config_dir.mkdir(parents=True, exist_ok=True)
198+
199+
manager.run_agent(
200+
agent="claude",
201+
image="vibepod/claude:latest",
202+
workspace=workspace,
203+
config_dir=config_dir,
204+
config_mount_path="/claude",
205+
env={},
206+
command=["claude"],
207+
auto_remove=True,
208+
name=None,
209+
version="0.2.1",
210+
network="vibepod-network",
211+
userns_mode="keep-id",
212+
)
213+
214+
run_kwargs = manager.client.containers.run_kwargs # type: ignore[union-attr]
215+
assert run_kwargs is not None
216+
assert "userns_mode" not in run_kwargs
217+
218+
219+
def test_run_agent_preserves_host_userns_mode_for_docker(tmp_path: Path) -> None:
220+
class _FakeContainers:
221+
def __init__(self) -> None:
222+
self.run_kwargs: dict | None = None
223+
224+
def run(self, **kwargs):
225+
self.run_kwargs = kwargs
226+
return {"id": "agent"}
227+
228+
class _FakeClient:
229+
def __init__(self) -> None:
230+
self.containers = _FakeContainers()
231+
232+
manager = object.__new__(DockerManager)
233+
manager.client = _FakeClient() # type: ignore[assignment]
234+
manager.runtime = "docker"
235+
236+
workspace = tmp_path / "workspace"
237+
config_dir = tmp_path / "agents" / "claude"
238+
workspace.mkdir(parents=True, exist_ok=True)
239+
config_dir.mkdir(parents=True, exist_ok=True)
240+
241+
manager.run_agent(
242+
agent="claude",
243+
image="vibepod/claude:latest",
244+
workspace=workspace,
245+
config_dir=config_dir,
246+
config_mount_path="/claude",
247+
env={},
248+
command=["claude"],
249+
auto_remove=True,
250+
name=None,
251+
version="0.2.1",
252+
network="vibepod-network",
253+
userns_mode="host",
254+
)
255+
256+
run_kwargs = manager.client.containers.run_kwargs # type: ignore[union-attr]
257+
assert run_kwargs is not None
258+
assert run_kwargs["userns_mode"] == "host"
259+
260+
177261
def test_run_agent_forwards_entrypoint(tmp_path: Path) -> None:
178262
class _FakeContainers:
179263
def __init__(self) -> None:

0 commit comments

Comments
 (0)