fix: run manifest containers as root and drop privileges in entrypoint

The compose file was forcing the container to run as uid 1001, so the
entrypoint could not create /workspace even with sudo configured.

- Remove Dockerfile USER directive so containers start as root
- Make compile_compose use user: 0:0 when the manifest declares a user
- Make the entrypoint drop to the container user via  after setup,
  preserving environment variables and command arguments
- Update unit tests to assert root startup and privilege drop

Quality gates:
- pytest tests/unit: 210 passed
- ruff: clean on changed files
- mypy: clean on changed files
This commit is contained in:
Developer
2026-06-14 21:32:27 +00:00
parent 47de2a0133
commit a4e6c46a47
11 changed files with 74 additions and 20 deletions
@@ -252,9 +252,10 @@ def compile_dockerfile(manifest: dict) -> str:
lines.append("RUN chmod +x /usr/local/bin/headquarter-entrypoint")
lines.append("")
# Switch to runtime user
if user:
lines.append(f"USER {user['name']}")
# Do not switch to the runtime user in the Dockerfile. The entrypoint
# starts as root so it can create the /workspace compatibility symlink
# (which lives under /) and fix mount ownership, then it drops privileges
# to the container user before exec-ing the real command.
# Set WORKDIR to the configured home directory unless runtime.working_dir
# explicitly overrides it.
@@ -373,7 +374,16 @@ def compile_entrypoint(manifest: dict) -> str:
lines.append(script)
lines.append("")
lines.append('exec "$@"')
# Drop from root to the container user before running the real command.
# The Dockerfile no longer sets USER, so the entrypoint has root for the
# setup above. Use `su -c` to preserve environment variables (HOME, PATH,
# WORKSPACE_NAME, etc.) and keep the container user as the running user.
if user:
name = user["name"]
lines.append("# Drop privileges to the container user")
lines.append(f'exec su -s /bin/bash -c "exec \\"$@\\"" {name} -- "$@"')
else:
lines.append('exec "$@"')
return "\n".join(lines)
@@ -414,9 +424,12 @@ def compile_compose(manifest: dict, variables: dict[str, Any]) -> str:
runtime["working_dir"], home_dir
)
# User override
# The entrypoint starts as root (Dockerfile does not set USER) so it can
# create the /workspace compatibility symlink and 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"] = f"{user['uid']}:{user['gid']}"
service["user"] = "0:0"
# Ports for web tools
default_port = manifest.get("default_port")