docs: tool container home directory design, plan, and test plan

- Add design doc / ADR for configurable /home/user home directory
   - Add implementation plan with phased rollout
   - Add test plan / QA checklist
   - Update OpenSpec task for home-path-expansion
This commit is contained in:
Developer
2026-06-14 10:23:59 +00:00
parent e6114ed18c
commit 5fc8e035e6
4 changed files with 540 additions and 51 deletions
@@ -0,0 +1,112 @@
# Design / ADR: Tool Container Home Directory
## Status
Proposed
## Context
Tool containers currently mix home-directory conventions:
- Legacy `dockerfile` and `compose` tool types mount the repository at `/workspace` and run as `root`.
- Manifest-based tool types may run as a non-root user (`user`) and already support `~` / `$HOME` expansion to `/home/{user.name}` via the `home-path-expansion` work.
- `tool-images/base.dockerfile` already creates a `user` with `WORKDIR /home/user`, but the platform does not guarantee this for all tool types.
Users expect a predictable, writable home directory inside every tool container and want the repository/workspace to live under it, preserving the repository's root directory name so paths are meaningful (e.g., `/home/user/my-app`, not `/home/user/workspace` or a generic `/workspace`).
## Goals
1. Provide a **configurable workspace/home directory** that **defaults to `/home/user`** for all tool containers.
2. Mount the repository/workspace under that directory using the **actual directory name** (`{repo_name}` or `{workspace_name}`).
3. Make the setting part of the **ToolType / manifest definition** so tool authors control it.
4. Keep **`manifest.user.name`** as the runtime user; the new field controls the directory path.
5. Preserve **`/workspace` as a compatibility symlink** to avoid breaking existing scripts, bookmarks, and settings.
6. Ensure **config-profile and git mounts** staged by the API remain writable by the non-root container user.
## Non-Goals
- Changing the container runtime user model (still driven by `manifest.user`).
- Moving non-tool services (API, web, Postgres, Redis) under `/home/user`.
- Forcing every existing manifest to migrate; explicit repo mount targets remain respected.
- Adding a frontend UI for the new field in this iteration (backend field + manifest schema change only).
## Decision
### Configuration surface
Add a `home_directory` field to the ToolType model and the manifest schema. It defaults to `/home/user` and can be overridden per tool type.
- **ToolType level**: `ToolType.home_directory: str = "/home/user"` (non-nullable, default).
- **Manifest level**: `manifest.home_directory: str` (optional; if absent, fall back to `ToolType.home_directory`).
The runtime home directory for a container is resolved in this precedence order:
1. Manifest `home_directory` if present.
2. ToolType `home_directory` (database column, default `/home/user`).
3. Legacy fallback: `/root` for compose/dockerfile tools without the new field.
### What `home_directory` controls
1. **Repository/workspace mount target** when no explicit repo mount is defined:
- Repositories: `{home_directory}/{repo_name}`
- Workspaces: `{home_directory}/{workspace_name}` (fall back to `{repo_name}`)
2. **Dockerfile `HOME` environment variable** for manifest tools: `ENV HOME={home_directory}`.
3. **Dockerfile `WORKDIR`** for manifest tools (when no explicit `runtime.working_dir` overrides it).
4. **Base for `~` / `$HOME` expansion** in config-profile mounts and git-mount mappings.
### Precedence: explicit manifest mount wins
If a manifest already contains a mount with `source_type: repo` and an explicit `target`, that target is used unchanged. `home_directory` is only used to synthesize the default repo/workspace mount when none is explicitly specified.
### Migration for legacy tools
Use an **Alembic data migration**:
1. Add `home_directory` column to `tool_types`.
2. Set existing rows to `/home/user`.
3. Rewrite stored `compose_template` and `dockerfile_template` strings to replace `/workspace` with `{home_directory}/{{WORKSPACE_NAME}}` (or equivalent template variable) where the mount target is the workspace.
The migration is reversible via downgrade: restore the original `/workspace` strings and drop the column.
### `/workspace` compatibility
Keep `/workspace` as a symlink inside the container pointing to the resolved repo/workspace target (e.g., `/home/user/my-app`). This protects:
- Existing user startup commands and scripts.
- Bookmarks and IDE settings that reference `/workspace`.
- Legacy templates that were migrated.
The symlink is created by the generated Dockerfile (or startup entrypoint) because the actual repo is mounted at runtime.
### Permission model
Use an **entrypoint permission fixer** at container startup:
- Run as `root` (or via `sudo`) before switching to the runtime user.
- Chown mounted paths under `{home_directory}` to the container user.
- Avoid `chown -R` on large repo subtrees; chown the top-level directories and rely on the runtime user owning newly created files.
- This handles config-profile and git mounts that arrive at runtime as bind mounts from the host, which may otherwise be owned by `root` because the API stages them.
## Consequences
### Positive
- Predictable, user-writable home directory across all tool containers.
- Repository mount paths are meaningful (`/home/user/my-app`).
- Backward-compatible symlink keeps existing user data and scripts working.
- Tool authors can opt into other home directories without changing the runtime user.
### Negative / Risks
- Requires an Alembic data migration that rewrites stored templates; downgrade must be carefully tested.
- Entrypoint permission fixer adds startup complexity and requires `root`/`sudo` inside the container.
- Creating `/workspace` symlink at build time vs. runtime needs care because the target may not exist until the repo is mounted.
- Legacy `tool-images/base.dockerfile` runs as `USER user`; it will need an entrypoint that can elevate permissions or the base image must be adjusted.
## Related Documents
- `openspec/designs/home-path-expansion.md`
- `openspec/specs/home-path-expansion.md`
- `openspec/tasks/home-path-expansion.md`
- `docs/superpowers/plans/tool-container-home-directory.md`
- `docs/superpowers/specs/tool-container-home-directory-test-plan.md`
+143 -51
View File
@@ -1,75 +1,167 @@
# Tasks: ~ / $HOME Expansion in Mount Paths
# Tasks: Tool Container Home Directory
## T1: Backend — Core helpers and pipeline
## Overview
### T1.1: Add `expand_container_path` helper
**File**: `apps/api/src/services/config_profile_resolver.py`
- Add `expand_container_path(path: str, home_dir: str) -> str`
- Handle `~/`, `~`, `$HOME/`, `$HOME` patterns
- Must not expand if path doesn't start with these patterns
This task extends the earlier `home-path-expansion` work into a full configurable workspace/home directory for tool containers. It is tracked as the implementation of `openspec/designs/tool-container-home-directory.md`.
### T1.2: Add `get_manifest_home_dir` helper
**File**: `apps/api/src/services/manifest_compiler.py`
- Add `get_manifest_home_dir(manifest: dict) -> str`
- Returns `/home/{user.name}` if user block exists, else `/root`
## T1: Schema and migration
### T1.3: Set `HOME` and `USER` env vars in Dockerfile
**File**: `apps/api/src/services/manifest_compiler.py`
- In `compile_dockerfile()`, after user creation block, add `ENV HOME=...` and `ENV USER=...`
- Update existing manifest compiler tests
### T1.1: Add `home_directory` column to `ToolType`
### T1.4: Update `apply_resolved_profile` to expand paths
**File**: `apps/api/src/services/config_profile_resolver.py`
- Add `home_dir: str = "/root"` parameter
- Expand mount targets before creating mount directories and volume entries
**File**: `apps/api/src/models/tool/tool_type.py`
### T1.5: Update git mount resolution to expand paths
**File**: `apps/api/src/api/tool_instances.py`
- Add `home_dir: str = "/root"` parameter to `_resolve_git_mount_mappings()`
- Expand mapping target paths before resolving
- Add `home_dir` parameter to `_resolve_git_mounts()` and `_resolve_single_git_mount()`
- Add `home_directory: Mapped[str]` column.
- Non-nullable with server default `"/home/user"`.
### T1.6: Determine home_dir in instance lifecycle
**File**: `apps/api/src/api/tool_instances.py`
- In `create_instance`: determine `home_dir` from tool type + manifest (if manifest), pass to `_modify_compose_file`
- In `start_instance`: determine `home_dir` from tool type + resolved manifest, pass to `apply_resolved_profile` and `_resolve_git_mounts`
- In `_prepare_manifest_instance`: return `home_dir` alongside image_tag and compose_content
### T1.2: Alembic migration for legacy tool types
### T1.7: Update `_modify_compose_file` to expand paths
**File**: `apps/api/src/api/tool_instances.py`
- Add `home_dir: str = "/root"` parameter
- Expand `working_directory` and any mount targets in extra_volumes
**File**: `apps/api/alembic/versions/<new>_add_tool_type_home_directory.py`
### T1.8: Unit tests
**File**: `apps/api/tests/unit/test_home_path_expansion.py` (new)
- Test `expand_container_path` with `~`, `~/foo`, `$HOME`, `$HOME/foo`, `/abs/path`, `rel/path`
- Test `get_manifest_home_dir` with user, without user
- Add the column.
- Set existing rows to `/home/user`.
- Rewrite `compose_template` and `dockerfile_template` to replace `/workspace` with `/home/user/{{WORKSPACE_NAME}}` where applicable.
- Provide downgrade that reverses rewrites and drops the column.
**File**: `apps/api/tests/unit/test_manifest_compiler.py`
- Test Dockerfile contains `ENV HOME=...` for user-based manifests
- Test Dockerfile contains `ENV HOME=/root` for root manifests
## T2: Manifest compiler
---
### T2.1: Honor `manifest.home_directory`
## T2: Verification
**File**: `apps/api/src/services/build/manifest_compiler.py`
- Update `get_manifest_home_dir()` to return `manifest.home_directory` if present, else derive from `user.name`, else `/root`.
### T2.2: Dockerfile generation
**File**: `apps/api/src/services/build/manifest_compiler.py`
- Set `ENV HOME={home_directory}` and `ENV USER={user.name}`.
- Set `WORKDIR {home_directory}` unless overridden by `runtime.working_dir`.
- Create `/workspace` symlink pointing to `{home_directory}/{{WORKSPACE_NAME}}` placeholder or create it at runtime.
### T2.3: Compose generation
**File**: `apps/api/src/services/build/manifest_compiler.py`
- Use `{home_directory}/{repo_name}` as default repo mount target when the manifest has no explicit repo mount.
- Keep `~`/`$HOME` expansion base equal to `home_directory`.
## T3: Legacy instance generation
### T3.1: Dockerfile-based tools
**File**: `apps/api/src/services/tool/instance_service.py`
- Use `tool_type.home_directory` (default `/home/user`).
- Mount `{repo_path}:{home_directory}/{repo_name}`.
- Ensure `/workspace` symlink exists.
### T3.2: Compose-based tools
**File**: `apps/api/src/services/docker/compose.py`, `apps/api/src/services/tool/instance_service.py`
- Add `WORKSPACE_NAME` and `HOME_DIRECTORY` to template variables.
- Validate migrated templates render correctly.
## T4: Config-profile and git mount expansion
### T4.1: Thread `home_dir` through startup
**File**: `apps/api/src/services/tool/instance_service.py`
- Compute `home_dir` from ToolType/manifest in `start_tool_instance()`.
- Pass to `apply_resolved_profile()` and `resolve_git_mounts()`.
### T4.2: Verify `~`/`$HOME` expansion
**File**: `apps/api/src/services/config/config_profile_resolver.py`
- Ensure `expand_container_path()` is called with the resolved `home_dir`.
## T5: Entrypoint permission fixer
### T5.1: Generate permission-fixing entrypoint
**File**: `apps/api/src/services/build/manifest_compiler.py`
- Generate a startup script that chowns `{home_directory}` and mounted paths to the container user.
- Create `/workspace` symlink at runtime if needed.
- Avoid recursive chown of large repo subtrees.
### T5.2: Base image support
**File**: `tool-images/base.dockerfile` (optional)
- Ensure `sudo` is available and the runtime user can elevate if the fixer runs inside the base image.
## T6: Tests
### T6.1: Unit 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`
- `apps/api/tests/unit/test_alembic_migrations.py`
Cover:
- `expand_container_path` edge cases.
- `get_manifest_home_dir` precedence.
- `compile_dockerfile`/`compile_compose` behavior.
- Legacy instance mount target.
- Alembic migration round-trip.
### T6.2: Integration tests
**File**: `apps/api/tests/integration/test_tool_instance_lifecycle.py`
Cover:
- Container starts with `HOME=/home/user` and repo at `/home/user/{repo_name}`.
- `/workspace` symlink works.
- Config-profile and git mounts under `~` are writable.
- Explicit manifest repo mount target is preserved.
## T7: Verification
### T7.1: Run affected tests
### T2.1: Run all affected tests
```bash
cd apps/api && pytest tests/unit/test_home_path_expansion.py tests/unit/test_manifest_compiler.py tests/unit/test_config_profile_resolver.py tests/unit/test_git_mounts.py -xvs
cd apps/api
pytest tests/unit/test_home_path_expansion.py tests/unit/test_manifest_compiler.py tests/unit/test_instance_service.py tests/unit/test_alembic_migrations.py -xvs
pytest tests/integration/test_tool_instance_lifecycle.py -xvs
```
### T2.2: Frontend typecheck
### T7.2: Alembic round-trip
```bash
cd apps/api
alembic upgrade head
alembic downgrade -1
alembic upgrade head
```
### T7.3: Frontend typecheck
```bash
cd apps/web && npm run typecheck
```
---
## Estimation
| Task | Effort | Files |
|------|--------|-------|
| T1.1-T1.7 | 2h | 3 |
| T1.8 | 1h | 2 |
| T2.1-T2.2 | 0.5h | |
| **Total** | **3.5h** | **5** |
| T1 | 1h | 2 |
| T2 | 2h | 1 |
| T3 | 2h | 2 |
| T4 | 1h | 2 |
| T5 | 2h | 2 |
| T6 | 3h | 4 |
| T7 | 1h | — |
| **Total** | **12h** | **13** |
## Related Documents
- `openspec/designs/tool-container-home-directory.md`
- `docs/superpowers/plans/tool-container-home-directory.md`
- `docs/superpowers/specs/tool-container-home-directory-test-plan.md`
- `openspec/designs/home-path-expansion.md`
- `openspec/specs/home-path-expansion.md`