Skip to content

Commit 84eb166

Browse files
committed
Apply fixes for user mapping
1 parent f6a0199 commit 84eb166

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
@@ -122,6 +122,27 @@ def _prepare_volume_dir(self, path: Path) -> None:
122122
except OSError:
123123
pass
124124

125+
def _resolved_userns_mode(self, userns_mode: str | None) -> str | None:
126+
"""Normalize user namespace modes per runtime.
127+
128+
Podman supports modes like ``keep-id`` that Docker rejects, so only
129+
forward values that make sense for the selected runtime.
130+
"""
131+
if userns_mode is None:
132+
return None
133+
134+
normalized = userns_mode.strip().lower()
135+
if not normalized:
136+
return None
137+
138+
if self.runtime == RUNTIME_PODMAN:
139+
return normalized if normalized in {"auto", "keep-id", "nomap"} else None
140+
141+
if normalized in {"auto", "keep-id", "nomap"}:
142+
return None
143+
144+
return normalized
145+
125146
def _run_container(self, **kwargs: Any) -> Any:
126147
"""Create and start a container via the high-level SDK."""
127148
return self.client.containers.run(**kwargs)
@@ -254,6 +275,7 @@ def run_agent(
254275
volumes.extend(f"{host}:{bind}:{mode}" for host, bind, mode in extra_volumes)
255276

256277
try:
278+
resolved_userns_mode = self._resolved_userns_mode(userns_mode)
257279
run_kwargs: dict[str, Any] = {
258280
"image": image,
259281
"name": container_name,
@@ -274,8 +296,8 @@ def run_agent(
274296
run_kwargs["entrypoint"] = entrypoint
275297
if user:
276298
run_kwargs["user"] = user
277-
if userns_mode:
278-
run_kwargs["userns_mode"] = userns_mode
299+
if resolved_userns_mode:
300+
run_kwargs["userns_mode"] = resolved_userns_mode
279301

280302
return self._run_container(**run_kwargs)
281303
except APIError as exc:
@@ -346,6 +368,7 @@ def ensure_datasette(
346368
logs_db_container_path = f"/mount/logs/{logs_db_path.name}"
347369
proxy_db_container_path = f"/mount/proxy/{proxy_db_path.name}"
348370

371+
resolved_userns_mode = self._resolved_userns_mode(userns_mode)
349372
run_kwargs: dict[str, Any] = {
350373
"image": image,
351374
"name": "vibepod-datasette",
@@ -359,8 +382,8 @@ def ensure_datasette(
359382
"volumes": volumes,
360383
"ports": {"8001/tcp": port},
361384
}
362-
if userns_mode:
363-
run_kwargs["userns_mode"] = userns_mode
385+
if resolved_userns_mode:
386+
run_kwargs["userns_mode"] = resolved_userns_mode
364387

365388
return self._run_container(**run_kwargs)
366389

@@ -414,13 +437,14 @@ def ensure_proxy(
414437
"extra_hosts": {"host.docker.internal": "host-gateway"},
415438
}
416439

440+
resolved_userns_mode = self._resolved_userns_mode(userns_mode)
417441
if self.runtime != RUNTIME_PODMAN:
418442
getuid = getattr(os, "getuid", None)
419443
getgid = getattr(os, "getgid", None)
420444
if callable(getuid) and callable(getgid):
421445
run_kwargs["user"] = f"{getuid()}:{getgid()}"
422-
if userns_mode:
423-
run_kwargs["userns_mode"] = userns_mode
446+
if resolved_userns_mode:
447+
run_kwargs["userns_mode"] = resolved_userns_mode
424448

425449
container = self._run_container(**run_kwargs)
426450
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 run_kwargs["extra_hosts"] == {"host.docker.internal": "host-gateway"}
9393
assert db_path.parent.exists()
@@ -128,6 +128,74 @@ def __init__(self) -> None:
128128
assert run_kwargs["userns_mode"] == "keep-id"
129129

130130

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

tests/test_run.py

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

189189

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

0 commit comments

Comments
 (0)