Skip to content

Commit ba9bc1c

Browse files
committed
Apply fixes for user mapping
1 parent 0ea4883 commit ba9bc1c

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

@@ -413,13 +436,14 @@ def ensure_proxy(
413436
"network": network,
414437
}
415438

439+
resolved_userns_mode = self._resolved_userns_mode(userns_mode)
416440
if self.runtime != RUNTIME_PODMAN:
417441
getuid = getattr(os, "getuid", None)
418442
getgid = getattr(os, "getgid", None)
419443
if callable(getuid) and callable(getgid):
420444
run_kwargs["user"] = f"{getuid()}:{getgid()}"
421-
if userns_mode:
422-
run_kwargs["userns_mode"] = userns_mode
445+
if resolved_userns_mode:
446+
run_kwargs["userns_mode"] = resolved_userns_mode
423447

424448
container = self._run_container(**run_kwargs)
425449
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
@@ -185,6 +185,90 @@ def __init__(self) -> None:
185185
assert run_kwargs["userns_mode"] == "keep-id"
186186

187187

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

0 commit comments

Comments
 (0)