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:
Developer
2026-06-17 20:51:46 +00:00
parent 3e59a257dc
commit 9f720930ea
28 changed files with 358 additions and 131 deletions
@@ -55,10 +55,12 @@ class TerminalSession:
container_id: str,
startup_command: str | None = None,
name: str | None = None,
container_user: str | None = None,
) -> None:
self.session_id = session_id
self.instance_id = instance_id
self.container_id = container_id
self.container_user = container_user
self.startup_command = startup_command
self.process: asyncio.subprocess.Process | None = None
self._closed = False
@@ -105,6 +107,19 @@ class TerminalSession:
async def start(self, startup_command: str | None = None) -> None:
"""Start the docker exec process with a shell using a PTY."""
# Build the docker exec command. When the tool manifest declares a
# non-root container user, run the shell as that user so terminal
# sessions match the privileges of the main container process.
exec_cmd = [
"docker",
"exec",
"-it",
"-e",
"TERM=xterm-256color",
]
if self.container_user:
exec_cmd.extend(["--user", self.container_user])
# Create a pseudo-terminal on the host
self._master_fd, slave_fd = pty.openpty()
@@ -131,16 +146,9 @@ class TerminalSession:
shell_cmd = "bash -il"
# Start docker exec with the slave fd as stdin/stdout/stderr
exec_cmd.extend([self.container_id, "bash", "-c", shell_cmd])
self.process = await asyncio.create_subprocess_exec(
"docker",
"exec",
"-it",
"-e",
"TERM=xterm-256color",
self.container_id,
"bash",
"-c",
shell_cmd,
*exec_cmd,
stdin=slave_fd,
stdout=slave_fd,
stderr=slave_fd,