- Add destructiveHint=True to deck_remove_label_from_card and
deck_unassign_user_from_card (ADR-017 compliance)
- Set idempotentHint=True since remove operations produce same end state
- Update test_annotations.py to exclude nc_webdav_create_directory from
non-idempotent check (MKCOL is idempotent by design - returns 405 if exists)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Security improvements:
- Add in-memory rate limiter for app password provisioning (5 attempts/hour/user)
- Returns 429 Too Many Requests with Retry-After header when limit exceeded
- Rate limiting is per-user to prevent cross-user DoS
Code quality improvements:
- Extract _extract_basic_auth() helper to reduce duplication across 3 endpoints
- Move base64, re imports to module level
- Add APP_PASSWORD_PATTERN constant for regex validation
- Add NEXTCLOUD_VALIDATION_TIMEOUT constant (10s)
Test coverage:
- Add test_provision_app_password_rate_limiting
- Add test_rate_limiting_is_per_user
- Add autouse fixture to clear rate limit state between tests
- Total: 15 tests for management API endpoints
Addresses reviewer feedback on PR #473.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Management API:
- Extract _get_app_password_storage() helper function
- Reduces code duplication across 3 endpoints
- Adds TYPE_CHECKING import for type hints
PHP CredentialsController:
- Add partial_success field to distinguish full vs partial success
- Add local_storage and mcp_sync boolean fields for clarity
- Rename 'warning' to 'mcp_error' for consistency
- Improves UI feedback when MCP server sync fails
Response structure now clearly indicates:
- Full success: partial_success=false, local_storage=true, mcp_sync=true
- Partial success: partial_success=true, local_storage=true, mcp_sync=false
- Full failure: success=false (unchanged)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously, the multi-user BasicAuth mode attempted to retrieve app passwords
via OAuth client_credentials grant, which Nextcloud OIDC doesn't support.
This fix implements local storage for app passwords:
- Add app_passwords table via Alembic migration (002)
- Add store/get/delete methods to RefreshTokenStorage
- Add management API endpoints for app password provisioning:
- POST /api/v1/users/{user_id}/app-password
- GET /api/v1/users/{user_id}/app-password
- DELETE /api/v1/users/{user_id}/app-password
- Update oauth_sync.py to read from local storage
- Update Astrolabe to send app passwords to MCP server after validation
- Add app-hook to configure mcp_server_url in Nextcloud
The flow is now:
1. User creates app password in Nextcloud Security settings
2. User enters it in Astrolabe Personal Settings
3. Astrolabe validates against Nextcloud, then sends to MCP server
4. MCP server stores encrypted app password locally
5. Background sync uses locally stored password
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The Deck PUT API is a full replacement, not a partial update.
Previously, title and description were conditionally sent, causing:
- 400 errors when title not provided (it's required)
- Description being cleared when not explicitly set
Now all required fields (title, type, owner) and description are
always included in the payload using current card values when not
explicitly provided. This matches the existing pattern for type/owner.
Also simplified owner extraction since DeckCard.validate_owner
already ensures it's always a string.
Fixes#452🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Refactor tests to assert what SHOULD happen (partial updates preserve
unchanged fields) rather than documenting current buggy behavior.
Tests will fail until fix is implemented in client or upstream.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Tests document current behavior of update_card method:
- Updating without title fails (400) - title required but conditionally sent
- Updating with title clears description - PUT is full replacement
Related: #452🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Two issues prevented CSS from loading correctly:
1. Entry point naming mismatch: Vite output `main.css` but Nextcloud's
`Util::addStyle('astrolabe', 'astrolabe-main')` expected `astrolabe-main.css`
2. CSS code splitting: Vite extracted @nextcloud/vue component styles
into separate chunks (e.g., NcUserBubble-*.css) that Nextcloud doesn't
load automatically. Without these styles, the UI rendered incorrectly.
Changes:
- Rename entry point from `main` to `astrolabe-main`
- Add `cssCodeSplit: false` to bundle all CSS into the entry point
- Update assetFileNames to output consistent `astrolabe-main.css`
This increases CSS bundle from 11KB to 286KB but ensures all component
styles are available when the page loads.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The "Revoke Access" button in Astrolabe personal settings was failing
with "Unable to connect to server" error in multi-user basic auth mode.
Root cause: The JavaScript sends a POST request but the route was
configured to accept DELETE. Changed the route to:
- Use POST method (matching the JavaScript fetch call)
- Use /api/v1/background-sync/credentials/revoke path (avoiding
conflict with storeAppPassword which uses POST on the base URL)
Added integration test that verifies the complete revoke flow:
enable background sync → click revoke → verify credentials deleted.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The /oauth/login route was returning 404 in multi-user BasicAuth mode with
offline access enabled. This was because browser OAuth routes were gated
by `oauth_enabled` (only True for MCP OAuth modes), not by
`oauth_provisioning_available` which correctly includes hybrid mode.
The Management API (admin UI, webhook management) requires OAuth
authentication regardless of how MCP tools authenticate. These are
independent security concerns:
- MCP Tools: BasicAuth (waiting for upstream Nextcloud OAuth patches)
- Management API: OAuth (for admin UI, webhook management, vector sync)
Changes:
- Gate browser OAuth routes by oauth_provisioning_available instead of
oauth_enabled
- Add follow_redirects=True to OIDC discovery HTTP clients
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add module-scoped autouse fixture `reset_all_singletons` in
tests/integration/conftest.py that resets all global singletons
between test modules:
- _qdrant_client (vector/qdrant_client.py)
- _embedding_service, _bm25_service (embedding/service.py)
- _provider (providers/registry.py)
- _vector_sync_state with memory streams (app.py)
- _tracer (observability/tracing.py)
- _registry (auth/client_registry.py)
- _token_exchange_service (auth/token_exchange.py)
This fixes anyio.WouldBlock errors that occurred when running the
full integration test suite together. The errors were caused by
stale singleton state holding references to dead event loops or
closed memory streams from previous test modules.
Results:
- Before: 22 passed, 26 errors (WouldBlock), 12 failed
- After: 48 passed, 25 skipped, 1 failed (unrelated timeout)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- test_qdrant_collection_creation.py:
- Add get_vector_params() helper to handle named vectors format
- Collections use {"dense": VectorParams(...)} instead of direct VectorParams
- Fix otel_service_name setting in test_collection_name_generation
- test_sampling.py:
- Fix MCP response parsing: use json.loads(result.content[0].text)
instead of result.structuredContent (which is None)
- Add require_vector_sync_tools() helper for graceful skipping
- Add helper call to all 5 test functions
- test_rag.py:
- Add require_vector_sync_tools() helper for graceful skipping
- Fix MCP response parsing (same as sampling tests)
- Prevents 600s timeout when VECTOR_SYNC_ENABLED is not set
Tests now pass/skip cleanly when run independently. The anyio.WouldBlock
errors in full test suite runs are fixture isolation issues, not code bugs.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Completely separates multi-user BasicAuth mode from OAuth mode with no
fallback between them. These are now mutually exclusive authentication
strategies based on deployment configuration.
Changes:
- Create separate functions: get_user_client_basic_auth() and
get_user_client_oauth() with clear separation of concerns
- Update get_user_client() to dispatch based on use_basic_auth parameter
- Pass use_basic_auth through all background sync tasks
- Update app.py to determine auth mode at startup
- Rewrite integration tests to verify no OAuth fallback in BasicAuth mode
- Fix test assertions for response field names and duplicate title handling
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixes NC PHP app (Astrolabe) OAuth integration by making token validation
more lenient for management API access.
Problem:
- Astrolabe calls Nextcloud OIDC token endpoint via internal URL (http://localhost)
- Tokens are issued with iss: http://localhost (internal)
- MCP server expects iss: http://localhost:8080 (external)
- Token validation failed with "Invalid issuer"
Solution:
- Add skip_issuer_check parameter to _verify_jwt_signature()
- verify_token_for_management_api() now skips both audience and issuer checks
- Security maintained: signature still verified, authorization checked by API
Also includes related fixes from previous session:
- Update test selectors for Vue 3 UI ("Enable Semantic Search")
- Fix OIDC discovery URL transformation in OAuthController.php
- Add overwrite.cli.url to setup hook for proper external URLs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Migrate all direct ENABLE_OFFLINE_ACCESS environment variable checks to
use settings.enable_offline_access, which handles both the new
ENABLE_BACKGROUND_OPERATIONS and deprecated ENABLE_OFFLINE_ACCESS vars.
Also fixes JWT issuer validation in Docker by using NEXTCLOUD_PUBLIC_ISSUER_URL
when set, resolving 401 errors caused by internal/external URL mismatch.
Changes:
- app.py: Use settings for offline access checks in setup_oauth_config,
register_oauth_client, and tool registration
- oauth_tools.py: Use settings in provision_nextcloud_access and check_logged_in
- management.py: Use settings in get_user_session
- scope_authorization.py: Use settings in require_scopes decorator
- Remove unused os imports after migration
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove URL rewriting logic from MCP server that was converting
public URLs to internal Docker URLs. This was a workaround for
Nextcloud's overwritehost setting forcing URLs to localhost:8080.
Changes:
- Remove OIDC endpoint rewriting in app.py (setup_oauth_config)
- Remove OIDC_JWKS_URI override support (no longer needed)
- Remove URL rewriting in browser_oauth_routes.py
- Remove URL rewriting in token_broker.py
- Update Helm chart values and README
- Add hybrid auth setup unit tests
- Update Astrolabe admin UI for Vue 3
The proper fix is in the previous commit which removes the
overwritehost setting from Nextcloud, allowing it to respect
the Host header from incoming requests.
Remove the overwritehost and overwrite.cli.url settings that were forcing
Nextcloud to generate URLs with localhost:8080 regardless of the incoming
request's Host header.
This was breaking Dynamic Client Registration (DCR) from the mcp-oauth
container, which needs to reach Nextcloud at http://app:80 but was getting
discovery documents with http://localhost:8080 URLs that are unreachable
from inside the Docker network.
Now Nextcloud respects the Host header:
- Browser requests to localhost:8080 → returns localhost:8080 URLs
- Container requests to app:80 → returns app:80 URLs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The deployment template only checked for clientId being set in
values.yaml, so when using existingSecret without setting clientId,
the NEXTCLOUD_OIDC_CLIENT_ID and NEXTCLOUD_OIDC_CLIENT_SECRET env
vars were never created.
This broke existingSecret for OIDC-based auth - the server would
always fall back to DCR even when pre-registered credentials were
provided via secret.
Fix: Check for EITHER clientId OR existingSecret being set before
creating the OIDC client credential env vars.
Affects both OIDC-based auth modes:
- auth.oauth.existingSecret (OAuth mode)
- auth.multiUserBasic.existingSecret (multi-user BasicAuth with offline access)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The helm-release workflow was only triggering on v* tags (MCP server
releases), not on nextcloud-mcp-server-* tags (helm chart releases).
This caused chart releases to be skipped because:
1. Helm chart version bump creates tag nextcloud-mcp-server-X.Y.Z
2. Workflow never runs for this tag (pattern didn't match)
3. Next v* tag triggers workflow at wrong commit (Chart.yaml not updated)
4. chart-releaser skips because version already exists
Fix: Add nextcloud-mcp-server-* to workflow trigger pattern so chart
releases execute at the correct commit where Chart.yaml has the new version.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Critical fix:
- deployment.yaml: Only reference OAuth credentials when clientId is set
- Fixes pod failure when using existingSecret without static OAuth creds
- Aligns deployment behavior with secret template logic
Previously, the deployment referenced OAuth credentials when either
clientId OR existingSecret was set. However, the secret template only
includes OAuth credentials when clientId is explicitly provided. This
caused pod failures when users provided an existingSecret for offline
access without static OAuth credentials (intending to use DCR).
The fix ensures OAuth env vars are only referenced when clientId is set,
matching the OAuth mode pattern and allowing DCR to work correctly with
existingSecret configurations.
Minor improvements:
- values.yaml: Clarify OAuth credentials are optional (uses DCR if not provided)
Testing verified all scenarios:
✅ Pass-through only (no offline access): No secrets/PVCs/OAuth vars
✅ Offline + DCR (no clientId): Secret with encryption key only, no OAuth vars
✅ Offline + static OAuth: Secret with all keys, OAuth vars present
✅ existingSecret without clientId: No auto secret, no OAuth vars (FIXED)
Resolves reviewer feedback from PR #447
Updates helm chart commitizen config to recognize MCP server version
bump commits (which update appVersion in Chart.yaml) as valid triggers
for helm chart version bumps.
Problem:
- When MCP server version bumps, it updates Chart.yaml appVersion
- Helm chart commitizen only matched "(helm)" scoped commits
- Result: appVersion updated but chart version not bumped
Solution:
- Extended changelog_pattern to include "bump: version X → Y" commits
- Now helm chart version will bump when either:
1. Commits with (helm) scope are made, OR
2. MCP server version bumps (updating appVersion)
This ensures chart version stays in sync with appVersion updates.
Allows multi-user BasicAuth mode to use Dynamic Client Registration (DCR)
for OAuth credentials when ENABLE_OFFLINE_ACCESS is enabled, making it
consistent with OAuth modes and reducing configuration burden.
**Changes:**
Configuration Validation:
- Relaxed OAuth credential requirements for multi-user BasicAuth
- OAuth credentials now optional when offline access enabled
- Will use DCR as fallback if NEXTCLOUD_OIDC_CLIENT_ID/SECRET not set
- Updated validation to log info instead of error when DCR will be used
Startup Logic (app.py):
- Added DCR workflow for multi-user BasicAuth before uvicorn starts
- Creates oauth_context for management APIs when offline access enabled
- Allows Astrolabe to authenticate management API calls with OAuth
- DCR runs synchronously at same lifecycle point as OAuth modes
- Added traceback import for better error logging
- Fixed type assertions for nextcloud_host
- Fixed undefined variable references in vector sync logging
Management API:
- Improved auth mode detection using proper detect_auth_mode()
- Added auth_mode field to /status endpoint:
* "basic" - Single-user BasicAuth
* "multi_user_basic" - Multi-user BasicAuth
* "oauth" - OAuth modes
* "smithery" - Smithery stateless
- Added supports_app_passwords indicator for multi-user BasicAuth
Docker Compose:
- Updated mcp-multi-user-basic service configuration:
* Enabled vector sync (VECTOR_SYNC_ENABLED=true)
* Added ENABLE_OFFLINE_ACCESS=true for app password support
* Added NEXTCLOUD_MCP_SERVER_URL for Astrolabe integration
* Documented optional static OAuth credentials
Testing:
- Updated test_config_validators.py to expect DCR fallback
- Enhanced configure_astrolabe_for_mcp_server fixture with verification
- Added debug logging to test_users_setup fixture
**Workflow:**
1. User configures ENABLE_OFFLINE_ACCESS=true
2. Server checks for static NEXTCLOUD_OIDC_CLIENT_ID/SECRET
3. If not found, performs DCR before uvicorn starts
4. DCR registers client with Nextcloud OIDC provider
5. OAuth credentials used for Astrolabe management API auth
6. Background sync can retrieve user app passwords via Astrolabe
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Adds complete app password provisioning workflow for multi-user BasicAuth
deployments, allowing users to independently enable background sync by
generating and storing Nextcloud app passwords.
**New Components:**
Backend (PHP):
- CredentialsController: Validates and stores app passwords
* Validates app password format and authenticity via OCS API
* Stores encrypted passwords in oc_preferences
* Provides status and credential management endpoints
- AstrolabeAdminSettings: Admin configuration page for MCP server URL
- AstrolabeAdminSettingsListener: Event listener for admin section
- Updated McpTokenStorage: Added background sync credential methods
Frontend:
- personalSettings.js: Form handling for app password entry
* AJAX submission with error handling
* Shows success/error notifications
* Triggers page reload after successful save
- settings.css: Styling for settings pages
- Updated personal.php template: Two-option UI
* Option 1: OAuth refresh token (future, not yet available)
* Option 2: App password (works today, recommended)
* Shows "Active" badge when provisioned
* Displays credential type and provisioned timestamp
Routes:
- POST /api/v1/background-sync/credentials - Store app password
- GET /api/v1/background-sync/status - Get provisioning status
- DELETE /api/v1/background-sync/credentials - Revoke credentials
- GET /api/v1/background-sync/credentials/{userId} - Admin only
**Testing:**
- test_astrolabe_settings_buttons.py: Integration test for UI buttons
**Workflow:**
1. User generates app password in Nextcloud Security settings
2. User navigates to Astrolabe personal settings
3. User enters app password in "Option 2: App Password" form
4. Backend validates password via OCS API call
5. Password stored encrypted in oc_preferences
6. Page reloads showing "Active" badge with credential details
7. MCP server can now use stored password for background operations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixes the Playwright-based integration test that verifies multi-user app
password provisioning for background sync in Astrolabe.
**Root Cause:**
The test was failing to extract the generated app password from Nextcloud's
"New app password" dialog due to overly specific CSS selectors that didn't
match the actual DOM structure.
**Changes:**
- Enhanced network response logging to capture HTTP status codes
- Simplified app password extraction logic:
* Wait for dialog heading using text selector
* Iterate through ALL text inputs on page
* Find password by pattern: contains dashes and length > 20
* Validate extracted password against expected format
- Added format validation with regex before returning password
- Added detailed debug logging for each extraction step
- Improved error messages with screenshot paths
**Testing:**
Test now successfully completes for both alice and bob test users:
- Logs in to Nextcloud
- Generates app password in Security settings
- Extracts password from dialog
- Navigates to Astrolabe settings
- Enters and saves app password
- Verifies "Active" badge appears
- Confirms credentials stored in database
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Restore CI test filter (-m unit -m smoke) for faster CI runs
- Replace local path reference with ADR-020 reference in config_validators.py
- Add comprehensive BasicAuthMiddleware unit tests (10 tests covering all edge cases)
Addresses critical CI issue and improves test coverage for multi-user BasicAuth mode.
Add build step for Astrolabe app in CI workflow to compile frontend
assets before docker-compose starts.
Changes:
- Install Node.js 20 for Astrolabe build
- Run composer install --no-dev for Astrolabe PHP dependencies
- Run npm ci and npm run build to compile frontend assets
This ensures the Astrolabe app is properly built in CI, similar to
the existing OIDC app build process.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Implement multi-user BasicAuth pass-through mode (ADR-020) where each
request includes BasicAuth credentials that are forwarded to Nextcloud
APIs without persistent storage.
Changes:
- Add _get_client_from_basic_auth() in context.py to extract credentials
from Authorization header (set by BasicAuthMiddleware)
- Add AstrolabeClient for app password provisioning via Astrolabe API
- Update oauth_sync.py with dual credential support (app passwords first,
then refresh tokens as fallback)
- Simplify oauth_tools.py provisioning logic
- Add integration tests for app password provisioning and multi-user BasicAuth
Features:
- Stateless multi-user mode: credentials passed per-request
- Optional background sync via app passwords (stored in Astrolabe)
- Falls back to refresh tokens if app password not available
- Test coverage for provisioning flow and pass-through mode
Related: ADR-019 (Multi-user BasicAuth), ADR-020 (Deployment Modes)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Replace static post-installation configuration with dynamic test-time
configuration to support testing multiple MCP server deployments.
Changes:
- Remove static MCP server URL and OAuth client setup from post-installation
- Add configure_astrolabe_for_mcp_server fixture (session-scoped)
- Fixture dynamically configures:
* Nextcloud system config (mcp_server_url, mcp_server_public_url)
* OAuth client creation via occ oidc:create
* Client credential storage (astrolabe_client_id, astrolabe_client_secret)
- Update existing OAuth tests to use dynamic configuration
- Add test_astrolabe_multi_server_integration.py with parametrized tests
Benefits:
- Test Astrolabe with mcp-oauth, mcp-keycloak, mcp-multi-user-basic
- Each test configures for its specific MCP server
- No static configuration conflicts between deployments
- Cleaner post-installation (37 lines, down from 85)
Test Results:
- test_astrolabe_configuration_for_different_servers: PASSED (mcp-oauth, mcp-keycloak)
- test_astrolabe_reconfiguration: PASSED
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
When commitizen finds no eligible commits to bump, it exits with
code 1 and outputs [NO_COMMITS_TO_BUMP]. This was causing the
GitHub Actions workflow to fail even though this is an expected
scenario.
Updated all three bump scripts (bump-mcp.sh, bump-helm.sh,
bump-astrolabe.sh) to:
- Detect the [NO_COMMITS_TO_BUMP] message
- Exit with code 0 (success) instead of code 1
- Output an informational message instead of an error
This allows the bump-version workflow to complete successfully
when no version bumps are needed, matching the workflow's existing
logic that handles empty BUMPED_COMPONENTS.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The chart-releaser workflow was failing when the Helm chart version hadn't
changed but the MCP server version was bumped. Added skip_existing: true to
gracefully handle this scenario.
Allows forcing specific version bumps (PATCH|MINOR|MAJOR) instead of
relying solely on commitizen's automatic detection based on conventional
commits.
Usage:
./scripts/bump-mcp.sh --increment MINOR
./scripts/bump-helm.sh --increment PATCH
./scripts/bump-astrolabe.sh --increment MAJOR
The workflow was failing to create GitHub releases with 'Not Found' error
because it lacked the required permissions. Added contents:write permission
to allow creating releases and uploading artifacts.
The pattern 'version' was too broad and matched multiple lines:
- <?xml version="1.0"?>
- <version>0.2.1</version>
- min-version="30" max-version="32"
Changed to '<version>' to specifically match only the version tag.
Also fixed version mismatch: info.xml now correctly shows 0.3.0 to match
the version in .cz.toml and package.json.
When filtering commits with grep -v, if all commits are filtered out,
grep returns exit code 1 which causes the pipeline to fail with set -e.
Wrap grep commands in { ... || true; } to ensure they don't fail the
pipeline when they filter out all results.
This fixes the workflow failure when a fix(astrolabe): commit is pushed
without any MCP server changes.
The --follow-tags flag only pushes annotated tags by default.
Commitizen creates lightweight tags, so we need to explicitly push
all tags with --tags to ensure version tags are pushed to trigger
release workflows.
BREAKING CHANGE: MCP server now bumps for ANY conventional commit except
those explicitly scoped to helm or astrolabe.
Previous behavior:
- MCP bumped only for unscoped or scope=mcp commits
- fix(ci): commits were ignored → no version bump
New behavior:
- MCP bumps for ALL commits except scope=helm or scope=astrolabe
- fix(ci): commits now trigger MCP version bump ✓
- feat(api): commits now trigger MCP version bump ✓
- Any custom scope triggers MCP version bump ✓
This treats the MCP server as the default/primary component in the
monorepo, with Helm chart and Astrolabe as opt-in specialized components.
Changes:
1. Updated bump-version.yml workflow logic to exclude helm/astrolabe
instead of only including mcp/unscoped
2. Updated pyproject.toml commitizen patterns to use negative lookahead:
(?!\((?:helm|astrolabe)\))
3. Fixed docker-build-publish.yml to only trigger on v* tags (MCP only)
4. Fixed appstore-build-publish.yml action version (v1.0.4)
5. Updated test script to use grep -P for PCRE support
6. Added test cases for ci, api, and custom scopes
All 19 scope filtering tests now pass.
Docker images should only be built for MCP server releases (v* tags),
not for Helm chart (nextcloud-mcp-server-*) or Astrolabe (astrolabe-v*)
releases.
Changed trigger from all tags to v* pattern only.
Replace commitizen-action with custom workflow that detects which
components have changes based on commit scopes and bumps them
independently.
The workflow:
1. Checks for commits with scope patterns since last tag for each component:
- MCP server: scope=mcp or unscoped, tags=v*
- Helm chart: scope=helm, tags=nextcloud-mcp-server-*
- Astrolabe: scope=astrolabe, tags=astrolabe-v*
2. Runs appropriate bump script for components with changes:
- ./scripts/bump-mcp.sh
- ./scripts/bump-helm.sh
- ./scripts/bump-astrolabe.sh
3. Pushes all created tags at once
4. Provides GitHub Actions summary showing which components were bumped
This ensures each component versions independently based on its
relevant commits, preventing the issue where all components bump
together or some components are missed.
Fixes the issue where PR #418 only bumped MCP server, leaving Helm
chart and Astrolabe at their previous versions despite having changes.
Addresses remaining high-priority code review feedback:
VERSIONING SCHEME FIXES:
- Helm chart: Changed from pep440 to semver (correct for Helm)
- Astrolabe: Changed from pep440 to semver (correct for Nextcloud apps)
- MCP server: Remains pep440 (correct for Python packages)
Helm charts must use semantic versioning per Helm specification.
Nextcloud apps use semantic versioning in info.xml and package.json.
ENHANCED ERROR HANDLING IN BUMP SCRIPTS:
All three bump scripts now include:
- Comprehensive validation checks
* Tool availability (uv)
* Directory structure (must run from repo root)
* Required files exist (Chart.yaml, info.xml, package.json)
- Better error messages
* Stderr output for errors
* Captured commitizen output on failure
* Common failure causes listed
- Success confirmation
* Clear indication of what was updated
* Next steps guidance (git push --follow-tags)
- Robust shell options (set -euo pipefail)
Scripts now provide helpful guidance when:
- No conventional commits found
- No commits with required scope
- Git working directory not clean
- Required dependencies missing
Addresses code review feedback on PR #418:
CRITICAL FIXES:
1. Workflow trigger: Changed from release:published to push:tags
- Enables "tag and publish in one step" workflow as intended
- Automatically creates GitHub release on tag push
- Removed redundant if condition (filtering now via trigger)
- Added prerelease detection based on tag (-alpha, -beta, -rc)
2. Server path: Explicitly pass server_dir to make command
- Fixes path mismatch between local (../../server) and CI
- Uses absolute path: server_dir=${{ github.workspace }}/server
- Prevents signing failures in GitHub Actions
3. Regex validation: Added test script for commitizen patterns
- Validates scope filtering works correctly
- Tests all three components: mcp, helm, astrolabe
- Tests unscoped commits (default to mcp)
- Tests breaking changes and invalid commits
- Location: scripts/test-commitizen-scopes.sh
WORKFLOW IMPROVEMENTS:
- Release creation now automatic on tag push
- Better step naming for clarity
- Consistent prerelease handling across GitHub and App Store
- Explicit server_dir prevents reliance on fragile relative paths
All 16 test cases pass for scope filtering patterns.
CRITICAL FIXES:
- Fix tag parsing in workflow to strip "astrolabe-v" instead of "v"
For tag astrolabe-v0.1.0, now correctly extracts "0.1.0"
- Add workflow filtering to only run on astrolabe-v* tags
Prevents wasting CI resources on MCP/Helm releases
RECOMMENDED IMPROVEMENTS:
- Make Nextcloud server path configurable in Makefile
Can now override: make appstore server_dir=/path/to/server
- Add dependency validation to Makefile
Checks for composer, npm, php before building
- Add signing prerequisite validation
Verifies server/occ, private key, and certificate exist
- Add dependency checks to all bump scripts
Validates uv is installed before running cz bump
These changes improve local build experience and prevent common
errors with clear error messages and installation guidance.
Add complete CI/CD pipeline for automated Astrolabe app releases:
- GitHub Actions workflow for build, sign, and publish
- Makefile for app store package creation
- Version synchronization between info.xml and package.json
- CHANGELOG.md with v0.1.0 release notes
feat: configure commitizen monorepo with independent versioning
Enable independent versioning for three components using scope-based commits:
- MCP Server (feat(mcp) or unscoped): v* tags, updates pyproject.toml + Chart.yaml:appVersion
- Helm Chart (feat(helm)): nextcloud-mcp-server-* tags, updates Chart.yaml:version
- Astrolabe App (feat(astrolabe)): astrolabe-v* tags, updates info.xml + package.json
Changes:
- Add .cz.toml configs for Astrolabe and Helm chart
- Update root pyproject.toml with scope filtering and tag ignores
- Create bump helper scripts (bump-mcp.sh, bump-helm.sh, bump-astrolabe.sh)
- Add CONTRIBUTING.md with version management documentation
- Create component-specific changelogs
- Configure appVersion/version separation for Helm chart
This allows each component to release independently while maintaining
proper version tracking and changelog generation.
This commit fixes two OAuth issues in the Astrolabe app:
1. **Always use PKCE (RFC 9207)**:
- PKCE is now used for all OAuth flows (public and confidential clients)
- Previous code only used PKCE for public clients, causing failures
- Confidential clients now use both PKCE + client_secret (defense in depth)
- Nextcloud OIDC provider requires PKCE, so token exchange was failing
2. **Add token_broker to oauth_context**:
- Token broker is now stored in oauth_context for management API access
- Fixes "Token broker not configured" error when revoking access
- Revoke endpoint needs token_broker to delete refresh tokens and invalidate cache
Changes:
- OAuthController.php: Always generate PKCE verifier/challenge for all clients
- OAuthController.php: Always include code_verifier in token exchange
- app.py: Store token_broker in oauth_context after creation
Fixes:
- Astrolabe OAuth flow now works with Nextcloud OIDC
- Revoke/disconnect functionality now works in Astrolabe settings
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The files_pdfviewer app route is internal to Nextcloud and not a valid
external URL. Reverted to using the standard Files app viewer URL for
all file types.
- Removed PDF-specific handling that used /apps/files_pdfviewer/
- All files now link to /apps/files/files/{id} (standard Files viewer)
- Fixes broken links in chunk modal titles and search results
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add type casts for Starlette app state access
- Add assertions for cipher, card, board, stack after initialization
- Add None checks for XML element text attributes
- Handle __package__ being None in tracing setup
- Fix TokenBrokerService initialization to use storage credentials
Resolves 42 type warnings from ty-check, enabling CI linting to pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Move alembic/ directory to nextcloud_mcp_server/alembic/ subpackage
- Update migrations.py to use package location instead of alembic.ini
- Update env.py to set script_location dynamically
- Update alembic.ini for development CLI usage
- Fix Dockerfile typo: .vnev -> .venv
This fixes FileNotFoundError when running in Docker with non-editable
install. The alembic package is now installed with the main package,
making it work in both development and production environments.
Resolves: Docker startup error 'alembic.ini not found at
/app/.venv/lib/python3.12/site-packages/alembic.ini'
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Rewrote Astrolabe README to be user-friendly and release-ready by
incorporating pitch.md content and moving technical details to linked
documentation.
Key changes:
- Incorporated compelling pitch narrative as opening
- Restructured around "What You Can Do" rather than architecture
- Added clear use cases for individuals, teams, and developers
- Simplified installation to 3 steps
- Moved OAuth flow and architecture details to ADR links
- Added emoji sections for visual appeal
- Focused on benefits over implementation
Sections:
- What You Can Do (search, visualization, AI agents)
- Installation (app store + manual)
- Quick Start (3-step setup)
- Features (personal, admin, unified search)
- Use Cases (research, collaboration, RAG workflows)
- Requirements (Nextcloud 30+, MCP server, OAuth)
- Documentation (links to installation, configuration, ADRs)
- Troubleshooting (quick fixes with links to detailed guides)
This README is now suitable for Nextcloud App Store submission.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Updated docs/running.md to use Docker container examples instead of
direct Python commands. This aligns with the CLI change to require
explicit 'run' subcommand while maintaining backward compatibility
for Docker users (ENTRYPOINT includes 'run').
Key changes:
- Quick Start: Use Docker commands instead of uv run
- Running Locally → Running with Docker: All examples use Docker
- Development Mode: Added CLI subcommands documentation (run/db)
- Database Migrations: Documented Alembic integration for developers
- Server Options: Docker port mapping instead of --host/--port flags
- Process Management: Simplified to Docker Compose only (removed systemd)
- Performance Tuning: Production Docker Compose with resource limits
- Troubleshooting: Docker logs and debug commands
Updated Dockerfile ENTRYPOINT:
- Changed from: ["/app/.venv/bin/nextcloud-mcp-server", "--host", "0.0.0.0"]
- Changed to: ["/app/.venv/bin/nextcloud-mcp-server", "run", "--host", "0.0.0.0"]
No breaking changes for Docker/Helm users - container interface unchanged.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Implements Alembic for managing token storage database schema versions.
Migrations run automatically on startup with full backward compatibility.
**Changes:**
- Add Alembic dependency (1.14.0+) and SQLAlchemy (auto-installed)
- Create migration infrastructure in alembic/ directory
- Add initial migration (001) capturing current schema
- Modify RefreshTokenStorage.initialize() to run migrations via anyio
- Add CLI commands: db upgrade, current, history, downgrade, migrate
- Add comprehensive migration documentation
**Backward Compatibility:**
- Pre-Alembic databases automatically stamped with revision 001
- No schema changes for existing databases
- Automatic upgrade on first startup after update
**Migration Strategy:**
Three scenarios handled:
1. New database → Run migrations from scratch
2. Pre-Alembic database → Stamp with 001 (no changes)
3. Alembic-managed → Upgrade to latest
**Architecture:**
- Uses anyio.to_thread.run_sync() for structured concurrency
- Alembic env.py runs with anyio.run() in worker thread
- SQLite-friendly migration patterns documented
- No ThreadPoolExecutor needed (anyio handles it)
**CLI Usage:**
```bash
nextcloud-mcp-server db upgrade # Upgrade to latest
nextcloud-mcp-server db current # Show version
nextcloud-mcp-server db history # View changelog
nextcloud-mcp-server db downgrade # Rollback (with confirmation)
nextcloud-mcp-server db migrate "description" # Create migration
```
**Testing:**
- All 13 webhook storage tests pass
- New/pre-Alembic database scenarios validated
- anyio integration tested
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add clickable link to modal title with OpenInNew icon
- Store currentResult to enable document navigation
- Fix deck_card URLs to use metadata.board_id
- Fix news_item URLs to use external article URL from metadata.url
- Add hover styling for title link and icon
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Update the unified search provider to show only chunk/page metadata
in search results, consistent with the chunk visualization result list.
Also fix news item URLs to link directly to the specific item.
Changes to SemanticSearchProvider:
1. Result display improvements:
- Remove excerpt text from search result subline
- Show only chunk/page metadata (e.g., "Chunk 2/5 · Page 3/10")
- Consistent with chunk visualization UI in App.vue
2. News item URL fix:
- Change from generic news index to specific item URL
- Format: /apps/news/item/{id}
- Allows direct navigation to the news article
3. Code cleanup:
- Remove unused $excerpt variable
- Remove unused truncateExcerpt() method
- Simplify transformResult() logic
Benefits:
- Cleaner, more scannable search results
- Consistent UX between unified search and app UI
- Functional links to news items instead of generic news page
- Reduced code complexity
Replace expensive Plotly.restyle() hover handlers with native hoverlabel
styling to indicate clickable points without performance degradation.
Implementation:
- Add hoverlabel configuration to document trace with distinct styling
- Bright blue background (#0082c9) to make hover state obvious
- Larger font size (15px) for better visibility
- White text for contrast against blue background
- Handled natively by Plotly - no JavaScript event handlers needed
Benefits:
- Zero performance impact - no chart re-renders on hover
- Smooth, responsive hover feedback
- Clear visual indication that points are clickable
- Consistent with existing hover tooltip pattern
Removed:
- Expensive handlePlotHover() and handlePlotUnhover() methods
- Plotly.restyle() calls that caused severe lag and freezing
- hover/unhover event listener registrations
The hover tooltip now uses the styled hoverlabel to stand out visually,
providing clear feedback that points are interactive without any
performance cost.
Enable users to click on points in the vector space visualization to
open the chunk viewer modal, providing a more direct interaction
method alongside the existing "Show Chunk" button.
Implementation details:
- Register plotly_click event handler in renderPlot() after chart creation
- Add handlePlotClick() method to process click events
- Use point index mapping to access full result object from this.results
- Add loading guard in viewChunk() to prevent concurrent chunk loading
- Add cursor styling: pointer for result points, default for query point
- Add beforeDestroy() lifecycle hook to cleanup event handlers
Features:
- Both interaction methods work: click chart points OR "Show Chunk" button
- Only result points (trace 0) are clickable, query point (trace 1) ignored
- Pointer cursor on hover indicates clickable points
- Loading state prevents rapid clicks from causing issues
- Memory leak prevention through proper event handler cleanup
Technical approach:
- Uses index mapping (not data duplication) for efficiency
- Results and coordinates arrays have guaranteed 1:1 mapping from API
- Event handler re-registered on each chart re-render
- CSS-based cursor styling (more performant than JS hover handlers)
Testing:
- ESLint validation passed
- Follows Vue 2.7 component property order conventions
- Compatible with existing chunk viewer modal
This commit implements three UI improvements for the chunk viewer:
1. Fixed modal footer with navigation controls
- Moved PDF navigation buttons to a fixed footer
- Footer remains visible while scrolling content
- Three-section layout: fixed header, scrollable body, fixed footer
2. Removed duplicate navigation controls
- Removed previous/next buttons from PDFViewer component
- Controls now only in App.vue modal footer
- Cleaned up unused imports and CSS
3. Markdown rendering for chunk content
- Created MarkdownViewer component using markdown-it
- Renders markdown content aligned with Nextcloud design system
- Removed problematic markdown-it-task-checkbox dependency
- Combines before/chunk/after context with visual separators
4. Cleaned up search results display
- Removed excerpt snippets from results list
- Kept only chunk/page metadata for cleaner UI
The modal structure now has:
- Fixed header (title + close button)
- Scrollable body (PDF canvas or markdown content)
- Fixed footer (page navigation - always visible)
Fixes markdown rendering "require is not defined" error by using
only markdown-it without CommonJS plugins.
Fixes 401 errors after first token refresh when using IdPs that
implement refresh token rotation (Keycloak, modern OAuth providers).
**Root Cause**:
McpTokenStorage::getAccessToken() was discarding the new refresh token
returned by the IdP after successful refresh, always keeping the old one.
This caused:
- First refresh: works (uses original refresh token)
- Second refresh: fails with 401 (old refresh token invalidated by IdP)
**Solution**:
Use new refresh token from IdP response if provided, fall back to old
token for providers that don't rotate refresh tokens.
**Changed**:
- lib/Service/McpTokenStorage.php:184
From: $token['refresh_token'] // Always old token
To: $newTokenData['refresh_token'] ?? $token['refresh_token']
**Verified**:
ApiController already handles rotation correctly using the same pattern.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit includes two improvements to the Astroglobe semantic search UI:
1. **Multi-select Document Types** (App.vue):
- Changed NcCheckboxRadioSwitch binding from v-model to :checked/:update:checked
- Implemented toggleDocType() method to manually manage selectedDocTypes array
- Fixes issue where only single document type could be selected at a time
- Users can now filter search results by multiple doc types simultaneously
2. **PDF Viewer Reactive Rendering** (PDFViewer.vue):
- Refactored canvas rendering to use Vue reactive watcher pattern
- Added watcher on 'loading' state that triggers rendering when canvas available
- Removed imperative renderPage() call from loadPDF() method
- Inspired by files_pdfviewer's promise/event-based initialization approach
- Improves alignment with Vue's reactive data flow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit addresses 4 critical issues identified in code review:
1. **Token Rotation Race Condition** (token_broker.py)
- Added per-user locking mechanism to prevent concurrent refresh token corruption
- Implemented double-check pattern for cache after acquiring lock
- Users can now safely refresh concurrently without token desync
2. **Hardcoded OAuth Client ID** (PHP files)
- Made client ID configurable via `astroglobe_client_id` in system config
- Updated McpServerClient to provide getClientId() method
- Injected McpServerClient into IdpTokenRefresher and OAuthController
- Updated admin settings UI to display client ID configuration status
- App gracefully handles missing client ID with warnings in admin UI
3. **Missing Cache Invalidation** (management.py:revoke_user_access)
- Added cache.invalidate() call when revoking user access
- Ensures both storage AND cache are cleared atomically
- Prevents stale cached tokens from being used after revocation
4. **Error Message Exposure** (management.py)
- Created _sanitize_error_for_client() helper function
- Updated all error handlers to log detailed errors internally
- Returns generic messages to clients to prevent information leakage
- Protects against exposing database paths, API URLs, tokens, etc.
All changes are backward compatible and preserve existing functionality.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Configure Astroglobe as a confidential OAuth client with client_secret
to support token refresh for long-lived sessions.
Changes:
- Update install-astroglobe-app hook to:
- Create confidential client instead of public
- Add offline_access scope for refresh tokens
- Extract and store client_secret in system config
- Display secret (truncated) for verification
- Update trusted-domains hook (formatting)
Benefits:
- Enables automatic token refresh without re-authentication
- Supports long-lived backend operations
- Better security for server-side OAuth flows
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Improve unified search results with chunk/page metadata and add
webhook management capabilities to McpServerClient.
Changes:
- SemanticSearchProvider improvements:
- Display chunk position (e.g., "Chunk 2/5")
- Display page numbers for PDFs (e.g., "Page 3/10")
- Fix file links to open in Files app correctly
- Fix deck card links to use proper URL format
- Show metadata in subline before excerpt
- Use proper icons and thumbnails for each doc type
- McpServerClient webhook methods:
- listWebhooks() - Get all registered webhooks
- createWebhook() - Register new webhook
- deleteWebhook() - Remove webhook registration
- enableWebhook() / disableWebhook() - Toggle webhook status
- getWebhookLogs() - Retrieve delivery logs
Benefits:
- Better search result context with chunk and page info
- Clickable links that open correct resources
- Full webhook lifecycle management via API
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add admin interface for configuring real-time webhook sync with
pre-configured presets for common scenarios.
Changes:
- Add webhook presets section to admin settings page
- Shows available presets filtered by installed apps
- Enable/disable presets with one click
- Displays current webhook status
- Add client secret configuration status display
- Shows whether confidential client is configured
- Provides setup instructions for optional client secret
- Add adminSettings.js for webhook management
- Load webhook presets via API
- Enable/disable webhook presets
- Handle search settings form submission
- Update vite.config.js to build adminSettings entry point
- Pass clientSecretConfigured flag to template
UI Features:
- Real-time preset status (enabled/disabled)
- One-click enable/disable for webhook bundles
- App-aware filtering (only shows presets for installed apps)
- Clear instructions for requirements and setup
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Changes:
- Add file_path to metadata in semantic and BM25 hybrid search algorithms
for PDF viewer integration (search/semantic.py:161-163, search/bm25_hybrid.py:230-232)
- Include chunk_start_offset, chunk_end_offset, page_number, and page_count
in search results for rich chunk display (api/management.py:981-1004)
- Add point_id field to SearchResult for batch retrieval (models/semantic.py)
- Fix type narrowing for chunk context API parameters (api/management.py:1102-1111)
- Fix None-safety in doc_types discovery (search/algorithms.py:114)
This enables the Astroglobe UI to display PDF pages at the correct
location for matched chunks.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Improve search result display to match Nextcloud's native search providers by using mimetype-specific icons and preview thumbnails.
**File Results:**
- Use preview thumbnails for images/PDFs (core.Preview API)
- Use mimetype-specific icon classes (icon-pdf, icon-text, icon-image, etc.)
- Detect folders and use icon-folder appropriately
**Other Document Types:**
- Notes: icon-notes
- Deck Cards: icon-deck
- Calendar: icon-calendar
- News: icon-rss
- Contacts: icon-contacts
**API Changes:**
- Management API now includes mime_type in search results
- SemanticSearchProvider uses IMimeTypeDetector and IPreview services
This makes Astroglobe search results visually consistent with Files, Notes, and other native providers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>