# Implementation Plan: Tool Container Home Directory ## Overview Make `/home/user` the configurable, default workspace/home directory for all tool containers, preserve the repository/workspace directory name in the mount target, migrate legacy tool types, and keep `/workspace` as a compatibility symlink. ## Goals - Add a `home_directory` field to `ToolType` and the manifest schema. - Use that field to control workspace mount target, `HOME`, `WORKDIR`, and `~`/`$HOME` expansion. - Preserve `{repo_name}` / `{workspace_name}` in the mount target. - Migrate legacy Dockerfile/Compose templates via Alembic. - Keep `/workspace` symlink for backward compatibility. - Add an entrypoint permission fixer for runtime-mounted paths. - Validate with unit and integration tests. ## Phases ### Phase 1: Database and model changes **Files:** - `apps/api/src/models/tool/tool_type.py` - `apps/api/alembic/versions/_add_tool_type_home_directory.py` **Tasks:** 1. Add `home_directory: Mapped[str]` column to `ToolType`, non-nullable with server default `"/home/user"`. 2. Create Alembic migration that: - Adds the column. - Updates existing rows to `/home/user`. - Rewrites `compose_template` and `dockerfile_template` to replace `/workspace` with `/home/user/{{WORKSPACE_NAME}}` (or a compatible template variable). 3. Provide downgrade that reverses template rewrites and drops the column. **Quality gate:** - `cd apps/api && alembic upgrade head` succeeds. - `alembic downgrade -1` succeeds and restores `/workspace` strings. - Existing tests still pass. ### Phase 2: Manifest schema and compiler **Files:** - `apps/api/src/services/build/manifest_compiler.py` - `apps/api/src/services/tool/instance_service.py` - `apps/api/src/api/tool/tool_types_validation.py` (if schema validation is added) **Tasks:** 1. Accept `home_directory` in the manifest schema (optional; fallback to ToolType value). 2. In `compile_dockerfile()`: - Set `ENV HOME={home_directory}` and `ENV USER={user.name}`. - Set `WORKDIR {home_directory}` (unless `runtime.working_dir` is present). - Create the home directory and pre-create `.config`, `.local/share`, `.cache` under it. - Add a step to create `/workspace` as a symlink to `{home_directory}/{repo_name}` (placeholder or startup-time). 3. In `compile_compose()`: - Use `{home_directory}/{repo_name}` as the default repo mount target when the manifest has no explicit repo mount. - Keep `~`/`$HOME` expansion base equal to `home_directory`. 4. Update `get_manifest_home_dir()` to honor `manifest.home_directory` before deriving from `user.name`. 5. Pass `home_directory` through the manifest-based instance lifecycle. **Quality gate:** - `compile_dockerfile()` output contains `ENV HOME=/home/user` and `WORKDIR /home/user` for default manifests. - `compile_compose()` output mounts repo at `/home/user/{repo_name}` when no explicit repo mount exists. ### Phase 3: Legacy instance generation **Files:** - `apps/api/src/services/tool/instance_service.py` - `apps/api/src/services/docker/compose.py` **Tasks:** 1. In `create_tool_instance()` for `definition_type == "dockerfile"`: - Read `tool_type.home_directory` (default `/home/user`). - Mount `{repo_path}:{home_directory}/{repo_name}` instead of `{repo_path}:/workspace`. - Generate or adjust Dockerfile/Compose to create `/workspace` symlink. 2. For `definition_type == "compose"`: - Render `{home_directory}` and `{WORKSPACE_NAME}` into the template. - Validate that `{WORKSPACE_NAME}` is available as a template variable. 3. Add `WORKSPACE_NAME` to the render variables in `render_compose_template()`. **Quality gate:** - Legacy `dockerfile` instance compose mounts repo at `/home/user/{repo_name}`. - Legacy `compose` template with `/home/user/{{WORKSPACE_NAME}}` renders correctly. ### Phase 4: Config-profile and git mount expansion **Files:** - `apps/api/src/services/config/config_profile_resolver.py` - `apps/api/src/services/tool/instance_service.py` **Tasks:** 1. Ensure `expand_container_path()` uses the resolved `home_directory` (already present; verify it is threaded through). 2. In `start_tool_instance()`, compute `home_dir` from ToolType/manifest and pass it to: - `apply_resolved_profile()` - `resolve_git_mounts()` / `resolve_git_mount_mappings()` 3. Confirm workspace/repo name is used as the mount target, not a generic `workspace` string. **Quality gate:** - Config profile mount target `~/config` expands to `/home/user/config`. - Git mount target `~/repo` expands to `/home/user/repo`. ### Phase 5: Entrypoint permission fixer **Files:** - `apps/api/src/services/build/manifest_compiler.py` - `apps/api/src/services/shared/permission_fixer.py` - `tool-images/base.dockerfile` or generated entrypoint **Tasks:** 1. Generate an entrypoint script that, before switching to the runtime user: - Detects the container user name/uid. - Runs `chown` on `{home_directory}` and key mount points. - Creates `/workspace` symlink if it does not yet exist. - Avoids recursive chown of large subtrees; target top-level dirs and runtime-created files. 2. Ensure manifest-generated Dockerfiles install `sudo` and configure passwordless sudo for the runtime user (already partially done). 3. Consider updating `tool-images/base.dockerfile` to include `sudo` and an entrypoint hook, or keep the fixer entirely in generated images. **Quality gate:** - Container starts successfully. - Container user can write to `{home_directory}` and mounted config/git directories. - `/workspace` symlink resolves to the repo/workspace directory. ### Phase 6: Tests **Files:** - `apps/api/tests/unit/test_home_path_expansion.py` - `apps/api/tests/unit/test_manifest_compiler.py` - `apps/api/tests/unit/test_instance_service.py` (new or expanded) - `apps/api/tests/integration/test_tool_instance_lifecycle.py` (new or expanded) **Tasks:** 1. Unit tests: - `expand_container_path` with `~`, `$HOME`, absolute, and relative paths. - `get_manifest_home_dir` with and without `home_directory`, with and without `user.name`. - `compile_dockerfile` includes correct `ENV HOME`, `WORKDIR`, and `/workspace` symlink step. - `compile_compose` defaults repo mount to `/home/user/{repo_name}` when no explicit repo mount exists. - Explicit manifest repo mount target is preserved. 2. Integration tests: - Create a manifest tool instance and verify the running container has `HOME=/home/user`, repo at `/home/user/{repo_name}`, and `/workspace` symlink. - Verify a config-profile mount under `~` is writable by the container user. - Verify git mount under `~` is writable. - Verify legacy dockerfile tool uses `/home/user/{repo_name}` after migration. **Quality gate:** - `cd apps/api && pytest tests/unit/... tests/integration/... -xvs` passes. - `cd apps/web && npm run typecheck` passes. ## Dependencies - The existing `home-path-expansion` helpers (`expand_container_path`) must already be in place. - Alembic migration infrastructure must be working. - Manifest compiler must support generated entrypoints (already partially implemented). ## Risks and Mitigations | Risk | Mitigation | |------|-----------| | Alembic migration rewrites commands/env vars that contain `/workspace` | Scope the replacement to volume mount lines only; add tests for edge cases. | | `/workspace` symlink target missing at build time | Create placeholder directory in Dockerfile; finalize symlink in entrypoint at startup. | | Entrypoint chown slow on large repos | Chown top-level directories only; rely on runtime user for new files. | | Base image `USER user` cannot run sudo | Either install `sudo` in base image or keep entrypoint running as root before `su`/`gosu` to runtime user. | | Legacy templates without `{{WORKSPACE_NAME}}` break | Migration injects the variable; validation rejects missing required variables. | ## OpenSpec Task Update After this plan is accepted, update or replace `openspec/tasks/home-path-expansion.md` to reflect the expanded scope (configurable `home_directory`, repo-name mount target, migration, compatibility symlink, permission fixer, tests). ## Rollout 1. Merge Phase 1 (schema + migration) first so the column exists. 2. Merge Phase 2 and 3 (compiler + legacy generation) next. 3. Merge Phase 4 and 5 (profile/git mounts + permission fixer). 4. Merge Phase 6 (tests) with the previous phases as appropriate. 5. Run migration in production during a maintenance window; validate a few existing tool types before full rollout.