Compare commits

...

18 Commits

Author SHA1 Message Date
Developer bb38b37ceb fix: reinitialize terminal on mobile viewport changes
Recreate the terminal when responsive classification changes so mobile scrollback and touch listeners are installed.\n\nOpenSpec: fix-mobile-terminal-scrolling\nQuality gates: npm run typecheck, npm run lint, npm test (89 passed), npm run build
2026-07-14 19:41:34 +00:00
Developer 6698c20f25 fix: restore mobile terminal scrolling
- Retain xterm normal-buffer history on mobile while preserving desktop zero-scrollback behavior\n- Repair ProjectsPage tests for session context and current project list markup\n- Add focused terminal scrollback coverage\n\nOpenSpec: fix-mobile-terminal-scrolling\nQuality gates: npm run typecheck, npm run lint, npm test (89 passed), npm run build
2026-07-14 19:07:02 +00:00
Developer 5e06a2a226 Merge fix/terminal-paste-partial-writes into dev 2026-07-14 10:34:33 +00:00
Developer 49180e4c6d fix: fully drain terminal paste writes to PTY
- Make the host PTY master non-blocking and wait for write readiness.
- Retry partial writes so a large bracketed paste always delivers its closing
  marker instead of leaving pi in paste mode.
- Add regression coverage for partial PTY writes and resolve diagnostics.

Quality gates: ruff, mypy, focused pytest (3 passed)
2026-07-14 10:34:33 +00:00
Developer 7a5538b53f Merge fix/web-terminal-bracketed-paste into dev 2026-07-14 10:02:30 +00:00
Developer 41d24beade fix: forward browser terminal pastes as bracketed input
- Track application bracketed-paste mode from terminal output.
- Capture browser and mobile clipboard pastes only while that mode is enabled,
  normalizing line endings and sending a single BPM-framed input event.
- Remove the synchronous output decoding and console diagnostics that could
  stall terminal rendering under output load.
- Keep the terminal's zero-scrollback configuration and resolve existing
  no-case-declarations lint blockers in terminal keyboard shortcuts.

Quality gates: npm run typecheck, npm run lint
2026-07-14 10:02:30 +00:00
Developer d1777b88ad Merge fix/terminal-paste-bpm into dev 2026-07-13 13:42:56 +00:00
Developer 691bbaa87b fix: restore web terminal multiline paste by reverting scrollback to 10000
- Revert xterm.js scrollback from 0 to 10000; empirical correlation with
  bracketed-paste mode failing (each line submitted as separate command).
- Add temporary browser-console diagnostics to confirm whether xterm receives
  pi's \e[?2004h enable sequence and whether outbound paste is BPM-wrapped.
- Keep CSS scrollbar hiding and wheel-sensitivity 0 so the original scroll-jank
  fix remains effective.

Typecheck: passed (apps/web)
2026-07-13 13:42:56 +00:00
Developer 6191565e80 Merge branch 'fix/terminal-tui-scrollbar' into dev 2026-07-11 12:07:58 +00:00
Developer 468f34202a fix(terminal): hide scrollbar and stop stale-frame wheel scroll for TUI tools
The web terminal only hosts full-screen TUI tools (pi-agent, opencode),
which repaint in place in the normal buffer and do not use the alternate
screen or mouse tracking. With scrollback enabled, every repaint
accumulated as history, so xterm's viewport scrollbar appeared and the
mouse-wheel scrolled through stale frames instead of interacting with the
app; the scrollbar column also perturbed FitAddon's column count.

- terminal.tsx: set scrollback:0 and scrollSensitivity/fastScrollSensitivity:0
  so only the live viewport is kept (no bar, no stale-frame wheel jank).
- utilities.css: hide .xterm-viewport scrollbar (scrollbar-width:none +
  ::-webkit-scrollbar display:none) as belt-and-suspenders.

Wheel no longer scrolls stale frames; in-app scrolling uses the tool's own
keys. Headquarter-only change; no tool is touched.
2026-07-11 12:07:47 +00:00
Developer 996a858892 Merge branch 'fix/terminal-pong-leak' into dev 2026-07-11 11:49:01 +00:00
Developer ff7fcb6e8d fix(terminal): stop heartbeat pong leaking into the PTY as input
The WebSocket control-frame heuristic only recognized resize/ack/reset,
so the frontend's heartbeat reply {"type":"pong"} fell through to
write_input() and was typed into the shell / pi every ~30s. That garbage
corrupted the foreground app: stray text in the input line, rerenders,
and scroll-position resets (visible on resize/scroll redraws).

Treat any text frame that parses to a JSON object carrying a "type" field
as control traffic that must NEVER reach the PTY: handle known types and
ignore unknown ones. Keystrokes, bracketed-paste content, and plain text
are still forwarded as raw input.
2026-07-11 11:48:48 +00:00
Developer 3f06224b75 Merge branch 'fix/web-terminal-multiline-paste' into dev 2026-07-11 11:32:49 +00:00
Developer a1a77c99a6 fix: enable multiline paste in web terminal
Multiline pastes into the web terminal (especially into pi) were split
into one prompt per line because bracketed-paste markers were not
reaching the foreground app intact.

- Put the host PTY into raw mode (tty.setraw) after openpty() so it acts
  as a pass-through pipe. The default canonical line discipline was
  line-buffering input, splitting multiline pastes at newlines, and
  mangling bracketed-paste markers before docker exec / pi could see
  them. The in-container PTY (docker exec -t) provides real discipline.
- Route the mobile Paste button through xterm.js (term.paste) instead of
  sending raw clipboard text to the WebSocket, so content is wrapped in
  bracketed-paste markers when the app has enabled BPM.
- Treat a text frame as a control message only when it is a JSON object
  with a known type (resize/ack/reset); otherwise forward as raw input
  so JSON-shaped pastes are no longer silently dropped.

Quality gates: ruff, mypy (changed files), pytest unit (227 passed),
tsc, eslint
2026-07-11 11:32:23 +00:00
alex 3c96c7b153 Merge branch 'fix/remove-workspace-working-dir' into dev 2026-06-19 12:10:48 +02:00
alex 10955dfe8e fix(tool): stop defaulting manifest working_dir to /workspace
The /workspace compatibility symlink was removed from the manifest
compiler/entrypoint in 3e59a25. Tool definitions that still set
runtime.working_dir to /workspace therefore start in an empty directory
instead of /home/user/{repo_name}.

- manifest-editor.tsx: default working_dir to empty instead of /workspace;
  update startup-script placeholder to reference /home/alex/.
- Add Alembic migration 2026_06_19_113000 that clears the stale
  runtime.working_dir = /workspace from the built-in pi-agent manifest.
- Add migration import test.

Quality gates: pytest tests/api tests/services/test_terminal_manager_multi.py tests/unit (248 passed), ruff check (clean), npx tsc --noEmit (clean), eslint (clean).
2026-06-19 12:10:42 +02:00
alex 6d7f538a79 Merge branch 'fix/terminal-manifest-lazyload' into dev 2026-06-19 11:48:59 +02:00
alex b32fea671f fix(terminal): avoid lazy-loading tool manifest in async websocket handler
The container-user resolver introduced in 9f72093 accessed
'tool_type.manifest', which triggers a SQLAlchemy lazy load inside the
async WebSocket coroutine and raises MissingGreenlet. Fetch the manifest
explicitly with db_session.get() instead, matching the pattern used in
instance_service.py.

- Replace relationship access with explicit async loads in
  _resolve_container_user().
- Add unit tests covering manifest, base-definition, legacy, and missing
  manifest cases.
- Update project map artifacts.

