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
@@ -72,9 +72,10 @@ class TerminalManager:
session_id,
instance_id,
)
session = self._sessions.pop(key, None)
if session:
await session.close()
idle_session = self._sessions.get(key)
if idle_session is not None:
del self._sessions[key]
await idle_session.close()
# Update DB status fire-and-forget
asyncio.create_task(self._mark_closed_in_db(session_id))
@@ -141,6 +142,7 @@ class TerminalManager:
startup_command: str | None = None,
name: str | None = None,
session_id: str | None = None,
container_user: str | None = None,
) -> TerminalSession:
"""Create a new terminal session for an instance.
@@ -152,6 +154,8 @@ class TerminalManager:
container_id: Docker container ID.
startup_command: Optional startup command to run.
name: Optional session name (auto-generated if omitted).
session_id: Optional explicit session UUID.
container_user: Optional container user for docker exec.
Returns:
The newly created TerminalSession.
@@ -177,6 +181,7 @@ class TerminalManager:
container_id=container_id,
startup_command=startup_command,
name=name,
container_user=container_user,
)
await session.start(startup_command=startup_command)
@@ -201,6 +206,7 @@ class TerminalManager:
instance_id: uuid.UUID,
container_id: str,
startup_command: str | None = None,
container_user: str | None = None,
) -> TerminalSession:
"""Get existing session or create a new one.
@@ -243,6 +249,7 @@ class TerminalManager:
container_id=container_id,
startup_command=startup_command,
name="Session 1",
container_user=container_user,
)
await session.start(startup_command=startup_command)
self._sessions[key] = session
@@ -358,6 +365,7 @@ class TerminalManager:
startup_command: str | None = None,
session_id: str | None = None,
name: str | None = None,
container_user: str | None = None,
) -> TerminalSession:
"""Reset a session by killing it and creating a new one.
@@ -367,6 +375,7 @@ class TerminalManager:
startup_command: Optional startup command.
session_id: Specific session to reset. If None, resets the default session.
name: Optional name to preserve for the new session.
container_user: Optional container user for docker exec.
Returns:
The newly created TerminalSession.
@@ -400,6 +409,7 @@ class TerminalManager:
container_id=container_id,
startup_command=startup_command,
name=old_name or ("Session 1" if target_session_id == "default" else None),
container_user=container_user,
)
await new_session.start(startup_command=startup_command)
self._sessions[key] = new_session