9f720930ea
- 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)
5.2 KiB
5.2 KiB
Fix pi container repo mount and npm update permissions
Problem
After implementing configurable tool container home directories, new pi-agent containers still bind-mount the git repository at /workspace instead of under /home/user/{repo_name}. In addition, users cannot run npm update -g @earendil-works/pi-coding-agent inside the container because the global npm prefix (/usr/lib/node_modules) is owned by root.
Root cause
- The built-in
pi-agentmanifest intool_definition_manifestsstill declares an explicit repo mount with"target": "/workspace"and"working_dir": "/workspace". This masks the generated/workspace → /home/user/{repo}compatibility symlink. manifest_compiler.pydoes not substitute the instance-specific{{WORKSPACE_NAME}}placeholder in explicit mount targets, andinstance_service.pydoes not passWORKSPACE_NAME/REPO_NAMEtocompile_composefor manifest-based tools.- The generated entrypoint hardcodes the literal string
{{WORKSPACE_NAME}}as the symlink target. npm_globalpackages are installed withRUN npm install -g ...as root into the system npm prefix, so the non-root container user cannot update them.- Once the repo mount moves out of
/workspace, the generated/workspacecompatibility symlink is created in the image as root. The non-root entrypoint cannot replace it (write permission is required on/), so container startup fails. - Older images baked a literal
{{WORKSPACE_NAME}}directory into/home/user, which survives alongside the real repo-named mount directory.
Fix
- Remove the compose-level
user: 0:0override frommanifest_compiler.py. The Dockerfile intentionally omitsUSERso the entrypoint can start as root, fix mount ownership, and drop privileges to the container user internally. Pinninguser: 0:0in the compose service forcesdocker execsessions to run as root even after the entrypoint drops privileges. - Pass the manifest-declared container user into terminal sessions so
docker execis invoked with--user <user>. This makes WebSocket terminal sessions run as the same non-root user as the main container process. - Add an Alembic data migration that updates the built-in
pi-agentmanifest:- Change the repo mount target to
~/{{WORKSPACE_NAME}}. - Keep
runtime.working_diras/workspace(the compatibility symlink). - Update the startup script to chown the real mount path (
$HOME/$WORKSPACE_NAME).
- Change the repo mount target to
- Update
manifest_compiler.py:- Substitute
{{WORKSPACE_NAME}}in mount targets incompile_compose. - Pass
WORKSPACE_NAMEas a container environment variable. - Generate the entrypoint symlink from the runtime
WORKSPACE_NAMEenvironment variable. - Install
npm_globalpackages into a user-writable prefix ({home_dir}/.npm-global) and add it toPATH. - Use
sudoor root to create the/workspacecompatibility symlink, because/is owned by root and the non-root entrypoint cannot replace a root-owned symlink. - Start the container as root and drop privileges to the container user inside the entrypoint via
su. - Do not create mount target directories or the
/workspacesymlink in the image when they depend on the runtime{{WORKSPACE_NAME}}placeholder. - Remove any stale literal
{{WORKSPACE_NAME}}directory left over from older images at container startup.
- Substitute
- Update
instance_service.pyto passREPO_NAMEandWORKSPACE_NAMEinto manifest compilation. - Remove the explicit repo mount from the built-in
pi-agentmanifest so the repo mount is synthesized bycompile_composerather than depending on tool config. Add a follow-up Alembic data migration that strips thesource_type: repomount from the manifest. - Add
_get_repository_mount_name()helper. When the instance is bound to a workspace, the helper returns the basename ofworkspace.path. For legacy repo-only instances it falls back to parsing the remote URL likegit clonewould, then to the user-provided repository name. - Switch workspace storage layout to
/data/working-copies/{workspace_id}/{repo_name}/sogit clonecreates the repo-named directory naturally, makingworkspace.path.basenamethe correct container mount name. This replaces the previous/data/working-copies/{repo_id}/{workspace_name}/layout. - Update unit tests for the new behavior.
Affected files
apps/api/alembic/versions/2026_06_14_182955_fix_pi_agent_home_directory_mount.pyapps/api/alembic/versions/2026_06_15_090500_remove_pi_agent_explicit_repo_mount.pyapps/api/src/services/build/manifest_compiler.pyapps/api/src/services/terminal/terminal_session.pyapps/api/src/services/terminal/terminal_manager.pyapps/api/src/api/system/terminal.pyapps/api/src/services/shared/workspace_manager.pyapps/api/src/services/tool/instance_service.pyapps/api/tests/unit/test_manifest_compiler.pyapps/api/tests/unit/test_terminal_session.pyapps/api/tests/unit/test_instance_service.pyapps/api/tests/unit/test_alembic_migrations.py
Verification
pytest apps/api/tests/unit/test_manifest_compiler.pypytest apps/api/tests/unit/test_terminal_session.pypytest apps/api/tests/unit/test_alembic_migrations.pyruff,mypy,npm run typecheck,npm run lint