- apply_mount_permissions now skips mounts with readonly=true to avoid
'Read-only file system' warnings on post-start chown/chmod
- Removed the ssh_keys mount from the pi-agent manifest definition;
instance-level SSH key mounting now handles this exclusively
- Added unit test for read-only mount skipping
Quality gates: pytest (15 passed)
- start_instance now deep-merges manifest with base definition before extracting user.uid/user.gid
- The user config is typically defined in the base image (ubuntu-24.04-dev), not the extending manifest
- Add debug logging to verify resolved uid/gid/home_dir
- Add logging to prepare_ssh_key_files for chown success/failure visibility
- Log current process uid when chown fails to diagnose permission issues
Quality gates: pytest 239 passed (6 pre-existing failures), tsc --noEmit clean
- Extend prepare_ssh_key_files() with optional uid/gid parameters
- Call os.chown on created files when uid/gid are provided
- Gracefully handle PermissionError if API process is not root
- In start_instance, extract container user UID/GID from manifest
- Pass container UID/GID when preparing instance-level SSH key mounts
- Legacy clone-mode SSH keys continue to use root (0,0)
- Add unit tests for prepare_ssh_key_files ownership logic
- Keep apply_ssh_permissions() as fallback for cases where host chown fails
Quality gates: pytest 239 passed (6 pre-existing failures), tsc --noEmit clean
- Replace apply_ssh_permissions internals with _exec_and_log for full visibility
- Log every docker exec command, stdout, and stderr at DEBUG level
- After chown/chmod, run ls -la and stat to verify final state
- Log verified state at INFO level so users can see exactly what happened
- Update tests to mock subprocess.run instead of _run_in_container
Quality gates: pytest 236 passed (6 pre-existing), tsc --noEmit clean
- lifecycle_hooks.publish_lifecycle_event now skips notification creation
when event_type='instance.started' and status='starting'
- Users only see notifications for terminal states:
- Failed: instance.error
- Successful: instance.health_changed with status='running'
- Updated integration tests to verify new behavior:
- test_lifecycle_started_intermediate_skips_notification
- test_lifecycle_running_creates_notification
Quality gates: pytest 42 passed, ruff clean
- Add POST /config-profiles/validate-git-url endpoint:
- Parses URL using existing parse_git_url utility
- Suggests corrected URL for browser URLs
- Runs git ls-remote --heads to verify reachability
- Lists available branches from remote
- Supports SSH key for private repos
- Returns structured response: valid, suggested_url, branches,
default_branch, error, error_code
- Update frontend GitMountEditor:
- Add Check button next to URL field with loading state
- Show validation result: valid (green), suggestion (yellow),
invalid (red)
- Suggestion includes Use this button to apply corrected URL
- Branch field becomes dropdown when URL is validated,
populated with remote branches
- Mappings section disabled until URL is validated
- Shows hint: Validate the URL first
- Quality gates: pytest (218 passed, 6 pre-existing),
tsc --noEmit (clean)
- Add notifications table with Alembic migration
- Notification model with user-scoped indexing and partial index on unread
- NotificationService singleton with create/list/count/mark-read/dismiss
- FastAPI router: GET /notifications, GET /unread, PATCH /{id}/read,
POST /mark-all-read, DELETE /{id}
- Mute categories filtering from UserConfig
- 13 unit tests for NotificationService
- 10 integration tests for API endpoints
- Updated test_models.py with new table registration
Quality gates: pytest 23 new passed, ruff clean
The previous sorting fix exposed a deeper bug: ResolvedMount always
mounted its staging directory as a single bind mount. When a config
profile mount targeted /workspace/x/y and contained a single file
z.json, the staging directory (containing only z.json) replaced the
ENTIRE /workspace/x/y directory, hiding all sibling files from git
repo mounts.
- Change apply_resolved_profile to mount each file individually:
- source: staging_dir/relative_path
- target: expanded_target/relative_path
- Sibling files from other mounts are preserved.
- Empty mounts produce no volume entries.
- Keep volume sorting (parent paths before child paths) which is
still necessary for directory mounts and ensures parent dirs exist
before file mounts inside them.
- Add 4 unit tests for file-level mount behavior.
Quality gates: pytest (218 passed, 6 pre-existing), tsc --noEmit (clean)
- Add instance_events and health_checks tables with Alembic migration
- InstanceEventBus: typed pub/sub singleton with wildcard support
- HealthMonitor: async background loop polling containers every 15s
- SSE endpoint GET /events/stream with auth and connection limits
- Lifecycle hooks in tool_instances.py (create/start/stop/restart/delete)
- Structured JSON logging with correlation IDs
- 15 new unit tests (EventBus, HealthMonitor, MonitoringModels)
Quality gates: pytest 15 new passed, ruff clean
- Add expand_container_path() helper that resolves ~/ and $HOME/ prefixes
- Add get_manifest_home_dir() to compute /home/{user.name} or /root from manifest
- Set ENV HOME=... and ENV USER=... in generated Dockerfile for runtime compatibility
- Pass home_dir through instance creation and startup pipeline
- Expand mount targets in apply_resolved_profile() for regular profile mounts
- Expand mapping targets in _resolve_git_mount_mappings() for git mounts
- Expand working_directory and volume targets in _modify_compose_file()
- Update _prepare_manifest_instance to return home_dir alongside image tag
- Fetch tool_type early in start_instance to determine home_dir before profile application
Quality gates: pytest 188 passed, frontend typecheck clean
Addresses: home-path-expansion
- Add mappings array support to git_mount entries
- Clone repository once per git_mount entry, mount multiple subdirectories
- Normalize legacy source_path+target_path to mappings on read
- Update _merge_git_mounts to dedup by (remote_url, branch) and concatenate mappings
- Add _normalize_git_mount, _clone_git_repo, _resolve_git_mount_mappings helpers
- Update GitMountItem Pydantic model with GitMountMapping and model_validator
- Update frontend GitMountEditor component with mappings UI
- Auto-convert legacy git mount entries to mappings format on load
- Add 15 backend unit tests for normalization, resolution, and glob expansion
- Update existing config profile resolver tests for new merge behavior
Quality gates: pytest 167 passed, frontend typecheck clean
Addresses: config-profile-multi-repo-mounts
The frontend router navigates to /instances/:instanceId/terminal without
project_id or repo_id. The backend terminal REST endpoints were requiring
these path params, causing 404s.
- Simplify _get_terminal_instance to validate by instance_id only
- Update all REST routes from /projects/{pid}/repositories/{rid}/instances/{iid}/terminal/*
to /instances/{instance_id}/terminal/*
- Update frontend API client to match new paths
- Update useTerminalSessions hook to take instanceId only
- Update TerminalPage to use simplified hook
- Update tests to match new paths
Fixes: 404 on GET /projects/repositories/instances/{id}/terminal/sessions
- Add test_tool_instances_legacy.py with 8 unit tests:
- dockerfile definition type builds from template
- dockerfile build failure raises HTTP 500
- compose definition type renders template
- manifest compiler is NOT called for legacy types
- start_instance legacy/compose/dockerfile types all skip manifest flow
- start_instance manifest type correctly invokes compiler
- Mark T3.2 and T3.3 tasks complete in OpenSpec
- Add openspec/docs/tool-workshop-guide.md with user guide covering
definition types, manifest creation workflow, base definitions,
migration path, and permissions
- Add ToolDefinitionManifest model with base image versioning
- Add manifest compiler: Dockerfile + Compose generation from JSON manifests
- Add permission fixer: post-start chown/chmod for mount policies
- Add tool definition CRUD API with live compile preview endpoint
- Integrate manifest-based startup flow in start_instance
- Add Alembic migration with data conversion for pi-agent
- Add 48 unit tests for manifest compiler, permission fixer, docker service
- Keep backward compatibility with legacy dockerfile_template/compose_template
Migration: applied successfully. Pi-agent converted to manifest.
Quality gates: pytest (146 passed, 4 pre-existing unrelated failures)
- Add WebSocket route /ws/tool-instances/{instance_id}/terminal/{session_id}
- Preserve /terminal as default-session alias for backward compatibility
- Extract shared _handle_terminal_websocket handler for both routes
- Add REST endpoints: GET list, POST create, DELETE close, POST reset, POST rename
- Preserve legacy POST .../terminal/reset as default session alias
- Add frontend API client (apps/web/src/api/terminal.ts)
- Add useTerminalSessions React hook for session CRUD + state management
- Add integration tests for auth requirements on all new endpoints
Quality gates: pytest (8 new passed, 182 total passed, 51 pre-existing failures)
- Add TerminalSessionModel DB table with instance_id FK, name, status,
created_at, last_activity_at, closed_at columns
- Add Alembic migration for terminal_sessions table
- Refactor TerminalManager to use composite key (instance_id, session_id)
supporting up to 5 concurrent sessions per instance
- Add create_session, get_session, get_sessions_for_instance, close_session
- Preserve get_or_create_session for backward compatibility (default session)
- Fix attach_websocket to only close sockets within same session
- Add name (auto-generated 'Session N') and status tracking to TerminalSession
- Add 7 unit tests for multi-session logic
Quality gates: pytest (7 new passed, 174 total passed, 51 pre-existing failures)
- get_container_id() and get_container_name() now lowercase the
instance name before passing to docker ps --filter, because
Docker container names are lowercase internally and the filter
is case-sensitive. This caused container_id to never be captured
when instance.name contained uppercase chars (e.g. 'Headquarter'),
breaking terminal WebSocket connections.
- Also guard proc.stdout being None in start_cloudflared_tunnel().
- Add unit tests for get_container_id and get_container_name.
Quality gates: pytest (14 passed), python clean
- Remove absolute path requirement from target_path validation
- Resolve relative paths against working_directory at instance startup
- Fall back to /home/user if no working_directory is configured
- Update frontend to allow relative target paths
- Update spec to document relative path support
- Update tests to allow relative paths and test path traversal rejection
- _checkout_branch now returns bool and falls back gracefully on failure
- Glob warning message includes matched file count
- Fix database model comment to reference remote_url
- Update tests for new branch checkout behavior
All 51 tests pass
- Change git mount schema from repo_id to remote_url
- Remove database lookups for git mount resolution
- Clone directly from URL at instance startup
- Simplify frontend UI to text input for Git URL
- Fix route ordering in git_repositories.py to prevent 422 errors
- Update all tests to use remote_url field
Breaking change: Git mounts now use remote_url instead of repo_id
- Add startup_command field to ToolType model and API
- Execute startup command before interactive shell in terminal sessions
- Add tmux and ranger to OpenCode container spec
- Update Tool Workshop UI with startup_command input for terminal types
- Add backend tests for startup_command CRUD operations
- Sync specs: tool-terminal, tool-types-definition, opencode-web-server
- New spec: tool-terminal-startup-command
Quality gates: Frontend typecheck/lint passed. Backend tests blocked by environment (Python/Docker not available).
OpenSpec: terminal-startup-and-container-tools
- Add ConfigProfile and ConfigProfileInclude data models with migrations
- Implement profile resolver service with ordered includes and merge rules
- Add profile CRUD API with validation, compatibility, and cycle detection
- Add instance API plumbing for profile selection on create/start/restart
- Add resolved profile preview and default resolution APIs
- Add frontend config profile API client and management UI
- Add launch/restart profile selection UI
- Add backend integration and unit tests (31 passing)
OpenSpec: add-config-profiles
Quality gates: ruff, TypeScript compile, 31 tests passing
Resolved conflicts:
- Moved branch selection UI from inline sessions.tsx to CreateSessionForm component
- Integrated branch dropdown and new branch creation into CreateSessionForm
- Removed duplicate branch state management from sessions.tsx
All branch selection tests pass (7/7).
- Drop is_builtin column from tool_types table
- Remove built-in tool seeding from startup
- Remove is_builtin from API schemas and frontend types
- Update tool-types spec to reflect removal of built-in concept
- Add Alembic migration for column removal
- Update tests to work without built-in distinction
- git-repo-working-clones: Complete remaining test task
- opencode-web-terminal: Add port validation tests, fix model validator
- session-management-fixes: Mark frontend tasks as complete (already implemented)
All in-progress changes now complete.
- Fall back to symbolic-ref when checkout --orphan fails on bare repos\n- Fall back to symbolic-ref when checkout fails on bare repos\n- Make get_current_branch handle bare repos with unborn branches\n- Add integration tests for bare repo branch operations\n\nQuality gates: pytest integration tests (12 passed)
- Add POST /tool-types/validate endpoint for pre-creation validation
- Add ToolConfigUpdate model with optional fields for PUT endpoint
- Fix tool_configs POST to return 201 status code
- Fix tool_configs list endpoint to return list instead of dict
- Fix tool_configs defaults endpoint to return 'suggested_configs'
- Fix tool_types create endpoint to include category and interfaces
- Add model_validator to enforce dockerfile/compose template requirements
- Update tests to match API response format
- Create orphan branches when HEAD does not exist yet\n- Use current branch as the default branch base in the toolbar\n- Keep push disabled for remote-less repos\n\nQuality gates: vitest repositories-settings-tab (passed); api pytest blocked by missing fastapi in environment
- Create normal working clones for remote repositories
- Initialize blank repositories with a main branch
- Align pull and branch helpers with unborn HEAD handling
- Gate fetch/pull on repositories with a remote
Quality gates: vitest repositories-settings-tab (passed); api pytest blocked by missing fastapi in environment
Backend tests:
- Unit tests for docker_build service (successful/failed builds, context, paths)
- Unit tests for readiness_probe service (success, timeout, retries, edge cases)
- Integration tests for config_folders API (CRUD + project overrides)
- Integration tests for tool_types API with new fields
- Integration tests for tool_configs API with new fields
Frontend tests:
- ToolWorkshopPage component tests (all 3 tabs, create/edit/delete)
- API client tests for tool_types and config_folders
Fixes:
- Add field_validator import to tool_configs.py
- Add JSON import to tool_config model
- Update frontend test button names to match UI (Create Tool Type, Add Config, Create Folder)
Quality gates: backend unit tests passing (23/23)