diff --git a/docs/superpowers/plans/tool-container-home-directory.md b/docs/superpowers/plans/tool-container-home-directory.md new file mode 100644 index 0000000..85fe4f1 --- /dev/null +++ b/docs/superpowers/plans/tool-container-home-directory.md @@ -0,0 +1,171 @@ +# 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. diff --git a/docs/superpowers/specs/tool-container-home-directory-test-plan.md b/docs/superpowers/specs/tool-container-home-directory-test-plan.md new file mode 100644 index 0000000..481030c --- /dev/null +++ b/docs/superpowers/specs/tool-container-home-directory-test-plan.md @@ -0,0 +1,114 @@ +# Test Plan / QA Checklist: Tool Container Home Directory + +## Scope + +This plan covers validation of the configurable `home_directory` feature for tool containers, including: + +- `home_directory` field on `ToolType` and manifest schema. +- Repository/workspace mount target using `{home_directory}/{repo_name}` or `{home_directory}/{workspace_name}`. +- Manifest compiler behavior (`HOME`, `WORKDIR`, `/workspace` symlink). +- Legacy tool type migration via Alembic. +- Config-profile and git-mount `~`/`$HOME` expansion. +- Entrypoint permission fixer. + +## Unit Tests + +### `apps/api/tests/unit/test_home_path_expansion.py` + +| ID | Test | Expected Result | +|----|------|-----------------| +| U1 | `expand_container_path("~/config", "/home/user")` | returns `/home/user/config` | +| U2 | `expand_container_path("~", "/home/user")` | returns `/home/user` | +| U3 | `expand_container_path("$HOME/config", "/home/user")` | returns `/home/user/config` | +| U4 | `expand_container_path("$HOME", "/home/user")` | returns `/home/user` | +| U5 | `expand_container_path("/opt/data", "/home/user")` | returns `/opt/data` unchanged | +| U6 | `expand_container_path("relative/path", "/home/user")` | returns `relative/path` unchanged | +| U7 | `expand_container_path("path/$HOME/other", "/home/user")` | returns `path/$HOME/other` unchanged (mid-string not expanded) | + +### `apps/api/tests/unit/test_manifest_compiler.py` + +| ID | Test | Expected Result | +|----|------|-----------------| +| U10 | `get_manifest_home_dir({"home_directory": "/home/dev"})` | returns `/home/dev` | +| U11 | `get_manifest_home_dir({"user": {"name": "user"}})` | returns `/home/user` | +| U12 | `get_manifest_home_dir({"user": {"name": "dev"}})` | returns `/home/dev` | +| U13 | `get_manifest_home_dir({})` | returns `/root` | +| U14 | `compile_dockerfile()` for manifest with `home_directory: /home/user` and `user.name: user` | Dockerfile contains `ENV HOME=/home/user`, `ENV USER=user`, `WORKDIR /home/user`, and creates `/workspace` symlink | +| U15 | `compile_dockerfile()` for manifest without user block | Dockerfile does not create user and defaults `HOME=/root` | +| U16 | `compile_compose()` with no explicit repo mount and `home_directory: /home/user` | Compose mounts `{repo_path}:/home/user/{repo_name}` | +| U17 | `compile_compose()` with explicit repo mount target `/custom/path` | Compose preserves `/custom/path`; `/home/user` default is not injected | + +### `apps/api/tests/unit/test_instance_service.py` (new or expanded) + +| ID | Test | Expected Result | +|----|------|-----------------| +| U20 | `create_tool_instance()` for `definition_type == "dockerfile"` | Compose mounts `{repo_path}:/home/user/{repo_name}` | +| U21 | `create_tool_instance()` for `definition_type == "compose"` with migrated template | Template renders `/home/user/{{WORKSPACE_NAME}}` correctly | +| U22 | `start_tool_instance()` passes `home_dir` to `apply_resolved_profile()` | Config-profile `~/config` expands to `/home/user/config` | +| U23 | `start_tool_instance()` passes `home_dir` to `resolve_git_mounts()` | Git mount `~/repo` expands to `/home/user/repo` | + +### `apps/api/tests/unit/test_alembic_migrations.py` (new or expanded) + +| ID | Test | Expected Result | +|----|------|-----------------| +| U30 | Upgrade adds `home_directory` column with default `/home/user` | Column exists and legacy rows have value `/home/user` | +| U31 | Upgrade rewrites `compose_template` containing `/workspace` | Template now uses `/home/user/{{WORKSPACE_NAME}}` | +| U32 | Downgrade restores `/workspace` strings | Original templates are restored | +| U33 | Downgrade drops `home_directory` column | Column no longer exists | + +## Integration Tests + +### `apps/api/tests/integration/test_tool_instance_lifecycle.py` (new or expanded) + +| ID | Test | Steps | Expected Result | +|----|------|-------|-----------------| +| I1 | Manifest tool starts with `/home/user` home | Create manifest tool type with default `home_directory`; create and start instance | Container has `HOME=/home/user`, repo at `/home/user/{repo_name}`, `/workspace` symlink resolves correctly | +| I2 | Manifest tool with overridden `home_directory` | Set `home_directory: /home/dev` in manifest; create and start instance | Container has `HOME=/home/dev`, repo at `/home/dev/{repo_name}` | +| I3 | Container user can write under `/home/user` | Start instance; exec `touch /home/user/test-file` as container user | Command succeeds and file is owned by container user | +| I4 | Config-profile mount under `~` is writable | Create profile with mount `{"target": "~/config", "files": {"settings.json": "{}"}}`; start instance with profile | File appears at `/home/user/config/settings.json` and is writable by container user | +| I5 | Git mount under `~` is writable | Create profile with git mount mapping `{"target_path": "~/dotfiles"}`; start instance | Repo files appear at `/home/user/dotfiles` and are writable by container user | +| I6 | Legacy dockerfile tool uses new mount target after migration | Run Alembic migration; create legacy dockerfile tool instance | Repo mounts at `/home/user/{repo_name}` and `/workspace` symlink works | +| I7 | Explicit manifest repo mount target is preserved | Create manifest with explicit repo mount target `/opt/repo`; start instance | Repo mounts at `/opt/repo`; no `/home/user/{repo_name}` mount injected | + +## Manual QA Checklist + +- [ ] Create a new manifest tool type without specifying `home_directory`; verify default `/home/user` is used. +- [ ] Create a manifest tool type with `home_directory: /home/custom`; verify container reflects the override. +- [ ] Verify repository root directory name appears in the mount target (e.g., `/home/user/my-app`). +- [ ] Verify workspace-based instance uses workspace name in the mount target. +- [ ] Verify `/workspace` symlink points to the actual repo/workspace directory and `cd /workspace && pwd` works. +- [ ] Verify legacy dockerfile tool, after migration, mounts repo under `/home/user/{repo_name}`. +- [ ] Verify config-profile file mount under `~/config` lands at `/home/user/config`. +- [ ] Verify git mount under `~/dotfiles` lands at `/home/user/dotfiles`. +- [ ] Verify container user can create files under `/home/user` and mounted config directories. +- [ ] Verify `alembic downgrade -1` restores `/workspace` strings and removes the column. + +## Regression Coverage + +- Existing manifest tools without `home_directory` continue to work (default `/home/user`). +- Existing config profiles without `~`/`$HOME` continue to work. +- Existing legacy tools that did not use `/workspace` are unaffected by the migration rewrite. +- Existing `home-path-expansion` tests still pass. + +## Test Commands + +```bash +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 + +# Alembic round-trip +alembic upgrade head +alembic downgrade -1 +alembic upgrade head + +cd apps/web +npm run typecheck +``` + +## Exit Criteria + +- All listed unit and integration tests pass. +- Manual QA checklist is completed or equivalent automated coverage is added. +- Alembic migration round-trip passes without errors. +- No regressions in existing `home-path-expansion` or tool instance tests. diff --git a/openspec/designs/tool-container-home-directory.md b/openspec/designs/tool-container-home-directory.md new file mode 100644 index 0000000..04140c7 --- /dev/null +++ b/openspec/designs/tool-container-home-directory.md @@ -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` diff --git a/openspec/tasks/home-path-expansion.md b/openspec/tasks/home-path-expansion.md index 00c64e3..c87f8e7 100644 --- a/openspec/tasks/home-path-expansion.md +++ b/openspec/tasks/home-path-expansion.md @@ -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/_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`