pytest prints a failing test's fixture arguments via repr() as part of the default traceback header — no -v or --showlocals needed — so any test taking the test_database_url fixture and failing printed the real TEST_DATABASE_URL straight into the terminal, password included. test_database_url now returns a MaskedURL: a str subclass identical to a plain string for every functional use (passed to .replace(), urlsplit(), a subprocess environment) but whose repr() masks the password while still showing host and database name. __str__ is untouched, so every caller still connects with the real value. tests/test_conftest.py proves it, both for the fixture itself and for MaskedURL's string behaviour in isolation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
25 KiB
25 KiB
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
Changed
- Alembic refuses to run without a named migration target (core-api T-3) —
alembic/env.pyexits non-zero before building any connection unless-x target=testor-x target=productionis given; nothing defaults, so the most natural command in the repo, a barealembic current, can no longer reach production by accident.make migrate-test,make migrate-productionandmake dev-test-serverare the three documented ways to reach either database; none of them print a password or a credential-bearing URL.
Fixed
- Migration round-trip tests leave
core_api_testat head (core-api T-14) — every round-trip test intests/test_book_migration.py,tests/test_scribe_migration.pyandtests/test_tenants_migration.pynow ends onupgrade headrather than its own revision, so a full suite run leaves the test database at head instead of one or two migrations behind it. - A failing database test no longer prints the test database password (core-api
T-12) — the
test_database_urlfixture now returns aMaskedURL: identical to a plain string for every functional use, but itsrepr()— which pytest prints for a failing test's fixture arguments, without needing-v— masks the password.
[1.15.0] - 2026-10-19
Added
- A
feedbackmessage totower-controlmay have no sender (scribe_messages.from_identitynullable, migration 010, core-api T-15 / workspace D-70) — the failure channel must not depend on the thing most likely to break.POST /scribe/messagesaccepts a missingX-Office-Identityfor exactly one shape: an office caller,tois["tower-control"]alone, andtagscontainsfeedback; stored withfrom_identitynull andfrom_consumerthe caller's consumer. Every other send without an identity is still 400identity_required, and auser-kind caller gains nothing from the exemption.MessageResponse.from_identityis now optional, somailbox,history,findand themessage_idlookup all answer such a message with a null sender.
[1.14.0] - 2026-10-19
Added
- Archive and trash a message in your own mailbox (
scribe_mailbox_entries.archived_atandtrashed_at, migration 009, core-api T-13) — deletion is per mailbox entry, never per message, since a message is retained and shared by every recipient (D-66/D-70).PATCH /scribe/mailbox/{entry_id}takesarchivedandtrashedbooleans besideread(true sets the timestamp, false clears it); the response carries both states.GET /scribe/mailboxtakesview:inbox(default, neither archived nor trashed),archive(archived, not trashed),trash(trashed, regardless ofarchived_at),all.findnow excludes trashed entries. - Look up a mailbox entry by message number (core-api T-9) —
GET /scribe/mailbox?message_id=Nanswers the caller's own entry for that message, zero or one, in any view, bypassingunread/tag/before_idso no client pages the unread-first/ id-descending listing to find one.
[1.13.0] - 2026-10-19
Added
- A default address on each consumer grant (
tenant_consumers.default_address, migration 008, core-api T-11) — the Office's fallback when a call names no address and holds no session binding, since claude.ai's connector keeps no MCP session between tool calls. A composite foreign key(tenant_id, default_address)->scribe_addresses(owner_tenant, name)ensures a default always names a real address in the same tenant.jpmschweitzer/office-claudedefaults toclaude-aiwhere that address already exists; every other grant stays null.GET /tenants/meanswers it for the caller's own consumer grant, or null for a user-kind caller or a grant without one;GET /tenantsis unchanged. No write route this round — provisioning is later.
[1.12.0] - 2026-10-19
Added
- The tenant registry (
src/domains/tenants/, core-api T-8 / workspace D-73) -tenantsandtenant_consumers(migration 007), andTenantsService.require_tenant, the one enforcement point Book's and Scribe's own_require_tenantnow delegate to instead of checking only presence: a tenant that is not in the table is 403tenant_unknown, one markedsuspendedis 403tenant_suspended, and an Office caller whose consumer holds no grant in that tenant is 403consumer_not_granted- closing the gap where a typo'dtatlock_userclaim opened a fresh, empty namespace rather than being refused. Every existingowner_tenantcolumn (book_records,book_tenant_counters,scribe_addresses,scribe_messages,scribe_mailbox_entries) now carries a foreign key totenants.id(ON DELETE RESTRICT). Migration 007 seeds the two tenants in use today -jpmschweitzer(household) andllm_tester(test) - both granted tooffice-claude, the only Office consumer configured in Authentik. Read-only this round:GET /tenants/meanswers the caller's own tenant and its granted consumers (the cheap "am I allowed here" probe);GET /tenantsanswers every tenant for atatlock/schedulercaller or a list of one for a tenant-bearing caller, never revealing another tenant's existence. No write routes - provisioning a tenant is later (workspace T-195). - The Scribe (
src/domains/scribe/) - the household's message bus (dossier core-api T-5 / workspace D-66, D-68, D-73):register/listaddresses,send,mailbox,history,find,mark,tag/untag, all under/scribe/*, behindrequire_caller("office", "user"). An address is a topic owned by a tenant and a consumer;send'sfromis always the caller's own identity, never the request body. Every query filters on the caller's tenant (D-73) - an address or message in another tenant answers exactly like one that does not exist, so no id is ever confirmed across that boundary. Messaging only this round: groups, subscriptions, the notification matrix and delivery are later work (workspace T-176). Migration 006 createsscribe_addresses,scribe_messages(retained, full-text indexed),scribe_mailbox_entries. - Caller carries consumer and tenant from the Office (
src/domains/auth/ callers.py, core-api T-4 / workspace D-73) -Callergainsconsumerandtenant. For anofficecaller bothX-Office-ConsumerandX-Office-Tenantare required on every call, reads included: missing is 400consumer_required/tenant_required, malformed is 400consumer_invalid/tenant_invalid. Every other kind ignores both headers outright -tatlockandschedulercarry no tenant, and ausercaller's tenant comes only from their own JWT'statlock_userclaim, never from a header. - Caller auth (
src/domains/auth/callers.py) - the first thing in this repo that reads a service key.require_caller()resolves a static office/tatlock/ scheduler key (constant-time compare, checked before any JWT parsing) or falls through to the existing OIDC path. New settings:office_service_key,tatlock_service_key,scheduler_service_key,book_timezone. - The Book (
src/domains/book/) - Notes, Tasks, Decisions, Questions and Rejected options (workspace D-68/D-69/D-70). Routes:add,show,list,find,change,close,link/unlink,tag/untag,history, all under/book/*, behindrequire_caller("office", "user"). Migration 005 creates the whole schema:book_records,book_tags,book_record_links,book_attachments,book_events(append-only, DB trigger-enforced),book_tenant_counters. - The Book is behind the tenant boundary (core-api T-7 / workspace D-73) -
every route now requires a tenant on the caller (400
tenant_requiredfor auser-kind caller with notatlock_userclaim; anofficecaller already has one from every call per T-4) and filters on it: a record in another tenant answers exactly like one that does not exist (404not_found), in both directions, andlink/add'sparentconvenience refuse a target in another tenant the same way.addsetsowner_tenantfrom the caller and allocatesnumber- a short outward id numbered per tenant (each tenant's Book starts at 1, unique on(owner_tenant, number), never reused), allocated via a per-tenant counter row (book_tenant_counters) lockedFOR UPDATEin the same transaction as the insert. Responses includenumber;listacceptsnumber=to find one. Migration 005 (unreleased - production was still at 004) is amended in place to addbook_records.owner_tenant/numberandbook_tenant_counters, rather than patching them in with a later migration.
Changed
X-Office-Clientis retired in favour ofX-Office-Consumer(core-api T-4) - the informational header is no longer read;book_events.client(no migration this round) now records the consumer instead.- Housekeeping reads are locked -
GET /housekeeping/*(health, devices, areas, scenes, scripts, automations, history) now require a caller (office,tatlock, oruser) instead of being unauthenticated. Writes are unchanged (get_admin_user).api.schweitz.netcarries no forward-auth, so this was an open read of the house's device list once Home Assistant's token is restored (T-1 / D-68).
[1.11.0] - 2026-01-08
Added
- News Headlines API - New endpoint for news ticker integration
GET /tools/news- Fetch news headlines from user's volatile collection- Returns headlines with title, description, source, and URL
- Data sourced from
volatile_{user}Qdrant collection (news namespace) - Uses
preferred_usernamefrom OIDC, falls back todefault
- News subdomain under tools (
src/domains/tools/news/) NewsHeadlineandNewsResponsePydantic schemasNewsServicefor parsing news data from Qdrant
[1.10.12] - 2026-01-08
Fixed
- Forecast data retrieval - Pass raw_data to service instead of extracting wrong field
- Qdrant client was extracting
days(integer 7) instead ofdaily(list) - Now passes full raw_data for service to parse correctly
- Qdrant client was extracting
[1.10.11] - 2026-01-08
Fixed
- Weather wind direction - Convert integer degrees to cardinal direction string
- Scheduler stores wind_direction as degrees (e.g., 135)
- Schema expects string (e.g., "SE")
- Added
_degrees_to_cardinal()conversion
[1.10.10] - 2026-01-08
Fixed
- Environment data parsing - Fix parsing of scheduler-generated Qdrant data
- Forecast: Check
dailykey first (scheduler stores day count indays, list indaily) - Sun times: Use
sunrise_iso/sunset_isofields, handle time-only format fallback - Sun times: Calculate daylight from
daylight_duration_secondsordaylight_hours - Air quality: Support
aqi_us/aqi_europeanandnitrogen_dioxidefield names
- Forecast: Check
[1.10.9] - 2026-01-08
Fixed
- Environment user ID cleanup - Strip email domain from user identifier
- If
preferred_usernameis an email, extract just the username part - Ensures Qdrant collection name matches (e.g.,
volatile_jpmschweitzernotvolatile_jpmschweitzer@gmail.com)
- If
[1.10.8] - 2026-01-08
Fixed
- OIDC audience validation - python-jose requires string audience, not list
- Extract and validate audience from unverified claims first
- Use token's actual audience for JWT decode (after validating it's allowed)
- Fixes "audience must be a string or None" error
[1.10.7] - 2026-01-08
Added
- Multi-issuer OIDC support - Accept tokens from multiple OAuth providers
- Changed
oidc_issuer(string) tooidc_issuers(list) - Each issuer has its own JWKS endpoint, now cached per-issuer
- Validates token issuer against allowed list before fetching JWKS
- Supports tokens from:
core-api,tatlock-ui,tatlockOAuth applications - Completes fix for environment endpoint user resolution
- Changed
Removed
- Deprecated
src/config.py- consolidated tosrc/shared/config.py - Deprecated
src/security.py- consolidated tosrc/shared/security.py
[1.10.6] - 2026-01-08
Fixed
- OIDC audience mismatch - Accept tokens from multiple clients
- Changed
oidc_audience(string) tooidc_audiences(list) - Now accepts tokens with audience:
core-api,tatlock-ui, ortatlock - Fixes environment endpoint returning "default" user instead of authenticated username
- Fixed main.py to use
oidc_audiences[0]for Swagger UI OAuth client
- Changed
Changed
- Documentation cleanup in README
[1.10.4] - 2026-01-07
Removed
- Ollama integration removed - AI inference is no longer handled by this API
- Removed
src/models/ollama_client.pyand all Ollama-related configuration - Removed
src/models/embeddings.pyandsrc/models/embeddings_ollama.py - Removed model aliases and AI configuration from settings
- Health endpoints no longer check Ollama status
- Tests updated to reflect database-only health checks
- Removed
Changed
- Health check
/health/fullnow only checks database connectivity - Diagnostics endpoint simplified (removed Ollama component info)
[1.10.3] - 2026-01-07
Fixed
- OIDC config not applied to domains module - Both
src.auth.oidcandsrc.domains.auth.oidcconfigs are now initialized- Previously only
src.auth.oidcwas configured, leaving domains tools using hardcoded "local" user - Environment endpoint now correctly uses authenticated user from OIDC token
- Previously only
[1.10.2] - 2026-01-07
Changed
- Enhanced environment endpoint logging - Added detailed user claim logging for debugging
- Logs both
preferred_usernameandsubclaims when resolving user - Distinguishes between authenticated and unauthenticated requests
- Logs both
[1.10.1] - 2026-01-06
Fixed
- Fix OIDC import path in tools controller (
src.oidc.dependencies→src.auth.oidc)- Environment endpoint was returning
user: "local"instead of authenticated username - Caused queries to wrong Qdrant collection (
volatile_localvsvolatile_{username})
- Environment endpoint was returning
[1.10.0] - 2026-01-06
Added
- Environment Data API - Qdrant-backed endpoint for weather, forecast, and sun position data
GET /tools/environment- Fetch environment data from user's volatile collection- Weather: current temperature, conditions, humidity, wind speed
- Forecast: multi-day outlook with high/low temperatures
- Sun times: sunrise, sunset, daylight duration
- Air quality: AQI and quality level (when available)
- Data sourced from
volatile_{user}Qdrant collection - Uses
preferred_usernamefrom OIDC, falls back todefault
qdrant-clientdependency for vector database accessQdrantReadClientwrapper for read-only collection queries- Comprehensive test suite for environment service parsing
[1.9.4] - 2026-01-04
Fixed
- Fix SQLAlchemy async lazy loading error for new users in
/auth/sync- Initialize
user.roles = []anduser.preferencesto avoid greenlet error - Was causing "MissingGreenlet: greenlet_spawn has not been called" on new user creation
- Initialize
[1.9.3] - 2026-01-04
Fixed
- Handle non-UUID
subclaim in/auth/sync- Authentik JWT may return non-UUID subject identifiers- Now derives deterministic UUID from sub string if direct parsing fails
[1.9.2] - 2026-01-04
Fixed
/auth/users/meendpoint now supports both NPM forward auth headers AND JWT Bearer tokens- Added
get_current_user_or_forward_auth()combined auth dependency - Fixes web authentication where NPM passes
X-authentik-*headers instead of JWT - Mobile/native clients continue to use JWT Bearer tokens as before
- Added
[1.9.1] - 2026-01-03
Fixed
- CORS configuration now uses explicit origins instead of
"*"- When
allow_credentials=True, wildcard origins are rejected by browsers - Added
home.schweitz.net,tatlock.schweitz.net, and localhost origins
- When
[1.9.0] - 2026-01-03
Added
- NPM Forward Auth Support - Web authentication via Nginx Proxy Manager forward auth
GET /auth/me- Get current user from NPM forward auth headers (X-authentik-uid, X-authentik-email, etc.)- Auto-creates user on first web login if not in database
- Syncs roles from NPM forward auth groups header
get_user_by_emailandget_user_by_authentik_idmethods in AuthService- Comprehensive tests for
/auth/meendpoint
[1.8.0] - 2026-01-03
Added
- Group-Role Mapping & Permissions (Phase 3)
- Decoupled group-role architecture (groups from Authentik, roles admin-managed)
- Permission format:
domain.category:actionwith action hierarchy require_permissionandrequire_any_permissiondependency factories- Global admin override (
admin.general:admin)
- User Profile & API Keys (Phase 4)
GET /auth/users/me- Full user profile with roles and preferencesGET/PATCH /auth/users/me/preferences- User preferences managementGET/POST/DELETE /auth/users/me/api-keys- API key lifecycle- API keys with
tak_prefix, SHA-256 hashing, shown only once on creation
[1.7.0] - 2026-01-03
Added
- System Stats API - Host system resource monitoring for dashboard widgets
GET /tools/system/stats- Real-time host system statistics- CPU: usage percentage, core count, load averages
- Memory: usage percentage, total/used/available bytes
- Disks: all mounted filesystems with usage stats (auto-discovers mounts)
- Network: total bytes sent/received
- GPU/VRAM: NVIDIA GPU memory usage (via nvidia-smi if available)
psutildependency for cross-platform system metrics
[1.6.1] - 2026-01-03
Added
link_typefield to quick links for iframe vs new tab behavior
[1.6.0] - 2026-01-03
Added
- Dashboard API - Quick links and widgets management for Organizr-style dashboard
GET /dashboard/quick-links- List quick links with category/visibility filteringGET /dashboard/quick-links/{id}- Get single quick linkPOST /dashboard/quick-links- Create quick linkPUT /dashboard/quick-links/{id}- Update quick linkDELETE /dashboard/quick-links/{id}- Delete quick linkPOST /dashboard/quick-links/reorder- Reorder quick links by positionGET /dashboard/widgets- List dashboard widgetsGET /dashboard/widgets/{id}- Get single widgetPOST /dashboard/widgets- Create widgetPUT /dashboard/widgets/{id}- Update widgetDELETE /dashboard/widgets/{id}- Delete widget
- Database migrations for
quick_linksanddashboard_widgetstables - Static file controller for serving Organizr widgets (
/static/widgets) - Default local user authentication when OIDC is disabled
Changed
- Domain-based architecture - Refactored codebase to domain-driven structure
src/domains/- Domain modules (auth, dashboard, health, housekeeping, infrastructure, tools)src/shared/- Shared utilities (base, config, database, logging, security, clients)
- Test suite updated for new domain structure (285 tests passing)
[1.5.0] - 2026-01-01
Added
- Groups Management - Authentik group synchronization
GET /auth/groups- List all groups with search and paginationPOST /auth/groups/sync-from-authentik- Bulk sync groups from Authentik admin API
- Database model and migration for groups table
[1.4.6] - 2026-01-01
Changed
- Code cleanup: move inline
reimport to top of auth/service.py
[1.4.5] - 2026-01-01
Fixed
- Separate httpx and SQLAlchemy async contexts in bulk sync (fixes greenlet error)
[1.4.4] - 2026-01-01
Fixed
- Use
uuidfield instead ofpkfor Authentik user sync (pk is integer, uuid is proper UUID) - Skip internal_service_account type users during bulk sync
[1.4.3] - 2026-01-01
Fixed
- Manually extract and send session cookies for Authentik flow auth (fixes cross-domain cookie handling)
[1.4.2] - 2026-01-01
Fixed
- Use Authentik domain URL (auth.schweitz.net) instead of IP to fix cookie domain matching
[1.4.1] - 2026-01-01
Fixed
- Authentik config now uses AUTHENTIK_USERNAME/PASSWORD to match production env vars
[1.4.0] - 2026-01-01
Added
- Authentication & User Management - Authentik integration for user synchronization
GET /auth/me- Get current authenticated user infoGET /auth/users- List all users with search and paginationPOST /auth/users/sync-from-authentik- Bulk sync users from Authentik admin API
- PostgreSQL database integration with async SQLAlchemy
- Alembic database migrations for schema management
- Database models: User, Role, UserPreferences, ApiKey
- Token validation via Authentik userinfo endpoint
- Role synchronization from Authentik groups
- Health check now includes database connectivity status
Changed
- Authentik configuration now uses username/password for admin API access
- Health endpoint includes database status in diagnostics
[1.3.1] - 2025-12-31
Changed
- Simplified configuration: all settings now read from environment variables/.env only
- Removed Docker socket fallback for container operations (Portainer API is now required)
- Updated default search provider to SearXNG
Removed
src/credentials.py- credentials now managed via environment variables- Docker socket fallback methods from Portainer client
- Unused search API settings (Brave, Google)
[1.3.0] - 2025-12-31
Added
- Stack Management Endpoints for Tatlock Control Room integration
GET /infrastructure/stacks/{stackId}/compose- Get stack Docker Compose YAMLPUT /infrastructure/stacks/{stackId}/compose- Update stack Docker Compose YAMLGET /infrastructure/stacks/{stackId}/env- Get stack environment variablesPUT /infrastructure/stacks/{stackId}/env- Update stack environment variablesPOST /infrastructure/stacks/{stackId}/deploy- Redeploy stackPOST /infrastructure/stacks/{stackId}/rebuild- Pull images and recreate containers
DELETE /infrastructure/containers/{id}- Delete container with optional force flag- Portainer client methods:
get_stack_file,redeploy_stack,update_stack_env,delete_container,restart_container
Removed
- REQUESTED_SERVICES.md - endpoint specifications now implemented
[1.2.1] - 2025-12-17
Fixed
- Device control endpoint now returns correct new state after action (added 300ms delay for HA state propagation)
Added
- AGENTS.md with project coding guidelines and release flow documentation
Changed
- Removed obsolete web scraper settings from .env.example
[1.2.0] - 2025-12-17
Added
- Housekeeping API - Home Assistant integration for smart home control
GET /housekeeping/health- HA connection statusGET /housekeeping/devices- List controllable devices with optional domain/area filteringGET /housekeeping/devices/{entity_id}- Get device detailsPOST /housekeeping/devices/{entity_id}/control- Control devices (turn_on, turn_off, toggle, set_brightness)GET /housekeeping/scenes- List available scenesPOST /housekeeping/scenes/{scene_id}/activate- Activate a sceneGET /housekeeping/scripts- List available scriptsPOST /housekeeping/scripts/{script_id}/run- Run a scriptGET /housekeeping/automations- List automationsPOST /housekeeping/automations/{automation_id}/toggle- Enable/disable automationGET /housekeeping/history- Query state historyGET /housekeeping/areas- List rooms/areas
- Home Assistant REST API client (
src/clients/homeassistant_client.py) - Home Assistant configuration in credentials and settings
- Comprehensive test suite with 65% code coverage (285 tests)
- Tests for NPM client, Ollama client, AI client, OIDC authentication
- Tests for infrastructure, health, tools, and housekeeping endpoints
Removed
- Web Scraper - Entire web scraping module removed
src/web_scraper/directory deleted/web-scraper/scrapeendpoint removed- Trafilatura and BeautifulSoup dependencies removed from scraping use
Changed
- Updated README.md with current architecture and all endpoints
- Tools controller now only contains DNS lookup functionality
- Health controller endpoints list updated to reflect current features
[1.1.2] - 2024-12-14
Added
- Version field to /health endpoint response
[1.1.1] - 2024-12-14
Added
- Watchtower trigger step in CI workflow for automatic container updates
[1.1.0] - 2024-12-14
Removed
- Uptime Kuma integration (kuma_client.py deleted)
- All /infrastructure/monitors endpoints
- Kuma monitor pause/resume from service start/stop operations
- Uptime percentage display from service control widget
- Kuma-related configuration and credentials
- Stale memory tests (memory functionality moved to core-ai service)
Changed
- Service control widget now uses Portainer container status exclusively
- Simplified widget-data endpoint response (removed monitors field)
- Updated service start/stop to only manage containers via Portainer
[1.0.0] - 2024-12-14
Added
- Initial release of Core Code API service extracted from portainer-core
- Infrastructure management endpoints for Portainer, NPM, and Uptime Kuma
- Web scraping service with content extraction
- DNS management endpoints
- Health check endpoints with diagnostics
- OIDC authentication support via Authentik
- Ollama integration for embeddings
- OpenAPI documentation (Swagger UI and ReDoc)
- Docker containerization with CI/CD workflow