fix: run tool terminal sessions as container user instead of root
- Remove compose-level user: 0:0 override from manifest_compiler.py so the entrypoint can start as root, fix mount ownership, and drop privileges to the container user internally. - Add get_manifest_container_user() helper to resolve the manifest-declared container user (with uid:gid fallback). - Pass container user through TerminalSession, TerminalManager, and the terminal WebSocket handler so docker exec is invoked with --user <user>. - Update and add unit tests for the manifest compiler and terminal session. - Record the additional root-user fix in the fix-pi-container-mount-permissions OpenSpec change/tasks. Quality gates: pytest tests/unit/ (226 passed), pytest tests/services/test_terminal_manager_multi.py (7 passed), ruff check on changed files (clean), mypy on changed files (clean)
This commit is contained in:
@@ -408,7 +408,6 @@ def compile_compose(manifest: dict, variables: dict[str, Any]) -> str:
|
||||
Docker Compose YAML content.
|
||||
"""
|
||||
runtime = manifest.get("runtime", {})
|
||||
user = manifest.get("user")
|
||||
interface_type = manifest["interface_type"]
|
||||
home_dir = get_manifest_home_dir(manifest)
|
||||
|
||||
@@ -436,12 +435,11 @@ def compile_compose(manifest: dict, variables: dict[str, Any]) -> str:
|
||||
else:
|
||||
service["working_dir"] = f"{home_dir}/{workspace_name}"
|
||||
|
||||
# The entrypoint starts as root (Dockerfile does not set USER) so it can
|
||||
# fix mount ownership. It drops privileges to the container user internally
|
||||
# before exec-ing the real command, so do not set compose-level user
|
||||
# override here.
|
||||
if user:
|
||||
service["user"] = "0:0"
|
||||
# The Dockerfile does not set USER so the entrypoint starts as root,
|
||||
# fixes mount ownership, and drops privileges to the container user
|
||||
# internally. Do not set a compose-level user override: that would pin
|
||||
# the container metadata to root and make docker exec sessions run as
|
||||
# root even after the entrypoint drops privileges.
|
||||
|
||||
# Ports for web tools
|
||||
default_port = manifest.get("default_port")
|
||||
@@ -568,6 +566,35 @@ def get_manifest_home_dir(manifest: dict) -> str:
|
||||
return "/root"
|
||||
|
||||
|
||||
def get_manifest_container_user(manifest: dict) -> str | None:
|
||||
"""Resolve the container user identifier from a manifest.
|
||||
|
||||
Returns the user name when available so that docker exec sessions can
|
||||
attach as the container user instead of defaulting to root. Falls back
|
||||
to ``uid:gid`` when a name is absent but numeric ids are present.
|
||||
|
||||
Args:
|
||||
manifest: Fully resolved manifest JSON.
|
||||
|
||||
Returns:
|
||||
User name (e.g. ``user``), ``uid:gid`` string, or None when the
|
||||
manifest does not declare a user.
|
||||
"""
|
||||
user = manifest.get("user")
|
||||
if not user:
|
||||
return None
|
||||
|
||||
name = user.get("name")
|
||||
if name:
|
||||
return name
|
||||
|
||||
uid = user.get("uid")
|
||||
gid = user.get("gid")
|
||||
if uid is not None and gid is not None:
|
||||
return f"{uid}:{gid}"
|
||||
return None
|
||||
|
||||
|
||||
def compute_image_tag(tool_name: str, manifest: dict) -> str:
|
||||
"""Compute a deterministic image tag from manifest content.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user