Quality gates: pytest tests/api tests/services/test_terminal_manager_multi.py tests/unit (247 passed), ruff check (clean).
2026-06-19 11:48:53 +02:00
46 changed files with 1667 additions and 1122 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ dir: .
Trust boundary: index routes, map orients, source decides.
## role
A self-hosted development platform that manages projects, git repositories, and development tools with OAuth2 authentication, containerized via Docker Compose.
Package .
## parent
-
## children
+3 -3
View File
@@ -18,7 +18,7 @@ index: ./.pi-map.index.md
Trust boundary: index routes, map orients, source decides.
## role
A self-hosted development platform that manages projects, git repositories, and development tools with OAuth2 authentication, containerized via Docker Compose.
Package .
## files
- .env.example | Provides example environment variable configuration for a full-stack application with database, caching, authentication, and deployment settings
- .gitignore | Specifies files and directories for Git to ignore across a project using Beads/Dolt, Python, Node, and various IDE/OS tooling. | dep: git, dolt, beads, python, node, npm, yarn, pnpm, pytest, mypy, ruff, coverage
@@ -32,9 +32,9 @@ A self-hosted development platform that manages projects, git repositories, and
- swap-pane | Provides a command to swap the position of two tmux panes within a window or between windows | dep: tmux, client, window, layout, cmd-find, cmd-parse, options
- ui-audit-spacing-typography.md | A detailed audit report identifying critical CSS styling gaps, mobile layout failures, and design system inconsistencies in a web application's UI components and stylesheets. | dep: React/TSX components, CSS stylesheets (global.css, utilities.css, tokens.css, page-specific CSS), JSX/TSX files in apps/web/src/components and apps/web/src/pages
- ui-rework-foundations-apply.md | Documents the implementation of Pass 1 (Foundations) of a web UI spacing/typography/visual-rhythm rework, including design token expansion, primitive CSS class additions, component refactors, and OpenSpec documentation. | dep: CSS design tokens, React/TSX components, OpenSpec documentation system, Git/SSH, npm build toolchain
- ui-rework-pass2-apply.md | Documents the implementation of Pass 2 of a web UI spacing/typography rework, refactoring inline styles into token-based utility classes across multiple components. | dep: React/TSX components, CSS custom properties, OpenSpec documentation system, git workflow
- ui-rework-pass2-apply.md | Documents the implementation and verification of Pass 2 of a web UI spacing/typography rework, refactoring inline styles into utility classes and design tokens across six components. | dep: CSS custom properties, BEM methodology, utility-first CSS, React/TSX components, OpenSpec documentation system
## arch
Full-stack containerized architecture with PostgreSQL database, Redis caching, separate API/backend and web frontend services, reverse-proxy ready (Traefik), using a Makefile-driven development workflow with AI agent collaboration via OpenSpec governance.
Contains 13 files.
## tags
docker, web, git, application, ui, rework, python, redis
## symbols
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps
## role
Contains the main application entry points and executable modules for the project.
Package apps
## parent
index: ./.pi-map.index.md
map: ./.pi-map.md
+2 -2
View File
@@ -4,10 +4,10 @@ dir: apps
index: apps/.pi-map.index.md
## role
Contains the main application entry points and executable modules for the project.
Package apps
## files
## arch
Top-level directory for deployable application boundaries, typically organized by target platform or deployment variant (e.g., web, mobile, desktop, CLI).
Contains 0 files.
## tags
-
## symbols
+7 -1
View File
@@ -2,7 +2,7 @@
dir: apps/api
## role
FastAPI backend API that manages projects, git repositories, development tools, and tool instances via Docker Compose
Backend API server for the Headquarter platform, providing self-hosted project management, git repository management, and development tool orchestration services.
## parent
index: apps/.pi-map.index.md
map: apps/.pi-map.md
@@ -22,6 +22,12 @@ map: apps/.pi-map.md
- apps/api/alembic
index: apps/api/alembic/.pi-map.index.md
map: apps/api/alembic/.pi-map.md
- apps/api/app
index: apps/api/app/.pi-map.index.md
map: apps/api/app/.pi-map.md
- apps/api/headquarter_api.egg-info
index: apps/api/headquarter_api.egg-info/.pi-map.index.md
map: apps/api/headquarter_api.egg-info/.pi-map.md
- apps/api/src
index: apps/api/src/.pi-map.index.md
map: apps/api/src/.pi-map.md
+10 -10
View File
@@ -4,19 +4,19 @@ dir: apps/api
index: apps/api/.pi-map.index.md
## role
FastAPI backend API that manages projects, git repositories, development tools, and tool instances via Docker Compose
Backend API server for the Headquarter platform, providing self-hosted project management, git repository management, and development tool orchestration services.
## files
- .dockerignore | Specifies files and directories to exclude from Docker build context to reduce image size and avoid caching unnecessary files | dep: Docker
- Dockerfile | Multi-stage Docker build for a Python web application with Docker socket access, database connectivity, and Cloudflare tunnel support | dep: python:3.11-slim, gcc, libpq-dev, docker-ce-cli, docker-compose-plugin, cloudflared, uvicorn, pyproject.toml dependencies
- README.md | README documentation for a self-hosted FastAPI backend API that manages projects, git repositories, development tools, and tool instances via Docker Compose | dep: FastAPI, SQLAlchemy, PostgreSQL, asyncpg, Alembic, Docker, Docker Compose, Authentik, uvicorn, pytest, ruff, mypy
- alembic.ini | Configuration file for Alembic database migration tool, specifying script location, database connection URL, and logging settings | 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 generated by the uv Python package manager that pins exact dependency versions with cryptographic hashes for reproducible installations | dep: uv, python, pypi, aiosqlite, alembic, annotated-doc, annotated-types, anyio, ast-serialize, asyncpg, mako, sqlalchemy, typing-extensions, idna
- wait-for-db.sh | Waits for a PostgreSQL database to become available by polling its TCP port before executing subsequent commands. | dep: nc (netcat), sh (POSIX shell), sleep
- .dockerignore | Specifies files and directories to exclude from the Docker build context to optimize image build times and prevent sensitive or unnecessary files from being included.
- Dockerfile | Multi-stage Dockerfile that builds and runs a Python application with Docker CLI access, cloudflared, and database readiness checks. | dep: python:3.11-slim, libpq5, git, openssh-client, docker-ce-cli, docker-compose-plugin, cloudflared, uvicorn, netcat-openbsd
- README.md | Provides comprehensive documentation for the Headquarter API, a self-hosted platform for managing projects, git repositories, and development tools. | dep: FastAPI, SQLAlchemy, PostgreSQL, asyncpg, Alembic, Docker, Authentik, Pydantic, Ruff, mypy, pytest
- alembic.ini | Configuration file for Alembic database migration tool, defining database connection and logging settings. | dep: alembic, sqlalchemy, asyncpg, postgresql
- pyproject.toml | Defines project metadata, dependencies, and tool configuration for the Headquarter platform API. | dep: fastapi, uvicorn, sqlalchemy, asyncpg, alembic, pydantic, pydantic-settings, httpx, structlog, cryptography, pytest, mypy, ruff
- uv.lock | This file is a UV lockfile that pins exact versions, hashes, and metadata for all Python project dependencies to ensure reproducible environments. | dep: uv, aiosqlite, alembic, annotated-types, anyio, asyncpg, sqlalchemy, mako
- wait-for-db.sh | Polls a PostgreSQL host/port until it is available or a retry limit is reached, then executes the passed command. | dep: nc, sleep
## arch
Containerized Python microservice using FastAPI, SQLAlchemy with Alembic migrations, multi-stage Docker builds, and Docker Compose orchestration with external database dependency
Modern Python async API using FastAPI/Starlette with SQLAlchemy ORM, Alembic migrations, multi-stage Docker containerization with cloudflared tunneling, UV package management, and PostgreSQL database with health-checked startup orchestration.
## tags
docker, alembic, python, database, fastapi, sqlalchemy, asyncpg, uvicorn
alembic, docker, sqlalchemy, asyncpg, dockerfile, database, postgresql, pydantic
## symbols
-
## workflows
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/api/alembic
## role
Database migration infrastructure for the API application, enabling version-controlled schema changes with async database support.
Database migration tooling for the API service, enabling version-controlled schema changes with async SQLAlchemy support.
## parent
index: apps/api/.pi-map.index.md
map: apps/api/.pi-map.md
+5 -5
View File
@@ -4,14 +4,14 @@ dir: apps/api/alembic
index: apps/api/alembic/.pi-map.index.md
## role
Database migration infrastructure for the API application, enabling version-controlled schema changes with async database support.
Database migration tooling for the API service, enabling version-controlled schema changes with async SQLAlchemy support.
## files
- env.py | Configures Alembic database migration environment with async SQLAlchemy support | exp: func:run_migrations_offline() → None, call:context.configure, call:context.begin_transaction, call:context.run_migrations, func:do_run_migrations(connection: Connection) → None, call:context.configure, call:context.begin_transaction, call:context.run_migrations, func:run_async_migrations() → None, call:async_engine_from_config, call:config.get_section, call:connectable.connect, call:connection.run_sync, call:connectable.dispose, func:run_migrations_online() → None, call:asyncio.run, call:run_async_migrations | dep: logging.config, alembic, sqlalchemy, sqlalchemy.engine, sqlalchemy.ext.asyncio, src.config, src.models, asyncio
- script.py.mako | Alembic database migration script template that generates upgrade/downgrade functions for SQL schema revisions | dep: alembic, sqlalchemy
- env.py | Configures Alembic database migration environment with async SQLAlchemy support. | exp: func:run_migrations_offline() → None, call:context.configure, call:context.begin_transaction, call:context.run_migrations, func:do_run_migrations(connection: Connection) → None, call:context.configure, call:context.begin_transaction, call:context.run_migrations, func:run_async_migrations() → None, call:async_engine_from_config, call:config.get_section, call:connectable.connect, call:connection.run_sync, call:connectable.dispose, func:run_migrations_online() → None, call:asyncio.run, call:run_async_migrations | dep: logging.config, alembic, sqlalchemy, sqlalchemy.engine, sqlalchemy.ext.asyncio, src.config, src.models, asyncio
- script.py.mako | Alembic database migration script template that generates Python migration files for SQLAlchemy database schema changes | dep: alembic, sqlalchemy, mako
## arch
Template-based migration generation using Alembic with async SQLAlchemy engine configuration and environment setup.
Template-based migration generation using Alembic's standard env.py configuration pattern with async SQLAlchemy engine integration and Mako templating for migration script scaffolding.
## tags
migrations, run, async, sqlalchemy, alembic, call:context.configure, call:context.begin, transaction
migrations, run, sqlalchemy, async, alembic, call:context.configure, call:context.begin, transaction
## symbols
- run_migrations_offline
- do_run_migrations
+7 -2
View File
@@ -2,12 +2,14 @@
dir: apps/api/alembic/versions
## role
Manages database schema evolution and version control for the API application using Alembic migration scripts.
Database schema versioning and incremental migration management for the API's relational data model using Alembic.
## parent
index: apps/api/alembic/.pi-map.index.md
map: apps/api/alembic/.pi-map.md
## children
-
- apps/api/alembic/versions/.ruff_cache
index: apps/api/alembic/versions/.ruff_cache/.pi-map.index.md
map: apps/api/alembic/versions/.ruff_cache/.pi-map.md
## files
- 0001_initial_schema.py
- 0002_refresh_tokens.py
@@ -51,6 +53,7 @@ map: apps/api/alembic/.pi-map.md
- 2026_06_14_104415_add_tool_type_home_directory.py
- 2026_06_14_182955_fix_pi_agent_home_directory_mount.py
- 2026_06_15_090500_remove_pi_agent_explicit_repo_mount.py
- 2026_06_19_113000_remove_pi_agent_workspace_symlink.py
- 398082499c30_add_tool_config_fields.py
- 6fc7bfcf199f_merge_remove_is_builtin_and_add_config_.py
- 86cec91fdb00_merge_profile_resolver_and_workspaces_.py
@@ -69,5 +72,7 @@ map: apps/api/alembic/versions/.pi-map.md
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
- explore versions subdirectories
index: apps/api/alembic/versions/.ruff_cache/.pi-map.index.md
## dirty
-
+47 -44
View File
@@ -4,60 +4,61 @@ dir: apps/api/alembic/versions
index: apps/api/alembic/versions/.pi-map.index.md
## role
Manages database schema evolution and version control for the API application using Alembic migration scripts.
Database schema versioning and incremental migration management for the API's relational data model using Alembic.
## files
- 0001_initial_schema.py | Creates the initial database schema with five tables (users, ssh_keys, projects, git_repositories, user_configs) using Alembic migrations | 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, sqlalchemy.dialects.postgresql
- 0002_refresh_tokens.py | Alembic database migration that creates a refresh_tokens table with indexes for user authentication/session 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, postgresql dialect
- 0003_user_configs.py | Alembic database migration that creates a user_configs table with JSON configuration storage linked to users via foreign key | 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 user relationship columns | 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
- 0001_initial_schema.py | Alembic database migration that creates the initial schema with five tables (users, ssh_keys, projects, git_repositories, user_configs) for a Git/SSH management application. | 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, sqlalchemy.dialects.postgresql
- 0002_refresh_tokens.py | Alembic database migration that creates a refresh_tokens table with indexes for secure session 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 per user | 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 user tracking fields | 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 UUID primary key, foreign key relationships, timestamps, and indexes for tracking deployed tool instances | 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, postgresql dialect
- 0006_tool_instances.py | Alembic database migration that creates a tool_instances table with foreign key relationships to tool_types, git_repositories, projects, and users tables, plus indexes for common query patterns. | 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 for storing user tool configuration key-value pairs with optional project scoping | 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, sqlalchemy.dialects.postgresql
- 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 primary keys, foreign keys to users/tool_types/projects, key-value configuration storage, and supporting indexes. | 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, sqlalchemy.dialects.postgresql
- 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 public_url and tunnel_id columns to the tool_instances table for tunnel functionality. | 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 backfills default_port values for existing tool types and 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 | Database migration that adds config profiles, profile includes, profile mounts, and tool instance profile selection tables 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, start_command, working_directory, port, is_default) to config_profiles table, renames mount_path to target_path and adds mode/files columns to config_mounts table while removing source_profile_id and content columns | 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 combines 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 plural JSON interfaces column with singular interface_type and requires_port columns on tool_types table | 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, sqlalchemy.dialects.postgresql
- 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 default_port values for existing tool types and 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, config_includes, config_mounts tables and a selected_profile_id column to tool_instances with defensive checks for idempotent execution | 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 | Database migration to add 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 to config_profiles and config_mounts tables with idempotent schema 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:_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 without applying schema changes | 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 a single `interface_type` string column and adds a `requires_port` boolean column, with dialect-specific SQL for PostgreSQL and SQLite data migration. | 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, sqlalchemy.dialects.postgresql
- 069d3da4dc9b_add_ssh_key_id_to_config_profiles.py | Alembic database migration that adds an ssh_key_id foreign key column to the config_profiles table | 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 new "pi-agent" tool type with terminal-based Docker development environment configuration | 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 and clone_mode/branch columns to tool_instances | 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 that removes the `is_builtin` column from the `tool_types` table with downgrade support to restore it. | 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 selected_config_profile_id foreign key to tool_instances | 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, sqlalchemy.dialects.postgresql
- 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 to support external repositories and expands alembic_version version_num column to prevent truncation. | 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 check status. | 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 foreign key to tool_instances for tracking terminal session lifecycle | 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 support to tool_types and tool_instances, and migrates the pi-agent tool from Dockerfile/compose templates to a JSON-based manifest system with a base Ubuntu image definition. | 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 | Database migration that drops the `tool_configs` and `config_folders` tables with conditional existence checks, and provides downgrade to recreate them. | 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, and severity-based notification records including read/dismissed tracking and optimized 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
- 20260527160017_add_pi_agent_tool_type.py | Adds a database migration that inserts a new "pi-agent" tool type into a tool_types table, defining a terminal-based Docker development environment with nvim, ranger, and tmux. | 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 that removes the `is_builtin` column from the `tool_types` table with a downgrade path to restore it. | 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 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 selected_config_profile_id foreign key to tool_instances. | 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, sqlalchemy.dialects.postgresql
- 2026_05_26_add_git_mounts.py | Database migration to add 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 VARCHAR(64). | 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 | Creates two database tables (instance_events and health_checks) for monitoring tool instances via an Alembic migration. | 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 foreign key to tool_instances, including indexes and audit timestamps | 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 support to tool_types and tool_instances, and migrates the pi-agent tool to a manifest-based definition with a base Ubuntu image. | 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 the tool_configs and config_folders tables with conditional existence checks, and provides downgrade to recreate them | 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 | Creates a notifications table with user-linked, categorized, severity-graded messages supporting read/dismissed states and optimized querying via partial 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 ssh_key_ids column 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 by replacing broken `--host` flags with correct `--bind-addr 0.0.0.0:{port}` configurations in both database tool_type templates and on-disk instance compose files. | 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 fixes code-server and jupyter-notebook tool 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 records and on-disk 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
- 2026_05_29_remove_ssh_keys_mount_from_manifest.py | Alembic database migration that removes (and can restore) the ssh_keys mount from the pi-agent tool definition manifest stored as JSON | 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 indexes and adds a workspace_id foreign key 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
- 2026_06_13_make_clone_mode_nullable.py | Alembic database migration to make the `clone_mode` column nullable in the `tool_instances` table | exp: func:upgrade() → None, call:op.alter_column, call:sa.String, func:downgrade() → None, call:op.alter_column, call:sa.String | dep: alembic, sqlalchemy
- 2026_06_14_104415_add_tool_type_home_directory.py | Alembic database migration that adds a home_directory column to tool_types table and updates template strings from /workspace to a configurable path | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.String, call:op.execute, call:sa.update(tool_types) .where(tool_types.c.compose_template.is_not(None)) .values, call:tool_types.c.compose_template.is_not, call:sa.func.replace, call:sa.update(tool_types) .where(tool_types.c.dockerfile_template.is_not(None)) .values, call:tool_types.c.dockerfile_template.is_not, func:downgrade() → None, call:op.execute, call:sa.update(tool_types) .where(tool_types.c.compose_template.is_not(None)) .values, call:tool_types.c.compose_template.is_not, call:sa.func.replace, call:sa.update(tool_types) .where(tool_types.c.dockerfile_template.is_not(None)) .values, call:tool_types.c.dockerfile_template.is_not, call:op.drop_column | dep: typing, alembic, sqlalchemy.sql, sqlalchemy
- 2026_06_14_182955_fix_pi_agent_home_directory_mount.py | Alembic database migration that updates the pi-agent tool definition manifest to mount repos under the home directory instead of /workspace | exp: func:_find_pi_agent_manifest(conn: sa.Connection) → tuple[Union[str, None], Union[dict, None]], call:conn.execute( sa.select(tool_definition_manifests.c.id, tool_definition_manifests.c.manifest) .where(tool_definition_manifests.c.name == "pi-agent") ).fetchone, call:sa.select(tool_definition_manifests.c.id, tool_definition_manifests.c.manifest) .where, call:dict, func:_update_manifest(conn: sa.Connection, manifest_id: str, manifest: dict) → None, call:conn.execute, call:sa.update(tool_definition_manifests) .where(tool_definition_manifests.c.id == manifest_id) .values, func:upgrade() → None, call:op.get_bind, call:_find_pi_agent_manifest, call:manifest.get, call:mount.get, call:manifest.setdefault, call:_update_manifest, func:downgrade() → None, call:op.get_bind, call:_find_pi_agent_manifest, call:manifest.get, call:mount.get, call:manifest.setdefault, call:_update_manifest | dep: typing, alembic, sqlalchemy.sql, sqlalchemy
- 2026_06_15_090500_remove_pi_agent_explicit_repo_mount.py | Alembic database migration that removes explicit repo mounts from pi-agent tool definition manifests since they are now synthesized by compile_compose | exp: func:_find_pi_agent_manifest(conn: sa.Connection) → tuple[Union[str, None], Union[dict, None]], call:conn.execute( sa.select( tool_definition_manifests.c.id, tool_definition_manifests.c.manifest ).where(tool_definition_manifests.c.name == "pi-agent") ).fetchone, call:sa.select( tool_definition_manifests.c.id, tool_definition_manifests.c.manifest ).where, call:dict, func:_update_manifest(conn: sa.Connection, manifest_id: str, manifest: dict) → None, call:conn.execute, call:sa.update(tool_definition_manifests) .where(tool_definition_manifests.c.id == manifest_id) .values, func:upgrade() → None, call:op.get_bind, call:_find_pi_agent_manifest, call:manifest.get, call:mount.get, call:_update_manifest, func:downgrade() → None, call:op.get_bind, call:_find_pi_agent_manifest, call:manifest.setdefault, call:any, call:mount.get, call:mounts.append, call:_update_manifest | dep: typing, alembic, sqlalchemy.sql, sqlalchemy
- 398082499c30_add_tool_config_fields.py | Alembic database migration that adds configuration fields (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 branches (removing is_builtin and adding config_profiles) with no actual schema changes | 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
- 8c6d1dbd4798_remove_pi_config_and_state_mounts_from_.py | Alembic database migration that removes pi_state and pi_config mounts from the pi-agent manifest in upgrade, and restores them in downgrade | exp: func:_load_manifest(manifest_json), call:isinstance, call:json.loads, func:upgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:_load_manifest, 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:_load_manifest, call:manifest.get, call:m.get, call:mounts.append, call:json.dumps | dep: json, alembic, sqlalchemy
- 8ed7dd80973d_create_config_folders_table.py | Creates a database migration that adds a config_folders table for storing user configuration folders with JSONB metadata, foreign key relationships, and indexing. | 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, postgresql dialect
- 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 parent revisions (single_interface and clone_mode) into a single branch point with no actual schema changes | exp: func:upgrade() → None, func:downgrade() → None | dep: typing, alembic
- 2026_05_29_fix_code_server_bind_addr_port.py | Alembic database migration that fixes code-server Docker compose templates by replacing incorrect `--host` flags with proper `--bind-addr 0.0.0.0:PORT` configurations in both database-stored tool type templates and on-disk instance compose files. | 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 fixes web tool (code-server and jupyter-notebook) compose templates to bind to 0.0.0.0 for network accessibility. | 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 from LinuxServer.io (LSIO) 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
- 2026_05_29_remove_ssh_keys_mount_from_manifest.py | Alembic database migration that removes the ssh_keys mount from the pi-agent manifest in the tool_definition_manifests table, with downgrade support to restore it. | 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
- 2026_06_13_make_clone_mode_nullable.py | Alembic database migration that makes the `clone_mode` column in the `tool_instances` table nullable to allow NULL values for new rows. | exp: func:upgrade() → None, call:op.alter_column, call:sa.String, func:downgrade() → None, call:op.alter_column, call:sa.String | dep: alembic, sqlalchemy
- 2026_06_14_104415_add_tool_type_home_directory.py | An Alembic database migration that adds a `home_directory` column to `tool_types` table and updates template strings from `/workspace` to a configurable `/home/user/{{WORKSPACE_NAME}}` path. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.String, call:op.execute, call:sa.update(tool_types) .where(tool_types.c.compose_template.is_not(None)) .values, call:tool_types.c.compose_template.is_not, call:sa.func.replace, call:sa.update(tool_types) .where(tool_types.c.dockerfile_template.is_not(None)) .values, call:tool_types.c.dockerfile_template.is_not, func:downgrade() → None, call:op.execute, call:sa.update(tool_types) .where(tool_types.c.compose_template.is_not(None)) .values, call:tool_types.c.compose_template.is_not, call:sa.func.replace, call:sa.update(tool_types) .where(tool_types.c.dockerfile_template.is_not(None)) .values, call:tool_types.c.dockerfile_template.is_not, call:op.drop_column | dep: typing, alembic, sqlalchemy.sql, sqlalchemy
- 2026_06_14_182955_fix_pi_agent_home_directory_mount.py | Alembic database migration that fixes the pi-agent tool definition manifest's home directory mount path from /workspace to ~/{{WORKSPACE_NAME}} with a compatibility symlink and updated startup script. | exp: func:_find_pi_agent_manifest(conn: sa.Connection) → tuple[Union[str, None], Union[dict, None]], call:conn.execute( sa.select(tool_definition_manifests.c.id, tool_definition_manifests.c.manifest) .where(tool_definition_manifests.c.name == "pi-agent") ).fetchone, call:sa.select(tool_definition_manifests.c.id, tool_definition_manifests.c.manifest) .where, call:dict, func:_update_manifest(conn: sa.Connection, manifest_id: str, manifest: dict) → None, call:conn.execute, call:sa.update(tool_definition_manifests) .where(tool_definition_manifests.c.id == manifest_id) .values, func:upgrade() → None, call:op.get_bind, call:_find_pi_agent_manifest, call:manifest.get, call:mount.get, call:manifest.setdefault, call:_update_manifest, func:downgrade() → None, call:op.get_bind, call:_find_pi_agent_manifest, call:manifest.get, call:mount.get, call:manifest.setdefault, call:_update_manifest | dep: typing, alembic, sqlalchemy.sql, sqlalchemy
- 2026_06_15_090500_remove_pi_agent_explicit_repo_mount.py | Alembic database migration that removes explicit repo mounts from the pi-agent tool definition manifest and restores them on downgrade | exp: func:_find_pi_agent_manifest(conn: sa.Connection) → tuple[Union[str, None], Union[dict, None]], call:conn.execute( sa.select( tool_definition_manifests.c.id, tool_definition_manifests.c.manifest ).where(tool_definition_manifests.c.name == "pi-agent") ).fetchone, call:sa.select( tool_definition_manifests.c.id, tool_definition_manifests.c.manifest ).where, call:dict, func:_update_manifest(conn: sa.Connection, manifest_id: str, manifest: dict) → None, call:conn.execute, call:sa.update(tool_definition_manifests) .where(tool_definition_manifests.c.id == manifest_id) .values, func:upgrade() → None, call:op.get_bind, call:_find_pi_agent_manifest, call:manifest.get, call:mount.get, call:_update_manifest, func:downgrade() → None, call:op.get_bind, call:_find_pi_agent_manifest, call:manifest.setdefault, call:any, call:mount.get, call:mounts.append, call:_update_manifest | dep: typing, alembic, sqlalchemy.sql, sqlalchemy
- 2026_06_19_113000_remove_pi_agent_workspace_symlink.py | Alembic database migration that updates the pi-agent tool definition manifest to remove the /workspace symlink dependency and adjust working directory and startup script accordingly. | exp: func:_find_pi_agent_manifest(conn: sa.Connection) → tuple[str | None, dict | None], call:conn.execute( sa.select( tool_definition_manifests.c.id, tool_definition_manifests.c.manifest ).where(tool_definition_manifests.c.name == "pi-agent") ).fetchone, call:sa.select( tool_definition_manifests.c.id, tool_definition_manifests.c.manifest ).where, call:dict, func:_update_manifest(conn: sa.Connection, manifest_id: str, manifest: dict) → None, call:conn.execute, call:sa.update(tool_definition_manifests) .where(tool_definition_manifests.c.id == manifest_id) .values, func:upgrade() → None, call:op.get_bind, call:_find_pi_agent_manifest, call:manifest.setdefault, call:runtime.get, call:_update_manifest, func:downgrade() → None, call:op.get_bind, call:_find_pi_agent_manifest, call:manifest.setdefault, call:_update_manifest | dep: collections.abc, alembic, sqlalchemy.sql, sqlalchemy
- 398082499c30_add_tool_config_fields.py | Alembic database migration that adds configuration fields (port_override, start_command, working_directory, environment_variables, volumes) to the tool_configs table with a port range validation 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 merge migration that combines two parallel migration branches (remove_is_builtin and add_config_profiles) into a single revision history | exp: func:upgrade() → None, func:downgrade() → None | dep: alembic
- 86cec91fdb00_merge_profile_resolver_and_workspaces_.py | Merges two Alembic migration branches (profile resolver and workspaces) into a single migration head | exp: func:upgrade() → None, func:downgrade() → None | dep: alembic
- 8c6d1dbd4798_remove_pi_config_and_state_mounts_from_.py | Alembic database migration that removes or restores pi_state and pi_config mounts from a JSON manifest stored in the tool_definition_manifests table for the 'pi-agent' tool. | exp: func:_load_manifest(manifest_json), call:isinstance, call:json.loads, func:upgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:_load_manifest, 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:_load_manifest, call:manifest.get, call:m.get, call:mounts.append, call:json.dumps | dep: json, alembic, sqlalchemy
- 8ed7dd80973d_create_config_folders_table.py | Alembic database migration that creates a config_folders table with UUID primary key, user foreign key, JSONB fields for files and project overrides, and supporting indexes/constraints | 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, postgresql dialect
- af8512103d67_add_tool_type_fields.py | Alembic database migration that adds columns (definition_type, dockerfile_template, build_context, readiness_probe) and a check constraint to the tool_types table. | 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 previous migrations (single_interface and clone_mode) into a single revision point | exp: func:upgrade() → None, func:downgrade() → None | dep: typing, alembic
- fc8f1a20cbf6_merge_home_directory_and_pi_agent_mount_.py | Alembic database migration that merges two branch heads (home directory and pi agent mount cleanup) into a single revision point | exp: func:upgrade() → None, func:downgrade() → None | dep: alembic
## arch
Linear migration history with branching/merge resolution pattern; each migration is an immutable, timestamped or sequenced script containing declarative SQLAlchemy operations (create_table, add_column, drop_column) with bidirectional upgrade/downgrade functions; includes data migrations, idempotent guards, and cross-references to on-disk file mutations.
Linear migration history with occasional branch/merge patterns (using merge heads), sequential numbered and timestamped revision files, each containing declarative schema changes (CREATE TABLE/ALTER TABLE/DROP TABLE) with idempotent guards, downgrade paths, and occasional data migrations; supports PostgreSQL and SQLite dialects.
## tags
column, table, call:op.drop, downgrade, upgrade, alembic, key, call:sa.text
## symbols
@@ -76,5 +77,7 @@ column, table, call:op.drop, downgrade, upgrade, alembic, key, call:sa.text
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
- explore versions subdirectories
index: apps/api/alembic/versions/.ruff_cache/.pi-map.index.md
## dirty
-
@@ -0,0 +1,87 @@
"""remove pi agent workspace symlink
Revision ID: 2026_06_19_113000
Revises: 2026_06_15_090500
Create Date: 2026-06-19 11:30:00.000000
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
from sqlalchemy.sql import column, table
# revision identifiers, used by Alembic.
revision: str = "2026_06_19_113000"
down_revision: str | Sequence[str] | None = "2026_06_15_090500"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
tool_definition_manifests = table(
"tool_definition_manifests",
column("id", sa.UUID),
column("name", sa.String),
column("manifest", sa.JSON),
)
def _find_pi_agent_manifest(
conn: sa.Connection,
) -> tuple[str | None, dict | None]:
result = conn.execute(
sa.select(
tool_definition_manifests.c.id, tool_definition_manifests.c.manifest
).where(tool_definition_manifests.c.name == "pi-agent")
).fetchone()
if result is None:
return None, None
return result.id, dict(result.manifest)
def _update_manifest(conn: sa.Connection, manifest_id: str, manifest: dict) -> None:
conn.execute(
sa.update(tool_definition_manifests)
.where(tool_definition_manifests.c.id == manifest_id)
.values(manifest=manifest)
)
def upgrade() -> None:
conn = op.get_bind()
manifest_id, manifest = _find_pi_agent_manifest(conn)
if not manifest_id or not manifest:
return
runtime = manifest.setdefault("runtime", {})
# The /workspace compatibility symlink is no longer created by the
# compiler/entrypoint. Leaving working_dir set to /workspace causes the
# container to start in an empty directory. Let compile_compose default
# to /home/user/{workspace_name} instead.
if runtime.get("working_dir") == "/workspace":
del runtime["working_dir"]
scripts = manifest.setdefault("scripts", {})
# Update the startup script to operate on the real repo-named directory.
scripts["startup"] = [
'if [ -n "$WORKSPACE_NAME" ]; then sudo chown -R user:user "$HOME/$WORKSPACE_NAME" 2>/dev/null || true; fi',
]
_update_manifest(conn, manifest_id, manifest)
def downgrade() -> None:
conn = op.get_bind()
manifest_id, manifest = _find_pi_agent_manifest(conn)
if not manifest_id or not manifest:
return
runtime = manifest.setdefault("runtime", {})
runtime["working_dir"] = "/workspace"
scripts = manifest.setdefault("scripts", {})
scripts["startup"] = [
"if [ -d /workspace ]; then sudo chown -R user:user /workspace 2>/dev/null || true; fi",
]
_update_manifest(conn, manifest_id, manifest)
+8 -2
View File
@@ -2,17 +2,23 @@
dir: apps/api/src
## role
Core FastAPI web application package that bootstraps and configures the Headquarter API service with database, authentication, logging, and modular routing infrastructure.
Core FastAPI application package that initializes and configures the Headquarter API with database, authentication, logging, and middleware infrastructure.
## parent
index: apps/api/.pi-map.index.md
map: apps/api/.pi-map.md
## children
- apps/api/src/.ruff_cache
index: apps/api/src/.ruff_cache/.pi-map.index.md
map: apps/api/src/.ruff_cache/.pi-map.md
- 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
@@ -46,6 +52,6 @@ map: apps/api/src/.pi-map.md
- 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/models/.pi-map.index.md
index: apps/api/src/.ruff_cache/.pi-map.index.md, apps/api/src/api/.pi-map.index.md, apps/api/src/auth/.pi-map.index.md
## dirty
-
+8 -8
View File
@@ -4,17 +4,17 @@ dir: apps/api/src
index: apps/api/src/.pi-map.index.md
## role
Core bootstrap and infrastructure package for the Headquarter API FastAPI application, handling configuration, database connectivity, logging, and application startup.
Core FastAPI application package that initializes and configures the Headquarter API with database, authentication, logging, and middleware infrastructure.
## files
- __init__.py | Marks the directory as a Python package for the Headquarter API.
- config.py | Defines application configuration settings with environment-based overrides using Pydantic, including database URLs, domain/public URL resolution, Authentik OAuth integration, session/JWT settings, and cookie security policies. | 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 an async SQLAlchemy database engine with session management and provides retry logic for database initialization via Alembic migrations. | 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:os.path.dirname, call:os.path.abspath, call:logger.debug, call:logger.error, call:asyncio.sleep, call:str(exc).lower, call:logger.warning | dep: asyncio, logging, os, subprocess, sqlalchemy.ext.asyncio, sqlalchemy.pool, src.config, sqlalchemy
- logging_config.py | Configures structured JSON logging with correlation IDs 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, starlette
- main.py | Bootstraps a FastAPI application for "Headquarter API" with CORS, logging, validation error handling, database initialization, health monitoring, and registration of modular API routers. | 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.*
- config.py | Defines application configuration settings using pydantic-settings, including database connectivity, Authentik SSO, JWT, session, and domain-based URL resolution. | 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 an async SQLAlchemy database engine/session and provides a retry-based initialization function that runs 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:os.path.dirname, call:os.path.abspath, call:logger.debug, call:logger.error, call:asyncio.sleep, call:str(exc).lower, call:logger.warning | dep: asyncio, logging, os, subprocess, sqlalchemy.ext.asyncio, sqlalchemy.pool, src.config, sqlalchemy
- logging_config.py | Configures structured JSON logging with correlation ID injection and provides ASGI middleware for logging HTTP requests, responses, and unhandled exceptions. | 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, starlette
- main.py | Initializes and configures the FastAPI application, setting up middleware, routers, database connections, and lifecycle event handlers for the Headquarter API. | 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, src.seeds, src.services
## arch
Layered infrastructure pattern with Pydantic-based settings management, async SQLAlchemy with Alembic migrations, structured JSON logging with correlation ID tracking, and modular FastAPI router registration with middleware composition.
Layered architecture using Pydantic-settings for configuration, async SQLAlchemy with Alembic migrations, structured JSON logging with correlation ID tracking, and FastAPI lifecycle management with dependency injection for cross-cutting concerns.
## tags
src, database, logging, api, call:logger.info, fastapi, filter, call:logging.get
src, logging, database, call:logger.info, api, middleware, filter, call:logging.get
## symbols
- Settings
- CorrelationIdFilter
@@ -30,6 +30,6 @@ src, database, logging, api, call:logger.info, fastapi, filter, call:logging.get
- 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/models/.pi-map.index.md
index: apps/api/src/.ruff_cache/.pi-map.index.md, apps/api/src/api/.pi-map.index.md, apps/api/src/auth/.pi-map.index.md
## dirty
-
+5 -2
View File
@@ -2,11 +2,14 @@
dir: apps/api/src/api
## role
Defines reusable Pydantic validation utilities for API schema fields used across API endpoints.
Defines the core API router package with reusable Pydantic validation utilities for container and filesystem-related API schemas.
## parent
index: apps/api/src/.pi-map.index.md
map: apps/api/src/.pi-map.md
## children
- apps/api/src/api/.ruff_cache
index: apps/api/src/api/.ruff_cache/.pi-map.index.md
map: apps/api/src/api/.ruff_cache/.pi-map.md
- apps/api/src/api/config
index: apps/api/src/api/config/.pi-map.index.md
map: apps/api/src/api/config/.pi-map.md
@@ -35,6 +38,6 @@ map: apps/api/src/api/.pi-map.md
- 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
index: apps/api/src/api/.ruff_cache/.pi-map.index.md, apps/api/src/api/config/.pi-map.index.md, apps/api/src/api/project/.pi-map.index.md
## dirty
-
+4 -4
View File
@@ -4,12 +4,12 @@ dir: apps/api/src/api
index: apps/api/src/api/.pi-map.index.md
## role
Defines reusable Pydantic validation utilities for API schema fields used across API endpoints.
Defines the core API router package with reusable Pydantic validation utilities for container and filesystem-related API schemas.
## files
- __init__.py | Marks the directory as a Python package for API routers.
- shared_validators.py | Provides reusable Pydantic validation functions for API schema fields including mount paths, file uploads, 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
- shared_validators.py | Provides reusable Pydantic validator functions for validating mount paths, file contents, environment variables, and volume mounts in API schemas. | 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
Utility module pattern with shared validation functions for common Docker/container resource types (mounts, volumes, files, environment variables).
Modular package structure separating router organization from shared cross-cutting validation concerns using Pydantic validators.
## tags
validate, raise:value, error, call:isinstance, mount, api, init, path
## symbols
@@ -25,6 +25,6 @@ validate, raise:value, error, call:isinstance, mount, api, init, path
- 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
index: apps/api/src/api/.ruff_cache/.pi-map.index.md, apps/api/src/api/config/.pi-map.index.md, apps/api/src/api/project/.pi-map.index.md
## dirty
-
+6 -2
View File
@@ -2,12 +2,14 @@
dir: apps/api/src/api/system
## role
Provides system-level infrastructure endpoints for monitoring, real-time communication, and instance management in the API service.
Provides system-level API endpoints for monitoring, administration, and infrastructure operations including dashboards, health checks, event streaming, instance proxying, notifications, and terminal access.
## parent
index: apps/api/src/api/.pi-map.index.md
map: apps/api/src/api/.pi-map.md
## children
-
- apps/api/src/api/system/.ruff_cache
index: apps/api/src/api/system/.ruff_cache/.pi-map.index.md
map: apps/api/src/api/system/.ruff_cache/.pi-map.md
## files
- __init__.py
- dashboard.py
@@ -22,5 +24,7 @@ map: apps/api/src/api/system/.pi-map.md
## workflows
- change system behavior
read: __init__.py, dashboard.py, events.py
- explore system subdirectories
index: apps/api/src/api/system/.ruff_cache/.pi-map.index.md
## dirty
-
+12 -10
View File
@@ -4,19 +4,19 @@ dir: apps/api/src/api/system
index: apps/api/src/api/system/.pi-map.index.md
## role
Provides system-level infrastructure endpoints for monitoring, real-time communication, and instance management in the API service.
Provides system-level API endpoints for monitoring, administration, and infrastructure operations including dashboards, health checks, event streaming, instance proxying, notifications, and terminal access.
## files
- __init__.py | Aggregates and re-exports system API router modules 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 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
- events.py | Implements an SSE streaming endpoint that delivers instance events to authenticated users with per-user connection limiting and automatic heartbeat 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
- __init__.py | Aggregates and exports system API routers for a modular web application framework. | 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 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
- events.py | Implements an SSE streaming endpoint that delivers instance events to authenticated users with per-user connection limits and heartbeat pings. | dep: asyncio, contextlib, json, uuid, collections.abc, fastapi, src.auth.dependencies, src.services.instance.event_bus
- health.py | Provides FastAPI health check endpoints that monitor system health including database connectivity/response time and disk usage, returning structured health status responses. | 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 | HTTP request proxy router that forwards requests from authenticated users to their running containerized tool instances. | 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 all) with support for muted categories. | 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, src.models.UserConfig
- terminal.py | Provides WebSocket endpoints for browser-based terminal access to running tool instances, handling session lifecycle, authentication, input/output streaming, and terminal resize/reset operations. | 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:_resolve_container_user(db_session: AsyncSession, instance: ToolInstance) → str | None, call:db_session.get, call:dict, call:resolve_base, call:deep_merge, call:get_manifest_container_user, 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:_resolve_container_user, 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:_resolve_container_user, 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:_resolve_container_user, 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:_resolve_container_user, 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.build.manifest_compiler, src.services.terminal.terminal_manager, src.services.docker, src.auth.session, src.config, starlette
- instance_proxy.py | Proxies HTTP requests from authenticated users to running containerized tool instances after verifying ownership and instance 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 user notification management including listing, marking as read, dismissing, and retrieving unread counts with support for muted categories. | 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 endpoints for browser-based terminal access to running Docker container tool instances, handling authentication, session management, input/output streaming, and terminal resize/reset operations. | 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:_resolve_container_user(db_session: AsyncSession, instance: ToolInstance) → str | None, call:db_session.get, call:dict, call:resolve_base, call:deep_merge, call:get_manifest_container_user, 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:_resolve_container_user, 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:_resolve_container_user, 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:_resolve_container_user, 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:_resolve_container_user, 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.build.manifest_compiler, src.services.terminal.terminal_manager, src.services.docker, src.auth.session, src.config, starlette
## arch
Modular FastAPI router composition with separate modules for distinct protocols (REST/SSE/WebSocket/proxy), each handling authentication, connection lifecycle management, and resource-specific business logic.
Modular FastAPI router pattern with per-feature separation, combining standard REST endpoints, SSE streaming, and WebSocket connections, all with unified authentication and user-scoped access control.
## tags
terminal, call:, session, call:terminal, src, get, response, websocket
terminal, session, call:terminal, call:, src, get, response, instance
## symbols
- NotificationItem
- NotificationListResponse
@@ -25,9 +25,11 @@ terminal, call:, session, call:terminal, src, get, response, websocket
- ClearAllResponse
- SessionRef
- get_dashboard_summary
- events_stream
- health_check
## workflows
- change system behavior
read: __init__.py, dashboard.py, events.py
- explore system subdirectories
index: apps/api/src/api/system/.ruff_cache/.pi-map.index.md
## dirty
-
+77 -64
View File
@@ -12,10 +12,12 @@ from sqlalchemy.ext.asyncio import AsyncSession
from starlette.websockets import WebSocketDisconnect
from src.auth.dependencies import get_current_user_id, get_db_session
from src.models import TerminalSessionModel
from src.models import ToolDefinitionManifest
from src.models import ToolInstance
from src.models import ToolType
from src.models import (
TerminalSessionModel,
ToolDefinitionManifest,
ToolInstance,
ToolType,
)
from src.services.build.manifest_compiler import (
deep_merge,
get_manifest_container_user,
@@ -81,13 +83,17 @@ async def _resolve_container_user(
if not tool_type or tool_type.definition_type != "manifest":
return None
if not tool_type.manifest_id or not tool_type.manifest:
if not tool_type.manifest_id:
return None
manifest = dict(tool_type.manifest.manifest)
if tool_type.manifest.base_definition_id:
manifest_def = await db_session.get(ToolDefinitionManifest, tool_type.manifest_id)
if not manifest_def:
return None
manifest = dict(manifest_def.manifest)
if manifest_def.base_definition_id:
base_def = await db_session.get(
ToolDefinitionManifest, tool_type.manifest.base_definition_id
ToolDefinitionManifest, manifest_def.base_definition_id
)
if base_def:
manifest = resolve_base(deep_merge(dict(base_def.manifest), manifest))
@@ -328,66 +334,73 @@ async def _write_loop(session_ref: SessionRef, websocket, instance_id: str) -> N
await session.write_input(message["bytes"])
elif "text" in message:
text = message["text"]
# A text frame that parses to a JSON object with a
# "type" field is a control message and must NEVER be
# written to the PTY (e.g. the heartbeat {"type":"pong"}
# must be consumed, not typed into the shell/pi). Handle
# known types and ignore unknown ones. Everything else
# (keystrokes, bracketed-paste content, plain text) is
# forwarded as raw terminal input.
ctrl = None
if text.startswith("{"):
# Control message (JSON)
try:
ctrl = json.loads(text)
msg_type = ctrl.get("type")
if msg_type == "resize":
cols = ctrl.get("cols", 80)
rows = ctrl.get("rows", 24)
logger.debug(
"Received resize message for instance %s: %sx%s",
instance_id,
cols,
rows,
)
await session.resize(cols, rows)
elif msg_type == "ack":
char_count = ctrl.get("chars", 0)
if char_count > 0:
session.acknowledge_data(char_count)
elif msg_type == "reset":
# Reset terminal session (scoped to current slot)
logger.debug(
"Resetting terminal session for instance %s (slot=%s)",
session.instance_id,
session_ref.slot_session_id,
)
await websocket.send_json(
{"type": "status", "status": "resetting"}
)
# Reset the session scoped to its slot
new_session = await terminal_manager.reset_session(
session.instance_id,
session.container_id,
startup_command=session.startup_command,
session_id=session_ref.slot_session_id,
name=session.name,
container_user=session.container_user,
)
# Update the mutable session reference
session_ref.session = new_session
# Attach to new session
await terminal_manager.attach_websocket(
new_session, websocket
)
await websocket.send_json(
{"type": "status", "status": "connected"}
)
# Continue the loop with the new session
continue
parsed = json.loads(text)
except json.JSONDecodeError:
# Not a valid JSON control message, treat as regular input
await session.write_input(text.encode("utf-8"))
else:
parsed = None
if isinstance(parsed, dict) and "type" in parsed:
ctrl = parsed
if ctrl is None:
await session.write_input(text.encode("utf-8"))
continue
msg_type = ctrl["type"]
if msg_type == "resize":
cols = ctrl.get("cols", 80)
rows = ctrl.get("rows", 24)
logger.debug(
"Received resize message for instance %s: %sx%s",
instance_id,
cols,
rows,
)
await session.resize(cols, rows)
elif msg_type == "ack":
char_count = ctrl.get("chars", 0)
if char_count > 0:
session.acknowledge_data(char_count)
elif msg_type == "reset":
# Reset terminal session (scoped to current slot)
logger.debug(
"Resetting terminal session for instance %s (slot=%s)",
session.instance_id,
session_ref.slot_session_id,
)
await websocket.send_json(
{"type": "status", "status": "resetting"}
)
# Reset the session scoped to its slot
new_session = await terminal_manager.reset_session(
session.instance_id,
session.container_id,
startup_command=session.startup_command,
session_id=session_ref.slot_session_id,
name=session.name,
container_user=session.container_user,
)
# Update the mutable session reference
session_ref.session = new_session
# Attach to new session
await terminal_manager.attach_websocket(new_session, websocket)
await websocket.send_json(
{"type": "status", "status": "connected"}
)
# Continue the loop with the new session
continue
elif message["type"] == "websocket.disconnect":
break
except Exception:
@@ -12,12 +12,15 @@ import signal
import struct
import fcntl
import time
import tty
import uuid
from collections import deque
from typing import Any
logger = logging.getLogger(__name__)
_session_counters_by_instance: dict[str, int] = {}
class TerminalSession:
"""Manages a single terminal session with event-driven PTY I/O.
@@ -45,9 +48,6 @@ class TerminalSession:
# Max WebSocket frame size
MAX_FRAME_SIZE = 64 * 1024
# Session number counter per instance_id for auto-naming
_instance_counters: dict[str, int] = {}
def __init__(
self,
session_id: str,
@@ -98,11 +98,11 @@ class TerminalSession:
# Ack timeout fallback
self._ack_timeout_handle: asyncio.TimerHandle | None = None
@classmethod
def _generate_name(cls, instance_id: str) -> str:
@staticmethod
def _generate_name(instance_id: str) -> str:
"""Generate an auto-incremented session name for the instance."""
count = cls._instance_counters.get(instance_id, 0) + 1
cls._instance_counters[instance_id] = count
count = _session_counters_by_instance.get(instance_id, 0) + 1
_session_counters_by_instance[instance_id] = count
return f"Session {count}"
async def start(self, startup_command: str | None = None) -> None:
@@ -120,8 +120,21 @@ class TerminalSession:
if self.container_user:
exec_cmd.extend(["--user", self.container_user])
# Create a pseudo-terminal on the host
# Create a pseudo-terminal on the host. The master must be
# non-blocking: a browser paste can be larger than the PTY input
# buffer, and write_input() drains it asynchronously without dropping
# the closing bracketed-paste marker.
self._master_fd, slave_fd = pty.openpty()
os.set_blocking(self._master_fd, False)
# Put the host PTY into raw mode so it behaves as a pass-through
# pipe. openpty() leaves the slave in canonical mode by default,
# which line-buffers input, splits multiline pastes at newlines,
# and mangles bracketed-paste markers before docker exec / the
# foreground app (e.g. pi) ever see them. Real terminal discipline
# (echo, canonical editing for readline) is provided by the
# in-container PTY that `docker exec -t` allocates.
tty.setraw(slave_fd)
# Set the terminal size initially
self._set_terminal_size(self._cols, self._rows)
@@ -194,6 +207,9 @@ class TerminalSession:
try:
data = os.read(self._master_fd, 4096)
except BlockingIOError:
# The readiness notification raced with another callback.
return
except (OSError, IOError) as exc:
logger.debug("PTY read error for session %s: %s", self.session_id, exc)
self._handle_eof()
@@ -333,12 +349,42 @@ class TerminalSession:
pass
logger.info("Session %s EOF handled, websockets closed", self.session_id)
async def _wait_for_write_ready(self, fd: int) -> None:
"""Wait until a non-blocking PTY master can accept more input."""
loop = asyncio.get_running_loop()
writable = loop.create_future()
def mark_writable() -> None:
if not writable.done():
writable.set_result(None)
loop.add_writer(fd, mark_writable)
try:
await writable
finally:
loop.remove_writer(fd)
async def write_input(self, data: bytes) -> None:
"""Write input to the PTY master."""
"""Write all terminal input bytes to the PTY master in order."""
if self._master_fd is None or self._closed:
return
fd = self._master_fd
remaining = memoryview(data)
try:
os.write(self._master_fd, data)
while remaining and not self._closed and self._master_fd == fd:
try:
written = os.write(fd, remaining)
except BlockingIOError:
await self._wait_for_write_ready(fd)
continue
if written == 0:
await self._wait_for_write_ready(fd)
continue
remaining = remaining[written:]
self.last_activity = time.time()
except (OSError, IOError) as exc:
logger.debug("PTY write error for session %s: %s", self.session_id, exc)
+7 -1
View File
@@ -2,7 +2,7 @@
dir: apps/api/tests
## role
Provides shared test infrastructure and fixtures for FastAPI API integration tests.
Provides shared pytest fixtures and test utilities for FastAPI application testing
## parent
index: apps/api/.pi-map.index.md
map: apps/api/.pi-map.md
@@ -19,6 +19,12 @@ map: apps/api/.pi-map.md
- apps/api/tests/system
index: apps/api/tests/system/.pi-map.index.md
map: apps/api/tests/system/.pi-map.md
- apps/api/tests/test_routers
index: apps/api/tests/test_routers/.pi-map.index.md
map: apps/api/tests/test_routers/.pi-map.md
- apps/api/tests/tools
index: apps/api/tests/tools/.pi-map.index.md
map: apps/api/tests/tools/.pi-map.md
- apps/api/tests/unit
index: apps/api/tests/unit/.pi-map.index.md
map: apps/api/tests/unit/.pi-map.md
+3 -3
View File
@@ -4,11 +4,11 @@ dir: apps/api/tests
index: apps/api/tests/.pi-map.index.md
## role
Provides shared test infrastructure and fixtures for FastAPI API integration tests.
Provides shared pytest fixtures and test utilities for FastAPI application testing
## files
- conftest.py | Provides shared pytest fixtures for FastAPI integration testing, including test clients, database sessions, authenticated users, and admin users with SQLite in-memory database. | exp: func:test_client() → Generator[TestClient, None, None], call:create_async_engine, call:engine.begin, call:conn.run_sync, call:asyncio.run, call:init_db, call:async_sessionmaker, call:patch, call:TestClient, call:app.dependency_overrides.pop, call:engine.dispose, func:init_db(), call:engine.begin, call:conn.run_sync, func:override_get_db_session() → AsyncGenerator[AsyncSession, None], call:async_sessionmaker, func:db_session(test_client) → AsyncGenerator[AsyncSession, None], call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:gen.aclose, call:create_async_engine, call:engine.begin, call:conn.run_sync, call:async_sessionmaker, call:engine.dispose, func:authenticated_client(test_client) → Generator[TestClient, None, None], call:str, call:uuid.uuid4, call:Settings, call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose, call:asyncio.run, call:create_test_user, call:create_session_cookie, call:test_client.cookies.set, func:create_test_user(), call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose, func:test_project_and_repo(authenticated_client) → tuple[str, str], call:uuid.uuid4, call:Settings, call:authenticated_client.cookies.get, call:decode_session_cookie, call:uuid.UUID, call:asyncio.run, call:get_user_id, call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:Project, call:session.add, call:GitRepository, call:session.commit, call:gen.aclose, call:create_project_and_repo, call:str, raise:RuntimeError, func:get_user_id(), call:Settings, call:authenticated_client.cookies.get, call:decode_session_cookie, call:uuid.UUID, func:create_project_and_repo(), call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:Project, call:session.add, call:GitRepository, call:session.commit, call:gen.aclose, func:admin_client(test_client) → Generator[TestClient, None, None], call:str, call:uuid.uuid4, call:Settings, call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose, call:asyncio.run, call:create_admin_user, call:create_session_cookie, call:test_client.cookies.set, func:create_admin_user(), call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose | dep: asyncio, os, typing, unittest.mock, pytest, pytest_asyncio, fastapi.testclient, sqlalchemy.ext.asyncio, src.config, src.models.base, src.main, src.auth.dependencies, uuid, src.auth.session, src.models.user.user, src.models.project.project, src.models.project.git_repository, fastapi, sqlalchemy, aiosqlite
- conftest.py | Provides shared pytest fixtures including test clients, database sessions, authenticated clients, and test data for FastAPI application testing. | exp: func:test_client() → Generator[TestClient, None, None], call:create_async_engine, call:engine.begin, call:conn.run_sync, call:asyncio.run, call:init_db, call:async_sessionmaker, call:patch, call:TestClient, call:app.dependency_overrides.pop, call:engine.dispose, func:init_db(), call:engine.begin, call:conn.run_sync, func:override_get_db_session() → AsyncGenerator[AsyncSession, None], call:async_sessionmaker, func:db_session(test_client) → AsyncGenerator[AsyncSession, None], call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:gen.aclose, call:create_async_engine, call:engine.begin, call:conn.run_sync, call:async_sessionmaker, call:engine.dispose, func:authenticated_client(test_client) → Generator[TestClient, None, None], call:str, call:uuid.uuid4, call:Settings, call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose, call:asyncio.run, call:create_test_user, call:create_session_cookie, call:test_client.cookies.set, func:create_test_user(), call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose, func:test_project_and_repo(authenticated_client) → tuple[str, str], call:uuid.uuid4, call:Settings, call:authenticated_client.cookies.get, call:decode_session_cookie, call:uuid.UUID, call:asyncio.run, call:get_user_id, call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:Project, call:session.add, call:GitRepository, call:session.commit, call:gen.aclose, call:create_project_and_repo, call:str, raise:RuntimeError, func:get_user_id(), call:Settings, call:authenticated_client.cookies.get, call:decode_session_cookie, call:uuid.UUID, func:create_project_and_repo(), call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:Project, call:session.add, call:GitRepository, call:session.commit, call:gen.aclose, func:admin_client(test_client) → Generator[TestClient, None, None], call:str, call:uuid.uuid4, call:Settings, call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose, call:asyncio.run, call:create_admin_user, call:create_session_cookie, call:test_client.cookies.set, func:create_admin_user(), call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose | dep: asyncio, os, typing, unittest.mock, pytest, pytest_asyncio, fastapi.testclient, sqlalchemy.ext.asyncio, src.config, src.models.base, src.main, src.auth.dependencies, uuid, src.auth.session, src.models.user.user, src.models.project.project, src.models.project.git_repository, fastapi.testclient.TestClient
## arch
Pytest fixture-based testing architecture with dependency injection overrides, SQLite in-memory database for test isolation, and pre-configured authenticated/admin user states.
Pytest plugin pattern with dependency injection fixtures for database sessions, HTTP clients, and authentication state management
## tags
call:app.dependency, call:create, overrides.get, call:override, fn, call:gen.asend, call:gen.aclose, user
## symbols
+7 -2
View File
@@ -2,12 +2,14 @@
dir: apps/api/tests/unit
## role
Contains unit tests for core API services and utilities, covering database migrations, configuration, Docker operations, Git integration, event handling, file management, health monitoring, manifest compilation, notifications, permissions, SSH keys, and terminal sessions.
Contains isolated unit tests for individual components of the API application, covering configuration, services, utilities, and core business logic without external dependencies.
## parent
index: apps/api/tests/.pi-map.index.md
map: apps/api/tests/.pi-map.md
## children
-
- apps/api/tests/unit/.ruff_cache
index: apps/api/tests/unit/.ruff_cache/.pi-map.index.md
map: apps/api/tests/unit/.ruff_cache/.pi-map.md
## files
- __init__.py
- test_alembic_migrations.py
@@ -32,6 +34,7 @@ map: apps/api/tests/.pi-map.md
- test_permission_fixer.py
- test_readiness_probe.py
- test_ssh_keys.py
- test_terminal_container_user.py
- test_terminal_session.py
## links
index: apps/api/tests/unit/.pi-map.index.md
@@ -41,5 +44,7 @@ map: apps/api/tests/unit/.pi-map.md
read: __init__.py, test_alembic_migrations.py, test_config.py
- change unit config
read: test_config.py, test_config_profile_resolver.py
- explore unit subdirectories
index: apps/api/tests/unit/.ruff_cache/.pi-map.index.md
## dirty
-
+28 -25
View File
@@ -4,36 +4,37 @@ dir: apps/api/tests/unit
index: apps/api/tests/unit/.pi-map.index.md
## role
Contains unit tests for core API services and utilities, covering database migrations, configuration, Docker operations, Git integration, event handling, file management, health monitoring, manifest compilation, notifications, permissions, SSH keys, and terminal sessions.
Contains isolated unit tests for individual components of the API application, covering configuration, services, utilities, and core business logic without external dependencies.
## files
- __init__.py | Provides a command to swap the position of two tmux panes within a window or between windows | dep: tmux, client, window, layout, cmd-find, cmd-parse, options
- test_alembic_migrations.py | Unit tests that verify Alembic database migrations are importable, have correct revision identifiers, and declare expected dependencies without requiring a live database. | exp: func:test_home_directory_migration_imports_and_rewrites() → None, call:Path, call:migration_path.exists, call:importlib.util.spec_from_file_location, call:importlib.util.module_from_spec, call:spec.loader.exec_module, call:callable, func:test_merge_migration_resolves_heads() → None, call:Path, call:migration_path.exists, call:importlib.util.spec_from_file_location, call:importlib.util.module_from_spec, call:spec.loader.exec_module, call:callable, func:test_remove_pi_agent_repo_mount_migration_imports() → None, call:Path, call:migration_path.exists, call:importlib.util.spec_from_file_location, call:importlib.util.module_from_spec, call:spec.loader.exec_module, call:callable | dep: importlib.util, pathlib, pytest, importlib
- test_config.py | Unit tests for application configuration settings including database URLs, auth defaults, and cookie policies across environments. | exp: func:test_settings_default_database_url_uses_asyncpg(monkeypatch) → None, call:monkeypatch.delenv, call:Settings, func:test_build_database_url_uses_explicit_values() → None, call:build_database_url, func:test_settings_prefers_explicit_database_url_env(monkeypatch) → None, call:monkeypatch.setenv, call:Settings, func:test_auth_settings_have_secure_defaults() → None, call:Settings, call:settings.resolved_authentik_authorize_url.endswith, call:settings.resolved_authentik_token_url.endswith, call:settings.resolved_authentik_jwks_url.endswith, func:test_cookie_policy_is_strict_in_production(monkeypatch) → None, call:monkeypatch.setenv, call:Settings, func:test_cookie_policy_is_relaxed_for_local_dev(monkeypatch) → None, call:monkeypatch.setenv, call:Settings | dep: pytest, src.config, src.database
- test_config_profile_resolver.py | Unit tests for config profile resolution, merging, and application, including inheritance, cycle detection, and mount normalization. | exp: class:TestMergeFunctions, method:test_merge_env_vars_basic(self) → None, call:_merge_env_vars, method:test_merge_env_vars_tracks_overrides(self) → None, call:_merge_env_vars, method:test_merge_runtime_hints_basic(self) → None, call:_merge_runtime_hints, method:test_merge_files_basic(self) → None, call:_merge_files, method:test_merge_mounts_basic(self) → None, call:_merge_mounts, method:test_merge_mounts_file_override(self) → None, call:_merge_mounts, call:ResolvedMount, method:test_merge_mounts_mode_conflict(self) → None, call:_merge_mounts, call:ResolvedMount, method:test_merge_git_mounts_basic(self) → None, call:_merge_git_mounts, call:len, method:test_merge_git_mounts_concatenate_same_repo_branch(self) → None, call:_merge_git_mounts, call:len, method:test_merge_git_mounts_dedup_same_mapping(self) → None, call:_merge_git_mounts, call:len, method:test_merge_git_mounts_different_repos(self) → None, call:_merge_git_mounts, call:len, method:test_merge_git_mounts_different_branches(self) → None, call:_merge_git_mounts, call:len, call:m.get, class:TestResolveProfile, class:TestApplyResolvedProfile, method:test_mounts_directory_not_individual_files(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:Path(volumes[0]["source"]).is_dir, call:(Path(volumes[0]["source"]) / "config.json").exists, call:(Path(volumes[0]["source"]) / "nested" / "file.txt").exists, method:test_directory_mount_target(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:Path, call:(Path(volumes[0]["source"]) / "z.json").exists, method:test_empty_mount_produces_no_volumes(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, method:test_home_expansion_in_directory_mount_target(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:(Path(volumes[0]["source"]) / "app.toml").exists, call:Path, method:test_readonly_mount_sets_readonly_flag(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:volumes[0].get, method:test_writable_mount_does_not_set_readonly_flag(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:volumes[0].get, class:TestCheckIncludeCycle | dep: uuid, pathlib, pytest, sqlalchemy.ext.asyncio, src.models.config.config_profile, src.services.config.config_profile_resolver
- test_docker_build.py | Unit tests for a Docker image build service that verifies subprocess invocation, Dockerfile/context file writing, command structure, error handling, and path traversal prevention. | exp: class:TestBuildImage | dep: subprocess, tempfile, pathlib, unittest.mock, pytest, src.services.build.docker_build
- test_docker_service.py | Unit tests for Docker service utilities including container ID/name retrieval and volume sorting by specificity. | exp: class:TestGetContainerId, class:TestGetContainerName, class:TestSortVolumesBySpecificity, method:test_parent_before_child(self) → None, call:sort_volumes_by_specificity, method:test_stable_sort_for_equal_depth(self) → None, call:sort_volumes_by_specificity, method:test_with_type_suffix(self) → None, call:sort_volumes_by_specificity, method:test_empty_list(self) → None, call:sort_volumes_by_specificity, method:test_single_volume(self) → None, call:sort_volumes_by_specificity, method:test_duplicate_target_warning(self, caplog) → None, call:caplog.at_level, call:sort_volumes_by_specificity | dep: unittest.mock, logging, src.services.docker.container, src.services.docker.compose, subprocess
- test_event_bus.py | Unit tests for an instance event bus that validates publish/subscribe, error isolation, async callback support, and unsubscribe functionality. | exp: func:event_bus() → InstanceEventBus, call:InstanceEventBus, call:bus._reset_for_testing, func:sample_payload() → InstanceEventPayload, call:str, call:uuid.uuid4, func:test_publish_delivers_to_all_subscribers(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:received.append, call:event_bus.subscribe, call:event_bus.publish, call:len, func:callback_1(payload: InstanceEventPayload) → None, call:received.append, func:callback_2(payload: InstanceEventPayload) → None, call:received.append, func:callback_3(payload: InstanceEventPayload) → None, call:received.append, func:test_subscriber_exception_isolation(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:received.append, call:event_bus.subscribe, call:event_bus.publish, raise:RuntimeError, func:bad_callback(_payload: InstanceEventPayload) → None, raise:RuntimeError, func:good_callback(_payload: InstanceEventPayload) → None, call:received.append, func:test_unsubscribe_removes_callback(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:received.append, call:event_bus.subscribe, call:unsubscribe, call:event_bus.publish, func:callback(_payload: InstanceEventPayload) → None, call:received.append, func:test_publish_to_empty_subscriber_list(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:event_bus.publish, func:test_async_subscriber_supported(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:asyncio.sleep, call:received.append, call:event_bus.subscribe, call:event_bus.publish, func:async_callback(_payload: InstanceEventPayload) → None, call:asyncio.sleep, call:received.append, func:test_unsubscribe_all_clears_subscribers(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:received.append, call:event_bus.subscribe, call:event_bus.unsubscribe_all, call:event_bus.publish, func:callback(_payload: InstanceEventPayload) → None, call:received.append | dep: asyncio, uuid, typing, pytest, src.services.instance.event_bus
- test_file_service.py | Unit tests for FileService validating directory listing, file read/write, binary rejection, and path traversal prevention. | exp: class:TestFileService, method:test_list_directory_empty(self, temp_workspace: Workspace), call:FileService, call:service.list_directory, method:test_list_directory_with_files(self, temp_workspace: Workspace), call:os.makedirs, call:os.path.join, call:open, call:f.write, call:FileService, call:service.list_directory, call:len, method:test_read_file(self, temp_workspace: Workspace), call:open, call:os.path.join, call:f.write, call:FileService, call:service.read_file, method:test_read_binary_file_rejected(self, temp_workspace: Workspace), call:open, call:os.path.join, call:f.write, call:FileService, call:pytest.raises, call:service.read_file, method:test_write_file(self, temp_workspace: Workspace), call:FileService, call:service.write_file, call:os.path.exists, call:os.path.join, call:open, call:f.read, method:test_path_escapes_workspace(self, temp_workspace: Workspace), call:FileService, call:pytest.raises, call:service.list_directory, func:temp_workspace(), call:tempfile.TemporaryDirectory, call:Workspace | dep: os, tempfile, pytest, src.models, src.services.shared.file_service, src.models.Workspace, src.services.shared.file_service.FileService
- test_git_operations.py | Unit tests for GitOperations class covering status, commit, history, and branch operations | exp: class:TestGitOperationsStatus, method:test_status_clean(self, temp_workspace: Workspace), call:GitOperations, call:asyncio.run, call:git.status, method:test_status_modified(self, temp_workspace: Workspace), call:open, call:os.path.join, call:f.write, call:GitOperations, call:asyncio.run, call:git.status, method:test_status_untracked(self, temp_workspace: Workspace), call:open, call:os.path.join, call:f.write, call:GitOperations, call:asyncio.run, call:git.status, class:TestGitOperationsCommit, method:test_commit_stages_and_commits(self, temp_workspace: Workspace), call:open, call:os.path.join, call:f.write, call:GitOperations, call:asyncio.run, call:git.commit, call:git.status, call:git.history, method:test_commit_fails_without_changes(self, temp_workspace: Workspace), call:GitOperations, call:pytest.raises, call:asyncio.run, call:git.commit, class:TestGitOperationsHistory, method:test_history_returns_commits(self, temp_workspace: Workspace), call:GitOperations, call:asyncio.run, call:git.history, call:len, method:test_history_filters_by_path(self, temp_workspace: Workspace), call:open, call:os.path.join, call:f.write, call:GitOperations, call:asyncio.run, call:git.commit, call:git.history, call:len, class:TestGitOperationsBranches, method:test_branches_lists_main(self, temp_workspace: Workspace), call:GitOperations, call:asyncio.run, call:git.branches, method:test_checkout_switches_branch(self, temp_workspace: Workspace), call:_run_git, call:GitOperations, call:asyncio.run, call:git.checkout, call:git.status, func:_run_git(*args: str, cwd: str) → None, call:subprocess.run, func:temp_workspace(), call:tempfile.TemporaryDirectory, call:_run_git, call:os.path.join, call:open, call:f.write, call:Workspace | dep: asyncio, os, subprocess, tempfile, pytest, src.models, src.services.git.git_operations, src.models.Workspace, src.services.git.git_operations.GitOperations
- test_git_service.py | Unit tests for GitService covering clone, fetch, pull, and branch existence checks via mocked subprocess calls | exp: class:TestGitServiceClone, class:TestGitServiceFetch, class:TestGitServicePull, class:TestGitServiceBranchExistsRemotely, method:test_branch_exists(self), call:MagicMock, call:patch, call:GitService.branch_exists_remotely, call:mock_run.assert_called_once_with, method:test_branch_not_exists(self), call:MagicMock, call:patch, call:GitService.branch_exists_remotely, method:test_ls_remote_fails(self), call:MagicMock, call:patch, call:GitService.branch_exists_remotely | dep: asyncio, unittest.mock, pytest, src.services.git.git_service
- test_git_url_parser.py | Unit tests for git URL parsing utilities that validate extraction, validation, and parsing of various GitHub, GitLab, and Bitbucket URL formats. | exp: class:TestExtractBaseRepoUrl, method:test_github_tree_url(self), call:extract_base_repo_url, method:test_github_blob_url(self), call:extract_base_repo_url, method:test_github_pull_url(self), call:extract_base_repo_url, method:test_github_issues_url(self), call:extract_base_repo_url, method:test_github_valid_url(self), call:extract_base_repo_url, method:test_github_url_with_query_params(self), call:extract_base_repo_url, method:test_gitlab_tree_url(self), call:extract_base_repo_url, method:test_gitlab_blob_url(self), call:extract_base_repo_url, method:test_gitlab_merge_request_url(self), call:extract_base_repo_url, method:test_gitlab_valid_url(self), call:extract_base_repo_url, method:test_bitbucket_src_url(self), call:extract_base_repo_url, method:test_bitbucket_valid_url(self), call:extract_base_repo_url, method:test_ssh_url(self), call:extract_base_repo_url, method:test_ssh_url_without_git_suffix(self), call:extract_base_repo_url, method:test_invalid_url(self), call:extract_base_repo_url, method:test_empty_url(self), call:extract_base_repo_url, class:TestIsValidCloneUrl, method:test_valid_ssh_url(self), call:is_valid_clone_url, method:test_valid_https_url(self), call:is_valid_clone_url, method:test_browser_url(self), call:is_valid_clone_url, method:test_url_without_git_suffix(self), call:is_valid_clone_url, method:test_invalid_url(self), call:is_valid_clone_url, class:TestParseGitUrl, method:test_valid_git_url(self), call:parse_git_url, method:test_browser_url(self), call:parse_git_url, method:test_invalid_url(self), call:parse_git_url, method:test_empty_url(self), call:parse_git_url, method:test_ssh_url(self), call:parse_git_url | dep: src.utils.git_url_parser, pytest
- test_health_monitor.py | Unit tests for HealthMonitor state-transition logic covering container crash detection, tunnel failure detection, recovery detection, write deduplication, and exception resilience. | exp: func:event_bus() → InstanceEventBus, call:InstanceEventBus, call:bus._reset_for_testing, func:health_monitor(event_bus: InstanceEventBus) → HealthMonitor, call:HealthMonitor, func:_create_running_instance(db_session) → ToolInstance, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, func:test_detects_container_crash(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:events_captured.append, call:event_bus.subscribe, call:patch, call:health_monitor._check_instance, call:db_session.refresh, call:len, call:db_session.execute, call:select(HealthCheck).where, call:result.scalar_one, func:capture_event(payload: InstanceEventPayload) → None, call:events_captured.append, func:test_detects_tunnel_failure(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:events_captured.append, call:event_bus.subscribe, call:patch, call:health_monitor._check_instance, call:db_session.refresh, call:len, call:db_session.execute, call:select(HealthCheck).where, call:result.scalar_one, func:capture_event(payload: InstanceEventPayload) → None, call:events_captured.append, func:test_detects_recovery(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:db_session.commit, call:HealthSnapshot, call:events_captured.append, call:event_bus.subscribe, call:patch, call:health_monitor._check_instance, call:db_session.refresh, call:len, call:db_session.execute, call:select(HealthCheck).where, call:result.scalar_one, func:capture_event(payload: InstanceEventPayload) → None, call:events_captured.append, func:test_skips_writes_when_no_state_change(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:HealthSnapshot, call:patch, call:health_monitor._check_instance, call:db_session.execute, call:select(HealthCheck).where, call:len, call:result.scalars().all, func:test_docker_exception_resilience(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:events_captured.append, call:event_bus.subscribe, call:patch, call:RuntimeError, call:health_monitor._check_instance, call:db_session.execute, call:select(HealthCheck).where, call:result.scalar_one_or_none, func:capture_event(payload: InstanceEventPayload) → None, call:events_captured.append, func:test_monitor_start_stop(health_monitor: HealthMonitor) → None, call:health_monitor.start, call:task.done, call:health_monitor.stop, call:suppress, call:task.cancelled | dep: asyncio, uuid, contextlib, unittest.mock, pytest, sqlalchemy, src.models.system.health_check, src.models.tool.tool_instance, src.models.user.user, src.services.instance.event_bus, src.services.instance.health_monitor
- test_home_path_expansion.py | Unit tests for tilde and $HOME expansion in container paths, and manifest-based home directory resolution. | exp: class:TestExpandContainerPath, method:test_tilde_slash_expands(self) → None, call:expand_container_path, method:test_tilde_alone_expands(self) → None, call:expand_container_path, method:test_dollar_home_slash_expands(self) → None, call:expand_container_path, method:test_dollar_home_alone_expands(self) → None, call:expand_container_path, method:test_absolute_path_unchanged(self) → None, call:expand_container_path, method:test_relative_path_unchanged(self) → None, call:expand_container_path, method:test_tilde_in_middle_unchanged(self) → None, call:expand_container_path, method:test_dollar_home_in_middle_unchanged(self) → None, call:expand_container_path, method:test_root_home(self) → None, call:expand_container_path, class:TestGetManifestHomeDir, method:test_with_user_block(self) → None, call:get_manifest_home_dir, method:test_without_user_block(self) → None, call:get_manifest_home_dir, method:test_with_empty_user_name(self) → None, call:get_manifest_home_dir, method:test_with_none_user_name(self) → None, call:get_manifest_home_dir | dep: pytest, src.services.config.config_profile_resolver, src.services.build.manifest_compiler
- test_instance_service.py | Unit tests for the tool instance service covering compose file modification, repository mount naming, profile/git mount stacking, and manifest instance preparation. | exp: class:TestModifyComposeFile, method:test_extra_volumes_expand_home_dir(self, tmp_path), call:compose_path.write_text, call:modify_compose_file, call:str, call:compose_path.read_text, method:test_working_directory_expands_home_dir(self, tmp_path), call:compose_path.write_text, call:modify_compose_file, call:str, call:compose_path.read_text, class:TestGetRepositoryMountName, method:test_uses_project_name(self), call:MagicMock, call:_get_repository_mount_name, method:test_slugifies_project_name(self), call:MagicMock, call:_get_repository_mount_name, class:TestStackProfileMountsWithGitMounts, method:test_exact_overlap_merges_profile_files_into_git_source(self, tmp_path) → None, call:git_source.mkdir, call:(git_source / "existing.txt").write_text, call:profile_source.mkdir, call:(profile_source / "settings.json").write_text, call:str, call:_stack_profile_mounts_with_git_mounts, call:(git_source / "existing.txt").read_text, call:(git_source / "settings.json").read_text, method:test_descendant_overlap_copies_into_subdirectory(self, tmp_path) → None, call:git_source.mkdir, call:(git_source / "README").write_text, call:profile_source.mkdir, call:(profile_source / "settings.json").write_text, call:str, call:_stack_profile_mounts_with_git_mounts, call:(git_source / "agent" / "settings.json").read_text, call:(git_source / "README").read_text, method:test_non_overlapping_mounts_left_untouched(self, tmp_path) → None, call:git_source.mkdir, call:profile_source.mkdir, call:(profile_source / "config").write_text, call:str, call:_stack_profile_mounts_with_git_mounts, method:test_git_source_file_does_not_consume_profile_mount(self, tmp_path) → None, call:git_source.write_text, call:profile_source.mkdir, call:(profile_source / "settings.json").write_text, call:str, call:_stack_profile_mounts_with_git_mounts, method:test_profile_source_file_copied_into_git_source(self, tmp_path) → None, call:git_source.mkdir, call:profile_source.write_text, call:str, call:_stack_profile_mounts_with_git_mounts, call:(git_source / "settings.json").read_text, class:Result, func:test_prepare_manifest_instance_uses_workspace_path_basename(), call:MagicMock, call:AsyncMock, call:Result, call:prepare_manifest_instance, func:session_get(model, obj_id), func:fake_run(cmd), call:Result | dep: unittest.mock, pytest, src.services.tool.instance_service, subprocess, src.services.tool
- __init__.py | Empty package initialization file that marks a directory as a Python package.
- test_alembic_migrations.py | Unit tests that verify Alembic database migration files are importable, have correct revision identifiers, and declare expected dependencies without requiring a live database. | exp: func:test_home_directory_migration_imports_and_rewrites() → None, call:Path, call:migration_path.exists, call:importlib.util.spec_from_file_location, call:importlib.util.module_from_spec, call:spec.loader.exec_module, call:callable, func:test_merge_migration_resolves_heads() → None, call:Path, call:migration_path.exists, call:importlib.util.spec_from_file_location, call:importlib.util.module_from_spec, call:spec.loader.exec_module, call:callable, func:test_remove_pi_agent_repo_mount_migration_imports() → None, call:Path, call:migration_path.exists, call:importlib.util.spec_from_file_location, call:importlib.util.module_from_spec, call:spec.loader.exec_module, call:callable, func:test_remove_pi_agent_workspace_symlink_migration_imports() → None, call:Path, call:migration_path.exists, call:importlib.util.spec_from_file_location, call:importlib.util.module_from_spec, call:spec.loader.exec_module, call:callable | dep: importlib.util, pathlib, pytest, pathlib.Path
- test_config.py | Unit tests verifying configuration settings including database URLs, auth defaults, and environment-specific cookie policies. | exp: func:test_settings_default_database_url_uses_asyncpg(monkeypatch) → None, call:monkeypatch.delenv, call:Settings, func:test_build_database_url_uses_explicit_values() → None, call:build_database_url, func:test_settings_prefers_explicit_database_url_env(monkeypatch) → None, call:monkeypatch.setenv, call:Settings, func:test_auth_settings_have_secure_defaults() → None, call:Settings, call:settings.resolved_authentik_authorize_url.endswith, call:settings.resolved_authentik_token_url.endswith, call:settings.resolved_authentik_jwks_url.endswith, func:test_cookie_policy_is_strict_in_production(monkeypatch) → None, call:monkeypatch.setenv, call:Settings, func:test_cookie_policy_is_relaxed_for_local_dev(monkeypatch) → None, call:monkeypatch.setenv, call:Settings | dep: pytest, src.config, src.database, src.config.Settings, src.database.build_database_url
- test_config_profile_resolver.py | Tests config profile resolution logic including merge functions, profile inheritance with includes, cycle detection, and git mount normalization. | exp: class:TestMergeFunctions, method:test_merge_env_vars_basic(self) → None, call:_merge_env_vars, method:test_merge_env_vars_tracks_overrides(self) → None, call:_merge_env_vars, method:test_merge_runtime_hints_basic(self) → None, call:_merge_runtime_hints, method:test_merge_files_basic(self) → None, call:_merge_files, method:test_merge_mounts_basic(self) → None, call:_merge_mounts, method:test_merge_mounts_file_override(self) → None, call:_merge_mounts, call:ResolvedMount, method:test_merge_mounts_mode_conflict(self) → None, call:_merge_mounts, call:ResolvedMount, method:test_merge_git_mounts_basic(self) → None, call:_merge_git_mounts, call:len, method:test_merge_git_mounts_concatenate_same_repo_branch(self) → None, call:_merge_git_mounts, call:len, method:test_merge_git_mounts_dedup_same_mapping(self) → None, call:_merge_git_mounts, call:len, method:test_merge_git_mounts_different_repos(self) → None, call:_merge_git_mounts, call:len, method:test_merge_git_mounts_different_branches(self) → None, call:_merge_git_mounts, call:len, call:m.get, class:TestResolveProfile, class:TestApplyResolvedProfile, method:test_mounts_directory_not_individual_files(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:Path(volumes[0]["source"]).is_dir, call:(Path(volumes[0]["source"]) / "config.json").exists, call:(Path(volumes[0]["source"]) / "nested" / "file.txt").exists, method:test_directory_mount_target(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:Path, call:(Path(volumes[0]["source"]) / "z.json").exists, method:test_empty_mount_produces_no_volumes(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, method:test_home_expansion_in_directory_mount_target(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:(Path(volumes[0]["source"]) / "app.toml").exists, call:Path, method:test_readonly_mount_sets_readonly_flag(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:volumes[0].get, method:test_writable_mount_does_not_set_readonly_flag(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:volumes[0].get, class:TestCheckIncludeCycle | dep: uuid, pathlib, pytest, sqlalchemy.ext.asyncio, src.models.config.config_profile, src.services.config.config_profile_resolver, pathlib.Path, sqlalchemy.ext.asyncio.AsyncSession
- test_docker_build.py | Unit tests for the `build_image` Docker build service function, verifying successful builds, error handling, file writing, and security. | exp: class:TestBuildImage | dep: subprocess, tempfile, pathlib, unittest.mock, pytest, src.services.build.docker_build, pathlib.Path
- test_docker_service.py | Unit tests for Docker container utilities (ID/name lookup) and volume sorting by mount specificity. | exp: class:TestGetContainerId, class:TestGetContainerName, class:TestSortVolumesBySpecificity, method:test_parent_before_child(self) → None, call:sort_volumes_by_specificity, method:test_stable_sort_for_equal_depth(self) → None, call:sort_volumes_by_specificity, method:test_with_type_suffix(self) → None, call:sort_volumes_by_specificity, method:test_empty_list(self) → None, call:sort_volumes_by_specificity, method:test_single_volume(self) → None, call:sort_volumes_by_specificity, method:test_duplicate_target_warning(self, caplog) → None, call:caplog.at_level, call:sort_volumes_by_specificity | dep: unittest.mock, logging, src.services.docker.container, src.services.docker.compose, subprocess
- test_event_bus.py | Unit tests for the InstanceEventBus pub/sub system, verifying event delivery, subscriber isolation, unsubscription, and async callback support. | exp: func:event_bus() → InstanceEventBus, call:InstanceEventBus, call:bus._reset_for_testing, func:sample_payload() → InstanceEventPayload, call:str, call:uuid.uuid4, func:test_publish_delivers_to_all_subscribers(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:received.append, call:event_bus.subscribe, call:event_bus.publish, call:len, func:callback_1(payload: InstanceEventPayload) → None, call:received.append, func:callback_2(payload: InstanceEventPayload) → None, call:received.append, func:callback_3(payload: InstanceEventPayload) → None, call:received.append, func:test_subscriber_exception_isolation(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:received.append, call:event_bus.subscribe, call:event_bus.publish, raise:RuntimeError, func:bad_callback(_payload: InstanceEventPayload) → None, raise:RuntimeError, func:good_callback(_payload: InstanceEventPayload) → None, call:received.append, func:test_unsubscribe_removes_callback(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:received.append, call:event_bus.subscribe, call:unsubscribe, call:event_bus.publish, func:callback(_payload: InstanceEventPayload) → None, call:received.append, func:test_publish_to_empty_subscriber_list(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:event_bus.publish, func:test_async_subscriber_supported(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:asyncio.sleep, call:received.append, call:event_bus.subscribe, call:event_bus.publish, func:async_callback(_payload: InstanceEventPayload) → None, call:asyncio.sleep, call:received.append, func:test_unsubscribe_all_clears_subscribers(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:received.append, call:event_bus.subscribe, call:event_bus.unsubscribe_all, call:event_bus.publish, func:callback(_payload: InstanceEventPayload) → None, call:received.append | dep: asyncio, uuid, typing, pytest, src.services.instance.event_bus
- test_file_service.py | Unit tests for FileService covering directory listing, file reading/writing, binary file rejection, and path traversal prevention. | exp: class:TestFileService, method:test_list_directory_empty(self, temp_workspace: Workspace), call:FileService, call:service.list_directory, method:test_list_directory_with_files(self, temp_workspace: Workspace), call:os.makedirs, call:os.path.join, call:open, call:f.write, call:FileService, call:service.list_directory, call:len, method:test_read_file(self, temp_workspace: Workspace), call:open, call:os.path.join, call:f.write, call:FileService, call:service.read_file, method:test_read_binary_file_rejected(self, temp_workspace: Workspace), call:open, call:os.path.join, call:f.write, call:FileService, call:pytest.raises, call:service.read_file, method:test_write_file(self, temp_workspace: Workspace), call:FileService, call:service.write_file, call:os.path.exists, call:os.path.join, call:open, call:f.read, method:test_path_escapes_workspace(self, temp_workspace: Workspace), call:FileService, call:pytest.raises, call:service.list_directory, func:temp_workspace(), call:tempfile.TemporaryDirectory, call:Workspace | dep: os, tempfile, pytest, src.models, src.services.shared.file_service, src.models.Workspace, src.services.shared.file_service.FileService
- test_git_operations.py | Unit tests for GitOperations methods including status, commit, history, and branch operations using temporary git repositories. | exp: class:TestGitOperationsStatus, method:test_status_clean(self, temp_workspace: Workspace), call:GitOperations, call:asyncio.run, call:git.status, method:test_status_modified(self, temp_workspace: Workspace), call:open, call:os.path.join, call:f.write, call:GitOperations, call:asyncio.run, call:git.status, method:test_status_untracked(self, temp_workspace: Workspace), call:open, call:os.path.join, call:f.write, call:GitOperations, call:asyncio.run, call:git.status, class:TestGitOperationsCommit, method:test_commit_stages_and_commits(self, temp_workspace: Workspace), call:open, call:os.path.join, call:f.write, call:GitOperations, call:asyncio.run, call:git.commit, call:git.status, call:git.history, method:test_commit_fails_without_changes(self, temp_workspace: Workspace), call:GitOperations, call:pytest.raises, call:asyncio.run, call:git.commit, class:TestGitOperationsHistory, method:test_history_returns_commits(self, temp_workspace: Workspace), call:GitOperations, call:asyncio.run, call:git.history, call:len, method:test_history_filters_by_path(self, temp_workspace: Workspace), call:open, call:os.path.join, call:f.write, call:GitOperations, call:asyncio.run, call:git.commit, call:git.history, call:len, class:TestGitOperationsBranches, method:test_branches_lists_main(self, temp_workspace: Workspace), call:GitOperations, call:asyncio.run, call:git.branches, method:test_checkout_switches_branch(self, temp_workspace: Workspace), call:_run_git, call:GitOperations, call:asyncio.run, call:git.checkout, call:git.status, func:_run_git(*args: str, cwd: str) → None, call:subprocess.run, func:temp_workspace(), call:tempfile.TemporaryDirectory, call:_run_git, call:os.path.join, call:open, call:f.write, call:Workspace | dep: asyncio, os, subprocess, tempfile, pytest, src.models, src.services.git.git_operations, src.models.Workspace, src.services.git.git_operations.GitOperations
- test_git_service.py | Unit tests for GitService methods including clone, fetch, pull, and branch_exists_remotely operations. | exp: class:TestGitServiceClone, class:TestGitServiceFetch, class:TestGitServicePull, class:TestGitServiceBranchExistsRemotely, method:test_branch_exists(self), call:MagicMock, call:patch, call:GitService.branch_exists_remotely, call:mock_run.assert_called_once_with, method:test_branch_not_exists(self), call:MagicMock, call:patch, call:GitService.branch_exists_remotely, method:test_ls_remote_fails(self), call:MagicMock, call:patch, call:GitService.branch_exists_remotely | dep: asyncio, unittest.mock, pytest, src.services.git.git_service, src.services.git.git_service.GitService
- test_git_url_parser.py | Tests for git URL parsing utilities including base URL extraction, clone URL validation, and full URL parsing across multiple Git hosting platforms. | exp: class:TestExtractBaseRepoUrl, method:test_github_tree_url(self), call:extract_base_repo_url, method:test_github_blob_url(self), call:extract_base_repo_url, method:test_github_pull_url(self), call:extract_base_repo_url, method:test_github_issues_url(self), call:extract_base_repo_url, method:test_github_valid_url(self), call:extract_base_repo_url, method:test_github_url_with_query_params(self), call:extract_base_repo_url, method:test_gitlab_tree_url(self), call:extract_base_repo_url, method:test_gitlab_blob_url(self), call:extract_base_repo_url, method:test_gitlab_merge_request_url(self), call:extract_base_repo_url, method:test_gitlab_valid_url(self), call:extract_base_repo_url, method:test_bitbucket_src_url(self), call:extract_base_repo_url, method:test_bitbucket_valid_url(self), call:extract_base_repo_url, method:test_ssh_url(self), call:extract_base_repo_url, method:test_ssh_url_without_git_suffix(self), call:extract_base_repo_url, method:test_invalid_url(self), call:extract_base_repo_url, method:test_empty_url(self), call:extract_base_repo_url, class:TestIsValidCloneUrl, method:test_valid_ssh_url(self), call:is_valid_clone_url, method:test_valid_https_url(self), call:is_valid_clone_url, method:test_browser_url(self), call:is_valid_clone_url, method:test_url_without_git_suffix(self), call:is_valid_clone_url, method:test_invalid_url(self), call:is_valid_clone_url, class:TestParseGitUrl, method:test_valid_git_url(self), call:parse_git_url, method:test_browser_url(self), call:parse_git_url, method:test_invalid_url(self), call:parse_git_url, method:test_empty_url(self), call:parse_git_url, method:test_ssh_url(self), call:parse_git_url | dep: src.utils.git_url_parser
- test_health_monitor.py | Unit tests for HealthMonitor state-transition logic, validating container crash detection, tunnel failure handling, recovery detection, and resilience to Docker exceptions. | exp: func:event_bus() → InstanceEventBus, call:InstanceEventBus, call:bus._reset_for_testing, func:health_monitor(event_bus: InstanceEventBus) → HealthMonitor, call:HealthMonitor, func:_create_running_instance(db_session) → ToolInstance, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, func:test_detects_container_crash(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:events_captured.append, call:event_bus.subscribe, call:patch, call:health_monitor._check_instance, call:db_session.refresh, call:len, call:db_session.execute, call:select(HealthCheck).where, call:result.scalar_one, func:capture_event(payload: InstanceEventPayload) → None, call:events_captured.append, func:test_detects_tunnel_failure(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:events_captured.append, call:event_bus.subscribe, call:patch, call:health_monitor._check_instance, call:db_session.refresh, call:len, call:db_session.execute, call:select(HealthCheck).where, call:result.scalar_one, func:capture_event(payload: InstanceEventPayload) → None, call:events_captured.append, func:test_detects_recovery(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:db_session.commit, call:HealthSnapshot, call:events_captured.append, call:event_bus.subscribe, call:patch, call:health_monitor._check_instance, call:db_session.refresh, call:len, call:db_session.execute, call:select(HealthCheck).where, call:result.scalar_one, func:capture_event(payload: InstanceEventPayload) → None, call:events_captured.append, func:test_skips_writes_when_no_state_change(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:HealthSnapshot, call:patch, call:health_monitor._check_instance, call:db_session.execute, call:select(HealthCheck).where, call:len, call:result.scalars().all, func:test_docker_exception_resilience(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:events_captured.append, call:event_bus.subscribe, call:patch, call:RuntimeError, call:health_monitor._check_instance, call:db_session.execute, call:select(HealthCheck).where, call:result.scalar_one_or_none, func:capture_event(payload: InstanceEventPayload) → None, call:events_captured.append, func:test_monitor_start_stop(health_monitor: HealthMonitor) → None, call:health_monitor.start, call:task.done, call:health_monitor.stop, call:suppress, call:task.cancelled | dep: asyncio, uuid, contextlib, unittest.mock, pytest, sqlalchemy, src.models.system.health_check, src.models.tool.tool_instance, src.models.user.user, src.services.instance.event_bus, src.services.instance.health_monitor
- test_home_path_expansion.py | Unit tests verifying tilde (~) and $HOME path expansion and home directory resolution from container manifests. | exp: class:TestExpandContainerPath, method:test_tilde_slash_expands(self) → None, call:expand_container_path, method:test_tilde_alone_expands(self) → None, call:expand_container_path, method:test_dollar_home_slash_expands(self) → None, call:expand_container_path, method:test_dollar_home_alone_expands(self) → None, call:expand_container_path, method:test_absolute_path_unchanged(self) → None, call:expand_container_path, method:test_relative_path_unchanged(self) → None, call:expand_container_path, method:test_tilde_in_middle_unchanged(self) → None, call:expand_container_path, method:test_dollar_home_in_middle_unchanged(self) → None, call:expand_container_path, method:test_root_home(self) → None, call:expand_container_path, class:TestGetManifestHomeDir, method:test_with_user_block(self) → None, call:get_manifest_home_dir, method:test_without_user_block(self) → None, call:get_manifest_home_dir, method:test_with_empty_user_name(self) → None, call:get_manifest_home_dir, method:test_with_none_user_name(self) → None, call:get_manifest_home_dir | dep: pytest, src.services.config.config_profile_resolver, src.services.build.manifest_compiler
- test_instance_service.py | Unit tests for tool instance service functions including compose file modification, repository mount name generation, profile/git mount stacking, and manifest instance preparation. | exp: class:TestModifyComposeFile, method:test_extra_volumes_expand_home_dir(self, tmp_path), call:compose_path.write_text, call:modify_compose_file, call:str, call:compose_path.read_text, method:test_working_directory_expands_home_dir(self, tmp_path), call:compose_path.write_text, call:modify_compose_file, call:str, call:compose_path.read_text, class:TestGetRepositoryMountName, method:test_uses_project_name(self), call:MagicMock, call:_get_repository_mount_name, method:test_slugifies_project_name(self), call:MagicMock, call:_get_repository_mount_name, class:TestStackProfileMountsWithGitMounts, method:test_exact_overlap_merges_profile_files_into_git_source(self, tmp_path) → None, call:git_source.mkdir, call:(git_source / "existing.txt").write_text, call:profile_source.mkdir, call:(profile_source / "settings.json").write_text, call:str, call:_stack_profile_mounts_with_git_mounts, call:(git_source / "existing.txt").read_text, call:(git_source / "settings.json").read_text, method:test_descendant_overlap_copies_into_subdirectory(self, tmp_path) → None, call:git_source.mkdir, call:(git_source / "README").write_text, call:profile_source.mkdir, call:(profile_source / "settings.json").write_text, call:str, call:_stack_profile_mounts_with_git_mounts, call:(git_source / "agent" / "settings.json").read_text, call:(git_source / "README").read_text, method:test_non_overlapping_mounts_left_untouched(self, tmp_path) → None, call:git_source.mkdir, call:profile_source.mkdir, call:(profile_source / "config").write_text, call:str, call:_stack_profile_mounts_with_git_mounts, method:test_git_source_file_does_not_consume_profile_mount(self, tmp_path) → None, call:git_source.write_text, call:profile_source.mkdir, call:(profile_source / "settings.json").write_text, call:str, call:_stack_profile_mounts_with_git_mounts, method:test_profile_source_file_copied_into_git_source(self, tmp_path) → None, call:git_source.mkdir, call:profile_source.write_text, call:str, call:_stack_profile_mounts_with_git_mounts, call:(git_source / "settings.json").read_text, class:Result, func:test_prepare_manifest_instance_uses_workspace_path_basename(), call:MagicMock, call:AsyncMock, call:Result, call:prepare_manifest_instance, func:session_get(model, obj_id), func:fake_run(cmd), call:Result, func:test_create_tool_instance_resolves_project_when_display_name_given(monkeypatch, tmp_path), call:uuid.uuid4, call:MagicMock, call:getattr, call:AsyncMock, call:monkeypatch.setattr, call:str, call:CreateInstanceRequest, call:instance_service.create_tool_instance, func:session_get(model, _obj_id), call:getattr | dep: uuid, unittest.mock, pytest, src.services.tool.instance_service, subprocess, src.services.tool, src.schemas.tool
- test_lifecycle_hooks.py | Unit tests for lifecycle hook helper functions that derive notification titles and determine whether events should trigger notifications. | exp: class:TestDeriveTitle, method:test_known_event_types(self) → None, call:_derive_title, method:test_unknown_event_type(self) → None, call:_derive_title, class:TestShouldNotify, method:test_error_events_are_notified(self) → None, call:_should_notify, method:test_health_changed_running_is_notified(self) → None, call:_should_notify, method:test_created_started_stopped_restarted_deleted_filtered(self) → None, call:_should_notify, method:test_health_changed_non_running_filtered(self) → None, call:_should_notify | dep: pytest, src.services.instance.lifecycle_hooks
- test_manifest_compiler.py | Unit tests for a manifest compiler that generates Dockerfiles, docker-compose files, and entrypoint scripts from manifest configurations. | exp: class:TestGetManifestHomeDir, method:test_home_directory_in_manifest_wins(self) → None, call:get_manifest_home_dir, method:test_user_name_derives_home(self) → None, call:get_manifest_home_dir, method:test_root_fallback(self) → None, call:get_manifest_home_dir, method:test_empty_home_directory_falls_back(self) → None, call:get_manifest_home_dir, class:TestCompileDockerfileHomeDirectory, method:test_env_home_and_workdir_use_project_directory(self) → None, call:compile_dockerfile, method:test_project_directory_created(self) → None, call:compile_dockerfile, method:test_runtime_workspace_not_baked_into_image(self) → None, call:compile_dockerfile, method:test_runtime_working_dir_overrides_home_workdir(self) → None, call:compile_dockerfile, method:test_working_dir_expands_tilde(self) → None, call:compile_dockerfile, class:TestCompileComposeHomeDirectory, method:test_default_repo_mount_synthesized(self) → None, call:compile_compose, method:test_explicit_repo_mount_preserved(self) → None, call:compile_compose, method:test_workspace_name_substituted_in_mount_target(self) → None, call:compile_compose, method:test_working_dir_expands_home(self) → None, call:compile_compose, class:TestCompileEntrypoint, method:test_entrypoint_creates_home_and_project_directory(self) → None, call:compile_entrypoint, method:test_entrypoint_removes_stale_placeholder_directory(self) → None, call:compile_entrypoint, method:test_entrypoint_does_not_create_workspace_symlink(self) → None, call:compile_entrypoint, method:test_entrypoint_fixes_mount_owners(self) → None, call:compile_entrypoint, func:test_compile_dockerfile_creates_config_dirs_for_user() → None, call:compile_dockerfile, func:test_compile_dockerfile_no_user_does_not_create_home() → None, call:compile_dockerfile, func:test_compile_dockerfile_uses_user_npm_prefix() → None, call:compile_dockerfile, func:test_compile_dockerfile_starts_as_root_and_drops_privileges() → None, call:compile_dockerfile, call:compile_entrypoint, func:test_compile_compose_does_not_pin_root_user() → None, call:compile_compose, func:test_get_manifest_container_user_returns_name() → None, call:get_manifest_container_user, func:test_get_manifest_container_user_falls_back_to_uid_gid() → None, call:get_manifest_container_user, func:test_get_manifest_container_user_returns_none_without_user() → None, call:get_manifest_container_user | dep: pytest, src.services.build.manifest_compiler
- test_migration_metadata.py | Validates Alembic migration files by dynamically importing them and verifying table definitions and revision chain metadata. | exp: func:test_initial_migration_defines_all_core_tables() → None, call:Path(__file__).resolve, call:spec_from_file_location, call:module_from_spec, call:spec.loader.exec_module, func:test_refresh_tokens_migration_has_expected_revision_chain() → None, call:Path(__file__).resolve, call:spec_from_file_location, call:module_from_spec, call:spec.loader.exec_module | dep: pytest, importlib.util, pathlib, pathlib.Path
- test_monitoring_models.py | Unit tests verifying creation, persistence, and querying of monitoring models (InstanceEvent and HealthCheck) with database migration compatibility. | exp: func:test_instance_event_creation(db_session) → None, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, call:InstanceEvent, call:db_session.refresh, call:isinstance, func:test_health_check_creation(db_session) → None, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, call:HealthCheck, call:db_session.refresh, call:isinstance, func:test_instance_event_query_by_instance(db_session) → None, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, call:InstanceEvent, call:db_session.execute, call:select(InstanceEvent).where, call:result.scalar_one | dep: uuid, datetime, pytest, sqlalchemy, src.models.system.health_check, src.models.system.instance_event, src.models.tool.tool_instance, src.models.user.user
- test_notification_service.py | Unit tests for NotificationService covering CRUD operations, filtering, ownership validation, and bulk actions on user notifications. | exp: func:notification_service() → NotificationService, call:NotificationService, func:user_a(db_session: AsyncSession) → User, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, func:user_b(db_session: AsyncSession) → User, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, func:test_create_notification(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:uuid.uuid4, func:test_list_notifications_orders_by_created_at_desc(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:datetime.now, call:timedelta, call:db_session.commit, call:db_session.refresh, call:notification_service.list_notifications, func:test_list_notifications_excludes_dismissed(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.dismiss, call:notification_service.list_notifications, func:test_list_notifications_unread_only(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.mark_read, call:notification_service.list_notifications, func:test_get_unread_count(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:range, call:notification_service.create_notification, call:notification_service.mark_read, call:notification_service.get_unread_count, func:test_mark_read_sets_read_at(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.mark_read, func:test_mark_all_read_affects_all_unread(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:range, call:notification_service.create_notification, call:notification_service.mark_all_read, call:notification_service.get_unread_count, func:test_dismiss_sets_dismissed_at(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.dismiss, call:db_session.execute, call:select(Notification).where, call:result.scalar_one, func:test_mark_read_wrong_owner_raises(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:notification_service.create_notification, call:pytest.raises, call:notification_service.mark_read, func:test_dismiss_wrong_owner_raises(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:notification_service.create_notification, call:pytest.raises, call:notification_service.dismiss, func:test_list_notifications_mute_categories(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.list_notifications, func:test_get_unread_count_excludes_dismissed(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.dismiss, call:notification_service.get_unread_count, func:test_dismiss_all_affects_all_non_dismissed(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:range, call:notification_service.create_notification, call:notification_service.dismiss_all, call:notification_service.list_notifications, func:test_dismiss_all_affects_only_caller(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:range, call:notification_service.create_notification, call:notification_service.dismiss_all, call:notification_service.list_notifications, func:test_mark_all_read_affects_only_caller(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:range, call:notification_service.create_notification, call:notification_service.mark_all_read, call:notification_service.get_unread_count | dep: uuid, datetime, pytest, sqlalchemy, sqlalchemy.ext.asyncio, src.models.system.notification, src.models.user.user, src.services.shared.notification_service, Notification, User, NotificationService
- test_notifications_api_routes.py | Tests FastAPI route ordering to ensure DELETE /notifications matches the bulk clear endpoint before the parameterized DELETE /notifications/{id} endpoint. | exp: func:test_delete_notifications_route_order() → None, call:FastAPI, call:app.include_router, call:TestClient, call:client.delete | dep: fastapi, fastapi.testclient, src.api.system.notifications
- test_permission_fixer.py | Unit tests for a Docker container permission fixer that applies chown/chmod/file_mode to mounts and SSH directories. | exp: class:TestApplyMountPermissions, class:TestRunInContainer, class:TestApplySshPermissions, class:TestCheckRootUserAvailable | dep: unittest.mock, pytest, src.services.shared.permission_fixer, subprocess
- test_readiness_probe.py | Unit tests for a Docker container readiness probe service that executes commands with retry logic and timeout handling | exp: class:TestExecuteProbe, class:TestIntegrationScenarios | dep: unittest.mock, src.services.shared.readiness_probe, subprocess
- test_ssh_keys.py | Unit tests for SSH key file preparation functionality including file creation, permissions, ownership, and error handling. | exp: class:TestPrepareSshKeyFiles | dep: os, pathlib, unittest.mock, pytest, src.services.shared.ssh_keys
- test_terminal_session.py | Unit tests for TerminalSession verifying docker exec command construction with/without container user flag | exp: func:test_start_passes_container_user_to_docker_exec() → None, call:TerminalSession, call:str, call:uuid.uuid4, call:patch, call:AsyncMock, call:session.start, call:args.index, func:test_start_omits_user_when_not_configured() → None, call:TerminalSession, call:str, call:uuid.uuid4, call:patch, call:AsyncMock, call:session.start | dep: uuid, unittest.mock, pytest, src.services.terminal.terminal_session, pty, asyncio, os
- test_migration_metadata.py | Unit tests that dynamically load and validate Alembic migration files for correct table definitions and revision chains. | exp: func:test_initial_migration_defines_all_core_tables() → None, call:Path(__file__).resolve, call:spec_from_file_location, call:module_from_spec, call:spec.loader.exec_module, func:test_refresh_tokens_migration_has_expected_revision_chain() → None, call:Path(__file__).resolve, call:spec_from_file_location, call:module_from_spec, call:spec.loader.exec_module | dep: pytest, importlib.util, pathlib
- test_monitoring_models.py | Tests creation, persistence, and querying of monitoring models (InstanceEvent and HealthCheck) with related User and ToolInstance fixtures. | exp: func:test_instance_event_creation(db_session) → None, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, call:InstanceEvent, call:db_session.refresh, call:isinstance, func:test_health_check_creation(db_session) → None, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, call:HealthCheck, call:db_session.refresh, call:isinstance, func:test_instance_event_query_by_instance(db_session) → None, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, call:InstanceEvent, call:db_session.execute, call:select(InstanceEvent).where, call:result.scalar_one | dep: uuid, datetime, pytest, sqlalchemy, src.models.system.health_check, src.models.system.instance_event, src.models.tool.tool_instance, src.models.user.user
- test_notification_service.py | Unit tests for NotificationService covering creation, listing, filtering, reading, dismissing, and ownership validation of user notifications. | exp: func:notification_service() → NotificationService, call:NotificationService, func:user_a(db_session: AsyncSession) → User, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, func:user_b(db_session: AsyncSession) → User, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, func:test_create_notification(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:uuid.uuid4, func:test_list_notifications_orders_by_created_at_desc(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:datetime.now, call:timedelta, call:db_session.commit, call:db_session.refresh, call:notification_service.list_notifications, func:test_list_notifications_excludes_dismissed(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.dismiss, call:notification_service.list_notifications, func:test_list_notifications_unread_only(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.mark_read, call:notification_service.list_notifications, func:test_get_unread_count(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:range, call:notification_service.create_notification, call:notification_service.mark_read, call:notification_service.get_unread_count, func:test_mark_read_sets_read_at(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.mark_read, func:test_mark_all_read_affects_all_unread(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:range, call:notification_service.create_notification, call:notification_service.mark_all_read, call:notification_service.get_unread_count, func:test_dismiss_sets_dismissed_at(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.dismiss, call:db_session.execute, call:select(Notification).where, call:result.scalar_one, func:test_mark_read_wrong_owner_raises(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:notification_service.create_notification, call:pytest.raises, call:notification_service.mark_read, func:test_dismiss_wrong_owner_raises(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:notification_service.create_notification, call:pytest.raises, call:notification_service.dismiss, func:test_list_notifications_mute_categories(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.list_notifications, func:test_get_unread_count_excludes_dismissed(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.dismiss, call:notification_service.get_unread_count, func:test_dismiss_all_affects_all_non_dismissed(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:range, call:notification_service.create_notification, call:notification_service.dismiss_all, call:notification_service.list_notifications, func:test_dismiss_all_affects_only_caller(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:range, call:notification_service.create_notification, call:notification_service.dismiss_all, call:notification_service.list_notifications, func:test_mark_all_read_affects_only_caller(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:range, call:notification_service.create_notification, call:notification_service.mark_all_read, call:notification_service.get_unread_count | dep: uuid, datetime, pytest, sqlalchemy, sqlalchemy.ext.asyncio, src.models.system.notification, src.models.user.user, src.services.shared.notification_service
- test_notifications_api_routes.py | Tests that the FastAPI route ordering for notifications is correct so the bulk DELETE endpoint doesn't get intercepted by the single-item path parameter route. | exp: func:test_delete_notifications_route_order() → None, call:FastAPI, call:app.include_router, call:TestClient, call:client.delete | dep: fastapi, fastapi.testclient, src.api.system.notifications, FastAPI, TestClient
- test_permission_fixer.py | Unit tests for the permission fixer module that validates Docker container permission management (chown/chmod operations on mounts and SSH directories). | exp: class:TestApplyMountPermissions, class:TestRunInContainer, class:TestApplySshPermissions, class:TestCheckRootUserAvailable | dep: unittest.mock, pytest, src.services.shared.permission_fixer, subprocess
- test_readiness_probe.py | Unit tests for the readiness probe service that validates container health checks via Docker exec commands with retry logic. | exp: class:TestExecuteProbe, class:TestIntegrationScenarios | dep: unittest.mock, src.services.shared.readiness_probe
- test_ssh_keys.py | Unit tests for the `prepare_ssh_key_files` function, verifying file creation, permissions, ownership setting, and error handling. | exp: class:TestPrepareSshKeyFiles | dep: os, pathlib, unittest.mock, pytest, src.services.shared.ssh_keys, pathlib.Path
- test_terminal_container_user.py | Unit tests for resolving container user from tool type definitions, handling legacy tools, manifest-based tools, base definition inheritance, and missing definitions. | exp: func:test_resolve_container_user_legacy_tool_type(), call:MagicMock, call:AsyncMock, call:_resolve_container_user, call:session.get.assert_awaited_once, func:test_resolve_container_user_manifest_no_manifest_id(), call:MagicMock, call:AsyncMock, call:_resolve_container_user, func:test_resolve_container_user_manifest_with_user(), call:MagicMock, call:AsyncMock, call:_resolve_container_user, func:session_get(model, obj_id), func:test_resolve_container_user_manifest_with_base_definition(), call:MagicMock, call:AsyncMock, call:_resolve_container_user, func:session_get(model, obj_id), func:test_resolve_container_user_missing_manifest_definition(), call:MagicMock, call:AsyncMock, call:_resolve_container_user, func:session_get(model, obj_id) | dep: unittest.mock, pytest, src.api.system.terminal, src.api.system.terminal._resolve_container_user
- test_terminal_session.py | Unit tests verifying TerminalSession passes container_user to docker exec --user flag when set, and omits it when not configured. | exp: func:test_start_passes_container_user_to_docker_exec() → None, call:TerminalSession, call:str, call:uuid.uuid4, call:patch, call:AsyncMock, call:session.start, call:args.index, func:test_start_omits_user_when_not_configured() → None, call:TerminalSession, call:str, call:uuid.uuid4, call:patch, call:AsyncMock, call:session.start | dep: uuid, unittest.mock, pytest, src.services.terminal.terminal_session
## arch
Follows pytest-based unit testing architecture with heavy use of mocking (subprocess, filesystem, database) to test services in isolation; tests are organized by service/module with descriptive naming; employs parametrization and fixture-based setup for testing edge cases, error conditions, and path traversal prevention across infrastructure and security-critical components.
Standard Python unittest/pytest package structure with test files mirroring the source codebase; uses dependency injection, mocking (likely unittest.mock or pytest-mock), and temporary filesystem/git fixtures to test components in isolation.
## tags
test, url, call:, git, call:notification, mounts, home, merge
test, url, call:, git, user, call:notification, container, mounts
## symbols
- TestMergeFunctions
- TestResolveProfile
@@ -48,5 +49,7 @@ test, url, call:, git, call:notification, mounts, home, merge
read: __init__.py, test_alembic_migrations.py, test_config.py
- change unit config
read: test_config.py, test_config_profile_resolver.py
- explore unit subdirectories
index: apps/api/tests/unit/.ruff_cache/.pi-map.index.md
## dirty
-
@@ -67,3 +67,23 @@ def test_remove_pi_agent_repo_mount_migration_imports() -> None:
assert module.down_revision == "2026_06_14_182955"
assert callable(module.upgrade)
assert callable(module.downgrade)
@pytest.mark.unit
def test_remove_pi_agent_workspace_symlink_migration_imports() -> None:
migration_path = Path(__file__).parent.parent.parent / (
"alembic/versions/2026_06_19_113000_remove_pi_agent_workspace_symlink.py"
)
assert migration_path.exists()
spec = importlib.util.spec_from_file_location(
"remove_workspace_symlink_migration", migration_path
)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
assert module.revision == "2026_06_19_113000"
assert module.down_revision == "2026_06_15_090500"
assert callable(module.upgrade)
assert callable(module.downgrade)
@@ -0,0 +1,143 @@
"""Unit tests for terminal container-user resolution."""
from unittest.mock import AsyncMock, MagicMock
import pytest
from src.api.system.terminal import _resolve_container_user
@pytest.mark.unit
async def test_resolve_container_user_legacy_tool_type():
"""Legacy tools have no manifest user."""
tool_type = MagicMock()
tool_type.definition_type = "legacy"
tool_type.manifest_id = None
instance = MagicMock()
instance.tool_type_id = "tool-type-uuid"
session = AsyncMock()
session.get = AsyncMock(return_value=tool_type)
result = await _resolve_container_user(session, instance)
assert result is None
session.get.assert_awaited_once()
@pytest.mark.unit
async def test_resolve_container_user_manifest_no_manifest_id():
"""Manifest-based tools without a manifest_id return None."""
tool_type = MagicMock()
tool_type.definition_type = "manifest"
tool_type.manifest_id = None
instance = MagicMock()
instance.tool_type_id = "tool-type-uuid"
session = AsyncMock()
session.get = AsyncMock(return_value=tool_type)
result = await _resolve_container_user(session, instance)
assert result is None
@pytest.mark.unit
async def test_resolve_container_user_manifest_with_user():
"""Manifest-based tools resolve the declared container user."""
manifest_id = "manifest-uuid"
tool_type = MagicMock()
tool_type.definition_type = "manifest"
tool_type.manifest_id = manifest_id
manifest_def = MagicMock()
manifest_def.manifest = {"user": {"name": "dev", "uid": 1000, "gid": 1000}}
manifest_def.base_definition_id = None
instance = MagicMock()
instance.tool_type_id = "tool-type-uuid"
session = AsyncMock()
async def session_get(model, obj_id):
if obj_id == "tool-type-uuid":
return tool_type
if obj_id == manifest_id:
return manifest_def
return None
session.get.side_effect = session_get
result = await _resolve_container_user(session, instance)
assert result == "dev"
@pytest.mark.unit
async def test_resolve_container_user_manifest_with_base_definition():
"""Base-definition users are inherited and overridden by tool manifests."""
base_id = "base-uuid"
manifest_id = "manifest-uuid"
tool_type = MagicMock()
tool_type.definition_type = "manifest"
tool_type.manifest_id = manifest_id
manifest_def = MagicMock()
manifest_def.manifest = {"user": {"name": "override", "uid": 1001, "gid": 1001}}
manifest_def.base_definition_id = base_id
base_def = MagicMock()
base_def.manifest = {
"base_image": "ubuntu:24.04",
"user": {"name": "base", "uid": 1000, "gid": 1000},
}
instance = MagicMock()
instance.tool_type_id = "tool-type-uuid"
session = AsyncMock()
async def session_get(model, obj_id):
if obj_id == "tool-type-uuid":
return tool_type
if obj_id == manifest_id:
return manifest_def
if obj_id == base_id:
return base_def
return None
session.get.side_effect = session_get
result = await _resolve_container_user(session, instance)
assert result == "override"
@pytest.mark.unit
async def test_resolve_container_user_missing_manifest_definition():
"""A manifest_id pointing to a missing definition returns None."""
manifest_id = "manifest-uuid"
tool_type = MagicMock()
tool_type.definition_type = "manifest"
tool_type.manifest_id = manifest_id
instance = MagicMock()
instance.tool_type_id = "tool-type-uuid"
session = AsyncMock()
async def session_get(model, obj_id):
if obj_id == "tool-type-uuid":
return tool_type
return None
session.get.side_effect = session_get
result = await _resolve_container_user(session, instance)
assert result is None
+51 -18
View File
@@ -1,9 +1,9 @@
"""Unit tests for TerminalSession docker exec invocation."""
"""Unit tests for TerminalSession PTY handling."""
import uuid
from unittest.mock import AsyncMock, patch
import pytest
import pytest # pyright: ignore[reportMissingImports]
from src.services.terminal.terminal_session import TerminalSession
@@ -19,16 +19,20 @@ async def test_start_passes_container_user_to_docker_exec() -> None:
container_user="dev",
)
with patch(
"src.services.terminal.terminal_session.pty.openpty",
return_value=(1, 2),
):
with patch(
with (
patch(
"src.services.terminal.terminal_session.pty.openpty",
return_value=(1, 2),
),
patch("src.services.terminal.terminal_session.os.set_blocking") as set_blocking,
patch("src.services.terminal.terminal_session.tty.setraw"),
patch(
"src.services.terminal.terminal_session.asyncio.create_subprocess_exec",
new=AsyncMock(),
) as mock_exec:
with patch("src.services.terminal.terminal_session.os.close"):
await session.start()
) as mock_exec,
patch("src.services.terminal.terminal_session.os.close"),
):
await session.start()
args, _kwargs = mock_exec.call_args
assert "docker" in args
@@ -37,6 +41,7 @@ async def test_start_passes_container_user_to_docker_exec() -> None:
user_index = args.index("--user")
assert args[user_index + 1] == "dev"
assert "container-123" in args
set_blocking.assert_called_once_with(1, False)
@pytest.mark.unit
@@ -49,16 +54,44 @@ async def test_start_omits_user_when_not_configured() -> None:
container_id="container-123",
)
with patch(
"src.services.terminal.terminal_session.pty.openpty",
return_value=(1, 2),
):
with patch(
with (
patch(
"src.services.terminal.terminal_session.pty.openpty",
return_value=(1, 2),
),
patch("src.services.terminal.terminal_session.os.set_blocking"),
patch("src.services.terminal.terminal_session.tty.setraw"),
patch(
"src.services.terminal.terminal_session.asyncio.create_subprocess_exec",
new=AsyncMock(),
) as mock_exec:
with patch("src.services.terminal.terminal_session.os.close"):
await session.start()
) as mock_exec,
patch("src.services.terminal.terminal_session.os.close"),
):
await session.start()
args, _kwargs = mock_exec.call_args
assert "--user" not in args
@pytest.mark.unit
@pytest.mark.asyncio
async def test_write_input_retries_partial_pty_writes() -> None:
"""A large paste is fully written even when the PTY accepts it in chunks."""
session = TerminalSession(
session_id=str(uuid.uuid4()),
instance_id=uuid.uuid4(),
container_id="container-123",
)
session._master_fd = 42
with patch(
"src.services.terminal.terminal_session.os.write",
side_effect=[2, 2, 1],
) as write:
await session.write_input(b"hello")
assert [bytes(call.args[1]) for call in write.call_args_list] == [
b"hello",
b"llo",
b"o",
]
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/web
## role
Frontend web application serving as the main user interface for the "Headquarter" project, built with React and Vite.
Frontend web application for a terminal/code-editor interface named "Headquarter," built with React/TypeScript using Vite and served as a static single-page app.
## parent
index: apps/.pi-map.index.md
map: apps/.pi-map.md
+10 -10
View File
@@ -4,21 +4,21 @@ dir: apps/web
index: apps/web/.pi-map.index.md
## role
Frontend web application serving as the main user interface for the "Headquarter" project, built with React and Vite.
Frontend web application for a terminal/code-editor interface named "Headquarter," built with React/TypeScript using Vite and served as a static single-page app.
## files
- .env.example | Template file showing required environment variables for a Vite-based frontend application | dep: Vite
- .env.example | Template/example file defining environment variables for a Vite-based application's API and OAuth configuration | dep: Vite
- .eslintrc.cjs | Configures ESLint for a TypeScript browser project with modern ES2022 module support | dep: eslint, @typescript-eslint/parser, @typescript-eslint/eslint-plugin
- Dockerfile | Multi-stage Docker build for a Vite/Node.js frontend application, compiling static assets and serving them via nginx as a non-root user | dep: node:20-alpine, nginx:alpine, npm, wget
- index.html | Serves as the entry point HTML file for a React application named "Headquarter" that loads a TypeScript module and uses Google Fonts. | dep: Google Fonts (Inter, IBM Plex Mono), React (implied by root div and main.tsx)
- Dockerfile | Multi-stage Docker build for a Node.js/Vite frontend application compiled to static assets and served by nginx as a non-root user | dep: node:20-alpine, nginx:alpine, npm, wget
- index.html | Entry point HTML file for a React application named "Headquarter" that loads a TypeScript module and Google Fonts | dep: Google Fonts (Inter, IBM Plex Mono), main.tsx module
- nginx.conf | Configures Nginx as a web server for a single-page application with gzip compression, client-side routing support, cache control for static assets, and a health check endpoint. | dep: nginx
- package-lock.json | Auto-generated dependency lock file that records exact versions and resolved URLs of all npm packages for reproducible installs in the "headquarter-web" React web application. | dep: npm, Node.js, React, Vite, TailwindCSS, xterm, PrismJS, axios, react-router-dom, TypeScript, ESLint, Vitest, @testing-library/react, @phosphor-icons/react, react-simple-code-editor, jsdom, postcss, autoprefixer
- package.json | Defines a React-based web application project named "headquarter-web" with configuration for building, development, testing, and linting using Vite and TypeScript. | dep: react, react-dom, react-router-dom, axios, tailwindcss, prismjs, xterm, @phosphor-icons/react, react-simple-code-editor, vite, typescript, vitest, eslint, @testing-library/react
- tsconfig.json | Configures TypeScript compiler settings for a modern React project using Vite with ES modules | dep: TypeScript, Vite, React, DOM
- vite.config.ts | Configures Vite build tool for a React project with custom dev server port and testing setup | dep: vite, @vitejs/plugin-react
- package-lock.json | Records exact dependency versions for a React-based web application to ensure reproducible installs | dep: npm, React, React Router, Tailwind CSS, Vite, Vitest, TypeScript, ESLint, xterm, PrismJS, axios, phosphor-icons
- package.json | Defines a React-based web application project named "headquarter-web" with build tooling, testing, and UI dependencies for a terminal/code editor interface. | dep: react, react-dom, react-router-dom, vite, typescript, tailwindcss, axios, prismjs, xterm, @phosphor-icons/react, react-simple-code-editor, vitest, eslint
- tsconfig.json | Configures TypeScript compiler options for a modern React/Vite project with ES2020 target and strict type checking | dep: TypeScript, React, Vite
- vite.config.ts | Configures Vite build tool with React support, dev server port, and Vitest testing settings | dep: vite, @vitejs/plugin-react
## arch
Modern SPA architecture using React with TypeScript, Vite for build tooling, Docker multi-stage builds with nginx static serving, and environment-based configuration.
Containerized single-page React/TypeScript app using Vite for build/dev, Vitest for testing, ESLint for linting, and nginx for static asset delivery with client-side routing support.
## tags
react, vite, eslint, typescript, application, nginx, web, configures
react, vite, eslint, application, typescript, nginx, configures, web
## symbols
-
## workflows
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/web/src
## role
Frontend web application entry point and routing layer for a React-based collaborative development platform.
Frontend web client that bootstraps and renders a React single-page application with routing, authentication, and session-driven UI.
## parent
index: apps/web/.pi-map.index.md
map: apps/web/.pi-map.md
+6 -6
View File
@@ -4,15 +4,15 @@ dir: apps/web/src
index: apps/web/src/.pi-map.index.md
## role
Frontend web application entry point and routing layer for a React-based collaborative development platform.
Frontend web client that bootstraps and renders a React single-page application with routing, authentication, and session-driven UI.
## files
- main.tsx | Entry point that bootstraps a React application with routing, authentication, and session management providers. | dep: react, react-dom/client, react-router-dom, ./router, ./state/auth, ./state/sessions, ./styles/tokens.css, ./styles/global.css, ./styles/utilities.css, ./styles/syntax-highlight.css, ./styles/pages/git-history.css, ./styles/pages/projects.css, ./styles/pages/sessions.css, ./styles/pages/ssh-keys.css, ./styles/pages/workspace-detail.css, ./styles/pages/workspaces.css, react-dom
- router.tsx | Defines the client-side routing configuration for a React application using nested routes with authentication protection and redirects. | exp: AppRouter | dep: react-router-dom, ./components/app-shell, ./components/protected-route, ./pages/DashboardPage, ./pages/PlaceholderPage, ./pages/ProfilePage, ./pages/ProjectsPage, ./pages/GitRepositoriesPage, ./pages/GitHistoryPage, ./pages/ProjectSettingsPage, ./pages/SettingsPage, ./pages/TerminalPage, ./pages/ToolWorkshopPage, ./pages/SshKeysPage, ./pages/ConfigProfilesPage, ./pages/SessionsPage, ./pages/WorkspacesPage, ./pages/WorkspaceDetailPage
- types.ts | Defines TypeScript type declarations for session users, projects, workspaces, and repositories in an application. | exp: SessionUser, SessionPayload, Project, WorkspaceSummary, RepositorySummary, ProjectWithRepos
- main.tsx | Entry point that bootstraps a React SPA with routing, authentication, and session state management | dep: react, react-dom/client, react-router-dom, ./router, ./state/auth, ./state/sessions, ./styles/tokens.css, ./styles/global.css, ./styles/utilities.css, ./styles/syntax-highlight.css, ./styles/pages/git-history.css, ./styles/pages/projects.css, ./styles/pages/sessions.css, ./styles/pages/ssh-keys.css, ./styles/pages/workspace-detail.css, ./styles/pages/workspaces.css, react-dom
- router.tsx | Defines the React Router configuration for a multi-page application with nested routes, protected authentication, and redirects. | exp: AppRouter | dep: react-router-dom, ./components/app-shell, ./components/protected-route, ./pages/DashboardPage, ./pages/PlaceholderPage, ./pages/ProfilePage, ./pages/ProjectsPage, ./pages/GitRepositoriesPage, ./pages/GitHistoryPage, ./pages/ProjectSettingsPage, ./pages/SettingsPage, ./pages/TerminalPage, ./pages/ToolWorkshopPage, ./pages/SshKeysPage, ./pages/ConfigProfilesPage, ./pages/SessionsPage, ./pages/WorkspacesPage, ./pages/WorkspaceDetailPage
- types.ts | Defines TypeScript type declarations for user sessions, projects, repositories, and workspaces in an application. | exp: SessionUser, SessionPayload, Project, WorkspaceSummary, RepositorySummary, ProjectWithRepos
## arch
Modular React SPA with provider-based dependency injection, nested route guards for authentication/authorization, and centralized TypeScript type definitions.
React SPA built with a declarative React Router configuration, protected/nested routes, and centralized TypeScript types for users, sessions, projects, repositories, and workspaces.
## tags
pages, styles, css, react, router, session, dom, project
pages, styles, css, router, react, session, dom, project
## symbols
- AppRouter
- SessionUser
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/web/src/components
## role
Reusable React UI components providing the application's shared visual elements, layout shell, authentication guards, and notification infrastructure.
Package components
## parent
index: apps/web/src/.pi-map.index.md
map: apps/web/src/.pi-map.md
+10 -10
View File
@@ -4,22 +4,22 @@ dir: apps/web/src/components
index: apps/web/src/components/.pi-map.index.md
## role
Reusable React UI components providing the application's shared visual elements, layout shell, authentication guards, and notification infrastructure.
Package components
## files
- app-shell.tsx | Provides the main application shell layout with navigation, session management, and responsive mobile/desktop rendering for a React application. | exp: AppShell | dep: react-router-dom, ../api/sessions, ../hooks/use-theme, ../utils/open-session, ../state/auth, ../state/sessions, ../hooks/use-mobile-viewport, ../state/events, ../state/toast, ../state/notifications, ./features/notification/event-toast-bridge, ./features/notification/notification-center, ./icon, ./features/mobile/mobile-nav, ./features/tool/start-tool-fab, ../utils/icons
- app-shell.tsx | Main application layout shell with responsive navigation, session management, and global state providers for notifications and toasts. | exp: AppShell | dep: react-router-dom, ../api/sessions, ../hooks/use-theme, ../utils/open-session, ../state/auth, ../state/sessions, ../hooks/use-mobile-viewport, ../state/events, ../state/toast, ../state/notifications, ./features/notification/event-toast-bridge, ./features/notification/notification-center, ./icon, ./features/mobile/mobile-nav, ./features/tool/start-tool-fab, ../utils/icons
- code-editor.tsx | A React component that renders a syntax-highlighting code editor with line numbers using react-simple-code-editor. | exp: CodeEditor | dep: react, react-simple-code-editor, ../utils/language
- data-states.tsx | Provides reusable React components for displaying loading, error, and empty data states in a UI. | exp: LoadingState, ErrorState, EmptyState | dep: ./icon, icon
- icon.tsx | Provides a centralized, type-safe React icon component that maps semantic names to Phosphor icons with configurable size, weight, color, and accessibility properties. | exp: IconName, IconProps, Icon | dep: react, @phosphor-icons/react
- loading-overlay.tsx | Renders an accessible loading overlay with optional label when visible prop is true | exp: func:LoadingOverlay({ visible, label }: LoadingOverlayProps) | dep: ./icon, icon
- protected-route.test.tsx | Tests a ProtectedRoute component that guards private content based on authentication state | dep: @testing-library/react, react-router-dom, vitest, ./protected-route, ../state/auth
- protected-route.tsx | A React component that conditionally renders children or redirects to login based on authentication state, preserving the intended destination URL. | exp: ProtectedRoute | dep: react-router-dom, ../state/auth
- syntax-highlighter.tsx | Renders syntax-highlighted code blocks with optional line numbers, language badge, and copy-to-clipboard functionality. | exp: SyntaxHighlighter | dep: react, ./icon, ../utils/language, React, icon, language utilities
- data-states.tsx | Provides reusable UI components for rendering loading, error, and empty data states. | exp: LoadingState, ErrorState, EmptyState | dep: ./icon, Icon
- icon.tsx | Provides a centralized, type-safe React icon component that maps semantic names to Phosphor icons with configurable sizes, weights, and colors. | exp: IconName, IconProps, Icon | dep: react, @phosphor-icons/react
- loading-overlay.tsx | Renders an accessible loading overlay with optional text label when visible | exp: func:LoadingOverlay({ visible, label }: LoadingOverlayProps) | dep: ./icon, icon
- protected-route.test.tsx | Tests a ProtectedRoute component that guards content based on authentication state, showing loading or redirecting to login | dep: @testing-library/react, react-router-dom, vitest, ./protected-route
- protected-route.tsx | Conditionally renders children or redirects to login based on authentication state | exp: ProtectedRoute | dep: react-router-dom, ../state/auth
- syntax-highlighter.tsx | A React component that renders syntax-highlighted code with optional line numbers and a copy-to-clipboard button | exp: SyntaxHighlighter | dep: react, ./icon, ../utils/language, React, icon, language utils
- toast-rules.test.ts | Unit tests for mapping instance events to toast notification categories and severities | dep: vitest, ./toast-rules, ../types/events
- toast-rules.ts | Maps instance events to toast notifications with deduplication logic | exp: func:mapEventToCategory(event: InstanceEventPayload) → string, call:event.event.startsWith, func:mapEventToSeverity(event: InstanceEventPayload) → "info" | "warning" | "error" | "success", func:handleEventToast(event: InstanceEventPayload) → void, call:shouldShowToast, call:toast.info, call:toast.success, call:toast.warning, call:toast.error, func:clearToastDedup() → void, call:lastToastTime.clear | dep: ../state/toast, ../types/events, toast, InstanceEventPayload
## arch
Component-based architecture with functional React patterns, separation of presentational components (data states, icons, syntax highlighting) from behavioral logic (toast rules, protected routes), and centralized design abstractions (icon mapping, code editor wrapping).
Contains 10 files.
## tags
toast, react, state, icon, code, loading, event, editor
toast, state, icon, react, loading, code, event, editor
## symbols
- LoadingOverlay
- mapEventToCategory
@@ -2,7 +2,7 @@
dir: apps/web/src/components/features
## role
Reusable UI feature components that compose domain-specific functionality for the web application.
Feature-specific UI components organized by business domain/capability for the web
## parent
index: apps/web/src/components/.pi-map.index.md
map: apps/web/src/components/.pi-map.md
+2 -2
View File
@@ -4,10 +4,10 @@ dir: apps/web/src/components/features
index: apps/web/src/components/features/.pi-map.index.md
## role
Reusable UI feature components that compose domain-specific functionality for the web application.
Feature-specific UI components organized by business domain/capability for the web
## files
## arch
Feature-based component organization with domain-driven design patterns, likely combining presentational components with localized state and business logic.
Contains 0 files.
## tags
-
## symbols
@@ -2,7 +2,7 @@
dir: apps/web/src/components/features/terminal
## role
Provides responsive, cross-platform terminal UI components with session management, virtual special keys, and WebSocket-backed xterm.js integration for web-based terminal access.
Provides the UI components for a responsive, multi-session terminal interface in the web app, including desktop and mobile layouts, session tabs, and special-key input controls.
## parent
index: apps/web/src/components/features/.pi-map.index.md
map: apps/web/src/components/features/.pi-map.md
@@ -4,19 +4,19 @@ dir: apps/web/src/components/features/terminal
index: apps/web/src/components/features/terminal/.pi-map.index.md
## role
Provides responsive, cross-platform terminal UI components with session management, virtual special keys, and WebSocket-backed xterm.js integration for web-based terminal access.
Provides the UI components for a responsive, multi-session terminal interface in the web app, including desktop and mobile layouts, session tabs, and special-key input controls.
## files
- DesktopTerminalView.tsx | Renders a desktop terminal view with session tabs, fullscreen controls, and multiple terminal instances | exp: DesktopTerminalView | dep: react, ./terminal, ./terminal-session-tabs, ../../../api/terminal
- MobileTerminalView.tsx | Renders a mobile-optimized terminal interface with session tabs, toolbar controls, special keys input, and keyboard-aware layout adjustments. | exp: MobileTerminalView | dep: react, ./terminal, ./terminal-session-tabs, ../../icon, ./special-keys-strip, ./special-keys-panel, ../../../hooks/use-special-keys, ../../../api/terminal, terminal, terminal-session-tabs, icon, special-keys-strip, special-keys-panel, use-special-keys, api/terminal
- special-keys-panel.tsx | Renders an overlay panel of special keyboard keys (Home, End, F1-F12, Ctrl+C, etc.) that sends terminal sequences when clicked and supports modifier key combinations. | exp: SpecialKeysPanel | dep: react, ../../../hooks/use-special-keys, React, use-special-keys hook
- special-keys-strip.tsx | Renders a strip of virtual special keys (Esc, Tab, Ctrl, Alt, arrows, etc.) for sending terminal key sequences with optional modifier support. | exp: SpecialKeysStrip | dep: react, ../../../hooks/use-special-keys, use-special-keys hook
- terminal-session-tabs.test.tsx | Tests a React terminal session tabs component for rendering, selection, closing with confirmation, renaming, session limits, and connection status display. | dep: @testing-library/react, vitest, ./terminal-session-tabs
- terminal-session-tabs.tsx | Renders an interactive tab bar for managing multiple terminal sessions with selection, renaming, closing, and creation capabilities | exp: TerminalSessionInfo, TerminalSessionTabsProps, TerminalSessionTabs | dep: react, React, useState, useRef, useCallback
- terminal.tsx | A React terminal component that provides an interactive xterm.js-based terminal with WebSocket connectivity, mobile touch support, and session management. | exp: TerminalProps, TerminalRef, TerminalComponent | dep: react, xterm, xterm-addon-fit, xterm-addon-web-links, xterm/css/xterm.css, ../../../hooks/use-special-keys, React
- DesktopTerminalView.tsx | Renders the desktop layout for a multi-session terminal UI, managing tabs, fullscreen mode, font controls, and reset confirmation. | exp: DesktopTerminalView | dep: react, ./terminal, ./terminal-session-tabs, ../../../api/terminal, React, TerminalComponent, TerminalSessionTabs, TerminalSession
- MobileTerminalView.tsx | Renders a mobile-specific terminal interface with session tabs, toolbar controls, special key inputs, and multiple terminal instances. | exp: MobileTerminalView | dep: react, ./terminal, ./terminal-session-tabs, ../../icon, ./special-keys-strip, ./special-keys-panel, ../../../hooks/use-special-keys, ../../../api/terminal, React, TerminalComponent, TerminalSessionTabs, Icon, SpecialKeysStrip, SpecialKeysPanel
- special-keys-panel.tsx | Renders a modal overlay panel of special keyboard keys (Home, End, F1-F12, Ctrl+C, etc.) that sends escape sequences to a terminal when clicked. | exp: SpecialKeysPanel | dep: react, ../../../hooks/use-special-keys
- special-keys-strip.tsx | Renders a touch-accessible strip of special keyboard keys (Esc, Tab, arrows, modifiers) that sends terminal escape sequences with optional modifier support. | exp: SpecialKeysStrip | dep: react, ../../../hooks/use-special-keys, React, use-special-keys hook
- terminal-session-tabs.test.tsx | Unit tests for a TerminalSessionTabs React component covering tab rendering, selection, close confirmation, inline renaming, session limit enforcement, and connection status display. | dep: @testing-library/react, vitest, ./terminal-session-tabs
- terminal-session-tabs.tsx | Renders a tabbed interface for managing multiple terminal sessions with support for switching, renaming, closing with confirmation, and creating new sessions. | exp: TerminalSessionInfo, TerminalSessionTabsProps, TerminalSessionTabs | dep: react, React
- terminal.tsx | | exp: TerminalProps, TerminalRef, TerminalComponent | dep: react, xterm, xterm-addon-fit, xterm-addon-web-links, xterm/css/xterm.css, ../../../hooks/use-special-keys
## arch
Component-based architecture with platform-specific view layers (desktop/mobile), modular special keys subsystems (strip/overlay panel), tab-based session management with React state, and xterm.js integration via WebSocket abstraction with touch/mobile adaptations.
Modular React component architecture organized by feature, with separate presentational components for desktop/mobile viewports, shared tab/session management, and helper components for terminal input handling.
## tags
terminal, special, keys, session, tabs, react, view, strip
terminal, special, session, keys, react, tabs, view, strip
## symbols
- DesktopTerminalView
- MobileTerminalView
@@ -0,0 +1,13 @@
import { describe, expect, it } from "vitest";
import { getTerminalScrollbackLimit } from "./terminal.tsx";
describe("getTerminalScrollbackLimit", () => {
it("retains normal-buffer history for custom mobile swipe scrolling", () => {
expect(getTerminalScrollbackLimit(true)).toBe(10_000);
});
it("keeps desktop scrollback disabled to prevent stale-frame wheel scrolling", () => {
expect(getTerminalScrollbackLimit(false)).toBe(0);
});
});
File diff suppressed because it is too large Load Diff
@@ -2,7 +2,7 @@
dir: apps/web/src/components/features/tool
## role
Provides UI components for managing containerized development tools, including instance lifecycle management, manifest configuration, and workspace-integrated tool launching.
Provides UI components for managing containerized development tools, including starting, configuring, monitoring, and editing tool instances across workspaces.
## parent
index: apps/web/src/components/features/.pi-map.index.md
map: apps/web/src/components/features/.pi-map.md
@@ -4,18 +4,18 @@ dir: apps/web/src/components/features/tool
index: apps/web/src/components/features/tool/.pi-map.index.md
## role
Provides UI components for managing containerized development tools, including instance lifecycle management, manifest configuration, and workspace-integrated tool launching.
Provides UI components for managing containerized development tools, including starting, configuring, monitoring, and editing tool instances across workspaces.
## files
- instance-list.tsx | React component that displays and manages a list of tool instances with CRUD operations, real-time status updates, and configuration profile selection. | exp: InstanceList | dep: react, react-router-dom, ../../icon, ../../loading-overlay, ../../../api/sessions, ../../../api/tool-types, ../session/create-session-form, ../../../api/config-profiles, ../../../api/ssh-keys, ../../../state/events, ../../../state/sessions
- manifest-editor.tsx | A React component that provides a form-based UI for editing container manifest configurations including base images, packages, environment variables, scripts, mounts, and runtime settings, with live preview generation capability. | exp: ManifestEditor | dep: react, ../../icon, ../../../utils/errors, ../../../api/tool-definitions, icon, errors utils, tool-definitions API
- start-tool-fab.tsx | Renders a floating action button that opens a modal to select a workspace and start a tool. | exp: func:StartToolFAB(), call:useState, call:setOpen, call:setWorkspacesLoading, call:listAllWorkspaces, call:setWorkspaces, call:setSelectedWorkspace, call:e.stopPropagation, call:workspaces.find, call:workspaces.map | dep: react, ../../icon, ./tool-starter, ../../../types/workspace, ../../../api/workspaces, icon, tool-starter, workspace types, workspaces api
- start-tool-modal.tsx | React modal component for selecting a tool type and optional config profile to start a tool on a workspace | exp: StartToolModalProps, func:StartToolModal({ workspace, onClose, onStart, }: StartToolModalProps), call:useState, call:useAsyncData, call:e.preventDefault, call:setError, call:setSubmitting, call:onStart, call:onClose, call:e.stopPropagation, call:setToolTypeId, call:toolTypes?.map, call:setConfigProfileId | dep: react, ../../icon, ../../../api/tool-types, ../../../hooks/use-async-data, ../../../types/workspace
- tool-starter.tsx | React component for starting a tool instance within a workspace with configurable tool types, profiles, and SSH keys | exp: ToolStarterProps, func:ToolStarter({ workspace, onStarted, onCancel, }: ToolStarterProps), call:useSessions, call:useState, call:useEffect, call:listToolTypes, call:setToolTypes, call:setToolTypesError, call:setToolTypesLoading, call:load, call:setProfiles, call:setSelectedProfileId, call:setProfilesLoading, call:listConfigProfiles, call:data.find, call:listSSHKeys, call:setSshKeys, call:setSelectedSshKeyIds, call:console.error, call:setSshKeysLoading, call:sshKeys.find, call:useCallback, call:setError, call:setStarting, call:createInstance, call:displayName.trim, call:startInstance, call:addOrUpdateSession, call:onStarted, call:setSelectedToolTypeId, call:toolTypes.find, call:setDisplayName, call:toolTypes.map, call:setNameEdited, call:profiles.map, call:sshKeys.map, call:selectedSshKeyIds.includes, call:prev.filter | dep: react, ../../icon, ../../loading-overlay, ../../../api/tool-types, ../../../api/config-profiles, ../../../api/ssh-keys, ../../../state/sessions, ../../../types/workspace, ../../../api/sessions, icon, loading-overlay, tool-types, config-profiles, ssh-keys, sessions, workspace types, sessions api
- instance-list.tsx | Displays and manages tool instances for a project/repo with start/stop/restart/delete actions, config profile selection, SSH key selection, and real-time status updates. | exp: InstanceList | dep: react, react-router-dom, ../../icon, ../../loading-overlay, ../../../api/sessions, ../../../api/tool-types, ../session/create-session-form, ../../../api/config-profiles, ../../../api/ssh-keys, ../../../state/events, ../../../state/sessions, Icon, LoadingOverlay, sessions API, config-profiles API, ssh-keys API, events state, sessions state
- manifest-editor.tsx | A React component that provides a form-based UI for editing container manifest configurations including base images, packages, environment variables, scripts, mounts, and runtime settings, with live preview compilation. | exp: ManifestEditor | dep: react, ../../icon, ../../../utils/errors, ../../../api/tool-definitions, icon, errors utils, tool-definitions API
- start-tool-fab.tsx | Renders a floating action button that opens a modal for selecting a workspace and starting a tool. | exp: func:StartToolFAB(), call:useState, call:setOpen, call:setWorkspacesLoading, call:listAllWorkspaces, call:setWorkspaces, call:setSelectedWorkspace, call:e.stopPropagation, call:workspaces.find, call:workspaces.map | dep: react, ../../icon, ./tool-starter, ../../../types/workspace, ../../../api/workspaces
- start-tool-modal.tsx | A React modal component that allows users to select a tool type and optional configuration profile to start a tool on a specific workspace. | exp: StartToolModalProps, func:StartToolModal({ workspace, onClose, onStart, }: StartToolModalProps), call:useState, call:useAsyncData, call:e.preventDefault, call:setError, call:setSubmitting, call:onStart, call:onClose, call:e.stopPropagation, call:setToolTypeId, call:toolTypes?.map, call:setConfigProfileId | dep: react, ../../icon, ../../../api/tool-types, ../../../hooks/use-async-data, ../../../types/workspace, icon, tool-types, use-async-data, workspace
- tool-starter.tsx | React component for starting a tool instance within a workspace context, fetching and configuring tool types, profiles, and SSH keys. | exp: ToolStarterProps, func:ToolStarter({ workspace, onStarted, onCancel, }: ToolStarterProps), call:useSessions, call:useState, call:useEffect, call:listToolTypes, call:setToolTypes, call:setToolTypesError, call:setToolTypesLoading, call:load, call:setProfiles, call:setSelectedProfileId, call:setProfilesLoading, call:listConfigProfiles, call:data.find, call:listSSHKeys, call:setSshKeys, call:setSelectedSshKeyIds, call:console.error, call:setSshKeysLoading, call:sshKeys.find, call:useCallback, call:setError, call:setStarting, call:createInstance, call:displayName.trim, call:startInstance, call:addOrUpdateSession, call:onStarted, call:setSelectedToolTypeId, call:toolTypes.find, call:setDisplayName, call:toolTypes.map, call:setNameEdited, call:profiles.map, call:sshKeys.map, call:selectedSshKeyIds.includes, call:prev.filter | dep: react, ../../icon, ../../loading-overlay, ../../../api/tool-types, ../../../api/config-profiles, ../../../api/ssh-keys, ../../../state/sessions, ../../../types/workspace, ../../../api/sessions, icon, loading-overlay, tool-types, config-profiles, ssh-keys, sessions, workspace
- tools-bottom-sheet.tsx | Renders a mobile bottom sheet navigation menu for tools with active route highlighting | exp: ToolsBottomSheet | dep: react-router-dom, ../../icon, icon
## arch
Feature-based React component architecture with modal/bottom-sheet mobile-responsive patterns, form-driven configuration editing with live preview, and workspace-contextual tool orchestration.
React component-based architecture with feature-specific composition, using modal/bottom-sheet patterns for mobile-responsive navigation, form-based configuration editors with live preview, and real-time status polling for instance management.
## tags
call:set, tool, types, start, call:use, ssh, react, loading
call:set, tool, types, start, ssh, call:use, icon, loading
## symbols
- StartToolFAB
- StartToolModal
@@ -49,7 +49,7 @@ export const ManifestEditor = ({
const [startupScripts, setStartupScripts] = useState<string[]>([""]);
const [mounts, setMounts] = useState<MountEntry[]>([]);
const [command, setCommand] = useState<string[]>([""]);
const [workingDir, setWorkingDir] = useState("/workspace");
const [workingDir, setWorkingDir] = useState("");
const [stdinOpen, setStdinOpen] = useState(true);
const [tty, setTty] = useState(true);
@@ -94,7 +94,7 @@ export const ManifestEditor = ({
const runtime = (manifest.runtime as Record<string, unknown>) || {};
setCommand((runtime.command as string[]) || [""]);
setWorkingDir((runtime.working_dir as string) || "/workspace");
setWorkingDir((runtime.working_dir as string) || "");
setStdinOpen((runtime.stdin_open as boolean) ?? true);
setTty((runtime.tty as boolean) ?? true);
}, [manifest]);
@@ -561,7 +561,8 @@ export const ManifestEditor = ({
<textarea
value={script}
onChange={(e) => updateStartupScript(idx, e.target.value)}
placeholder="chown -R user:user /workspace"
placeholder="chown -R user:user $HOME/$WORKSPACE_NAME"
className="form-input font-mono text-sm flex-1"
rows={2}
/>
@@ -723,7 +724,7 @@ export const ManifestEditor = ({
type="text"
value={workingDir}
onChange={(e) => setWorkingDir(e.target.value)}
placeholder="/workspace"
placeholder="e.g., /workspace or ~/code"
className="form-input"
/>
</div>
+10 -7
View File
@@ -169,6 +169,10 @@ export const useTerminalPage = () => {
const isAltShift = e.altKey && e.shiftKey && !e.ctrlKey && !e.metaKey;
if (!isAltShift) return;
const activeSessionIndex = activeSessionId
? sessions.findIndex((session) => session.id === activeSessionId)
: -1;
switch (e.key.toLowerCase()) {
case "n":
e.preventDefault();
@@ -187,17 +191,14 @@ export const useTerminalPage = () => {
break;
case "arrowleft":
e.preventDefault();
if (activeSessionId) {
const idx = sessions.findIndex((s) => s.id === activeSessionId);
if (idx > 0) setActiveSessionId(sessions[idx - 1].id);
if (activeSessionIndex > 0) {
setActiveSessionId(sessions[activeSessionIndex - 1].id);
}
break;
case "arrowright":
e.preventDefault();
if (activeSessionId) {
const idx = sessions.findIndex((s) => s.id === activeSessionId);
if (idx < sessions.length - 1)
setActiveSessionId(sessions[idx + 1].id);
if (activeSessionIndex >= 0 && activeSessionIndex < sessions.length - 1) {
setActiveSessionId(sessions[activeSessionIndex + 1].id);
}
break;
case "r":
@@ -208,6 +209,8 @@ export const useTerminalPage = () => {
e.preventDefault();
setIsFullscreen((prev) => !prev);
break;
default:
break;
}
};
+22 -27
View File
@@ -1,17 +1,26 @@
import "@testing-library/jest-dom/vitest";
import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { afterEach, describe, expect, it, vi } from "vitest";
import { ProjectsPage } from "./ProjectsPage";
import * as projectsApi from "../api/projects";
import { SessionsProvider } from "../state/sessions";
import type { ProjectWithRepos } from "../types";
const mockProjects = [
vi.mock("../api/sessions", () => ({
getUserSessions: vi.fn().mockResolvedValue([]),
}));
const mockProjects: ProjectWithRepos[] = [
{
id: "proj-1",
name: "Alpha Project",
description: "First project",
owner_id: "user-1",
default_ssh_key_id: null,
repositories: [],
created_at: "2026-07-01T00:00:00Z",
},
{
id: "proj-2",
@@ -19,6 +28,8 @@ const mockProjects = [
description: null,
owner_id: "user-1",
default_ssh_key_id: null,
repositories: [],
created_at: "2026-07-01T00:00:00Z",
},
];
@@ -31,9 +42,7 @@ describe("ProjectsPage", () => {
it("renders loading state initially", () => {
vi.spyOn(projectsApi, "listProjects").mockImplementation(() => new Promise(() => {}));
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
);
expect(screen.getByText(/loading projects/i)).toBeInTheDocument();
});
@@ -41,9 +50,7 @@ describe("ProjectsPage", () => {
it("renders project list after loading", async () => {
vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects);
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
);
await waitFor(() => {
@@ -56,9 +63,7 @@ describe("ProjectsPage", () => {
it("renders empty state when no projects", async () => {
vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]);
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
);
await waitFor(() => {
@@ -69,9 +74,7 @@ describe("ProjectsPage", () => {
it("renders error state with retry button", async () => {
vi.spyOn(projectsApi, "listProjects").mockRejectedValue(new Error("fail"));
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
);
await waitFor(() => {
@@ -85,9 +88,7 @@ describe("ProjectsPage", () => {
const createMock = vi.spyOn(projectsApi, "createProject").mockResolvedValue(mockProjects[0]);
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
);
await waitFor(() => {
@@ -118,9 +119,7 @@ describe("ProjectsPage", () => {
vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]);
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
);
await waitFor(() => {
@@ -138,16 +137,14 @@ describe("ProjectsPage", () => {
const updateMock = vi.spyOn(projectsApi, "updateProject").mockResolvedValue(mockProjects[0]);
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
);
await waitFor(() => {
expect(screen.getByText("Alpha Project")).toBeInTheDocument();
});
const alphaCard = screen.getByText("Alpha Project").closest(".project-card") as HTMLElement | null;
const alphaCard = screen.getByText("Alpha Project").closest(".project-list-item") as HTMLElement | null;
if (!alphaCard) throw new Error("Card not found");
fireEvent.click(within(alphaCard).getByRole("button", { name: /edit/i }));
@@ -171,16 +168,14 @@ describe("ProjectsPage", () => {
const deleteMock = vi.spyOn(projectsApi, "deleteProject").mockResolvedValue(undefined);
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
);
await waitFor(() => {
expect(screen.getByText("Alpha Project")).toBeInTheDocument();
});
const alphaCard = screen.getByText("Alpha Project").closest(".project-card") as HTMLElement | null;
const alphaCard = screen.getByText("Alpha Project").closest(".project-list-item") as HTMLElement | null;
if (!alphaCard) throw new Error("Card not found");
fireEvent.click(within(alphaCard).getByRole("button", { name: /delete/i }));
+33 -8
View File
@@ -152,7 +152,9 @@
}
.font-mono {
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-family:
"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
monospace;
}
.font-medium {
@@ -938,6 +940,14 @@ a.nav-item,
overscroll-behavior: auto;
max-height: 100% !important;
width: 100% !important;
/* Full-screen TUI tools repaint in place; scrollback is 0 (see terminal.tsx).
Hide the viewport scrollbar so it never appears or captures input. */
scrollbar-width: none;
-ms-overflow-style: none;
}
.terminal-container .xterm-viewport::-webkit-scrollbar {
display: none;
}
/* On mobile the custom touch handler scrolls the buffer; disable native
@@ -3809,7 +3819,9 @@ a:active,
color: var(--muted);
cursor: pointer;
border-radius: var(--radius-md);
transition: background 0.12s ease, color 0.12s ease;
transition:
background 0.12s ease,
color 0.12s ease;
}
.delete-btn:hover {
@@ -3836,7 +3848,9 @@ a:active,
font-size: var(--font-size-sm);
font-weight: 500;
text-decoration: none;
transition: background 0.12s ease, border-color 0.12s ease;
transition:
background 0.12s ease,
border-color 0.12s ease;
}
.workspace-header-action-btn:hover {
@@ -3911,7 +3925,9 @@ a:active,
color: var(--muted);
font-size: var(--font-size-xs);
cursor: pointer;
transition: background 0.12s ease, color 0.12s ease;
transition:
background 0.12s ease,
color 0.12s ease;
}
.copy-button:hover {
@@ -4157,7 +4173,10 @@ a:active,
}
.drag-item-active {
background: var(--brand-light, color-mix(in srgb, var(--brand) 10%, transparent));
background: var(
--brand-light,
color-mix(in srgb, var(--brand) 10%, transparent)
);
border-color: var(--brand);
}
@@ -4173,7 +4192,9 @@ a:active,
padding: var(--space-4);
border-radius: var(--radius-md);
overflow: auto;
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-family:
"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
monospace;
font-size: var(--font-size-sm);
line-height: var(--line-height-normal);
}
@@ -4321,7 +4342,9 @@ a:active,
color: var(--muted);
cursor: pointer;
font-weight: 600;
transition: border-color 0.15s, color 0.15s;
transition:
border-color 0.15s,
color 0.15s;
}
.sidebar-create-button:hover {
@@ -4346,7 +4369,9 @@ a:active,
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.15s ease, box-shadow 0.15s ease;
transition:
transform 0.15s ease,
box-shadow 0.15s ease;
}
.mobile-fab:hover {
@@ -0,0 +1,30 @@
# Restore Mobile Terminal Scrolling
## Summary
The recent terminal scrollback optimization disabled scrollback for every viewport. Mobile terminal swipe handling still scrolls xterm's normal buffer programmatically, so swipes in normal-buffer tools no longer have retained output to move through.
## Root Cause
`468f342` changed the terminal configuration to `scrollback: 0` globally to prevent stale repaint frames and wheel scrolling on desktop. The mobile touch handler calls `term.scrollLines()` when the normal buffer is active. With zero scrollback, that call has no scrollable history and becomes a no-op.
## Scope
- `apps/web/src/components/features/terminal/terminal.tsx`
- Focused terminal configuration test
## Fix
Retain a bounded xterm scrollback buffer on mobile only (`10000` lines), while leaving desktop at zero scrollback and with wheel sensitivity disabled. Mobile's existing custom touch handler remains responsible for moving through normal-buffer history; alternate-screen swipes continue to send SGR wheel events to the active TUI.
## Acceptance Criteria
- [ ] A mobile terminal with normal-buffer output exceeding one screen scrolls via a vertical swipe.
- [ ] Alternate-screen terminal scrolling continues to use the existing SGR wheel-event path.
- [ ] Desktop keeps zero xterm scrollback and disabled native wheel scrolling, so stale repaint frames do not return.
- [ ] Focused unit test and frontend quality gates pass.
## Related
- `468f342 fix(terminal): hide scrollbar and stop stale-frame wheel scroll for TUI tools`
- `openspec/changes/fix-terminal-container-overflow`
@@ -0,0 +1,9 @@
# Restore Mobile Terminal Scrolling — Tasks
- [x] Add a mobile-specific terminal scrollback limit while preserving zero scrollback on desktop.
- [x] Reinitialize the terminal when the responsive mobile classification changes so its scrollback and touch handler match the active viewport.
- [x] Add focused tests for the responsive scrollback configuration.
- [x] Run the full frontend test suite (89 tests passed after repairing the `ProjectsPage` test setup).
- [x] Run frontend typecheck, lint, focused tests, and production build.
- [ ] Perform mobile normal-buffer and alternate-screen manual QA.
- [ ] Update project maps for changed source files (the map patch tool currently fails with an unsupported `temperature` parameter).