Compare commits

..

72 Commits

Author SHA1 Message Date
Developer 79be4eb525 chore: merge project map patches 2026-06-12 19:09:15 +00:00
Developer 04225ef890 chore: patch project maps after workspace detail formatting
Regenerate .pi-map*.md files for files touched by the formatting pass.
2026-06-12 19:09:15 +00:00
Developer 7f78d92fef style: merge workspace detail formatting fix 2026-06-12 19:08:16 +00:00
Developer bbfcde3d1b style: apply formatting to workspace detail extraction
Post-write formatter pass on components extracted for reorganize-long-files.
No behavioral changes.
2026-06-12 19:08:16 +00:00
Developer 9f4e9845c0 feat: merge reorganize-long-files completion
See feature commit for details.
2026-06-12 18:53:23 +00:00
Developer ce8b5dc86d feat: complete reorganize-long-files cleanup
- Extract tool instance lifecycle endpoints (start/stop/restart/delete) from
  api/tool/tool_instances.py into new api/tool/tool_lifecycle.py.
- Register tool_lifecycle_router in main.py and api/tool/__init__.py.
- Extract inline WorkspaceDetailPage components into
  components/features/workspace/: detail header, tab bars, file/git/tools/settings
  panels. Slim page from ~446 to ~62 lines.
- Update OpenSpec reorganize-long-files tasks to reflect completed work and
  current source state; mark change completed.
- Regenerate project maps.

Quality gates: python3 -m py_compile (backend clean), npm run typecheck,
npm run lint, npm test -- --run (87 passed), pytest workspace integration
and unit tests (27 passed, 1 skipped).
2026-06-12 18:53:23 +00:00
Developer efb62fe41a feat: merge workspace-first-ui completion
See feature commit for details.
2026-06-12 17:25:09 +00:00
Developer c26e9eacfa feat: complete workspace-first-ui cleanup and tests
- Delete dead repo-workspace code: RepoWorkspacePage, useRepoWorkspace,
  WorkspaceLayout, FileBrowser, old git components (git-toolbar, file-editor,
  commit-panel), and repo-workspace.css.
- Fix stale backend test imports for moved models/services.
- Add GitOperations unit tests.
- Add integration tests for workspace files, git, and instances endpoints.
- Add frontend tests for WorkspaceDetailPage and ProjectCard.
- Update OpenSpec workspace-first-ui tasks and mark change completed.
- Regenerate project maps.

Quality gates: npm run typecheck, npm run lint, npm test -- --run (87 passed),
python3 -m py_compile on changed backend files, pytest backend workspace tests.
2026-06-12 17:25:09 +00:00
Developer aa49efb236 feat: merge working-copies completion
See feature commit for details.
2026-06-12 15:47:41 +00:00
Developer 6b947b7593 feat: complete working-copies workspace-first cleanup
- Remove clone_mode/branch/new_branch from frontend create session flow.
- Add workspace picker to CreateSessionForm; auto-create default workspace when repo selected.
- Fix tool-starter.tsx and use-start-tool.ts createInstance signatures after API change.
- Remove clone mode badge from SessionCard.
- Delete stale backend unit tests referencing removed clone_mode schema fields.
- Update OpenSpec working-copies tasks and mark change completed.
- Regenerate project maps.

Quality gates: npm run typecheck, npm run lint, npm test -- --run (82 passed),
python3 -m py_compile on changed backend files.
2026-06-12 15:47:26 +00:00
Developer 3da7ea6408 WIP: frontend clone_mode removal in create form and API
- Remove clone_mode/branch/new_branch from createInstance API helper
- Remove clone mode UI and branch fields from CreateSessionForm

Remaining: wire workspace_id in form/tool-starter, remove session-card badge, tests
2026-06-12 15:32:19 +00:00
Developer 79aabd6f43 WIP: working-copies backend cleanup
- Remove clone_mode/branch from API responses and make DB columns nullable
- Remove legacy clone-mode branches from create_tool_instance
- Add WORKSPACE_PATH compose variable alongside REPO_PATH
- Add workspace migration helpers in WorkspaceManager

Remaining: POST /workspaces/:id/instances, frontend clone_mode cleanup, tests
2026-06-12 15:17:16 +00:00
Developer 40ac931c65 chore: patch project maps for multi-session-terminal-ux metadata updates 2026-06-12 14:43:22 +00:00
Developer aa51ba219b docs: complete multi-session-terminal-ux OpenSpec metadata 2026-06-12 14:41:12 +00:00
Developer 7623f29ffb docs: complete multi-session-terminal-ux OpenSpec metadata
- Fill empty apply-pr2.md with backend API + frontend client apply report
- Mark all 12 tasks as completed in tasks.md
- Update .openspec.yaml status from exploring to completed

The implementation was already merged to dev across PR 1, PR 2, and PR 3.
This commit only synchronizes the OpenSpec change metadata.
2026-06-12 14:41:12 +00:00
Developer 3979b1f4e7 chore: archive 15 completed OpenSpec changes 2026-06-12 14:27:18 +00:00
Developer caadd59441 chore: archive 15 completed OpenSpec changes
Move the following audited-and-implemented changes into
openspec/changes/archive/2026-06-12-completed-changes-archive/:

- backend-frontend-refactoring
- config-profile-git-mounts
- config-profile-includes-ui
- config-profile-multi-repo-mounts
- container-monitoring-notifications
- git-mount-url-validation
- home-path-expansion
- mobile-terminal-ux
- mount-specificity-ordering
- notification-center
- persistent-terminal-sessions
- session-list-overhaul
- ssh-key-mounting
- terminal-fullscreen-unified-header
- tool-session-progress-and-updates

Also regenerated .pi-map*.md files for openspec/changes so the
remaining active changes (multi-session-terminal-ux, reorganize-long-files,
working-copies, workspace-first-ui) reflect the new layout.
2026-06-12 14:26:55 +00:00
Developer 30549f4863 chore: track all project map and index files 2026-06-12 13:47:13 +00:00
Developer 070cd4d5a5 chore: track all project map and index files
Add generated .pi-map.md and .pi-map.index.md files across the repository
so the project navigation maps are shared and versioned. These artifacts
are maintained by project_map_init/patch/validate and must be kept in
sync with source edits.

Note: .cache/ remains ignored (added in previous commit).
2026-06-12 13:46:23 +00:00
Developer 44ef62271e feat: merge tool-session progress panel and live list updates 2026-06-12 13:25:49 +00:00
Developer 7440720b7b feat: implement tool-session progress panel and live list updates
- Add SessionOperationsContext + SessionProgressPanel for global,
  non-blocking lifecycle progress (create/start/stop/restart/delete/
  recreate-tunnel) driven by SSE events.
- Promote SessionsContext to authoritative shared session state with
  refresh, addOrUpdateSession, and removeSession helpers.
- Wire AppShell, DashboardPage, SessionsPage, useInstanceActions,
  ToolStarter, and InstanceList into shared state so lists update
  immediately after create/delete without manual refresh.
- Remove legacy blocking overlays from CreateSessionForm, SessionCard,
  and InstanceList; keep disabled states and inline spinners only.
- Update DashboardPage tests to wrap with SessionsProvider and
  SessionOperationsProvider.
- Add .cache/ to .gitignore.

Quality gates: npm run typecheck, npm run lint, npm test -- --run
(82 passed).
2026-06-12 13:19:58 +00:00
Developer 110844e597 fix: use defined CSS var --panel instead of undefined --surface
The session options dropdown used var(--surface) which was never
defined anywhere in the stylesheet, causing a transparent background.
Changed to var(--panel) which is defined in tokens.css.

Quality gates: tsc --noEmit pass, npm run build pass, 82/82 tests pass
2026-06-11 15:52:30 +00:00
Developer b4d08b0232 feat: combine session actions into options dropdown
SessionCard desktop view:
- Retain primary action button: Open (active) or Start (inactive)
- Replace individual Stop/Tunnel/Delete buttons with a single Options
  dropdown triggered by a ⋯ button
- Dropdown contains applicable actions:
  - Active: Stop, Recreate Tunnel (web), Delete
  - Inactive: Start, Delete
- Add window.confirm before Delete as a safety net
- Dropdown closes on outside click or Escape key
- Add 'more' icon (DotsThreeVertical) to icon component
- Add session-options-dropdown CSS with subtle animation

Mobile view unchanged (already uses MobileActionSheet).

Quality gates: tsc --noEmit pass, npm run build pass, 82/82 tests pass
2026-06-11 15:32:03 +00:00
Developer f1180f6053 fix: ensure container user owns ~/.config and other home dirs
Root cause: manifest-based Dockerfile created the home directory and
chowned only the home root. Files/directories copied from /etc/skel by
useradd -m (or created later by root) remained root-owned, so apps like
ranger failed when writing to ~/.config.

Changes:
- manifest_compiler.py: recursive chown of the home directory after
  useradd so /etc/skel contents are owned by the container user
- Pre-create .config, .local/share, .cache and chown them to the user
  so first-run apps have writable directories immediately
- Add unit test verifying the Dockerfile emits the expected user/home
  setup and config directory creation

Quality gates: py_compile all backend files pass, test file compiles,
tsc --noEmit pass, npm run build pass, 82/82 web tests pass
Note: pytest not available in this shell; backend unit test was not
executed but follows existing project conventions.
2026-06-11 15:05:09 +00:00
Developer 59bec31046 style: add separator between workspace and tool type in terminal tab title
Before: 'MyWorkspace code-server Session 1'
After:  'MyWorkspace · code-server Session 1'

Quality gates: tsc --noEmit pass, npm run build pass, 82/82 tests pass
2026-06-11 09:19:26 +00:00
Developer 8a7bec8df2 fix: terminal tab title uses workspace + tool type + session name
Instead of relying on display_name (which for old instances is just the
workspace name), fetch the full session info and build the title from
individual fields:

- Base: '{workspace_name} {tool_type_name}'
- With multiple terminal sessions: '{workspace_name} {tool_type_name} {session_name}'
- Fallback: 'Instance {id}' if session lookup fails

This gives meaningful titles like 'MyWorkspace code-server Session 1'
instead of just 'MyWorkspace — Terminal'.

