11 KiB
Phase 01 Plan 01: Monorepo Scaffolding, Data Models, and RLS Tenant Isolation Summary
uv workspace monorepo with PostgreSQL RLS proving Tenant A cannot access Tenant B's data through SQLAlchemy + asyncpg with FORCE ROW LEVEL SECURITY enforced at schema level
Performance
- Duration: 12 min
- Started: 2026-03-23T15:45:02Z
- Completed: 2026-03-23T15:57:51Z
- Tasks: 2
- Files modified: 32
Accomplishments
- uv workspace monorepo with 5 member packages — all importable with
uv run python -c "from shared.models.message import KonstructMessage" - PostgreSQL RLS tenant isolation proven green: 7 integration tests confirm cross-tenant data leakage is impossible via
FORCE ROW LEVEL SECURITYon agents and channel_connections tables - Alembic async migration creates all tables, grants to konstruct_app role, and configures RLS policies via
current_setting('app.current_tenant', TRUE)::uuid - Docker Compose dev environment boots cleanly: PostgreSQL 16 + Redis 7 + Ollama (GPU-optional)
- 33 unit tests for KonstructMessage normalization, tenant resolution logic, and Redis key namespacing
Task Commits
Each task was committed atomically:
- Task 1: Monorepo scaffolding, Docker Compose, and shared data models -
5714acf(feat) - Task 2: Alembic migrations with RLS and tenant isolation tests -
47e7862(feat)
Plan metadata: (docs commit — forthcoming)
Files Created/Modified
pyproject.toml— uv workspace root with 5 members, pytest config, ruff/mypy settingsdocker-compose.yml— PostgreSQL 16, Redis 7, Ollama with shared konstruct-net.env.example— all env vars documented with konstruct_app role (not superuser)packages/shared/shared/models/message.py— KonstructMessage, ChannelType, SenderInfo, MessageContentpackages/shared/shared/models/tenant.py— Tenant, Agent, ChannelConnection (SQLAlchemy 2.0 Mapped[] style)packages/shared/shared/models/auth.py— PortalUser for portal authenticationpackages/shared/shared/db.py— async engine, session factory, get_session FastAPI dependencypackages/shared/shared/rls.py— current_tenant_id ContextVar, configure_rls_hook (idempotent, UUID-sanitized)packages/shared/shared/redis_keys.py— rate_limit_key, idempotency_key, session_key, engaged_thread_keypackages/shared/shared/config.py— Pydantic Settings loading all env varsmigrations/versions/001_initial_schema.py— full schema with RLS policies and FORCE ROW LEVEL SECURITYtests/integration/test_tenant_isolation.py— 7 tests proving RLS isolation across both tenant-scoped tables
Decisions Made
- SET LOCAL parameterization: asyncpg does not support
%splaceholders forSET LOCALstatements. Switched to f-string with UUID round-trip sanitization —str(UUID(str(tenant_id)))ensures only valid hex+hyphen characters reach the DB. - channel_type as TEXT: Native
sa.Enumtype caused SQLAlchemy to auto-emit a secondCREATE TYPE channel_type_enumstatement increate_table, conflicting with our explicitCREATE TYPE. Stored asTEXTwith aCHECK (channel_type IN (...))constraint instead. - Function-scoped AsyncEngine in tests: pytest-asyncio 1.3.0 raises cross-loop-scope errors with session-scoped async fixtures. Moved engine creation to function scope; used a session-scoped synchronous fixture to create the test DB and run migrations once via subprocess.
Deviations from Plan
Auto-fixed Issues
1. [Rule 1 - Bug] asyncpg does not support %s parameterized SET LOCAL
- Found during: Task 2 (integration test execution)
- Issue:
rls.pyusedcursor.execute("SET LOCAL app.current_tenant = %s", (tenant_id,))— asyncpg raisesPostgresSyntaxError: syntax error at or near "%"because asyncpg uses$1style and SET LOCAL doesn't accept prepared statement parameters at all - Fix: Changed to
cursor.execute(f"SET LOCAL app.current_tenant = '{safe_id}'") wheresafe_id = str(UUID(str(tenant_id)))sanitizes the value - Files modified:
packages/shared/shared/rls.py - Verification: All 7 integration tests pass including cross-tenant isolation test
- Committed in:
47e7862(Task 2 commit)
2. [Rule 1 - Bug] SQLAlchemy double-emits CREATE TYPE for sa.Enum in create_table
- Found during: Task 2 (alembic upgrade head execution)
- Issue:
sa.Enum(..., create_type=False)inop.create_tablestill triggered a secondCREATE TYPE channel_type_enumDDL, causingDuplicateObjectErroreven when our explicitCREATE TYPEran first - Fix: Changed
channel_typecolumn tosa.Textwith aCHECKconstraint enforcing valid values - Files modified:
migrations/versions/001_initial_schema.py - Verification:
alembic upgrade headcompletes cleanly; constraint visible inpg_constraint - Committed in:
47e7862(Task 2 commit)
3. [Rule 3 - Blocking] Root pyproject.toml lacked build-system for uv workspace root
- Found during: Task 1 (uv sync execution)
- Issue: Including
[build-system]in rootpyproject.tomlcaused hatchling to fail looking for akonstructpackage directory; workspace roots should not be buildable packages - Fix: Removed
[build-system]from rootpyproject.toml; useduv sync --all-packagesto install workspace members - Files modified:
pyproject.toml - Verification:
uv sync --all-packagessucceeds; all workspace packages listed inuv pip list - Committed in:
5714acf(Task 1 commit)
Total deviations: 3 auto-fixed (2 bugs, 1 blocking) Impact on plan: All fixes necessary for correctness. No scope creep.
Issues Encountered
- pytest-asyncio 1.3.0 cross-loop-scope issue with session-scoped async
db_enginefixture — resolved by making engine function-scoped and using a synchronous session-scoped fixture for one-time DB creation ::jsonbcast syntax in SQLAlchemytext()query rejected by asyncpg parameter parser — removed cast, PostgreSQL coerces JSON text implicitly
User Setup Required
None — the Docker Compose environment provides all required services. Copy .env.example to .env to start.
Next Phase Readiness
- All packages/shared imports available for Plans 02–04
DATABASE_URLmust usekonstruct_approle (not superuser) — documented in.env.exampleDATABASE_ADMIN_URLmust be set foralembic upgrade headin CI/CD- Docker Compose boots cleanly with
docker compose up -d postgres redis - Ollama GPU reservation is optional — service starts without GPU
Self-Check: PASSED
All created files verified present. All task commits verified in git history.
Phase: 01-foundation Completed: 2026-03-23