Quality gates: tsc --noEmit pass, npm run build pass, 82/82 tests pass
2026-06-11 09:12:04 +00:00
Developer c881bbdad3 fix: clean up tab titles and sidebar session display
- Remove useless document.title from handleOpen (it only changed the
  caller page's title, not the new tab)
- Terminal tab title: simpler format without extra '— Headquarter' suffix
  - Single session: '{name} — Terminal'
  - Multiple sessions: '{name} · {session_name} — Terminal'
- Sidebar live sessions: show tool type in small muted text next to
  display_name so sessions aren't indistinguishable when display_name
  is just the workspace name

Quality gates: tsc --noEmit pass, npm run build pass, 82/82 tests pass
2026-06-11 09:04:34 +00:00
Developer f1b968bb88 feat: default session name to '{workspace} {tool_type}', improve terminal tab title
ToolStarter:
- Auto-populate Session Name as '{workspace.name} {tool_type.display_name}'
  when a tool type is selected
- Track whether user has manually edited the name (nameEdited flag) to avoid
  overwriting their custom input

Terminal page:
- Fetch instance display_name via getUserSessions for tab title
- Tab title format: '{display_name} {terminal_session_name} — Terminal'
  instead of just '{session_name} — Terminal'

Quality gates: tsc --noEmit pass, npm run build pass, 82/82 tests pass
2026-06-11 08:11:04 +00:00
Developer 3c222d4f0f feat: add session name input to tool starter
ToolStarter (used by FAB and workspace detail):
- Add Session Name text input, defaulting to workspace.name
- Pass user-provided name to createInstance display_name parameter
- If left empty or only whitespace, falls back to auto-generated name

Quality gates: tsc --noEmit pass, npm run build pass, 82/82 tests pass
2026-06-10 20:26:34 +00:00
Developer 1d10283fc9 feat: improve session naming, project display, rename support, tab titles
Backend:
- sessions.py: include workspace_name in session response
- instance_service.py: auto-generate display names as
  'Project / Workspace / Tool #N' instead of 'Workspace / Tool #N'
- instance_service.py: add rename_tool_instance() service function
- tool_instances.py: add PATCH /instances/{id} endpoint for renaming
  display_name

Frontend:
- api/sessions.ts: add workspace_name to Session type, add renameInstance()
- use-instance-actions.ts: add handleRename, set document.title when opening
- session-card.tsx: click-to-edit display_name inline; always show project
  context line (Project / Workspace or Repo / Tool)
- session-list.tsx: pass through onRename prop
- SessionsPage.tsx: wire handleRename to SessionCard and SessionList
- app-shell.tsx: sidebar tooltip includes workspace or repo name
- use-terminal-page.ts: set document.title based on active terminal session

Quality gates: py_compile all backend files pass, tsc --noEmit pass,
npm run build pass, 82/82 tests pass
2026-06-10 17:04:03 +00:00
Developer 886be83af5 fix: enable folder navigation in workspace file browser
FilesTab in WorkspaceDetailPage was returning early for directories with
no action, making folders unclickable.

Changes:
- use-workspace-files.ts: add currentPath state and navigateTo() function;
  refresh() now passes currentPath to listWorkspaceFiles API
- WorkspaceDetailPage.tsx FilesTab: handleSelect now calls navigateTo()
  for directories; added navigateUp() button using '..' when not at root
- Clear selected file/editor state when changing directories

Quality gates: tsc --noEmit pass, npm run build pass, 82/82 tests pass
2026-06-10 13:09:42 +00:00
Developer 82091e31a8 fix: stop health monitor spam and garbled notification metadata
Root causes:
1. No dedup for monitor restarts — _last_known_state is cleared on stop,
   so every restart re-sent notifications for all unhealthy instances.
2. Aggressive error classification — any Docker state other than 'running'
   was treated as 'error', including transient 'created' and 'restarting'.
3. Confusing metadata — when new_status == previous_status (after restart),
   notifications showed previous_status equal to current status.

Fixes:
- _check_instance: when previous is None (first check) and new_status equals
  the DB status, just record the snapshot and skip _handle_state_change.
  This prevents duplicate events/notifications on monitor restart.
- _derive_status: only treat 'exited' and 'dead' as error. Preserve current
  status for transient Docker states ('created', 'restarting').
- _derive_status: if DB says 'running' but container is 'not_found',
  return 'error' instead of preserving 'running' (fixes silent failure).
- _handle_state_change: improved unhealthy message to 'Container tunnel is
  unreachable' instead of generic 'Container is now unhealthy'.

Quality gates: py_compile all backend files pass, tsc --noEmit pass,
npm run build pass, 82/82 tests pass
2026-06-09 15:04:53 +00:00
Developer b2c84e2064 fix: default to full URL mode and short-circuit SSH URL validation in repo dialog
RepositoryCreateDialog fixes:
- Change useAdvancedUrl default from false to true so full URL is the default
- Move isSshUrl helper before the effect that references it
- Short-circuit SSH URLs client-side in debounced validation so they always
  show as valid without depending on backend parseGitUrl behavior
- Keeps submit-time SSH key requirement: error shown if SSH URL without key

Tests:
- Update repositories-settings-tab tests for full-URL default mode
- Add SSH URL acceptance test with key selected (client-side short-circuit)
- Add SSH URL rejection test without key selected

Quality gates: tsc --noEmit pass, npm run build pass, 82/82 tests pass
2026-06-09 14:07:02 +00:00
Developer 680417a0a2 fix: remove rel=noopener from all session/instance links to enable tab reuse
rel=noopener forces a fresh browsing context, breaking target name matching
and window reference tracking across browsers.

Changes:
- use-instance-actions.ts: track opened tabs in Map, call .focus() on existing
- session-card.tsx: all Open buttons now go through handleOpen callback
- app-shell.tsx: sidebar session links use target='session-{id}' + noreferrer
- workspace-instance-chips.tsx: remove noopener from chip links
- instance-list.tsx: remove noopener from Open links
- WorkspaceDetailPage.tsx: use named target + noreferrer

Quality gates: tsc --noEmit pass, npm run build pass, 80/80 tests pass
2026-06-09 12:39:32 +00:00
Developer e5e29aca49 fix: remove features string from window.open to enable tab reuse
window.open(url, name, 'noopener,noreferrer') with a non-empty features
string forces a new popup window and ignores the name for tab reuse.
Remove the third parameter so the browser focuses existing named tabs.

Quality gates: tsc --noEmit pass, npm run build pass, 80/80 tests pass
2026-06-09 12:18:28 +00:00
Developer 486f3cbc44 feat: reuse existing tabs when opening sessions instead of always creating new ones
Use named window targets (session-{id}, instance-{id}) instead of _blank:
- use-instance-actions.ts: window.open(..., 'session-{id}') for programmatic opens
- session-card.tsx: <a target='session-{id}'> for direct link opens (mobile + desktop)
- workspace-instance-chips.tsx: <a target='instance-{id}'> for chip links
- instance-list.tsx: <a target='instance-{id}'> for instance open links

Browser behavior: if a tab with that target name exists, it navigates/focuses
that tab instead of opening a new one.

Quality gates: tsc --noEmit pass, npm run build pass, 80/80 tests pass
2026-06-09 11:01:40 +00:00
Developer 152f87a254 feat: add SSH URL validation, inline radio buttons in repo creation dialog
RepositoryCreateDialog (already unified, used everywhere):
- Add isSshUrl() helper to detect git@ and ssh:// URLs
- Require SSH key selection when URL is SSH; show error otherwise
- Inline existing/new radio buttons with smaller styling (.repo-mode-radios)

Tests:
- Update repositories-settings-tab.test.tsx to select SSH key for owner/repo mode
- Mock listSSHKeys in tests

Quality gates: tsc --noEmit pass, npm run build pass, 80/80 tests pass
2026-06-09 10:29:48 +00:00
Developer 349066bcfa fix: render RepositoryCreateDialog on desktop when adding repository
RepositoryCreateDialog was only rendered inside the isMobile block.
Desktop 'Add Repository' clicks set state but the dialog never appeared.
Add conditional rendering in the desktop section.

Quality gates: tsc --noEmit pass, npm run build pass, 80/80 tests pass
2026-06-09 09:32:59 +00:00
Developer c353bceb97 feat: unify repository creation flow for desktop and mobile
- ProjectCard.tsx: add onAddRepository prop, show 'Add Repository' button in expanded view
- ProjectCard.tsx: add showBackButton/onBack props for mobile detail reuse
- ProjectsPage.tsx (desktop): wire onAddRepository to open RepositoryCreateDialog
- ProjectsPage.tsx (mobile): reuse ProjectCard for detail view instead of inline duplication
- pages/projects.css: add .project-add-repo style

Both platforms now use the same ProjectCard component and RepositoryCreateDialog
for adding repositories to projects.

Quality gates: tsc --noEmit pass, npm run build pass, 80/80 tests pass
2026-06-09 09:22:51 +00:00
Developer 64dcdc9d0d feat: add mobile support for Projects page with repository creation
- ProjectsPage.tsx: detect mobile viewport, show MobileListView / custom detail view / MobileFAB
- Mobile list: tap project to view details (name, description, repositories, workspaces)
- Mobile detail: shows repositories with New Workspace buttons, Add Repository button, Edit/Delete project actions
- Mobile FAB: opens inline project creation form
- RepositoryCreateDialog reused for mobile 'Add Repository' flow
- New CSS: .mobile-form-actions, .mobile-form-group for mobile form layouts

Quality gates: tsc --noEmit pass, npm run build pass, 80/80 tests pass
2026-06-07 16:00:07 +00:00
Developer 4ea2e3659d feat: add mobile support for Workspaces page with workspace creation
- WorkspacesPage.tsx: detect mobile viewport, show MobileListView / MobileDetailView / MobileFAB
- Mobile list: tap workspace to view details (branch, status, path, instances, sync time)
- Mobile detail: view workspace fields with Edit and Delete actions
- Mobile FAB: opens inline WorkspaceCreateForm
- New styles/pages/workspaces.css with responsive grid and card styles
- Import workspaces.css in main.tsx

Quality gates: tsc --noEmit pass, npm run build pass, 80/80 tests pass
2026-06-07 14:09:28 +00:00
Developer 734bd9529a feat: add error message and expandable metadata to notification items
- Show notification.message in notification-item.tsx
- Add Details toggle to expand metadata (exit_code, previous_status, etc.)
- Add CSS styles for message and metadata display

Quality gates: tsc --noEmit pass, 80/80 tests pass
2026-06-06 09:58:24 +00:00
Developer 1ef9d66eed fix: reduce false container-failed notifications, add error details to UI
Backend (health_monitor.py):
- Skip health checks for instances with no container_id
- Treat 'not_found' as error only when container was previously running
- Skip duplicate error notifications when already in error state
- Skip 'not_found' notifications for containers that never ran

Frontend (notification-item.tsx):
- Display notification.message (detailed error text)
- Add expandable Details section showing metadata (exit_code, previous_status, etc.)
- New CSS styles for message and metadata display

Quality gates: py_compile, tsc --noEmit, 80/80 tests pass
2026-06-06 09:01:01 +00:00
Developer 2169b24875 fix: repair 3 failing tests after page/component extraction
- DashboardPage.test.tsx: update expected text 'Available projects' → 'Workspaces'
- ProjectsPage.test.tsx: add project.description rendering to ProjectCard
- repositories-settings-tab.test.tsx: default RepositoryCreateDialog to owner/repo mode (useAdvancedUrl=false)

Quality gates: all 80 tests pass
2026-06-05 21:45:46 +00:00
Developer f2a3399f27 merge: resolve dev branch conflicts, add compose_template guard
Incorporate remote bug fixes into slimmed instance_service.py:
- Add tool_type.compose_template guard before render_compose_template

Quality gates: py_compile pass
2026-06-05 21:32:50 +00:00
Developer 5266e64be2 refactor: slim ConfigProfilesPage to 150 lines
Extract handleReset callback, compact loading/error states.

Quality gates: tsc --noEmit pass
2026-06-05 21:22:44 +00:00
Developer 9a17916dd2 refactor: slim backend routers to ≤500 lines
- tool_instances.py: 2108 → 496 lines
- git_repositories.py: 1422 → 500 lines
- config_profiles.py: 474 → 300 lines (already committed)

Extract business logic into services:
- services/tool/instance_service.py
- services/git/operations.py
- services/config/crud_service.py

Quality gates: py_compile pass on all files
2026-06-05 21:18:57 +00:00
Alex Blank a388a8bec9 fix: move router to api layer and add missing imports/guards
- Move APIRouter definition from instance_service.py back to tool_instances.py
  (service files should not define FastAPI routers)
- Add missing prepare_manifest_instance import in tool_instances.py
- Guard repo.remote_url before clone_repository call
- Guard tool_type.compose_template before render_compose_template call
- Rename subprocess result variable to avoid shadowing SQLAlchemy Result
- Build error message as local string to avoid None/bool type issues

Quality gates: py_compile pass, LSP clean
2026-06-05 23:11:08 +02:00
Alex Blank 51a98a0c63 fix: add missing APIRouter import and None-safety in instance_service
- Import APIRouter from fastapi (NameError on module load)
- Add None check after session.get(ToolType) to prevent AttributeError
- Type-annotate volume_mounts and guard extend() with isinstance(list)

Quality gates: py_compile pass, LSP clean
2026-06-05 23:03:40 +02:00
Developer 6efe524974 refactor: slim config_profiles router to 301 lines
Extract CRUD helpers into services/config/crud_service.py.
Move instance-related config logic to services/tool/instance_service.py.

Quality gates: py_compile pass
2026-06-05 20:56:50 +00:00
Developer 88c56a83b7 feat: slim backend routers - extract helpers to services
- Extract tool_instances.py helpers (790 lines) to services/tool/instance_service.py
  Reduces tool_instances.py from 2900 to 2108 lines
- Previously merged: config_profiles helpers and git_repositories helpers

Quality gates: py_compile passes
2026-06-05 20:23:55 +00:00
Developer b4aa4c5fcb refactor: extract tool_instances helpers to service
- Extract 700+ lines of helper functions to services/tool/instance_service.py
- Slim tool_instances.py from 2900 to 2108 lines
- Extracted functions: resolve_git_mounts, normalize_git_mount, clone_git_repo,
  resolve_git_mount_mappings, resolve_single_git_mount, checkout_branch,
  pull_repository_updates, expand_glob_source, validate_config_profile,
  sanitize_compose_file, modify_compose_file, ensure_container_name_in_compose,
  ensure_web_bind_address, ensure_backend_network_in_compose,
  prepare_manifest_instance

Quality gates: py_compile passes
2026-06-05 20:23:39 +00:00
Developer 183e910afd feat: reorganize long files - frontend pages, CSS, partial backend
Frontend:
- Extract ToolWorkshopPage (1269→110), ConfigProfilesPage (1611→170),
  TerminalPage (571→112), SettingsPage (284→137), SshKeysPage (277→84),
  ProjectsPage (433→113), RepoWorkspacePage (505→89)
- Extract 15+ components and 8 hooks for state management
- Delete monolithic styles.css (5683 lines), extract to styles/ directory

Backend:
- Extract config_profiles helpers to services/config/crud_service.py
  and resolver_service.py (842→474 lines)
- Extract git_repositories helpers to services/git/operations.py
  (1588→1422 lines)

Quality gates: tsc --noEmit pass, npm run build pass, py_compile pass
Tests: 9/12 files pass (3 pre-existing failures)
2026-06-05 20:19:45 +00:00
Developer d80ee4157c fix: add defensive checks for missing repositories in ProjectCard
- Prevents crashes when project.repositories is undefined
- ProjectsPage tests: 1 pre-existing failure, 7 passing
2026-06-05 20:06:15 +00:00
Developer d8ab7734cb refactor: extract git repository helpers to service
- Extract git operations helpers to services/git/operations.py
- Slim git_repositories.py from 1,588 to 1,422 lines

Quality gates: py_compile passes
2026-06-05 20:00:50 +00:00
Developer de6a6a3b00 refactor: slim config_profiles router
- Extract CRUD helpers to services/config/crud_service.py
- Extract resolver/default logic to services/config/resolver_service.py
- Slim config_profiles.py from 842 to 474 lines

Quality gates: py_compile passes
2026-06-05 19:55:09 +00:00
Developer 9503f6cb4f refactor: delete monolithic styles.css
All styles have been extracted to styles/ directory and component modules.
Build verified without styles.css.

Quality gates: tsc --noEmit passes, npm run build passes
2026-06-05 19:44:48 +00:00
Developer 6b118307eb refactor: extract RepoWorkspacePage components
- Extract use-repo-workspace hook for data loading
- Extract WorkspaceLayout and FileBrowser components
- Slim RepoWorkspacePage from 505 to ~80 lines

Quality gates: tsc --noEmit passes, npm run build passes
2026-06-05 19:43:13 +00:00
Developer 070e960c05 refactor: extract ProjectsPage components
- Extract use-projects hook for state management
- Extract ProjectCard and ProjectDialog components
- Slim ProjectsPage from 433 to ~100 lines

Quality gates: tsc --noEmit passes, npm run build passes
2026-06-05 19:31:21 +00:00
Developer 96ce3f4c53 refactor: extract SshKeysPage components
- Extract use-ssh-keys hook for state management
- Extract SSHKeyCreateForm and SSHKeyList components
- Slim SshKeysPage from 277 to ~80 lines

Quality gates: tsc --noEmit passes, npm run build passes
2026-06-05 19:27:38 +00:00
Developer e49d049455 refactor: extract SettingsPage GeneralSettingsTab
- Move GeneralSettingsTab to components/features/settings/
- Re-export from page for backward compatibility
- Slim SettingsPage from 284 to ~150 lines

Quality gates: tsc --noEmit passes, npm run build passes
2026-06-05 19:20:36 +00:00
Developer e7f219f7c3 refactor: extract TerminalPage components
- Extract use-terminal-page hook for terminal state and effects
- Extract MobileTerminalView and DesktopTerminalView components
- Slim TerminalPage from 571 to 112 lines

Quality gates: tsc --noEmit passes, npm run build passes
2026-06-05 19:17:04 +00:00
Developer d7d5baa41a refactor: extract ConfigProfilesPage components
- Extract use-config-profiles hook for state management
- Extract ConfigProfileListSidebar, ConfigProfileEditorPanel, ConfigProfilesMobileView
- Slim ConfigProfilesPage from 1,611 to 170 lines

Quality gates: tsc --noEmit passes, npm run build passes
2026-06-05 19:09:09 +00:00
Developer 61072f4c07 refactor: reorganize CSS and Tool Workshop page
- Extract styles.css into styles/ directory (tokens, global, utilities, syntax-highlight, pages)
- Extract ToolWorkshopPage into components:
  - ToolTypeListSidebar, ToolTypeEditorPanel, ToolWorkshopMobileView
  - use-tool-workshop hook for state management
- Slim ToolWorkshopPage from 1,269 to 110 lines

Quality gates: tsc --noEmit passes, npm run build passes
2026-06-05 18:58:14 +00:00
Developer 7070867393 docs: update reorganize-long-files spec to include CSS reorganization
Add Phase 2 (CSS Reorganization) covering:
- Restore styles/ directory (tokens, global, utilities, syntax-highlight)
- Restore styles/pages/*.css for page-specific styles
- Restore 11 component CSS modules from monolithic styles.css
- Delete styles.css after extraction

Shift frontend page extraction phases to 3-7.
Add visual regression checks to integration phase.

Quality gates unchanged: tsc, build, py_compile, file size limits
2026-06-05 10:33:10 +00:00
Developer d472c41092 docs: add OpenSpec spec for reorganize-long-files
Create SDD proposal, spec, and tasks for splitting monolithic pages
and routers into focused components and services.

Targets:
- Frontend pages: 50-150 lines max (from 300-1600)
- Backend routers: 300-400 lines max (from 800-2900)
- Follow main branch pattern: thin pages + extracted components

Quality gates: tsc, build, py_compile, file size limits
2026-06-05 10:19:27 +00:00
Developer a9e2dd3552 fix: complete refactoring integration — rename remaining snake_case API files and fix test imports
During refactoring verification found several remaining inconsistencies:

API files (kebab-case naming):
- Rename config_profiles.ts → config-profiles.ts
- Rename tool_definitions.ts → tool-definitions.ts
- Update all imports across 8 files

Missing Python __init__.py (backend package structure):
- Add utils/__init__.py
- Add services/__init__.py
- Add schemas/__init__.py
- Add services/build/__init__.py

Test file import fixes (component reorganization fallout):
- DashboardPage.test.tsx: import from ./dashboard → ./DashboardPage
- ProjectsPage.test.tsx: import from ./projects → ./ProjectsPage
- event-toast-bridge.test.tsx: fix relative paths for moved components
  (../state/events → ../../../state/events, ./toast-rules → ../../toast-rules)
- notification-center.test.tsx: fix relative path
  (../state/notifications → ../../../state/notifications)

Quality gates: tsc --noEmit (pass), build (pass), py_compile (pass)
Tests: 9/12 test files pass (3 pre-existing UI test failures unrelated to refactoring)
2026-06-05 09:30:44 +00:00
Developer 6553a8845b fix: disable WebGL renderer to fix black-on-black text in tmux
The xterm.js WebGL addon has known rendering bugs with reverse-video
(inverse color) ANSI sequences — exactly what tmux uses for its status
bar, pane borders, and selected text. On desktop the WebGL addon loaded
successfully, causing characters to render as black-on-black and appear
to 'disappear'. On mobile WebGL typically fails to initialize, so the
terminal silently fell back to the DOM renderer which handles these
color attributes correctly.

- Remove WebGL addon loading and its cleanup logic
- Remove unused xterm-addon-webgl import and dependency
- DOM renderer is the default and correctly handles all ANSI color
  attributes including reverse video

Quality gates: tsc --noEmit (pass), build (pass), bundle -100KB
Refs: xterm.js WebGL reverse-video / minimumContrastRatio issues
2026-06-05 09:04:20 +00:00
Developer 994b1cf3b7 feat: make notification center mobile friendly
- Use useMobileViewport to detect mobile and position dropdown
  centered with left/right margins instead of right-aligned, which
  caused overflow on small screens.
- Add a semi-transparent backdrop overlay on mobile so tapping
  outside the dropdown naturally closes it.
- Update mobile CSS: notification-dropdown fills screen width
  with 0.75rem margins, max-height capped at 70vh for reachability.
- Remove the 360px max-width cap on mobile so the dropdown uses
  available screen space properly.

Quality gates: tsc --noEmit (pass), build (pass)
2026-06-05 08:57:00 +00:00
Developer 6aea83bf17 fix: render notification dropdown via portal for true always-on-top
The notification dropdown was trapped inside .shell-header's stacking
context (created by backdrop-filter). Even with z-index: 9999, it
remained below any element with a higher root-level z-index such as
modal overlays (1000), dialog overlays (1000), and fullscreen
terminals (1000).

- Render the dropdown via ReactDOM.createPortal into document.body
  so it escapes all parent stacking contexts.
- Dynamically measure the bell button's bounding rect to position
  the dropdown correctly with position: fixed.
- Update click-outside handler to also ignore clicks on the bell
  button itself.
- Add window resize listener to keep dropdown aligned.
- Change .notification-dropdown from position: absolute to fixed.

Quality gates: tsc --noEmit (pass), build (pass)
2026-06-05 08:45:36 +00:00
Developer 8d51877afa Merge branch 'fix/notification-center-zindex' into dev 2026-06-04 19:15:23 +00:00
Developer 597cfb9573 fix: notification center always on top
The notification dropdown was at z-index: 100, well below fullscreen
terminals (1000), modal overlays (1000), and dialog overlays (1000).
Because .notification-center creates a stacking context with no explicit
z-index, the dropdown was trapped behind any of those overlays and
became unclickable.

- Set .notification-center z-index to 9999 so it competes above all
  other overlays in the root stacking context.
- Set .notification-dropdown z-index to 9999 for consistency.

Quality gates: tsc --noEmit (pass), build (pass)
2026-06-04 18:47:46 +00:00
846 changed files with 33568 additions and 19313 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
{
"fingerprint": "c36b11ec5edebc02aa51b1113a7a11dc2559e812"
"fingerprint": "639c16d45210921c3c8ece071ef18bbe0c426ea2"
}
+7 -9
View File
@@ -1,14 +1,13 @@
# Skill Registry — headquarter
# Skill Registry — workspace
<!-- Auto-generated by gentle-pi extensions/skill-registry.ts. Run /skill-registry:refresh to regenerate. -->
Last updated: 2026-06-02
Last updated: 2026-06-05
## Sources scanned
- .opencode/skills
- .claude/skills
- /home/alex/.config/opencode/skills
## Contract
@@ -20,12 +19,11 @@ Last updated: 2026-06-02
| Skill | Trigger / description | Scope | Path |
| --- | --- | --- | --- |
| `auto-commit` | Use when you are making multiple edits or completing significant work in a git repository to automatically create commits | user | `/home/alex/.config/opencode/skills/auto-commit/SKILL.md` |
| `openspec-apply-change` | Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks. | project | `/home/alex/projects/headquarter/.opencode/skills/openspec-apply-change/SKILL.md` |
| `openspec-archive-change` | Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete. | project | `/home/alex/projects/headquarter/.opencode/skills/openspec-archive-change/SKILL.md` |
| `openspec-explore` | Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change. | project | `/home/alex/projects/headquarter/.opencode/skills/openspec-explore/SKILL.md` |
| `openspec-propose` | Propose a new change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation. | project | `/home/alex/projects/headquarter/.opencode/skills/openspec-propose/SKILL.md` |
| `sift-backlog` | Triage and organize backlog tasks into actionable plans. Use when asked to review the backlog, prioritize tasks, create plans from backlog items, or move tasks from backlog to open status. Handles the full workflow of listing backlog tasks, grouping related tasks into plans, setting priorities and dependencies, activating plans, and changing task status from backlog to open. | project | `/home/alex/projects/headquarter/.claude/skills/sift-backlog/SKILL.md` |
| `openspec-apply-change` | Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks. | project | `/workspace/.opencode/skills/openspec-apply-change/SKILL.md` |
| `openspec-archive-change` | Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete. | project | `/workspace/.opencode/skills/openspec-archive-change/SKILL.md` |
| `openspec-explore` | Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change. | project | `/workspace/.opencode/skills/openspec-explore/SKILL.md` |
| `openspec-propose` | Propose a new change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation. | project | `/workspace/.opencode/skills/openspec-propose/SKILL.md` |
| `sift-backlog` | Triage and organize backlog tasks into actionable plans. Use when asked to review the backlog, prioritize tasks, create plans from backlog items, or move tasks from backlog to open status. Handles the full workflow of listing backlog tasks, grouping related tasks into plans, setting priorities and dependencies, activating plans, and changing task status from backlog to open. | project | `/workspace/.claude/skills/sift-backlog/SKILL.md` |
## Loading protocol
+20
View File
@@ -0,0 +1,20 @@
# .claude (index)
dir: .claude
## role
Configuration and instruction files for the Claude AI assistant integration.
## parent
index: ./.pi-map.index.md
map: ./.pi-map.md
## children
- .claude/skills
index: .claude/skills/.pi-map.index.md
map: .claude/skills/.pi-map.md
## files
## links
index: .claude/.pi-map.index.md
map: .claude/.pi-map.md
## workflows
-
## dirty
-
+18
View File
@@ -0,0 +1,18 @@
# .claude
dir: .claude
index: .claude/.pi-map.index.md
## role
Configuration and instruction files for the Claude AI assistant integration.
## files
## arch
Simple flat-file configuration storage with no complex architecture; contains prompt templates, system instructions, or context files used to guide Claude's behavior in this project.
## tags
-
## symbols
-
## workflows
-
## dirty
-
+20
View File
@@ -0,0 +1,20 @@
# .claude/skills (index)
dir: .claude/skills
## role
Empty directory placeholder for future Claude Code skill definitions or custom AI-assisted workflows.
## parent
index: .claude/.pi-map.index.md
map: .claude/.pi-map.md
## children
- .claude/skills/sift-backlog
index: .claude/skills/sift-backlog/.pi-map.index.md
map: .claude/skills/sift-backlog/.pi-map.md
## files
## links
index: .claude/skills/.pi-map.index.md
map: .claude/skills/.pi-map.md
## workflows
-
## dirty
-
+18
View File
@@ -0,0 +1,18 @@
# .claude/skills
dir: .claude/skills
index: .claude/skills/.pi-map.index.md
## role
Empty directory placeholder for future Claude Code skill definitions or custom AI-assisted workflows.
## files
## arch
None (no files present); likely intended for declarative skill modules using Claude's skill system pattern.
## tags
-
## symbols
-
## workflows
-
## dirty
-
@@ -0,0 +1,19 @@
# .claude/skills/sift-backlog (index)
dir: .claude/skills/sift-backlog
## role
Defines a workflow skill for triaging, organizing, and activating backlog tasks into actionable plans using a custom CLI tool.
## parent
index: .claude/skills/.pi-map.index.md
map: .claude/skills/.pi-map.md
## children
-
## files
- SKILL.md
## links
index: .claude/skills/sift-backlog/.pi-map.index.md
map: .claude/skills/sift-backlog/.pi-map.md
## workflows
-
## dirty
-
+19
View File
@@ -0,0 +1,19 @@
# .claude/skills/sift-backlog
dir: .claude/skills/sift-backlog
index: .claude/skills/sift-backlog/.pi-map.index.md
## role
Defines a workflow skill for triaging, organizing, and activating backlog tasks into actionable plans using a custom CLI tool.
## files
- SKILL.md | Defines a workflow skill for triaging, organizing, and activating backlog tasks into actionable plans using a custom CLI tool. | dep: sf (custom CLI tool), task management system, plan management system
## arch
Skill-based workflow automation following a structured triage pipeline (scan → filter → prioritize → activate) with CLI tool integration for backlog management.
## tags
skill, defines, workflow, triaging, organizing, activating, backlog, tasks
## symbols
-
## workflows
-
## dirty
-
+4
View File
@@ -17,6 +17,7 @@ __pycache__/
*.so
.python-version
.venv/
.venv-test/
venv/
env/
.pytest_cache/
@@ -54,3 +55,6 @@ Thumbs.db
.atl/
.sisyphus/
.pi-lens/
minerv3/
.cache/
openspec-audit-report.md
+23
View File
@@ -0,0 +1,23 @@
# .opencode (index)
dir: .opencode
## role
Configuration directory for the OpenCode AI coding assistant tool
## parent
index: ./.pi-map.index.md
map: ./.pi-map.md
## children
- .opencode/commands
index: .opencode/commands/.pi-map.index.md
map: .opencode/commands/.pi-map.md
- .opencode/skills
index: .opencode/skills/.pi-map.index.md
map: .opencode/skills/.pi-map.md
## files
## links
index: .opencode/.pi-map.index.md
map: .opencode/.pi-map.md
## workflows
-
## dirty
-
+18
View File
@@ -0,0 +1,18 @@
# .opencode
dir: .opencode
index: .opencode/.pi-map.index.md
## role
Configuration directory for the OpenCode AI coding assistant tool
## files
## arch
Tool-specific configuration storage using standard dot-directory convention with likely JSON/YAML config files and optional workspace state/logs
## tags
-
## symbols
-
## workflows
-
## dirty
-
+22
View File
@@ -0,0 +1,22 @@
# .opencode/commands (index)
dir: .opencode/commands
## role
Defines structured workflow commands and AI assistant stances for an experimental OpenSpec-based development system that guides changes through exploration, proposal, implementation, and archival phases.
## parent
index: .opencode/.pi-map.index.md
map: .opencode/.pi-map.md
## children
-
## files
- opsx-apply.md
- opsx-archive.md
- opsx-explore.md
- opsx-propose.md
## links
index: .opencode/commands/.pi-map.index.md
map: .opencode/commands/.pi-map.md
## workflows
-
## dirty
-
+22
View File
@@ -0,0 +1,22 @@
# .opencode/commands
dir: .opencode/commands
index: .opencode/commands/.pi-map.index.md
## role
Defines structured workflow commands and AI assistant stances for an experimental OpenSpec-based development system that guides changes through exploration, proposal, implementation, and archival phases.
## files
- opsx-apply.md | Defines an experimental workflow skill for implementing tasks from an OpenSpec change through a structured, interactive process with CLI integration and progress tracking. | dep: openspec CLI, AskUserQuestion tool, filesystem (for reading context files)
- opsx-archive.md | Defines a workflow for archiving completed changes in an experimental openspec-based development system | dep: openspec CLI, AskUserQuestion tool, Task tool, Skill tool, filesystem (mkdir, mv), JSON parsing
- opsx-explore.md | Defines the "explore mode" stance for an AI assistant - a thinking/discovery mode for investigating problems and clarifying requirements without implementing code | dep: OpenSpec system
- opsx-propose.md | Defines a workflow for proposing new changes in the openspec system by creating a change directory and generating all required artifacts (proposal.md, design.md, tasks.md) in dependency order | dep: openspec CLI, AskUserQuestion tool, TodoWrite tool, JSON parsing
## arch
Markdown-based declarative workflow definitions using a command pattern where each `.md` file specifies a distinct operational stance or phase (explore/propose/apply/archive) with embedded metadata, structured steps, and CLI integration points for progressive change lifecycle management.
## tags
opsx, defines, workflow, openspec, openspec cli, askuserquestion tool, explore, experimental
## symbols
-
## workflows
-
## dirty
-
+29
View File
@@ -0,0 +1,29 @@
# .opencode/skills (index)
dir: .opencode/skills
## role
Contains reusable skill modules that provide specific capabilities or integrations for the OpenCode agent framework.
## parent
index: .opencode/.pi-map.index.md
map: .opencode/.pi-map.md
## children
- .opencode/skills/openspec-apply-change
index: .opencode/skills/openspec-apply-change/.pi-map.index.md
map: .opencode/skills/openspec-apply-change/.pi-map.md
- .opencode/skills/openspec-archive-change
index: .opencode/skills/openspec-archive-change/.pi-map.index.md
map: .opencode/skills/openspec-archive-change/.pi-map.md
- .opencode/skills/openspec-explore
index: .opencode/skills/openspec-explore/.pi-map.index.md
map: .opencode/skills/openspec-explore/.pi-map.md
- .opencode/skills/openspec-propose
index: .opencode/skills/openspec-propose/.pi-map.index.md
map: .opencode/skills/openspec-propose/.pi-map.md
## files
## links
index: .opencode/skills/.pi-map.index.md
map: .opencode/skills/.pi-map.md
## workflows
-
## dirty
-
+18
View File
@@ -0,0 +1,18 @@
# .opencode/skills
dir: .opencode/skills
index: .opencode/skills/.pi-map.index.md
## role
Contains reusable skill modules that provide specific capabilities or integrations for the OpenCode agent framework.
## files
## arch
Modular plugin-based architecture where each skill is a self-contained unit implementing standardized interfaces for dynamic discovery and loading by the agent core.
## tags
-
## symbols
-
## workflows
-
## dirty
-
@@ -0,0 +1,19 @@
# .opencode/skills/openspec-apply-change (index)
dir: .opencode/skills/openspec-apply-change
## role
Provides a spec-driven AI skill that implements tasks from OpenSpec changes by applying structured modifications to a codebase.
## parent
index: .opencode/skills/.pi-map.index.md
map: .opencode/skills/.pi-map.md
## children
-
## files
- SKILL.md
## links
index: .opencode/skills/openspec-apply-change/.pi-map.index.md
map: .opencode/skills/openspec-apply-change/.pi-map.md
## workflows
-
## dirty
-
@@ -0,0 +1,19 @@
# .opencode/skills/openspec-apply-change
dir: .opencode/skills/openspec-apply-change
index: .opencode/skills/openspec-apply-change/.pi-map.index.md
## role
Provides a spec-driven AI skill that implements tasks from OpenSpec changes by applying structured modifications to a codebase.
## files
- SKILL.md | Defines an AI assistant skill for implementing tasks from an OpenSpec change using a spec-driven workflow | dep: openspec CLI, AskUserQuestion tool, filesystem access
## arch
Skill-based modular architecture using markdown-defined capabilities with a workflow-driven pattern that separates specification parsing from implementation execution.
## tags
skill, defines, assistant, implementing, tasks, openspec, change, spec
## symbols
-
## workflows
-
## dirty
-
@@ -0,0 +1,19 @@
# .opencode/skills/openspec-archive-change (index)
dir: .opencode/skills/openspec-archive-change
## role
Provides a skill definition for archiving completed changes in an experimental workflow using the openspec CLI.
## parent
index: .opencode/skills/.pi-map.index.md
map: .opencode/skills/.pi-map.md
## children
-
## files
- SKILL.md
## links
index: .opencode/skills/openspec-archive-change/.pi-map.index.md
map: .opencode/skills/openspec-archive-change/.pi-map.md
## workflows
-
## dirty
-
@@ -0,0 +1,19 @@
# .opencode/skills/openspec-archive-change
dir: .opencode/skills/openspec-archive-change
index: .opencode/skills/openspec-archive-change/.pi-map.index.md
## role
Provides a skill definition for archiving completed changes in an experimental workflow using the openspec CLI.
## files
- SKILL.md | Defines a skill for archiving completed changes in an experimental workflow using the openspec CLI. | dep: openspec CLI, AskUserQuestion tool, Task tool, file system (mkdir, mv, read), JSON parsing
## arch
Single-file skill definition using markdown-based skill documentation pattern with CLI tool integration.
## tags
skill, defines, archiving, completed, changes, experimental, workflow, openspec
## symbols
-
## workflows
-
## dirty
-
@@ -0,0 +1,19 @@
# .opencode/skills/openspec-explore (index)
dir: .opencode/skills/openspec-explore
## role
Defines a conversational AI skill that acts as a thinking partner for exploring ideas, investigating problems, and clarifying requirements without implementing code.
## parent
index: .opencode/skills/.pi-map.index.md
map: .opencode/skills/.pi-map.md
## children
-
## files
- SKILL.md
## links
index: .opencode/skills/openspec-explore/.pi-map.index.md
map: .opencode/skills/openspec-explore/.pi-map.md
## workflows
-
## dirty
-
@@ -0,0 +1,19 @@
# .opencode/skills/openspec-explore
dir: .opencode/skills/openspec-explore
index: .opencode/skills/openspec-explore/.pi-map.index.md
## role
Defines a conversational AI skill that acts as a thinking partner for exploring ideas, investigating problems, and clarifying requirements without implementing code.
## files
- SKILL.md | Defines a conversational AI skill/persona for "explore mode" - a thinking partner for exploring ideas, investigating problems, and clarifying requirements without implementing code. | dep: openspec CLI
## arch
Markdown-based skill definition using persona/role-prompting pattern with structured behavioral guidelines for non-implementation exploratory conversations.
## tags
skill, defines, conversational, persona, explore, mode, thinking, partner
## symbols
-
## workflows
-
## dirty
-
@@ -0,0 +1,19 @@
# .opencode/skills/openspec-propose (index)
dir: .opencode/skills/openspec-propose
## role
Defines a structured workflow skill for proposing new changes using the openspec CLI, generating proposal, design, and task artifacts in dependency order.
## parent
index: .opencode/skills/.pi-map.index.md
map: .opencode/skills/.pi-map.md
## children
-
## files
- SKILL.md
## links
index: .opencode/skills/openspec-propose/.pi-map.index.md
map: .opencode/skills/openspec-propose/.pi-map.md
## workflows
-
## dirty
-
@@ -0,0 +1,19 @@
# .opencode/skills/openspec-propose
dir: .opencode/skills/openspec-propose
index: .opencode/skills/openspec-propose/.pi-map.index.md
## role
Defines a structured workflow skill for proposing new changes using the openspec CLI, generating proposal, design, and task artifacts in dependency order.
## files
- SKILL.md | Defines a structured workflow for proposing new changes using the openspec CLI, generating proposal, design, and task artifacts in dependency order. | dep: openspec CLI, AskUserQuestion tool, TodoWrite tool
## arch
Template-based skill definition using markdown documentation with structured workflow patterns for CLI-driven artifact generation and dependency-ordered execution.
## tags
skill, defines, structured, workflow, proposing, new, changes, openspec
## symbols
-
## workflows
-
## dirty
-
+79
View File
@@ -0,0 +1,79 @@
# . (index)
dir: .
## Project Map Protocol
1. Read this protocol and the root `.pi-map.index.md` first.
2. Use `index:` / `map:` references to open relevant directory indexes and maps.
3. Load indexes before rich maps during task-start navigation.
4. Read the local rich map and actual source before editing.
5. Treat non-empty `## dirty` sections in either artifact as stale.
6. If source and generated artifacts disagree, trust source.
7. If map and index disagree, trust neither blindly; verify from source and regenerate the pair.
8. After editing source, run `project_map_patch` for each changed file.
9. Before broad architectural claims or final handoff, run `project_map_validate` when freshness matters.
Trust boundary: index routes, map orients, source decides.
## role
Infrastructure and deployment configuration for a self-hosted project management platform with OAuth2 authentication, providing Docker Compose orchestration, environment templates, and development tooling.
## parent
-
## children
- .atl
index: .atl/.pi-map.index.md
map: .atl/.pi-map.md
- .claude
index: .claude/.pi-map.index.md
map: .claude/.pi-map.md
- .opencode
index: .opencode/.pi-map.index.md
map: .opencode/.pi-map.md
- .pi
index: .pi/.pi-map.index.md
map: .pi/.pi-map.md
- .sisyphus
index: .sisyphus/.pi-map.index.md
map: .sisyphus/.pi-map.md
- .stoneforge
index: .stoneforge/.pi-map.index.md
map: .stoneforge/.pi-map.md
- apps
index: apps/.pi-map.index.md
map: apps/.pi-map.md
- docs
index: docs/.pi-map.index.md
map: docs/.pi-map.md
- e2e
index: e2e/.pi-map.index.md
map: e2e/.pi-map.md
- minerv3
index: minerv3/.pi-map.index.md
map: minerv3/.pi-map.md
- openspec
index: openspec/.pi-map.index.md
map: openspec/.pi-map.md
- scripts
index: scripts/.pi-map.index.md
map: scripts/.pi-map.md
- tool-images
index: tool-images/.pi-map.index.md
map: tool-images/.pi-map.md
## files
- .env.example
- .gitignore
- AGENTS.md
- CHANGELOG.md
- Makefile
- README.md
- docker-compose.traefik.yml
- docker-compose.yml
- progress.md
- swap-pane
## links
index: ./.pi-map.index.md
map: ./.pi-map.md
## workflows
-
## dirty
-
+42
View File
@@ -0,0 +1,42 @@
# .
dir: .
index: ./.pi-map.index.md
## Project Map Protocol
1. Read this protocol and the root `.pi-map.index.md` first.
2. Use `index:` / `map:` references to open relevant directory indexes and maps.
3. Load indexes before rich maps during task-start navigation.
4. Read the local rich map and actual source before editing.
5. Treat non-empty `## dirty` sections in either artifact as stale.
6. If source and generated artifacts disagree, trust source.
7. If map and index disagree, trust neither blindly; verify from source and regenerate the pair.
8. After editing source, run `project_map_patch` for each changed file.
9. Before broad architectural claims or final handoff, run `project_map_validate` when freshness matters.
Trust boundary: index routes, map orients, source decides.
## role
Infrastructure and deployment configuration for a self-hosted project management platform with OAuth2 authentication, providing Docker Compose orchestration, environment templates, and development tooling.
## files
- .env.example | Provides a template of environment variables for configuring a Headquarter application with PostgreSQL, Redis, Authentik SSO, and Docker/Traefik deployment
- .gitignore | Specifies files and directories for Git to ignore across a multi-language project with Python, Node, and custom tooling | dep: Git
- AGENTS.md | Defines operational rules, workflows, and constraints for AI agents working within an OpenSpec-driven software development project. | dep: OpenSpec, superpowers, git, docker compose, conventional commits
- CHANGELOG.md | Documents version history and notable changes for a Git-based project management web application
- Makefile | Provides standard development commands for containerized web application lifecycle management via Docker Compose | dep: docker compose, alembic, pytest, ruff, mypy, playwright, npm, postgres, redis
- README.md | A self-hosted platform for managing projects, git repositories, and development tools with OAuth2 authentication. | dep: FastAPI, SQLAlchemy, Pydantic, Alembic, python-jose, React, TypeScript, Vite, React Router, Docker, PostgreSQL, Traefik, Authentik, Git
- docker-compose.traefik.yml | Deploys a multi-service web application (frontend, API, PostgreSQL, Redis) behind an existing Traefik reverse proxy with TLS termination and environment-configurable domains. | dep: docker, traefik, postgres, redis, authentik, docker-compose
- docker-compose.yml | Defines a multi-service Docker Compose stack with PostgreSQL, Redis, API backend, and web frontend services for a "headquarter" application | dep: Docker, PostgreSQL, Redis, Vite, asyncpg, nginx
- progress.md | Tracks completed and remaining tasks for a backend-frontend code refactoring project organized in 7 phases
- swap-pane | Empty file with no functionality
## arch
Containerized microservices architecture using Docker Compose with PostgreSQL/Redis data layer, Traefik reverse proxy for TLS termination, environment-based configuration pattern, and Makefile-driven development workflow.
## tags
docker, redis, git, application, postgresql, compose, traefik, project
## symbols
-
## workflows
-
## dirty
-
+23
View File
@@ -0,0 +1,23 @@
# .stoneforge (index)
dir: .stoneforge
## role
Hidden system directory storing runtime state, configuration, and version-control metadata for the Stoneforge application.
## parent
index: ./.pi-map.index.md
map: ./.pi-map.md
## children
- .stoneforge/sync
index: .stoneforge/sync/.pi-map.index.md
map: .stoneforge/sync/.pi-map.md
## files
- .dashboard-opened
- .gitignore
- config.yaml
## links
index: .stoneforge/.pi-map.index.md
map: .stoneforge/.pi-map.md
## workflows
-
## dirty
-
+21
View File
@@ -0,0 +1,21 @@
# .stoneforge
dir: .stoneforge
index: .stoneforge/.pi-map.index.md
## role
Hidden system directory storing runtime state, configuration, and version-control metadata for the Stoneforge application.
## files
- .dashboard-opened | Stores timestamp and identifier data for tracking when a dashboard was opened
- .gitignore | Specifies files and patterns for Git to ignore in version control
- config.yaml | Configuration file for the Stoneforge application defining database, sync, playbook, identity, merge, workflow, and agent settings.
## arch
Simple flat-file storage with YAML-based configuration, dotfile-based state tracking, and standard Git ignore patterns.
## tags
config, stores, timestamp, identifier, data, tracking, dashboard, was
## symbols
-
## workflows
-
## dirty
-
+20
View File
@@ -0,0 +1,20 @@
# .stoneforge/sync (index)
dir: .stoneforge/sync
## role
Distributed task coordination and ephemeral worker agent management for a multi-agent execution system.
## parent
index: .stoneforge/.pi-map.index.md
map: .stoneforge/.pi-map.md
## children
-
## files
- dependencies.jsonl
- elements.jsonl
## links
index: .stoneforge/sync/.pi-map.index.md
map: .stoneforge/sync/.pi-map.md
## workflows
-
## dirty
-
+20
View File
@@ -0,0 +1,20 @@
# .stoneforge/sync
dir: .stoneforge/sync
index: .stoneforge/sync/.pi-map.index.md
## role
Distributed task coordination and ephemeral worker agent management for a multi-agent execution system.
## files
- dependencies.jsonl | Stores a sequence of dependency relationships between entities in JSON Lines format, tracking parent-child, blocking, and reply relationships with timestamps and creators.
- elements.jsonl | Stores JSONL records of ephemeral worker agents with their session history, worktree assignments, and lifecycle metadata for a distributed task execution system.
## arch
Event-sourced JSONL append-only logs with entity-relationship graph tracking, session-based worker lifecycle management, and decentralized worktree assignment.
## tags
stores, relationships, dependencies, elements, sequence, dependency, entities, json
## symbols
-
## workflows
-
## dirty
-
+23
View File
@@ -0,0 +1,23 @@
# apps (index)
dir: apps
## role
Contains the main application entry points and executable modules for the project.
## parent
index: ./.pi-map.index.md
map: ./.pi-map.md
## children
- apps/api
index: apps/api/.pi-map.index.md
map: apps/api/.pi-map.md
- apps/web
index: apps/web/.pi-map.index.md
map: apps/web/.pi-map.md
## files
## links
index: apps/.pi-map.index.md
map: apps/.pi-map.md
## workflows
-
## dirty
-
+18
View File
@@ -0,0 +1,18 @@
# apps
dir: apps
index: apps/.pi-map.index.md
## role
Contains the main application entry points and executable modules for the project.
## files
## arch
Follows a modular application structure with separate directories for each deployable application, typically using framework-specific patterns (e.g., MVC, MVVM, or component-based architecture depending on the stack).
## tags
-
## symbols
-
## workflows
-
## dirty
-
+45
View File
@@ -0,0 +1,45 @@
# apps/api (index)
dir: apps/api
## role
Backend API service that provides a self-hosted FastAPI server for managing development projects, git repositories, and containerized tools via Docker.
## parent
index: apps/.pi-map.index.md
map: apps/.pi-map.md
## children
- apps/api/.pi-lens
index: apps/api/.pi-lens/.pi-map.index.md
map: apps/api/.pi-lens/.pi-map.md
- apps/api/.pytest_cache
index: apps/api/.pytest_cache/.pi-map.index.md
map: apps/api/.pytest_cache/.pi-map.md
- apps/api/.venv-test
index: apps/api/.venv-test/.pi-map.index.md
map: apps/api/.venv-test/.pi-map.md
- apps/api/alembic
index: apps/api/alembic/.pi-map.index.md
map: apps/api/alembic/.pi-map.md
- apps/api/src
index: apps/api/src/.pi-map.index.md
map: apps/api/src/.pi-map.md
- apps/api/tests
index: apps/api/tests/.pi-map.index.md
map: apps/api/tests/.pi-map.md
- apps/api/uploads
index: apps/api/uploads/.pi-map.index.md
map: apps/api/uploads/.pi-map.md
## files
- .dockerignore
- Dockerfile
- README.md
- alembic.ini
- pyproject.toml
- uv.lock
- wait-for-db.sh
## links
index: apps/api/.pi-map.index.md
map: apps/api/.pi-map.md
## workflows
-
## dirty
-
+25
View File
@@ -0,0 +1,25 @@
# apps/api
dir: apps/api
index: apps/api/.pi-map.index.md
## role
Backend API service that provides a self-hosted FastAPI server for managing development projects, git repositories, and containerized tools via Docker.
## files
- .dockerignore | Specifies files and directories to exclude from Docker build context to reduce image size and avoid copying unnecessary files into containers. | dep: Docker
- Dockerfile | Multi-stage Docker build for a Python application with Docker socket access, Cloudflare tunneling, and database dependency waiting | dep: python:3.11-slim, gcc, libpq-dev, docker-ce-cli, docker-compose-plugin, cloudflared, uvicorn, pyproject.toml dependencies
- README.md | Documentation for a self-hosted FastAPI backend API that manages projects, git repositories, and development tools via Docker instances. | dep: FastAPI, SQLAlchemy, PostgreSQL, asyncpg, Alembic, Docker, Docker Compose, Authentik, uvicorn, pytest, ruff, mypy
- alembic.ini | Configuration file for Alembic database migration tool connecting to a PostgreSQL database with async driver | dep: alembic, sqlalchemy, asyncpg, PostgreSQL
- pyproject.toml | Defines Python project metadata, dependencies, and tool configurations for a FastAPI-based backend API called "headquarter-api" | dep: fastapi, uvicorn, sqlalchemy, asyncpg, alembic, pydantic, pydantic-settings, python-multipart, httpx, structlog, cryptography, pytest, pytest-asyncio, mypy, ruff, aiosqlite
- uv.lock | Lock file for the uv Python package manager that pins exact dependency versions and their artifact hashes for reproducible installations | dep: uv, Python 3.11+, aiosqlite, alembic, annotated-doc, annotated-types, anyio, ast-serialize, asyncpg, and many other PyPI packages
- wait-for-db.sh | Wait for a PostgreSQL database to become available before executing a command, with configurable retry logic. | dep: nc (netcat), sh (POSIX shell), sleep
## arch
Modern Python async architecture using FastAPI with SQLAlchemy/alembic for PostgreSQL, uv for dependency management, multi-stage Docker builds with Cloudflare tunneling, and health-check orchestration for database readiness.
## tags
docker, alembic, python, database, fastapi, postgresql, asyncpg, uvicorn
## symbols
-
## workflows
-
## dirty
-
+25
View File
@@ -0,0 +1,25 @@
# apps/api/alembic (index)
dir: apps/api/alembic
## role
Database migration tooling that manages schema versioning and changes for the API's PostgreSQL database using Alembic with async SQLAlchemy.
## parent
index: apps/api/.pi-map.index.md
map: apps/api/.pi-map.md
## children
- apps/api/alembic/versions
index: apps/api/alembic/versions/.pi-map.index.md
map: apps/api/alembic/versions/.pi-map.md
## files
- env.py
- script.py.mako
## links
index: apps/api/alembic/.pi-map.index.md
map: apps/api/alembic/.pi-map.md
## workflows
- change alembic behavior
read: env.py, script.py.mako
- explore alembic subdirectories
index: apps/api/alembic/versions/.pi-map.index.md
## dirty
-
+23
View File
@@ -0,0 +1,23 @@
# apps/api/alembic
dir: apps/api/alembic
index: apps/api/alembic/.pi-map.index.md
## role
Database migration tooling that manages schema versioning and changes for the API's PostgreSQL database using Alembic with async SQLAlchemy.
## files
- env.py | Configures Alembic database migration environment with async SQLAlchemy support for a project. | dep: alembic, sqlalchemy, sqlalchemy.ext.asyncio, logging.config, asyncio, src.config, src.models
- script.py.mako | Alembic database migration script template that generates upgrade/downgrade functions for SQLAlchemy schema migrations | dep: alembic, sqlalchemy
## arch
Template-driven migration pattern using Alembic's declarative configuration with async engine setup, environment context management, and auto-generated revision scripts with upgrade/downgrade function pairs.
## tags
sqlalchemy, alembic, database, migration, asyncio, config, src, env
## symbols
-
## workflows
- change alembic behavior
read: env.py, script.py.mako
- explore alembic subdirectories
index: apps/api/alembic/versions/.pi-map.index.md
## dirty
-
@@ -0,0 +1,67 @@
# apps/api/alembic/versions (index)
dir: apps/api/alembic/versions
## role
Database schema evolution management for the API application, tracking incremental changes to tables supporting users, authentication, projects, git repositories, tool types/instances, config profiles, workspaces, monitoring, and notifications.
## parent
index: apps/api/alembic/.pi-map.index.md
map: apps/api/alembic/.pi-map.md
## children
-
## files
- 0001_initial_schema.py
- 0002_refresh_tokens.py
- 0003_user_configs.py
- 0004_tool_types.py
- 0005_ssh_keys_timestamps.py
- 0006_tool_instances.py
- 0007_instance_container_name.py
- 0008_tool_type_category.py
- 0009_tool_configs.py
- 0010_tool_type_default_port.py
- 0011_tool_instance_tunnel_fields.py
- 0012_default_port_req.py
- 0013_add_config_profiles.py
- 0013_add_probe_result.py
- 0014_add_profile_resolver_fields.py
- 0014_merge_heads.py
- 0015_single_interface.py
- 069d3da4dc9b_add_ssh_key_id_to_config_profiles.py
- 20260527160017_add_pi_agent_tool_type.py
- 2026_05_22_add_clone_mode.py
- 2026_05_23_remove_is_builtin.py
- 2026_05_24_220141_add_startup_command.py
- 2026_05_24_add_config_profiles.py
- 2026_05_26_add_git_mounts.py
- 2026_05_27_external_repos.py
- 2026_05_28_add_monitoring_tables.py
- 2026_05_28_add_terminal_sessions_table.py
- 2026_05_28_add_tool_definition_manifests.py
- 2026_05_28_drop_tool_configs_and_config_folders.py
- 2026_05_29_add_notifications_table.py
- 2026_05_29_add_ssh_key_ids_to_tool_instances.py
- 2026_05_29_drop_ssh_key_id_from_config_profiles.py
- 2026_05_29_fix_code_server_bind_addr.py
- 2026_05_29_fix_code_server_bind_addr_port.py
- 2026_05_29_fix_web_tool_bind_address.py
- 2026_05_29_remove_lsio_command_override.py
- 2026_05_29_remove_ssh_keys_mount_from_manifest.py
- 2026_06_01_add_workspaces.py
- 398082499c30_add_tool_config_fields.py
- 6fc7bfcf199f_merge_remove_is_builtin_and_add_config_.py
- 86cec91fdb00_merge_profile_resolver_and_workspaces_.py
- 8ed7dd80973d_create_config_folders_table.py
- af8512103d67_add_tool_type_fields.py
- f3d2dc90ba3a_merge_single_interface_and_clone_mode.py
## links
index: apps/api/alembic/versions/.pi-map.index.md
map: apps/api/alembic/versions/.pi-map.md
## workflows
- change versions behavior
read: 0001_initial_schema.py, 0002_refresh_tokens.py, 0003_user_configs.py
- change versions CLI
read: 2026_05_24_220141_add_startup_command.py, 2026_05_29_remove_lsio_command_override.py
- change versions config
read: 0003_user_configs.py, 0009_tool_configs.py, 0013_add_config_profiles.py
## dirty
-
+74
View File
@@ -0,0 +1,74 @@
# apps/api/alembic/versions
dir: apps/api/alembic/versions
index: apps/api/alembic/versions/.pi-map.index.md
## role
Database schema evolution management for the API application, tracking incremental changes to tables supporting users, authentication, projects, git repositories, tool types/instances, config profiles, workspaces, monitoring, and notifications.
## files
- 0001_initial_schema.py | Defines the initial database schema migration creating five tables (users, ssh_keys, projects, git_repositories, user_configs) with relationships, indexes, and constraints using Alembic. | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.String, call:postgresql.UUID, call:sa.DateTime, call:sa.func.now, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:op.create_index, call:op.f, call:sa.Text, call:sa.ForeignKeyConstraint, call:sa.Boolean, call:postgresql.JSONB, func:downgrade() → None, call:op.drop_table, call:op.drop_index, call:op.f | dep: alembic, sqlalchemy.dialects, sqlalchemy, postgresql dialect
- 0002_refresh_tokens.py | Alembic database migration that creates a refresh_tokens table with indexes for user authentication token management | exp: func:upgrade() → None, call:op.get_bind, call:sa.inspect, call:inspector.has_table, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.String, call:sa.DateTime, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:inspector.get_indexes, call:op.f, call:op.create_index, func:downgrade() → None, call:op.get_bind, call:sa.inspect, call:inspector.has_table, call:inspector.get_indexes, call:op.f, call:op.drop_index, call:op.drop_table | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
- 0003_user_configs.py | Alembic database migration that creates a user_configs table with JSON configuration storage linked to users | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.UUID, call:sa.JSON, call:sa.DateTime, call:sa.text, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, func:downgrade() → None, call:op.drop_table | dep: typing, alembic, sqlalchemy
- 0004_tool_types.py | Alembic database migration that creates a tool_types table with metadata, templates, and versioning columns for a tool management system. | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.Uuid, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.Boolean, call:sa.ForeignKey, call:sa.DateTime, call:sa.text, func:downgrade() → None, call:op.drop_table | dep: typing, alembic, sqlalchemy
- 0005_ssh_keys_timestamps.py | Alembic database migration that adds created_at and updated_at timestamp columns to the ssh_keys table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.DateTime, call:sa.text, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
- 0006_tool_instances.py | Alembic database migration that creates a tool_instances table with columns for tracking deployed tool instances, their status, container info, and foreign key relationships to tool_types, git_repositories, projects, and users. | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.text, call:sa.String, call:sa.Integer, call:sa.DateTime, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: typing, alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
- 0007_instance_container_name.py | Alembic database migration that adds a nullable container_name column to the tool_instances table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.String, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
- 0008_tool_type_category.py | Alembic database migration that adds `category` and `interfaces` columns to the `tool_types` table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.String, call:sa.JSON, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
- 0009_tool_configs.py | Alembic database migration that creates a tool_configs table with UUID keys, foreign key relationships, and indexes for storing user/project tool configuration settings. | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.text, call:sa.String, call:sa.Text, call:sa.DateTime, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: typing, alembic, sqlalchemy.dialects, sqlalchemy, postgresql dialect
- 0010_tool_type_default_port.py | Alembic database migration that adds a nullable default_port column to the tool_types table. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Integer, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
- 0011_tool_instance_tunnel_fields.py | Alembic database migration that adds tunnel-related fields (public_url and tunnel_id) to the tool_instances table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.String, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
- 0012_default_port_req.py | Alembic database migration that populates null default_port values for existing tool types and then makes the column non-nullable | exp: func:upgrade() → None, call:op.execute, call:op.alter_column, call:sa.Integer, func:downgrade() → None, call:op.alter_column, call:sa.Integer | dep: typing, alembic, sqlalchemy
- 0013_add_config_profiles.py | Alembic database migration that adds config profiles, includes, mounts tables and links tool instances to profiles with defensive idempotent checks | exp: func:_table_exists(table_name: str) → bool, call:sa.inspect(op.get_bind()).has_table, call:op.get_bind, func:_column_exists(table_name: str, column_name: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_columns, call:op.get_bind, func:_index_exists(table_name: str, index_name: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_indexes, call:op.get_bind, func:_foreign_key_exists(table_name: str, constrained_columns: list[str], referred_table: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_foreign_keys, call:op.get_bind, call:foreign_key.get, func:upgrade() → None, call:_table_exists, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.String, call:sa.Text, call:sa.DateTime, call:sa.text, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:_index_exists, call:op.create_index, call:sa.Integer, call:_column_exists, call:op.add_column, call:_foreign_key_exists, call:op.create_foreign_key, func:downgrade() → None, call:op.drop_index, call:op.drop_constraint, call:op.drop_column, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
- 0013_add_probe_result.py | Alembic database migration that adds a JSON probe_result column to the tool_instances table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
- 0014_add_profile_resolver_fields.py | Alembic database migration that adds profile resolver fields (project_id, tool_type_id, environment_variables, etc.) to config_profiles table and restructures config_mounts table (renaming mount_path to target_path, adding mode/files, removing content/source_profile_id). | exp: func:_table_exists(table_name: str) → bool, call:sa.inspect(op.get_bind()).has_table, call:op.get_bind, func:_column_exists(table_name: str, column_name: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_columns, call:op.get_bind, func:_index_exists(table_name: str, index_name: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_indexes, call:op.get_bind, func:_foreign_key_exists(table_name: str, constrained_columns: list[str], referred_table: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_foreign_keys, call:op.get_bind, call:foreign_key.get, func:_foreign_key_names_for_column(table_name: str, column_name: str) → list[str], call:_table_exists, call:sa.inspect(op.get_bind()).get_foreign_keys, call:op.get_bind, call:foreign_key.get, call:names.append, func:upgrade() → None, call:_column_exists, call:op.add_column, call:sa.Column, call:postgresql.UUID, call:sa.JSON, call:sa.Text, call:sa.Integer, call:sa.Boolean, call:_foreign_key_exists, call:op.create_foreign_key, call:_index_exists, call:op.create_index, call:op.alter_column, call:sa.String, call:_foreign_key_names_for_column, call:op.drop_constraint, call:op.drop_column, func:downgrade() → None, call:op.add_column, call:sa.Column, call:postgresql.UUID, call:sa.Text, call:op.drop_column, call:op.alter_column, call:op.drop_index, call:op.drop_constraint | dep: collections.abc, alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
- 0014_merge_heads.py | Alembic merge migration that reconciles two divergent migration branches into a single history line | exp: func:upgrade() → None, func:downgrade() → None | dep: typing, alembic
- 0015_single_interface.py | Alembic database migration that replaces a JSON array `interfaces` column with `interface_type` string and `requires_port` boolean columns in the `tool_types` table, with dialect-specific data migration for PostgreSQL and SQLite. | exp: func:_get_dialect() → str, call:op.get_bind, func:upgrade() → None, call:_get_dialect, call:op.add_column, call:sa.Column, call:sa.String, call:sa.Boolean, call:op.execute, call:op.alter_column, call:op.drop_column, call:op.create_check_constraint, call:sa.text, func:downgrade() → None, call:_get_dialect, call:op.drop_constraint, call:op.add_column, call:sa.Column, call:postgresql.JSONB, call:sa.Text, call:op.execute, call:sa.JSON, call:op.drop_column | dep: typing, alembic, sqlalchemy.dialects, sqlalchemy, postgresql (dialect)
- 069d3da4dc9b_add_ssh_key_id_to_config_profiles.py | Alembic database migration that adds a nullable UUID foreign key column `ssh_key_id` to the `config_profiles` table referencing `ssh_keys.id` with SET NULL on delete | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Uuid, call:sa.ForeignKey, func:downgrade() → None, call:op.drop_column | dep: alembic, sqlalchemy
- 20260527160017_add_pi_agent_tool_type.py | Alembic database migration that adds a "pi-agent" terminal-based coding tool type to a tool_types table with Docker configuration templates | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute( sa.text("SELECT id FROM tool_types WHERE name = 'pi-agent'") ).fetchone, call:sa.text, call:json.dumps, func:downgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text | dep: json, typing, alembic, uuid, sqlalchemy
- 2026_05_22_add_clone_mode.py | Alembic database migration that adds ssh_key_id foreign key to git_repositories table and clone_mode/branch columns to tool_instances table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:postgresql.UUID, call:op.create_foreign_key, call:sa.String, func:downgrade() → None, call:op.drop_column, call:op.drop_constraint | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
- 2026_05_23_remove_is_builtin.py | Alembic database migration to remove the `is_builtin` column from the `tool_types` table | exp: func:upgrade() → None, call:op.execute, func:downgrade() → None, call:op.add_column, call:sa.Column, call:sa.Boolean | dep: alembic, sqlalchemy
- 2026_05_24_220141_add_startup_command.py | Alembic database migration that adds a nullable `startup_command` text column to the `tool_types` table. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Text, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
- 2026_05_24_add_config_profiles.py | Alembic database migration that creates config_profiles and config_profile_includes tables with indexes, and adds a foreign key column to tool_instances for managing user configuration profiles. | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.text, call:sa.ForeignKey, call:sa.String, call:sa.Text, call:postgresql.JSONB, call:sa.Boolean, call:sa.DateTime, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:op.create_index, call:sa.Integer, call:op.add_column, func:downgrade() → None, call:op.drop_index, call:op.drop_column, call:op.drop_table | dep: typing, alembic, sqlalchemy.dialects, sqlalchemy, postgresql dialect
- 2026_05_26_add_git_mounts.py | Alembic database migration that adds a git_mounts JSON column to the config_profiles table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.JSON, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
- 2026_05_27_external_repos.py | Alembic database migration that makes project_id nullable in git_repositories table to support external repositories and expands alembic_version version_num column to 64 characters. | exp: func:upgrade() → None, call:op.execute, call:op.alter_column, call:sa.UUID, func:downgrade() → None, call:op.alter_column, call:sa.UUID, call:op.execute | dep: typing, alembic, sqlalchemy
- 2026_05_28_add_monitoring_tables.py | Alembic database migration that creates monitoring tables (instance_events and health_checks) with indexes for tracking tool instance events and health checks | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.Uuid, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.DateTime, call:sa.func.now, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, call:sa.Boolean, call:sa.Integer, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy
- 2026_05_28_add_terminal_sessions_table.py | Alembic database migration that creates a terminal_sessions table with tracking columns and foreign key to tool_instances | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.UUID, call:sa.String, call:sa.DateTime, call:sa.text, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, call:op.f, func:downgrade() → None, call:op.drop_index, call:op.f, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy
- 2026_05_28_add_tool_definition_manifests.py | Alembic database migration that creates a tool_definition_manifests table, adds manifest-related columns to tool_types and tool_instances, and migrates existing pi-agent data from Dockerfile templates to JSON-based manifest definitions | exp: func:upgrade() → None, call:op.get_bind, call:op.create_table, call:sa.Column, call:sa.UUID, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.Boolean, call:sa.TIMESTAMP, call:sa.func.now, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:sa.ForeignKeyConstraint, call:sa.CheckConstraint, call:conn.execute, call:sa.text, call:result.fetchone, call:op.add_column, call:op.create_foreign_key, call:op.drop_constraint, call:op.execute, call:json.dumps, call:str, func:downgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:op.drop_column, call:op.drop_constraint, call:op.drop_table | dep: json, uuid, typing, alembic, sqlalchemy
- 2026_05_28_drop_tool_configs_and_config_folders.py | Alembic database migration that drops `tool_configs` and `config_folders` tables with conditional existence checks and full downgrade recreation | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:op.drop_table, func:downgrade() → None, call:op.create_table, call:sa.Column, call:sa.UUID, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.Boolean, call:sa.TIMESTAMP, call:sa.func.now, call:sa.PrimaryKeyConstraint, call:sa.Integer | dep: typing, alembic, sqlalchemy
- 2026_05_29_add_notifications_table.py | Alembic database migration that creates a notifications table with user-linked, categorized, severity-graded messages supporting read/dismissed tracking and optimized querying indexes. | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.Uuid, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.DateTime, call:sa.func.now, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, call:sa.text, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy
- 2026_05_29_add_ssh_key_ids_to_tool_instances.py | Alembic database migration that adds a JSON column named ssh_key_ids to the tool_instances table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.JSON, func:downgrade() → None, call:op.drop_column | dep: alembic, sqlalchemy
- 2026_05_29_drop_ssh_key_id_from_config_profiles.py | Alembic database migration that removes the ssh_key_id column from the config_profiles table | exp: func:upgrade() → None, call:op.drop_column, func:downgrade() → None, call:op.add_column, call:sa.Column, call:sa.Uuid, call:sa.ForeignKey | dep: alembic, sqlalchemy
- 2026_05_29_fix_code_server_bind_addr.py | Alembic database migration that fixes code-server tool type compose templates by replacing deprecated `--bind-addr` flag with `--host` flag | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute( sa.text(""" SELECT id, compose_template FROM tool_types WHERE name = 'code-server' AND compose_template LIKE '%--bind-addr%' """) ).fetchall, call:sa.text, call:compose_template.replace( "--bind-addr 0.0.0.0:8443", "--host 0.0.0.0" ).replace, call:print, func:downgrade() → None | dep: typing, alembic, sqlalchemy
- 2026_05_29_fix_code_server_bind_addr_port.py | Alembic database migration that fixes code-server Docker compose templates and instance files by replacing broken `--host` flags with correct `--bind-addr 0.0.0.0:port` configurations | exp: func:_fix_tool_type_templates(conn) → None, call:conn.execute( sa.text(""" SELECT id, compose_template, default_port FROM tool_types WHERE name = 'code-server' AND compose_template LIKE '%--host%' """) ).fetchall, call:sa.text, call:compose_template.split, call:len, call:line.lstrip, call:new_lines.append, call:"\n".join, call:print, func:_fix_instance_compose_files(conn) → None, call:conn.execute( sa.text(""" SELECT column_name FROM information_schema.columns WHERE table_name = 'tool_instances' AND column_name = 'compose_path' """) ).fetchone, call:sa.text, call:print, call:conn.execute( sa.text(""" SELECT id, compose_path, tool_type_id FROM tool_instances WHERE compose_path IS NOT NULL """) ).fetchall, call:Path, call:path.exists, call:path.read_text, call:conn.execute( sa.text(""" SELECT default_port FROM tool_types WHERE id = :id """), {"id": tool_type_id}, ).fetchone, call:yaml.safe_load, call:data["services"].values, call:path.write_text, call:yaml.dump, func:upgrade() → None, call:op.get_bind, call:_fix_tool_type_templates, call:_fix_instance_compose_files, func:downgrade() → None | dep: typing, alembic, yaml, pathlib, sqlalchemy
- 2026_05_29_fix_web_tool_bind_address.py | Alembic database migration that updates code-server and jupyter-notebook tool type compose templates to bind to 0.0.0.0 | exp: func:_fix_code_server_compose(conn) → None, call:conn.execute( sa.text(""" SELECT id, compose_template, definition_type FROM tool_types WHERE name = 'code-server' """) ).fetchone, call:sa.text, call:compose_template.split, call:enumerate, call:len, call:line.lstrip, call:new_lines.append, call:image_line.lstrip, call:new_lines.index, call:new_lines.insert, call:"\n".join, call:print, func:_fix_jupyter_compose(conn) → None, call:conn.execute( sa.text(""" SELECT id, compose_template, definition_type FROM tool_types WHERE name = 'jupyter-notebook' """) ).fetchone, call:sa.text, call:compose_template.split, call:enumerate, call:new_lines.append, call:len, call:line.lstrip, call:"\n".join, call:print, func:upgrade() → None, call:op.get_bind, call:_fix_code_server_compose, call:_fix_jupyter_compose, func:downgrade() → None | dep: typing, alembic, sqlalchemy
- 2026_05_29_remove_lsio_command_override.py | Alembic database migration that removes broken command overrides containing --bind-addr or --host flags from LinuxServer.io code-server Docker Compose templates in both database tool_types records and on-disk instance compose files. | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute( sa.text(""" SELECT id, compose_template FROM tool_types WHERE name = 'code-server' """) ).fetchall, call:sa.text, call:yaml.safe_load, call:data["services"].values, call:svc.get, call:yaml.dump, call:print, call:conn.execute( sa.text(""" SELECT column_name FROM information_schema.columns WHERE table_name = 'tool_instances' AND column_name = 'compose_path' """) ).fetchone, call:conn.execute( sa.text(""" SELECT id, compose_path FROM tool_instances WHERE compose_path IS NOT NULL """) ).fetchall, call:Path, call:path.exists, call:path.read_text, call:path.write_text, func:downgrade() → None | dep: collections.abc, alembic, yaml, pathlib, sqlalchemy, pathlib.Path, information_schema
- 2026_05_29_remove_ssh_keys_mount_from_manifest.py | Alembic database migration that removes (or restores) the ssh_keys mount from a JSON manifest stored in the tool_definition_manifests table for the pi-agent tool definition. | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:isinstance, call:json.loads, call:manifest.get, call:len, call:m.get, call:json.dumps, func:downgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:isinstance, call:json.loads, call:manifest.get, call:any, call:m.get, call:mounts.append, call:json.dumps | dep: json, typing, alembic, sqlalchemy
- 2026_06_01_add_workspaces.py | Alembic database migration that creates a workspaces table with foreign keys to git_repositories and users, adds indexes, and adds a workspace_id column to tool_instances | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.Uuid, call:sa.String, call:sa.ForeignKey, call:sa.DateTime, call:sa.text, call:sa.UniqueConstraint, call:op.create_index, call:op.add_column, func:downgrade() → None, call:op.drop_index, call:op.drop_column, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy
- 398082499c30_add_tool_config_fields.py | Alembic database migration that adds five new columns (port_override, start_command, working_directory, environment_variables, volumes) to the tool_configs table with a port range check constraint. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Integer, call:sa.Text, call:postgresql.JSONB, call:op.create_check_constraint, call:sa.text, func:downgrade() → None, call:op.drop_constraint, call:op.drop_column | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
- 6fc7bfcf199f_merge_remove_is_builtin_and_add_config_.py | Alembic database migration that merges two parallel revision branches (removing is_builtin and adding config_profiles) into a single history line | exp: func:upgrade() → None, func:downgrade() → None | dep: alembic
- 86cec91fdb00_merge_profile_resolver_and_workspaces_.py | Alembic database migration that merges two divergent migration branches (profile resolver and workspaces) into a single head | exp: func:upgrade() → None, func:downgrade() → None | dep: alembic
- 8ed7dd80973d_create_config_folders_table.py | Alembic database migration that creates a config_folders table with user-owned configuration folders supporting JSONB file storage and project overrides | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.text, call:sa.ForeignKey, call:sa.String, call:sa.Text, call:postgresql.JSONB, call:sa.Boolean, call:sa.DateTime, call:sa.UniqueConstraint, call:op.create_index, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
- af8512103d67_add_tool_type_fields.py | Alembic database migration that adds new columns (definition_type, dockerfile_template, build_context, readiness_probe) to the tool_types table with a CHECK constraint on definition_type. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.String, call:sa.Text, call:postgresql.JSONB, call:op.create_check_constraint, call:sa.text, func:downgrade() → None, call:op.drop_constraint, call:op.drop_column | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
- f3d2dc90ba3a_merge_single_interface_and_clone_mode.py | Alembic database migration that merges two prior revisions (single_interface and clone_mode) into a single migration path | exp: func:upgrade() → None, func:downgrade() → None | dep: typing, alembic
## arch
Linear and branched Alembic migration pattern with merge migrations to reconcile divergent branches, using incremental revision files with upgrade/downgrade functions, idempotent operations, and occasional data migrations for schema transformations.
## tags
column, table, call:op.drop, key, alembic, downgrade, upgrade, constraint
## symbols
- upgrade
- downgrade
- _table_exists
- _column_exists
- _index_exists
- _foreign_key_exists
- _foreign_key_names_for_column
- _get_dialect
## workflows
- change versions behavior
read: 0001_initial_schema.py, 0002_refresh_tokens.py, 0003_user_configs.py
- change versions CLI
read: 2026_05_24_220141_add_startup_command.py, 2026_05_29_remove_lsio_command_override.py
- change versions config
read: 0003_user_configs.py, 0009_tool_configs.py, 0013_add_config_profiles.py
## dirty
-
+54
View File
@@ -0,0 +1,54 @@
# apps/api/src (index)
dir: apps/api/src
## role
Core application package that bootstraps and configures the Headquarter API FastAPI service with its infrastructure concerns.
## parent
index: apps/api/.pi-map.index.md
map: apps/api/.pi-map.md
## children
- apps/api/src/api
index: apps/api/src/api/.pi-map.index.md
map: apps/api/src/api/.pi-map.md
- apps/api/src/auth
index: apps/api/src/auth/.pi-map.index.md
map: apps/api/src/auth/.pi-map.md
- apps/api/src/headquarter_api.egg-info
index: apps/api/src/headquarter_api.egg-info/.pi-map.index.md
map: apps/api/src/headquarter_api.egg-info/.pi-map.md
- apps/api/src/models
index: apps/api/src/models/.pi-map.index.md
map: apps/api/src/models/.pi-map.md
- apps/api/src/schemas
index: apps/api/src/schemas/.pi-map.index.md
map: apps/api/src/schemas/.pi-map.md
- apps/api/src/scripts
index: apps/api/src/scripts/.pi-map.index.md
map: apps/api/src/scripts/.pi-map.md
- apps/api/src/seeds
index: apps/api/src/seeds/.pi-map.index.md
map: apps/api/src/seeds/.pi-map.md
- apps/api/src/services
index: apps/api/src/services/.pi-map.index.md
map: apps/api/src/services/.pi-map.md
- apps/api/src/utils
index: apps/api/src/utils/.pi-map.index.md
map: apps/api/src/utils/.pi-map.md
## files
- __init__.py
- config.py
- database.py
- logging_config.py
- main.py
## links
index: apps/api/src/.pi-map.index.md
map: apps/api/src/.pi-map.md
## workflows
- change src behavior
read: __init__.py, config.py, database.py
- change src config
read: config.py, logging_config.py
- explore src subdirectories
index: apps/api/src/api/.pi-map.index.md, apps/api/src/auth/.pi-map.index.md, apps/api/src/headquarter_api.egg-info/.pi-map.index.md
## dirty
-
+35
View File
@@ -0,0 +1,35 @@
# apps/api/src
dir: apps/api/src
index: apps/api/src/.pi-map.index.md
## role
Core application package that bootstraps and configures the Headquarter API FastAPI service with its infrastructure concerns.
## files
- __init__.py | Marks the directory as a Python package for the Headquarter API.
- config.py | Defines application configuration settings using Pydantic with environment variable loading, database URL construction, and computed properties for service URLs and OAuth endpoints. | exp: class:Settings, func:build_database_url(user: str, password: str, host: str, port: int, database: str) → str | dep: pydantic, pydantic_settings
- database.py | Configures async SQLAlchemy database engine and session factory, and provides retry logic for database initialization with Alembic migrations via subprocess. | exp: func:init_database(max_retries, retry_delay) → bool, call:range, call:engine.connect, call:test_conn.execute, call:text, call:test_conn.close, call:logger.info, call:asyncio.get_event_loop().run_in_executor, call:subprocess.run, call:logger.debug, call:logger.error, call:asyncio.sleep, call:str(exc).lower, call:logger.warning | dep: asyncio, logging, subprocess, sqlalchemy.ext.asyncio, sqlalchemy.pool, src.config, sqlalchemy
- logging_config.py | Configures structured JSON logging with correlation ID injection, custom formatters, and HTTP request/exception middleware for a FastAPI application. | exp: class:CorrelationIdFilter, method:filter(self, record: logging.LogRecord) → bool, call:get_correlation_id, class:JSONFormatter, method:format(self, record: logging.LogRecord) → str, call:self.formatTime, call:record.getMessage, call:getattr, call:self.formatException, call:json.dumps, method:formatTime(self, record: logging.LogRecord, datefmt) → str, call:time.strftime, call:time.gmtime, class:RequestLoggingMiddleware, method:dispatch(self, request: Request, call_next: Callable) → Response, call:time.time, call:logger.info, call:call_next, call:int, call:logger.error, call:type, call:traceback.format_exc, class:ExceptionLoggingMiddleware, method:dispatch(self, request: Request, call_next: Callable) → Response, call:call_next, call:logger.critical, call:traceback.format_exc, func:configure_logging(level) → None, call:JSONFormatter, call:logging.StreamHandler, call:console_handler.setFormatter, call:console_handler.addFilter, call:CorrelationIdFilter, call:root_logger.setLevel, call:logging.getLogger("uvicorn").setLevel, call:logging.getLogger("uvicorn.access").setLevel, call:logging.getLogger("sqlalchemy.engine").setLevel, call:logger.info, call:logging.getLevelName | dep: json, logging, sys, time, traceback, collections.abc, fastapi, starlette.middleware.base, src.services.shared.correlation
- main.py | Initializes and configures a FastAPI application for the "Headquarter API" with database setup, middleware, routing, and background services. | exp: func:_sanitize_validation_errors(errors), call:error.get, call:str, call:ctx.items, call:isinstance, call:type, call:sanitized.append, func:validation_exception_handler(request: Request, exc: RequestValidationError), call:exc.errors, call:logger.warning, call:_sanitize_validation_errors, call:JSONResponse, func:on_startup(), call:logger.info, call:init_database, call:logger.error, call:sys.exit, call:_health_monitor.start, call:seed_builtin_tool_types, func:on_shutdown(), call:logger.info, call:_health_monitor.stop | dep: logging, os, fastapi, fastapi.exceptions, fastapi.middleware.cors, fastapi.responses, fastapi.staticfiles, src.api.config, src.api.project, src.api.system, src.api.tool, src.api.user, src.api.workspace, src.config, src.models, src.database, src.logging_config, src.seeds.builtin_tool_types, src.services.instance, src.services.shared, sys, src.api.*
## arch
Layered configuration with Pydantic settings, async SQLAlchemy with Alembic migration integration, structured JSON logging with correlation ID tracking, and FastAPI middleware/routing setup.
## tags
src, database, logging, call:logger.info, api, middleware, fastapi, filter
## symbols
- Settings
- CorrelationIdFilter
- JSONFormatter
- RequestLoggingMiddleware
- ExceptionLoggingMiddleware
- build_database_url
- init_database
- filter
## workflows
- change src behavior
read: __init__.py, config.py, database.py
- change src config
read: config.py, logging_config.py
- explore src subdirectories
index: apps/api/src/api/.pi-map.index.md, apps/api/src/auth/.pi-map.index.md, apps/api/src/headquarter_api.egg-info/.pi-map.index.md
## dirty
-
+40
View File
@@ -0,0 +1,40 @@
# apps/api/src/api (index)
dir: apps/api/src/api
## role
Defines reusable API validation utilities and package structure for FastAPI router organization.
## parent
index: apps/api/src/.pi-map.index.md
map: apps/api/src/.pi-map.md
## children
- apps/api/src/api/config
index: apps/api/src/api/config/.pi-map.index.md
map: apps/api/src/api/config/.pi-map.md
- apps/api/src/api/project
index: apps/api/src/api/project/.pi-map.index.md
map: apps/api/src/api/project/.pi-map.md
- apps/api/src/api/system
index: apps/api/src/api/system/.pi-map.index.md
map: apps/api/src/api/system/.pi-map.md
- apps/api/src/api/tool
index: apps/api/src/api/tool/.pi-map.index.md
map: apps/api/src/api/tool/.pi-map.md
- apps/api/src/api/user
index: apps/api/src/api/user/.pi-map.index.md
map: apps/api/src/api/user/.pi-map.md
- apps/api/src/api/workspace
index: apps/api/src/api/workspace/.pi-map.index.md
map: apps/api/src/api/workspace/.pi-map.md
## files
- __init__.py
- shared_validators.py
## links
index: apps/api/src/api/.pi-map.index.md
map: apps/api/src/api/.pi-map.md
## workflows
- change api behavior
read: __init__.py, shared_validators.py
- explore api subdirectories
index: apps/api/src/api/config/.pi-map.index.md, apps/api/src/api/project/.pi-map.index.md, apps/api/src/api/system/.pi-map.index.md
## dirty
-
+30
View File
@@ -0,0 +1,30 @@
# apps/api/src/api
dir: apps/api/src/api
index: apps/api/src/api/.pi-map.index.md
## role
Defines reusable API validation utilities and package structure for FastAPI router organization.
## files
- __init__.py | Marks the directory as a Python package for API routers.
- shared_validators.py | Provides reusable Pydantic validator functions for API schema validation including mount paths, files, environment variables, and volume mounts. | exp: func:validate_mount_path(v: str | None) → str | None, call:v.startswith, raise:ValueError, func:validate_files(v: dict | None, max_size_bytes) → dict | None, call:v.items, call:path.startswith, call:len, call:content.encode, raise:ValueError, func:validate_env_vars(v: dict | None) → dict | None, call:isinstance, raise:ValueError, func:validate_volumes(v: list | None) → list | None, call:isinstance, call:enumerate, raise:ValueError
## arch
Modular utility package with shared Pydantic validators for cross-cutting API schema concerns, following separation of validation logic from route handlers.
## tags
validate, raise:value, error, call:isinstance, mount, api, init, path
## symbols
- validate_mount_path
- validate_files
- validate_env_vars
- validate_volumes
- call:v.startswith
- raise:ValueError
- call:v.items
- call:path.startswith
## workflows
- change api behavior
read: __init__.py, shared_validators.py
- explore api subdirectories
index: apps/api/src/api/config/.pi-map.index.md, apps/api/src/api/project/.pi-map.index.md, apps/api/src/api/system/.pi-map.index.md
## dirty
-
+24
View File
@@ -0,0 +1,24 @@
# apps/api/src/api/config (index)
dir: apps/api/src/api/config
## role
Exposes REST API endpoints for managing user configuration profiles and settings.
## parent
index: apps/api/src/api/.pi-map.index.md
map: apps/api/src/api/.pi-map.md
## children
-
## files
- __init__.py
- config_profiles.py
- user_config.py
## links
index: apps/api/src/api/config/.pi-map.index.md
map: apps/api/src/api/config/.pi-map.md
## workflows
- change config behavior
read: __init__.py, config_profiles.py, user_config.py
- change config config
read: config_profiles.py, user_config.py
## dirty
-
+31
View File
@@ -0,0 +1,31 @@
# apps/api/src/api/config
dir: apps/api/src/api/config
index: apps/api/src/api/config/.pi-map.index.md
## role
Exposes REST API endpoints for managing user configuration profiles and settings.
## files
- __init__.py | Aggregates and exports configuration-related API routers for the config module. | dep: src.api.config.config_profiles, src.api.config.user_config
- config_profiles.py | FastAPI router providing CRUD endpoints for user config profiles with includes, resolution, defaults, and git URL validation | exp: func:list_config_profiles(project_id, tool_type_id, current_user_id, session), call:select(ConfigProfile) .where(ConfigProfile.user_id == user_uuid) .options, call:selectinload, call:uuid.UUID, call:conditions.append, call:ConfigProfile.project_id.is_, call:ConfigProfile.tool_type_id.is_, call:query.where, call:or_, call:session.execute, call:result.scalars().all, call:profile_to_response, func:create_config_profile(data: ConfigProfileCreate, current_user_id, session), call:create_profile, call:logger.debug, call:profile_to_response, func:get_config_profile(profile_id: str, current_user_id, session), call:get_profile_with_includes, call:uuid.UUID, call:profile_to_response, raise:HTTPException, func:update_config_profile(profile_id: str, data: ConfigProfileUpdate, current_user_id, session), call:get_profile_with_includes, call:uuid.UUID, call:update_profile, call:logger.debug, call:profile_to_response, raise:HTTPException, func:delete_config_profile(profile_id: str, current_user_id, session), call:get_profile_with_includes, call:uuid.UUID, call:session.delete, call:session.commit, call:logger.debug, raise:HTTPException, func:update_profile_includes_endpoint(profile_id: str, data: ConfigProfileIncludeUpdate, current_user_id, session), call:get_profile_with_includes, call:uuid.UUID, call:update_includes, call:session.execute, call:select(ConfigProfileInclude).where, call:inc_result.scalars().all, call:logger.debug, call:profile_to_response, call:list, raise:HTTPException, func:preview_config_profile(profile_id: str, current_user_id, session), call:get_profile_with_includes, call:uuid.UUID, call:resolve_profile, call:resolved_profile_to_dict, raise:HTTPException, func:resolve_default_profile_endpoint(project_id, tool_type_id, current_user_id, session), call:resolve_default_profile, call:uuid.UUID, func:get_default_profiles_endpoint(user_id, session) → dict, call:session.execute, call:select(UserConfig).where, call:result.scalar_one_or_none, func:set_default_profiles_endpoint(data: DefaultProfilesUpdate, user_id, session) → dict, call:validate_default_profiles, call:get_or_create_user_config, call:session.commit, call:session.refresh, func:get_default_profile_for_tool_type_endpoint(tool_type_id: str, user_id, session) → dict, call:session.execute, call:select(UserConfig).where, call:result.scalar_one_or_none, call:user_config.default_profiles.get, func:validate_git_url_endpoint(data: ValidateGitUrlRequest, current_user_id, session) → ValidateGitUrlResponse, call:validate_git_url | dep: logging, uuid, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, sqlalchemy.orm, src.auth.dependencies, src.models, src.schemas.config, src.services.config.config_profile_resolver, src.services.config.crud_service, src.services.config.resolver_service
- user_config.py | Provides FastAPI endpoints to get and update the current user's configuration settings, creating a default config if none exists. | exp: func:_get_or_create_config(session: AsyncSession, user_id: uuid.UUID) → UserConfig, call:session.execute, call:select(UserConfig).where, call:result.scalar_one_or_none, call:UserConfig, call:session.add, call:session.commit, call:session.refresh, func:get_user_config(user_id, session) → UserConfigResponse, call:_get_user, call:_get_or_create_config, call:UserConfigResponse.model_validate, func:update_user_config(data: UserConfigUpdate, user_id, session) → UserConfigResponse, call:_get_user, call:_get_or_create_config, call:data.model_dump, call:logger.debug, call:session.commit, call:session.refresh, call:UserConfigResponse.model_validate | dep: logging, uuid, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models, src.schemas.user
## arch
FastAPI router modules following domain-driven separation with CRUD patterns, validation logic, and automatic default initialization.
## tags
config, profile, get, user, includes, call:uuid.uuid, or, default
## symbols
- list_config_profiles
- create_config_profile
- get_config_profile
- update_config_profile
- delete_config_profile
- update_profile_includes_endpoint
- preview_config_profile
- resolve_default_profile_endpoint
## workflows
- change config behavior
read: __init__.py, config_profiles.py, user_config.py
- change config config
read: config_profiles.py, user_config.py
## dirty
-
+43 -585
View File
@@ -1,10 +1,7 @@
"""Config profile API endpoints."""
import logging
import os
import subprocess
import uuid
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import select
@@ -12,10 +9,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from src.auth.dependencies import get_current_user_id, get_db_session
from src.models import ConfigProfile, ConfigProfileInclude
from src.models.project import Project
from src.models import ToolType
from src.models import UserConfig
from src.models import ConfigProfile, ConfigProfileInclude, UserConfig
from src.schemas.config import (
ConfigProfileCreate,
ConfigProfileIncludeUpdate,
@@ -27,125 +21,27 @@ from src.schemas.config import (
)
from src.services.config.config_profile_resolver import (
ConfigProfileCycleError,
check_include_cycle,
resolve_profile,
resolved_profile_to_dict,
)
from src.utils.git_url_parser import parse_git_url
from src.services.config.crud_service import (
create_profile,
get_or_create_user_config,
get_profile_with_includes,
profile_to_response,
update_includes,
update_profile,
validate_default_profiles,
)
from src.services.config.resolver_service import (
resolve_default_profile,
validate_git_url,
)
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/config-profiles", tags=["config-profiles"])
MAX_PROFILE_SIZE_MB = 10
MAX_PROFILE_SIZE_BYTES = MAX_PROFILE_SIZE_MB * 1024 * 1024
def _calculate_profile_size(data: dict) -> int:
"""Calculate approximate serialized size of profile data."""
total = 0
for key, value in data.get("env_vars", {}).items():
total += len(key.encode("utf-8")) + len(str(value).encode("utf-8"))
for key, value in data.get("runtime_hints", {}).items():
total += len(key.encode("utf-8")) + len(str(value).encode("utf-8"))
for mount in data.get("mounts", []):
total += len(str(mount.get("target", "")).encode("utf-8"))
total += len(str(mount.get("mode", "")).encode("utf-8"))
for path, content in mount.get("files", {}).items():
total += len(path.encode("utf-8")) + len(content.encode("utf-8"))
for path, content in data.get("files", {}).items():
total += len(path.encode("utf-8")) + len(content.encode("utf-8"))
return total
async def _get_profile_with_includes(
session: AsyncSession, profile_id: uuid.UUID
) -> ConfigProfile | None:
"""Fetch a profile with includes eagerly loaded."""
result = await session.execute(
select(ConfigProfile)
.where(ConfigProfile.id == profile_id)
.options(selectinload(ConfigProfile.includes))
)
return result.scalar_one_or_none()
async def _check_access(
session: AsyncSession,
user_id: uuid.UUID,
project_id: uuid.UUID | None = None,
tool_type_id: uuid.UUID | None = None,
) -> None:
"""Verify user has access to referenced project and tool type."""
if project_id is not None:
project = await session.get(Project, project_id)
if project is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
)
# Add ownership check if needed; for now just verify existence
if tool_type_id is not None:
tool_type = await session.get(ToolType, tool_type_id)
if tool_type is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Tool type not found"
)
async def _validate_git_mounts(
session: AsyncSession,
user_id: uuid.UUID,
git_mounts: list[Any],
project_id: uuid.UUID | None = None,
) -> None:
"""Validate git mount URLs.
Simply checks that remote_url looks like a valid git URL.
Actual clone validation happens at instance startup time.
"""
for mount in git_mounts:
remote_url = mount.get("remote_url")
if not remote_url:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Git mount missing remote_url",
)
if not remote_url.startswith(("http://", "https://", "git@", "ssh://")):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid git URL: {remote_url}",
)
def _profile_to_response(
profile: ConfigProfile, includes: list[ConfigProfileInclude] | None = None
) -> dict:
return {
"id": str(profile.id),
"user_id": str(profile.user_id),
"name": profile.name,
"description": profile.description,
"project_id": str(profile.project_id) if profile.project_id else None,
"tool_type_id": str(profile.tool_type_id) if profile.tool_type_id else None,
"env_vars": profile.env_vars or {},
"runtime_hints": profile.runtime_hints or {},
"mounts": profile.mounts or [],
"git_mounts": profile.git_mounts or [],
"files": profile.files or {},
"is_default": profile.is_default,
"includes": [
{
"id": str(inc.id),
"included_profile_id": str(inc.included_profile_id),
"order_index": inc.order_index,
}
for inc in (includes or profile.includes)
],
"created_at": profile.created_at.isoformat() if profile.created_at else None,
"updated_at": profile.updated_at.isoformat() if profile.updated_at else None,
}
@router.get("", response_model=list[ConfigProfileResponse])
async def list_config_profiles(
@@ -165,26 +61,21 @@ async def list_config_profiles(
)
if project_id or tool_type_id:
# Compatibility filter: include portable profiles and matching scoped profiles
from sqlalchemy import or_
project_uuid = uuid.UUID(project_id) if project_id else None
tool_uuid = uuid.UUID(tool_type_id) if tool_type_id else None
from sqlalchemy import or_
conditions: list = []
# Portable profiles (no project, no tool)
conditions.append(
(ConfigProfile.project_id.is_(None))
& (ConfigProfile.tool_type_id.is_(None))
)
if project_uuid:
# Profiles matching this project (with or without tool)
conditions.append(ConfigProfile.project_id == project_uuid)
if tool_uuid:
# Profiles matching this tool (with or without project)
conditions.append(ConfigProfile.tool_type_id == tool_uuid)
if project_uuid and tool_uuid:
# Exact match
conditions.append(
(ConfigProfile.project_id == project_uuid)
& (ConfigProfile.tool_type_id == tool_uuid)
@@ -194,7 +85,7 @@ async def list_config_profiles(
result = await session.execute(query)
profiles = result.scalars().all()
return [_profile_to_response(p) for p in profiles]
return [profile_to_response(p) for p in profiles]
@router.post(
@@ -206,69 +97,9 @@ async def create_config_profile(
session: AsyncSession = Depends(get_db_session),
):
"""Create a new config profile."""
user_uuid = current_user_id
# Check for duplicate name
existing = await session.execute(
select(ConfigProfile)
.where(
ConfigProfile.user_id == user_uuid,
ConfigProfile.name == data.name,
)
.options(selectinload(ConfigProfile.includes))
)
if existing.scalar_one_or_none() is not None:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"Profile with name '{data.name}' already exists",
)
# Validate references
project_uuid = uuid.UUID(data.project_id) if data.project_id else None
tool_uuid = uuid.UUID(data.tool_type_id) if data.tool_type_id else None
await _check_access(session, user_uuid, project_uuid, tool_uuid)
# Validate git mounts reference existing repositories
if data.git_mounts:
git_mounts_data = [
m.model_dump() if hasattr(m, "model_dump") else m for m in data.git_mounts
]
await _validate_git_mounts(session, user_uuid, git_mounts_data, project_uuid)
# Check size
size = _calculate_profile_size(data.model_dump())
if size > MAX_PROFILE_SIZE_BYTES:
raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail=f"Profile size exceeds {MAX_PROFILE_SIZE_MB}MB limit",
)
profile = ConfigProfile(
user_id=user_uuid,
name=data.name,
description=data.description,
project_id=project_uuid,
tool_type_id=tool_uuid,
env_vars=data.env_vars,
runtime_hints=data.runtime_hints,
mounts=[m.model_dump() for m in data.mounts],
git_mounts=[m.model_dump() for m in data.git_mounts],
files=data.files,
is_default=data.is_default,
)
session.add(profile)
await session.commit()
# Re-fetch with includes to avoid lazy loading issues
result = await session.execute(
select(ConfigProfile)
.where(ConfigProfile.id == profile.id)
.options(selectinload(ConfigProfile.includes))
)
profile = result.scalar_one()
logger.debug("Created config profile %s for user %s", profile.id, user_uuid)
return _profile_to_response(profile)
profile = await create_profile(session, current_user_id, data)
logger.debug("Created config profile %s for user %s", profile.id, current_user_id)
return profile_to_response(profile)
@router.get("/{profile_id}", response_model=ConfigProfileResponse)
@@ -278,7 +109,7 @@ async def get_config_profile(
session: AsyncSession = Depends(get_db_session),
):
"""Get a config profile by ID."""
profile = await _get_profile_with_includes(session, uuid.UUID(profile_id))
profile = await get_profile_with_includes(session, uuid.UUID(profile_id))
if profile is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Profile not found"
@@ -287,7 +118,7 @@ async def get_config_profile(
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized"
)
return _profile_to_response(profile)
return profile_to_response(profile)
@router.put("/{profile_id}", response_model=ConfigProfileResponse)
@@ -298,7 +129,7 @@ async def update_config_profile(
session: AsyncSession = Depends(get_db_session),
):
"""Update a config profile."""
profile = await _get_profile_with_includes(session, uuid.UUID(profile_id))
profile = await get_profile_with_includes(session, uuid.UUID(profile_id))
if profile is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Profile not found"
@@ -308,78 +139,9 @@ async def update_config_profile(
status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized"
)
update_data = data.model_dump(exclude_unset=True)
# Handle name uniqueness
if "name" in update_data:
existing = await session.execute(
select(ConfigProfile).where(
ConfigProfile.user_id == profile.user_id,
ConfigProfile.name == update_data["name"],
ConfigProfile.id != profile.id,
)
)
if existing.scalar_one_or_none() is not None:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"Profile with name '{update_data['name']}' already exists",
)
# Validate references
project_uuid = (
uuid.UUID(update_data["project_id"])
if "project_id" in update_data and update_data["project_id"]
else (profile.project_id if "project_id" not in update_data else None)
)
tool_uuid = (
uuid.UUID(update_data["tool_type_id"])
if "tool_type_id" in update_data and update_data["tool_type_id"]
else (profile.tool_type_id if "tool_type_id" not in update_data else None)
)
await _check_access(session, profile.user_id, project_uuid, tool_uuid)
# Validate git mounts reference existing repositories
if "git_mounts" in update_data and update_data["git_mounts"] is not None:
git_mounts_data = [
m.model_dump() if hasattr(m, "model_dump") else m
for m in update_data["git_mounts"]
]
await _validate_git_mounts(
session, profile.user_id, git_mounts_data, project_uuid
)
# Check size
current_data = _profile_to_response(profile)
merged = {**current_data, **update_data}
size = _calculate_profile_size(merged)
if size > MAX_PROFILE_SIZE_BYTES:
raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail=f"Profile size exceeds {MAX_PROFILE_SIZE_MB}MB limit",
)
# Apply updates
for field_name, value in update_data.items():
if field_name in ("project_id", "tool_type_id"):
value = uuid.UUID(value) if value else None
elif field_name == "mounts" and value is not None:
value = [m.model_dump() if not isinstance(m, dict) else m for m in value]
elif field_name == "git_mounts" and value is not None:
value = [m.model_dump() if not isinstance(m, dict) else m for m in value]
setattr(profile, field_name, value)
await session.commit()
# Re-fetch with includes to avoid lazy loading issues
result = await session.execute(
select(ConfigProfile)
.where(ConfigProfile.id == profile.id)
.options(selectinload(ConfigProfile.includes))
)
profile = result.scalar_one()
profile = await update_profile(session, profile, data)
logger.debug("Updated config profile %s", profile.id)
return _profile_to_response(profile)
return profile_to_response(profile)
@router.delete("/{profile_id}", status_code=status.HTTP_204_NO_CONTENT)
@@ -389,7 +151,7 @@ async def delete_config_profile(
session: AsyncSession = Depends(get_db_session),
):
"""Delete a config profile."""
profile = await _get_profile_with_includes(session, uuid.UUID(profile_id))
profile = await get_profile_with_includes(session, uuid.UUID(profile_id))
if profile is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Profile not found"
@@ -407,14 +169,14 @@ async def delete_config_profile(
@router.put("/{profile_id}/includes", response_model=ConfigProfileResponse)
async def update_profile_includes(
async def update_profile_includes_endpoint(
profile_id: str,
data: ConfigProfileIncludeUpdate,
current_user_id: uuid.UUID = Depends(get_current_user_id),
session: AsyncSession = Depends(get_db_session),
):
"""Update the ordered includes for a config profile."""
profile = await _get_profile_with_includes(session, uuid.UUID(profile_id))
profile = await get_profile_with_includes(session, uuid.UUID(profile_id))
if profile is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Profile not found"
@@ -424,69 +186,8 @@ async def update_profile_includes(
status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized"
)
# Validate all included profiles exist and belong to the user
included_uuids = [uuid.UUID(inc_id) for inc_id in data.includes]
for inc_uuid in included_uuids:
inc_profile = await session.get(ConfigProfile, inc_uuid)
if inc_profile is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Included profile not found: {inc_uuid}",
)
if inc_profile.user_id != current_user_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Not authorized to include profile: {inc_uuid}",
)
if inc_uuid == profile.id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Profile cannot include itself",
)
# Check for cycles
cycle = await check_include_cycle(session, profile.id, None)
if cycle is None and included_uuids:
# Check each new include would not create a cycle
for inc_uuid in included_uuids:
cycle = await check_include_cycle(session, profile.id, inc_uuid)
if cycle is not None:
break
if cycle is not None:
cycle_str = " -> ".join(str(c) for c in cycle)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Include cycle detected: {cycle_str}",
)
# Remove existing includes
result = await session.execute(
select(ConfigProfileInclude).where(
ConfigProfileInclude.profile_id == profile.id
)
)
for existing in result.scalars().all():
await session.delete(existing)
await session.flush()
# Add new includes
for order_index, inc_uuid in enumerate(included_uuids):
include = ConfigProfileInclude(
profile_id=profile.id,
included_profile_id=inc_uuid,
order_index=order_index,
)
session.add(include)
await session.flush()
await session.commit()
# Re-fetch profile (includes loaded separately due to SQLite async issue)
result = await session.execute(
select(ConfigProfile).where(ConfigProfile.id == profile.id)
)
profile = result.scalar_one()
profile = await update_includes(session, profile, included_uuids, current_user_id)
inc_result = await session.execute(
select(ConfigProfileInclude).where(
@@ -496,7 +197,7 @@ async def update_profile_includes(
direct_includes = inc_result.scalars().all()
logger.debug("Updated includes for config profile %s", profile.id)
return _profile_to_response(profile, list(direct_includes))
return profile_to_response(profile, list(direct_includes))
@router.get("/{profile_id}/preview")
@@ -506,7 +207,7 @@ async def preview_config_profile(
session: AsyncSession = Depends(get_db_session),
):
"""Preview the resolved output of a config profile."""
profile = await _get_profile_with_includes(session, uuid.UUID(profile_id))
profile = await get_profile_with_includes(session, uuid.UUID(profile_id))
if profile is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Profile not found"
@@ -528,120 +229,19 @@ async def preview_config_profile(
@router.get("/defaults/resolve")
async def resolve_default_profile(
async def resolve_default_profile_endpoint(
project_id: str = Query(..., description="Project ID"),
tool_type_id: str = Query(..., description="Tool type ID"),
current_user_id: uuid.UUID = Depends(get_current_user_id),
session: AsyncSession = Depends(get_db_session),
):
"""Resolve the default config profile for a project/tool combination.
Selects by specificity:
1. project+tool explicit default
2. project explicit default
3. tool explicit default
4. global/user explicit default
5. first created compatible profile
6. none (returns null)
"""
user_uuid = current_user_id
project_uuid = uuid.UUID(project_id)
tool_uuid = uuid.UUID(tool_type_id)
# Fetch all compatible profiles ordered by created_at
query = (
select(ConfigProfile)
.where(ConfigProfile.user_id == user_uuid)
.where(
(ConfigProfile.project_id.is_(None) & ConfigProfile.tool_type_id.is_(None))
| (ConfigProfile.project_id == project_uuid)
| (ConfigProfile.tool_type_id == tool_uuid)
| (
(ConfigProfile.project_id == project_uuid)
& (ConfigProfile.tool_type_id == tool_uuid)
)
)
.order_by(ConfigProfile.created_at)
"""Resolve the default config profile for a project/tool combination."""
return await resolve_default_profile(
session,
current_user_id,
uuid.UUID(project_id),
uuid.UUID(tool_type_id),
)
result = await session.execute(query)
profiles = result.scalars().all()
if not profiles:
return {"profile_id": None, "profile_name": None}
# Check explicit defaults by specificity
explicit_defaults = [p for p in profiles if p.is_default]
# Most specific: project+tool
for p in explicit_defaults:
if p.project_id == project_uuid and p.tool_type_id == tool_uuid:
return {"profile_id": str(p.id), "profile_name": p.name}
# Next: project only
for p in explicit_defaults:
if p.project_id == project_uuid and p.tool_type_id is None:
return {"profile_id": str(p.id), "profile_name": p.name}
# Next: tool only
for p in explicit_defaults:
if p.project_id is None and p.tool_type_id == tool_uuid:
return {"profile_id": str(p.id), "profile_name": p.name}
# Next: global/user (no project, no tool)
for p in explicit_defaults:
if p.project_id is None and p.tool_type_id is None:
return {"profile_id": str(p.id), "profile_name": p.name}
# Fall back to first created compatible profile
first = profiles[0]
return {"profile_id": str(first.id), "profile_name": first.name}
# ---------------------------------------------------------------------------
# Default profile management
# ---------------------------------------------------------------------------
async def _get_or_create_user_config(
session: AsyncSession,
user_id: uuid.UUID,
) -> UserConfig:
"""Get existing user config or create a new one."""
result = await session.execute(
select(UserConfig).where(UserConfig.user_id == user_id)
)
user_config = result.scalar_one_or_none()
if user_config is None:
user_config = UserConfig(user_id=user_id, config={})
session.add(user_config)
return user_config
async def _validate_default_profiles(
session: AsyncSession,
user_id: uuid.UUID,
default_profiles: dict[str, str],
) -> None:
"""Validate that all profile IDs in default_profiles belong to the user."""
for tool_type_id, profile_id_str in default_profiles.items():
try:
profile_uuid = uuid.UUID(profile_id_str)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid profile ID for tool type {tool_type_id}: {profile_id_str}",
)
profile = await session.get(ConfigProfile, profile_uuid)
if profile is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Profile not found: {profile_id_str}",
)
if profile.user_id != user_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Profile does not belong to user: {profile_id_str}",
)
@router.get("/defaults")
@@ -664,8 +264,8 @@ async def set_default_profiles_endpoint(
session: AsyncSession = Depends(get_db_session),
) -> dict:
"""Set default profile mappings for the current user."""
await _validate_default_profiles(session, user_id, data.default_profiles)
user_config = await _get_or_create_user_config(session, user_id)
await validate_default_profiles(session, user_id, data.default_profiles)
user_config = await get_or_create_user_config(session, user_id)
user_config.config = {
**user_config.config,
"default_profiles": data.default_profiles,
@@ -691,152 +291,10 @@ async def get_default_profile_for_tool_type_endpoint(
@router.post("/validate-git-url", response_model=ValidateGitUrlResponse)
async def validate_git_url(
async def validate_git_url_endpoint(
data: ValidateGitUrlRequest,
current_user_id: uuid.UUID = Depends(get_current_user_id),
session: AsyncSession = Depends(get_db_session),
) -> ValidateGitUrlResponse:
"""Validate a git remote URL and list available branches.
Parses the URL, suggests corrections for browser URLs, and runs
git ls-remote to verify reachability and enumerate branches.
"""
parse_result = parse_git_url(data.url)
original_url = data.url.strip()
url_to_check = parse_result.get("base_url") or original_url
if not url_to_check:
return ValidateGitUrlResponse(
valid=False,
error=parse_result.get("message", "Invalid URL"),
error_code=parse_result.get("error_code", "INVALID_URL"),
)
# If the URL needed parsing, return suggestion without checking remote
if parse_result.get("needs_parsing") and url_to_check != original_url:
return ValidateGitUrlResponse(
valid=False,
suggested_url=url_to_check,
error=parse_result.get("message"),
error_code=parse_result.get("error_code", "URL_NEEDS_PARSING"),
)
# Optional SSH key for private repos
env = None
key_path = None
if data.ssh_key_id:
from src.models import SSHKey
from src.services.shared.ssh_keys import _get_fernet
try:
ssh_key_uuid = uuid.UUID(data.ssh_key_id)
except ValueError:
return ValidateGitUrlResponse(
valid=False,
error="Invalid SSH key ID format",
error_code="INVALID_SSH_KEY",
)
ssh_key = await session.get(SSHKey, ssh_key_uuid)
if ssh_key is None or ssh_key.user_id != current_user_id:
return ValidateGitUrlResponse(
valid=False,
error="SSH key not found or not authorized",
error_code="SSH_KEY_NOT_FOUND",
)
import tempfile
fernet = _get_fernet()
private_key = fernet.decrypt(ssh_key.private_key_encrypted.encode()).decode()
fd, key_path = tempfile.mkstemp(prefix="ssh_key_")
try:
os.write(fd, private_key.encode())
finally:
os.close(fd)
os.chmod(key_path, 0o600)
env = {
"GIT_SSH_COMMAND": f"ssh -i {key_path} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
}
try:
result = subprocess.run(
["git", "ls-remote", "--heads", url_to_check],
capture_output=True,
text=True,
timeout=30,
env={**os.environ, **env} if env else None,
)
except subprocess.TimeoutExpired:
if key_path and os.path.exists(key_path):
os.unlink(key_path)
return ValidateGitUrlResponse(
valid=False,
error="Remote repository check timed out",
error_code="TIMEOUT",
)
except FileNotFoundError:
if key_path and os.path.exists(key_path):
os.unlink(key_path)
return ValidateGitUrlResponse(
valid=False,
error="git command not found on server",
error_code="GIT_NOT_FOUND",
)
finally:
if key_path and os.path.exists(key_path):
os.unlink(key_path)
if result.returncode != 0:
stderr = result.stderr.strip()
if (
"could not resolve" in stderr.lower()
or "unable to access" in stderr.lower()
):
error_msg = "Could not reach repository. Check the URL and network access."
error_code = "UNREACHABLE"
elif (
"authentication" in stderr.lower() or "permission denied" in stderr.lower()
):
error_msg = (
"Authentication failed. Provide an SSH key for private repositories."
)
error_code = "AUTH_FAILED"
else:
error_msg = f"Repository not accessible: {stderr[:200]}"
error_code = "REMOTE_ERROR"
return ValidateGitUrlResponse(
valid=False,
error=error_msg,
error_code=error_code,
)
# Parse branches from ls-remote output
branches: list[str] = []
default_branch = "main"
for line in result.stdout.strip().split("\n"):
if not line.strip():
continue
parts = line.split()
if len(parts) == 2:
ref = parts[1]
# refs/heads/branch-name
if ref.startswith("refs/heads/"):
branch_name = ref[len("refs/heads/") :]
branches.append(branch_name)
if branch_name in ("main", "master"):
default_branch = branch_name
if not branches:
return ValidateGitUrlResponse(
valid=False,
error="No branches found in remote repository",
error_code="NO_BRANCHES",
)
return ValidateGitUrlResponse(
valid=True,
suggested_url=url_to_check if url_to_check != original_url else None,
branches=branches,
default_branch=default_branch,
)
"""Validate a git remote URL and list available branches."""
return await validate_git_url(session, current_user_id, data.url, data.ssh_key_id)
+22
View File
@@ -0,0 +1,22 @@
# apps/api/src/api/project (index)
dir: apps/api/src/api/project
## role
Provides REST API endpoints for project and Git repository management in the backend service.
## parent
index: apps/api/src/api/.pi-map.index.md
map: apps/api/src/api/.pi-map.md
## children
-
## files
- __init__.py
- git_repositories.py
- projects.py
## links
index: apps/api/src/api/project/.pi-map.index.md
map: apps/api/src/api/project/.pi-map.md
## workflows
- change project behavior
read: __init__.py, git_repositories.py, projects.py
## dirty
-
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
# apps/api/src/api/system (index)
dir: apps/api/src/api/system
## role
Provides system-level API endpoints for monitoring, administration, and infrastructure interaction including dashboards, health checks, events, notifications, and container terminal access.
## parent
index: apps/api/src/api/.pi-map.index.md
map: apps/api/src/api/.pi-map.md
## children
-
## files
- __init__.py
- dashboard.py
- events.py
- health.py
- instance_proxy.py
- notifications.py
- terminal.py
## links
index: apps/api/src/api/system/.pi-map.index.md
map: apps/api/src/api/system/.pi-map.md
## workflows
- change system behavior
read: __init__.py, dashboard.py, events.py
## dirty
-
+33
View File
@@ -0,0 +1,33 @@
# apps/api/src/api/system
dir: apps/api/src/api/system
index: apps/api/src/api/system/.pi-map.index.md
## role
Provides system-level API endpoints for monitoring, administration, and infrastructure interaction including dashboards, health checks, events, notifications, and container terminal access.
## files
- __init__.py | Aggregates and re-exports system API router modules from submodules for centralized access. | dep: src.api.system.dashboard, src.api.system.events, src.api.system.health, src.api.system.instance_proxy, src.api.system.notifications, src.api.system.terminal
- dashboard.py | Provides a FastAPI endpoint that returns a dashboard summary with aggregated counts of projects, repositories, SSH keys, and recent activity for the authenticated user. | exp: func:get_dashboard_summary(user_id, session) → dict, call:session.execute, call:select(func.count()).select_from(Project).where, call:func.count, call:projects_result.scalar, call:select(func.count()).select_from(GitRepository).where, call:repos_result.scalar, call:select(func.count()).select_from(SSHKey).where, call:ssh_keys_result.scalar, call:select(Project) .where(Project.owner_id == user_id) .order_by(Project.created_at.desc()) .limit, call:Project.created_at.desc, call:recent_projects.scalars().all | dep: uuid, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models, src.models.project, src.models (GitRepository, Project, SSHKey)
- events.py | Implements an SSE streaming endpoint that delivers instance events to authenticated users with per-user connection limits and keep-alive pings. | exp: func:events_stream(request: Request, user_id) → StreamingResponse, call:_connection_counts.get, call:InstanceEventBus, call:asyncio.Queue, call:queue.put_nowait, call:contextlib.suppress, call:queue.get_nowait, call:event_bus.subscribe, call:asyncio.wait_for, call:queue.get, call:json.dumps, call:unsubscribe, call:max, call:_connection_counts.pop, call:StreamingResponse, call:event_generator, raise:HTTPException, func:event_generator() → AsyncGenerator[str, None], call:InstanceEventBus, call:asyncio.Queue, call:queue.put_nowait, call:contextlib.suppress, call:queue.get_nowait, call:event_bus.subscribe, call:asyncio.wait_for, call:queue.get, call:json.dumps, call:unsubscribe, call:max, call:_connection_counts.get, call:_connection_counts.pop, func:on_event(payload: InstanceEventPayload) → None, call:queue.put_nowait, call:contextlib.suppress, call:queue.get_nowait | dep: asyncio, contextlib, json, uuid, collections.abc, fastapi, fastapi.responses, src.auth.dependencies, src.services.instance.event_bus
- health.py | Implements FastAPI health check endpoints for monitoring system status including database connectivity and disk space. | exp: func:health_check() → dict[str, Any], call:HealthChecks, call:time_module.perf_counter, call:SessionLocal, call:session.execute, call:text, call:DatabaseHealth, call:round, call:shutil.disk_usage, call:DiskHealth, call:HealthResponse( status=overall_status, timestamp=datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), version="0.1.0", checks=checks, uptime_seconds=round(time.time() - _start_time, 2), ).model_dump, call:datetime.now(timezone.utc).isoformat().replace, call:time.time, func:health_check_db() → dict[str, Any], call:time_module.perf_counter, call:SessionLocal, call:session.execute, call:text, call:DatabaseHealthResponse( status="healthy", response_time_ms=round(db_time, 2), ).model_dump, call:round, call:DatabaseHealthResponse( status="unhealthy", response_time_ms=0.0, ).model_dump | dep: time, datetime, typing, fastapi, sqlalchemy, src.database, src.schemas.system, shutil
- instance_proxy.py | Proxies HTTP requests from FastAPI endpoints to running containerized tool instances after verifying ownership and status. | exp: func:_proxy_request(request: Request, instance_id: uuid.UUID, path: str, user_id: uuid.UUID, session: AsyncSession) → Response, call:session.get, call:str, call:request.headers.items, call:key.lower, call:httpx.AsyncClient, call:request.body, call:client.request, call:logger.error, call:dict, call:response_headers.pop, call:Response, raise:HTTPException, func:proxy_to_instance(request: Request, instance_id: uuid.UUID, path, user_id, session) → Response, call:_proxy_request | dep: logging, uuid, httpx, fastapi, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models
- notifications.py | Defines FastAPI REST endpoints for managing user notifications (list, unread count, mark read, dismiss/clear) with support for muted categories from user config. | exp: class:NotificationItem, class:NotificationListResponse, class:UnreadCountResponse, class:MarkAllReadResponse, class:ClearAllResponse, func:_get_mute_categories(session: AsyncSession, user_id: uuid.UUID) → list[str], call:session.execute, call:select(UserConfig).where, call:result.scalar_one_or_none, call:config.config.get, call:isinstance, func:list_notifications(limit, offset, unread_only, user, session) → NotificationListResponse, call:_get_mute_categories, call:notification_service.list_notifications, call:NotificationListResponse, call:NotificationItem.model_validate, func:get_unread_count(user, session) → UnreadCountResponse, call:notification_service.get_unread_count, call:UnreadCountResponse, func:mark_notification_read(notification_id: uuid.UUID, user, session) → NotificationItem, call:notification_service.mark_read, call:NotificationItem.model_validate, raise:HTTPException, func:mark_all_read(user, session) → MarkAllReadResponse, call:notification_service.mark_all_read, call:MarkAllReadResponse, func:clear_all_notifications(user, session) → ClearAllResponse, call:notification_service.dismiss_all, call:ClearAllResponse, func:dismiss_notification(notification_id: uuid.UUID, user, session) → None, call:notification_service.dismiss, raise:HTTPException | dep: uuid, datetime, fastapi, pydantic, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models.user, src.models, src.services.shared.notification_service, sqlalchemy
- terminal.py | Provides WebSocket and HTTP endpoints for managing interactive terminal sessions attached to running Docker container tool instances, including session creation, attachment, input/output streaming, resize/reset control messages, and session listing. | exp: class:SessionRef, method:__init__(self, session, slot_session_id), func:terminal_websocket_default(websocket: WebSocket, instance_id: str, db_session) → None, call:_handle_terminal_websocket, func:terminal_websocket_specific(websocket: WebSocket, instance_id: str, session_id: str, db_session) → None, call:_handle_terminal_websocket, func:_handle_terminal_websocket(websocket: WebSocket, instance_id: str, target_session_id: str | None, db_session: AsyncSession) → None, call:logger.debug, call:websocket.accept, call:uuid.UUID, call:logger.error, call:websocket.close, call:_get_user_from_websocket, call:logger.warning, call:db_session.get, call:get_container_status, call:terminal_manager.get_or_create_session, call:terminal_manager.get_session, call:logger.info, call:terminal_manager.create_session, call:terminal_manager._find_key_by_internal_id, call:terminal_manager.attach_websocket, call:websocket.send_json, call:SessionRef, call:asyncio.create_task, call:_write_loop, call:_heartbeat_loop, call:asyncio.wait, call:len, call:task.cancel, call:str, call:suppress, call:terminal_manager.detach_websocket, func:_write_loop(session_ref: SessionRef, websocket, instance_id: str) → None, call:session.is_alive, call:asyncio.sleep, call:websocket.receive, call:session.write_input, call:text.startswith, call:json.loads, call:ctrl.get, call:logger.debug, call:session.resize, call:session.acknowledge_data, call:websocket.send_json, call:terminal_manager.reset_session, call:terminal_manager.attach_websocket, call:text.encode, func:_heartbeat_loop(websocket: WebSocket) → None, call:asyncio.sleep, call:websocket.send_json, func:_get_terminal_instance(instance_id: uuid.UUID, user_id: uuid.UUID, db_session: AsyncSession) → ToolInstance, call:db_session.get, raise:HTTPException, func:list_terminal_sessions(instance_id: uuid.UUID, user_id, db_session) → dict, call:_get_terminal_instance, call:db_session.execute, call:select(TerminalSessionModel) .where(TerminalSessionModel.instance_id == instance_id) .where(TerminalSessionModel.status != "closed") .order_by, call:TerminalSessionModel.created_at.asc, call:result.scalars().all, call:terminal_manager.get_session, call:str, call:sessions.append, call:live_session.has_websockets, call:row.created_at.isoformat, call:row.last_activity_at.isoformat, func:create_terminal_session(instance_id: uuid.UUID, data: dict, user_id, db_session) → dict, call:_get_terminal_instance, call:db_session.get, call:data.get, call:terminal_manager.create_session, raise:HTTPException, func:close_terminal_session(instance_id: uuid.UUID, session_id: str, user_id, db_session) → dict, call:_get_terminal_instance, call:terminal_manager._find_key_by_internal_id, call:str, call:terminal_manager.get_session, call:terminal_manager.close_session, raise:HTTPException, func:reset_specific_terminal_session(instance_id: uuid.UUID, session_id: str, user_id, db_session) → dict, call:_get_terminal_instance, call:terminal_manager._find_key_by_internal_id, call:str, call:terminal_manager.get_session, call:db_session.get, call:terminal_manager.reset_session, raise:HTTPException, func:rename_terminal_session(instance_id: uuid.UUID, session_id: str, data: dict, user_id, db_session) → dict, call:_get_terminal_instance, call:data.get, call:isinstance, call:terminal_manager.get_session, call:str, call:db_session.get, call:uuid.UUID, call:db_session.commit, raise:HTTPException, func:reset_terminal_session(instance_id: uuid.UUID, user_id, db_session) → dict, call:_get_terminal_instance, call:db_session.get, call:terminal_manager.reset_session, call:logger.info, call:str, call:logger.error, raise:HTTPException, func:_get_user_from_websocket(websocket: WebSocket, db_session: AsyncSession) → uuid.UUID | None, call:websocket.cookies.get, call:Settings, call:decode_session_cookie, call:uuid.UUID, call:str | dep: asyncio, json, logging, uuid, contextlib, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, starlette.websockets, src.auth.dependencies, src.models, src.services.terminal.terminal_manager, src.services.docker, src.auth.session, src.config, starlette
## arch
FastAPI modular router pattern with separate files per domain concern, combining REST endpoints, SSE streaming, WebSocket connections, and HTTP proxying; uses dependency injection for auth and integrates with Docker/container runtime for terminal and instance proxy features.
## tags
session, terminal, call:terminal, call:, src, get, response, websocket
## symbols
- NotificationItem
- NotificationListResponse
- UnreadCountResponse
- MarkAllReadResponse
- ClearAllResponse
- SessionRef
- get_dashboard_summary
- events_stream
## workflows
- change system behavior
read: __init__.py, dashboard.py, events.py
## dirty
-
+26
View File
@@ -0,0 +1,26 @@
# apps/api/src/api/tool (index)
dir: apps/api/src/api/tool
## role
Provides FastAPI REST API endpoints and routers for managing the complete lifecycle of Docker-based tools, including type definitions, instances, sessions, and lifecycle operations.
## parent
index: apps/api/src/api/.pi-map.index.md
map: apps/api/src/api/.pi-map.md
## children
-
## files
- __init__.py
- sessions.py
- tool_definitions.py
- tool_instances.py
- tool_lifecycle.py
- tool_types.py
- tool_types_validation.py
## links
index: apps/api/src/api/tool/.pi-map.index.md
map: apps/api/src/api/tool/.pi-map.md
## workflows
- change tool behavior
read: __init__.py, sessions.py, tool_definitions.py
## dirty
-
+33
View File
@@ -0,0 +1,33 @@
# apps/api/src/api/tool
dir: apps/api/src/api/tool
index: apps/api/src/api/tool/.pi-map.index.md
## role
Provides FastAPI REST API endpoints and routers for managing the complete lifecycle of Docker-based tools, including type definitions, instances, sessions, and lifecycle operations.
## files
- __init__.py | Aggregates and exports all tool-related API routers from submodules as a unified module interface. | dep: src.api.tool.sessions, src.api.tool.tool_definitions, src.api.tool.tool_instances, src.api.tool.tool_lifecycle, src.api.tool.tool_types
- sessions.py | API endpoint that retrieves all active tool instances (sessions) for the currently authenticated user with related metadata | exp: func:get_user_sessions(user_id, session) → dict, call:_get_user, call:session.execute, call:select(ToolInstance) .where(ToolInstance.owner_id == user_id) .where( ToolInstance.status.in_( ["running", "building", "pending", "stopped", "error"] ) ) .order_by, call:ToolInstance.status.in_, call:ToolInstance.created_at.desc, call:result.scalars().all, call:session.get, call:sessions.append, call:str, call:instance.created_at.isoformat | dep: uuid, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models
- tool_definitions.py | FastAPI router providing CRUD endpoints and manifest compilation for tool definition manifests stored in a database. | exp: class:CreateToolDefinitionRequest, class:UpdateToolDefinitionRequest, func:create_tool_definition(data: CreateToolDefinitionRequest, user_id, session) → dict, call:uuid.UUID, call:session.get, call:session.execute, call:select(ToolDefinitionManifest).where, call:existing.scalar_one_or_none, call:ToolDefinitionManifest, call:session.add, call:session.commit, call:session.refresh, call:logger.info, call:str, call:definition.created_at.isoformat, raise:HTTPException, func:list_tool_definitions(user_id, session, include_bases) → dict, call:select, call:query.where, call:ToolDefinitionManifest.is_base.is_, call:session.execute, call:query.order_by, call:ToolDefinitionManifest.created_at.desc, call:result.scalars().all, call:str, call:d.created_at.isoformat, func:get_tool_definition(definition_id: uuid.UUID, user_id, session) → dict, call:session.get, call:str, call:definition.created_at.isoformat, call:definition.updated_at.isoformat, raise:HTTPException, func:update_tool_definition(definition_id: uuid.UUID, data: UpdateToolDefinitionRequest, user_id, session) → dict, call:session.get, call:session.commit, call:session.refresh, call:logger.info, call:str, call:definition.updated_at.isoformat, raise:HTTPException, func:delete_tool_definition(definition_id: uuid.UUID, user_id, session) → dict, call:session.get, call:session.execute, call:select(ToolType).where, call:result.scalars().all, call:", ".join, call:session.delete, call:session.commit, call:logger.info, call:str, raise:HTTPException, func:compile_tool_definition(definition_id: uuid.UUID, user_id, session) → dict, call:session.get, call:dict, call:resolve_base, call:deep_merge, call:compile_dockerfile, call:compile_entrypoint, call:compute_image_tag, call:compile_compose, call:session.commit, call:str, raise:HTTPException | dep: logging, uuid, fastapi, pydantic, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models, src.services.build.manifest_compiler
- tool_instances.py | Provides FastAPI REST endpoints for managing tool instances including CRUD operations, logs, health checks, tunnel recreation, event history, and HTTP proxying to running containers. | exp: func:create_instance(project_id: uuid.UUID, repo_id: uuid.UUID, data: CreateInstanceRequest, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:create_tool_instance, call:str, call:instance.created_at.isoformat, raise:HTTPException, func:list_instances(project_id: uuid.UUID, repo_id: uuid.UUID, user_id, session) → list[dict], call:_get_user, call:_get_owned_project, call:session.execute, call:select(ToolInstance) .where(ToolInstance.repository_id == repo_id) .where(ToolInstance.owner_id == user_id) .order_by, call:ToolInstance.created_at.desc, call:result.scalars().all, call:str, call:i.created_at.isoformat, func:get_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:session.get, call:get_container_status, call:str, call:instance.created_at.isoformat, call:instance.last_started_at.isoformat, raise:HTTPException, func:rename_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, data: dict, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:data.get("display_name", "").strip, call:rename_tool_instance, call:str, raise:HTTPException, func:get_instance_logs(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, tail, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:session.get, call:get_container_logs, raise:HTTPException, func:recreate_tunnel_endpoint(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:recreate_instance_tunnel, raise:HTTPException, func:check_instance_tunnel_health(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:session.get, call:get_container_status, call:instance.probe_result.get, call:"\n".join, call:check_tunnel_health, call:tunnel_health.get, raise:HTTPException, func:get_instance_events(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, limit, user_id, session) → list[dict], call:_get_user, call:_get_owned_project, call:session.get, call:session.execute, call:select(InstanceEvent) .where(InstanceEvent.instance_id == instance_id) .order_by(InstanceEvent.created_at.desc()) .limit, call:InstanceEvent.created_at.desc, call:result.scalars().all, call:str, call:row.created_at.isoformat, raise:HTTPException, func:proxy_to_instance(request: Request, project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, path, user_id, session) → Response, call:session.get, call:str, call:dict, call:headers.pop, call:httpx.AsyncClient, call:request.body, call:client.request, call:logger.error, call:response_headers.pop, call:Response, raise:HTTPException | dep: logging, uuid, httpx, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models, src.services.docker, src.services.shared.tunnel, src.schemas.tool, src.services.tool.instance_service
- tool_lifecycle.py | FastAPI router providing REST endpoints for managing Docker-based tool instance lifecycle operations (start, stop, restart, delete). | exp: func:start_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, data, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:start_tool_instance, raise:HTTPException, func:stop_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:stop_tool_instance, raise:HTTPException, func:restart_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:restart_tool_instance, raise:HTTPException, func:delete_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, force, user_id, session) → None, call:_get_user, call:_get_owned_project, call:delete_tool_instance, call:str, call:detail.lower, raise:HTTPException | dep: logging, uuid, fastapi, sqlalchemy.ext.asyncio, src.auth.dependencies, src.schemas.tool, src.services.tool.instance_service
- tool_types.py | Defines FastAPI routes for CRUD operations and validation of Docker tool types with compose/dockerfile/manifest definitions. | exp: func:_require_admin(user: User) → None, func:create_tool_type(data: ToolTypeCreate, user_id, session) → ToolType, call:_get_user, call:_require_admin, call:session.scalar, call:select(ToolType).where, call:ToolType, call:session.add, call:session.commit, call:session.refresh, raise:HTTPException, func:list_tool_types(user_id, session) → list[ToolType], call:_get_user, call:session.execute, call:select(ToolType).order_by, call:list, call:result.scalars().all, func:get_tool_type(tool_type_id: uuid.UUID, user_id, session) → ToolType, call:_get_user, call:session.get, raise:HTTPException, func:update_tool_type(tool_type_id: uuid.UUID, data: ToolTypeUpdate, user_id, session) → ToolType, call:_get_user, call:_require_admin, call:session.get, call:data.model_dump, call:update_data.get, call:validate_compose_yaml, call:check_port_exposed, call:validate_required_variables, call:update_data.items, call:setattr, call:session.commit, call:session.refresh, raise:HTTPException, func:validate_tool_type_template(data: ToolTypeValidateRequest, user_id, session) → dict, call:_get_user, call:errors.append, call:validate_compose_yaml, call:str, call:data.dockerfile_template.strip().startswith, call:len, func:validate_tool_type(tool_type_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:session.get, call:errors.append, call:validate_compose_yaml, call:str, call:tool_type.dockerfile_template.strip().startswith, call:len, raise:HTTPException, func:delete_tool_type(tool_type_id: uuid.UUID, user_id, session) → None, call:_get_user, call:_require_admin, call:session.get, call:session.delete, call:session.commit, raise:HTTPException | dep: uuid, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, src.api.tool.tool_types_validation, src.auth.dependencies, src.models, src.models.user, src.schemas.tool
- tool_types_validation.py | Validates Docker Compose YAML templates by sanitizing template variables, parsing YAML, checking required structure, verifying port exposure, and ensuring required variables are present. | exp: func:sanitize_template_vars(template: str) → str, call:re.sub, func:validate_compose_yaml(template: str) → dict, call:sanitize_template_vars, call:yaml.safe_load, call:isinstance, raise:ValueError, func:check_port_exposed(parsed: dict, port: int) → bool, call:str, call:isinstance, call:parsed["services"].values, func:validate_required_variables(template: str, variables: list[str]) → None, raise:HTTPException | dep: re, yaml, fastapi, fastapi.HTTPException, fastapi.status
## arch
Modular FastAPI router composition pattern with separated concerns across CRUD operations, validation, lifecycle management, and proxying, aggregated through `__init__.py` for unified module interface.
## tags
get, call:, tool, raise:httpexception, user, instance, call:str, call:session.get
## symbols
- CreateToolDefinitionRequest
- UpdateToolDefinitionRequest
- get_user_sessions
- create_tool_definition
- list_tool_definitions
- get_tool_definition
- update_tool_definition
- delete_tool_definition
## workflows
- change tool behavior
read: __init__.py, sessions.py, tool_definitions.py
## dirty
-
+2
View File
@@ -3,11 +3,13 @@
from src.api.tool.sessions import sessions_router
from src.api.tool.tool_definitions import router as tool_definitions_router
from src.api.tool.tool_instances import router as tool_instances_router
from src.api.tool.tool_lifecycle import router as tool_lifecycle_router
from src.api.tool.tool_types import router as tool_types_router
__all__ = [
"sessions_router",
"tool_definitions_router",
"tool_instances_router",
"tool_lifecycle_router",
"tool_types_router",
]
+8 -2
View File
@@ -53,6 +53,13 @@ async def get_user_sessions(
repo = await session.get(GitRepository, instance.repository_id)
project = await session.get(Project, instance.project_id)
workspace_name = None
if instance.workspace_id:
from src.models import Workspace as WorkspaceModel
workspace = await session.get(WorkspaceModel, instance.workspace_id)
if workspace:
workspace_name = workspace.name
sessions.append(
{
"id": str(instance.id),
@@ -64,10 +71,9 @@ async def get_user_sessions(
"repository_id": str(instance.repository_id),
"project_name": project.name if project else "unknown",
"project_id": str(instance.project_id),
"workspace_name": workspace_name,
"status": instance.status,
"url": instance.url,
"clone_mode": instance.clone_mode,
"branch": instance.branch,
"selected_config_profile_id": str(instance.selected_config_profile_id)
if instance.selected_config_profile_id
else None,
File diff suppressed because it is too large Load Diff
+135
View File
@@ -0,0 +1,135 @@
"""Tool instance lifecycle API endpoints."""
import logging
import uuid
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from src.auth.dependencies import (
_get_owned_project,
_get_user,
get_current_user_id,
get_db_session,
)
from src.schemas.tool import StartInstanceRequest
from src.services.tool.instance_service import (
delete_tool_instance,
restart_tool_instance,
start_tool_instance,
stop_tool_instance,
)
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/projects", tags=["tool-instances"])
@router.post(
"/{project_id}/repositories/{repo_id}/instances/{instance_id}/start",
summary="Start instance",
description="Start a tool instance using Docker Compose.",
)
async def start_instance(
project_id: uuid.UUID,
repo_id: uuid.UUID,
instance_id: uuid.UUID,
data: StartInstanceRequest | None = None,
user_id: uuid.UUID = Depends(get_current_user_id),
session: AsyncSession = Depends(get_db_session),
) -> dict:
_user = await _get_user(session, user_id)
_project = await _get_owned_project(project_id, user_id, session)
try:
return await start_tool_instance(
session, user_id, project_id, repo_id, instance_id, data
)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
except RuntimeError as exc:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)
)
@router.post(
"/{project_id}/repositories/{repo_id}/instances/{instance_id}/stop",
summary="Stop instance",
description="Stop a running tool instance.",
)
async def stop_instance(
project_id: uuid.UUID,
repo_id: uuid.UUID,
instance_id: uuid.UUID,
user_id: uuid.UUID = Depends(get_current_user_id),
session: AsyncSession = Depends(get_db_session),
) -> dict:
_user = await _get_user(session, user_id)
_project = await _get_owned_project(project_id, user_id, session)
try:
return await stop_tool_instance(session, user_id, project_id, repo_id, instance_id)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
@router.post(
"/{project_id}/repositories/{repo_id}/instances/{instance_id}/restart",
summary="Restart instance",
description="Restart a tool instance.",
)
async def restart_instance(
project_id: uuid.UUID,
repo_id: uuid.UUID,
instance_id: uuid.UUID,
user_id: uuid.UUID = Depends(get_current_user_id),
session: AsyncSession = Depends(get_db_session),
) -> dict:
_user = await _get_user(session, user_id)
_project = await _get_owned_project(project_id, user_id, session)
try:
return await restart_tool_instance(
session, user_id, project_id, repo_id, instance_id
)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
except RuntimeError as exc:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)
)
@router.delete(
"/{project_id}/repositories/{repo_id}/instances/{instance_id}",
summary="Delete instance",
description="Delete a tool instance and remove its Docker containers and files.",
)
async def delete_instance(
project_id: uuid.UUID,
repo_id: uuid.UUID,
instance_id: uuid.UUID,
force: bool = False,
user_id: uuid.UUID = Depends(get_current_user_id),
session: AsyncSession = Depends(get_db_session),
) -> None:
_user = await _get_user(session, user_id)
_project = await _get_owned_project(project_id, user_id, session)
try:
await delete_tool_instance(
session, user_id, project_id, repo_id, instance_id, force
)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
except RuntimeError as exc:
detail = str(exc)
if "uncommitted changes" in detail.lower():
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail={
"message": "Repository has uncommitted changes",
"changed_files": detail,
"force_required": True,
},
)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=detail
)
+23
View File
@@ -0,0 +1,23 @@
# apps/api/src/api/user (index)
dir: apps/api/src/api/user
## role
Provides user-facing API endpoints for authentication, profile management, and SSH key operations in a FastAPI backend application.
## parent
index: apps/api/src/api/.pi-map.index.md
map: apps/api/src/api/.pi-map.md
## children
-
## files
- __init__.py
- auth.py
- ssh_keys.py
- users.py
## links
index: apps/api/src/api/user/.pi-map.index.md
map: apps/api/src/api/user/.pi-map.md
## workflows
- change user behavior
read: __init__.py, auth.py, ssh_keys.py
## dirty
-
+30
View File
@@ -0,0 +1,30 @@
# apps/api/src/api/user
dir: apps/api/src/api/user
index: apps/api/src/api/user/.pi-map.index.md
## role
Provides user-facing API endpoints for authentication, profile management, and SSH key operations in a FastAPI backend application.
## files
- __init__.py | Aggregates and exports user-related API routers from submodules for easy importing | dep: src.api.user.auth, src.api.user.ssh_keys, src.api.user.users
- auth.py | Implements OAuth2/OIDC authentication endpoints for a FastAPI application using Authentik as the identity provider, including login initiation, callback handling, logout, and current user retrieval. | exp: func:get_db_session() → AsyncGenerator[AsyncSession, None], call:SessionLocal, func:login(next) → RedirectResponse, call:Settings, call:token_urlsafe, call:build_login_redirect_url, call:logger.debug, call:RedirectResponse, call:response.set_cookie, func:callback(code: str, state: str, auth_state, auth_next, session) → RedirectResponse, call:logger.debug, call:logger.warning, call:Settings, call:httpx.AsyncClient, call:exchange_code_for_tokens, call:logger.info, call:logger.error, call:fetch_user_info, call:str, call:user_info.get, call:session.scalar, call:select(User).where, call:User, call:session.add, call:session.commit, call:session.refresh, call:create_session_cookie, call:build_cookie_options, call:cast, call:bool, call:cookie_options.get, call:RedirectResponse, call:redirect_response.set_cookie, call:redirect_response.delete_cookie, raise:HTTPException, func:logout(response: Response) → dict[str, str], call:Settings, call:build_cookie_options, call:cast, call:bool, call:str, call:cookie_options.get, call:response.delete_cookie, func:me(session_cookie, session) → dict[str, Any], call:logger.debug, call:bool, call:logger.warning, call:Settings, call:decode_session_cookie, call:session.get, call:logger.info, call:str, raise:HTTPException | dep: logging, secrets, typing, httpx, fastapi, fastapi.responses, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.cookies, src.auth.oidc, src.auth.session, src.config, src.database, src.models.user
- ssh_keys.py | Implements a FastAPI router for managing Ed25519 SSH key pairs, including generation, storage with encrypted private keys, listing, deletion, and cryptographic signing/verification operations. | exp: func:_get_fernet() → Fernet, call:Settings, call:hashlib.sha256(settings.session_secret.encode()).digest, call:settings.session_secret.encode, call:base64.urlsafe_b64encode, call:Fernet, func:generate_ssh_key_pair() → tuple[str, str], call:Ed25519PrivateKey.generate, call:private_key.public_key, call:private_key.private_bytes, call:serialization.NoEncryption, call:public_key.public_bytes, call:private_bytes.decode, call:public_bytes.decode, func:create_ssh_key(data: SSHKeyCreate, user_id, session) → SSHKey, call:_get_user, call:generate_ssh_key_pair, call:_get_fernet, call:fernet.encrypt(private_key.encode()).decode, call:private_key.encode, call:SSHKey, call:session.add, call:session.commit, call:session.refresh, func:list_ssh_keys(user_id, session) → list[SSHKey], call:_get_user, call:session.execute, call:select(SSHKey).where, call:list, call:result.scalars().all, func:delete_ssh_key(key_id: uuid.UUID, user_id, session) → None, call:_get_user, call:session.get, call:session.delete, call:session.commit, raise:HTTPException, func:sign_payload(key_id: uuid.UUID, data: SignPayloadRequest, user_id, session) → SignatureResponse, call:_get_user, call:session.get, call:_get_fernet, call:fernet.decrypt(ssh_key.private_key_encrypted.encode()).decode, call:ssh_key.private_key_encrypted.encode, call:serialization.load_ssh_private_key, call:private_key_pem.encode, call:private_key.sign, call:data.payload.encode, call:SignatureResponse, call:base64.b64encode(signature).decode, raise:HTTPException, func:verify_signature(key_id: uuid.UUID, data: VerifySignatureRequest, user_id, session) → VerifySignatureResponse, call:_get_user, call:session.get, call:serialization.load_ssh_public_key, call:ssh_key.public_key.encode, call:base64.b64decode, call:public_key.verify, call:data.payload.encode, call:VerifySignatureResponse, raise:HTTPException | dep: base64, uuid, cryptography.fernet, cryptography.hazmat.primitives, cryptography.hazmat.primitives.asymmetric.ed25519, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.dependencies, src.config, src.models, src.schemas.project, hashlib, cryptography
- users.py | Implements FastAPI endpoints for authenticated user profile management including retrieval, updates, and avatar uploads. | exp: func:get_profile(user_id, session) → User, call:_get_user, func:update_profile(data: UserProfileUpdate, user_id, session) → User, call:_get_user, call:len, call:data.name.strip, call:data.email.strip, call:session.commit, call:session.refresh, raise:HTTPException, func:upload_avatar(file: UploadFile, user_id, session) → User, call:_get_user, call:file.read, call:len, call:Path, call:old_path.exists, call:old_path.unlink, call:filename_part.split(".")[-1].lower, call:uuid.uuid4, call:file_path.write_bytes, call:session.commit, call:session.refresh, raise:HTTPException | dep: uuid, pathlib, fastapi, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models.user, src.schemas.user
## arch
Modular FastAPI router pattern with separate submodule files per domain concern (auth, SSH keys, users), aggregated via __init__.py, using OAuth2/OIDC external identity provider (Authentik) and encrypted key storage for cryptographic operations.
## tags
user, get, call:, key, ssh, src, cookie, raise:httpexception
## symbols
- get_db_session
- login
- callback
- logout
- me
- _get_fernet
- generate_ssh_key_pair
- create_ssh_key
## workflows
- change user behavior
read: __init__.py, auth.py, ssh_keys.py
## dirty
-
@@ -0,0 +1,24 @@
# apps/api/src/api/workspace (index)
dir: apps/api/src/api/workspace
## role
Provides FastAPI REST endpoints for workspace management, file operations, and git integration in user-scoped development environments.
## parent
index: apps/api/src/api/.pi-map.index.md
map: apps/api/src/api/.pi-map.md
## children
-
## files
- __init__.py
- workspace_files.py
- workspace_git.py
- workspace_instances.py
- workspaces.py
## links
index: apps/api/src/api/workspace/.pi-map.index.md
map: apps/api/src/api/workspace/.pi-map.md
## workflows
- change workspace behavior
read: __init__.py, workspace_files.py, workspace_git.py
## dirty
-
+31
View File
@@ -0,0 +1,31 @@
# apps/api/src/api/workspace
dir: apps/api/src/api/workspace
index: apps/api/src/api/workspace/.pi-map.index.md
## role
Provides FastAPI REST endpoints for workspace management, file operations, and git integration in user-scoped development environments.
## files
- __init__.py | Aggregates and re-exports workspace API router modules for centralized access | dep: src.api.workspace.workspace_files, src.api.workspace.workspace_git, src.api.workspace.workspace_instances, src.api.workspace.workspaces
- workspace_files.py | Provides FastAPI endpoints for listing, reading, and writing files within user workspaces with optional git commit integration. | exp: func:_get_workspace(session: AsyncSession, workspace_id: uuid.UUID, user_id: uuid.UUID) → Workspace, call:session.execute, call:select(Workspace).where, call:result.scalar_one_or_none, raise:HTTPException, func:list_files(workspace_id: uuid.UUID, path, user_id, session) → dict, call:_get_workspace, call:FileService, call:service.list_directory, raise:HTTPException, func:get_file_content(workspace_id: uuid.UUID, path: str, user_id, session) → dict, call:_get_workspace, call:FileService, call:service.read_file, raise:HTTPException, func:write_file(workspace_id: uuid.UUID, data: dict, user_id, session) → dict, call:_get_workspace, call:FileService, call:data.get("path", "").strip, call:data.get("message", "").strip, call:service.write_file, call:GitOperations, call:git.commit, raise:HTTPException | dep: uuid, fastapi, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models, src.services.shared.file_service, sqlalchemy, src.services.git.git_operations
- workspace_git.py | Provides FastAPI REST endpoints for git operations (status, branches, commit, push, pull, fetch, checkout, history) scoped to user workspaces. | exp: func:_get_workspace(session: AsyncSession, workspace_id: uuid.UUID, user_id: uuid.UUID) → Workspace, call:session.execute, call:select(Workspace).where, call:result.scalar_one_or_none, raise:HTTPException, func:git_status(workspace_id: uuid.UUID, user_id, session) → dict, call:_get_workspace, call:GitOperations, call:git.status, raise:HTTPException, func:git_branches(workspace_id: uuid.UUID, user_id, session) → dict, call:_get_workspace, call:GitOperations, call:git.branches, raise:HTTPException, func:git_commit(workspace_id: uuid.UUID, data: dict, user_id, session) → dict, call:_get_workspace, call:data.get("message", "").strip, call:GitOperations, call:git.commit, raise:HTTPException, func:git_push(workspace_id: uuid.UUID, user_id, session) → dict, call:_get_workspace, call:GitOperations, call:git.push, raise:HTTPException, func:git_pull(workspace_id: uuid.UUID, user_id, session) → dict, call:_get_workspace, call:GitOperations, call:git.pull, raise:HTTPException, func:git_fetch(workspace_id: uuid.UUID, user_id, session) → dict, call:_get_workspace, call:GitOperations, call:git.fetch, raise:HTTPException, func:git_checkout(workspace_id: uuid.UUID, data: dict, user_id, session) → dict, call:_get_workspace, call:data.get("branch", "").strip, call:GitOperations, call:git.checkout, call:session.commit, raise:HTTPException, func:git_history(workspace_id: uuid.UUID, path, limit, user_id, session) → dict, call:_get_workspace, call:GitOperations, call:git.history, raise:HTTPException | dep: uuid, fastapi, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models, src.services.git.git_operations, sqlalchemy
- workspace_instances.py | Defines API endpoints for listing tool instances associated with a specific workspace, verifying workspace ownership. | exp: func:_get_workspace(session: AsyncSession, workspace_id: uuid.UUID, user_id: uuid.UUID) → Workspace, call:session.execute, call:select(Workspace).where, call:result.scalar_one_or_none, raise:HTTPException, func:list_workspace_instances(workspace_id: uuid.UUID, user_id, session) → list[dict], call:_get_workspace, call:session.execute, call:select(ToolInstance) .where(ToolInstance.workspace_id == workspace_id) .order_by, call:ToolInstance.created_at.desc, call:result.scalars().all, call:str, call:i.created_at.isoformat | dep: uuid, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models
- workspaces.py | Provides FastAPI CRUD endpoints for managing Git repository workspaces with nested and top-level URL structures, including listing, creating, updating, deleting, and syncing workspaces with instance count tracking. | exp: func:list_all_workspaces(user_id, session) → list[dict], call:select(func.count(ToolInstance.id)) .where(ToolInstance.workspace_id == Workspace.id) .correlate(Workspace) .scalar_subquery, call:func.count, call:session.execute, call:select( Workspace, GitRepository.name.label("repo_name"), GitRepository.project_id, GitRepository.ssh_key_id.label("repo_ssh_key_id"), instance_count.label("instance_count"), ) .join(GitRepository, Workspace.repo_id == GitRepository.id) .where(Workspace.user_id == user_id) .order_by, call:GitRepository.name.label, call:GitRepository.ssh_key_id.label, call:instance_count.label, call:Workspace.created_at.desc, call:result.all, call:str, call:ws.last_sync_at.isoformat, call:ws.created_at.isoformat, call:ws.updated_at.isoformat, func:delete_workspace_top_level(workspace_id: uuid.UUID, force, user_id, session) → dict, call:session.get, call:WorkspaceManager, call:manager.delete, call:session.commit, call:session.rollback, call:logger.error, raise:HTTPException, func:create_workspace_top_level(data: dict, user_id, session) → dict, call:data.get("repo_id", "").strip, call:uuid.UUID, call:session.get, call:data.get("name", "").strip, call:data.get("branch", "main").strip, call:WorkspaceManager, call:manager.create, call:session.add, call:session.commit, call:session.rollback, call:logger.error, call:session.refresh, call:str, call:workspace.created_at.isoformat, raise:HTTPException, func:list_workspaces(project_id: uuid.UUID, repo_id: uuid.UUID, user_id, session) → list[dict], call:_get_repo, call:select(func.count(ToolInstance.id)) .where(ToolInstance.workspace_id == Workspace.id) .correlate(Workspace) .scalar_subquery, call:func.count, call:session.execute, call:select( Workspace, instance_count.label("instance_count"), ) .where(Workspace.repo_id == repo_id) .order_by, call:instance_count.label, call:Workspace.created_at.desc, call:result.all, call:str, call:ws.last_sync_at.isoformat, call:ws.created_at.isoformat, call:ws.updated_at.isoformat, func:create_workspace(project_id: uuid.UUID, repo_id: uuid.UUID, data: dict, user_id, session) → dict, call:_get_repo, call:data.get("name", "").strip, call:data.get("branch", "main").strip, call:WorkspaceManager, call:manager.create, call:session.add, call:session.commit, call:session.rollback, call:logger.error, call:session.refresh, call:str, call:workspace.created_at.isoformat, raise:HTTPException, func:get_workspace_detail(project_id: uuid.UUID, repo_id: uuid.UUID, workspace_id: uuid.UUID, user_id, session) → dict, call:_get_repo, call:_get_workspace, call:session.execute, call:select(func.count(ToolInstance.id)).where, call:func.count, call:result.scalar, call:str, call:workspace.last_sync_at.isoformat, call:workspace.created_at.isoformat, call:workspace.updated_at.isoformat, func:update_workspace(project_id: uuid.UUID, repo_id: uuid.UUID, workspace_id: uuid.UUID, data: dict, user_id, session) → dict, call:_get_repo, call:_get_workspace, call:data.get("name", "").strip, call:data.get("branch", "").strip, call:session.commit, call:session.rollback, call:logger.error, call:str, raise:HTTPException, func:delete_workspace(project_id: uuid.UUID, repo_id: uuid.UUID, workspace_id: uuid.UUID, force, user_id, session) → dict, call:_get_repo, call:_get_workspace, call:WorkspaceManager, call:manager.delete, call:session.commit, call:session.rollback, call:logger.error, raise:HTTPException, func:sync_workspace(project_id: uuid.UUID, repo_id: uuid.UUID, workspace_id: uuid.UUID, user_id, session) → dict, call:_get_repo, call:_get_workspace, call:WorkspaceManager, call:manager.sync, call:session.commit, call:workspace.last_sync_at.isoformat, raise:HTTPException, func:_get_repo(session: AsyncSession, repo_id: uuid.UUID, project_id: uuid.UUID, user_id: uuid.UUID) → GitRepository, call:session.execute, call:select(GitRepository) .where( GitRepository.id == repo_id, GitRepository.project_id == project_id, ) .options, call:selectinload, call:result.scalar_one_or_none, raise:HTTPException, func:_get_workspace(session: AsyncSession, workspace_id: uuid.UUID, repo_id: uuid.UUID) → Workspace, call:session.execute, call:select(Workspace).where, call:result.scalar_one_or_none, raise:HTTPException | dep: logging, uuid, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, sqlalchemy.orm, src.auth.dependencies, src.models, src.services.shared.workspace_manager
## arch
Modular router composition with domain-separated endpoint modules (files, git, instances, workspaces) using FastAPI dependency injection for workspace ownership verification and nested resource URL patterns.
## tags
workspace, get, raise:httpexception, call:, at.isoformat, git, call:select, call:data.get
## symbols
- _get_workspace
- list_files
- get_file_content
- write_file
- git_status
- git_branches
- git_commit
- git_push
## workflows
- change workspace behavior
read: __init__.py, workspace_files.py, workspace_git.py
## dirty
-
@@ -2,13 +2,16 @@
import uuid
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from src.auth.dependencies import get_current_user_id, get_db_session
from src.models import GitRepository
from src.models import ToolInstance
from src.models import Workspace
from src.schemas.tool import CreateInstanceRequest, CreateWorkspaceInstanceRequest
from src.services.tool.instance_service import create_tool_instance
router = APIRouter(prefix="/workspaces/{workspace_id}/instances")
@@ -30,6 +33,63 @@ async def _get_workspace(
return workspace
@router.post(
"/",
summary="Create instance from workspace",
description="Create a new tool instance mounted on this workspace.",
status_code=status.HTTP_201_CREATED,
)
async def create_workspace_instance(
workspace_id: uuid.UUID,
data: CreateWorkspaceInstanceRequest,
user_id: uuid.UUID = Depends(get_current_user_id),
session: AsyncSession = Depends(get_db_session),
) -> dict:
"""Create a tool instance directly on a workspace."""
workspace = await _get_workspace(session, workspace_id, user_id)
repo = await session.get(GitRepository, workspace.repo_id)
if repo is None:
raise HTTPException(status_code=404, detail="Repository not found")
if repo.project_id is None:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Repository is not associated with a project",
)
request = CreateInstanceRequest(
tool_type_id=data.tool_type_id,
display_name=data.display_name,
workspace_id=str(workspace.id),
config_profile_id=data.config_profile_id,
ssh_key_ids=data.ssh_key_ids,
)
try:
instance = await create_tool_instance(
session, user_id, repo.project_id, repo.id, request
)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
except RuntimeError as exc:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)
)
return {
"id": str(instance.id),
"name": instance.name,
"display_name": instance.display_name,
"tool_type_id": str(instance.tool_type_id),
"status": instance.status,
"workspace_id": str(instance.workspace_id) if instance.workspace_id else None,
"selected_config_profile_id": str(instance.selected_config_profile_id)
if instance.selected_config_profile_id
else None,
"created_at": instance.created_at.isoformat(),
}
@router.get("/")
async def list_workspace_instances(
workspace_id: uuid.UUID,
+24
View File
@@ -0,0 +1,24 @@
# apps/api/src/auth (index)
dir: apps/api/src/auth
## role
Provides authentication and authorization infrastructure for the API, including OIDC/OAuth2 integration, session management, and FastAPI dependency injection.
## parent
index: apps/api/src/.pi-map.index.md
map: apps/api/src/.pi-map.md
## children
-
## files
- __init__.py
- cookies.py
- dependencies.py
- oidc.py
- session.py
## links
index: apps/api/src/auth/.pi-map.index.md
map: apps/api/src/auth/.pi-map.md
## workflows
- change auth behavior
read: __init__.py, cookies.py, dependencies.py
## dirty
-
+31
View File
@@ -0,0 +1,31 @@
# apps/api/src/auth
dir: apps/api/src/auth
index: apps/api/src/auth/.pi-map.index.md
## role
Provides authentication and authorization infrastructure for the API, including OIDC/OAuth2 integration, session management, and FastAPI dependency injection.
## files
- __init__.py | Re-exports authentication utility functions from submodules to provide a unified public API for the auth package | dep: src.auth.cookies, src.auth.oidc, src.auth.session
- cookies.py | Builds a dictionary of HTTP cookie options from application settings for secure cookie configuration. | exp: func:build_cookie_options(settings: Settings) → dict[str, str | bool | None] | dep: src.config, src.config.Settings
- dependencies.py | Provides FastAPI dependency functions for database sessions, user authentication, and project authorization. | exp: func:get_db_session(), call:SessionLocal, func:get_current_user_id(session_cookie) → uuid.UUID, call:Settings, call:decode_session_cookie, call:uuid.UUID, call:str, raise:HTTPException, func:get_current_user(session_cookie, db_session) → User, call:Settings, call:decode_session_cookie, call:uuid.UUID, call:str, call:db_session.get, raise:HTTPException, func:_get_user(session: AsyncSession, user_id: uuid.UUID) → User, call:session.get, raise:HTTPException, func:_get_owned_project(project_id: uuid.UUID, user_id: uuid.UUID, session: AsyncSession) → "Project", call:session.get, raise:HTTPException | dep: uuid, typing, fastapi, sqlalchemy.ext.asyncio, src.auth.session, src.config, src.database, src.models.project, src.models.user, sqlalchemy
- oidc.py | Implements OIDC/OAuth2 authentication flow with Authentik, including login URL construction, authorization code exchange for tokens, and user info retrieval. | exp: func:build_login_redirect_url(settings: Settings, redirect_uri: str, state: str) → str, call:urlencode, func:exchange_code_for_tokens(settings: Settings, code: str, redirect_uri: str, client: httpx.AsyncClient) → dict[str, str], call:client.post, call:response.raise_for_status, call:response.json, call:payload.get, func:fetch_user_info(settings: Settings, access_token: str, client: httpx.AsyncClient) → dict[str, Any], call:client.get, call:response.raise_for_status, call:response.json | dep: typing, urllib.parse, httpx, src.config
- session.py | Implements creation and verification of signed session cookies using HMAC-SHA256 in a JWS-like format. | exp: func:_base64url_encode(data: bytes) → str, call:base64.urlsafe_b64encode(data).rstrip(b"=").decode, func:_base64url_decode(data: str) → bytes, call:len, call:base64.urlsafe_b64decode, func:create_session_cookie(settings: Settings, user_id: str) → str, call:int, call:(datetime.now(timezone.utc) + timedelta(hours=settings.session_ttl_hours)).timestamp, call:datetime.now, call:timedelta, call:_base64url_encode, call:json.dumps({"alg": "HS256", "typ": "session"}).encode, call:json.dumps(payload).encode, call:hmac.new( settings.session_secret.encode(), message.encode(), hashlib.sha256, ).digest, call:settings.session_secret.encode, call:message.encode, func:decode_session_cookie(settings: Settings, cookie_value: str) → dict[str, Any], call:cookie_value.split, call:len, call:hmac.new( settings.session_secret.encode(), message.encode(), hashlib.sha256, ).digest, call:settings.session_secret.encode, call:message.encode, call:_base64url_encode, call:hmac.compare_digest, call:_base64url_decode, call:json.loads, call:payload.get, call:int, call:datetime.now(timezone.utc).timestamp, raise:ValueError | dep: hmac, hashlib, json, base64, datetime, typing, src.config, src.config.Settings
## arch
Modular utility package with separation of concerns across cookies, sessions, OIDC flows, and FastAPI dependencies; uses HMAC-SHA256 JWS-like tokens and external Authentik identity provider.
## tags
session, src, cookie, user, get, base64url, raise:httpexception, call:
## symbols
- build_cookie_options
- get_db_session
- get_current_user_id
- get_current_user
- _get_user
- _get_owned_project
- build_login_redirect_url
- exchange_code_for_tokens
## workflows
- change auth behavior
read: __init__.py, cookies.py, dependencies.py
## dirty
-
+2
View File
@@ -21,6 +21,7 @@ from src.api.tool import (
sessions_router,
tool_definitions_router,
tool_instances_router,
tool_lifecycle_router,
tool_types_router,
)
from src.api.user import auth_router, ssh_keys_router, users_router
@@ -165,6 +166,7 @@ app.include_router(tool_types_router)
app.include_router(tool_definitions_router)
app.include_router(config_profiles_router)
app.include_router(tool_instances_router)
app.include_router(tool_lifecycle_router)
app.include_router(sessions_router)
app.include_router(instance_proxy_router)
app.include_router(terminal_router)
+37
View File
@@ -0,0 +1,37 @@
# apps/api/src/models (index)
dir: apps/api/src/models
## role
Provides the core database model layer with a centralized public API, defining SQLAlchemy base classes and reusable mixins for consistent schema patterns across the API application.
## parent
index: apps/api/src/.pi-map.index.md
map: apps/api/src/.pi-map.md
## children
- apps/api/src/models/config
index: apps/api/src/models/config/.pi-map.index.md
map: apps/api/src/models/config/.pi-map.md
- apps/api/src/models/project
index: apps/api/src/models/project/.pi-map.index.md
map: apps/api/src/models/project/.pi-map.md
- apps/api/src/models/system
index: apps/api/src/models/system/.pi-map.index.md
map: apps/api/src/models/system/.pi-map.md
- apps/api/src/models/tool
index: apps/api/src/models/tool/.pi-map.index.md
map: apps/api/src/models/tool/.pi-map.md
- apps/api/src/models/user
index: apps/api/src/models/user/.pi-map.index.md
map: apps/api/src/models/user/.pi-map.md
## files
- __init__.py
- base.py
## links
index: apps/api/src/models/.pi-map.index.md
map: apps/api/src/models/.pi-map.md
## workflows
- change models behavior
read: __init__.py, base.py
- explore models subdirectories
index: apps/api/src/models/config/.pi-map.index.md, apps/api/src/models/project/.pi-map.index.md, apps/api/src/models/system/.pi-map.index.md
## dirty
-
+25
View File
@@ -0,0 +1,25 @@
# apps/api/src/models
dir: apps/api/src/models
index: apps/api/src/models/.pi-map.index.md
## role
Provides the core database model layer with a centralized public API, defining SQLAlchemy base classes and reusable mixins for consistent schema patterns across the API application.
## files
- __init__.py | Re-exports model classes from submodules to provide a centralized public API for the src.models package | dep: src.models.base, src.models.config.config_profile, src.models.project.git_repository, src.models.project.project, src.models.project.workspace, src.models.system.health_check, src.models.system.instance_event, src.models.system.notification, src.models.system.terminal_session, src.models.tool.tool_definition_manifest, src.models.tool.tool_instance, src.models.tool.tool_type, src.models.user.ssh_key, src.models.user.user, src.models.user.user_config
- base.py | Defines SQLAlchemy base model and reusable mixins for UUID primary keys and automatic timestamp tracking in database models. | exp: class:Base, class:UUIDPrimaryKeyMixin, class:TimestampMixin | dep: uuid, datetime, sqlalchemy, sqlalchemy.orm
## arch
Uses the declarative base pattern with mixin composition for cross-cutting concerns (UUIDs, timestamps), and a facade/aggregate module pattern via __init__.py re-exports to simplify imports for consumers.
## tags
models, src, base, project, system, user, mixin, tool
## symbols
- Base
- UUIDPrimaryKeyMixin
- TimestampMixin
## workflows
- change models behavior
read: __init__.py, base.py
- explore models subdirectories
index: apps/api/src/models/config/.pi-map.index.md, apps/api/src/models/project/.pi-map.index.md, apps/api/src/models/system/.pi-map.index.md
## dirty
-
@@ -0,0 +1,23 @@
# apps/api/src/models/config (index)
dir: apps/api/src/models/config
## role
Provides SQLAlchemy ORM models for managing configuration profiles and their hierarchical includes in the API application.
## parent
index: apps/api/src/models/.pi-map.index.md
map: apps/api/src/models/.pi-map.md
## children
-
## files
- __init__.py
- config_profile.py
## links
index: apps/api/src/models/config/.pi-map.index.md
map: apps/api/src/models/config/.pi-map.md
## workflows
- change config behavior
read: __init__.py, config_profile.py
- change config config
read: config_profile.py
## dirty
-
+24
View File
@@ -0,0 +1,24 @@
# apps/api/src/models/config
dir: apps/api/src/models/config
index: apps/api/src/models/config/.pi-map.index.md
## role
Provides SQLAlchemy ORM models for managing configuration profiles and their hierarchical includes in the API application.
## files
- __init__.py | Exports config model classes from a subpackage for simplified importing | dep: src.models.config.config_profile
- config_profile.py | Defines SQLAlchemy ORM models for configuration profiles and their includes, storing environment variables, runtime hints, mounts, files, and git mounts with user/project/tool associations and self-referential inclusion relationships. | exp: class:ConfigProfile, class:ConfigProfileInclude | dep: uuid, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models.project, src.models, src.models.user, src.models (ToolType)
## arch
Uses SQLAlchemy declarative ORM pattern with self-referential many-to-many relationships for profile inclusion, and associations to users, projects, and tools.
## tags
config, src, models, profile, sqlalchemy, init, orm, mounts
## symbols
- ConfigProfile
- ConfigProfileInclude
## workflows
- change config behavior
read: __init__.py, config_profile.py
- change config config
read: config_profile.py
## dirty
-
@@ -0,0 +1,23 @@
# apps/api/src/models/project (index)
dir: apps/api/src/models/project
## role
Defines the core data models for project-related entities (GitRepository, Project, Workspace) using SQLAlchemy ORM.
## parent
index: apps/api/src/models/.pi-map.index.md
map: apps/api/src/models/.pi-map.md
## children
-
## files
- __init__.py
- git_repository.py
- project.py
- workspace.py
## links
index: apps/api/src/models/project/.pi-map.index.md
map: apps/api/src/models/project/.pi-map.md
## workflows
- change project behavior
read: __init__.py, git_repository.py, project.py
## dirty
-
+25
View File
@@ -0,0 +1,25 @@
# apps/api/src/models/project
dir: apps/api/src/models/project
index: apps/api/src/models/project/.pi-map.index.md
## role
Defines the core data models for project-related entities (GitRepository, Project, Workspace) using SQLAlchemy ORM.
## files
- __init__.py | Exports the three main project model classes (GitRepository, Project, Workspace) as the public API for the models.project package. | dep: src.models.project.git_repository, src.models.project.project, src.models.project.workspace
- git_repository.py | Defines a SQLAlchemy ORM model for Git repositories with relationships to projects, users, and SSH keys. | exp: class:GitRepository | dep: uuid, datetime, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models.project, src.models, src.models.user, src.models.ssh_key
- project.py | Defines a SQLAlchemy ORM model for a Project entity with relationships to users, Git repositories, and SSH keys. | exp: class:Project | dep: uuid, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models, src.models.user, src.models.GitRepository, src.models.SSHKey, src.models.user.User
- workspace.py | Defines a SQLAlchemy ORM model for persistent writable Git repository clones associated with users. | exp: class:Workspace | dep: uuid, datetime, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models, src.models.user, src.models.GitRepository, src.models.user.User
## arch
Standard SQLAlchemy ORM pattern with declarative models, package-level public API abstraction via __init__.py, and bidirectional relationships between entities.
## tags
models, src, project, sqlalchemy, git, user, workspace, orm
## symbols
- GitRepository
- Project
- Workspace
## workflows
- change project behavior
read: __init__.py, git_repository.py, project.py
## dirty
-
@@ -0,0 +1,24 @@
# apps/api/src/models/system (index)
dir: apps/api/src/models/system
## role
Provides SQLAlchemy ORM models for system-level operational and audit data including health checks, instance events, notifications, and terminal sessions.
## parent
index: apps/api/src/models/.pi-map.index.md
map: apps/api/src/models/.pi-map.md
## children
-
## files
- __init__.py
- health_check.py
- instance_event.py
- notification.py
- terminal_session.py
## links
index: apps/api/src/models/system/.pi-map.index.md
map: apps/api/src/models/system/.pi-map.md
## workflows
- change system behavior
read: __init__.py, health_check.py, instance_event.py
## dirty
-
+27
View File
@@ -0,0 +1,27 @@
# apps/api/src/models/system
dir: apps/api/src/models/system
index: apps/api/src/models/system/.pi-map.index.md
## role
Provides SQLAlchemy ORM models for system-level operational and audit data including health checks, instance events, notifications, and terminal sessions.
## files
- __init__.py | Exports system-related model classes from a package for convenient importing. | dep: src.models.system.health_check, src.models.system.instance_event, src.models.system.notification, src.models.system.terminal_session
- health_check.py | Defines a SQLAlchemy ORM model for storing health check snapshots of tool instances with various status and probe fields. | exp: class:HealthCheck | dep: uuid, datetime, sqlalchemy, sqlalchemy.orm, src.models.base
- instance_event.py | Defines a SQLAlchemy ORM model for auditing instance lifecycle events with metadata, status tracking, and user attribution. | exp: class:InstanceEvent | dep: uuid, datetime, typing, sqlalchemy, sqlalchemy.orm, src.models.base
- notification.py | Defines a SQLAlchemy ORM model for storing user notifications with metadata, read/dismissed tracking, and polymorphic source references. | exp: class:Notification | dep: datetime, typing, uuid, sqlalchemy, sqlalchemy.orm, sqlalchemy.sql, src.models.base
- terminal_session.py | Defines a SQLAlchemy database model for storing terminal session metadata with lifecycle tracking. | exp: class:TerminalSessionModel | dep: uuid, datetime, sqlalchemy, sqlalchemy.orm, src.models.base
## arch
Standard SQLAlchemy declarative ORM pattern with timestamped base models, polymorphic relationships (notification sources), lifecycle state tracking, and audit fields (user attribution, status enums).
## tags
sqlalchemy, src, models, model, orm, system, notification, health
## symbols
- HealthCheck
- InstanceEvent
- Notification
- TerminalSessionModel
## workflows
- change system behavior
read: __init__.py, health_check.py, instance_event.py
## dirty
-
+23
View File
@@ -0,0 +1,23 @@
# apps/api/src/models/tool (index)
dir: apps/api/src/models/tool
## role
Defines SQLAlchemy ORM models for managing tool definitions, instances, and types in a container-based deployment system.
## parent
index: apps/api/src/models/.pi-map.index.md
map: apps/api/src/models/.pi-map.md
## children
-
## files
- __init__.py
- tool_definition_manifest.py
- tool_instance.py
- tool_type.py
## links
index: apps/api/src/models/tool/.pi-map.index.md
map: apps/api/src/models/tool/.pi-map.md
## workflows
- change tool behavior
read: __init__.py, tool_definition_manifest.py, tool_instance.py
## dirty
-
+25
View File
@@ -0,0 +1,25 @@
# apps/api/src/models/tool
dir: apps/api/src/models/tool
index: apps/api/src/models/tool/.pi-map.index.md
## role
Defines SQLAlchemy ORM models for managing tool definitions, instances, and types in a container-based deployment system.
## files
- __init__.py | Exports the public API for the tool models module by re-exporting three key classes. | dep: src.models.tool.tool_definition_manifest, src.models.tool.tool_instance, src.models.tool.tool_type
- tool_definition_manifest.py | Defines a SQLAlchemy ORM model for storing tool definition manifests that compile to Dockerfiles and Compose files, supporting both base definitions and tool-specific definitions with inheritance. | exp: class:ToolDefinitionManifest | dep: uuid, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models.user
- tool_instance.py | Defines a SQLAlchemy ORM model for tool instances that represent deployed tools with container/runtime metadata and relationships to users, projects, repositories, and workspaces. | exp: class:ToolInstance | dep: uuid, datetime, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models, src.models.project, src.models.user, src.models (ConfigProfile, GitRepository, Project, ToolType, User, Workspace)
- tool_type.py | Defines a SQLAlchemy ORM model for tool types that represent configurable categories of tools with deployment templates, manifest references, and metadata. | exp: class:ToolType | dep: uuid, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models.tool.tool_definition_manifest, src.models.user
## arch
Domain-driven SQLAlchemy ORM models with inheritance patterns (base/tool-specific manifests), rich relationships (users, projects, repositories, workspaces), and template-based deployment abstractions.
## tags
tool, models, src, sqlalchemy, orm, definition, manifest, base
## symbols
- ToolDefinitionManifest
- ToolInstance
- ToolType
## workflows
- change tool behavior
read: __init__.py, tool_definition_manifest.py, tool_instance.py
## dirty
-
+4 -2
View File
@@ -53,9 +53,11 @@ class ToolInstance(UUIDPrimaryKeyMixin, TimestampMixin, Base):
)
image_tag: Mapped[str | None] = mapped_column(String(256), nullable=True)
probe_result: Mapped[dict | None] = mapped_column(JSON, nullable=True)
clone_mode: Mapped[str] = mapped_column(String(20), nullable=False, default="mount")
clone_mode: Mapped[str | None] = mapped_column(
String(20), nullable=True, default=None
)
branch: Mapped[str | None] = mapped_column(
String(255), nullable=True, default="main"
String(255), nullable=True, default=None
)
selected_config_profile_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(), ForeignKey("config_profiles.id", ondelete="SET NULL"), nullable=True
+25
View File
@@ -0,0 +1,25 @@
# apps/api/src/models/user (index)
dir: apps/api/src/models/user
## role
Provides the core data models for user identity, authentication credentials, and personal configuration in the API's database layer.
## parent
index: apps/api/src/models/.pi-map.index.md
map: apps/api/src/models/.pi-map.md
## children
-
## files
- __init__.py
- ssh_key.py
- user.py
- user_config.py
## links
index: apps/api/src/models/user/.pi-map.index.md
map: apps/api/src/models/user/.pi-map.md
## workflows
- change user behavior
read: __init__.py, ssh_key.py, user.py
- change user config
read: user_config.py
## dirty
-
+27
View File
@@ -0,0 +1,27 @@
# apps/api/src/models/user
dir: apps/api/src/models/user
index: apps/api/src/models/user/.pi-map.index.md
## role
Provides the core data models for user identity, authentication credentials, and personal configuration in the API's database layer.
## files
- __init__.py | Exports the user-related model classes (SSHKey, User, UserConfig) from a package for convenient importing. | dep: src.models.user.ssh_key, src.models.user.user, src.models.user.user_config
- ssh_key.py | Defines an SSHKey SQLAlchemy ORM model for storing encrypted SSH key pairs with user and optional project associations. | exp: class:SSHKey | dep: uuid, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models.project, src.models.user
- user.py | Defines a SQLAlchemy ORM model for a User entity with authentication fields and relationships to projects, SSH keys, and user configuration. | exp: class:User | dep: typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models.project, src.models, src.models.SSHKey, src.models.UserConfig
- user_config.py | Defines a SQLAlchemy model for storing per-user JSON configuration with typed property accessors for default profile IDs. | exp: class:UserConfig | dep: uuid, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models.user
## arch
Follows SQLAlchemy ORM declarative model pattern with package-level aggregation via __init__.py, using explicit table relationships (foreign keys, one-to-many associations) and a JSON-backed configuration store with typed property accessors.
## tags
user, src, models, sqlalchemy, sshkey, orm, model, ssh
## symbols
- SSHKey
- User
- UserConfig
## workflows
- change user behavior
read: __init__.py, ssh_key.py, user.py
- change user config
read: user_config.py
## dirty
-
+36
View File
@@ -0,0 +1,36 @@
# apps/api/src/schemas (index)
dir: apps/api/src/schemas
## role
Defines Pydantic schemas for data validation, serialization, and API request/response models in the FastAPI application.
## parent
index: apps/api/src/.pi-map.index.md
map: apps/api/src/.pi-map.md
## children
- apps/api/src/schemas/config
index: apps/api/src/schemas/config/.pi-map.index.md
map: apps/api/src/schemas/config/.pi-map.md
- apps/api/src/schemas/project
index: apps/api/src/schemas/project/.pi-map.index.md
map: apps/api/src/schemas/project/.pi-map.md
- apps/api/src/schemas/system
index: apps/api/src/schemas/system/.pi-map.index.md
map: apps/api/src/schemas/system/.pi-map.md
- apps/api/src/schemas/tool
index: apps/api/src/schemas/tool/.pi-map.index.md
map: apps/api/src/schemas/tool/.pi-map.md
- apps/api/src/schemas/user
index: apps/api/src/schemas/user/.pi-map.index.md
map: apps/api/src/schemas/user/.pi-map.md
## files
- __init__.py
## links
index: apps/api/src/schemas/.pi-map.index.md
map: apps/api/src/schemas/.pi-map.md
## workflows
- change schemas behavior
read: __init__.py
- explore schemas subdirectories
index: apps/api/src/schemas/config/.pi-map.index.md, apps/api/src/schemas/project/.pi-map.index.md, apps/api/src/schemas/system/.pi-map.index.md
## dirty
-
+22
View File
@@ -0,0 +1,22 @@
# apps/api/src/schemas
dir: apps/api/src/schemas
index: apps/api/src/schemas/.pi-map.index.md
## role
Defines Pydantic schemas for data validation, serialization, and API request/response models in the FastAPI application.
## files
- __init__.py | Empty file with no functionality
## arch
Modular schema organization following FastAPI/Pydantic patterns, with empty __init__.py indicating package initialization for future schema modules.
## tags
init, empty, functionality
## symbols
-
## workflows
- change schemas behavior
read: __init__.py
- explore schemas subdirectories
index: apps/api/src/schemas/config/.pi-map.index.md, apps/api/src/schemas/project/.pi-map.index.md, apps/api/src/schemas/system/.pi-map.index.md
## dirty
-
View File
@@ -0,0 +1,23 @@
# apps/api/src/schemas/config (index)
dir: apps/api/src/schemas/config
## role
Provides Pydantic validation schemas for configuration profile API endpoints, ensuring data integrity for mounts, git URLs, UUIDs, and file paths.
## parent
index: apps/api/src/schemas/.pi-map.index.md
map: apps/api/src/schemas/.pi-map.md
## children
-
## files
- __init__.py
- config_profile.py
## links
index: apps/api/src/schemas/config/.pi-map.index.md
map: apps/api/src/schemas/config/.pi-map.md
## workflows
- change config behavior
read: __init__.py, config_profile.py
- change config config
read: config_profile.py
## dirty
-
+30
View File
@@ -0,0 +1,30 @@
# apps/api/src/schemas/config
dir: apps/api/src/schemas/config
index: apps/api/src/schemas/config/.pi-map.index.md
## role
Provides Pydantic validation schemas for configuration profile API endpoints, ensuring data integrity for mounts, git URLs, UUIDs, and file paths.
## files
- __init__.py | Re-exports Pydantic schemas for configuration profile operations from a submodule to provide a clean public API. | dep: src.schemas.config.config_profile
- config_profile.py | Defines Pydantic request/response schemas for config profile API operations with validation for mounts, git URLs, UUIDs, and file paths. | exp: class:GitMountMapping, class:GitMountItem, class:MountItem, class:ConfigProfileCreate, class:ConfigProfileUpdate, class:ConfigProfileIncludeUpdate, class:ConfigProfileResponse, class:DefaultProfilesUpdate, class:ValidateGitUrlRequest, class:ValidateGitUrlResponse, func:_validate_uuid(v: str | None) → str | None, call:uuid.UUID, raise:ValueError | dep: uuid, pydantic, src.api.shared_validators
## arch
Simple schema-as-code pattern using Pydantic models with field validators; clean public API via explicit re-exports in `__init__.py`.
## tags
profile, config, git, mount, update, validate, response, item
## symbols
- GitMountMapping
- GitMountItem
- MountItem
- ConfigProfileCreate
- ConfigProfileUpdate
- ConfigProfileIncludeUpdate
- ConfigProfileResponse
- DefaultProfilesUpdate
## workflows
- change config behavior
read: __init__.py, config_profile.py
- change config config
read: config_profile.py
## dirty
-
@@ -0,0 +1,23 @@
# apps/api/src/schemas/project (index)
dir: apps/api/src/schemas/project
## role
Provides Pydantic data validation schemas for project-related API endpoints including projects, Git repositories, and SSH keys.
## parent
index: apps/api/src/schemas/.pi-map.index.md
map: apps/api/src/schemas/.pi-map.md
## children
-
## files
- __init__.py
- git_repository.py
- project.py
- ssh_key.py
## links
index: apps/api/src/schemas/project/.pi-map.index.md
map: apps/api/src/schemas/project/.pi-map.md
## workflows
- change project behavior
read: __init__.py, git_repository.py, project.py
## dirty
-
+30
View File
@@ -0,0 +1,30 @@
# apps/api/src/schemas/project
dir: apps/api/src/schemas/project
index: apps/api/src/schemas/project/.pi-map.index.md
## role
Provides Pydantic data validation schemas for project-related API endpoints including projects, Git repositories, and SSH keys.
## files
- __init__.py | Re-exports Pydantic schema classes from submodules to provide a unified public API for the project schemas module | dep: src.schemas.project.git_repository, src.schemas.project.project, src.schemas.project.ssh_key
- git_repository.py | Defines Pydantic request/response schemas for Git repository operations in an API. | exp: class:GitRepositoryCreate, class:URLParseRequest, class:URLParseResponse, class:GitRepositoryResponse, class:UpdateSSHKeyRequest, class:FileListResponse, class:FileContentResponse, class:BranchesResponse, class:FileUpdateRequest, class:FileUpdateResponse, class:StatusResponse, class:BranchCreateRequest, class:CheckoutRequest, class:CommitRequest, class:CommitResponse, class:FetchResponse, class:PullResponse, class:PushResponse, class:MergeRequest, class:MergeResponse | dep: uuid, datetime, pydantic
- project.py | Defines Pydantic request/response schemas for project-related API operations. | exp: class:ProjectCreate, class:ProjectUpdate, class:ProjectResponse, class:SetDefaultSSHKeyRequest | dep: uuid, pydantic
- ssh_key.py | Defines Pydantic schemas for SSH key creation, response, signing, and signature verification in an API. | exp: class:SSHKeyCreate, class:SSHKeyResponse, class:SignPayloadRequest, class:SignatureResponse, class:VerifySignatureRequest, class:VerifySignatureResponse | dep: uuid, datetime, pydantic
## arch
Modular schema-per-domain pattern with clean separation of concerns, using Pydantic models for request/response validation and an `__init__.py` facade for unified public API access.
## tags
response, request, project, create, update, sshkey, pydantic, schemas
## symbols
- GitRepositoryCreate
- URLParseRequest
- URLParseResponse
- GitRepositoryResponse
- UpdateSSHKeyRequest
- FileListResponse
- FileContentResponse
- BranchesResponse
## workflows
- change project behavior
read: __init__.py, git_repository.py, project.py
## dirty
-
@@ -45,3 +45,91 @@ class GitRepositoryResponse(BaseModel):
class UpdateSSHKeyRequest(BaseModel):
ssh_key_id: str | None = None
class FileListResponse(BaseModel):
path: str
branch: str
entries: list[dict]
class FileContentResponse(BaseModel):
path: str
branch: str
content: str
size: int
encoding: str
language: str | None
is_binary: bool
last_commit: dict | None
class BranchesResponse(BaseModel):
branches: list[dict]
default_branch: str
class FileUpdateRequest(BaseModel):
path: str
branch: str
content: str
commit_message: str
class FileUpdateResponse(BaseModel):
commit_hash: str
message: str
branch: str
class StatusResponse(BaseModel):
branch: str
modified: list[str]
added: list[str]
deleted: list[str]
untracked: list[str]
renamed: list[str]
ahead: int
behind: int
class BranchCreateRequest(BaseModel):
name: str
base_branch: str = "HEAD"
class CheckoutRequest(BaseModel):
branch: str
class CommitRequest(BaseModel):
message: str
files: list[str] | None = None
class CommitResponse(BaseModel):
commit_hash: str
message: str
class FetchResponse(BaseModel):
message: str
class PullResponse(BaseModel):
message: str
class PushResponse(BaseModel):
message: str
class MergeRequest(BaseModel):
source_branch: str
target_branch: str | None = None
message: str | None = None
class MergeResponse(BaseModel):
commit_hash: str
message: str
@@ -0,0 +1,21 @@
# apps/api/src/schemas/system (index)
dir: apps/api/src/schemas/system
## role
Provides Pydantic schemas for system health monitoring endpoints in the API.
## parent
index: apps/api/src/schemas/.pi-map.index.md
map: apps/api/src/schemas/.pi-map.md
## children
-
## files
- __init__.py
- health.py
## links
index: apps/api/src/schemas/system/.pi-map.index.md
map: apps/api/src/schemas/system/.pi-map.md
## workflows
- change system behavior
read: __init__.py, health.py
## dirty
-
+25
View File
@@ -0,0 +1,25 @@
# apps/api/src/schemas/system
dir: apps/api/src/schemas/system
index: apps/api/src/schemas/system/.pi-map.index.md
## role
Provides Pydantic schemas for system health monitoring endpoints in the API.
## files
- __init__.py | Re-exports health check schema classes from a submodule to provide a unified interface for the system schemas module. | dep: src.schemas.system.health
- health.py | Defines Pydantic data models for health check API responses including database, disk, and overall system health status. | exp: class:DatabaseHealth, class:DiskHealth, class:HealthChecks, class:HealthResponse, class:DatabaseHealthResponse | dep: pydantic
## arch
Simple submodule pattern with explicit re-exports in __init__.py, using Pydantic models for structured API response validation.
## tags
health, database, response, system, disk, check, schemas, init
## symbols
- DatabaseHealth
- DiskHealth
- HealthChecks
- HealthResponse
- DatabaseHealthResponse
## workflows
- change system behavior
read: __init__.py, health.py
## dirty
-
@@ -0,0 +1,22 @@
# apps/api/src/schemas/tool (index)
dir: apps/api/src/schemas/tool
## role
Provides Pydantic data validation schemas for tool-related API operations including tool types and instances.
## parent
index: apps/api/src/schemas/.pi-map.index.md
map: apps/api/src/schemas/.pi-map.md
## children
-
## files
- __init__.py
- tool_instance.py
- tool_type.py
## links
index: apps/api/src/schemas/tool/.pi-map.index.md
map: apps/api/src/schemas/tool/.pi-map.md
## workflows
- change tool behavior
read: __init__.py, tool_instance.py, tool_type.py
## dirty
-
+27
View File
@@ -0,0 +1,27 @@
# apps/api/src/schemas/tool
dir: apps/api/src/schemas/tool
index: apps/api/src/schemas/tool/.pi-map.index.md
## role
Provides Pydantic data validation schemas for tool-related API operations including tool types and instances.
## files
- __init__.py | Re-exports tool-related schema classes from submodules to provide a unified import interface for the tool schemas module. | dep: src.schemas.tool.tool_instance, src.schemas.tool.tool_type
- tool_instance.py | Defines Pydantic request/response schemas for tool instance lifecycle operations (create and start). | exp: class:CreateInstanceRequest, class:StartInstanceRequest | dep: pydantic
- tool_type.py | Defines Pydantic request/response schemas for creating, updating, and validating tool types with Docker compose/dockerfile/manifest definitions | exp: class:ToolTypeCreate, class:ToolTypeUpdate, class:ToolTypeResponse, class:ToolTypeValidateRequest | dep: uuid, datetime, pydantic, src.api.tool.tool_types_validation
## arch
Modular schema organization with submodule-per-entity pattern and unified re-export interface via `__init__.py`, following standard Python package structure for clean API imports.
## tags
tool, type, request, instance, schemas, create, pydantic, response
## symbols
- CreateInstanceRequest
- StartInstanceRequest
- ToolTypeCreate
- ToolTypeUpdate
- ToolTypeResponse
- ToolTypeValidateRequest
## workflows
- change tool behavior
read: __init__.py, tool_instance.py, tool_type.py
## dirty
-
+6 -1
View File
@@ -1,6 +1,10 @@
"""Tool schemas module."""
from src.schemas.tool.tool_instance import CreateInstanceRequest, StartInstanceRequest
from src.schemas.tool.tool_instance import (
CreateInstanceRequest,
CreateWorkspaceInstanceRequest,
StartInstanceRequest,
)
from src.schemas.tool.tool_type import (
ToolTypeCreate,
ToolTypeResponse,
@@ -10,6 +14,7 @@ from src.schemas.tool.tool_type import (
__all__ = [
"CreateInstanceRequest",
"CreateWorkspaceInstanceRequest",
"StartInstanceRequest",
"ToolTypeCreate",
"ToolTypeResponse",
+15 -7
View File
@@ -13,16 +13,24 @@ class CreateInstanceRequest(BaseModel):
default=None, description="Optional display name for the instance"
)
workspace_id: str | None = Field(
default=None, description="UUID of workspace to mount (replaces clone_mode)"
default=None, description="UUID of workspace to mount"
)
clone_mode: str = Field(
default="mount", description="Repository access mode: 'mount' or 'clone'"
config_profile_id: str | None = Field(
default=None, description="Optional config profile ID for launch"
)
branch: str | None = Field(
default="main", description="Branch to clone (when clone_mode='clone')"
ssh_key_ids: list[str] = Field(
default_factory=list, description="SSH key IDs to mount into container ~/.ssh"
)
new_branch: str | None = Field(
default=None, description="Create a new local branch after cloning"
class CreateWorkspaceInstanceRequest(BaseModel):
"""Request body for creating a tool instance directly on a workspace."""
model_config = {"extra": "ignore"}
tool_type_id: str = Field(description="UUID of the tool type to instantiate")
display_name: str | None = Field(
default=None, description="Optional display name for the instance"
)
config_profile_id: str | None = Field(
default=None, description="Optional config profile ID for launch"
@@ -0,0 +1,24 @@
# apps/api/src/schemas/user (index)
dir: apps/api/src/schemas/user
## role
Provides centralized Pydantic data validation schemas for user-related API requests and responses.
## parent
index: apps/api/src/schemas/.pi-map.index.md
map: apps/api/src/schemas/.pi-map.md
## children
-
## files
- __init__.py
- user.py
- user_config.py
## links
index: apps/api/src/schemas/user/.pi-map.index.md
map: apps/api/src/schemas/user/.pi-map.md
## workflows
- change user behavior
read: __init__.py, user.py, user_config.py
- change user config
read: user_config.py
## dirty
-
+27
View File
@@ -0,0 +1,27 @@
# apps/api/src/schemas/user
dir: apps/api/src/schemas/user
index: apps/api/src/schemas/user/.pi-map.index.md
## role
Provides centralized Pydantic data validation schemas for user-related API requests and responses.
## files
- __init__.py | Re-exports user-related Pydantic schemas from submodules to provide a unified public API for the user schemas package. | dep: src.schemas.user.user, src.schemas.user.user_config
- user.py | Defines Pydantic response and update schemas for user profile data | exp: class:UserProfileResponse, class:UserProfileUpdate | dep: uuid, pydantic
- user_config.py | Defines Pydantic models for user configuration data transfer objects (response and update schemas). | exp: class:UserConfigResponse, class:UserConfigUpdate | dep: pydantic
## arch
Modular package pattern with submodule separation of concerns (user profile vs. user config) and unified public API via `__init__.py` re-exports.
## tags
user, schemas, response, update, config, pydantic, profile, src
## symbols
- UserProfileResponse
- UserProfileUpdate
- UserConfigResponse
- UserConfigUpdate
## workflows
- change user behavior
read: __init__.py, user.py, user_config.py
- change user config
read: user_config.py
## dirty
-
+21
View File
@@ -0,0 +1,21 @@
# apps/api/src/scripts (index)
dir: apps/api/src/scripts
## role
Provides utility scripts for administrative and setup tasks for the API application.
## parent
index: apps/api/src/.pi-map.index.md
map: apps/api/src/.pi-map.md
## children
-
## files
- __init__.py
- seed.py
## links
index: apps/api/src/scripts/.pi-map.index.md
map: apps/api/src/scripts/.pi-map.md
## workflows
- change scripts behavior
read: __init__.py, seed.py
## dirty
-
+28
View File
@@ -0,0 +1,28 @@
# apps/api/src/scripts
dir: apps/api/src/scripts
index: apps/api/src/scripts/.pi-map.index.md
## role
Provides utility scripts for administrative and setup tasks for the API application.
## files
- __init__.py | Marks the directory as a Python package and provides a docstring describing it as utility scripts for the API package.
- seed.py | Seeds a development user into the database using SQLAlchemy async operations, with idempotency check to avoid duplicates. | exp: func:build_seed_user() → Mapping[str, str | None], func:seed_database(session: AsyncSession) → User, call:build_seed_user, call:session.scalar, call:select(User).where, call:User, call:session.add, call:session.commit, call:session.refresh, func:run() → None, call:SessionLocal, call:seed_database, call:print, call:str, func:main() → None, call:asyncio.run, call:run | dep: collections.abc, sqlalchemy, sqlalchemy.ext.asyncio, src.database, src.models.user, asyncio
## arch
Simple procedural scripts using async SQLAlchemy for database operations, with idempotency guards for safe re-runs.
## tags
seed, user, database, sqlalchemy, package, init, call:build, call:session.scalar
## symbols
- build_seed_user
- seed_database
- run
- main
- call:build_seed_user
- call:session.scalar
- call:select(User).where
- call:User
## workflows
- change scripts behavior
read: __init__.py, seed.py
## dirty
-
+21
View File
@@ -0,0 +1,21 @@
# apps/api/src/seeds (index)
dir: apps/api/src/seeds
## role
Provides database seeding utilities for initializing built-in tool types in the API application.
## parent
index: apps/api/src/.pi-map.index.md
map: apps/api/src/.pi-map.md
## children
-
## files
- __init__.py
- builtin_tool_types.py
## links
index: apps/api/src/seeds/.pi-map.index.md
map: apps/api/src/seeds/.pi-map.md
## workflows
- change seeds behavior
read: __init__.py, builtin_tool_types.py
## dirty
-
+28
View File
@@ -0,0 +1,28 @@
# apps/api/src/seeds
dir: apps/api/src/seeds
index: apps/api/src/seeds/.pi-map.index.md
## role
Provides database seeding utilities for initializing built-in tool types in the API application.
## files
- __init__.py | Marks the directory as a Python package for database seeding utilities.
- builtin_tool_types.py | Seeds built-in Docker Compose-based tool types (VS Code Server, Jupyter Notebook, OpenCode) into a database with idempotent create-or-update logic. | exp: func:_table_exists(session, table_name: str) → bool, call:session.execute, call:text, call:result.scalar, func:seed_builtin_tool_types(), call:SessionLocal, call:_table_exists, call:logger.warning, call:session.scalar, call:select(ToolType).where, call:ToolType, call:tool_data.get, call:session.add, call:logger.info, call:session.commit | dep: logging, sqlalchemy, src.database, src.models
## arch
Simple procedural seeding scripts with idempotent create-or-update logic using direct database operations, organized as a Python package with one seed module per entity type.
## tags
tool, types, table, exists, builtin, call:tool, database, init
## symbols
- _table_exists
- seed_builtin_tool_types
- call:session.execute
- call:text
- call:result.scalar
- call:SessionLocal
- call:_table_exists
- call:logger.warning
## workflows
- change seeds behavior
read: __init__.py, builtin_tool_types.py
## dirty
-
+42
View File
@@ -0,0 +1,42 @@
# apps/api/src/services (index)
dir: apps/api/src/services
## role
Initializes the services directory as a Python package for business logic modules.
## parent
index: apps/api/src/.pi-map.index.md
map: apps/api/src/.pi-map.md
## children
- apps/api/src/services/config
index: apps/api/src/services/config/.pi-map.index.md
map: apps/api/src/services/config/.pi-map.md
- apps/api/src/services/docker
index: apps/api/src/services/docker/.pi-map.index.md
map: apps/api/src/services/docker/.pi-map.md
- apps/api/src/services/git
index: apps/api/src/services/git/.pi-map.index.md
map: apps/api/src/services/git/.pi-map.md
- apps/api/src/services/instance
index: apps/api/src/services/instance/.pi-map.index.md
map: apps/api/src/services/instance/.pi-map.md
- apps/api/src/services/shared
index: apps/api/src/services/shared/.pi-map.index.md
map: apps/api/src/services/shared/.pi-map.md
- apps/api/src/services/terminal
index: apps/api/src/services/terminal/.pi-map.index.md
map: apps/api/src/services/terminal/.pi-map.md
- apps/api/src/services/tool
index: apps/api/src/services/tool/.pi-map.index.md
map: apps/api/src/services/tool/.pi-map.md
## files
- __init__.py
## links
index: apps/api/src/services/.pi-map.index.md
map: apps/api/src/services/.pi-map.md
## workflows
- change services behavior
read: __init__.py
- explore services subdirectories
index: apps/api/src/services/config/.pi-map.index.md, apps/api/src/services/docker/.pi-map.index.md, apps/api/src/services/git/.pi-map.index.md
## dirty
-
+22
View File
@@ -0,0 +1,22 @@
# apps/api/src/services
dir: apps/api/src/services
index: apps/api/src/services/.pi-map.index.md
## role
Initializes the services directory as a Python package for business logic modules.
## files
- __init__.py | Empty file with no functionality
## arch
Standard Python package structure using empty __init__.py for namespace declaration.
## tags
init, empty, functionality
## symbols
-
## workflows
- change services behavior
read: __init__.py
- explore services subdirectories
index: apps/api/src/services/config/.pi-map.index.md, apps/api/src/services/docker/.pi-map.index.md, apps/api/src/services/git/.pi-map.index.md
## dirty
-
View File
@@ -172,10 +172,19 @@ def compile_dockerfile(manifest: dict) -> str:
lines.append(f"ENV HOME={home}")
lines.append(f"ENV USER={name}")
lines.append("")
# Ensure home directory exists and is writable by the user
# Ensure home directory exists and is writable by the user.
# Recursively chown so any files copied from /etc/skel by useradd -m
# (e.g. .bashrc, .config) are owned by the container user.
lines.append(
f"RUN mkdir -p {home} && chown {name}:{name} {home} && chmod 755 {home}"
f"RUN mkdir -p {home} && chown -R {name}:{name} {home} && chmod 755 {home}"
)
# Pre-create common config directories so apps like ranger can write
# their configs on first run without permission errors.
common_dirs = [".config", ".local/share", ".cache"]
for d in common_dirs:
lines.append(
f"RUN mkdir -p {home}/{d} && chown -R {name}:{name} {home}/{d}"
)
lines.append("")
# Configure passwordless sudo so startup scripts can fix permissions

Some files were not shown because too many files have changed in this diff Show More