Scan report
cbcoutinho / nextcloud-mcp-server
Could an attacker exploit mistakes in this code?
Strong warning signs. Do not run or install this without a careful review.
- High
- 3
- Medium
- 179
- Low
- 54
Sources: Bandit, Semgrep, pip-audit
Does this code look like it is trying to cause harm?
Real issues worth reviewing before you rely on this code.
- High
- 6
- Medium
- 6
- Low
- 46
Sources: YARA, ClamAV, GuardDog, dependency checker
- Likely real
- 196
- Uncertain
- 19
- Likely noise
- 79
The risk classifier rates every finding for how likely it is to be a real issue. Likely noise still counts, just much less.
What we found, in plain English
This repository has two major security issues that are critical to address before anyone can use it. The most serious problem is that the code contains insecure patterns, which means an attacker could exploit these flaws and steal sensitive information or break into the system. Additionally, there are several signs of malicious intent in the code, including suspicious typosquatting and workflows that leak secrets by default. While some findings appear to be ordinary mistakes like logging hardcoded passwords or missing version locks for dependencies, they still represent significant risks because they could lead to credential exposure or supply chain attacks if not fixed. The overall score indicates a high level of vulnerability that requires immediate attention from the developer team to ensure the code is safe and secure.
Written by a local language model from the findings below. The risk scores come from the classifier, not from this summary.
Detections by engine
Score over time
Overall score each time this repository was scanned.
Findings
Highest severity first. Select a finding to see the code and what it means.
-
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Yes, 90% confidence
Typosquatting: 'psycopg' is suspiciously similar to 'psycopg2'
psycopg[binary,pool]>=3.2 -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /.github/workflows/bump-version.yml line 130
- Likely real
- Yes, 87% confidence
This workflow uses `secrets: inherit` to pass all of the calling workflow's secrets to a reusable workflow. This violates the principle of least privilege because the called workflow receives access to every secret in the repository, not just the ones it needs. If the called workflow is compromised or sourced from a third party, an attacker gains access to all repository secrets. Instead, explicitly pass only the secrets that the called workflow requires using the `secrets:` map, e.g. `secrets: { MY_SECRET: ${{ secrets.MY_SECRET }} }`.
secrets: inherit -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /.github/workflows/pact.yml line 86
- Likely real
- Yes, 57% confidence
Using variable interpolation `${{...}}` with `github` context data in a `run:` step could allow an attacker to inject their own code into the runner. This would allow them to steal secrets and code. `github` context data can have arbitrary user input and should be treated as untrusted. Instead, use an intermediate environment variable with `env:` to store the data and use the environment variable in the `run:` script. Be sure to use double-quotes the environment variable, like this: "$ENVVAR".
run: | publish() { pact broker publish tests/contract/pacts \ --broker-base-url "$PACT_BROKER" \ --broker-username "$PACT_USERNAME" \ --broker-password "$PACT_PASSWORD" \ --consumer-app-version "${{ github.sha }}" \ --branch "${{ github.head_ref || github.ref_name }}" } publish && exit 0 echo "::warning title=Retrying pact publish::First attempt failed; retrying once to rule out a transient broker/tailnet error." publish -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 1654
- Likely real
- Yes, 53% confidence
Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.
cursor = await db.execute( f""" UPDATE oauth_sessions SET {", ".join(update_fields)} WHERE session_id = ? """, # NOSONAR params, ) -
- Engine
- YARA · Malware signature rules
- Counts towards
- Malicious pattern
- File
- /.github/workflows/bump-version.yml
- Likely real
- Probably noise, 1% confidence
YARA rule matched: detect_download_and_execute — Downloads a remote resource AND executes it - the actually dangerous combination, not just downloading alone
-
- Engine
- YARA · Malware signature rules
- Counts towards
- Malicious pattern
- File
- /.github/workflows/release.yml
- Likely real
- Probably noise, 1% confidence
YARA rule matched: detect_download_and_execute — Downloads a remote resource AND executes it - the actually dangerous combination, not just downloading alone
-
- Engine
- YARA · Malware signature rules
- Counts towards
- Malicious pattern
- File
- /.github/workflows/docker-build-publish.yml
- Likely real
- Probably noise, 1% confidence
YARA rule matched: detect_download_and_execute — Downloads a remote resource AND executes it - the actually dangerous combination, not just downloading alone
-
- Engine
- YARA · Malware signature rules
- Counts towards
- Malicious pattern
- File
- /.github/scripts/install-pact-cli.sh
- Likely real
- Probably noise, 1% confidence
YARA rule matched: detect_download_and_execute — Downloads a remote resource AND executes it - the actually dangerous combination, not just downloading alone
-
- Engine
- YARA · Malware signature rules
- Counts towards
- Malicious pattern
- File
- /mcpb/run.sh
- Likely real
- Probably noise, 1% confidence
YARA rule matched: detect_download_and_execute — Downloads a remote resource AND executes it - the actually dangerous combination, not just downloading alone
-
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/api/passwords.py line 156
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Username mismatch in app password operation for path user %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.warning( "Username mismatch in app password operation for path user %s", path_user_id ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/vector/oauth_sync.py line 213
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Using app password for background sync: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info("Using app password for background sync: %s", user_id) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 2167
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Stored scoped app password for user %s (scopes=%s, username=%s)" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info( "Stored scoped app password for user %s (scopes=%s, username=%s)", user_id, # NULL is "no additional restriction", not "all allowed" — the # OAuth token still bounds the session (see require_scopes). "unrestricted" if scopes is None else len(scopes), username or "N/A", ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/browser_oauth_routes.py line 613
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Refresh token stored for user %s (lookup key: %s...)" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info( "Refresh token stored for user %s (lookup key: %s...)", user_id, state[:16], ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/unified_verifier.py line 476
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Management API token validated (no audience check) for user: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug( "Management API token validated (no audience check) for user: %s", payload.get("sub"), ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 2219
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "No app password found for user %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug("No app password found for user %s", user_id) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/token_broker.py line 275
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Token found in cache after lock acquisition for user %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug( "Token found in cache after lock acquisition for user %s", user_id ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/token_broker.py line 289
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Token refreshed by another thread for user %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug( "Token refreshed by another thread for user %s", user_id ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/token_broker.py line 316
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Generated background token for user %s with scopes: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info( "Generated background token for user %s with scopes: %s", user_id, required_scopes, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/token_broker.py line 325
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Failed to get background token for user %s: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.error( "Failed to get background token for user %s: %s", user_id, e, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/token_broker.py line 495
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Refreshed access token with scopes %s (expires in %ss)" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info( "Refreshed access token with scopes %s (expires in %ss)", scopes, expires_in ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 2080
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "App password for %s validated (HTTP %s)" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug( "App password for %s validated (HTTP %s)", user_id, response.status_code, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/unified_verifier.py line 903
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Token introspected successfully for user: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug( "Token introspected successfully for user: %s", introspection_data.get("sub"), ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/server/oauth_tools.py line 118
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret " get_provisioning_status: app password FOUND for user_id=%s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug( " get_provisioning_status: app password FOUND for user_id=%s", user_id, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/server/oauth_tools.py line 579
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "No refresh token found for user_id=%s (checked provisioning_client_id=%s... and user_id) — " "user completed elicitation but token wasn't stored" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.warning( "No refresh token found for user_id=%s (checked provisioning_client_id=%s... and user_id) — " "user completed elicitation but token wasn't stored", user_id, state[:16], ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/vector/oauth_sync.py line 150
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "[BasicAuth] Removed stale app password for %s after HTTP %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info( "[BasicAuth] Removed stale app password for %s after HTTP %s", user_id, status_code, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/vector/oauth_sync.py line 158
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "[BasicAuth] No stale app password to remove for %s (HTTP %s)" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug( "[BasicAuth] No stale app password to remove for %s (HTTP %s)", user_id, status_code, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/vector/oauth_sync.py line 164
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "[BasicAuth] Failed to remove stale app password for %s: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.warning( "[BasicAuth] Failed to remove stale app password for %s: %s", user_id, e ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/vector/oauth_sync.py line 523
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "[BasicAuth] Periodic cleanup removed %s stale app password(s): %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info( "[BasicAuth] Periodic cleanup removed %s stale app password(s): %s", len(removed), removed, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/vector/queue/procrastinate.py line 221
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "ingest.skip_no_credentials user=%s doc=%s:%s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.warning( "ingest.skip_no_credentials user=%s doc=%s:%s", user_id, doc_type, doc_id ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/api/passwords.py line 613
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Deleted app password for user: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info("Deleted app password for user: %s", username) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 2072
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "App password for %s is invalid (HTTP %s), removing" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info( "App password for %s is invalid (HTTP %s), removing", user_id, response.status_code, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 1996
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "No app password to delete for user %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug("No app password to delete for user %s", user_id) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 1989
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Deleted app password for user %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info("Deleted app password for user %s", user_id) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 1959
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Failed to decrypt app password for user %s: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.error("Failed to decrypt app password for user %s: %s", user_id, e) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 1952
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Retrieved app password for user %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug("Retrieved app password for user %s", user_id) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 1942
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "No app password found for user %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug("No app password found for user %s", user_id) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 1899
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Stored app password for user %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info("Stored app password for user %s", user_id) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 949
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Retrieved refresh token for user %s (flow_type: %s)" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug( "Retrieved refresh token for user %s (flow_type: %s)", user_id, flow_type, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/session_backend.py line 100
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Session %s… has no refresh token for user %s; rejecting" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.warning( "Session %s… has no refresh token for user %s; rejecting", session_id[:8], user_id, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/api/passwords.py line 520
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Provisioned app password for user: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info("Provisioned app password for user: %s", username) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/app.py line 844
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "BasicAuth credentials extracted for user: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug( "BasicAuth credentials extracted for user: %s", username ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/app.py line 1226
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Using static OIDC client credentials: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info("Using static OIDC client credentials: %s", client_id) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/app.py line 1917
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "🔍 list_tools called - Token type: %s, User scopes: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info( "🔍 list_tools called - Token type: %s, User scopes: %s", "JWT" if is_jwt else "opaque/none", user_scopes, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/app.py line 2453
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Cleaned up %s stale app password(s): %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info( "Cleaned up %s stale app password(s): %s", len(removed), removed, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/grant_ownership.py line 119
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Could not revoke the refused Login Flow app password: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.warning("Could not revoke the refused Login Flow app password: %s", e) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/scope_authorization.py line 147
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "No access token for %s - allowing (BasicAuth mode)" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug( "No access token for %s - allowing (BasicAuth mode)", func_name ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 936
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Refresh token for user %s has expired (expired at %s)" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.warning( "Refresh token for user %s has expired (expired at %s)", user_id, expires_at, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/browser_oauth_routes.py line 805
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Refresh token revocation returned HTTP %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.warning( "Refresh token revocation returned HTTP %s", response.status_code ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/api/passwords.py line 249
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Credential validation failed: HTTP %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.warning("Credential validation failed: HTTP %s", response.status_code) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/api/passwords.py line 286
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Credential validation failed: OCS statuscode=%s, user_id present=%s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.warning( "Credential validation failed: OCS statuscode=%s, user_id present=%s", statuscode, ocs_user_id is not None, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/app.py line 2459
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "App password cleanup failed (non-fatal): %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.warning("App password cleanup failed (non-fatal): %s", e) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/astrolabe_client.py line 127
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "No background-sync credentials for user: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug("No background-sync credentials for user: %s", user_id) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/browser_oauth_routes.py line 464
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Token exchange failed (correlation_id=%s): HTTP %s - %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.error( "Token exchange failed (correlation_id=%s): HTTP %s - %s", correlation_id, e.response.status_code, error_body, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/browser_oauth_routes.py line 487
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Token exchange failed (correlation_id=%s): %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.error("Token exchange failed (correlation_id=%s): %s", correlation_id, e) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/browser_oauth_routes.py line 543
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "ID token verification failed (correlation_id=%s): %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.error( "ID token verification failed (correlation_id=%s): %s", correlation_id, e, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/token_broker.py line 407
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Refreshed access token (expires in %ss)" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info("Refreshed access token (expires in %ss)", expires_in) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 1234
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Stored OAuth client credentials (client_id: %s..., expires at %s)" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info( "Stored OAuth client credentials (client_id: %s..., expires at %s)", client_id[:16], client_secret_expires_at, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 1320
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Retrieved OAuth client credentials (client_id: %s...)" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug( "Retrieved OAuth client credentials (client_id: %s...)", client_id[:16] ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 2087
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Could not validate app password for %s: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.warning("Could not validate app password for %s: %s", user_id, e) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/token_broker.py line 59
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Cached token expired for user %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug("Cached token expired for user %s", user_id) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/token_broker.py line 64
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Cached token expiring soon for user %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug("Cached token expiring soon for user %s", user_id) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/token_broker.py line 80
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Cached token for user %s until %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug("Cached token for user %s until %s", user_id, expiry) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/token_broker.py line 87
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Invalidated cached token for user %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug("Invalidated cached token for user %s", user_id) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/token_broker.py line 217
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "No refresh token found for user %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info("No refresh token found for user %s", user_id) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/token_broker.py line 233
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Failed to get Nextcloud token for user %s: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.error("Failed to get Nextcloud token for user %s: %s", user_id, e) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/token_broker.py line 301
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "No refresh token found for user %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info("No refresh token found for user %s", user_id) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/token_broker.py line 382
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Token refresh failed: %s - %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.error( "Token refresh failed: %s - %s", response.status_code, response.text ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/token_broker.py line 403
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Stored rotated refresh token for user %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info("Stored rotated refresh token for user %s", user_id) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 1101
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "No refresh token to delete for user %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug("No refresh token to delete for user %s", user_id) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/token_broker.py line 453
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Token refresh request to %s with client_id=%s..." being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info( "Token refresh request to %s with client_id=%s...", token_endpoint, self.client_id[:16], ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/token_broker.py line 466
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Token refresh with scopes failed: %s - %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.error( "Token refresh with scopes failed: %s - %s", response.status_code, response.text, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/token_broker.py line 491
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Stored rotated refresh token for user %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info("Stored rotated refresh token for user %s", user_id) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/token_broker.py line 515
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "No refresh token to rotate for user %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.warning("No refresh token to rotate for user %s", user_id) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/token_broker.py line 567
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Rotated master refresh token for user %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info("Rotated master refresh token for user %s", user_id) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/token_broker.py line 576
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Failed to refresh master token for user %s: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.error("Failed to refresh master token for user %s: %s", user_id, e) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/unified_verifier.py line 87
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Userinfo token validation fallback enabled: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info( "Userinfo token validation fallback enabled: %s", self.userinfo_uri ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/unified_verifier.py line 685
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Token rejected (%s/%s, %s) for client %s%s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.warning( "Token rejected (%s/%s, %s) for client %s%s", method, reason, "our fault" if result == "error" else "caller's token", client_id or "unknown", f": {detail}" if detail else "", ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/unified_verifier.py line 1016
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Token validated via userinfo for user: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug("Token validated via userinfo for user: %s", data.get("sub")) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/unified_verifier.py line 1071
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Extracted scopes from token - scope claim: '%s' -> scopes list: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug( "Extracted scopes from token - scope claim: '%s' -> scopes list: %s", scope_string, scopes, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/unified_verifier.py line 1088
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Token validated via userinfo has no 'exp'; caching for %ss only" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug( "Token validated via userinfo has no 'exp'; caching for %ss only", ttl, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/oauth_routes.py line 1539
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "AS proxy token refresh failed: %s %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.error( "AS proxy token refresh failed: %s %s", response.status_code, _redact_error_body(response.text), ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/browser_oauth_routes.py line 598
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Storing refresh token for user_id=%s state=%s... scopes=%s expires_at=%s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug( "Storing refresh token for user_id=%s state=%s... scopes=%s expires_at=%s", user_id, state[:16], granted_scopes, refresh_expires_at, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/browser_oauth_routes.py line 723
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Refresh token revoked + deleted for user %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info("Refresh token revoked + deleted for user %s", user_id) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/providers/gateway.py line 106
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Obtained embedding-gateway M2M token (expires in %ss)" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info( "Obtained embedding-gateway M2M token (expires in %ss)", expires_in ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/oauth_routes.py line 907
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "✓ Stored Flow 2 master refresh token for user %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug("✓ Stored Flow 2 master refresh token for user %s", user_id) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/oauth_routes.py line 1126
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "AS proxy token exchange failed: %s %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.error( "AS proxy token exchange failed: %s %s", response.status_code, _redact_error_body(response.text), ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/oauth_routes.py line 1154
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "AS proxy: IdP token response: refresh_token=%s scope=%s expires_in=%s " "token_type=%s fields=%s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info( "AS proxy: IdP token response: refresh_token=%s scope=%s expires_in=%s " "token_type=%s fields=%s", "issued" if nc_token_response.get("refresh_token") else "ABSENT", nc_token_response.get("scope"), nc_token_response.get("expires_in"), nc_token_response.get("token_type"), _redact(nc_token_response), ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/oauth_routes.py line 1259
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "AS proxy token request: grant_type=%s params=%s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info( "AS proxy token request: grant_type=%s params=%s", grant_type, _redact_form(form), ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/oauth_routes.py line 1328
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "AS proxy token: received code=%s client_id=%s redirect_uri=%s " "code_verifier=%s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug( "AS proxy token: received code=%s client_id=%s redirect_uri=%s " "code_verifier=%s", code[:8] + "..." if code else None, client_id, redirect_uri, "present" if code_verifier else "missing", ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/oauth_routes.py line 1382
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "AS proxy token: client_id mismatch (got=%s, expected=%s)" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.warning( "AS proxy token: client_id mismatch (got=%s, expected=%s)", client_id, entry.client_id, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/oauth_routes.py line 1407
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "AS proxy token: redirect_uri mismatch (got=%s, expected=%s)" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.warning( "AS proxy token: redirect_uri mismatch (got=%s, expected=%s)", redirect_uri, entry.client_redirect_uri, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/oauth_routes.py line 1452
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "AS proxy token: returning IdP token to client %s: refresh_token=%s " "scope=%s expires_in=%s fields=%s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info( "AS proxy token: returning IdP token to client %s: refresh_token=%s " "scope=%s expires_in=%s fields=%s", entry.client_id, "issued" if has_refresh else "ABSENT", entry.nc_token_response.get("scope"), entry.nc_token_response.get("expires_in"), _redact(entry.nc_token_response), ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/browser_oauth_routes.py line 566
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Refresh token expires in %ss (at timestamp %s)" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug( "Refresh token expires in %ss (at timestamp %s)", refresh_expires_in, refresh_expires_at, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/oauth_routes.py line 1560
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "AS proxy refresh: succeeded: refresh_token=%s scope=%s expires_in=%s fields=%s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info( "AS proxy refresh: succeeded: refresh_token=%s scope=%s expires_in=%s fields=%s", "rotated" if has_refresh else "not-rotated", refreshed.get("scope"), refreshed.get("expires_in"), _redact(refreshed), ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/provision_routes.py line 120
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Login Flow v2 completed but no app_password (provision_id=%s)" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.error( "Login Flow v2 completed but no app_password (provision_id=%s)", provision_id, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/scope_authorization.py line 420
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "✅ Extracted scopes from access token: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info("✅ Extracted scopes from access token: %s", scopes) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 516
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Initialized refresh token storage at %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info( "Initialized refresh token storage at %s", mask_db_password(self.database_url), ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 919
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "No refresh token found for user %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug("No refresh token found for user %s", user_id) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 971
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Failed to decrypt refresh token for user %s: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.error("Failed to decrypt refresh token for user %s: %s", user_id, e) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 1014
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "No refresh token found for provisioning_client_id %s..." being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug( "No refresh token found for provisioning_client_id %s...", provisioning_client_id[:16], ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 1033
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Refresh token for provisioning_client_id %s... has expired" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.warning( "Refresh token for provisioning_client_id %s... has expired", provisioning_client_id[:16], ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 1043
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Retrieved refresh token for provisioning_client_id %s... (user_id: %s)" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug( "Retrieved refresh token for provisioning_client_id %s... (user_id: %s)", provisioning_client_id[:16], user_id, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 1060
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Failed to decrypt refresh token for provisioning_client_id %s...: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.error( "Failed to decrypt refresh token for provisioning_client_id %s...: %s", provisioning_client_id[:16], e, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 1094
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Deleted refresh token for user %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info("Deleted refresh token for user %s", user_id) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/server/oauth_tools.py line 138
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret " get_provisioning_status: looking up refresh token for user_id=%s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug( " get_provisioning_status: looking up refresh token for user_id=%s", user_id ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/server/oauth_tools.py line 166
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret " get_provisioning_status: no credentials found for user_id=%s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug( " get_provisioning_status: no credentials found for user_id=%s", user_id ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/server/oauth_tools.py line 171
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret " get_provisioning_status: refresh token FOUND for user_id=%s " "flow_type=%s provisioning_client_id=%s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug( " get_provisioning_status: refresh token FOUND for user_id=%s " "flow_type=%s provisioning_client_id=%s", user_id, token_data.get("flow_type"), token_data.get("provisioning_client_id", "N/A"), ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/server/oauth_tools.py line 560
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "No token via provisioning_client_id=%s...; falling back to user_id=%s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug( "No token via provisioning_client_id=%s...; falling back to user_id=%s", state[:16], user_id, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/vector/oauth_sync.py line 282
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "[BasicAuth] Credential validation failed for %s (HTTP %s), " "removing stale credential and not starting scan loop" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.warning( "[BasicAuth] Credential validation failed for %s (HTTP %s), " "removing stale credential and not starting scan loop", user_id, e.response.status_code, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/vector/oauth_sync.py line 503
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "[BasicAuth] Credential cleanup task started (interval: %ss)" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info( "[BasicAuth] Credential cleanup task started (interval: %ss)", CREDENTIAL_CLEANUP_INTERVAL, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/vector/oauth_sync.py line 529
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "[BasicAuth] Periodic credential cleanup failed (non-fatal): %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.warning( "[BasicAuth] Periodic credential cleanup failed (non-fatal): %s", format_exception_group(e), ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/browser_oauth_routes.py line 809
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Refresh token revocation failed: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.warning("Refresh token revocation failed: %s", e) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/browser_oauth_routes.py line 511
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "ID token present: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug("ID token present: %s", id_token is not None) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/oauth_routes.py line 855
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "ID token verification failed: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.error("ID token verification failed: %s", e) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/oauth_routes.py line 1184
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "AS proxy: ID token verification failed: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.error("AS proxy: ID token verification failed: %s", e) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/oauth_routes.py line 1270
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "AS proxy token: unsupported grant_type=%s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.warning("AS proxy token: unsupported grant_type=%s", grant_type) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/scope_authorization.py line 508
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Token format check: is_jwt=%s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug("Token format check: is_jwt=%s", is_jwt) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 1335
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Failed to decrypt OAuth client credentials: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.error("Failed to decrypt OAuth client credentials: %s", e) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/token_broker.py line 550
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Master token refresh failed: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.error("Master token refresh failed: %s", response.status_code) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/token_broker.py line 650
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Token revocation returned %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.warning("Token revocation returned %s", response.status_code) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/unified_verifier.py line 78
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Token introspection enabled: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info("Token introspection enabled: %s", self.introspection_uri) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/unified_verifier.py line 139
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "UnifiedTokenVerifier initialized (multi-audience). MCP audience: %s or %s, Nextcloud resource URI: %s, Valid issuers: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info( "UnifiedTokenVerifier initialized (multi-audience). MCP audience: %s or %s, Nextcloud resource URI: %s, Valid issuers: %s", settings.oidc_client_id, settings.nextcloud_mcp_server_url, settings.nextcloud_resource_uri, self.valid_issuers, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/vector/oauth_sync.py line 279
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "[BasicAuth] Credentials validated for %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info("[BasicAuth] Credentials validated for %s", user_id) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/browser_oauth_routes.py line 510
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Refresh token present: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug("Refresh token present: %s", refresh_token is not None) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/api/passwords.py line 238
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Failed to reach Nextcloud for credential validation: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.error("Failed to reach Nextcloud for credential validation: %s", e) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/app.py line 848
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Failed to extract BasicAuth credentials: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.warning("Failed to extract BasicAuth credentials: %s", e) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/app.py line 926
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Requesting token type: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info("Requesting token type: %s", token_type) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/app.py line 1216
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Failed to initialize refresh token storage: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.error("Failed to initialize refresh token storage: %s", e) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/app.py line 1486
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Failed to initialize refresh token storage: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.error("Failed to initialize refresh token storage: %s", e) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/astrolabe_client.py line 63
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Discovering token endpoint from %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug("Discovering token endpoint from %s", discovery_url) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/astrolabe_client.py line 68
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Requesting client credentials token from %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug("Requesting client credentials token from %s", token_endpoint) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/astrolabe_client.py line 90
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Obtained Astrolabe API token (expires in %ss)" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info("Obtained Astrolabe API token (expires in %ss)", expires_in) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/browser_oauth_routes.py line 509
- Likely real
- Yes, 100% confidence
Detected a python logger call with a potential hardcoded secret "Token exchange response keys: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug("Token exchange response keys: %s", token_data.keys()) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /infra/terraform/nextcloud-mcp-server/alb.tf line 55
- Likely real
- Yes, 100% confidence
ELB has no logging. Missing logs can cause missing important event information.
resource "aws_lb" "this" { name = var.name load_balancer_type = "application" internal = false subnets = var.public_subnet_ids security_groups = [aws_security_group.alb.id] drop_invalid_header_fields = true } -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /infra/terraform/nextcloud-mcp-server/ecs.tf line 10
- Likely real
- Yes, 99% confidence
By default, AWS CloudWatch Log Group is encrypted using AWS-managed keys. However, for added security, it's recommended to configure your own AWS KMS encryption key to protect your log group in CloudWatch. You can either create a new aws_kms_key resource or use the ARN of an existing key in your AWS account to do so.
resource "aws_cloudwatch_log_group" "this" { name = "/ecs/${var.name}" retention_in_days = var.log_retention_days } -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /infra/terraform/nextcloud-mcp-server/qdrant.tf line 6
- Likely real
- Yes, 99% confidence
By default, AWS CloudWatch Log Group is encrypted using AWS-managed keys. However, for added security, it's recommended to configure your own AWS KMS encryption key to protect your log group in CloudWatch. You can either create a new aws_kms_key resource or use the ARN of an existing key in your AWS account to do so.
resource "aws_cloudwatch_log_group" "qdrant" { count = var.use_external_qdrant ? 0 : 1 name = "/ecs/${local.qdrant_name}" retention_in_days = var.log_retention_days } -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/scope_authorization.py line 257
- Likely real
- Yes, 99% confidence
Detected a python logger call with a potential hardcoded secret "Stored app password scope check passed for %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug( "Stored app password scope check passed for %s", func_name ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/server/oauth_tools.py line 135
- Likely real
- Yes, 99% confidence
Detected a python logger call with a potential hardcoded secret " App password check failed for %s: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug(" App password check failed for %s: %s", user_id, e) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/browser_oauth_routes.py line 330
- Likely real
- Yes, 99% confidence
Detected user input flowing into a manually constructed HTML string. You may be accidentally bypassing secure methods of rendering HTML by manually constructing HTML and this could create a cross-site scripting vulnerability, which could let attackers steal sensitive user data. To be sure this is safe, check that the HTML is rendered safely. Otherwise, use templates (`django.shortcuts.render`) which will safely render HTML instead.
f""" <!DOCTYPE html> <html> <head><title>Login Failed</title></head> <body> <h1>Login Failed</h1> <p>Error: {html_escape(error)}</p> <p>{html_escape(error_description)}</p> <p><a href="{html_escape(login_url)}">Try again</a></p> </body> </html> """, -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /infra/terraform/nextcloud-mcp-server/efs.tf line 29
- Likely real
- Yes, 99% confidence
Ensure EFS filesystem is encrypted at rest using KMS CMKs. CMKs gives you control over the encryption key in terms of access and rotation.
resource "aws_efs_file_system" "this" { encrypted = true performance_mode = "generalPurpose" throughput_mode = "bursting" lifecycle_policy { transition_to_ia = "AFTER_30_DAYS" } tags = { Name = var.name } } -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/browser_oauth_routes.py line 330
- Likely real
- Yes, 99% confidence
Untrusted input could be used to tamper with a web page rendering, which can lead to a Cross-site scripting (XSS) vulnerability. XSS vulnerabilities occur when untrusted input executes malicious JavaScript code, leading to issues such as account compromise and sensitive information leakage. When returning a String in FastAPI views, the default content type is 'text/plain'. This means that any injected HTML or JavaScript will be rendered as text and not as HTML. So this can not lead to XSS and is secure by default. Turning this behavior off and setting the content type to 'text/html' by setting the `response_class` to `HTMLResponse` or wrapping the string in `HTMLResponse` is not recommended. If you need to return HTML, use templates such as Jinja2 or other template engines.
f""" <!DOCTYPE html> <html> <head><title>Login Failed</title></head> <body> <h1>Login Failed</h1> <p>Error: {html_escape(error)}</p> <p>{html_escape(error_description)}</p> <p><a href="{html_escape(login_url)}">Try again</a></p> </body> </html> """, -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/provision_routes.py line 231
- Likely real
- Yes, 99% confidence
The application builds a URL using user-controlled input which can lead to an open redirect vulnerability. An attacker can manipulate the URL and redirect users to an arbitrary domain. Open redirect vulnerabilities can lead to issues such as Cross-site scripting (XSS) or redirecting to a malicious domain for activities such as phishing to capture users' credentials. To prevent this vulnerability perform strict input validation of the domain against an allowlist of approved domains. Notify a user in your application that they are leaving the website. Display a domain where they are redirected to the user. A user can then either accept or deny the redirect to an untrusted site.
return RedirectResponse(redirect_uri) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/token_broker.py line 67
- Likely real
- Yes, 99% confidence
Detected a python logger call with a potential hardcoded secret "Using cached token for user %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.debug("Using cached token for user %s", user_id) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 383
- Likely real
- Yes, 99% confidence
Detected a python logger call with a potential hardcoded secret "Using ephemeral token storage at %s " "(set DATABASE_URL or TOKEN_STORAGE_DB to persist tokens across restarts)" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info( "Using ephemeral token storage at %s " "(set DATABASE_URL or TOKEN_STORAGE_DB to persist tokens across restarts)", sqlite_path, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 389
- Likely real
- Yes, 99% confidence
Detected a python logger call with a potential hardcoded secret "Using centralized token storage at %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info( "Using centralized token storage at %s", mask_db_password(database_url) ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/userinfo_routes.py line 568
- Likely real
- Yes, 98% confidence
Detected user input flowing into a manually constructed HTML string. You may be accidentally bypassing secure methods of rendering HTML by manually constructing HTML and this could create a cross-site scripting vulnerability, which could let attackers steal sensitive user data. To be sure this is safe, check that the HTML is rendered safely. Otherwise, use templates (`django.shortcuts.render`) which will safely render HTML instead.
user_info_tab_html = f""" <h2>Authentication</h2> <table> <tr> <td><strong>Username</strong></td> <td>{username_html}</td> </tr> <tr> <td><strong>Authentication Mode</strong></td> <td><span class="badge badge-{auth_mode_html}">{auth_mode_html}</span></td> </tr> </table> {host_info_html} {session_info_html} {idp_profile_html} """ -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/browser_oauth_routes.py line 654
- Likely real
- Yes, 98% confidence
The application builds a URL using user-controlled input which can lead to an open redirect vulnerability. An attacker can manipulate the URL and redirect users to an arbitrary domain. Open redirect vulnerabilities can lead to issues such as Cross-site scripting (XSS) or redirecting to a malicious domain for activities such as phishing to capture users' credentials. To prevent this vulnerability perform strict input validation of the domain against an allowlist of approved domains. Notify a user in your application that they are leaving the website. Display a domain where they are redirected to the user. A user can then either accept or deny the redirect to an untrusted site.
response = RedirectResponse(next_url, status_code=302) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/browser_oauth_routes.py line 741
- Likely real
- Yes, 98% confidence
The application builds a URL using user-controlled input which can lead to an open redirect vulnerability. An attacker can manipulate the URL and redirect users to an arbitrary domain. Open redirect vulnerabilities can lead to issues such as Cross-site scripting (XSS) or redirecting to a malicious domain for activities such as phishing to capture users' credentials. To prevent this vulnerability perform strict input validation of the domain against an allowlist of approved domains. Notify a user in your application that they are leaving the website. Display a domain where they are redirected to the user. A user can then either accept or deny the redirect to an untrusted site.
response = RedirectResponse(next_url, status_code=302) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /.github/workflows/pact.yml line 28
- Likely real
- Yes, 98% confidence
A secret is exposed in the workflow-level `env:` block, making it available to every job and step in this workflow — including any untrusted code run in pull-request workflows. Scope secrets as narrowly as possible: prefer step-level `env:` so the secret is only available where it is actually needed.
PACT_PASSWORD: ${{ secrets.PACT_PASSWORD }} -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /.github/workflows/pact.yml line 27
- Likely real
- Yes, 98% confidence
A secret is exposed in the workflow-level `env:` block, making it available to every job and step in this workflow — including any untrusted code run in pull-request workflows. Scope secrets as narrowly as possible: prefer step-level `env:` so the secret is only available where it is actually needed.
PACT_USERNAME: ${{ secrets.PACT_USERNAME }} -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /.github/workflows/pact-record-deployment.yml line 39
- Likely real
- Yes, 98% confidence
A secret is exposed in the workflow-level `env:` block, making it available to every job and step in this workflow — including any untrusted code run in pull-request workflows. Scope secrets as narrowly as possible: prefer step-level `env:` so the secret is only available where it is actually needed.
PACT_PASSWORD: ${{ secrets.PACT_PASSWORD }} -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /.github/workflows/pact-record-deployment.yml line 38
- Likely real
- Yes, 98% confidence
A secret is exposed in the workflow-level `env:` block, making it available to every job and step in this workflow — including any untrusted code run in pull-request workflows. Scope secrets as narrowly as possible: prefer step-level `env:` so the secret is only available where it is actually needed.
PACT_USERNAME: ${{ secrets.PACT_USERNAME }} -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /.github/workflows/pact-record-deployment.yml line 37
- Likely real
- Yes, 98% confidence
A secret is exposed in the workflow-level `env:` block, making it available to every job and step in this workflow — including any untrusted code run in pull-request workflows. Scope secrets as narrowly as possible: prefer step-level `env:` so the secret is only available where it is actually needed.
PACT_BROKER: ${{ secrets.PACT_BROKER }} -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /.github/workflows/pact.yml line 26
- Likely real
- Yes, 98% confidence
A secret is exposed in the workflow-level `env:` block, making it available to every job and step in this workflow — including any untrusted code run in pull-request workflows. Scope secrets as narrowly as possible: prefer step-level `env:` so the secret is only available where it is actually needed.
PACT_BROKER: ${{ secrets.PACT_BROKER }} -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/app.py line 2445
- Likely real
- Yes, 97% confidence
Detected a python logger call with a potential hardcoded secret "Running startup credential sweep for %s stored user(s)" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
logger.info( "Running startup credential sweep for %s stored user(s)", len(stored), ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /scripts/dbquery.py line 33
- Likely real
- Yes, 96% confidence
Hardcoded password is used as a default argument to 'run_query'. This could be dangerous if a real password is not supplied.
def run_query( sql: str, user: str = "root", password: str = "password", database: str = "nextcloud", vertical: bool = False, json_output: bool = False, ) -> tuple[int, str, str]: """ Execute SQL via docker compose exec. Returns: Tuple of (return_code, stdout, stderr) """ compose_dir = find_compose_dir() cmd = [ "docker", "compose", "exec", "-T", # Disable pseudo-TTY allocation "db", "mariadb", f"-u{user}", f"-p{password}", database, "-e", sql, ] if vertical: cmd.insert(-2, "-E") # Vertical output format result = subprocess.run( cmd, capture_output=True, text=True, cwd=compose_dir, ) return result.returncode, result.stdout, result.stderr -
- Engine
- GuardDog · Supply-chain attack heuristics
- Counts towards
- Malicious pattern
- File
- /nextcloud_mcp_server/alembic/env.py line 20
- Likely real
- Yes, 96% confidence
Detects access to sensitive files (credentials, configs, keys) [threat-filesystem-read]
# Configure logging logger = logging.getLogger("alembic.env") # This is the Alembic Config object, which provides -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /renovate.json line 34
- Likely real
- Yes, 95% confidence
This Renovate configuration does not set a minimum release age. Newly published packages can be malicious or unstable. Add `"minimumReleaseAge": "7 days"` within a `packageRules` entry to wait 7 days before proposing updates to newly published package versions. Set `"minimumReleaseAge": false` to set an exception for minimal release age for the package rule. Added in: v42
{ "description": "Pin docker-compose.yml Nextcloud image to 32.x", "matchPackageNames": ["docker.io/library/nextcloud"], "matchFileNames": ["docker-compose.yml"], "allowedVersions": "/^32\\./" } -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /renovate.json line 15
- Likely real
- Yes, 95% confidence
This Renovate configuration does not set a minimum release age. Newly published packages can be malicious or unstable. Add `"minimumReleaseAge": "7 days"` within a `packageRules` entry to wait 7 days before proposing updates to newly published package versions. Set `"minimumReleaseAge": false` to set an exception for minimal release age for the package rule. Added in: v42
{ "description": "pymupdf and pymupdf4llm are released in lockstep — update them together", "matchDatasources": ["pypi"], "matchPackageNames": ["pymupdf", "pymupdf4llm"], "groupName": "pymupdf" }, -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /renovate.json line 9
- Likely real
- Yes, 95% confidence
This Renovate configuration does not set a minimum release age. Newly published packages can be malicious or unstable. Add `"minimumReleaseAge": "7 days"` within a `packageRules` entry to wait 7 days before proposing updates to newly published package versions. Set `"minimumReleaseAge": false` to set an exception for minimal release age for the package rule. Added in: v42
{ "matchPackageNames": [ "pillow" ], "allowedVersions": "<12.0.0" }, -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/templates/user_info.html line 261
- Likely real
- Yes, 81% confidence
Manually-created forms in django templates should specify a csrf_token to prevent CSRF attacks.
<form method="post" action="{{ logout_url }}" class="app-navigation-entry-link" style="display:contents;"> <button type="submit" class="app-navigation-entry-link" style="background:none;border:0;padding:0;font:inherit;color:inherit;cursor:pointer;display:flex;align-items:center;width:100%;"> <span class="app-navigation-entry-icon"> <svg class="nav-icon" viewBox="0 0 24 24"> <path d="M16,17V14H9V10H16V7L21,12L16,17M14,2A2,2 0 0,1 16,4V6H14V4H5V20H14V18H16V20A2,2 0 0,1 14,22H5A2,2 0 0,1 3,20V4A2,2 0 0,1 5,2H14Z" /> </svg> </span> <span class="app-navigation-entry__name">Logout</span> </button> </form> -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Yes, 54% confidence
Unpinned dependency: 'starlette' has no version lock — supply chain risk
starlette<1.0 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Yes, 54% confidence
Unpinned dependency: 'mcp' has no version lock — supply chain risk
mcp[cli] (>=2.1,<3) -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Yes, 54% confidence
Unpinned dependency: 'icalendar' has no version lock — supply chain risk
icalendar (>=7.3.0,<7.4.0) -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Yes, 54% confidence
Unpinned dependency: 'pillow' has no version lock — supply chain risk
pillow (>=10.3.0,<12.0.0) -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Yes, 54% confidence
Unpinned dependency: 'httpx' has no version lock — supply chain risk
httpx (>=0.28.1,<0.29.0) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/app.py line 3203
- Likely real
- Yes, 52% confidence
CORS policy allows any origin (using wildcard '*'). This is insecure and should be avoided.
allow_origins=cors_origins or ["*"], -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/client/webdav.py line 2274
- Likely real
- Yes, 52% confidence
Using xml.etree.ElementTree.fromstring to parse untrusted XML data is known to be vulnerable to XML attacks. Replace xml.etree.ElementTree.fromstring with its defusedxml equivalent function or make sure defusedxml.defuse_stdlib() is called
2273 # Parse XML response 2274 root = ET.fromstring(response.content) 2275 ns = { -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/client/base.py line 239
- Likely real
- Yes, 52% confidence
Using xml.etree.ElementTree.fromstring to parse untrusted XML data is known to be vulnerable to XML attacks. Replace xml.etree.ElementTree.fromstring with its defusedxml equivalent function or make sure defusedxml.defuse_stdlib() is called
238 ) 239 root = ET.fromstring(response.content) 240 href = None -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/client/webdav.py line 2965
- Likely real
- Yes, 52% confidence
Using xml.etree.ElementTree.fromstring to parse untrusted XML data is known to be vulnerable to XML attacks. Replace xml.etree.ElementTree.fromstring with its defusedxml equivalent function or make sure defusedxml.defuse_stdlib() is called
2964 2965 root = ET.fromstring(response.content) 2966 ids: List[int] = [] -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/client/webdav.py line 2908
- Likely real
- Yes, 52% confidence
Using xml.etree.ElementTree.fromstring to parse untrusted XML data is known to be vulnerable to XML attacks. Replace xml.etree.ElementTree.fromstring with its defusedxml equivalent function or make sure defusedxml.defuse_stdlib() is called
2907 2908 return self._tags_from_multistatus(ET.fromstring(response.content)) 2909 -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/client/webdav.py line 2835
- Likely real
- Yes, 52% confidence
Using xml.etree.ElementTree.fromstring to parse untrusted XML data is known to be vulnerable to XML attacks. Replace xml.etree.ElementTree.fromstring with its defusedxml equivalent function or make sure defusedxml.defuse_stdlib() is called
2834 2835 root = ET.fromstring(response.content) 2836 versions: List[Dict[str, Any]] = [] -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/client/webdav.py line 2772
- Likely real
- Yes, 52% confidence
Using xml.etree.ElementTree.fromstring to parse untrusted XML data is known to be vulnerable to XML attacks. Replace xml.etree.ElementTree.fromstring with its defusedxml equivalent function or make sure defusedxml.defuse_stdlib() is called
2771 2772 root = ET.fromstring(response.content) 2773 items: List[Dict[str, Any]] = [] -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/client/webdav.py line 2501
- Likely real
- Yes, 52% confidence
Using xml.etree.ElementTree.fromstring to parse untrusted XML data is known to be vulnerable to XML attacks. Replace xml.etree.ElementTree.fromstring with its defusedxml equivalent function or make sure defusedxml.defuse_stdlib() is called
2500 # Parse XML response 2501 root = ET.fromstring(response.content) 2502 ns = { -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/client/webdav.py line 2362
- Likely real
- Yes, 52% confidence
Using xml.etree.ElementTree.fromstring to parse untrusted XML data is known to be vulnerable to XML attacks. Replace xml.etree.ElementTree.fromstring with its defusedxml equivalent function or make sure defusedxml.defuse_stdlib() is called
2361 # Parse XML response 2362 root = ET.fromstring(response.content) 2363 ns = { -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/client/webdav.py line 2171
- Likely real
- Yes, 52% confidence
Using xml.etree.ElementTree.fromstring to parse untrusted XML data is known to be vulnerable to XML attacks. Replace xml.etree.ElementTree.fromstring with its defusedxml equivalent function or make sure defusedxml.defuse_stdlib() is called
2170 # Parse the XML response 2171 root = ET.fromstring(response.content) 2172 responses = root.findall(".//{DAV:}response") -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/client/webdav.py line 2085
- Likely real
- Yes, 52% confidence
Using xml.etree.ElementTree.fromstring to parse untrusted XML data is known to be vulnerable to XML attacks. Replace xml.etree.ElementTree.fromstring with its defusedxml equivalent function or make sure defusedxml.defuse_stdlib() is called
2084 2085 root = ET.fromstring(response.content) 2086 comments = [] -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/client/webdav.py line 2035
- Likely real
- Yes, 52% confidence
Using xml.etree.ElementTree.fromstring to parse untrusted XML data is known to be vulnerable to XML attacks. Replace xml.etree.ElementTree.fromstring with its defusedxml equivalent function or make sure defusedxml.defuse_stdlib() is called
2034 raise 2035 root = ET.fromstring(response.content) 2036 # Match oc:fileid by local name (namespace-agnostic) rather than a -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/client/webdav.py line 1677
- Likely real
- Yes, 52% confidence
Using xml.etree.ElementTree.fromstring to parse untrusted XML data is known to be vulnerable to XML attacks. Replace xml.etree.ElementTree.fromstring with its defusedxml equivalent function or make sure defusedxml.defuse_stdlib() is called
1676 """Parse the XML response from a SEARCH request.""" 1677 root = ET.fromstring(xml_content) 1678 items = [] -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/client/webdav.py line 736
- Likely real
- Yes, 52% confidence
Using xml.etree.ElementTree.fromstring to parse untrusted XML data is known to be vulnerable to XML attacks. Replace xml.etree.ElementTree.fromstring with its defusedxml equivalent function or make sure defusedxml.defuse_stdlib() is called
735 # Parse the XML response 736 root = ET.fromstring(response.content) 737 items = [] -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/client/dav_errors.py line 114
- Likely real
- Yes, 52% confidence
Using xml.etree.ElementTree.fromstring to parse untrusted XML data is known to be vulnerable to XML attacks. Replace xml.etree.ElementTree.fromstring with its defusedxml equivalent function or make sure defusedxml.defuse_stdlib() is called
113 try: 114 root = ET.fromstring(body) 115 except ET.ParseError: -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/client/contacts.py line 673
- Likely real
- Yes, 52% confidence
Using xml.etree.ElementTree.fromstring to parse untrusted XML data is known to be vulnerable to XML attacks. Replace xml.etree.ElementTree.fromstring with its defusedxml equivalent function or make sure defusedxml.defuse_stdlib() is called
672 # logger.info(response.text) 673 root = ET.fromstring(response.content) 674 contacts = [] -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/client/contacts.py line 438
- Likely real
- Yes, 52% confidence
Using xml.etree.ElementTree.fromstring to parse untrusted XML data is known to be vulnerable to XML attacks. Replace xml.etree.ElementTree.fromstring with its defusedxml equivalent function or make sure defusedxml.defuse_stdlib() is called
437 # logger.info(response.content) 438 root = ET.fromstring(response.content) 439 addressbooks = [] -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/client/contacts.py line 378
- Likely real
- Yes, 52% confidence
Using xml.etree.ElementTree.fromstring to parse untrusted XML data is known to be vulnerable to XML attacks. Replace xml.etree.ElementTree.fromstring with its defusedxml equivalent function or make sure defusedxml.defuse_stdlib() is called
377 ns = {"d": "DAV:"} 378 root = ET.fromstring(response.content) 379 names: list[str] = [] -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/userinfo_routes.py line 36
- Likely real
- Uncertain, 49% confidence
Detected direct use of jinja2. If not done properly, this may bypass HTML escaping which opens up the application to cross-site scripting (XSS) vulnerabilities. Prefer using the Flask method 'render_template()' and templates with a '.html' extension in order to prevent XSS.
_jinja_env = Environment( loader=FileSystemLoader(_template_dir), autoescape=select_autoescape(["html", "xml"]), ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/userinfo_routes.py line 404
- Likely real
- Uncertain, 49% confidence
Detected direct use of jinja2. If not done properly, this may bypass HTML escaping which opens up the application to cross-site scripting (XSS) vulnerabilities. Prefer using the Flask method 'render_template()' and templates with a '.html' extension in order to prevent XSS.
content=template.render( error_title="Error Retrieving User Info", error_message=user_context["error"], login_url=login_url, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/userinfo_routes.py line 602
- Likely real
- Uncertain, 49% confidence
Detected direct use of jinja2. If not done properly, this may bypass HTML escaping which opens up the application to cross-site scripting (XSS) vulnerabilities. Prefer using the Flask method 'render_template()' and templates with a '.html' extension in order to prevent XSS.
content=template.render( user_info_tab_html=user_info_tab_html, vector_sync_tab_html=vector_sync_tab_html, show_vector_sync_tab=show_vector_sync_tab, logout_url=logout_url if auth_mode == "oauth" else None, # Additional context for Welcome tab vector_sync_enabled=vector_sync_enabled, username=username, auth_mode=auth_mode, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/userinfo_routes.py line 634
- Likely real
- Uncertain, 49% confidence
Detected direct use of jinja2. If not done properly, this may bypass HTML escaping which opens up the application to cross-site scripting (XSS) vulnerabilities. Prefer using the Flask method 'render_template()' and templates with a '.html' extension in order to prevent XSS.
content=template.render( error_title="Error", error_message="OAuth mode not enabled", ), -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/userinfo_routes.py line 647
- Likely real
- Uncertain, 49% confidence
Detected direct use of jinja2. If not done properly, this may bypass HTML escaping which opens up the application to cross-site scripting (XSS) vulnerabilities. Prefer using the Flask method 'render_template()' and templates with a '.html' extension in order to prevent XSS.
content=template.render( error_title="Error", error_message="Session not found", ), -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/userinfo_routes.py line 665
- Likely real
- Uncertain, 49% confidence
Detected direct use of jinja2. If not done properly, this may bypass HTML escaping which opens up the application to cross-site scripting (XSS) vulnerabilities. Prefer using the Flask method 'render_template()' and templates with a '.html' extension in order to prevent XSS.
content=template.render( success_title="✓ Background Access Revoked", success_messages=[ "Your refresh token has been deleted successfully.", "Browser session remains active.", ], redirect_url=user_page_url, redirect_delay=2, ) -
- Engine
- Semgrep · Pattern-based code analysis
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/userinfo_routes.py line 680
- Likely real
- Uncertain, 49% confidence
Detected direct use of jinja2. If not done properly, this may bypass HTML escaping which opens up the application to cross-site scripting (XSS) vulnerabilities. Prefer using the Flask method 'render_template()' and templates with a '.html' extension in order to prevent XSS.
content=template.render( error_title="Error", error_message=f"Failed to revoke background access: {e}", ), -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/providers/simple.py line 64
- Likely real
- Uncertain, 49% confidence
Use of weak MD5 hash for security. Consider usedforsecurity=False
63 """ 64 hash_bytes = hashlib.md5(word.encode()).digest() 65 hash_int = int.from_bytes(hash_bytes[:4], byteorder="big") -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 1655
- Likely real
- Uncertain, 46% confidence
Possible SQL injection vector through string-based query construction.
1654 cursor = await db.execute( 1655 f""" 1656 UPDATE oauth_sessions 1657 SET {", ".join(update_fields)} 1658 WHERE session_id = ? 1659 """, # NOSONAR 1660 params, -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/vector/document_path_store.py line 116
- Likely real
- Uncertain, 46% confidence
Possible SQL injection vector through string-based query construction.
115 sql = ( 116 "SELECT doc_id, file_path FROM document_paths " 117 f"WHERE user_id = ? AND doc_type = ? AND doc_id IN ({placeholders})" 118 ) -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/client/webdav.py line 10
- Likely real
- Yes, 97% confidence
Using escape to parse untrusted XML data is known to be vulnerable to XML attacks. Replace escape with the equivalent defusedxml package, or make sure defusedxml.defuse_stdlib() is called.
9 from urllib.parse import quote, unquote 10 from xml.sax.saxutils import escape as xml_escape 11 -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/token_broker.py line 639
- Likely real
- Yes, 95% confidence
Possible hardcoded password: 'refresh_token'
638 639 data = {"token": token, "token_type_hint": "refresh_token"} 640 -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/browser_oauth_routes.py line 800
- Likely real
- Yes, 95% confidence
Possible hardcoded password: 'refresh_token'
799 "token": refresh_token, 800 "token_type_hint": "refresh_token", 801 }, 802 auth=(client_id, client_secret), 803 ) 804 if response.status_code >= 400: -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/app.py line 924
- Likely real
- Yes, 95% confidence
Possible hardcoded password: 'jwt'
923 # Special case: "bearer" should remain capitalized for compatibility 924 if token_type != "jwt": 925 token_type = "Bearer" -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/redaction.py line 152
- Likely real
- Yes, 95% confidence
Possible hardcoded password: '[\s_.\-]+'
151 # local-parts use ("KAREN_SMITH.pdf", "karen.smith@"). 152 _TOKEN_SEPARATOR = r"[\s_.\-]+" 153 # "Not preceded/followed by a letter or digit". Deliberately not \b: an -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/observability/metrics.py line 1473
- Likely real
- Yes, 95% confidence
Possible hardcoded password: 'unknown'
1472 1473 def record_oauth_grant( 1474 grant_type: str, result: str, refresh_token: str = "unknown" 1475 ) -> None: 1476 """Record a grant processed by the AS proxy token endpoint. 1477 1478 The label vocabulary is enumerated once, on the ``oauth_grants_total`` 1479 definition above; this docstring does not repeat it. 1480 1481 ``client_id`` is deliberately NOT a label here. It would be the natural 1482 fourth dimension, but the grant counters are already multiplied by 1483 grant_type x result x refresh_token, and the per-client attribution is 1484 available on ``oauth_token_validations_total``. The log line emitted 1485 alongside each of these carries the client_id when it is needed. 1486 1487 Args: 1488 grant_type: Which grant the client asked for. 1489 result: Whether the AS proxy could satisfy it. 1490 refresh_token: Whether the resulting token response carried a refresh 1491 token. "unknown" when the grant failed and there was no response 1492 to inspect. 1493 """ 1494 oauth_grants_total.labels( 1495 grant_type=grant_type, result=result, refresh_token=refresh_token 1496 ).inc() 1497 -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/oauth_routes.py line 1643
- Likely real
- Yes, 95% confidence
Possible hardcoded password: 'none'
1642 "redirect_uris": requested_redirect_uris, 1643 "token_endpoint_auth_method": "none", 1644 "grant_types": ["authorization_code", "refresh_token"], 1645 "response_types": ["code"], 1646 "client_id_issued_at": static_match.issued_at, 1647 }, 1648 status_code=201, 1649 ) 1650 1651 # Discover registration endpoint from OIDC discovery 1652 discovery_url = oauth_config.get("discovery_url") -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/client_registration.py line 122
- Likely real
- Yes, 95% confidence
Possible hardcoded password: 'client_secret_post'
121 "redirect_uris": redirect_uris, 122 "token_endpoint_auth_method": "client_secret_post", 123 "grant_types": ["authorization_code", "refresh_token"], 124 "response_types": ["code"], 125 "scope": scopes, 126 } 127 128 # Add token_type if provided (Nextcloud-specific, not RFC 7591 standard) 129 if token_type is not None: 130 client_metadata["token_type"] = token_type -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/oauth_routes.py line 902
- Likely real
- Yes, 95% confidence
Possible hardcoded password: 'nextcloud'
901 flow_type="flow2", 902 token_audience="nextcloud", 903 provisioning_client_id=state, # Store which client initiated provisioning 904 scopes=granted_scopes, 905 expires_at=refresh_expires_at, 906 ) 907 logger.debug("✓ Stored Flow 2 master refresh token for user %s", user_id) 908 logger.debug("=" * 60) 909 910 # Return success HTML page 911 success_html = """ -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/config.py line 2149
- Likely real
- Yes, 95% confidence
Possible hardcoded password: 'NEXTCLOUD_OIDC_CLIENT_SECRET'
2148 "oidc_client_id": "NEXTCLOUD_OIDC_CLIENT_ID", 2149 "oidc_client_secret": "NEXTCLOUD_OIDC_CLIENT_SECRET", 2150 "oidc_token_type": "NEXTCLOUD_OIDC_TOKEN_TYPE", 2151 "oidc_scopes": "NEXTCLOUD_OIDC_SCOPES", 2152 } 2153 2154 # Derived fields with no env var of their own, so there is nothing to map: 2155 # ``vector_sync_enabled``/``enable_offline_access`` come from the 2156 # ENABLE_SEMANTIC_SEARCH / ENABLE_BACKGROUND_OPERATIONS resolution, and -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/config.py line 2150
- Likely real
- Yes, 94% confidence
Possible hardcoded password: 'NEXTCLOUD_OIDC_TOKEN_TYPE'
2149 "oidc_client_secret": "NEXTCLOUD_OIDC_CLIENT_SECRET", 2150 "oidc_token_type": "NEXTCLOUD_OIDC_TOKEN_TYPE", 2151 "oidc_scopes": "NEXTCLOUD_OIDC_SCOPES", 2152 } 2153 2154 # Derived fields with no env var of their own, so there is nothing to map: 2155 # ``vector_sync_enabled``/``enable_offline_access`` come from the 2156 # ENABLE_SEMANTIC_SEARCH / ENABLE_BACKGROUND_OPERATIONS resolution, and 2157 # ``enable_login_flow``/``enable_multi_user_basic_auth`` from the deployment -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/app.py line 925
- Likely real
- Yes, 93% confidence
Possible hardcoded password: 'Bearer'
924 if token_type != "jwt": 925 token_type = "Bearer" 926 logger.info("Requesting token type: %s", token_type) -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/config.py line 67
- Likely real
- Yes, 93% confidence
Possible hardcoded password: 'Bearer'
66 # NEXTCLOUD_OIDC_TOKEN_TYPE / NEXTCLOUD_OIDC_SCOPES, matching _ENV_OVERRIDE. 67 "nextcloud_oidc_token_type": "Bearer", 68 "nextcloud_oidc_scopes": "", 69 "port": 8000, 70 "nextcloud_oidc_client_id": None, 71 "nextcloud_oidc_client_secret": None, 72 "oidc_issuer": None, 73 "jwks_uri": None, 74 "introspection_uri": None, 75 "userinfo_uri": None, 76 "oidc_resource_server_id": None, 77 # M2M / DCR / management — these MUST be declared here (lowercase) so dynaconf 78 # reads the matching UPPERCASE env var; with ignore_unknown_envvars=True an 79 # undeclared key is silently dropped (env value ignored), so they are read 80 # via cfg("ENV_NAME"). Defaults mirror the prior os.getenv(..., default). 81 "mcp_server_client_id": None, 82 "mcp_server_client_secret": None, 83 "allowed_mcp_clients": "", 84 "allowed_mgmt_client": "", 85 # Hosts whose Client ID Metadata Documents the AS proxy trusts (GH #1470). 86 # Empty = CIMD disabled; "*" = any publicly routable host. See auth/cimd.py. 87 "cimd_allowed_hosts": "", 88 "enable_dcr": False, 89 # Container-runtime / webhook self-URL overrides (local-dev docker-compose). 90 "docker_container": False, 91 "nextcloud_mcp_service_name": "mcp", 92 "nextcloud_mcp_port": 8000, 93 # Mode flags 94 # NOTE: `enable_multi_user_basic_auth` and `enable_login_flow` are 95 # intentionally absent — they are derived from MCP_DEPLOYMENT_MODE in 96 # Settings.__post_init__ (ADR-022) and not read from the dynaconf store. 97 # Escape hatch for the per-tool capability gate (capabilities.py). Set 98 # MCP_DISABLE_CAPABILITY_GATING=true to list and run every registered tool 99 # regardless of what the instance advertises — the answer to "why did my 100 # deck tools disappear?" without a code read. Read via cfg(), not a 101 # Settings field: nothing but the two gate helpers looks at it. 102 "mcp_disable_capability_gating": False, 103 "enable_semantic_search": False, 104 "enable_background_operations": False, 105 "vector_sync_enabled": False, 106 "enable_offline_access": False, 107 # Token storage 108 "token_encryption_key": None, 109 # None = ephemeral per-process tempfile (see get_token_db_path()). 110 # Set TOKEN_STORAGE_DB to persist tokens across restarts. 111 "token_storage_db": None, 112 # Centralized backend (any SQLAlchemy URL). Wins over TOKEN_STORAGE_DB 113 # when set. Use postgresql+psycopg://user:pw@host/db for HA k8s 114 # deployments so pods can be stateless. The URL is passed through 115 # verbatim — TLS (e.g. ?sslmode=require) and every other parameter are 116 # read from it by libpq/psycopg; the server never rewrites it. See ADR-026. 117 "database_url": None, 118 # Postgres connection pool sizing (ADR-026 → "Concurrency model and 119 # pool sizing"). Per-pod defaults to 2 + 5 overflow = 7 max 120 # connections. psycopg connections are single-flight, so the pool 121 # only needs to cover typical multi-user MCP burst — not every 122 # potential in-flight tool call. Tune up with DATABASE_POOL_SIZE / 123 # DATABASE_MAX_OVERFLOW for high-traffic prod fleets. 124 "database_pool_size": 2, 125 "database_max_overflow": 5, 126 # Webhook delivery authentication (ADR-010): when set, registrations 127 # tell NC to add `Authorization: Bearer <secret>` to webhook deliveries 128 # and the receiver rejects unauthenticated requests. 129 "webhook_secret": None, 130 # Internal URL override for webhook registration; wins over 131 # NEXTCLOUD_MCP_SERVER_URL when set (e.g. split internal/external URLs). 132 "webhook_internal_url": None, 133 # Vector sync 134 "vector_sync_scan_interval": 300, 135 "vector_sync_processor_workers": 3, 136 # Consecutive failed index attempts (embed / Qdrant / transport — NOT a 137 # parse failure, which is terminal on the first try) before a document is 138 # dead-lettered instead of re-queued forever. See vector/dead_letter.py. 139 "vector_sync_max_index_failures": 5, 140 # Optional per-tier concurrency overrides (None = fall back to 141 # vector_sync_processor_workers). Consumed by the `worker` CLI per --tier. 142 "vector_sync_fast_concurrency": None, 143 "vector_sync_structured_concurrency": None, 144 "vector_sync_queue_max_size": 10000, 145 "vector_sync_metrics_refresh_interval": 20, 146 "vector_density_snapshot_enabled": True, 147 "vector_density_snapshot_interval": 300, 148 "vector_density_snapshot_max_documents": 50000, 149 "usage_stock_snapshot_interval": 86400, 150 "vector_ram_hnsw_overhead_factor": 1.5, 151 "vector_sync_user_poll_interval": 60, 152 "health_ready_refresh_interval": 15, 153 # Orphan-sweep at Pod startup (card #101). When True, delete any 154 # placeholders carrying a different / absent ``instance_id`` before 155 # the scanner's first cycle, so a Pod restart mid-batch doesn't 156 # leave work stuck behind the 5x-scan-interval staleness gate. 157 # Escape hatch only — leave on by default. 158 "vector_sync_orphan_sweep_enabled": True, 159 # System tag that marks files for hybrid (dense + BM25 sparse) indexing. 160 # The scanner indexes files carrying this tag; verify-on-read gates results 161 # on current membership of this tag (ADR-019). 162 "vector_sync_tag": "vector-index", 163 # System tag that marks files for keyword-only (BM25 sparse) indexing. 164 # Defaults to ``keyword-index`` (symmetric with ``vector_sync_tag``), so 165 # a user who creates + applies that tag gets keyword-only indexing out of the 166 # box. Files carrying it are indexed sparse-only (no dense embedding, no 167 # embedding cost) into the SAME collection as hybrid files; ``vector-index`` 168 # wins if a file carries both. Set empty to disable the second tag entirely. 169 "vector_sync_keyword_tag": "keyword-index", 170 # Which MIME types tagged-file discovery will enqueue. None (unset) means 171 # every type a registered processor can read; a comma-separated list 172 # narrows it, and an empty string indexes no files. 173 "vector_sync_indexable_mime_types": None, 174 # Mail tag (an IMAP keyword) restricting which messages are indexed. Empty 175 # (the default) indexes every message in every mailbox, which is the 176 # behaviour before this setting existed. Set it to a tag display name and 177 # only messages carrying that tag are indexed — the mail analogue of 178 # ``vector_sync_tag`` for files. 179 "mail_index_tag": "", 180 # Fail-safe against a flaky/empty tag-discovery read: number of *consecutive* 181 # scan cycles for which a given index mode's tag discovery must return zero 182 # files (while Qdrant still holds indexed points for that mode) before the 183 # scanner believes it and starts deleting. A transient empty read (e.g. a 184 # customer-hosted Nextcloud that intermittently answers the systemtag REPORT 185 # with an empty 207) is thereby treated as a failed read, not "all files 186 # gone" — preventing a delete/re-index churn loop. A sustained empty (a real 187 # mass-untag) still eventually deletes once the streak reaches this value. 188 "vector_sync_empty_discovery_delete_threshold": 3, 189 # Verify-on-read concurrency cap (ADR-019) 190 "verification_concurrency": 20, 191 # Qdrant 192 "qdrant_url": None, 193 "qdrant_location": None, 194 "qdrant_api_key": None, 195 "qdrant_collection": "nextcloud_content", 196 # Ollama 197 "ollama_base_url": None, 198 "ollama_embedding_model": "nomic-embed-text", 199 "ollama_verify_ssl": True, 200 # Ollama embeds a batch serially on whatever hardware it has, so the wall 201 # clock of one /api/embed call scales with the batch's total text, not its 202 # item count. Bound the request by characters (GH #1345). 203 "ollama_embed_max_batch_chars": 16000, 204 "ollama_embed_timeout": 120, 205 # OpenAI 206 "openai_api_key": None, 207 "openai_base_url": None, 208 "openai_embedding_model": "text-embedding-3-small", 209 # Bedrock (AWS) 210 "aws_region": None, 211 "aws_access_key_id": None, 212 "aws_secret_access_key": None, 213 "bedrock_embedding_model": None, 214 # Mistral 215 "mistral_api_key": None, 216 "mistral_embedding_model": "mistral-embed", 217 "mistral_base_url": None, 218 # Matryoshka output width requested from the embedding endpoint (None = 219 # the model's full width). Provider-agnostic: only one provider is active 220 # at a time, so a single knob covers openai/ollama/gateway. 221 "embedding_dimensions": None, 222 # Simple (fallback) embedding dimension 223 "simple_embedding_dimension": 384, 224 # Document chunking 225 "document_chunk_size": 2048, 226 "document_chunk_overlap": 200, 227 # Page-aware chunking for paginated docs (PDFs): split on page boundaries 228 # first so no chunk spans a page (exact page_number, clean snippets, and 229 # predictable ~1 chunk/page when chunk_size >= the largest page). 230 "document_chunk_page_aware": True, 231 # Greedy page-packing (density fix, Deck #636): merge consecutive sub-budget 232 # pages into one chunk instead of one-per-page. Off by default until the 233 # post-change density re-measure + pricing re-calibration land. 234 "document_chunk_page_pack": False, 235 # Hybrid-search RRF ranking constant. Qdrant's native RRF hardcodes k=2, 236 # where adjacent ranks differ by 33% and a single-list outlier outranks a 237 # document both retrievers agree on. 60 is the standard value (~2% per rank), 238 # which restores consensus as the dominant signal. 239 "vector_search_rrf_k": 60, 240 # --- Cross-encoder reranking (opt-in, per request) ---------------------- 241 # A cross-encoder reorders the retrieved candidates by scoring each against 242 # the query directly, which is more accurate than the fusion rank it 243 # replaces. Ships OFF because it adds an upstream round-trip to every 244 # search, and how expensive that is depends on how the reranker is deployed 245 # — which this server deliberately knows nothing about beyond the URL. 246 "search_rerank_enabled": False, 247 # Where reranking is served. A FULL endpoint URL, because the path differs 248 # per backend and there is no safe guess: Infinity serves ``POST /rerank``, 249 # vLLM ``POST /v1/rerank``, Cohere ``POST /v2/rerank``, the Astrolabe 250 # embedding gateway ``POST /v1/rerank``. All four speak the same Cohere 251 # request/response protocol, so choosing one is configuration, not code. 252 # 253 # Unset = derive it from EMBEDDING_GATEWAY_URL, which is what every existing 254 # deployment does. Set it to run reranking WITHOUT a gateway — the reason 255 # this exists (discussion #1354): Ollama serves no rerank models at all, so 256 # a self-hoster's only options are a hosted rerank API or a local Infinity / 257 # vLLM, and neither should require standing up a gateway first. 258 "search_rerank_url": None, 259 # Static bearer for SEARCH_RERANK_URL (a Cohere API key, an Infinity 260 # ``--api-key``). Wins over the gateway's M2M OIDC credentials when both are 261 # present. Unset = send no Authorization header, which is correct for a 262 # local Infinity/vLLM on a private network. 263 "search_rerank_api_key": None, 264 # Rerank model, addressed the way the CONFIGURED ENDPOINT expects — which 265 # differs between the two, so the default is only right for one of them. 266 # 267 # Against the embedding gateway the ``<provider>/`` prefix is REQUIRED, not 268 # stylistic: the gateway splits on the FIRST slash to select a backend, so a 269 # bare ``BAAI/bge-reranker-v2-m3`` asks for a provider named ``BAAI`` and 270 # 503s — and because a failed rerank degrades to retrieval order 271 # (``reranked: false``) rather than erroring, that looks like "reranking does 272 # nothing" instead of like a misconfiguration. The prefix is also the backend 273 # selector: ``local/`` for a self-hosted cross-encoder, ``bedrock/`` or 274 # ``openrouter/`` for a hosted one. 275 # 276 # Against a direct SEARCH_RERANK_URL there is no such routing layer, so use 277 # the bare id the server serves (``BAAI/bge-reranker-v2-m3`` for 278 # Infinity/vLLM, ``rerank-v3.5`` for Cohere). The relevance calibration 279 # (ADR-034) strips known provider prefixes, so both spellings resolve to the 280 # same fitted curve. 281 "search_rerank_model": "local/BAAI/bge-reranker-v2-m3", 282 # Candidates handed to the reranker. Reranking can only reorder what 283 # retrieval supplied, so this depth — not the caller's ``limit`` — bounds 284 # how much a reranker can improve. Reranking just the rows a normal search 285 # returns captures little of the available gain. 286 # 287 # Treat this as a ceiling rather than a starting point. Under 288 # ``granularity="document"`` the grouped prefetch is bounded by 289 # MAX_DOCUMENT_PREFETCH, and requesting more groups than the prefetch can 290 # fill makes Qdrant widen its grouping search and reorder the head of the 291 # result set before the reranker sees it. Going deeper requires raising 292 # MAX_DOCUMENT_PREFETCH first, which carries its own latency cost. 293 "search_rerank_pool_size": 200, 294 # Generous headroom over a normal rerank, not a target. Exceeding it 295 # degrades to retrieval order rather than failing the search. 296 "search_rerank_timeout_seconds": 30.0, 297 # Concurrent rerank calls this process keeps in flight against the reranker. 298 # 299 # Conservative at 1 because reranking is the heaviest request this server 300 # makes of a service it may share with its own embedding traffic and with 301 # other callers. Bounding our own concurrency keeps a burst of searches from 302 # queueing unbounded work upstream, and keeps rerank latency predictable 303 # here. It is a client-side courtesy, not a throughput control: what the 304 # reranker does with the request, and how it schedules against everything 305 # else it serves, is its business. Raise it if yours has headroom — a CPU 306 # cross-encoder almost certainly does not. 307 "search_rerank_max_concurrency": 1, 308 # --- SAR export redaction (ADR-040) ------------------------------------- 309 # Names are detected by the embedding gateway's ``POST /v1/ner``, so SAR 310 # export is available only with EMBEDDING_GATEWAY_URL set. 311 # NER model, addressed the gateway way (``<provider>/<model>``). 312 # Subject access request cases and redacted export (ADR-040). Off by 313 # default: a deployment opts in, since only some need it. Also requires 314 # vector sync and EMBEDDING_GATEWAY_URL (for name detection). 315 "sar_enabled": False, 316 "ner_model": "local/urchade/gliner_multi_pii-v1", 317 # Per-request budget. Export runs in the background, so this only needs to 318 # cover one batch on the slowest backend (CPU GLiNER: ~570 chars/s). 319 "ner_timeout_seconds": 120.0, 320 # Texts (of up to 2,000 chars) per /v1/ner request. Small for CPU GLiNER, 321 # which must finish a batch inside the gateway's own upstream timeout; raise 322 # it (e.g. 32) on a GPU backend. 323 "ner_batch_size": 8, 324 # Minimum model confidence for a span to count as a person. Lower raises 325 # recall at the cost of over-redaction, which is the safe direction for a 326 # disclosure; 0.5 is GLiNER's customary operating point. 327 "ner_threshold": 0.5, 328 # Chunking config generation. Bump whenever chunker behaviour changes (size, 329 # overlap, page-aware, page-pack, split strategy) so the pricing model's 330 # density reference can't silently go stale. Pinned in stripe-catalog.tf. 331 "chunking_config_version": 1, 332 # PDF parse isolation (OOM guard) 333 "document_pdf_graphics_limit": 1000, 334 "document_parse_timeout_seconds": 120.0, 335 # Optional wall-clock cap (seconds) on the SYNCHRONOUS parse inside the 336 # nc_webdav_read_file MCP tool. None (default) = disabled: an interactive read 337 # is bounded only by the underlying processor timeout (DOCLING_TIMEOUT / 338 # DOCUMENT_OCR_TIMEOUT_SECONDS). Set a client-friendly bound (e.g. 45-60s) so a 339 # slow VLM/OCR convert returns base64 quickly instead of blocking past an MCP 340 # client's own timeout. Never applies to the async ingest/worker path (ADR-032). 341 "document_read_timeout_seconds": None, 342 "document_parse_mem_limit_mb": 1536, 343 # Pre-parse size cap (MB): PDFs larger than this fail fast with reason 344 # "oversize" instead of burning the OCR timeout to 0 chars on a pathological 345 # file. 0 disables the guard. 346 "document_max_pdf_size_mb": 50.0, 347 # Pre-flight cap (MB) on nc_webdav_write_file's content: a huge PUT built 348 # from a single in-memory MCP tool argument (no chunked/streaming upload 349 # exists for writes, unlike the read-side stream_to_file path) risks 350 # timing out or exhausting memory rather than failing predictably. 0 351 # disables the guard. 352 "webdav_write_max_mb": 50.0, 353 # Transport security (MCP SDK TransportSecuritySettings). Default False 354 # preserves the behaviour hardcoded before these knobs existed: MCP 1.23+ 355 # auto-enables localhost-only host checking, which breaks k8s/Docker service 356 # DNS names (docs/MCP-1.23-DNS-REBINDING-FIX.md). Enable it plus 357 # MCP_ALLOWED_HOSTS when the server is reachable from a browser. 358 "mcp_dns_rebinding_protection": False, 359 # Comma-separated Host / Origin allowlists, honoured only when the above is 360 # enabled. Empty means "the SDK's own defaults". 361 "mcp_allowed_hosts": "", 362 "mcp_allowed_origins": "", 363 # Comma-separated CORS origin allowlist. "*" preserves today's behaviour. 364 "cors_allow_origins": "*", 365 # Page ceiling for markdown reconstruction (see document_markdown_max_pages). 366 "document_markdown_max_pages": 150, 367 # Pages per pypdfium2 extraction window (see document_parse_page_window). 368 "document_parse_page_window": 100, 369 # Concurrent isolated parse subprocesses (see document_parse_process_slots). 370 "document_parse_process_slots": 2, 371 # Stream ingest downloads to disk instead of buffering them in memory. 372 "document_stream_download_enabled": True, 373 # Directory for ingest spool files (default: the system temp dir). 374 "document_spool_dir": None, 375 # Tier-0 classifier (records classification metrics on the tiered path) 376 "document_classify_enabled": True, 377 # Tiered PDF pipeline: pypdfium2 is the default/only hot-path extractor; 378 # "pymupdf" is a deprecated rollback escape hatch. OCR (tier-3) is the only 379 # escalation target and is off by default (no provider wired yet). 380 "document_tier1_engine": "pypdfium2", 381 "document_ocr_enabled": False, 382 # OCR backend: "auto" picks gateway (if EMBEDDING_GATEWAY_URL) else mistral 383 # (if MISTRAL_API_KEY); "gateway"/"mistral" force one; "docling" routes scanned 384 # PDFs to a docling-serve instance (DOCLING_API_URL); "none" disables. "auto" 385 # never selects docling (it needs a self-hosted URL, so it must be explicit). 386 # The gateway routes on the model's "<provider>/" prefix, so it serves Mistral, 387 # surya (in-cluster GPU over the tailnet), etc. — one configurable OCR tier. 388 "document_ocr_provider": "auto", 389 # Provider-namespaced OCR model id (gateway routes on the prefix; the direct 390 # mistral backend strips it). e.g. "mistral/mistral-ocr-latest" (Mistral) or 391 # "surya/surya-ocr-2" (surya via the gateway) — never hard-coded, only this 392 # default. 393 "document_ocr_model": "mistral/mistral-ocr-latest", 394 # OCR escalation triggers (tier-0). A page is OCR-worthy when its text is 395 # near-empty (< min_page_chars) OR low-quality (< min_text_quality) OR (when 396 # detect_scanned) mostly a raster image; a doc escalates when the OCR-worthy 397 # page fraction reaches page_fraction. Calibrate min_text_quality from the 398 # astrolabe_document_text_quality histogram per tenant. 399 "document_ocr_min_text_quality": 0.5, 400 "document_ocr_page_fraction": 0.5, 401 "document_ocr_min_page_chars": 16, 402 "document_ocr_detect_scanned": True, 403 # Tier-0 glyph-corruption trigger. When the fast (pypdfium2) extraction's 404 # doc-level C0-control-char ratio exceeds this, the text layer is treated as 405 # glyph-corrupt (a broken /ToUnicode mapping leaking raw glyph codes) and the 406 # doc escalates fast->structured (pymupdf re-extracts it correctly -- no OCR). 407 # 0 disables. Clean docs sit ~0; affected PDFs measured ~1-11% in testing. 408 "document_glyph_corruption_ratio": 0.02, 409 # OCR backend request timeout (seconds). Slow scanned newspapers can take 410 # 20-60s; raise/lower per tenant. Configurable so a tenant isn't stuck with 411 # the 180s default when its gateway has its own shorter ceiling. 412 "document_ocr_timeout_seconds": 180.0, 413 # OCR execution mode (Deck #332). "sync" (default) transcribes inline via the 414 # backend's synchronous path. "batch" routes to the gateway's async Batch OCR 415 # job (~50% cheaper, minutes-hours latency) for large-corpus backfill — opt-in 416 # and gateway-routed. It requires EMBEDDING_GATEWAY_URL (rejected at startup 417 # otherwise, see __post_init__) and the per-tier procrastinate path; the 418 # inline/memory pool can't defer a poll, so batch raises there rather than 419 # silently downgrading to sync. 420 "document_ocr_mode": "sync", 421 # Seconds between batch-job polls (the procrastinate re-enqueue delay). Each 422 # poll re-runs the tier; keep it well above a few seconds. 423 "document_ocr_batch_poll_seconds": 120, 424 # Observability 425 "metrics_enabled": True, 426 "metrics_port": 9090, 427 "otel_exporter_otlp_endpoint": None, 428 "otel_exporter_verify_ssl": None, 429 "otel_service_name": "nextcloud-mcp-server", 430 "pyroscope_enabled": False, 431 "pyroscope_server_address": None, 432 # Pod identity from the Kubernetes downward API (chart-injected). Facts 433 # about the pod, not user config — absent off-Kubernetes. 434 "pod_namespace": None, 435 "pod_name": None, 436 "log_format": "text", 437 "log_level": "INFO", 438 "log_include_trace_context": True, 439 # Reverse-proxy trust list for X-Forwarded-For (GH #1284). Passed to 440 # uvicorn; None keeps uvicorn's own resolution (which itself falls back to 441 # 127.0.0.1). Declared here so the key is settable from settings.toml, not 442 # only as the env var uvicorn already reads on its own. 443 "forwarded_allow_ips": None, 444 # Document processing (no master switch: each optional processor has its own 445 # ENABLE_* flag, and parsing on read is the caller's per-call decision) 446 "document_processor": "unstructured", 447 "enable_unstructured": False, 448 "unstructured_api_url": "http://unstructured:8000", 449 "unstructured_timeout": 120, 450 "unstructured_strategy": "auto", 451 "unstructured_languages": "eng,deu", 452 "progress_interval": 10, 453 "enable_tesseract": False, 454 "tesseract_cmd": None, 455 "tesseract_lang": "eng", 456 # pymupdf is the built-in "structured" tier and is always available -- there 457 # is no enable flag for it, only these two knobs. 458 "pymupdf_extract_images": True, 459 "pymupdf_image_dir": None, 460 "enable_custom_processor": False, 461 "custom_processor_url": None, 462 "custom_processor_types": "application/pdf", 463 "custom_processor_name": "custom", 464 "custom_processor_api_key": None, 465 "custom_processor_timeout": 60, 466 # Docling document-parsing backend (docling-serve HTTP API). One docling-serve 467 # instance feeds two touchpoints: the images-only DoclingProcessor 468 # (find_processor path, priority 20) and — when DOCUMENT_OCR_PROVIDER=docling — 469 # the PDF OCR backend for scanned/no-text-layer PDFs. The same processor can be 470 # force-selected per call to parse a text-layer PDF (tables/partial text). URL 471 # unset -> the image processor is not registered and the OCR backend resolves to 472 # None. See ADR-031. 473 "enable_docling": False, 474 "docling_api_url": None, 475 "docling_timeout": 120, 476 # docling-serve OCR language codes. The default engine (EasyOCR) uses 2-letter 477 # codes ("en","de"); a Tesseract-backed instance wants "eng","deu". Engine- 478 # dependent, so keep it operator-tunable (see ADR-031). 479 "docling_ocr_lang": "en,de", 480 # Run OCR on IMAGES routed to the DoclingProcessor (find_processor path). The 481 # docling OCR *backend* (scanned PDFs) always OCRs regardless of this flag. 482 "docling_do_ocr": True, 483 # Which docling-serve pipeline to request: "standard" (classic layout+OCR, 484 # default, unchanged) or "vlm" (Vision-LLM OCR). "vlm" needs a docling-serve 485 # instance configured with VLM presets (see ADR-032). 486 "docling_pipeline": "standard", 487 # VLM preset name sent when docling_pipeline == "vlm". None -> docling-serve 488 # picks its own DOCLING_SERVE_DEFAULT_VLM_PRESET. Preset names are server-defined. 489 "docling_vlm_preset": None, 490 # Caption raster pictures embedded in .pptx/.docx/.xlsx via the same 491 # docling-serve instance (ADR-037). Explicit opt-in beyond a bare 492 # DOCLING_API_URL, like DOCUMENT_OCR_PROVIDER=docling needs its own 493 # selection: a deployment that only wants docling for scanned-PDF OCR 494 # shouldn't start captioning every picture in every document for free. The 495 # native readers have no rendering engine, so this only reaches actual 496 # pictures -- native vector drawings (SmartArt, shapes) are unaffected. 497 "office_caption_images": False, 498 # Cap on pictures captioned per file (one docling-serve round trip each). 499 # nc_webdav_read_file blocks synchronously for the whole parse, so this 500 # bounds the worst case rather than leaving it to the file's picture count. 501 "office_caption_max_images": 8, 502 # Per-picture docling-serve request timeout (seconds). Deliberately short 503 # and independent of DOCLING_TIMEOUT/DOCUMENT_OCR_TIMEOUT_SECONDS (other 504 # touchpoints, other latency profiles): a caption is meant to be a quick 505 # per-picture round trip repeated up to OFFICE_CAPTION_MAX_IMAGES times, not 506 # a single long convert. Raise it if DOCLING_PIPELINE=vlm makes captions 507 # time out (VLM is far slower than the standard pipeline -- see ADR-032). 508 "office_caption_timeout_seconds": 15.0, 509 # Collabora Online (coolwsd) base URL for converting legacy .doc/.xls/.ppt 510 # and ODF .odt/.ods/.odp to OOXML, which the native readers then parse 511 # (ADR-039). Unset = those types are not claimed. coolwsd only answers 512 # convert-to for clients in its net.post_allow list (private ranges by 513 # default). 514 "collabora_url": None, 515 # Per-file convert-to request timeout (seconds). A typical document 516 # converts in well under a second; the headroom is for large workbooks. 517 "collabora_timeout_seconds": 60.0, 518 # Deprecated PPTX_CAPTION_* spellings (0.193.0, pptx-only) of the three keys 519 # above; see _apply_legacy_caption_settings. Declared so dynaconf reads 520 # them at all, with the same defaults so their validators still apply. 521 "pptx_caption_images": False, 522 "pptx_caption_max_images": 8, 523 "pptx_caption_timeout_seconds": 15.0, 524 # Tag-based file exclusion (issue #710): comma-separated list of 525 # Nextcloud system tag names. Files/folders carrying any of these tags 526 # are hidden from WebDAV MCP tools. Empty = feature off. 527 "excluded_tags": "", 528 # MCP decomposition hook points (design §10). Every default reproduces 529 # the current monolithic behavior; self-hosters who set none are 530 # unaffected. See docs/architecture/mcp-decomposition.md (sibling repo). 531 "embedding_provider": "autodetect", # autodetect | gateway 532 # Ingest queue backend (Deck #183). None → ``memory`` (the in-process anyio 533 # queue): procrastinate is strictly opt-in, even on a Postgres DATABASE_URL. 534 # Set ``postgres`` explicitly to split ingest into a procrastinate worker; 535 # that requires a PostgreSQL DATABASE_URL. 536 "ingest_queue": None, # memory | postgres 537 # Process role for the per-tenant two-pod model (Deck #183). ``api`` runs the 538 # MCP/query server + scanner (defers jobs); the ``worker`` role is the 539 # `nextcloud-mcp-server worker` process that drains the queue. ``all`` keeps 540 # the monolithic behaviour (API + in-process SQLite pool). 541 "mcp_role": "all", # api | worker | all 542 # Reclaim an ingest job orphaned in ``doing`` by a crashed worker once its 543 # worker heartbeat is this many seconds stale (Deck #183). Default is well 544 # above the longest expected document; raise it for slow embedding backends. 545 "ingest_stalled_job_seconds": 300, 546 # Backstop reclaim: also re-queue a job stuck in ``doing`` for this many seconds 547 # regardless of worker liveness. The heartbeat threshold above only catches DEAD 548 # workers; a job whose OWN completion crashed (e.g. an unhandled queueing_lock 549 # UniqueViolation on the doing->todo retry) is stranded in ``doing`` under a LIVE, 550 # heart-beating worker and is invisible to the heartbeat sweep. 551 # This fires purely on time-in-``doing``, NOT on liveness, so it can't tell "stuck" 552 # from "legitimately slow": if a single HEALTHY process_document attempt ever ran 553 # past this, it'd be re-queued mid-flight and two workers could process the doc 554 # concurrently. That's safe ONLY because ingest re-runs are idempotent (uuid5 555 # Qdrant point IDs — see the queue module's "Design notes"), NOT because the 556 # reclaim distinguishes the two — so keep the default comfortably above the longest 557 # real single attempt. OCR batch polling releases the worker between polls 558 # (BatchPending -> retry_in), so a job never legitimately holds ``doing`` this 559 # long; 1800s is deep headroom. (Deck: ingest doing-strand reclaim.) 560 "ingest_doing_max_seconds": 1800, 561 # Delete succeeded ingest jobs (keeps the queue table lean + the KEDA 562 # queue-depth metric clean). Set false to retain succeeded rows for audit 563 # (note: indexing success is also recorded in logs/metrics regardless). 564 "ingest_delete_succeeded_jobs": True, 565 # Whether the procrastinate worker uses LISTEN/NOTIFY for job pickup (Deck 566 # #424). True (default) = near-instant wakeup via a long-lived LISTEN 567 # connection. Set false to run POLL-ONLY when DATABASE_URL routes through a 568 # transaction-mode connection pooler (PgBouncer transaction mode), which is 569 # incompatible with LISTEN/NOTIFY — the LISTEN registration is dropped when 570 # the backend returns to the pool. Poll-only trades a few seconds of pickup 571 # latency (fetch_job_polling_interval) for pooler safety; job queries still 572 # multiplex through the pooler. Snapshotted at worker startup; needs a 573 # restart to change. 574 "ingest_listen_notify": True, 575 # Per-tier escalation on the procrastinate (postgres) ingest path (Deck 576 # #323). When true, a document that a tier cannot parse well is requeued onto 577 # the next tier's queue (fast -> structured -> ocr) via a native procrastinate 578 # queue-hop. When false the ``fast`` tier is terminal -- reproduces the 579 # pre-#323 behaviour where the cheap tier's output is indexed as-is. No effect 580 # on the in-process ``memory`` backend, which keeps the inline escalation. 581 # HOT: re-read per job (process_document_task), so it takes effect on the next 582 # job -- unlike INGEST_TRANSIENT_MAX_ATTEMPTS, which is snapshotted at worker 583 # startup and needs a restart. 584 "ingest_escalation_enabled": True, 585 # Global cap on SAME-tier retries for transient infra errors (doc fetch / 586 # embed / Qdrant blips) on the procrastinate path. Parse-quality failures 587 # escalate (one parse attempt per tier) and do NOT consume this budget; only 588 # whitelisted transient exceptions retry in place. Shared across tiers because 589 # a queue-hop cannot reset a per-tier counter (see TieredEscalationStrategy). 590 # Snapshotted at worker startup (blueprint build); restart to change it. 591 "ingest_transient_max_attempts": 5, 592 # Delay (seconds) before a reclaimed stalled job is re-run. A stall is often 593 # systemic (Qdrant/embedding outage), so reclaiming every crashed job at 594 # now() would thundering-herd a recovering dependency every reclaim tick 595 # (*/5min), bypassing TieredEscalationStrategy's per-job backoff. A small 596 # fixed delay staggers the retry. 0 = immediate (legacy behaviour). 597 "ingest_reclaim_retry_delay_seconds": 30, 598 "collection_metadata_source": "qdrant", # qdrant | api 599 # CP base URL for COLLECTION_METADATA_SOURCE=api (e.g. http://control-plane). 600 # Required only when the source is api. 601 "collection_metadata_api_url": None, 602 "embedding_gateway_url": None, # required when embedding_provider=gateway 603 # Provider-namespaced model the gateway serves, "<provider>/<model>" 604 # (the gateway routes on the "/"-prefix; mistral/mistral-embed → Mistral 605 # for the MVP). Only consulted when embedding_provider=gateway. 606 "embedding_gateway_model": "mistral/mistral-embed", 607 # Gateway auth: the MCP server is an OIDC *client* in the gateway's own 608 # M2M realm (parallel to, and distinct from, the tenant realm it already 609 # serves). It obtains a client-credentials token and the gateway maps the 610 # client-id → the tenant's underlying provider API key. All four unset = 611 # call the gateway unauthenticated (matches today's not-yet-authed gateway). 612 "embedding_gateway_token_url": None, # M2M token endpoint 613 "embedding_gateway_client_id": None, 614 "embedding_gateway_client_secret": None, 615 "embedding_gateway_scope": None, # e.g. astrolabe-embedding-gateway/embed 616 # Query-side ACL pre-filter (design §11). OFF by default: a Qdrant 617 # `match any` on `acl_hash` excludes points missing the key, so enabling 618 # this before a real ACL backfill would silently drop legacy results. 619 # verify-on-read remains the correctness backstop regardless. 620 "acl_prefilter_enabled": False, 621 # Usage metering (Deck #67, control-plane usage-metering.md). OFF by 622 # default so OSS self-hosters don't accrue a metering table or write 623 # overhead; Astrolabe Cloud provisioning sets it true. When on, billable 624 # ops record rows into the app-DB usage_events table (best-effort). 625 "usage_metering_enabled": False, 626 } 627 628 629 def _resolve_settings_files() -> list[str]: 630 """Find optional external settings files. 631 632 Priority: 633 1. NEXTCLOUD_MCP_SETTINGS_FILE env var (absolute or relative path). 634 If set but the file does not exist, raise FileNotFoundError — 635 silently falling back to defaults on a typo would be a footgun. 636 .secrets.toml is looked for alongside the explicit file. 637 2. Otherwise ./settings.toml in cwd (for docker / dev workflows), 638 with .secrets.toml also looked for in cwd. 639 640 Returns an empty list if nothing is configured — that's fine, defaults 641 and env vars still apply. 642 """ 643 files: list[str] = [] 644 # A legitimate os.environ read: this runs BEFORE the dynaconf instance is 645 # built (it computes the settings_files list passed to Dynaconf), so it 646 # cannot go through dynaconf — you can't read the location of the settings 647 # file from the settings file. This + the MCP_DEPLOYMENT_MODE env_switcher 648 # are dynaconf's own bootstrap. All config *values* are dynaconf-driven; the 649 # only other reads are the legacy-env deprecation check and 650 # _warn_unknown_env_vars, which inspect env var NAMES, not values. 651 explicit = os.environ.get("NEXTCLOUD_MCP_SETTINGS_FILE") 652 if explicit: 653 p = Path(explicit) 654 if not p.exists(): 655 raise FileNotFoundError( 656 f"NEXTCLOUD_MCP_SETTINGS_FILE points to a file that does " -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/client/contacts.py line 4
- Likely real
- Yes, 52% confidence
Using xml.etree.ElementTree to parse untrusted XML data is known to be vulnerable to XML attacks. Replace xml.etree.ElementTree with the equivalent defusedxml package, or make sure defusedxml.defuse_stdlib() is called.
3 import logging 4 import xml.etree.ElementTree as ET 5 from datetime import date -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/client/webdav.py line 5
- Likely real
- Yes, 52% confidence
Using xml.etree.ElementTree to parse untrusted XML data is known to be vulnerable to XML attacks. Replace xml.etree.ElementTree with the equivalent defusedxml package, or make sure defusedxml.defuse_stdlib() is called.
4 import mimetypes 5 import xml.etree.ElementTree as ET 6 from email.utils import parsedate_to_datetime -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/client/base.py line 6
- Likely real
- Yes, 51% confidence
Using xml.etree.ElementTree to parse untrusted XML data is known to be vulnerable to XML attacks. Replace xml.etree.ElementTree with the equivalent defusedxml package, or make sure defusedxml.defuse_stdlib() is called.
5 import time 6 import xml.etree.ElementTree as ET 7 from abc import ABC -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/client/dav_errors.py line 36
- Likely real
- Yes, 50% confidence
Using xml.etree.ElementTree to parse untrusted XML data is known to be vulnerable to XML attacks. Replace xml.etree.ElementTree with the equivalent defusedxml package, or make sure defusedxml.defuse_stdlib() is called.
35 36 import xml.etree.ElementTree as ET 37 -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/config.py line 44
- Likely real
- Uncertain, 45% confidence
Possible hardcoded password: 'None'
43 "nextcloud_username": None, 44 "nextcloud_password": None, 45 "nextcloud_verify_ssl": True, 46 "nextcloud_ca_bundle": None, 47 "nextcloud_http_keepalive": True, 48 "nextcloud_mcp_server_url": None, 49 "nextcloud_resource_uri": None, 50 "nextcloud_public_issuer_url": None, 51 "nextcloud_public_url": None, 52 "cookie_secure": None, 53 # OAuth/OIDC 54 "oidc_discovery_url": None, 55 # Startup OIDC-discovery retry/backoff. Declared here (lowercase) so 56 # dynaconf reads the UPPERCASE env vars under ignore_unknown_envvars=True; 57 # defaults mirror the Settings dataclass fields. 58 "oidc_discovery_max_attempts": 10, 59 "oidc_discovery_backoff_base": 1.0, 60 "oidc_discovery_backoff_max": 15.0, 61 # Startup Qdrant-collection-init retry/backoff (same rationale as OIDC). 62 "qdrant_init_max_attempts": 30, 63 "qdrant_init_backoff_base": 1.0, 64 "qdrant_init_backoff_max": 10.0, 65 # Keys must uppercase to the env var dynaconf reads (ignore_unknown_envvars): 66 # NEXTCLOUD_OIDC_TOKEN_TYPE / NEXTCLOUD_OIDC_SCOPES, matching _ENV_OVERRIDE. 67 "nextcloud_oidc_token_type": "Bearer", 68 "nextcloud_oidc_scopes": "", 69 "port": 8000, 70 "nextcloud_oidc_client_id": None, 71 "nextcloud_oidc_client_secret": None, 72 "oidc_issuer": None, 73 "jwks_uri": None, 74 "introspection_uri": None, 75 "userinfo_uri": None, 76 "oidc_resource_server_id": None, 77 # M2M / DCR / management — these MUST be declared here (lowercase) so dynaconf 78 # reads the matching UPPERCASE env var; with ignore_unknown_envvars=True an 79 # undeclared key is silently dropped (env value ignored), so they are read 80 # via cfg("ENV_NAME"). Defaults mirror the prior os.getenv(..., default). 81 "mcp_server_client_id": None, 82 "mcp_server_client_secret": None, 83 "allowed_mcp_clients": "", 84 "allowed_mgmt_client": "", 85 # Hosts whose Client ID Metadata Documents the AS proxy trusts (GH #1470). 86 # Empty = CIMD disabled; "*" = any publicly routable host. See auth/cimd.py. 87 "cimd_allowed_hosts": "", 88 "enable_dcr": False, 89 # Container-runtime / webhook self-URL overrides (local-dev docker-compose). 90 "docker_container": False, 91 "nextcloud_mcp_service_name": "mcp", 92 "nextcloud_mcp_port": 8000, 93 # Mode flags 94 # NOTE: `enable_multi_user_basic_auth` and `enable_login_flow` are 95 # intentionally absent — they are derived from MCP_DEPLOYMENT_MODE in 96 # Settings.__post_init__ (ADR-022) and not read from the dynaconf store. 97 # Escape hatch for the per-tool capability gate (capabilities.py). Set 98 # MCP_DISABLE_CAPABILITY_GATING=true to list and run every registered tool 99 # regardless of what the instance advertises — the answer to "why did my 100 # deck tools disappear?" without a code read. Read via cfg(), not a 101 # Settings field: nothing but the two gate helpers looks at it. 102 "mcp_disable_capability_gating": False, 103 "enable_semantic_search": False, 104 "enable_background_operations": False, 105 "vector_sync_enabled": False, 106 "enable_offline_access": False, 107 # Token storage 108 "token_encryption_key": None, 109 # None = ephemeral per-process tempfile (see get_token_db_path()). 110 # Set TOKEN_STORAGE_DB to persist tokens across restarts. 111 "token_storage_db": None, 112 # Centralized backend (any SQLAlchemy URL). Wins over TOKEN_STORAGE_DB 113 # when set. Use postgresql+psycopg://user:pw@host/db for HA k8s 114 # deployments so pods can be stateless. The URL is passed through 115 # verbatim — TLS (e.g. ?sslmode=require) and every other parameter are 116 # read from it by libpq/psycopg; the server never rewrites it. See ADR-026. 117 "database_url": None, 118 # Postgres connection pool sizing (ADR-026 → "Concurrency model and 119 # pool sizing"). Per-pod defaults to 2 + 5 overflow = 7 max 120 # connections. psycopg connections are single-flight, so the pool 121 # only needs to cover typical multi-user MCP burst — not every 122 # potential in-flight tool call. Tune up with DATABASE_POOL_SIZE / 123 # DATABASE_MAX_OVERFLOW for high-traffic prod fleets. 124 "database_pool_size": 2, 125 "database_max_overflow": 5, 126 # Webhook delivery authentication (ADR-010): when set, registrations 127 # tell NC to add `Authorization: Bearer <secret>` to webhook deliveries 128 # and the receiver rejects unauthenticated requests. 129 "webhook_secret": None, 130 # Internal URL override for webhook registration; wins over 131 # NEXTCLOUD_MCP_SERVER_URL when set (e.g. split internal/external URLs). 132 "webhook_internal_url": None, 133 # Vector sync 134 "vector_sync_scan_interval": 300, 135 "vector_sync_processor_workers": 3, 136 # Consecutive failed index attempts (embed / Qdrant / transport — NOT a 137 # parse failure, which is terminal on the first try) before a document is 138 # dead-lettered instead of re-queued forever. See vector/dead_letter.py. 139 "vector_sync_max_index_failures": 5, 140 # Optional per-tier concurrency overrides (None = fall back to 141 # vector_sync_processor_workers). Consumed by the `worker` CLI per --tier. 142 "vector_sync_fast_concurrency": None, 143 "vector_sync_structured_concurrency": None, 144 "vector_sync_queue_max_size": 10000, 145 "vector_sync_metrics_refresh_interval": 20, 146 "vector_density_snapshot_enabled": True, 147 "vector_density_snapshot_interval": 300, 148 "vector_density_snapshot_max_documents": 50000, 149 "usage_stock_snapshot_interval": 86400, 150 "vector_ram_hnsw_overhead_factor": 1.5, 151 "vector_sync_user_poll_interval": 60, 152 "health_ready_refresh_interval": 15, 153 # Orphan-sweep at Pod startup (card #101). When True, delete any 154 # placeholders carrying a different / absent ``instance_id`` before 155 # the scanner's first cycle, so a Pod restart mid-batch doesn't 156 # leave work stuck behind the 5x-scan-interval staleness gate. 157 # Escape hatch only — leave on by default. 158 "vector_sync_orphan_sweep_enabled": True, 159 # System tag that marks files for hybrid (dense + BM25 sparse) indexing. 160 # The scanner indexes files carrying this tag; verify-on-read gates results 161 # on current membership of this tag (ADR-019). 162 "vector_sync_tag": "vector-index", 163 # System tag that marks files for keyword-only (BM25 sparse) indexing. 164 # Defaults to ``keyword-index`` (symmetric with ``vector_sync_tag``), so 165 # a user who creates + applies that tag gets keyword-only indexing out of the 166 # box. Files carrying it are indexed sparse-only (no dense embedding, no 167 # embedding cost) into the SAME collection as hybrid files; ``vector-index`` 168 # wins if a file carries both. Set empty to disable the second tag entirely. 169 "vector_sync_keyword_tag": "keyword-index", 170 # Which MIME types tagged-file discovery will enqueue. None (unset) means 171 # every type a registered processor can read; a comma-separated list 172 # narrows it, and an empty string indexes no files. 173 "vector_sync_indexable_mime_types": None, 174 # Mail tag (an IMAP keyword) restricting which messages are indexed. Empty 175 # (the default) indexes every message in every mailbox, which is the 176 # behaviour before this setting existed. Set it to a tag display name and 177 # only messages carrying that tag are indexed — the mail analogue of 178 # ``vector_sync_tag`` for files. 179 "mail_index_tag": "", 180 # Fail-safe against a flaky/empty tag-discovery read: number of *consecutive* 181 # scan cycles for which a given index mode's tag discovery must return zero 182 # files (while Qdrant still holds indexed points for that mode) before the 183 # scanner believes it and starts deleting. A transient empty read (e.g. a 184 # customer-hosted Nextcloud that intermittently answers the systemtag REPORT 185 # with an empty 207) is thereby treated as a failed read, not "all files 186 # gone" — preventing a delete/re-index churn loop. A sustained empty (a real 187 # mass-untag) still eventually deletes once the streak reaches this value. 188 "vector_sync_empty_discovery_delete_threshold": 3, 189 # Verify-on-read concurrency cap (ADR-019) 190 "verification_concurrency": 20, 191 # Qdrant 192 "qdrant_url": None, 193 "qdrant_location": None, 194 "qdrant_api_key": None, 195 "qdrant_collection": "nextcloud_content", 196 # Ollama 197 "ollama_base_url": None, 198 "ollama_embedding_model": "nomic-embed-text", 199 "ollama_verify_ssl": True, 200 # Ollama embeds a batch serially on whatever hardware it has, so the wall 201 # clock of one /api/embed call scales with the batch's total text, not its 202 # item count. Bound the request by characters (GH #1345). 203 "ollama_embed_max_batch_chars": 16000, 204 "ollama_embed_timeout": 120, 205 # OpenAI 206 "openai_api_key": None, 207 "openai_base_url": None, 208 "openai_embedding_model": "text-embedding-3-small", 209 # Bedrock (AWS) 210 "aws_region": None, 211 "aws_access_key_id": None, 212 "aws_secret_access_key": None, 213 "bedrock_embedding_model": None, 214 # Mistral 215 "mistral_api_key": None, 216 "mistral_embedding_model": "mistral-embed", 217 "mistral_base_url": None, 218 # Matryoshka output width requested from the embedding endpoint (None = 219 # the model's full width). Provider-agnostic: only one provider is active 220 # at a time, so a single knob covers openai/ollama/gateway. 221 "embedding_dimensions": None, 222 # Simple (fallback) embedding dimension 223 "simple_embedding_dimension": 384, 224 # Document chunking 225 "document_chunk_size": 2048, 226 "document_chunk_overlap": 200, 227 # Page-aware chunking for paginated docs (PDFs): split on page boundaries 228 # first so no chunk spans a page (exact page_number, clean snippets, and 229 # predictable ~1 chunk/page when chunk_size >= the largest page). 230 "document_chunk_page_aware": True, 231 # Greedy page-packing (density fix, Deck #636): merge consecutive sub-budget 232 # pages into one chunk instead of one-per-page. Off by default until the 233 # post-change density re-measure + pricing re-calibration land. 234 "document_chunk_page_pack": False, 235 # Hybrid-search RRF ranking constant. Qdrant's native RRF hardcodes k=2, 236 # where adjacent ranks differ by 33% and a single-list outlier outranks a 237 # document both retrievers agree on. 60 is the standard value (~2% per rank), 238 # which restores consensus as the dominant signal. 239 "vector_search_rrf_k": 60, 240 # --- Cross-encoder reranking (opt-in, per request) ---------------------- 241 # A cross-encoder reorders the retrieved candidates by scoring each against 242 # the query directly, which is more accurate than the fusion rank it 243 # replaces. Ships OFF because it adds an upstream round-trip to every 244 # search, and how expensive that is depends on how the reranker is deployed 245 # — which this server deliberately knows nothing about beyond the URL. 246 "search_rerank_enabled": False, 247 # Where reranking is served. A FULL endpoint URL, because the path differs 248 # per backend and there is no safe guess: Infinity serves ``POST /rerank``, 249 # vLLM ``POST /v1/rerank``, Cohere ``POST /v2/rerank``, the Astrolabe 250 # embedding gateway ``POST /v1/rerank``. All four speak the same Cohere 251 # request/response protocol, so choosing one is configuration, not code. 252 # 253 # Unset = derive it from EMBEDDING_GATEWAY_URL, which is what every existing 254 # deployment does. Set it to run reranking WITHOUT a gateway — the reason 255 # this exists (discussion #1354): Ollama serves no rerank models at all, so 256 # a self-hoster's only options are a hosted rerank API or a local Infinity / 257 # vLLM, and neither should require standing up a gateway first. 258 "search_rerank_url": None, 259 # Static bearer for SEARCH_RERANK_URL (a Cohere API key, an Infinity 260 # ``--api-key``). Wins over the gateway's M2M OIDC credentials when both are 261 # present. Unset = send no Authorization header, which is correct for a 262 # local Infinity/vLLM on a private network. 263 "search_rerank_api_key": None, 264 # Rerank model, addressed the way the CONFIGURED ENDPOINT expects — which 265 # differs between the two, so the default is only right for one of them. 266 # 267 # Against the embedding gateway the ``<provider>/`` prefix is REQUIRED, not 268 # stylistic: the gateway splits on the FIRST slash to select a backend, so a 269 # bare ``BAAI/bge-reranker-v2-m3`` asks for a provider named ``BAAI`` and 270 # 503s — and because a failed rerank degrades to retrieval order 271 # (``reranked: false``) rather than erroring, that looks like "reranking does 272 # nothing" instead of like a misconfiguration. The prefix is also the backend 273 # selector: ``local/`` for a self-hosted cross-encoder, ``bedrock/`` or 274 # ``openrouter/`` for a hosted one. 275 # 276 # Against a direct SEARCH_RERANK_URL there is no such routing layer, so use 277 # the bare id the server serves (``BAAI/bge-reranker-v2-m3`` for 278 # Infinity/vLLM, ``rerank-v3.5`` for Cohere). The relevance calibration 279 # (ADR-034) strips known provider prefixes, so both spellings resolve to the 280 # same fitted curve. 281 "search_rerank_model": "local/BAAI/bge-reranker-v2-m3", 282 # Candidates handed to the reranker. Reranking can only reorder what 283 # retrieval supplied, so this depth — not the caller's ``limit`` — bounds 284 # how much a reranker can improve. Reranking just the rows a normal search 285 # returns captures little of the available gain. 286 # 287 # Treat this as a ceiling rather than a starting point. Under 288 # ``granularity="document"`` the grouped prefetch is bounded by 289 # MAX_DOCUMENT_PREFETCH, and requesting more groups than the prefetch can 290 # fill makes Qdrant widen its grouping search and reorder the head of the 291 # result set before the reranker sees it. Going deeper requires raising 292 # MAX_DOCUMENT_PREFETCH first, which carries its own latency cost. 293 "search_rerank_pool_size": 200, 294 # Generous headroom over a normal rerank, not a target. Exceeding it 295 # degrades to retrieval order rather than failing the search. 296 "search_rerank_timeout_seconds": 30.0, 297 # Concurrent rerank calls this process keeps in flight against the reranker. 298 # 299 # Conservative at 1 because reranking is the heaviest request this server 300 # makes of a service it may share with its own embedding traffic and with 301 # other callers. Bounding our own concurrency keeps a burst of searches from 302 # queueing unbounded work upstream, and keeps rerank latency predictable 303 # here. It is a client-side courtesy, not a throughput control: what the 304 # reranker does with the request, and how it schedules against everything 305 # else it serves, is its business. Raise it if yours has headroom — a CPU 306 # cross-encoder almost certainly does not. 307 "search_rerank_max_concurrency": 1, 308 # --- SAR export redaction (ADR-040) ------------------------------------- 309 # Names are detected by the embedding gateway's ``POST /v1/ner``, so SAR 310 # export is available only with EMBEDDING_GATEWAY_URL set. 311 # NER model, addressed the gateway way (``<provider>/<model>``). 312 # Subject access request cases and redacted export (ADR-040). Off by 313 # default: a deployment opts in, since only some need it. Also requires 314 # vector sync and EMBEDDING_GATEWAY_URL (for name detection). 315 "sar_enabled": False, 316 "ner_model": "local/urchade/gliner_multi_pii-v1", 317 # Per-request budget. Export runs in the background, so this only needs to 318 # cover one batch on the slowest backend (CPU GLiNER: ~570 chars/s). 319 "ner_timeout_seconds": 120.0, 320 # Texts (of up to 2,000 chars) per /v1/ner request. Small for CPU GLiNER, 321 # which must finish a batch inside the gateway's own upstream timeout; raise 322 # it (e.g. 32) on a GPU backend. 323 "ner_batch_size": 8, 324 # Minimum model confidence for a span to count as a person. Lower raises 325 # recall at the cost of over-redaction, which is the safe direction for a 326 # disclosure; 0.5 is GLiNER's customary operating point. 327 "ner_threshold": 0.5, 328 # Chunking config generation. Bump whenever chunker behaviour changes (size, 329 # overlap, page-aware, page-pack, split strategy) so the pricing model's 330 # density reference can't silently go stale. Pinned in stripe-catalog.tf. 331 "chunking_config_version": 1, 332 # PDF parse isolation (OOM guard) 333 "document_pdf_graphics_limit": 1000, 334 "document_parse_timeout_seconds": 120.0, 335 # Optional wall-clock cap (seconds) on the SYNCHRONOUS parse inside the 336 # nc_webdav_read_file MCP tool. None (default) = disabled: an interactive read 337 # is bounded only by the underlying processor timeout (DOCLING_TIMEOUT / 338 # DOCUMENT_OCR_TIMEOUT_SECONDS). Set a client-friendly bound (e.g. 45-60s) so a 339 # slow VLM/OCR convert returns base64 quickly instead of blocking past an MCP 340 # client's own timeout. Never applies to the async ingest/worker path (ADR-032). 341 "document_read_timeout_seconds": None, 342 "document_parse_mem_limit_mb": 1536, 343 # Pre-parse size cap (MB): PDFs larger than this fail fast with reason 344 # "oversize" instead of burning the OCR timeout to 0 chars on a pathological 345 # file. 0 disables the guard. 346 "document_max_pdf_size_mb": 50.0, 347 # Pre-flight cap (MB) on nc_webdav_write_file's content: a huge PUT built 348 # from a single in-memory MCP tool argument (no chunked/streaming upload 349 # exists for writes, unlike the read-side stream_to_file path) risks 350 # timing out or exhausting memory rather than failing predictably. 0 351 # disables the guard. 352 "webdav_write_max_mb": 50.0, 353 # Transport security (MCP SDK TransportSecuritySettings). Default False 354 # preserves the behaviour hardcoded before these knobs existed: MCP 1.23+ 355 # auto-enables localhost-only host checking, which breaks k8s/Docker service 356 # DNS names (docs/MCP-1.23-DNS-REBINDING-FIX.md). Enable it plus 357 # MCP_ALLOWED_HOSTS when the server is reachable from a browser. 358 "mcp_dns_rebinding_protection": False, 359 # Comma-separated Host / Origin allowlists, honoured only when the above is 360 # enabled. Empty means "the SDK's own defaults". 361 "mcp_allowed_hosts": "", 362 "mcp_allowed_origins": "", 363 # Comma-separated CORS origin allowlist. "*" preserves today's behaviour. 364 "cors_allow_origins": "*", 365 # Page ceiling for markdown reconstruction (see document_markdown_max_pages). 366 "document_markdown_max_pages": 150, 367 # Pages per pypdfium2 extraction window (see document_parse_page_window). 368 "document_parse_page_window": 100, 369 # Concurrent isolated parse subprocesses (see document_parse_process_slots). 370 "document_parse_process_slots": 2, 371 # Stream ingest downloads to disk instead of buffering them in memory. 372 "document_stream_download_enabled": True, 373 # Directory for ingest spool files (default: the system temp dir). 374 "document_spool_dir": None, 375 # Tier-0 classifier (records classification metrics on the tiered path) 376 "document_classify_enabled": True, 377 # Tiered PDF pipeline: pypdfium2 is the default/only hot-path extractor; 378 # "pymupdf" is a deprecated rollback escape hatch. OCR (tier-3) is the only 379 # escalation target and is off by default (no provider wired yet). 380 "document_tier1_engine": "pypdfium2", 381 "document_ocr_enabled": False, 382 # OCR backend: "auto" picks gateway (if EMBEDDING_GATEWAY_URL) else mistral 383 # (if MISTRAL_API_KEY); "gateway"/"mistral" force one; "docling" routes scanned 384 # PDFs to a docling-serve instance (DOCLING_API_URL); "none" disables. "auto" 385 # never selects docling (it needs a self-hosted URL, so it must be explicit). 386 # The gateway routes on the model's "<provider>/" prefix, so it serves Mistral, 387 # surya (in-cluster GPU over the tailnet), etc. — one configurable OCR tier. 388 "document_ocr_provider": "auto", 389 # Provider-namespaced OCR model id (gateway routes on the prefix; the direct 390 # mistral backend strips it). e.g. "mistral/mistral-ocr-latest" (Mistral) or 391 # "surya/surya-ocr-2" (surya via the gateway) — never hard-coded, only this 392 # default. 393 "document_ocr_model": "mistral/mistral-ocr-latest", 394 # OCR escalation triggers (tier-0). A page is OCR-worthy when its text is 395 # near-empty (< min_page_chars) OR low-quality (< min_text_quality) OR (when 396 # detect_scanned) mostly a raster image; a doc escalates when the OCR-worthy 397 # page fraction reaches page_fraction. Calibrate min_text_quality from the 398 # astrolabe_document_text_quality histogram per tenant. 399 "document_ocr_min_text_quality": 0.5, 400 "document_ocr_page_fraction": 0.5, 401 "document_ocr_min_page_chars": 16, 402 "document_ocr_detect_scanned": True, 403 # Tier-0 glyph-corruption trigger. When the fast (pypdfium2) extraction's 404 # doc-level C0-control-char ratio exceeds this, the text layer is treated as 405 # glyph-corrupt (a broken /ToUnicode mapping leaking raw glyph codes) and the 406 # doc escalates fast->structured (pymupdf re-extracts it correctly -- no OCR). 407 # 0 disables. Clean docs sit ~0; affected PDFs measured ~1-11% in testing. 408 "document_glyph_corruption_ratio": 0.02, 409 # OCR backend request timeout (seconds). Slow scanned newspapers can take 410 # 20-60s; raise/lower per tenant. Configurable so a tenant isn't stuck with 411 # the 180s default when its gateway has its own shorter ceiling. 412 "document_ocr_timeout_seconds": 180.0, 413 # OCR execution mode (Deck #332). "sync" (default) transcribes inline via the 414 # backend's synchronous path. "batch" routes to the gateway's async Batch OCR 415 # job (~50% cheaper, minutes-hours latency) for large-corpus backfill — opt-in 416 # and gateway-routed. It requires EMBEDDING_GATEWAY_URL (rejected at startup 417 # otherwise, see __post_init__) and the per-tier procrastinate path; the 418 # inline/memory pool can't defer a poll, so batch raises there rather than 419 # silently downgrading to sync. 420 "document_ocr_mode": "sync", 421 # Seconds between batch-job polls (the procrastinate re-enqueue delay). Each 422 # poll re-runs the tier; keep it well above a few seconds. 423 "document_ocr_batch_poll_seconds": 120, 424 # Observability 425 "metrics_enabled": True, 426 "metrics_port": 9090, 427 "otel_exporter_otlp_endpoint": None, 428 "otel_exporter_verify_ssl": None, 429 "otel_service_name": "nextcloud-mcp-server", 430 "pyroscope_enabled": False, 431 "pyroscope_server_address": None, 432 # Pod identity from the Kubernetes downward API (chart-injected). Facts 433 # about the pod, not user config — absent off-Kubernetes. 434 "pod_namespace": None, 435 "pod_name": None, 436 "log_format": "text", 437 "log_level": "INFO", 438 "log_include_trace_context": True, 439 # Reverse-proxy trust list for X-Forwarded-For (GH #1284). Passed to 440 # uvicorn; None keeps uvicorn's own resolution (which itself falls back to 441 # 127.0.0.1). Declared here so the key is settable from settings.toml, not 442 # only as the env var uvicorn already reads on its own. 443 "forwarded_allow_ips": None, 444 # Document processing (no master switch: each optional processor has its own 445 # ENABLE_* flag, and parsing on read is the caller's per-call decision) 446 "document_processor": "unstructured", 447 "enable_unstructured": False, 448 "unstructured_api_url": "http://unstructured:8000", 449 "unstructured_timeout": 120, 450 "unstructured_strategy": "auto", 451 "unstructured_languages": "eng,deu", 452 "progress_interval": 10, 453 "enable_tesseract": False, 454 "tesseract_cmd": None, 455 "tesseract_lang": "eng", 456 # pymupdf is the built-in "structured" tier and is always available -- there 457 # is no enable flag for it, only these two knobs. 458 "pymupdf_extract_images": True, 459 "pymupdf_image_dir": None, 460 "enable_custom_processor": False, 461 "custom_processor_url": None, 462 "custom_processor_types": "application/pdf", 463 "custom_processor_name": "custom", 464 "custom_processor_api_key": None, 465 "custom_processor_timeout": 60, 466 # Docling document-parsing backend (docling-serve HTTP API). One docling-serve 467 # instance feeds two touchpoints: the images-only DoclingProcessor 468 # (find_processor path, priority 20) and — when DOCUMENT_OCR_PROVIDER=docling — 469 # the PDF OCR backend for scanned/no-text-layer PDFs. The same processor can be 470 # force-selected per call to parse a text-layer PDF (tables/partial text). URL 471 # unset -> the image processor is not registered and the OCR backend resolves to 472 # None. See ADR-031. 473 "enable_docling": False, 474 "docling_api_url": None, 475 "docling_timeout": 120, 476 # docling-serve OCR language codes. The default engine (EasyOCR) uses 2-letter 477 # codes ("en","de"); a Tesseract-backed instance wants "eng","deu". Engine- 478 # dependent, so keep it operator-tunable (see ADR-031). 479 "docling_ocr_lang": "en,de", 480 # Run OCR on IMAGES routed to the DoclingProcessor (find_processor path). The 481 # docling OCR *backend* (scanned PDFs) always OCRs regardless of this flag. 482 "docling_do_ocr": True, 483 # Which docling-serve pipeline to request: "standard" (classic layout+OCR, 484 # default, unchanged) or "vlm" (Vision-LLM OCR). "vlm" needs a docling-serve 485 # instance configured with VLM presets (see ADR-032). 486 "docling_pipeline": "standard", 487 # VLM preset name sent when docling_pipeline == "vlm". None -> docling-serve 488 # picks its own DOCLING_SERVE_DEFAULT_VLM_PRESET. Preset names are server-defined. 489 "docling_vlm_preset": None, 490 # Caption raster pictures embedded in .pptx/.docx/.xlsx via the same 491 # docling-serve instance (ADR-037). Explicit opt-in beyond a bare 492 # DOCLING_API_URL, like DOCUMENT_OCR_PROVIDER=docling needs its own 493 # selection: a deployment that only wants docling for scanned-PDF OCR 494 # shouldn't start captioning every picture in every document for free. The 495 # native readers have no rendering engine, so this only reaches actual 496 # pictures -- native vector drawings (SmartArt, shapes) are unaffected. 497 "office_caption_images": False, 498 # Cap on pictures captioned per file (one docling-serve round trip each). 499 # nc_webdav_read_file blocks synchronously for the whole parse, so this 500 # bounds the worst case rather than leaving it to the file's picture count. 501 "office_caption_max_images": 8, 502 # Per-picture docling-serve request timeout (seconds). Deliberately short 503 # and independent of DOCLING_TIMEOUT/DOCUMENT_OCR_TIMEOUT_SECONDS (other 504 # touchpoints, other latency profiles): a caption is meant to be a quick 505 # per-picture round trip repeated up to OFFICE_CAPTION_MAX_IMAGES times, not 506 # a single long convert. Raise it if DOCLING_PIPELINE=vlm makes captions 507 # time out (VLM is far slower than the standard pipeline -- see ADR-032). 508 "office_caption_timeout_seconds": 15.0, 509 # Collabora Online (coolwsd) base URL for converting legacy .doc/.xls/.ppt 510 # and ODF .odt/.ods/.odp to OOXML, which the native readers then parse 511 # (ADR-039). Unset = those types are not claimed. coolwsd only answers 512 # convert-to for clients in its net.post_allow list (private ranges by 513 # default). 514 "collabora_url": None, 515 # Per-file convert-to request timeout (seconds). A typical document 516 # converts in well under a second; the headroom is for large workbooks. 517 "collabora_timeout_seconds": 60.0, 518 # Deprecated PPTX_CAPTION_* spellings (0.193.0, pptx-only) of the three keys 519 # above; see _apply_legacy_caption_settings. Declared so dynaconf reads 520 # them at all, with the same defaults so their validators still apply. 521 "pptx_caption_images": False, 522 "pptx_caption_max_images": 8, 523 "pptx_caption_timeout_seconds": 15.0, 524 # Tag-based file exclusion (issue #710): comma-separated list of 525 # Nextcloud system tag names. Files/folders carrying any of these tags 526 # are hidden from WebDAV MCP tools. Empty = feature off. 527 "excluded_tags": "", 528 # MCP decomposition hook points (design §10). Every default reproduces 529 # the current monolithic behavior; self-hosters who set none are 530 # unaffected. See docs/architecture/mcp-decomposition.md (sibling repo). 531 "embedding_provider": "autodetect", # autodetect | gateway 532 # Ingest queue backend (Deck #183). None → ``memory`` (the in-process anyio 533 # queue): procrastinate is strictly opt-in, even on a Postgres DATABASE_URL. 534 # Set ``postgres`` explicitly to split ingest into a procrastinate worker; 535 # that requires a PostgreSQL DATABASE_URL. 536 "ingest_queue": None, # memory | postgres 537 # Process role for the per-tenant two-pod model (Deck #183). ``api`` runs the 538 # MCP/query server + scanner (defers jobs); the ``worker`` role is the 539 # `nextcloud-mcp-server worker` process that drains the queue. ``all`` keeps 540 # the monolithic behaviour (API + in-process SQLite pool). 541 "mcp_role": "all", # api | worker | all 542 # Reclaim an ingest job orphaned in ``doing`` by a crashed worker once its 543 # worker heartbeat is this many seconds stale (Deck #183). Default is well 544 # above the longest expected document; raise it for slow embedding backends. 545 "ingest_stalled_job_seconds": 300, 546 # Backstop reclaim: also re-queue a job stuck in ``doing`` for this many seconds 547 # regardless of worker liveness. The heartbeat threshold above only catches DEAD 548 # workers; a job whose OWN completion crashed (e.g. an unhandled queueing_lock 549 # UniqueViolation on the doing->todo retry) is stranded in ``doing`` under a LIVE, 550 # heart-beating worker and is invisible to the heartbeat sweep. 551 # This fires purely on time-in-``doing``, NOT on liveness, so it can't tell "stuck" 552 # from "legitimately slow": if a single HEALTHY process_document attempt ever ran 553 # past this, it'd be re-queued mid-flight and two workers could process the doc 554 # concurrently. That's safe ONLY because ingest re-runs are idempotent (uuid5 555 # Qdrant point IDs — see the queue module's "Design notes"), NOT because the 556 # reclaim distinguishes the two — so keep the default comfortably above the longest 557 # real single attempt. OCR batch polling releases the worker between polls 558 # (BatchPending -> retry_in), so a job never legitimately holds ``doing`` this 559 # long; 1800s is deep headroom. (Deck: ingest doing-strand reclaim.) 560 "ingest_doing_max_seconds": 1800, 561 # Delete succeeded ingest jobs (keeps the queue table lean + the KEDA 562 # queue-depth metric clean). Set false to retain succeeded rows for audit 563 # (note: indexing success is also recorded in logs/metrics regardless). 564 "ingest_delete_succeeded_jobs": True, 565 # Whether the procrastinate worker uses LISTEN/NOTIFY for job pickup (Deck 566 # #424). True (default) = near-instant wakeup via a long-lived LISTEN 567 # connection. Set false to run POLL-ONLY when DATABASE_URL routes through a 568 # transaction-mode connection pooler (PgBouncer transaction mode), which is 569 # incompatible with LISTEN/NOTIFY — the LISTEN registration is dropped when 570 # the backend returns to the pool. Poll-only trades a few seconds of pickup 571 # latency (fetch_job_polling_interval) for pooler safety; job queries still 572 # multiplex through the pooler. Snapshotted at worker startup; needs a 573 # restart to change. 574 "ingest_listen_notify": True, 575 # Per-tier escalation on the procrastinate (postgres) ingest path (Deck 576 # #323). When true, a document that a tier cannot parse well is requeued onto 577 # the next tier's queue (fast -> structured -> ocr) via a native procrastinate 578 # queue-hop. When false the ``fast`` tier is terminal -- reproduces the 579 # pre-#323 behaviour where the cheap tier's output is indexed as-is. No effect 580 # on the in-process ``memory`` backend, which keeps the inline escalation. 581 # HOT: re-read per job (process_document_task), so it takes effect on the next 582 # job -- unlike INGEST_TRANSIENT_MAX_ATTEMPTS, which is snapshotted at worker 583 # startup and needs a restart. 584 "ingest_escalation_enabled": True, 585 # Global cap on SAME-tier retries for transient infra errors (doc fetch / 586 # embed / Qdrant blips) on the procrastinate path. Parse-quality failures 587 # escalate (one parse attempt per tier) and do NOT consume this budget; only 588 # whitelisted transient exceptions retry in place. Shared across tiers because 589 # a queue-hop cannot reset a per-tier counter (see TieredEscalationStrategy). 590 # Snapshotted at worker startup (blueprint build); restart to change it. 591 "ingest_transient_max_attempts": 5, 592 # Delay (seconds) before a reclaimed stalled job is re-run. A stall is often 593 # systemic (Qdrant/embedding outage), so reclaiming every crashed job at 594 # now() would thundering-herd a recovering dependency every reclaim tick 595 # (*/5min), bypassing TieredEscalationStrategy's per-job backoff. A small 596 # fixed delay staggers the retry. 0 = immediate (legacy behaviour). 597 "ingest_reclaim_retry_delay_seconds": 30, 598 "collection_metadata_source": "qdrant", # qdrant | api 599 # CP base URL for COLLECTION_METADATA_SOURCE=api (e.g. http://control-plane). 600 # Required only when the source is api. 601 "collection_metadata_api_url": None, 602 "embedding_gateway_url": None, # required when embedding_provider=gateway 603 # Provider-namespaced model the gateway serves, "<provider>/<model>" 604 # (the gateway routes on the "/"-prefix; mistral/mistral-embed → Mistral 605 # for the MVP). Only consulted when embedding_provider=gateway. 606 "embedding_gateway_model": "mistral/mistral-embed", 607 # Gateway auth: the MCP server is an OIDC *client* in the gateway's own 608 # M2M realm (parallel to, and distinct from, the tenant realm it already 609 # serves). It obtains a client-credentials token and the gateway maps the 610 # client-id → the tenant's underlying provider API key. All four unset = 611 # call the gateway unauthenticated (matches today's not-yet-authed gateway). 612 "embedding_gateway_token_url": None, # M2M token endpoint 613 "embedding_gateway_client_id": None, 614 "embedding_gateway_client_secret": None, 615 "embedding_gateway_scope": None, # e.g. astrolabe-embedding-gateway/embed 616 # Query-side ACL pre-filter (design §11). OFF by default: a Qdrant 617 # `match any` on `acl_hash` excludes points missing the key, so enabling 618 # this before a real ACL backfill would silently drop legacy results. 619 # verify-on-read remains the correctness backstop regardless. 620 "acl_prefilter_enabled": False, 621 # Usage metering (Deck #67, control-plane usage-metering.md). OFF by 622 # default so OSS self-hosters don't accrue a metering table or write 623 # overhead; Astrolabe Cloud provisioning sets it true. When on, billable 624 # ops record rows into the app-DB usage_events table (best-effort). 625 "usage_metering_enabled": False, 626 } 627 628 629 def _resolve_settings_files() -> list[str]: 630 """Find optional external settings files. 631 632 Priority: 633 1. NEXTCLOUD_MCP_SETTINGS_FILE env var (absolute or relative path). -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/config.py line 71
- Likely real
- Uncertain, 45% confidence
Possible hardcoded password: 'None'
70 "nextcloud_oidc_client_id": None, 71 "nextcloud_oidc_client_secret": None, 72 "oidc_issuer": None, 73 "jwks_uri": None, 74 "introspection_uri": None, 75 "userinfo_uri": None, 76 "oidc_resource_server_id": None, 77 # M2M / DCR / management — these MUST be declared here (lowercase) so dynaconf 78 # reads the matching UPPERCASE env var; with ignore_unknown_envvars=True an 79 # undeclared key is silently dropped (env value ignored), so they are read 80 # via cfg("ENV_NAME"). Defaults mirror the prior os.getenv(..., default). 81 "mcp_server_client_id": None, 82 "mcp_server_client_secret": None, 83 "allowed_mcp_clients": "", 84 "allowed_mgmt_client": "", 85 # Hosts whose Client ID Metadata Documents the AS proxy trusts (GH #1470). 86 # Empty = CIMD disabled; "*" = any publicly routable host. See auth/cimd.py. 87 "cimd_allowed_hosts": "", 88 "enable_dcr": False, 89 # Container-runtime / webhook self-URL overrides (local-dev docker-compose). 90 "docker_container": False, 91 "nextcloud_mcp_service_name": "mcp", 92 "nextcloud_mcp_port": 8000, 93 # Mode flags 94 # NOTE: `enable_multi_user_basic_auth` and `enable_login_flow` are 95 # intentionally absent — they are derived from MCP_DEPLOYMENT_MODE in 96 # Settings.__post_init__ (ADR-022) and not read from the dynaconf store. 97 # Escape hatch for the per-tool capability gate (capabilities.py). Set 98 # MCP_DISABLE_CAPABILITY_GATING=true to list and run every registered tool 99 # regardless of what the instance advertises — the answer to "why did my 100 # deck tools disappear?" without a code read. Read via cfg(), not a 101 # Settings field: nothing but the two gate helpers looks at it. 102 "mcp_disable_capability_gating": False, 103 "enable_semantic_search": False, 104 "enable_background_operations": False, 105 "vector_sync_enabled": False, 106 "enable_offline_access": False, 107 # Token storage 108 "token_encryption_key": None, 109 # None = ephemeral per-process tempfile (see get_token_db_path()). 110 # Set TOKEN_STORAGE_DB to persist tokens across restarts. 111 "token_storage_db": None, 112 # Centralized backend (any SQLAlchemy URL). Wins over TOKEN_STORAGE_DB 113 # when set. Use postgresql+psycopg://user:pw@host/db for HA k8s 114 # deployments so pods can be stateless. The URL is passed through 115 # verbatim — TLS (e.g. ?sslmode=require) and every other parameter are 116 # read from it by libpq/psycopg; the server never rewrites it. See ADR-026. 117 "database_url": None, 118 # Postgres connection pool sizing (ADR-026 → "Concurrency model and 119 # pool sizing"). Per-pod defaults to 2 + 5 overflow = 7 max 120 # connections. psycopg connections are single-flight, so the pool 121 # only needs to cover typical multi-user MCP burst — not every 122 # potential in-flight tool call. Tune up with DATABASE_POOL_SIZE / 123 # DATABASE_MAX_OVERFLOW for high-traffic prod fleets. 124 "database_pool_size": 2, 125 "database_max_overflow": 5, 126 # Webhook delivery authentication (ADR-010): when set, registrations 127 # tell NC to add `Authorization: Bearer <secret>` to webhook deliveries 128 # and the receiver rejects unauthenticated requests. 129 "webhook_secret": None, 130 # Internal URL override for webhook registration; wins over 131 # NEXTCLOUD_MCP_SERVER_URL when set (e.g. split internal/external URLs). 132 "webhook_internal_url": None, 133 # Vector sync 134 "vector_sync_scan_interval": 300, 135 "vector_sync_processor_workers": 3, 136 # Consecutive failed index attempts (embed / Qdrant / transport — NOT a 137 # parse failure, which is terminal on the first try) before a document is 138 # dead-lettered instead of re-queued forever. See vector/dead_letter.py. 139 "vector_sync_max_index_failures": 5, 140 # Optional per-tier concurrency overrides (None = fall back to 141 # vector_sync_processor_workers). Consumed by the `worker` CLI per --tier. 142 "vector_sync_fast_concurrency": None, 143 "vector_sync_structured_concurrency": None, 144 "vector_sync_queue_max_size": 10000, 145 "vector_sync_metrics_refresh_interval": 20, 146 "vector_density_snapshot_enabled": True, 147 "vector_density_snapshot_interval": 300, 148 "vector_density_snapshot_max_documents": 50000, 149 "usage_stock_snapshot_interval": 86400, 150 "vector_ram_hnsw_overhead_factor": 1.5, 151 "vector_sync_user_poll_interval": 60, 152 "health_ready_refresh_interval": 15, 153 # Orphan-sweep at Pod startup (card #101). When True, delete any 154 # placeholders carrying a different / absent ``instance_id`` before 155 # the scanner's first cycle, so a Pod restart mid-batch doesn't 156 # leave work stuck behind the 5x-scan-interval staleness gate. 157 # Escape hatch only — leave on by default. 158 "vector_sync_orphan_sweep_enabled": True, 159 # System tag that marks files for hybrid (dense + BM25 sparse) indexing. 160 # The scanner indexes files carrying this tag; verify-on-read gates results 161 # on current membership of this tag (ADR-019). 162 "vector_sync_tag": "vector-index", 163 # System tag that marks files for keyword-only (BM25 sparse) indexing. 164 # Defaults to ``keyword-index`` (symmetric with ``vector_sync_tag``), so 165 # a user who creates + applies that tag gets keyword-only indexing out of the 166 # box. Files carrying it are indexed sparse-only (no dense embedding, no 167 # embedding cost) into the SAME collection as hybrid files; ``vector-index`` 168 # wins if a file carries both. Set empty to disable the second tag entirely. 169 "vector_sync_keyword_tag": "keyword-index", 170 # Which MIME types tagged-file discovery will enqueue. None (unset) means 171 # every type a registered processor can read; a comma-separated list 172 # narrows it, and an empty string indexes no files. 173 "vector_sync_indexable_mime_types": None, 174 # Mail tag (an IMAP keyword) restricting which messages are indexed. Empty 175 # (the default) indexes every message in every mailbox, which is the 176 # behaviour before this setting existed. Set it to a tag display name and 177 # only messages carrying that tag are indexed — the mail analogue of 178 # ``vector_sync_tag`` for files. 179 "mail_index_tag": "", 180 # Fail-safe against a flaky/empty tag-discovery read: number of *consecutive* 181 # scan cycles for which a given index mode's tag discovery must return zero 182 # files (while Qdrant still holds indexed points for that mode) before the 183 # scanner believes it and starts deleting. A transient empty read (e.g. a 184 # customer-hosted Nextcloud that intermittently answers the systemtag REPORT 185 # with an empty 207) is thereby treated as a failed read, not "all files 186 # gone" — preventing a delete/re-index churn loop. A sustained empty (a real 187 # mass-untag) still eventually deletes once the streak reaches this value. 188 "vector_sync_empty_discovery_delete_threshold": 3, 189 # Verify-on-read concurrency cap (ADR-019) 190 "verification_concurrency": 20, 191 # Qdrant 192 "qdrant_url": None, 193 "qdrant_location": None, 194 "qdrant_api_key": None, 195 "qdrant_collection": "nextcloud_content", 196 # Ollama 197 "ollama_base_url": None, 198 "ollama_embedding_model": "nomic-embed-text", 199 "ollama_verify_ssl": True, 200 # Ollama embeds a batch serially on whatever hardware it has, so the wall 201 # clock of one /api/embed call scales with the batch's total text, not its 202 # item count. Bound the request by characters (GH #1345). 203 "ollama_embed_max_batch_chars": 16000, 204 "ollama_embed_timeout": 120, 205 # OpenAI 206 "openai_api_key": None, 207 "openai_base_url": None, 208 "openai_embedding_model": "text-embedding-3-small", 209 # Bedrock (AWS) 210 "aws_region": None, 211 "aws_access_key_id": None, 212 "aws_secret_access_key": None, 213 "bedrock_embedding_model": None, 214 # Mistral 215 "mistral_api_key": None, 216 "mistral_embedding_model": "mistral-embed", 217 "mistral_base_url": None, 218 # Matryoshka output width requested from the embedding endpoint (None = 219 # the model's full width). Provider-agnostic: only one provider is active 220 # at a time, so a single knob covers openai/ollama/gateway. 221 "embedding_dimensions": None, 222 # Simple (fallback) embedding dimension 223 "simple_embedding_dimension": 384, 224 # Document chunking 225 "document_chunk_size": 2048, 226 "document_chunk_overlap": 200, 227 # Page-aware chunking for paginated docs (PDFs): split on page boundaries 228 # first so no chunk spans a page (exact page_number, clean snippets, and 229 # predictable ~1 chunk/page when chunk_size >= the largest page). 230 "document_chunk_page_aware": True, 231 # Greedy page-packing (density fix, Deck #636): merge consecutive sub-budget 232 # pages into one chunk instead of one-per-page. Off by default until the 233 # post-change density re-measure + pricing re-calibration land. 234 "document_chunk_page_pack": False, 235 # Hybrid-search RRF ranking constant. Qdrant's native RRF hardcodes k=2, 236 # where adjacent ranks differ by 33% and a single-list outlier outranks a 237 # document both retrievers agree on. 60 is the standard value (~2% per rank), 238 # which restores consensus as the dominant signal. 239 "vector_search_rrf_k": 60, 240 # --- Cross-encoder reranking (opt-in, per request) ---------------------- 241 # A cross-encoder reorders the retrieved candidates by scoring each against 242 # the query directly, which is more accurate than the fusion rank it 243 # replaces. Ships OFF because it adds an upstream round-trip to every 244 # search, and how expensive that is depends on how the reranker is deployed 245 # — which this server deliberately knows nothing about beyond the URL. 246 "search_rerank_enabled": False, 247 # Where reranking is served. A FULL endpoint URL, because the path differs 248 # per backend and there is no safe guess: Infinity serves ``POST /rerank``, 249 # vLLM ``POST /v1/rerank``, Cohere ``POST /v2/rerank``, the Astrolabe 250 # embedding gateway ``POST /v1/rerank``. All four speak the same Cohere 251 # request/response protocol, so choosing one is configuration, not code. 252 # 253 # Unset = derive it from EMBEDDING_GATEWAY_URL, which is what every existing 254 # deployment does. Set it to run reranking WITHOUT a gateway — the reason 255 # this exists (discussion #1354): Ollama serves no rerank models at all, so 256 # a self-hoster's only options are a hosted rerank API or a local Infinity / 257 # vLLM, and neither should require standing up a gateway first. 258 "search_rerank_url": None, 259 # Static bearer for SEARCH_RERANK_URL (a Cohere API key, an Infinity 260 # ``--api-key``). Wins over the gateway's M2M OIDC credentials when both are 261 # present. Unset = send no Authorization header, which is correct for a 262 # local Infinity/vLLM on a private network. 263 "search_rerank_api_key": None, 264 # Rerank model, addressed the way the CONFIGURED ENDPOINT expects — which 265 # differs between the two, so the default is only right for one of them. 266 # 267 # Against the embedding gateway the ``<provider>/`` prefix is REQUIRED, not 268 # stylistic: the gateway splits on the FIRST slash to select a backend, so a 269 # bare ``BAAI/bge-reranker-v2-m3`` asks for a provider named ``BAAI`` and 270 # 503s — and because a failed rerank degrades to retrieval order 271 # (``reranked: false``) rather than erroring, that looks like "reranking does 272 # nothing" instead of like a misconfiguration. The prefix is also the backend 273 # selector: ``local/`` for a self-hosted cross-encoder, ``bedrock/`` or 274 # ``openrouter/`` for a hosted one. 275 # 276 # Against a direct SEARCH_RERANK_URL there is no such routing layer, so use 277 # the bare id the server serves (``BAAI/bge-reranker-v2-m3`` for 278 # Infinity/vLLM, ``rerank-v3.5`` for Cohere). The relevance calibration 279 # (ADR-034) strips known provider prefixes, so both spellings resolve to the 280 # same fitted curve. 281 "search_rerank_model": "local/BAAI/bge-reranker-v2-m3", 282 # Candidates handed to the reranker. Reranking can only reorder what 283 # retrieval supplied, so this depth — not the caller's ``limit`` — bounds 284 # how much a reranker can improve. Reranking just the rows a normal search 285 # returns captures little of the available gain. 286 # 287 # Treat this as a ceiling rather than a starting point. Under 288 # ``granularity="document"`` the grouped prefetch is bounded by 289 # MAX_DOCUMENT_PREFETCH, and requesting more groups than the prefetch can 290 # fill makes Qdrant widen its grouping search and reorder the head of the 291 # result set before the reranker sees it. Going deeper requires raising 292 # MAX_DOCUMENT_PREFETCH first, which carries its own latency cost. 293 "search_rerank_pool_size": 200, 294 # Generous headroom over a normal rerank, not a target. Exceeding it 295 # degrades to retrieval order rather than failing the search. 296 "search_rerank_timeout_seconds": 30.0, 297 # Concurrent rerank calls this process keeps in flight against the reranker. 298 # 299 # Conservative at 1 because reranking is the heaviest request this server 300 # makes of a service it may share with its own embedding traffic and with 301 # other callers. Bounding our own concurrency keeps a burst of searches from 302 # queueing unbounded work upstream, and keeps rerank latency predictable 303 # here. It is a client-side courtesy, not a throughput control: what the 304 # reranker does with the request, and how it schedules against everything 305 # else it serves, is its business. Raise it if yours has headroom — a CPU 306 # cross-encoder almost certainly does not. 307 "search_rerank_max_concurrency": 1, 308 # --- SAR export redaction (ADR-040) ------------------------------------- 309 # Names are detected by the embedding gateway's ``POST /v1/ner``, so SAR 310 # export is available only with EMBEDDING_GATEWAY_URL set. 311 # NER model, addressed the gateway way (``<provider>/<model>``). 312 # Subject access request cases and redacted export (ADR-040). Off by 313 # default: a deployment opts in, since only some need it. Also requires 314 # vector sync and EMBEDDING_GATEWAY_URL (for name detection). 315 "sar_enabled": False, 316 "ner_model": "local/urchade/gliner_multi_pii-v1", 317 # Per-request budget. Export runs in the background, so this only needs to 318 # cover one batch on the slowest backend (CPU GLiNER: ~570 chars/s). 319 "ner_timeout_seconds": 120.0, 320 # Texts (of up to 2,000 chars) per /v1/ner request. Small for CPU GLiNER, 321 # which must finish a batch inside the gateway's own upstream timeout; raise 322 # it (e.g. 32) on a GPU backend. 323 "ner_batch_size": 8, 324 # Minimum model confidence for a span to count as a person. Lower raises 325 # recall at the cost of over-redaction, which is the safe direction for a 326 # disclosure; 0.5 is GLiNER's customary operating point. 327 "ner_threshold": 0.5, 328 # Chunking config generation. Bump whenever chunker behaviour changes (size, 329 # overlap, page-aware, page-pack, split strategy) so the pricing model's 330 # density reference can't silently go stale. Pinned in stripe-catalog.tf. 331 "chunking_config_version": 1, 332 # PDF parse isolation (OOM guard) 333 "document_pdf_graphics_limit": 1000, 334 "document_parse_timeout_seconds": 120.0, 335 # Optional wall-clock cap (seconds) on the SYNCHRONOUS parse inside the 336 # nc_webdav_read_file MCP tool. None (default) = disabled: an interactive read 337 # is bounded only by the underlying processor timeout (DOCLING_TIMEOUT / 338 # DOCUMENT_OCR_TIMEOUT_SECONDS). Set a client-friendly bound (e.g. 45-60s) so a 339 # slow VLM/OCR convert returns base64 quickly instead of blocking past an MCP 340 # client's own timeout. Never applies to the async ingest/worker path (ADR-032). 341 "document_read_timeout_seconds": None, 342 "document_parse_mem_limit_mb": 1536, 343 # Pre-parse size cap (MB): PDFs larger than this fail fast with reason 344 # "oversize" instead of burning the OCR timeout to 0 chars on a pathological 345 # file. 0 disables the guard. 346 "document_max_pdf_size_mb": 50.0, 347 # Pre-flight cap (MB) on nc_webdav_write_file's content: a huge PUT built 348 # from a single in-memory MCP tool argument (no chunked/streaming upload 349 # exists for writes, unlike the read-side stream_to_file path) risks 350 # timing out or exhausting memory rather than failing predictably. 0 351 # disables the guard. 352 "webdav_write_max_mb": 50.0, 353 # Transport security (MCP SDK TransportSecuritySettings). Default False 354 # preserves the behaviour hardcoded before these knobs existed: MCP 1.23+ 355 # auto-enables localhost-only host checking, which breaks k8s/Docker service 356 # DNS names (docs/MCP-1.23-DNS-REBINDING-FIX.md). Enable it plus 357 # MCP_ALLOWED_HOSTS when the server is reachable from a browser. 358 "mcp_dns_rebinding_protection": False, 359 # Comma-separated Host / Origin allowlists, honoured only when the above is 360 # enabled. Empty means "the SDK's own defaults". 361 "mcp_allowed_hosts": "", 362 "mcp_allowed_origins": "", 363 # Comma-separated CORS origin allowlist. "*" preserves today's behaviour. 364 "cors_allow_origins": "*", 365 # Page ceiling for markdown reconstruction (see document_markdown_max_pages). 366 "document_markdown_max_pages": 150, 367 # Pages per pypdfium2 extraction window (see document_parse_page_window). 368 "document_parse_page_window": 100, 369 # Concurrent isolated parse subprocesses (see document_parse_process_slots). 370 "document_parse_process_slots": 2, 371 # Stream ingest downloads to disk instead of buffering them in memory. 372 "document_stream_download_enabled": True, 373 # Directory for ingest spool files (default: the system temp dir). 374 "document_spool_dir": None, 375 # Tier-0 classifier (records classification metrics on the tiered path) 376 "document_classify_enabled": True, 377 # Tiered PDF pipeline: pypdfium2 is the default/only hot-path extractor; 378 # "pymupdf" is a deprecated rollback escape hatch. OCR (tier-3) is the only 379 # escalation target and is off by default (no provider wired yet). 380 "document_tier1_engine": "pypdfium2", 381 "document_ocr_enabled": False, 382 # OCR backend: "auto" picks gateway (if EMBEDDING_GATEWAY_URL) else mistral 383 # (if MISTRAL_API_KEY); "gateway"/"mistral" force one; "docling" routes scanned 384 # PDFs to a docling-serve instance (DOCLING_API_URL); "none" disables. "auto" 385 # never selects docling (it needs a self-hosted URL, so it must be explicit). 386 # The gateway routes on the model's "<provider>/" prefix, so it serves Mistral, 387 # surya (in-cluster GPU over the tailnet), etc. — one configurable OCR tier. 388 "document_ocr_provider": "auto", 389 # Provider-namespaced OCR model id (gateway routes on the prefix; the direct 390 # mistral backend strips it). e.g. "mistral/mistral-ocr-latest" (Mistral) or 391 # "surya/surya-ocr-2" (surya via the gateway) — never hard-coded, only this 392 # default. 393 "document_ocr_model": "mistral/mistral-ocr-latest", 394 # OCR escalation triggers (tier-0). A page is OCR-worthy when its text is 395 # near-empty (< min_page_chars) OR low-quality (< min_text_quality) OR (when 396 # detect_scanned) mostly a raster image; a doc escalates when the OCR-worthy 397 # page fraction reaches page_fraction. Calibrate min_text_quality from the 398 # astrolabe_document_text_quality histogram per tenant. 399 "document_ocr_min_text_quality": 0.5, 400 "document_ocr_page_fraction": 0.5, 401 "document_ocr_min_page_chars": 16, 402 "document_ocr_detect_scanned": True, 403 # Tier-0 glyph-corruption trigger. When the fast (pypdfium2) extraction's 404 # doc-level C0-control-char ratio exceeds this, the text layer is treated as 405 # glyph-corrupt (a broken /ToUnicode mapping leaking raw glyph codes) and the 406 # doc escalates fast->structured (pymupdf re-extracts it correctly -- no OCR). 407 # 0 disables. Clean docs sit ~0; affected PDFs measured ~1-11% in testing. 408 "document_glyph_corruption_ratio": 0.02, 409 # OCR backend request timeout (seconds). Slow scanned newspapers can take 410 # 20-60s; raise/lower per tenant. Configurable so a tenant isn't stuck with 411 # the 180s default when its gateway has its own shorter ceiling. 412 "document_ocr_timeout_seconds": 180.0, 413 # OCR execution mode (Deck #332). "sync" (default) transcribes inline via the 414 # backend's synchronous path. "batch" routes to the gateway's async Batch OCR 415 # job (~50% cheaper, minutes-hours latency) for large-corpus backfill — opt-in 416 # and gateway-routed. It requires EMBEDDING_GATEWAY_URL (rejected at startup 417 # otherwise, see __post_init__) and the per-tier procrastinate path; the 418 # inline/memory pool can't defer a poll, so batch raises there rather than 419 # silently downgrading to sync. 420 "document_ocr_mode": "sync", 421 # Seconds between batch-job polls (the procrastinate re-enqueue delay). Each 422 # poll re-runs the tier; keep it well above a few seconds. 423 "document_ocr_batch_poll_seconds": 120, 424 # Observability 425 "metrics_enabled": True, 426 "metrics_port": 9090, 427 "otel_exporter_otlp_endpoint": None, 428 "otel_exporter_verify_ssl": None, 429 "otel_service_name": "nextcloud-mcp-server", 430 "pyroscope_enabled": False, 431 "pyroscope_server_address": None, 432 # Pod identity from the Kubernetes downward API (chart-injected). Facts 433 # about the pod, not user config — absent off-Kubernetes. 434 "pod_namespace": None, 435 "pod_name": None, 436 "log_format": "text", 437 "log_level": "INFO", 438 "log_include_trace_context": True, 439 # Reverse-proxy trust list for X-Forwarded-For (GH #1284). Passed to 440 # uvicorn; None keeps uvicorn's own resolution (which itself falls back to 441 # 127.0.0.1). Declared here so the key is settable from settings.toml, not 442 # only as the env var uvicorn already reads on its own. 443 "forwarded_allow_ips": None, 444 # Document processing (no master switch: each optional processor has its own 445 # ENABLE_* flag, and parsing on read is the caller's per-call decision) 446 "document_processor": "unstructured", 447 "enable_unstructured": False, 448 "unstructured_api_url": "http://unstructured:8000", 449 "unstructured_timeout": 120, 450 "unstructured_strategy": "auto", 451 "unstructured_languages": "eng,deu", 452 "progress_interval": 10, 453 "enable_tesseract": False, 454 "tesseract_cmd": None, 455 "tesseract_lang": "eng", 456 # pymupdf is the built-in "structured" tier and is always available -- there 457 # is no enable flag for it, only these two knobs. 458 "pymupdf_extract_images": True, 459 "pymupdf_image_dir": None, 460 "enable_custom_processor": False, 461 "custom_processor_url": None, 462 "custom_processor_types": "application/pdf", 463 "custom_processor_name": "custom", 464 "custom_processor_api_key": None, 465 "custom_processor_timeout": 60, 466 # Docling document-parsing backend (docling-serve HTTP API). One docling-serve 467 # instance feeds two touchpoints: the images-only DoclingProcessor 468 # (find_processor path, priority 20) and — when DOCUMENT_OCR_PROVIDER=docling — 469 # the PDF OCR backend for scanned/no-text-layer PDFs. The same processor can be 470 # force-selected per call to parse a text-layer PDF (tables/partial text). URL 471 # unset -> the image processor is not registered and the OCR backend resolves to 472 # None. See ADR-031. 473 "enable_docling": False, 474 "docling_api_url": None, 475 "docling_timeout": 120, 476 # docling-serve OCR language codes. The default engine (EasyOCR) uses 2-letter 477 # codes ("en","de"); a Tesseract-backed instance wants "eng","deu". Engine- 478 # dependent, so keep it operator-tunable (see ADR-031). 479 "docling_ocr_lang": "en,de", 480 # Run OCR on IMAGES routed to the DoclingProcessor (find_processor path). The 481 # docling OCR *backend* (scanned PDFs) always OCRs regardless of this flag. 482 "docling_do_ocr": True, 483 # Which docling-serve pipeline to request: "standard" (classic layout+OCR, 484 # default, unchanged) or "vlm" (Vision-LLM OCR). "vlm" needs a docling-serve 485 # instance configured with VLM presets (see ADR-032). 486 "docling_pipeline": "standard", 487 # VLM preset name sent when docling_pipeline == "vlm". None -> docling-serve 488 # picks its own DOCLING_SERVE_DEFAULT_VLM_PRESET. Preset names are server-defined. 489 "docling_vlm_preset": None, 490 # Caption raster pictures embedded in .pptx/.docx/.xlsx via the same 491 # docling-serve instance (ADR-037). Explicit opt-in beyond a bare 492 # DOCLING_API_URL, like DOCUMENT_OCR_PROVIDER=docling needs its own 493 # selection: a deployment that only wants docling for scanned-PDF OCR 494 # shouldn't start captioning every picture in every document for free. The 495 # native readers have no rendering engine, so this only reaches actual 496 # pictures -- native vector drawings (SmartArt, shapes) are unaffected. 497 "office_caption_images": False, 498 # Cap on pictures captioned per file (one docling-serve round trip each). 499 # nc_webdav_read_file blocks synchronously for the whole parse, so this 500 # bounds the worst case rather than leaving it to the file's picture count. 501 "office_caption_max_images": 8, 502 # Per-picture docling-serve request timeout (seconds). Deliberately short 503 # and independent of DOCLING_TIMEOUT/DOCUMENT_OCR_TIMEOUT_SECONDS (other 504 # touchpoints, other latency profiles): a caption is meant to be a quick 505 # per-picture round trip repeated up to OFFICE_CAPTION_MAX_IMAGES times, not 506 # a single long convert. Raise it if DOCLING_PIPELINE=vlm makes captions 507 # time out (VLM is far slower than the standard pipeline -- see ADR-032). 508 "office_caption_timeout_seconds": 15.0, 509 # Collabora Online (coolwsd) base URL for converting legacy .doc/.xls/.ppt 510 # and ODF .odt/.ods/.odp to OOXML, which the native readers then parse 511 # (ADR-039). Unset = those types are not claimed. coolwsd only answers 512 # convert-to for clients in its net.post_allow list (private ranges by 513 # default). 514 "collabora_url": None, 515 # Per-file convert-to request timeout (seconds). A typical document 516 # converts in well under a second; the headroom is for large workbooks. 517 "collabora_timeout_seconds": 60.0, 518 # Deprecated PPTX_CAPTION_* spellings (0.193.0, pptx-only) of the three keys 519 # above; see _apply_legacy_caption_settings. Declared so dynaconf reads 520 # them at all, with the same defaults so their validators still apply. 521 "pptx_caption_images": False, 522 "pptx_caption_max_images": 8, 523 "pptx_caption_timeout_seconds": 15.0, 524 # Tag-based file exclusion (issue #710): comma-separated list of 525 # Nextcloud system tag names. Files/folders carrying any of these tags 526 # are hidden from WebDAV MCP tools. Empty = feature off. 527 "excluded_tags": "", 528 # MCP decomposition hook points (design §10). Every default reproduces 529 # the current monolithic behavior; self-hosters who set none are 530 # unaffected. See docs/architecture/mcp-decomposition.md (sibling repo). 531 "embedding_provider": "autodetect", # autodetect | gateway 532 # Ingest queue backend (Deck #183). None → ``memory`` (the in-process anyio 533 # queue): procrastinate is strictly opt-in, even on a Postgres DATABASE_URL. 534 # Set ``postgres`` explicitly to split ingest into a procrastinate worker; 535 # that requires a PostgreSQL DATABASE_URL. 536 "ingest_queue": None, # memory | postgres 537 # Process role for the per-tenant two-pod model (Deck #183). ``api`` runs the 538 # MCP/query server + scanner (defers jobs); the ``worker`` role is the 539 # `nextcloud-mcp-server worker` process that drains the queue. ``all`` keeps 540 # the monolithic behaviour (API + in-process SQLite pool). 541 "mcp_role": "all", # api | worker | all 542 # Reclaim an ingest job orphaned in ``doing`` by a crashed worker once its 543 # worker heartbeat is this many seconds stale (Deck #183). Default is well 544 # above the longest expected document; raise it for slow embedding backends. 545 "ingest_stalled_job_seconds": 300, 546 # Backstop reclaim: also re-queue a job stuck in ``doing`` for this many seconds 547 # regardless of worker liveness. The heartbeat threshold above only catches DEAD 548 # workers; a job whose OWN completion crashed (e.g. an unhandled queueing_lock 549 # UniqueViolation on the doing->todo retry) is stranded in ``doing`` under a LIVE, 550 # heart-beating worker and is invisible to the heartbeat sweep. 551 # This fires purely on time-in-``doing``, NOT on liveness, so it can't tell "stuck" 552 # from "legitimately slow": if a single HEALTHY process_document attempt ever ran 553 # past this, it'd be re-queued mid-flight and two workers could process the doc 554 # concurrently. That's safe ONLY because ingest re-runs are idempotent (uuid5 555 # Qdrant point IDs — see the queue module's "Design notes"), NOT because the 556 # reclaim distinguishes the two — so keep the default comfortably above the longest 557 # real single attempt. OCR batch polling releases the worker between polls 558 # (BatchPending -> retry_in), so a job never legitimately holds ``doing`` this 559 # long; 1800s is deep headroom. (Deck: ingest doing-strand reclaim.) 560 "ingest_doing_max_seconds": 1800, 561 # Delete succeeded ingest jobs (keeps the queue table lean + the KEDA 562 # queue-depth metric clean). Set false to retain succeeded rows for audit 563 # (note: indexing success is also recorded in logs/metrics regardless). 564 "ingest_delete_succeeded_jobs": True, 565 # Whether the procrastinate worker uses LISTEN/NOTIFY for job pickup (Deck 566 # #424). True (default) = near-instant wakeup via a long-lived LISTEN 567 # connection. Set false to run POLL-ONLY when DATABASE_URL routes through a 568 # transaction-mode connection pooler (PgBouncer transaction mode), which is 569 # incompatible with LISTEN/NOTIFY — the LISTEN registration is dropped when 570 # the backend returns to the pool. Poll-only trades a few seconds of pickup 571 # latency (fetch_job_polling_interval) for pooler safety; job queries still 572 # multiplex through the pooler. Snapshotted at worker startup; needs a 573 # restart to change. 574 "ingest_listen_notify": True, 575 # Per-tier escalation on the procrastinate (postgres) ingest path (Deck 576 # #323). When true, a document that a tier cannot parse well is requeued onto 577 # the next tier's queue (fast -> structured -> ocr) via a native procrastinate 578 # queue-hop. When false the ``fast`` tier is terminal -- reproduces the 579 # pre-#323 behaviour where the cheap tier's output is indexed as-is. No effect 580 # on the in-process ``memory`` backend, which keeps the inline escalation. 581 # HOT: re-read per job (process_document_task), so it takes effect on the next 582 # job -- unlike INGEST_TRANSIENT_MAX_ATTEMPTS, which is snapshotted at worker 583 # startup and needs a restart. 584 "ingest_escalation_enabled": True, 585 # Global cap on SAME-tier retries for transient infra errors (doc fetch / 586 # embed / Qdrant blips) on the procrastinate path. Parse-quality failures 587 # escalate (one parse attempt per tier) and do NOT consume this budget; only 588 # whitelisted transient exceptions retry in place. Shared across tiers because 589 # a queue-hop cannot reset a per-tier counter (see TieredEscalationStrategy). 590 # Snapshotted at worker startup (blueprint build); restart to change it. 591 "ingest_transient_max_attempts": 5, 592 # Delay (seconds) before a reclaimed stalled job is re-run. A stall is often 593 # systemic (Qdrant/embedding outage), so reclaiming every crashed job at 594 # now() would thundering-herd a recovering dependency every reclaim tick 595 # (*/5min), bypassing TieredEscalationStrategy's per-job backoff. A small 596 # fixed delay staggers the retry. 0 = immediate (legacy behaviour). 597 "ingest_reclaim_retry_delay_seconds": 30, 598 "collection_metadata_source": "qdrant", # qdrant | api 599 # CP base URL for COLLECTION_METADATA_SOURCE=api (e.g. http://control-plane). 600 # Required only when the source is api. 601 "collection_metadata_api_url": None, 602 "embedding_gateway_url": None, # required when embedding_provider=gateway 603 # Provider-namespaced model the gateway serves, "<provider>/<model>" 604 # (the gateway routes on the "/"-prefix; mistral/mistral-embed → Mistral 605 # for the MVP). Only consulted when embedding_provider=gateway. 606 "embedding_gateway_model": "mistral/mistral-embed", 607 # Gateway auth: the MCP server is an OIDC *client* in the gateway's own 608 # M2M realm (parallel to, and distinct from, the tenant realm it already 609 # serves). It obtains a client-credentials token and the gateway maps the 610 # client-id → the tenant's underlying provider API key. All four unset = 611 # call the gateway unauthenticated (matches today's not-yet-authed gateway). 612 "embedding_gateway_token_url": None, # M2M token endpoint 613 "embedding_gateway_client_id": None, 614 "embedding_gateway_client_secret": None, 615 "embedding_gateway_scope": None, # e.g. astrolabe-embedding-gateway/embed 616 # Query-side ACL pre-filter (design §11). OFF by default: a Qdrant 617 # `match any` on `acl_hash` excludes points missing the key, so enabling 618 # this before a real ACL backfill would silently drop legacy results. 619 # verify-on-read remains the correctness backstop regardless. 620 "acl_prefilter_enabled": False, 621 # Usage metering (Deck #67, control-plane usage-metering.md). OFF by 622 # default so OSS self-hosters don't accrue a metering table or write 623 # overhead; Astrolabe Cloud provisioning sets it true. When on, billable 624 # ops record rows into the app-DB usage_events table (best-effort). 625 "usage_metering_enabled": False, 626 } 627 628 629 def _resolve_settings_files() -> list[str]: 630 """Find optional external settings files. 631 632 Priority: 633 1. NEXTCLOUD_MCP_SETTINGS_FILE env var (absolute or relative path). 634 If set but the file does not exist, raise FileNotFoundError — 635 silently falling back to defaults on a typo would be a footgun. 636 .secrets.toml is looked for alongside the explicit file. 637 2. Otherwise ./settings.toml in cwd (for docker / dev workflows), 638 with .secrets.toml also looked for in cwd. 639 640 Returns an empty list if nothing is configured — that's fine, defaults 641 and env vars still apply. 642 """ 643 files: list[str] = [] 644 # A legitimate os.environ read: this runs BEFORE the dynaconf instance is 645 # built (it computes the settings_files list passed to Dynaconf), so it 646 # cannot go through dynaconf — you can't read the location of the settings 647 # file from the settings file. This + the MCP_DEPLOYMENT_MODE env_switcher 648 # are dynaconf's own bootstrap. All config *values* are dynaconf-driven; the 649 # only other reads are the legacy-env deprecation check and 650 # _warn_unknown_env_vars, which inspect env var NAMES, not values. 651 explicit = os.environ.get("NEXTCLOUD_MCP_SETTINGS_FILE") 652 if explicit: 653 p = Path(explicit) 654 if not p.exists(): 655 raise FileNotFoundError( 656 f"NEXTCLOUD_MCP_SETTINGS_FILE points to a file that does " 657 f"not exist: {explicit}" 658 ) 659 files.append(str(p)) 660 secrets = p.parent / ".secrets.toml" -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/config.py line 82
- Likely real
- Uncertain, 45% confidence
Possible hardcoded password: 'None'
81 "mcp_server_client_id": None, 82 "mcp_server_client_secret": None, 83 "allowed_mcp_clients": "", 84 "allowed_mgmt_client": "", 85 # Hosts whose Client ID Metadata Documents the AS proxy trusts (GH #1470). 86 # Empty = CIMD disabled; "*" = any publicly routable host. See auth/cimd.py. 87 "cimd_allowed_hosts": "", 88 "enable_dcr": False, 89 # Container-runtime / webhook self-URL overrides (local-dev docker-compose). 90 "docker_container": False, 91 "nextcloud_mcp_service_name": "mcp", 92 "nextcloud_mcp_port": 8000, 93 # Mode flags 94 # NOTE: `enable_multi_user_basic_auth` and `enable_login_flow` are 95 # intentionally absent — they are derived from MCP_DEPLOYMENT_MODE in 96 # Settings.__post_init__ (ADR-022) and not read from the dynaconf store. 97 # Escape hatch for the per-tool capability gate (capabilities.py). Set 98 # MCP_DISABLE_CAPABILITY_GATING=true to list and run every registered tool 99 # regardless of what the instance advertises — the answer to "why did my 100 # deck tools disappear?" without a code read. Read via cfg(), not a 101 # Settings field: nothing but the two gate helpers looks at it. 102 "mcp_disable_capability_gating": False, 103 "enable_semantic_search": False, 104 "enable_background_operations": False, 105 "vector_sync_enabled": False, 106 "enable_offline_access": False, 107 # Token storage 108 "token_encryption_key": None, 109 # None = ephemeral per-process tempfile (see get_token_db_path()). 110 # Set TOKEN_STORAGE_DB to persist tokens across restarts. 111 "token_storage_db": None, 112 # Centralized backend (any SQLAlchemy URL). Wins over TOKEN_STORAGE_DB 113 # when set. Use postgresql+psycopg://user:pw@host/db for HA k8s 114 # deployments so pods can be stateless. The URL is passed through 115 # verbatim — TLS (e.g. ?sslmode=require) and every other parameter are 116 # read from it by libpq/psycopg; the server never rewrites it. See ADR-026. 117 "database_url": None, 118 # Postgres connection pool sizing (ADR-026 → "Concurrency model and 119 # pool sizing"). Per-pod defaults to 2 + 5 overflow = 7 max 120 # connections. psycopg connections are single-flight, so the pool 121 # only needs to cover typical multi-user MCP burst — not every 122 # potential in-flight tool call. Tune up with DATABASE_POOL_SIZE / 123 # DATABASE_MAX_OVERFLOW for high-traffic prod fleets. 124 "database_pool_size": 2, 125 "database_max_overflow": 5, 126 # Webhook delivery authentication (ADR-010): when set, registrations 127 # tell NC to add `Authorization: Bearer <secret>` to webhook deliveries 128 # and the receiver rejects unauthenticated requests. 129 "webhook_secret": None, 130 # Internal URL override for webhook registration; wins over 131 # NEXTCLOUD_MCP_SERVER_URL when set (e.g. split internal/external URLs). 132 "webhook_internal_url": None, 133 # Vector sync 134 "vector_sync_scan_interval": 300, 135 "vector_sync_processor_workers": 3, 136 # Consecutive failed index attempts (embed / Qdrant / transport — NOT a 137 # parse failure, which is terminal on the first try) before a document is 138 # dead-lettered instead of re-queued forever. See vector/dead_letter.py. 139 "vector_sync_max_index_failures": 5, 140 # Optional per-tier concurrency overrides (None = fall back to 141 # vector_sync_processor_workers). Consumed by the `worker` CLI per --tier. 142 "vector_sync_fast_concurrency": None, 143 "vector_sync_structured_concurrency": None, 144 "vector_sync_queue_max_size": 10000, 145 "vector_sync_metrics_refresh_interval": 20, 146 "vector_density_snapshot_enabled": True, 147 "vector_density_snapshot_interval": 300, 148 "vector_density_snapshot_max_documents": 50000, 149 "usage_stock_snapshot_interval": 86400, 150 "vector_ram_hnsw_overhead_factor": 1.5, 151 "vector_sync_user_poll_interval": 60, 152 "health_ready_refresh_interval": 15, 153 # Orphan-sweep at Pod startup (card #101). When True, delete any 154 # placeholders carrying a different / absent ``instance_id`` before 155 # the scanner's first cycle, so a Pod restart mid-batch doesn't 156 # leave work stuck behind the 5x-scan-interval staleness gate. 157 # Escape hatch only — leave on by default. 158 "vector_sync_orphan_sweep_enabled": True, 159 # System tag that marks files for hybrid (dense + BM25 sparse) indexing. 160 # The scanner indexes files carrying this tag; verify-on-read gates results 161 # on current membership of this tag (ADR-019). 162 "vector_sync_tag": "vector-index", 163 # System tag that marks files for keyword-only (BM25 sparse) indexing. 164 # Defaults to ``keyword-index`` (symmetric with ``vector_sync_tag``), so 165 # a user who creates + applies that tag gets keyword-only indexing out of the 166 # box. Files carrying it are indexed sparse-only (no dense embedding, no 167 # embedding cost) into the SAME collection as hybrid files; ``vector-index`` 168 # wins if a file carries both. Set empty to disable the second tag entirely. 169 "vector_sync_keyword_tag": "keyword-index", 170 # Which MIME types tagged-file discovery will enqueue. None (unset) means 171 # every type a registered processor can read; a comma-separated list 172 # narrows it, and an empty string indexes no files. 173 "vector_sync_indexable_mime_types": None, 174 # Mail tag (an IMAP keyword) restricting which messages are indexed. Empty 175 # (the default) indexes every message in every mailbox, which is the 176 # behaviour before this setting existed. Set it to a tag display name and 177 # only messages carrying that tag are indexed — the mail analogue of 178 # ``vector_sync_tag`` for files. 179 "mail_index_tag": "", 180 # Fail-safe against a flaky/empty tag-discovery read: number of *consecutive* 181 # scan cycles for which a given index mode's tag discovery must return zero 182 # files (while Qdrant still holds indexed points for that mode) before the 183 # scanner believes it and starts deleting. A transient empty read (e.g. a 184 # customer-hosted Nextcloud that intermittently answers the systemtag REPORT 185 # with an empty 207) is thereby treated as a failed read, not "all files 186 # gone" — preventing a delete/re-index churn loop. A sustained empty (a real 187 # mass-untag) still eventually deletes once the streak reaches this value. 188 "vector_sync_empty_discovery_delete_threshold": 3, 189 # Verify-on-read concurrency cap (ADR-019) 190 "verification_concurrency": 20, 191 # Qdrant 192 "qdrant_url": None, 193 "qdrant_location": None, 194 "qdrant_api_key": None, 195 "qdrant_collection": "nextcloud_content", 196 # Ollama 197 "ollama_base_url": None, 198 "ollama_embedding_model": "nomic-embed-text", 199 "ollama_verify_ssl": True, 200 # Ollama embeds a batch serially on whatever hardware it has, so the wall 201 # clock of one /api/embed call scales with the batch's total text, not its 202 # item count. Bound the request by characters (GH #1345). 203 "ollama_embed_max_batch_chars": 16000, 204 "ollama_embed_timeout": 120, 205 # OpenAI 206 "openai_api_key": None, 207 "openai_base_url": None, 208 "openai_embedding_model": "text-embedding-3-small", 209 # Bedrock (AWS) 210 "aws_region": None, 211 "aws_access_key_id": None, 212 "aws_secret_access_key": None, 213 "bedrock_embedding_model": None, 214 # Mistral 215 "mistral_api_key": None, 216 "mistral_embedding_model": "mistral-embed", 217 "mistral_base_url": None, 218 # Matryoshka output width requested from the embedding endpoint (None = 219 # the model's full width). Provider-agnostic: only one provider is active 220 # at a time, so a single knob covers openai/ollama/gateway. 221 "embedding_dimensions": None, 222 # Simple (fallback) embedding dimension 223 "simple_embedding_dimension": 384, 224 # Document chunking 225 "document_chunk_size": 2048, 226 "document_chunk_overlap": 200, 227 # Page-aware chunking for paginated docs (PDFs): split on page boundaries 228 # first so no chunk spans a page (exact page_number, clean snippets, and 229 # predictable ~1 chunk/page when chunk_size >= the largest page). 230 "document_chunk_page_aware": True, 231 # Greedy page-packing (density fix, Deck #636): merge consecutive sub-budget 232 # pages into one chunk instead of one-per-page. Off by default until the 233 # post-change density re-measure + pricing re-calibration land. 234 "document_chunk_page_pack": False, 235 # Hybrid-search RRF ranking constant. Qdrant's native RRF hardcodes k=2, 236 # where adjacent ranks differ by 33% and a single-list outlier outranks a 237 # document both retrievers agree on. 60 is the standard value (~2% per rank), 238 # which restores consensus as the dominant signal. 239 "vector_search_rrf_k": 60, 240 # --- Cross-encoder reranking (opt-in, per request) ---------------------- 241 # A cross-encoder reorders the retrieved candidates by scoring each against 242 # the query directly, which is more accurate than the fusion rank it 243 # replaces. Ships OFF because it adds an upstream round-trip to every 244 # search, and how expensive that is depends on how the reranker is deployed 245 # — which this server deliberately knows nothing about beyond the URL. 246 "search_rerank_enabled": False, 247 # Where reranking is served. A FULL endpoint URL, because the path differs 248 # per backend and there is no safe guess: Infinity serves ``POST /rerank``, 249 # vLLM ``POST /v1/rerank``, Cohere ``POST /v2/rerank``, the Astrolabe 250 # embedding gateway ``POST /v1/rerank``. All four speak the same Cohere 251 # request/response protocol, so choosing one is configuration, not code. 252 # 253 # Unset = derive it from EMBEDDING_GATEWAY_URL, which is what every existing 254 # deployment does. Set it to run reranking WITHOUT a gateway — the reason 255 # this exists (discussion #1354): Ollama serves no rerank models at all, so 256 # a self-hoster's only options are a hosted rerank API or a local Infinity / 257 # vLLM, and neither should require standing up a gateway first. 258 "search_rerank_url": None, 259 # Static bearer for SEARCH_RERANK_URL (a Cohere API key, an Infinity 260 # ``--api-key``). Wins over the gateway's M2M OIDC credentials when both are 261 # present. Unset = send no Authorization header, which is correct for a 262 # local Infinity/vLLM on a private network. 263 "search_rerank_api_key": None, 264 # Rerank model, addressed the way the CONFIGURED ENDPOINT expects — which 265 # differs between the two, so the default is only right for one of them. 266 # 267 # Against the embedding gateway the ``<provider>/`` prefix is REQUIRED, not 268 # stylistic: the gateway splits on the FIRST slash to select a backend, so a 269 # bare ``BAAI/bge-reranker-v2-m3`` asks for a provider named ``BAAI`` and 270 # 503s — and because a failed rerank degrades to retrieval order 271 # (``reranked: false``) rather than erroring, that looks like "reranking does 272 # nothing" instead of like a misconfiguration. The prefix is also the backend 273 # selector: ``local/`` for a self-hosted cross-encoder, ``bedrock/`` or 274 # ``openrouter/`` for a hosted one. 275 # 276 # Against a direct SEARCH_RERANK_URL there is no such routing layer, so use 277 # the bare id the server serves (``BAAI/bge-reranker-v2-m3`` for 278 # Infinity/vLLM, ``rerank-v3.5`` for Cohere). The relevance calibration 279 # (ADR-034) strips known provider prefixes, so both spellings resolve to the 280 # same fitted curve. 281 "search_rerank_model": "local/BAAI/bge-reranker-v2-m3", 282 # Candidates handed to the reranker. Reranking can only reorder what 283 # retrieval supplied, so this depth — not the caller's ``limit`` — bounds 284 # how much a reranker can improve. Reranking just the rows a normal search 285 # returns captures little of the available gain. 286 # 287 # Treat this as a ceiling rather than a starting point. Under 288 # ``granularity="document"`` the grouped prefetch is bounded by 289 # MAX_DOCUMENT_PREFETCH, and requesting more groups than the prefetch can 290 # fill makes Qdrant widen its grouping search and reorder the head of the 291 # result set before the reranker sees it. Going deeper requires raising 292 # MAX_DOCUMENT_PREFETCH first, which carries its own latency cost. 293 "search_rerank_pool_size": 200, 294 # Generous headroom over a normal rerank, not a target. Exceeding it 295 # degrades to retrieval order rather than failing the search. 296 "search_rerank_timeout_seconds": 30.0, 297 # Concurrent rerank calls this process keeps in flight against the reranker. 298 # 299 # Conservative at 1 because reranking is the heaviest request this server 300 # makes of a service it may share with its own embedding traffic and with 301 # other callers. Bounding our own concurrency keeps a burst of searches from 302 # queueing unbounded work upstream, and keeps rerank latency predictable 303 # here. It is a client-side courtesy, not a throughput control: what the 304 # reranker does with the request, and how it schedules against everything 305 # else it serves, is its business. Raise it if yours has headroom — a CPU 306 # cross-encoder almost certainly does not. 307 "search_rerank_max_concurrency": 1, 308 # --- SAR export redaction (ADR-040) ------------------------------------- 309 # Names are detected by the embedding gateway's ``POST /v1/ner``, so SAR 310 # export is available only with EMBEDDING_GATEWAY_URL set. 311 # NER model, addressed the gateway way (``<provider>/<model>``). 312 # Subject access request cases and redacted export (ADR-040). Off by 313 # default: a deployment opts in, since only some need it. Also requires 314 # vector sync and EMBEDDING_GATEWAY_URL (for name detection). 315 "sar_enabled": False, 316 "ner_model": "local/urchade/gliner_multi_pii-v1", 317 # Per-request budget. Export runs in the background, so this only needs to 318 # cover one batch on the slowest backend (CPU GLiNER: ~570 chars/s). 319 "ner_timeout_seconds": 120.0, 320 # Texts (of up to 2,000 chars) per /v1/ner request. Small for CPU GLiNER, 321 # which must finish a batch inside the gateway's own upstream timeout; raise 322 # it (e.g. 32) on a GPU backend. 323 "ner_batch_size": 8, 324 # Minimum model confidence for a span to count as a person. Lower raises 325 # recall at the cost of over-redaction, which is the safe direction for a 326 # disclosure; 0.5 is GLiNER's customary operating point. 327 "ner_threshold": 0.5, 328 # Chunking config generation. Bump whenever chunker behaviour changes (size, 329 # overlap, page-aware, page-pack, split strategy) so the pricing model's 330 # density reference can't silently go stale. Pinned in stripe-catalog.tf. 331 "chunking_config_version": 1, 332 # PDF parse isolation (OOM guard) 333 "document_pdf_graphics_limit": 1000, 334 "document_parse_timeout_seconds": 120.0, 335 # Optional wall-clock cap (seconds) on the SYNCHRONOUS parse inside the 336 # nc_webdav_read_file MCP tool. None (default) = disabled: an interactive read 337 # is bounded only by the underlying processor timeout (DOCLING_TIMEOUT / 338 # DOCUMENT_OCR_TIMEOUT_SECONDS). Set a client-friendly bound (e.g. 45-60s) so a 339 # slow VLM/OCR convert returns base64 quickly instead of blocking past an MCP 340 # client's own timeout. Never applies to the async ingest/worker path (ADR-032). 341 "document_read_timeout_seconds": None, 342 "document_parse_mem_limit_mb": 1536, 343 # Pre-parse size cap (MB): PDFs larger than this fail fast with reason 344 # "oversize" instead of burning the OCR timeout to 0 chars on a pathological 345 # file. 0 disables the guard. 346 "document_max_pdf_size_mb": 50.0, 347 # Pre-flight cap (MB) on nc_webdav_write_file's content: a huge PUT built 348 # from a single in-memory MCP tool argument (no chunked/streaming upload 349 # exists for writes, unlike the read-side stream_to_file path) risks 350 # timing out or exhausting memory rather than failing predictably. 0 351 # disables the guard. 352 "webdav_write_max_mb": 50.0, 353 # Transport security (MCP SDK TransportSecuritySettings). Default False 354 # preserves the behaviour hardcoded before these knobs existed: MCP 1.23+ 355 # auto-enables localhost-only host checking, which breaks k8s/Docker service 356 # DNS names (docs/MCP-1.23-DNS-REBINDING-FIX.md). Enable it plus 357 # MCP_ALLOWED_HOSTS when the server is reachable from a browser. 358 "mcp_dns_rebinding_protection": False, 359 # Comma-separated Host / Origin allowlists, honoured only when the above is 360 # enabled. Empty means "the SDK's own defaults". 361 "mcp_allowed_hosts": "", 362 "mcp_allowed_origins": "", 363 # Comma-separated CORS origin allowlist. "*" preserves today's behaviour. 364 "cors_allow_origins": "*", 365 # Page ceiling for markdown reconstruction (see document_markdown_max_pages). 366 "document_markdown_max_pages": 150, 367 # Pages per pypdfium2 extraction window (see document_parse_page_window). 368 "document_parse_page_window": 100, 369 # Concurrent isolated parse subprocesses (see document_parse_process_slots). 370 "document_parse_process_slots": 2, 371 # Stream ingest downloads to disk instead of buffering them in memory. 372 "document_stream_download_enabled": True, 373 # Directory for ingest spool files (default: the system temp dir). 374 "document_spool_dir": None, 375 # Tier-0 classifier (records classification metrics on the tiered path) 376 "document_classify_enabled": True, 377 # Tiered PDF pipeline: pypdfium2 is the default/only hot-path extractor; 378 # "pymupdf" is a deprecated rollback escape hatch. OCR (tier-3) is the only 379 # escalation target and is off by default (no provider wired yet). 380 "document_tier1_engine": "pypdfium2", 381 "document_ocr_enabled": False, 382 # OCR backend: "auto" picks gateway (if EMBEDDING_GATEWAY_URL) else mistral 383 # (if MISTRAL_API_KEY); "gateway"/"mistral" force one; "docling" routes scanned 384 # PDFs to a docling-serve instance (DOCLING_API_URL); "none" disables. "auto" 385 # never selects docling (it needs a self-hosted URL, so it must be explicit). 386 # The gateway routes on the model's "<provider>/" prefix, so it serves Mistral, 387 # surya (in-cluster GPU over the tailnet), etc. — one configurable OCR tier. 388 "document_ocr_provider": "auto", 389 # Provider-namespaced OCR model id (gateway routes on the prefix; the direct 390 # mistral backend strips it). e.g. "mistral/mistral-ocr-latest" (Mistral) or 391 # "surya/surya-ocr-2" (surya via the gateway) — never hard-coded, only this 392 # default. 393 "document_ocr_model": "mistral/mistral-ocr-latest", 394 # OCR escalation triggers (tier-0). A page is OCR-worthy when its text is 395 # near-empty (< min_page_chars) OR low-quality (< min_text_quality) OR (when 396 # detect_scanned) mostly a raster image; a doc escalates when the OCR-worthy 397 # page fraction reaches page_fraction. Calibrate min_text_quality from the 398 # astrolabe_document_text_quality histogram per tenant. 399 "document_ocr_min_text_quality": 0.5, 400 "document_ocr_page_fraction": 0.5, 401 "document_ocr_min_page_chars": 16, 402 "document_ocr_detect_scanned": True, 403 # Tier-0 glyph-corruption trigger. When the fast (pypdfium2) extraction's 404 # doc-level C0-control-char ratio exceeds this, the text layer is treated as 405 # glyph-corrupt (a broken /ToUnicode mapping leaking raw glyph codes) and the 406 # doc escalates fast->structured (pymupdf re-extracts it correctly -- no OCR). 407 # 0 disables. Clean docs sit ~0; affected PDFs measured ~1-11% in testing. 408 "document_glyph_corruption_ratio": 0.02, 409 # OCR backend request timeout (seconds). Slow scanned newspapers can take 410 # 20-60s; raise/lower per tenant. Configurable so a tenant isn't stuck with 411 # the 180s default when its gateway has its own shorter ceiling. 412 "document_ocr_timeout_seconds": 180.0, 413 # OCR execution mode (Deck #332). "sync" (default) transcribes inline via the 414 # backend's synchronous path. "batch" routes to the gateway's async Batch OCR 415 # job (~50% cheaper, minutes-hours latency) for large-corpus backfill — opt-in 416 # and gateway-routed. It requires EMBEDDING_GATEWAY_URL (rejected at startup 417 # otherwise, see __post_init__) and the per-tier procrastinate path; the 418 # inline/memory pool can't defer a poll, so batch raises there rather than 419 # silently downgrading to sync. 420 "document_ocr_mode": "sync", 421 # Seconds between batch-job polls (the procrastinate re-enqueue delay). Each 422 # poll re-runs the tier; keep it well above a few seconds. 423 "document_ocr_batch_poll_seconds": 120, 424 # Observability 425 "metrics_enabled": True, 426 "metrics_port": 9090, 427 "otel_exporter_otlp_endpoint": None, 428 "otel_exporter_verify_ssl": None, 429 "otel_service_name": "nextcloud-mcp-server", 430 "pyroscope_enabled": False, 431 "pyroscope_server_address": None, 432 # Pod identity from the Kubernetes downward API (chart-injected). Facts 433 # about the pod, not user config — absent off-Kubernetes. 434 "pod_namespace": None, 435 "pod_name": None, 436 "log_format": "text", 437 "log_level": "INFO", 438 "log_include_trace_context": True, 439 # Reverse-proxy trust list for X-Forwarded-For (GH #1284). Passed to 440 # uvicorn; None keeps uvicorn's own resolution (which itself falls back to 441 # 127.0.0.1). Declared here so the key is settable from settings.toml, not 442 # only as the env var uvicorn already reads on its own. 443 "forwarded_allow_ips": None, 444 # Document processing (no master switch: each optional processor has its own 445 # ENABLE_* flag, and parsing on read is the caller's per-call decision) 446 "document_processor": "unstructured", 447 "enable_unstructured": False, 448 "unstructured_api_url": "http://unstructured:8000", 449 "unstructured_timeout": 120, 450 "unstructured_strategy": "auto", 451 "unstructured_languages": "eng,deu", 452 "progress_interval": 10, 453 "enable_tesseract": False, 454 "tesseract_cmd": None, 455 "tesseract_lang": "eng", 456 # pymupdf is the built-in "structured" tier and is always available -- there 457 # is no enable flag for it, only these two knobs. 458 "pymupdf_extract_images": True, 459 "pymupdf_image_dir": None, 460 "enable_custom_processor": False, 461 "custom_processor_url": None, 462 "custom_processor_types": "application/pdf", 463 "custom_processor_name": "custom", 464 "custom_processor_api_key": None, 465 "custom_processor_timeout": 60, 466 # Docling document-parsing backend (docling-serve HTTP API). One docling-serve 467 # instance feeds two touchpoints: the images-only DoclingProcessor 468 # (find_processor path, priority 20) and — when DOCUMENT_OCR_PROVIDER=docling — 469 # the PDF OCR backend for scanned/no-text-layer PDFs. The same processor can be 470 # force-selected per call to parse a text-layer PDF (tables/partial text). URL 471 # unset -> the image processor is not registered and the OCR backend resolves to 472 # None. See ADR-031. 473 "enable_docling": False, 474 "docling_api_url": None, 475 "docling_timeout": 120, 476 # docling-serve OCR language codes. The default engine (EasyOCR) uses 2-letter 477 # codes ("en","de"); a Tesseract-backed instance wants "eng","deu". Engine- 478 # dependent, so keep it operator-tunable (see ADR-031). 479 "docling_ocr_lang": "en,de", 480 # Run OCR on IMAGES routed to the DoclingProcessor (find_processor path). The 481 # docling OCR *backend* (scanned PDFs) always OCRs regardless of this flag. 482 "docling_do_ocr": True, 483 # Which docling-serve pipeline to request: "standard" (classic layout+OCR, 484 # default, unchanged) or "vlm" (Vision-LLM OCR). "vlm" needs a docling-serve 485 # instance configured with VLM presets (see ADR-032). 486 "docling_pipeline": "standard", 487 # VLM preset name sent when docling_pipeline == "vlm". None -> docling-serve 488 # picks its own DOCLING_SERVE_DEFAULT_VLM_PRESET. Preset names are server-defined. 489 "docling_vlm_preset": None, 490 # Caption raster pictures embedded in .pptx/.docx/.xlsx via the same 491 # docling-serve instance (ADR-037). Explicit opt-in beyond a bare 492 # DOCLING_API_URL, like DOCUMENT_OCR_PROVIDER=docling needs its own 493 # selection: a deployment that only wants docling for scanned-PDF OCR 494 # shouldn't start captioning every picture in every document for free. The 495 # native readers have no rendering engine, so this only reaches actual 496 # pictures -- native vector drawings (SmartArt, shapes) are unaffected. 497 "office_caption_images": False, 498 # Cap on pictures captioned per file (one docling-serve round trip each). 499 # nc_webdav_read_file blocks synchronously for the whole parse, so this 500 # bounds the worst case rather than leaving it to the file's picture count. 501 "office_caption_max_images": 8, 502 # Per-picture docling-serve request timeout (seconds). Deliberately short 503 # and independent of DOCLING_TIMEOUT/DOCUMENT_OCR_TIMEOUT_SECONDS (other 504 # touchpoints, other latency profiles): a caption is meant to be a quick 505 # per-picture round trip repeated up to OFFICE_CAPTION_MAX_IMAGES times, not 506 # a single long convert. Raise it if DOCLING_PIPELINE=vlm makes captions 507 # time out (VLM is far slower than the standard pipeline -- see ADR-032). 508 "office_caption_timeout_seconds": 15.0, 509 # Collabora Online (coolwsd) base URL for converting legacy .doc/.xls/.ppt 510 # and ODF .odt/.ods/.odp to OOXML, which the native readers then parse 511 # (ADR-039). Unset = those types are not claimed. coolwsd only answers 512 # convert-to for clients in its net.post_allow list (private ranges by 513 # default). 514 "collabora_url": None, 515 # Per-file convert-to request timeout (seconds). A typical document 516 # converts in well under a second; the headroom is for large workbooks. 517 "collabora_timeout_seconds": 60.0, 518 # Deprecated PPTX_CAPTION_* spellings (0.193.0, pptx-only) of the three keys 519 # above; see _apply_legacy_caption_settings. Declared so dynaconf reads 520 # them at all, with the same defaults so their validators still apply. 521 "pptx_caption_images": False, 522 "pptx_caption_max_images": 8, 523 "pptx_caption_timeout_seconds": 15.0, 524 # Tag-based file exclusion (issue #710): comma-separated list of 525 # Nextcloud system tag names. Files/folders carrying any of these tags 526 # are hidden from WebDAV MCP tools. Empty = feature off. 527 "excluded_tags": "", 528 # MCP decomposition hook points (design §10). Every default reproduces 529 # the current monolithic behavior; self-hosters who set none are 530 # unaffected. See docs/architecture/mcp-decomposition.md (sibling repo). 531 "embedding_provider": "autodetect", # autodetect | gateway 532 # Ingest queue backend (Deck #183). None → ``memory`` (the in-process anyio 533 # queue): procrastinate is strictly opt-in, even on a Postgres DATABASE_URL. 534 # Set ``postgres`` explicitly to split ingest into a procrastinate worker; 535 # that requires a PostgreSQL DATABASE_URL. 536 "ingest_queue": None, # memory | postgres 537 # Process role for the per-tenant two-pod model (Deck #183). ``api`` runs the 538 # MCP/query server + scanner (defers jobs); the ``worker`` role is the 539 # `nextcloud-mcp-server worker` process that drains the queue. ``all`` keeps 540 # the monolithic behaviour (API + in-process SQLite pool). 541 "mcp_role": "all", # api | worker | all 542 # Reclaim an ingest job orphaned in ``doing`` by a crashed worker once its 543 # worker heartbeat is this many seconds stale (Deck #183). Default is well 544 # above the longest expected document; raise it for slow embedding backends. 545 "ingest_stalled_job_seconds": 300, 546 # Backstop reclaim: also re-queue a job stuck in ``doing`` for this many seconds 547 # regardless of worker liveness. The heartbeat threshold above only catches DEAD 548 # workers; a job whose OWN completion crashed (e.g. an unhandled queueing_lock 549 # UniqueViolation on the doing->todo retry) is stranded in ``doing`` under a LIVE, 550 # heart-beating worker and is invisible to the heartbeat sweep. 551 # This fires purely on time-in-``doing``, NOT on liveness, so it can't tell "stuck" 552 # from "legitimately slow": if a single HEALTHY process_document attempt ever ran 553 # past this, it'd be re-queued mid-flight and two workers could process the doc 554 # concurrently. That's safe ONLY because ingest re-runs are idempotent (uuid5 555 # Qdrant point IDs — see the queue module's "Design notes"), NOT because the 556 # reclaim distinguishes the two — so keep the default comfortably above the longest 557 # real single attempt. OCR batch polling releases the worker between polls 558 # (BatchPending -> retry_in), so a job never legitimately holds ``doing`` this 559 # long; 1800s is deep headroom. (Deck: ingest doing-strand reclaim.) 560 "ingest_doing_max_seconds": 1800, 561 # Delete succeeded ingest jobs (keeps the queue table lean + the KEDA 562 # queue-depth metric clean). Set false to retain succeeded rows for audit 563 # (note: indexing success is also recorded in logs/metrics regardless). 564 "ingest_delete_succeeded_jobs": True, 565 # Whether the procrastinate worker uses LISTEN/NOTIFY for job pickup (Deck 566 # #424). True (default) = near-instant wakeup via a long-lived LISTEN 567 # connection. Set false to run POLL-ONLY when DATABASE_URL routes through a 568 # transaction-mode connection pooler (PgBouncer transaction mode), which is 569 # incompatible with LISTEN/NOTIFY — the LISTEN registration is dropped when 570 # the backend returns to the pool. Poll-only trades a few seconds of pickup 571 # latency (fetch_job_polling_interval) for pooler safety; job queries still 572 # multiplex through the pooler. Snapshotted at worker startup; needs a 573 # restart to change. 574 "ingest_listen_notify": True, 575 # Per-tier escalation on the procrastinate (postgres) ingest path (Deck 576 # #323). When true, a document that a tier cannot parse well is requeued onto 577 # the next tier's queue (fast -> structured -> ocr) via a native procrastinate 578 # queue-hop. When false the ``fast`` tier is terminal -- reproduces the 579 # pre-#323 behaviour where the cheap tier's output is indexed as-is. No effect 580 # on the in-process ``memory`` backend, which keeps the inline escalation. 581 # HOT: re-read per job (process_document_task), so it takes effect on the next 582 # job -- unlike INGEST_TRANSIENT_MAX_ATTEMPTS, which is snapshotted at worker 583 # startup and needs a restart. 584 "ingest_escalation_enabled": True, 585 # Global cap on SAME-tier retries for transient infra errors (doc fetch / 586 # embed / Qdrant blips) on the procrastinate path. Parse-quality failures 587 # escalate (one parse attempt per tier) and do NOT consume this budget; only 588 # whitelisted transient exceptions retry in place. Shared across tiers because 589 # a queue-hop cannot reset a per-tier counter (see TieredEscalationStrategy). 590 # Snapshotted at worker startup (blueprint build); restart to change it. 591 "ingest_transient_max_attempts": 5, 592 # Delay (seconds) before a reclaimed stalled job is re-run. A stall is often 593 # systemic (Qdrant/embedding outage), so reclaiming every crashed job at 594 # now() would thundering-herd a recovering dependency every reclaim tick 595 # (*/5min), bypassing TieredEscalationStrategy's per-job backoff. A small 596 # fixed delay staggers the retry. 0 = immediate (legacy behaviour). 597 "ingest_reclaim_retry_delay_seconds": 30, 598 "collection_metadata_source": "qdrant", # qdrant | api 599 # CP base URL for COLLECTION_METADATA_SOURCE=api (e.g. http://control-plane). 600 # Required only when the source is api. 601 "collection_metadata_api_url": None, 602 "embedding_gateway_url": None, # required when embedding_provider=gateway 603 # Provider-namespaced model the gateway serves, "<provider>/<model>" 604 # (the gateway routes on the "/"-prefix; mistral/mistral-embed → Mistral 605 # for the MVP). Only consulted when embedding_provider=gateway. 606 "embedding_gateway_model": "mistral/mistral-embed", 607 # Gateway auth: the MCP server is an OIDC *client* in the gateway's own 608 # M2M realm (parallel to, and distinct from, the tenant realm it already 609 # serves). It obtains a client-credentials token and the gateway maps the 610 # client-id → the tenant's underlying provider API key. All four unset = 611 # call the gateway unauthenticated (matches today's not-yet-authed gateway). 612 "embedding_gateway_token_url": None, # M2M token endpoint 613 "embedding_gateway_client_id": None, 614 "embedding_gateway_client_secret": None, 615 "embedding_gateway_scope": None, # e.g. astrolabe-embedding-gateway/embed 616 # Query-side ACL pre-filter (design §11). OFF by default: a Qdrant 617 # `match any` on `acl_hash` excludes points missing the key, so enabling 618 # this before a real ACL backfill would silently drop legacy results. 619 # verify-on-read remains the correctness backstop regardless. 620 "acl_prefilter_enabled": False, 621 # Usage metering (Deck #67, control-plane usage-metering.md). OFF by 622 # default so OSS self-hosters don't accrue a metering table or write 623 # overhead; Astrolabe Cloud provisioning sets it true. When on, billable 624 # ops record rows into the app-DB usage_events table (best-effort). 625 "usage_metering_enabled": False, 626 } 627 628 629 def _resolve_settings_files() -> list[str]: 630 """Find optional external settings files. 631 632 Priority: 633 1. NEXTCLOUD_MCP_SETTINGS_FILE env var (absolute or relative path). 634 If set but the file does not exist, raise FileNotFoundError — 635 silently falling back to defaults on a typo would be a footgun. 636 .secrets.toml is looked for alongside the explicit file. 637 2. Otherwise ./settings.toml in cwd (for docker / dev workflows), 638 with .secrets.toml also looked for in cwd. 639 640 Returns an empty list if nothing is configured — that's fine, defaults 641 and env vars still apply. 642 """ 643 files: list[str] = [] 644 # A legitimate os.environ read: this runs BEFORE the dynaconf instance is 645 # built (it computes the settings_files list passed to Dynaconf), so it 646 # cannot go through dynaconf — you can't read the location of the settings 647 # file from the settings file. This + the MCP_DEPLOYMENT_MODE env_switcher 648 # are dynaconf's own bootstrap. All config *values* are dynaconf-driven; the 649 # only other reads are the legacy-env deprecation check and 650 # _warn_unknown_env_vars, which inspect env var NAMES, not values. 651 explicit = os.environ.get("NEXTCLOUD_MCP_SETTINGS_FILE") 652 if explicit: 653 p = Path(explicit) 654 if not p.exists(): 655 raise FileNotFoundError( 656 f"NEXTCLOUD_MCP_SETTINGS_FILE points to a file that does " 657 f"not exist: {explicit}" 658 ) 659 files.append(str(p)) 660 secrets = p.parent / ".secrets.toml" 661 else: 662 cwd_settings = Path.cwd() / "settings.toml" 663 if cwd_settings.exists(): 664 files.append(str(cwd_settings)) 665 secrets = Path.cwd() / ".secrets.toml" 666 if secrets.exists(): 667 files.append(str(secrets)) 668 return files 669 670 671 # Dynaconf instance — env vars always win (12-factor). Settings files are -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/config.py line 614
- Likely real
- Uncertain, 45% confidence
Possible hardcoded password: 'None'
613 "embedding_gateway_client_id": None, 614 "embedding_gateway_client_secret": None, 615 "embedding_gateway_scope": None, # e.g. astrolabe-embedding-gateway/embed 616 # Query-side ACL pre-filter (design §11). OFF by default: a Qdrant 617 # `match any` on `acl_hash` excludes points missing the key, so enabling 618 # this before a real ACL backfill would silently drop legacy results. 619 # verify-on-read remains the correctness backstop regardless. 620 "acl_prefilter_enabled": False, 621 # Usage metering (Deck #67, control-plane usage-metering.md). OFF by 622 # default so OSS self-hosters don't accrue a metering table or write 623 # overhead; Astrolabe Cloud provisioning sets it true. When on, billable 624 # ops record rows into the app-DB usage_events table (best-effort). 625 "usage_metering_enabled": False, 626 } 627 628 629 def _resolve_settings_files() -> list[str]: 630 """Find optional external settings files. 631 632 Priority: 633 1. NEXTCLOUD_MCP_SETTINGS_FILE env var (absolute or relative path). 634 If set but the file does not exist, raise FileNotFoundError — 635 silently falling back to defaults on a typo would be a footgun. 636 .secrets.toml is looked for alongside the explicit file. 637 2. Otherwise ./settings.toml in cwd (for docker / dev workflows), 638 with .secrets.toml also looked for in cwd. 639 640 Returns an empty list if nothing is configured — that's fine, defaults 641 and env vars still apply. 642 """ 643 files: list[str] = [] 644 # A legitimate os.environ read: this runs BEFORE the dynaconf instance is 645 # built (it computes the settings_files list passed to Dynaconf), so it 646 # cannot go through dynaconf — you can't read the location of the settings 647 # file from the settings file. This + the MCP_DEPLOYMENT_MODE env_switcher 648 # are dynaconf's own bootstrap. All config *values* are dynaconf-driven; the 649 # only other reads are the legacy-env deprecation check and 650 # _warn_unknown_env_vars, which inspect env var NAMES, not values. 651 explicit = os.environ.get("NEXTCLOUD_MCP_SETTINGS_FILE") 652 if explicit: 653 p = Path(explicit) 654 if not p.exists(): 655 raise FileNotFoundError( 656 f"NEXTCLOUD_MCP_SETTINGS_FILE points to a file that does " 657 f"not exist: {explicit}" 658 ) 659 files.append(str(p)) 660 secrets = p.parent / ".secrets.toml" 661 else: 662 cwd_settings = Path.cwd() / "settings.toml" 663 if cwd_settings.exists(): 664 files.append(str(cwd_settings)) 665 secrets = Path.cwd() / ".secrets.toml" 666 if secrets.exists(): 667 files.append(str(secrets)) 668 return files 669 670 671 # Dynaconf instance — env vars always win (12-factor). Settings files are 672 # optional; when absent the defaults above provide the full key schema so 673 # env vars still override correctly. See ADR-024/025 for architecture. 674 _dynaconf = Dynaconf( 675 settings_files=_resolve_settings_files(), 676 environments=True, 677 envvar_prefix=False, 678 env_switcher="MCP_DEPLOYMENT_MODE", 679 ignore_unknown_envvars=True, 680 load_dotenv=False, 681 **_DEFAULTS, 682 validators=[ 683 # Port ranges 684 Validator("METRICS_PORT", gte=1, lte=65535), 685 # Positive integers 686 Validator("INGEST_STALLED_JOB_SECONDS", gte=1), 687 Validator("INGEST_TRANSIENT_MAX_ATTEMPTS", gte=1), 688 Validator("INGEST_RECLAIM_RETRY_DELAY_SECONDS", gte=0), 689 Validator("OIDC_DISCOVERY_MAX_ATTEMPTS", gte=1), 690 Validator("OIDC_DISCOVERY_BACKOFF_BASE", gte=0), 691 Validator("OIDC_DISCOVERY_BACKOFF_MAX", gte=0), 692 Validator("OFFICE_CAPTION_MAX_IMAGES", gte=0), 693 Validator("OFFICE_CAPTION_TIMEOUT_SECONDS", gt=0), 694 Validator("COLLABORA_TIMEOUT_SECONDS", gt=0), 695 Validator("PPTX_CAPTION_MAX_IMAGES", gte=0), 696 Validator("PPTX_CAPTION_TIMEOUT_SECONDS", gt=0), 697 Validator("QDRANT_INIT_MAX_ATTEMPTS", gte=1), 698 Validator("QDRANT_INIT_BACKOFF_BASE", gte=0), 699 Validator("QDRANT_INIT_BACKOFF_MAX", gte=0), 700 Validator("OLLAMA_EMBED_MAX_BATCH_CHARS", gte=1), 701 Validator("OLLAMA_EMBED_TIMEOUT", gte=1), 702 Validator("VECTOR_SYNC_SCAN_INTERVAL", gte=1), 703 Validator("VECTOR_SYNC_PROCESSOR_WORKERS", gte=1), 704 # 1 = park on the first exhausted-retry round. 0/negative would park a 705 # document before any attempt was recorded. 706 Validator("VECTOR_SYNC_MAX_INDEX_FAILURES", gte=1), 707 # Optional per-tier concurrency overrides (None = fall back to 708 # VECTOR_SYNC_PROCESSOR_WORKERS). Like the sibling above they must be 709 # >=1 when set — a 0/negative value would otherwise reach 710 # app.run_worker_async(concurrency=...) and fail with an opaque runtime 711 # error instead of a clear config error at startup. 712 Validator( 713 "VECTOR_SYNC_FAST_CONCURRENCY", 714 condition=lambda v: v is None or v >= 1, 715 messages={ 716 "condition": "VECTOR_SYNC_FAST_CONCURRENCY must be >= 1 when set" 717 }, 718 ), 719 Validator( 720 "VECTOR_SYNC_STRUCTURED_CONCURRENCY", 721 condition=lambda v: v is None or v >= 1, 722 messages={ 723 "condition": "VECTOR_SYNC_STRUCTURED_CONCURRENCY must be >= 1 when set" 724 }, 725 ), 726 Validator("VECTOR_SYNC_QUEUE_MAX_SIZE", gte=1), 727 Validator("VECTOR_SYNC_METRICS_REFRESH_INTERVAL", gte=1), 728 Validator("VECTOR_DENSITY_SNAPSHOT_INTERVAL", gte=1), 729 Validator("VECTOR_DENSITY_SNAPSHOT_MAX_DOCUMENTS", gte=1), 730 Validator("VECTOR_RAM_HNSW_OVERHEAD_FACTOR", gte=1), 731 Validator("VECTOR_SYNC_USER_POLL_INTERVAL", gte=1), 732 Validator("HEALTH_READY_REFRESH_INTERVAL", gte=1), 733 Validator("PORT", gte=1, lte=65535), 734 Validator("VERIFICATION_CONCURRENCY", gte=1), 735 Validator("DOCUMENT_CHUNK_SIZE", gte=1), 736 Validator("CHUNKING_CONFIG_VERSION", gte=1), 737 Validator("DOCUMENT_PARSE_TIMEOUT_SECONDS", gte=1), 738 Validator("DOCUMENT_OCR_TIMEOUT_SECONDS", gte=1), 739 # DOCUMENT_OCR_MODE is normalised + membership-checked in 740 # Settings.__post_init__ via _enum_fields (case-insensitive, like 741 # DOCUMENT_OCR_PROVIDER) — no strict dynaconf Validator here, so 742 # "Batch"/"SYNC" normalise instead of erroring. 743 # Poll cadence well above a few seconds (each poll re-runs the tier). 744 Validator("DOCUMENT_OCR_BATCH_POLL_SECONDS", gte=5), 745 Validator("DOCUMENT_PARSE_MEM_LIMIT_MB", gte=128), 746 # 0 disables the pre-parse PDF size cap; otherwise it must be positive. 747 Validator("DOCUMENT_MAX_PDF_SIZE_MB", gte=0), 748 # 0 disables page-windowed extraction; otherwise it must be positive. 749 Validator("DOCUMENT_PARSE_PAGE_WINDOW", gte=0), 750 # 0 disables markdown reconstruction; otherwise it must be positive. A 751 # negative value must fail fast rather than silently disable markdown 752 # fleet-wide -- the same silent-disarm class this setting exists to fix. 753 Validator("DOCUMENT_MARKDOWN_MAX_PAGES", gte=0), 754 # At least one parse must be able to run. 755 Validator("DOCUMENT_PARSE_PROCESS_SLOTS", gte=1), 756 # >=1: pymupdf4llm treats graphics_limit=0 as "no cap", which would 757 # re-expose the OOM this guards against. 758 Validator("DOCUMENT_PDF_GRAPHICS_LIMIT", gte=1), 759 # OCR escalation thresholds: quality + page-fraction are [0, 1]. 760 Validator("DOCUMENT_OCR_MIN_TEXT_QUALITY", gte=0, lte=1), 761 Validator("DOCUMENT_OCR_PAGE_FRACTION", gte=0, lte=1), 762 Validator("DOCUMENT_OCR_MIN_PAGE_CHARS", gte=0), 763 Validator("DOCUMENT_GLYPH_CORRUPTION_RATIO", gte=0, lte=1), 764 # Non-negative 765 Validator("DOCUMENT_CHUNK_OVERLAP", gte=0), 766 # RRF ranking constant. Must be >= 1: the fused score is 767 # 1/(rank + k) with rank 0-indexed, so k=0 divides by zero on the 768 # top-ranked point. 769 Validator("VECTOR_SEARCH_RRF_K", gte=1), 770 # Matryoshka truncation width. Optional (None = the model's full 771 # width), but a non-positive value would size the Qdrant collection at 772 # zero — catch it here rather than at collection creation. 773 Validator( 774 "EMBEDDING_DIMENSIONS", 775 condition=lambda v: v is None or v >= 1, 776 messages={"condition": "EMBEDDING_DIMENSIONS must be >= 1 when set"}, 777 ), 778 Validator("SEARCH_RERANK_POOL_SIZE", gte=1), 779 Validator("SEARCH_RERANK_MAX_CONCURRENCY", gte=1), 780 Validator("SEARCH_RERANK_TIMEOUT_SECONDS", gt=0), 781 # Non-empty strings 782 Validator("VECTOR_SYNC_TAG", len_min=1), 783 # VECTOR_SYNC_KEYWORD_TAG is optional (empty disables keyword-only 784 # discovery), so no len_min — but when set it must be a usable tag name. 785 # MAIL_INDEX_TAG is likewise optional (empty indexes all mail), but the 786 # Mail app rejects display names over 128 characters — catch that at 787 # startup instead of failing every mail scan with an HTTP 400. 788 Validator("MAIL_INDEX_TAG", len_max=128), 789 # WEBHOOK_SECRET is optional (None disables webhooks — GHSA-8vh3-g2qg-2h2c), 790 # but when set it must be long enough to resist guessing. Surfaces a 791 # weak/placeholder secret at startup rather than in a later audit. 792 Validator( 793 "WEBHOOK_SECRET", 794 condition=lambda v: v is None or len(v) >= 16, 795 messages={ 796 "condition": "WEBHOOK_SECRET must be at least 16 characters when set" 797 }, 798 ), 799 # Enum constraints (document_* enums are validated + normalized in 800 # __post_init__ via _enum_fields instead, for case-insensitive input). 801 Validator("LOG_FORMAT", is_in=["text", "json"]), 802 Validator( 803 "LOG_LEVEL", 804 is_in=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], 805 ), 806 ], 807 ) 808 809 810 def _reload_config(): 811 """Reload dynaconf settings from files and environment. 812 813 Call this in tests after modifying os.environ to refresh the cache. 814 Re-validates all validators since reload() only checks unchecked ones. 815 """ 816 _dynaconf.reload() 817 _dynaconf.validators.validate_all() 818 _clear_settings_caches() 819 820 821 _ephemeral_db_path: str | None = None 822 823 824 def get_token_db_path() -> str: 825 """Resolve the token SQLite database path. 826 827 Priority: 828 1. TOKEN_STORAGE_DB if explicitly set — docker-compose pins 829 /app/data/tokens.db this way. Read via dynaconf, which picks up 830 the env var because TOKEN_STORAGE_DB is declared in _DEFAULTS. 831 2. Otherwise a per-process tempfile under tempfile.gettempdir(), 832 allocated lazily and deleted at interpreter exit via atexit. 833 Ephemeral: tokens are wiped on restart, matching the Qdrant 834 ":memory:" default pattern used elsewhere in this project. 835 """ 836 explicit = _dynaconf.get("TOKEN_STORAGE_DB") 837 if explicit: 838 return str(explicit) 839 global _ephemeral_db_path 840 if _ephemeral_db_path is None: 841 fd, path = tempfile.mkstemp( 842 prefix=f"nextcloud-mcp-tokens-{os.getpid()}-", suffix=".db" 843 ) 844 os.close(fd) 845 _ephemeral_db_path = path 846 847 def _cleanup(p: str = path) -> None: 848 try: 849 if os.path.exists(p): 850 os.unlink(p) 851 except OSError: 852 pass 853 854 atexit.register(_cleanup) 855 return _ephemeral_db_path 856 857 858 def is_ephemeral_token_db(path: str) -> bool: 859 """Return True if the given path is the process-local ephemeral tempfile. 860 861 Precondition: `get_token_db_path()` must have been called at least once 862 in this process to allocate the tempfile. If called before allocation, 863 this returns False for any input (including the eventual tempfile path), 864 because there is nothing to compare against yet. In practice every call 865 site in this repo resolves the path via `get_token_db_path()` first. 866 """ 867 return path == _ephemeral_db_path 868 869 870 def get_database_url() -> str: 871 """Resolve the SQLAlchemy database URL for token storage. 872 873 Priority: 874 1. ``DATABASE_URL`` if set — any SQLAlchemy URL is accepted; the primary 875 supported backends are ``postgresql+psycopg://...`` for HA k8s 876 deployments and ``sqlite+aiosqlite:///...`` for development. 877 2. Otherwise build ``sqlite+aiosqlite:///{get_token_db_path()}`` so the 878 legacy ``TOKEN_STORAGE_DB`` env var and the ephemeral-tempfile 879 fallback both keep working unchanged. 880 """ 881 explicit = _dynaconf.get("DATABASE_URL") 882 if explicit: 883 return str(explicit) 884 return f"sqlite+aiosqlite:///{get_token_db_path()}" 885 886 887 def is_sqlite_url(url: str) -> bool: 888 """Return True for SQLite SQLAlchemy URLs (used to gate sqlite-only logic 889 like file-permission hardening and ``sqlite_master`` legacy lookups). 890 891 Recognizes both file-backed (``sqlite+aiosqlite:///path/to/db``) and 892 in-memory (``sqlite+aiosqlite:///:memory:``) URLs. The caller is 893 responsible for handling ``:memory:`` as a magic value where a real 894 filesystem path is expected. 895 """ 896 return url.lower().startswith("sqlite") 897 898 899 def mask_db_password(url: str) -> str: 900 """Return a logger-safe rendering of a SQLAlchemy URL. 901 902 DATABASE_URL routinely carries a password (e.g. 903 ``postgresql+psycopg://mcp:secret@db/mcp``); logging it raw leaks the 904 secret to stdout/stderr and any aggregator. SQLAlchemy's 905 :func:`make_url` + ``render_as_string(hide_password=True)`` substitutes 906 a fixed ``***`` placeholder while keeping the rest of the URL intact 907 so operators can still see which host / driver they're hitting. 908 """ 909 try: 910 from sqlalchemy.engine.url import make_url # noqa: PLC0415 911 912 return make_url(url).render_as_string(hide_password=True) 913 except Exception: 914 # If parsing fails (e.g. an explicit ssl-disable test URL with an 915 # exotic shape), fall back to a regex that scrubs any 916 # ``://user:password@`` pattern. Never raise from a logging path. 917 return re.sub(r"(://[^:/]+):[^@]*@", r"\1:***@", url) 918 919 920 LOGGING_CONFIG = { 921 "version": 1, 922 "disable_existing_loggers": False, 923 "handlers": { 924 "default": { 925 "class": "logging.StreamHandler", 926 "formatter": "http", 927 }, 928 }, 929 "formatters": { 930 "http": { 931 "format": "%(levelname)s [%(asctime)s] %(name)s - %(message)s", 932 "datefmt": "%Y-%m-%d %H:%M:%S", 933 }, 934 }, 935 "loggers": { 936 "": { 937 "handlers": ["default"], 938 "level": "INFO", 939 }, 940 "httpx": { 941 "handlers": ["default"], 942 "level": "INFO", 943 "propagate": False, # Prevent propagation to root logger 944 }, 945 "httpcore": { 946 "handlers": ["default"], 947 "level": "INFO", 948 "propagate": False, # Prevent propagation to root logger 949 }, 950 "uvicorn": { 951 "handlers": ["default"], 952 "level": "INFO", 953 "propagate": False, 954 }, 955 "uvicorn.access": { 956 "handlers": ["default"], 957 "level": "INFO", 958 "propagate": False, 959 }, 960 "uvicorn.error": { 961 "handlers": ["default"], 962 "level": "INFO", 963 "propagate": False, 964 }, 965 }, 966 } 967 968 969 def setup_logging(): 970 logging.config.dictConfig(LOGGING_CONFIG) 971 972 973 # Document Processing Configuration 974 975 976 def get_document_processor_config() -> dict[str, Any]: 977 """Get document processor configuration from dynaconf. 978 979 Each optional processor is gated by its own ``ENABLE_*`` flag. There is no 980 master switch: parsing a document on read is a per-call decision made by the 981 caller (``nc_webdav_read_file``'s ``parse_document`` argument), not an 982 instance-wide setting. The built-in PDF tiers (pypdfium2 / pymupdf / OCR) 983 self-register when ``document_processors`` is first imported and never appear 984 here. 985 986 Returns: 987 Dict with processor configs: 988 { 989 "default_processor": str, 990 "processors": { 991 "unstructured": {...}, 992 "tesseract": {...}, 993 "custom": {...}, 994 } 995 } 996 """ 997 config: dict[str, Any] = { 998 "default_processor": _dynaconf.get("DOCUMENT_PROCESSOR"), 999 "processors": {}, 1000 } 1001 1002 # Unstructured configuration 1003 if _dynaconf.get("ENABLE_UNSTRUCTURED"): 1004 languages_str = _dynaconf.get("UNSTRUCTURED_LANGUAGES") 1005 config["processors"]["unstructured"] = { 1006 "api_url": _dynaconf.get("UNSTRUCTURED_API_URL"), 1007 "timeout": _dynaconf.get("UNSTRUCTURED_TIMEOUT"), 1008 "strategy": _dynaconf.get("UNSTRUCTURED_STRATEGY"), 1009 "languages": [ 1010 lang.strip() for lang in languages_str.split(",") if lang.strip() 1011 ], 1012 "progress_interval": _dynaconf.get("PROGRESS_INTERVAL"), 1013 } 1014 1015 # Tesseract configuration 1016 if _dynaconf.get("ENABLE_TESSERACT"): 1017 config["processors"]["tesseract"] = { 1018 "tesseract_cmd": _dynaconf.get("TESSERACT_CMD"), # None = auto-detect 1019 "lang": _dynaconf.get("TESSERACT_LANG"), 1020 } 1021 1022 # NB: pymupdf is deliberately absent. It is the built-in ``structured`` tier, 1023 # registered when ``document_processors`` is first imported (lazily, on the 1024 # first parse) and tuned from ``Settings.pymupdf_*``. Listing it here would 1025 # make ``processors`` non-empty on every deployment, which is what tells app 1026 # startup whether it has anything to register -- and so whether it must 1027 # import the parse stack at all (#877). 1028 1029 # Custom processor (via HTTP API) 1030 if _dynaconf.get("ENABLE_CUSTOM_PROCESSOR"): 1031 custom_url = _dynaconf.get("CUSTOM_PROCESSOR_URL") 1032 if custom_url: 1033 supported_types_str = _dynaconf.get("CUSTOM_PROCESSOR_TYPES") 1034 supported_types = { 1035 t.strip() for t in supported_types_str.split(",") if t.strip() 1036 } 1037 1038 config["processors"]["custom"] = { 1039 "name": _dynaconf.get("CUSTOM_PROCESSOR_NAME"), 1040 "api_url": custom_url, 1041 "api_key": _dynaconf.get("CUSTOM_PROCESSOR_API_KEY"), 1042 "timeout": _dynaconf.get("CUSTOM_PROCESSOR_TIMEOUT"), 1043 "supported_types": supported_types, 1044 } 1045 1046 # Docling configuration (docling-serve HTTP API). Registered only when a URL is 1047 # set, so a bare ENABLE_DOCLING doesn't shadow other image processors with a 1048 # dead endpoint (mirrors the custom_url guard above). The standalone processor 1049 # auto-serves images; PDFs go through the OCR backend 1050 # (DOCUMENT_OCR_PROVIDER=docling) -- docling is an OCR provider, not a tier of 1051 # its own. 1052 if _dynaconf.get("ENABLE_DOCLING"): 1053 docling_url = _dynaconf.get("DOCLING_API_URL") 1054 if docling_url: 1055 lang_str = _dynaconf.get("DOCLING_OCR_LANG") or "" 1056 config["processors"]["docling"] = { 1057 "api_url": docling_url, 1058 "timeout": _dynaconf.get("DOCLING_TIMEOUT"), 1059 "ocr_lang": [s.strip() for s in lang_str.split(",") if s.strip()], 1060 "do_ocr": _dynaconf.get("DOCLING_DO_OCR"), 1061 # Normalize like Settings.__post_init__ does (.strip().lower()) so the 1062 # image path matches convert_file()'s ``pipeline == "vlm"`` check -- 1063 # otherwise DOCLING_PIPELINE=VLM would silently fall back to standard 1064 # here while the OCR-backend path (Settings-validated) uses vlm. 1065 # vlm_preset is server-defined and case-sensitive, so it stays raw. 1066 "pipeline": (_dynaconf.get("DOCLING_PIPELINE") or "standard") 1067 .strip() 1068 .lower(), 1069 "vlm_preset": _dynaconf.get("DOCLING_VLM_PRESET"), 1070 "progress_interval": _dynaconf.get("PROGRESS_INTERVAL"), 1071 } 1072 1073 return config 1074 1075 1076 @dataclass 1077 class Settings: 1078 """Application settings from environment variables.""" 1079 1080 # Deployment mode (ADR-021: explicit mode selection; updated by ADR-022) 1081 # Optional: If not set, mode is auto-detected from other settings 1082 # Valid values: single_user_basic, multi_user_basic, login_flow 1083 # (ADR-022: `oauth_single_audience` was renamed to `login_flow`.) 1084 deployment_mode: str | None = None 1085 1086 # OAuth/OIDC settings 1087 oidc_discovery_url: str | None = None 1088 oidc_client_id: str | None = None 1089 oidc_client_secret: str | None = None 1090 oidc_issuer: str | None = None 1091 oidc_resource_server_id: str | None = None 1092 oidc_token_type: str = "Bearer" # NEXTCLOUD_OIDC_TOKEN_TYPE 1093 oidc_scopes: str = "" # NEXTCLOUD_OIDC_SCOPES (space-separated) 1094 1095 # OIDC discovery startup resilience. Discovery runs synchronously at boot 1096 # and is fatal on failure; on a freshly-scheduled pod the egress path (e.g. 1097 # Cilium toFQDN allow + egress-gateway SNAT programming) can take a few 1098 # seconds to converge, during which the request is dropped and times out. 1099 # Retry with capped exponential backoff + jitter so a cold-start race 1100 # doesn't crashloop the backend. Set OIDC_DISCOVERY_MAX_ATTEMPTS=1 to 1101 # restore the original fail-fast behavior. 1102 oidc_discovery_max_attempts: int = 10 # OIDC_DISCOVERY_MAX_ATTEMPTS 1103 oidc_discovery_backoff_base: float = 1.0 # OIDC_DISCOVERY_BACKOFF_BASE (s) 1104 oidc_discovery_backoff_max: float = 15.0 # OIDC_DISCOVERY_BACKOFF_MAX (s) 1105 # Startup Qdrant-collection-init retry/backoff. Qdrant may be briefly 1106 # unreachable during a rolling deploy (pod ordering, network-policy 1107 # convergence); retry transient connection failures with capped exponential 1108 # backoff + jitter instead of crashlooping with a full traceback. Genuine 1109 # errors (auth/config, e.g. a 4xx) still fail fast. Set 1110 # QDRANT_INIT_MAX_ATTEMPTS=1 to restore fail-fast. 1111 qdrant_init_max_attempts: int = 30 # QDRANT_INIT_MAX_ATTEMPTS 1112 qdrant_init_backoff_base: float = 1.0 # QDRANT_INIT_BACKOFF_BASE (s) 1113 qdrant_init_backoff_max: float = 10.0 # QDRANT_INIT_BACKOFF_MAX (s) 1114 port: int = 8000 # Server port (PORT); used to build fallback URLs 1115 1116 # Nextcloud settings 1117 nextcloud_host: str | None = None 1118 nextcloud_username: str | None = None 1119 nextcloud_password: str | None = None 1120 1121 # Browser-reachable public URL for OAuth/Login-Flow-v2 redirects when 1122 # NEXTCLOUD_HOST is an internal Docker hostname. Falls back to 1123 # nextcloud_host when unset. 1124 # 1125 # NOTE: this doubles as the OAuth *issuer* URL used for JWT ``iss`` 1126 # validation. In external-IdP mode (e.g. Keycloak) the issuer is the IdP, 1127 # NOT Nextcloud — so this value points at the IdP, not the browser-reachable 1128 # Nextcloud host. Use ``nextcloud_public_url`` / ``nextcloud_browser_url`` 1129 # for anything that must resolve to Nextcloud itself (Login Flow v2 login 1130 # URLs, elicitation links). 1131 nextcloud_public_issuer_url: str | None = None 1132 1133 # Browser-reachable public URL of the *Nextcloud* instance, used to rewrite 1134 # Login Flow v2 login URLs and elicitation links when NEXTCLOUD_HOST is an 1135 # internal Docker hostname. Distinct from ``nextcloud_public_issuer_url`` 1136 # because, in external-IdP (Keycloak/OIDC) deployments, the OAuth issuer is 1137 # the IdP while Login Flow v2 must still point the browser at Nextcloud. 1138 # Falls back to ``nextcloud_public_issuer_url`` then ``nextcloud_host`` (see 1139 # ``nextcloud_browser_url``) so single-IdP (login-flow) deployments that set 1140 # only NEXTCLOUD_PUBLIC_ISSUER_URL keep working unchanged. 1141 nextcloud_public_url: str | None = None 1142 1143 # Browser cookie Secure flag. None = auto-detect from nextcloud_host 1144 # scheme (https → True, else False). Set COOKIE_SECURE=true/false to 1145 # override. 1146 cookie_secure: bool | None = None 1147 1148 # Nextcloud SSL/TLS settings 1149 nextcloud_verify_ssl: bool = True 1150 nextcloud_ca_bundle: str | None = None 1151 1152 # Reuse pooled keep-alive connections for the Nextcloud httpx client. 1153 # Default True preserves low-latency interactive traffic. Set 1154 # NEXTCLOUD_HTTP_KEEPALIVE=false to force a fresh connection per request 1155 # (max_keepalive_connections=0) — mirrors the DATABASE_POOL_SIZE→NullPool 1156 # precedent and prevents a truncated/desynced response from poisoning a 1157 # pooled connection on flaky CDN/WAN paths (see #965). 1158 nextcloud_http_keepalive: bool = True 1159 1160 # Postgres connection pool sizing — DEPRECATED, retained for 1161 # backward compatibility. The psycopg engine switched to NullPool 1162 # in #799 (cross-event-loop crashes under anyio TaskGroups made 1163 # the original QueuePool + pool_pre_ping setup unsafe). These 1164 # fields no longer affect the Postgres engine; the validators 1165 # below still reject invalid values so misconfigured deploys 1166 # fail loudly rather than silently. See ADR-026 § Connection 1167 # pool and docs/configuration.md. 1168 database_pool_size: int = 2 1169 database_max_overflow: int = 5 1170 1171 # ADR-005: Token Audience Validation (required for OAuth mode) 1172 nextcloud_mcp_server_url: str | None = None # MCP server URL (used as audience) 1173 nextcloud_resource_uri: str | None = None # Nextcloud resource identifier 1174 1175 # Token verification endpoints 1176 jwks_uri: str | None = None 1177 introspection_uri: str | None = None 1178 userinfo_uri: str | None = None 1179 1180 # Progressive Consent settings (always enabled - no flag needed) 1181 enable_offline_access: bool = False 1182 1183 # Multi-user BasicAuth pass-through mode (ADR-019 interim solution). 1184 # Internal — not user-settable; the ENABLE_MULTI_USER_BASIC_AUTH env-var 1185 # alias was removed in the ADR-022 follow-up. Auto-set by 1186 # Settings.__post_init__ when MCP_DEPLOYMENT_MODE=multi_user_basic. When True, 1187 # the MCP server extracts BasicAuth credentials from request headers and 1188 # passes them through to Nextcloud APIs (no storage, stateless). Kept 1189 # as a field for backward compat with the runtime call sites that read it. 1190 enable_multi_user_basic_auth: bool = False 1191 1192 # Login Flow v2 derived flag (ADR-022). Internal — not user-settable. 1193 # Auto-set by Settings.__post_init__ when the resolved deployment mode is 1194 # LOGIN_FLOW. Kept as a field for backward compat with the runtime call 1195 # sites that read it (app.py, context.py, scope_authorization.py). 1196 enable_login_flow: bool = False 1197 1198 # Token and webhook storage settings 1199 # TOKEN_ENCRYPTION_KEY: Optional - Only required for OAuth token storage operations. 1200 # Webhook tracking works without encryption key. 1201 # If set, must be a valid base64-encoded Fernet key (32 bytes). 1202 # TOKEN_STORAGE_DB: Path to SQLite database for persistent storage. 1203 # Used for webhook tracking (all modes) and OAuth token storage. -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/config.py line 108
- Likely real
- Uncertain, 45% confidence
Possible hardcoded password: 'None'
107 # Token storage 108 "token_encryption_key": None, 109 # None = ephemeral per-process tempfile (see get_token_db_path()). 110 # Set TOKEN_STORAGE_DB to persist tokens across restarts. 111 "token_storage_db": None, 112 # Centralized backend (any SQLAlchemy URL). Wins over TOKEN_STORAGE_DB 113 # when set. Use postgresql+psycopg://user:pw@host/db for HA k8s 114 # deployments so pods can be stateless. The URL is passed through 115 # verbatim — TLS (e.g. ?sslmode=require) and every other parameter are 116 # read from it by libpq/psycopg; the server never rewrites it. See ADR-026. 117 "database_url": None, 118 # Postgres connection pool sizing (ADR-026 → "Concurrency model and 119 # pool sizing"). Per-pod defaults to 2 + 5 overflow = 7 max 120 # connections. psycopg connections are single-flight, so the pool 121 # only needs to cover typical multi-user MCP burst — not every 122 # potential in-flight tool call. Tune up with DATABASE_POOL_SIZE / 123 # DATABASE_MAX_OVERFLOW for high-traffic prod fleets. 124 "database_pool_size": 2, 125 "database_max_overflow": 5, 126 # Webhook delivery authentication (ADR-010): when set, registrations 127 # tell NC to add `Authorization: Bearer <secret>` to webhook deliveries 128 # and the receiver rejects unauthenticated requests. 129 "webhook_secret": None, 130 # Internal URL override for webhook registration; wins over 131 # NEXTCLOUD_MCP_SERVER_URL when set (e.g. split internal/external URLs). 132 "webhook_internal_url": None, 133 # Vector sync 134 "vector_sync_scan_interval": 300, 135 "vector_sync_processor_workers": 3, 136 # Consecutive failed index attempts (embed / Qdrant / transport — NOT a 137 # parse failure, which is terminal on the first try) before a document is 138 # dead-lettered instead of re-queued forever. See vector/dead_letter.py. 139 "vector_sync_max_index_failures": 5, 140 # Optional per-tier concurrency overrides (None = fall back to 141 # vector_sync_processor_workers). Consumed by the `worker` CLI per --tier. 142 "vector_sync_fast_concurrency": None, 143 "vector_sync_structured_concurrency": None, 144 "vector_sync_queue_max_size": 10000, 145 "vector_sync_metrics_refresh_interval": 20, 146 "vector_density_snapshot_enabled": True, 147 "vector_density_snapshot_interval": 300, 148 "vector_density_snapshot_max_documents": 50000, 149 "usage_stock_snapshot_interval": 86400, 150 "vector_ram_hnsw_overhead_factor": 1.5, 151 "vector_sync_user_poll_interval": 60, 152 "health_ready_refresh_interval": 15, 153 # Orphan-sweep at Pod startup (card #101). When True, delete any 154 # placeholders carrying a different / absent ``instance_id`` before 155 # the scanner's first cycle, so a Pod restart mid-batch doesn't 156 # leave work stuck behind the 5x-scan-interval staleness gate. 157 # Escape hatch only — leave on by default. 158 "vector_sync_orphan_sweep_enabled": True, 159 # System tag that marks files for hybrid (dense + BM25 sparse) indexing. 160 # The scanner indexes files carrying this tag; verify-on-read gates results 161 # on current membership of this tag (ADR-019). 162 "vector_sync_tag": "vector-index", 163 # System tag that marks files for keyword-only (BM25 sparse) indexing. 164 # Defaults to ``keyword-index`` (symmetric with ``vector_sync_tag``), so 165 # a user who creates + applies that tag gets keyword-only indexing out of the 166 # box. Files carrying it are indexed sparse-only (no dense embedding, no 167 # embedding cost) into the SAME collection as hybrid files; ``vector-index`` 168 # wins if a file carries both. Set empty to disable the second tag entirely. 169 "vector_sync_keyword_tag": "keyword-index", 170 # Which MIME types tagged-file discovery will enqueue. None (unset) means 171 # every type a registered processor can read; a comma-separated list 172 # narrows it, and an empty string indexes no files. 173 "vector_sync_indexable_mime_types": None, 174 # Mail tag (an IMAP keyword) restricting which messages are indexed. Empty 175 # (the default) indexes every message in every mailbox, which is the 176 # behaviour before this setting existed. Set it to a tag display name and 177 # only messages carrying that tag are indexed — the mail analogue of 178 # ``vector_sync_tag`` for files. 179 "mail_index_tag": "", 180 # Fail-safe against a flaky/empty tag-discovery read: number of *consecutive* 181 # scan cycles for which a given index mode's tag discovery must return zero 182 # files (while Qdrant still holds indexed points for that mode) before the 183 # scanner believes it and starts deleting. A transient empty read (e.g. a 184 # customer-hosted Nextcloud that intermittently answers the systemtag REPORT 185 # with an empty 207) is thereby treated as a failed read, not "all files 186 # gone" — preventing a delete/re-index churn loop. A sustained empty (a real 187 # mass-untag) still eventually deletes once the streak reaches this value. 188 "vector_sync_empty_discovery_delete_threshold": 3, 189 # Verify-on-read concurrency cap (ADR-019) 190 "verification_concurrency": 20, 191 # Qdrant 192 "qdrant_url": None, 193 "qdrant_location": None, 194 "qdrant_api_key": None, 195 "qdrant_collection": "nextcloud_content", 196 # Ollama 197 "ollama_base_url": None, 198 "ollama_embedding_model": "nomic-embed-text", 199 "ollama_verify_ssl": True, 200 # Ollama embeds a batch serially on whatever hardware it has, so the wall 201 # clock of one /api/embed call scales with the batch's total text, not its 202 # item count. Bound the request by characters (GH #1345). 203 "ollama_embed_max_batch_chars": 16000, 204 "ollama_embed_timeout": 120, 205 # OpenAI 206 "openai_api_key": None, 207 "openai_base_url": None, 208 "openai_embedding_model": "text-embedding-3-small", 209 # Bedrock (AWS) 210 "aws_region": None, 211 "aws_access_key_id": None, 212 "aws_secret_access_key": None, 213 "bedrock_embedding_model": None, 214 # Mistral 215 "mistral_api_key": None, 216 "mistral_embedding_model": "mistral-embed", 217 "mistral_base_url": None, 218 # Matryoshka output width requested from the embedding endpoint (None = 219 # the model's full width). Provider-agnostic: only one provider is active 220 # at a time, so a single knob covers openai/ollama/gateway. 221 "embedding_dimensions": None, 222 # Simple (fallback) embedding dimension 223 "simple_embedding_dimension": 384, 224 # Document chunking 225 "document_chunk_size": 2048, 226 "document_chunk_overlap": 200, 227 # Page-aware chunking for paginated docs (PDFs): split on page boundaries 228 # first so no chunk spans a page (exact page_number, clean snippets, and 229 # predictable ~1 chunk/page when chunk_size >= the largest page). 230 "document_chunk_page_aware": True, 231 # Greedy page-packing (density fix, Deck #636): merge consecutive sub-budget 232 # pages into one chunk instead of one-per-page. Off by default until the 233 # post-change density re-measure + pricing re-calibration land. 234 "document_chunk_page_pack": False, 235 # Hybrid-search RRF ranking constant. Qdrant's native RRF hardcodes k=2, 236 # where adjacent ranks differ by 33% and a single-list outlier outranks a 237 # document both retrievers agree on. 60 is the standard value (~2% per rank), 238 # which restores consensus as the dominant signal. 239 "vector_search_rrf_k": 60, 240 # --- Cross-encoder reranking (opt-in, per request) ---------------------- 241 # A cross-encoder reorders the retrieved candidates by scoring each against 242 # the query directly, which is more accurate than the fusion rank it 243 # replaces. Ships OFF because it adds an upstream round-trip to every 244 # search, and how expensive that is depends on how the reranker is deployed 245 # — which this server deliberately knows nothing about beyond the URL. 246 "search_rerank_enabled": False, 247 # Where reranking is served. A FULL endpoint URL, because the path differs 248 # per backend and there is no safe guess: Infinity serves ``POST /rerank``, 249 # vLLM ``POST /v1/rerank``, Cohere ``POST /v2/rerank``, the Astrolabe 250 # embedding gateway ``POST /v1/rerank``. All four speak the same Cohere 251 # request/response protocol, so choosing one is configuration, not code. 252 # 253 # Unset = derive it from EMBEDDING_GATEWAY_URL, which is what every existing 254 # deployment does. Set it to run reranking WITHOUT a gateway — the reason 255 # this exists (discussion #1354): Ollama serves no rerank models at all, so 256 # a self-hoster's only options are a hosted rerank API or a local Infinity / 257 # vLLM, and neither should require standing up a gateway first. 258 "search_rerank_url": None, 259 # Static bearer for SEARCH_RERANK_URL (a Cohere API key, an Infinity 260 # ``--api-key``). Wins over the gateway's M2M OIDC credentials when both are 261 # present. Unset = send no Authorization header, which is correct for a 262 # local Infinity/vLLM on a private network. 263 "search_rerank_api_key": None, 264 # Rerank model, addressed the way the CONFIGURED ENDPOINT expects — which 265 # differs between the two, so the default is only right for one of them. 266 # 267 # Against the embedding gateway the ``<provider>/`` prefix is REQUIRED, not 268 # stylistic: the gateway splits on the FIRST slash to select a backend, so a 269 # bare ``BAAI/bge-reranker-v2-m3`` asks for a provider named ``BAAI`` and 270 # 503s — and because a failed rerank degrades to retrieval order 271 # (``reranked: false``) rather than erroring, that looks like "reranking does 272 # nothing" instead of like a misconfiguration. The prefix is also the backend 273 # selector: ``local/`` for a self-hosted cross-encoder, ``bedrock/`` or 274 # ``openrouter/`` for a hosted one. 275 # 276 # Against a direct SEARCH_RERANK_URL there is no such routing layer, so use 277 # the bare id the server serves (``BAAI/bge-reranker-v2-m3`` for 278 # Infinity/vLLM, ``rerank-v3.5`` for Cohere). The relevance calibration 279 # (ADR-034) strips known provider prefixes, so both spellings resolve to the 280 # same fitted curve. 281 "search_rerank_model": "local/BAAI/bge-reranker-v2-m3", 282 # Candidates handed to the reranker. Reranking can only reorder what 283 # retrieval supplied, so this depth — not the caller's ``limit`` — bounds 284 # how much a reranker can improve. Reranking just the rows a normal search 285 # returns captures little of the available gain. 286 # 287 # Treat this as a ceiling rather than a starting point. Under 288 # ``granularity="document"`` the grouped prefetch is bounded by 289 # MAX_DOCUMENT_PREFETCH, and requesting more groups than the prefetch can 290 # fill makes Qdrant widen its grouping search and reorder the head of the 291 # result set before the reranker sees it. Going deeper requires raising 292 # MAX_DOCUMENT_PREFETCH first, which carries its own latency cost. 293 "search_rerank_pool_size": 200, 294 # Generous headroom over a normal rerank, not a target. Exceeding it 295 # degrades to retrieval order rather than failing the search. 296 "search_rerank_timeout_seconds": 30.0, 297 # Concurrent rerank calls this process keeps in flight against the reranker. 298 # 299 # Conservative at 1 because reranking is the heaviest request this server 300 # makes of a service it may share with its own embedding traffic and with 301 # other callers. Bounding our own concurrency keeps a burst of searches from 302 # queueing unbounded work upstream, and keeps rerank latency predictable 303 # here. It is a client-side courtesy, not a throughput control: what the 304 # reranker does with the request, and how it schedules against everything 305 # else it serves, is its business. Raise it if yours has headroom — a CPU 306 # cross-encoder almost certainly does not. 307 "search_rerank_max_concurrency": 1, 308 # --- SAR export redaction (ADR-040) ------------------------------------- 309 # Names are detected by the embedding gateway's ``POST /v1/ner``, so SAR 310 # export is available only with EMBEDDING_GATEWAY_URL set. 311 # NER model, addressed the gateway way (``<provider>/<model>``). 312 # Subject access request cases and redacted export (ADR-040). Off by 313 # default: a deployment opts in, since only some need it. Also requires 314 # vector sync and EMBEDDING_GATEWAY_URL (for name detection). 315 "sar_enabled": False, 316 "ner_model": "local/urchade/gliner_multi_pii-v1", 317 # Per-request budget. Export runs in the background, so this only needs to 318 # cover one batch on the slowest backend (CPU GLiNER: ~570 chars/s). 319 "ner_timeout_seconds": 120.0, 320 # Texts (of up to 2,000 chars) per /v1/ner request. Small for CPU GLiNER, 321 # which must finish a batch inside the gateway's own upstream timeout; raise 322 # it (e.g. 32) on a GPU backend. 323 "ner_batch_size": 8, 324 # Minimum model confidence for a span to count as a person. Lower raises 325 # recall at the cost of over-redaction, which is the safe direction for a 326 # disclosure; 0.5 is GLiNER's customary operating point. 327 "ner_threshold": 0.5, 328 # Chunking config generation. Bump whenever chunker behaviour changes (size, 329 # overlap, page-aware, page-pack, split strategy) so the pricing model's 330 # density reference can't silently go stale. Pinned in stripe-catalog.tf. 331 "chunking_config_version": 1, 332 # PDF parse isolation (OOM guard) 333 "document_pdf_graphics_limit": 1000, 334 "document_parse_timeout_seconds": 120.0, 335 # Optional wall-clock cap (seconds) on the SYNCHRONOUS parse inside the 336 # nc_webdav_read_file MCP tool. None (default) = disabled: an interactive read 337 # is bounded only by the underlying processor timeout (DOCLING_TIMEOUT / 338 # DOCUMENT_OCR_TIMEOUT_SECONDS). Set a client-friendly bound (e.g. 45-60s) so a 339 # slow VLM/OCR convert returns base64 quickly instead of blocking past an MCP 340 # client's own timeout. Never applies to the async ingest/worker path (ADR-032). 341 "document_read_timeout_seconds": None, 342 "document_parse_mem_limit_mb": 1536, 343 # Pre-parse size cap (MB): PDFs larger than this fail fast with reason 344 # "oversize" instead of burning the OCR timeout to 0 chars on a pathological 345 # file. 0 disables the guard. 346 "document_max_pdf_size_mb": 50.0, 347 # Pre-flight cap (MB) on nc_webdav_write_file's content: a huge PUT built 348 # from a single in-memory MCP tool argument (no chunked/streaming upload 349 # exists for writes, unlike the read-side stream_to_file path) risks 350 # timing out or exhausting memory rather than failing predictably. 0 351 # disables the guard. 352 "webdav_write_max_mb": 50.0, 353 # Transport security (MCP SDK TransportSecuritySettings). Default False 354 # preserves the behaviour hardcoded before these knobs existed: MCP 1.23+ 355 # auto-enables localhost-only host checking, which breaks k8s/Docker service 356 # DNS names (docs/MCP-1.23-DNS-REBINDING-FIX.md). Enable it plus 357 # MCP_ALLOWED_HOSTS when the server is reachable from a browser. 358 "mcp_dns_rebinding_protection": False, 359 # Comma-separated Host / Origin allowlists, honoured only when the above is 360 # enabled. Empty means "the SDK's own defaults". 361 "mcp_allowed_hosts": "", 362 "mcp_allowed_origins": "", 363 # Comma-separated CORS origin allowlist. "*" preserves today's behaviour. 364 "cors_allow_origins": "*", 365 # Page ceiling for markdown reconstruction (see document_markdown_max_pages). 366 "document_markdown_max_pages": 150, 367 # Pages per pypdfium2 extraction window (see document_parse_page_window). 368 "document_parse_page_window": 100, 369 # Concurrent isolated parse subprocesses (see document_parse_process_slots). 370 "document_parse_process_slots": 2, 371 # Stream ingest downloads to disk instead of buffering them in memory. 372 "document_stream_download_enabled": True, 373 # Directory for ingest spool files (default: the system temp dir). 374 "document_spool_dir": None, 375 # Tier-0 classifier (records classification metrics on the tiered path) 376 "document_classify_enabled": True, 377 # Tiered PDF pipeline: pypdfium2 is the default/only hot-path extractor; 378 # "pymupdf" is a deprecated rollback escape hatch. OCR (tier-3) is the only 379 # escalation target and is off by default (no provider wired yet). 380 "document_tier1_engine": "pypdfium2", 381 "document_ocr_enabled": False, 382 # OCR backend: "auto" picks gateway (if EMBEDDING_GATEWAY_URL) else mistral 383 # (if MISTRAL_API_KEY); "gateway"/"mistral" force one; "docling" routes scanned 384 # PDFs to a docling-serve instance (DOCLING_API_URL); "none" disables. "auto" 385 # never selects docling (it needs a self-hosted URL, so it must be explicit). 386 # The gateway routes on the model's "<provider>/" prefix, so it serves Mistral, 387 # surya (in-cluster GPU over the tailnet), etc. — one configurable OCR tier. 388 "document_ocr_provider": "auto", 389 # Provider-namespaced OCR model id (gateway routes on the prefix; the direct 390 # mistral backend strips it). e.g. "mistral/mistral-ocr-latest" (Mistral) or 391 # "surya/surya-ocr-2" (surya via the gateway) — never hard-coded, only this 392 # default. 393 "document_ocr_model": "mistral/mistral-ocr-latest", 394 # OCR escalation triggers (tier-0). A page is OCR-worthy when its text is 395 # near-empty (< min_page_chars) OR low-quality (< min_text_quality) OR (when 396 # detect_scanned) mostly a raster image; a doc escalates when the OCR-worthy 397 # page fraction reaches page_fraction. Calibrate min_text_quality from the 398 # astrolabe_document_text_quality histogram per tenant. 399 "document_ocr_min_text_quality": 0.5, 400 "document_ocr_page_fraction": 0.5, 401 "document_ocr_min_page_chars": 16, 402 "document_ocr_detect_scanned": True, 403 # Tier-0 glyph-corruption trigger. When the fast (pypdfium2) extraction's 404 # doc-level C0-control-char ratio exceeds this, the text layer is treated as 405 # glyph-corrupt (a broken /ToUnicode mapping leaking raw glyph codes) and the 406 # doc escalates fast->structured (pymupdf re-extracts it correctly -- no OCR). 407 # 0 disables. Clean docs sit ~0; affected PDFs measured ~1-11% in testing. 408 "document_glyph_corruption_ratio": 0.02, 409 # OCR backend request timeout (seconds). Slow scanned newspapers can take 410 # 20-60s; raise/lower per tenant. Configurable so a tenant isn't stuck with 411 # the 180s default when its gateway has its own shorter ceiling. 412 "document_ocr_timeout_seconds": 180.0, 413 # OCR execution mode (Deck #332). "sync" (default) transcribes inline via the 414 # backend's synchronous path. "batch" routes to the gateway's async Batch OCR 415 # job (~50% cheaper, minutes-hours latency) for large-corpus backfill — opt-in 416 # and gateway-routed. It requires EMBEDDING_GATEWAY_URL (rejected at startup 417 # otherwise, see __post_init__) and the per-tier procrastinate path; the 418 # inline/memory pool can't defer a poll, so batch raises there rather than 419 # silently downgrading to sync. 420 "document_ocr_mode": "sync", 421 # Seconds between batch-job polls (the procrastinate re-enqueue delay). Each 422 # poll re-runs the tier; keep it well above a few seconds. 423 "document_ocr_batch_poll_seconds": 120, 424 # Observability 425 "metrics_enabled": True, 426 "metrics_port": 9090, 427 "otel_exporter_otlp_endpoint": None, 428 "otel_exporter_verify_ssl": None, 429 "otel_service_name": "nextcloud-mcp-server", 430 "pyroscope_enabled": False, 431 "pyroscope_server_address": None, 432 # Pod identity from the Kubernetes downward API (chart-injected). Facts 433 # about the pod, not user config — absent off-Kubernetes. 434 "pod_namespace": None, 435 "pod_name": None, 436 "log_format": "text", 437 "log_level": "INFO", 438 "log_include_trace_context": True, 439 # Reverse-proxy trust list for X-Forwarded-For (GH #1284). Passed to 440 # uvicorn; None keeps uvicorn's own resolution (which itself falls back to 441 # 127.0.0.1). Declared here so the key is settable from settings.toml, not 442 # only as the env var uvicorn already reads on its own. 443 "forwarded_allow_ips": None, 444 # Document processing (no master switch: each optional processor has its own 445 # ENABLE_* flag, and parsing on read is the caller's per-call decision) 446 "document_processor": "unstructured", 447 "enable_unstructured": False, 448 "unstructured_api_url": "http://unstructured:8000", 449 "unstructured_timeout": 120, 450 "unstructured_strategy": "auto", 451 "unstructured_languages": "eng,deu", 452 "progress_interval": 10, 453 "enable_tesseract": False, 454 "tesseract_cmd": None, 455 "tesseract_lang": "eng", 456 # pymupdf is the built-in "structured" tier and is always available -- there 457 # is no enable flag for it, only these two knobs. 458 "pymupdf_extract_images": True, 459 "pymupdf_image_dir": None, 460 "enable_custom_processor": False, 461 "custom_processor_url": None, 462 "custom_processor_types": "application/pdf", 463 "custom_processor_name": "custom", 464 "custom_processor_api_key": None, 465 "custom_processor_timeout": 60, 466 # Docling document-parsing backend (docling-serve HTTP API). One docling-serve 467 # instance feeds two touchpoints: the images-only DoclingProcessor 468 # (find_processor path, priority 20) and — when DOCUMENT_OCR_PROVIDER=docling — 469 # the PDF OCR backend for scanned/no-text-layer PDFs. The same processor can be 470 # force-selected per call to parse a text-layer PDF (tables/partial text). URL 471 # unset -> the image processor is not registered and the OCR backend resolves to 472 # None. See ADR-031. 473 "enable_docling": False, 474 "docling_api_url": None, 475 "docling_timeout": 120, 476 # docling-serve OCR language codes. The default engine (EasyOCR) uses 2-letter 477 # codes ("en","de"); a Tesseract-backed instance wants "eng","deu". Engine- 478 # dependent, so keep it operator-tunable (see ADR-031). 479 "docling_ocr_lang": "en,de", 480 # Run OCR on IMAGES routed to the DoclingProcessor (find_processor path). The 481 # docling OCR *backend* (scanned PDFs) always OCRs regardless of this flag. 482 "docling_do_ocr": True, 483 # Which docling-serve pipeline to request: "standard" (classic layout+OCR, 484 # default, unchanged) or "vlm" (Vision-LLM OCR). "vlm" needs a docling-serve 485 # instance configured with VLM presets (see ADR-032). 486 "docling_pipeline": "standard", 487 # VLM preset name sent when docling_pipeline == "vlm". None -> docling-serve 488 # picks its own DOCLING_SERVE_DEFAULT_VLM_PRESET. Preset names are server-defined. 489 "docling_vlm_preset": None, 490 # Caption raster pictures embedded in .pptx/.docx/.xlsx via the same 491 # docling-serve instance (ADR-037). Explicit opt-in beyond a bare 492 # DOCLING_API_URL, like DOCUMENT_OCR_PROVIDER=docling needs its own 493 # selection: a deployment that only wants docling for scanned-PDF OCR 494 # shouldn't start captioning every picture in every document for free. The 495 # native readers have no rendering engine, so this only reaches actual 496 # pictures -- native vector drawings (SmartArt, shapes) are unaffected. 497 "office_caption_images": False, 498 # Cap on pictures captioned per file (one docling-serve round trip each). 499 # nc_webdav_read_file blocks synchronously for the whole parse, so this 500 # bounds the worst case rather than leaving it to the file's picture count. 501 "office_caption_max_images": 8, 502 # Per-picture docling-serve request timeout (seconds). Deliberately short 503 # and independent of DOCLING_TIMEOUT/DOCUMENT_OCR_TIMEOUT_SECONDS (other 504 # touchpoints, other latency profiles): a caption is meant to be a quick 505 # per-picture round trip repeated up to OFFICE_CAPTION_MAX_IMAGES times, not 506 # a single long convert. Raise it if DOCLING_PIPELINE=vlm makes captions 507 # time out (VLM is far slower than the standard pipeline -- see ADR-032). 508 "office_caption_timeout_seconds": 15.0, 509 # Collabora Online (coolwsd) base URL for converting legacy .doc/.xls/.ppt 510 # and ODF .odt/.ods/.odp to OOXML, which the native readers then parse 511 # (ADR-039). Unset = those types are not claimed. coolwsd only answers 512 # convert-to for clients in its net.post_allow list (private ranges by 513 # default). 514 "collabora_url": None, 515 # Per-file convert-to request timeout (seconds). A typical document 516 # converts in well under a second; the headroom is for large workbooks. 517 "collabora_timeout_seconds": 60.0, 518 # Deprecated PPTX_CAPTION_* spellings (0.193.0, pptx-only) of the three keys 519 # above; see _apply_legacy_caption_settings. Declared so dynaconf reads 520 # them at all, with the same defaults so their validators still apply. 521 "pptx_caption_images": False, 522 "pptx_caption_max_images": 8, 523 "pptx_caption_timeout_seconds": 15.0, 524 # Tag-based file exclusion (issue #710): comma-separated list of 525 # Nextcloud system tag names. Files/folders carrying any of these tags 526 # are hidden from WebDAV MCP tools. Empty = feature off. 527 "excluded_tags": "", 528 # MCP decomposition hook points (design §10). Every default reproduces 529 # the current monolithic behavior; self-hosters who set none are 530 # unaffected. See docs/architecture/mcp-decomposition.md (sibling repo). 531 "embedding_provider": "autodetect", # autodetect | gateway 532 # Ingest queue backend (Deck #183). None → ``memory`` (the in-process anyio 533 # queue): procrastinate is strictly opt-in, even on a Postgres DATABASE_URL. 534 # Set ``postgres`` explicitly to split ingest into a procrastinate worker; 535 # that requires a PostgreSQL DATABASE_URL. 536 "ingest_queue": None, # memory | postgres 537 # Process role for the per-tenant two-pod model (Deck #183). ``api`` runs the 538 # MCP/query server + scanner (defers jobs); the ``worker`` role is the 539 # `nextcloud-mcp-server worker` process that drains the queue. ``all`` keeps 540 # the monolithic behaviour (API + in-process SQLite pool). 541 "mcp_role": "all", # api | worker | all 542 # Reclaim an ingest job orphaned in ``doing`` by a crashed worker once its 543 # worker heartbeat is this many seconds stale (Deck #183). Default is well 544 # above the longest expected document; raise it for slow embedding backends. 545 "ingest_stalled_job_seconds": 300, 546 # Backstop reclaim: also re-queue a job stuck in ``doing`` for this many seconds 547 # regardless of worker liveness. The heartbeat threshold above only catches DEAD 548 # workers; a job whose OWN completion crashed (e.g. an unhandled queueing_lock 549 # UniqueViolation on the doing->todo retry) is stranded in ``doing`` under a LIVE, 550 # heart-beating worker and is invisible to the heartbeat sweep. 551 # This fires purely on time-in-``doing``, NOT on liveness, so it can't tell "stuck" 552 # from "legitimately slow": if a single HEALTHY process_document attempt ever ran 553 # past this, it'd be re-queued mid-flight and two workers could process the doc 554 # concurrently. That's safe ONLY because ingest re-runs are idempotent (uuid5 555 # Qdrant point IDs — see the queue module's "Design notes"), NOT because the 556 # reclaim distinguishes the two — so keep the default comfortably above the longest 557 # real single attempt. OCR batch polling releases the worker between polls 558 # (BatchPending -> retry_in), so a job never legitimately holds ``doing`` this 559 # long; 1800s is deep headroom. (Deck: ingest doing-strand reclaim.) 560 "ingest_doing_max_seconds": 1800, 561 # Delete succeeded ingest jobs (keeps the queue table lean + the KEDA 562 # queue-depth metric clean). Set false to retain succeeded rows for audit 563 # (note: indexing success is also recorded in logs/metrics regardless). 564 "ingest_delete_succeeded_jobs": True, 565 # Whether the procrastinate worker uses LISTEN/NOTIFY for job pickup (Deck 566 # #424). True (default) = near-instant wakeup via a long-lived LISTEN 567 # connection. Set false to run POLL-ONLY when DATABASE_URL routes through a 568 # transaction-mode connection pooler (PgBouncer transaction mode), which is 569 # incompatible with LISTEN/NOTIFY — the LISTEN registration is dropped when 570 # the backend returns to the pool. Poll-only trades a few seconds of pickup 571 # latency (fetch_job_polling_interval) for pooler safety; job queries still 572 # multiplex through the pooler. Snapshotted at worker startup; needs a 573 # restart to change. 574 "ingest_listen_notify": True, 575 # Per-tier escalation on the procrastinate (postgres) ingest path (Deck 576 # #323). When true, a document that a tier cannot parse well is requeued onto 577 # the next tier's queue (fast -> structured -> ocr) via a native procrastinate 578 # queue-hop. When false the ``fast`` tier is terminal -- reproduces the 579 # pre-#323 behaviour where the cheap tier's output is indexed as-is. No effect 580 # on the in-process ``memory`` backend, which keeps the inline escalation. 581 # HOT: re-read per job (process_document_task), so it takes effect on the next 582 # job -- unlike INGEST_TRANSIENT_MAX_ATTEMPTS, which is snapshotted at worker 583 # startup and needs a restart. 584 "ingest_escalation_enabled": True, 585 # Global cap on SAME-tier retries for transient infra errors (doc fetch / 586 # embed / Qdrant blips) on the procrastinate path. Parse-quality failures 587 # escalate (one parse attempt per tier) and do NOT consume this budget; only 588 # whitelisted transient exceptions retry in place. Shared across tiers because 589 # a queue-hop cannot reset a per-tier counter (see TieredEscalationStrategy). 590 # Snapshotted at worker startup (blueprint build); restart to change it. 591 "ingest_transient_max_attempts": 5, 592 # Delay (seconds) before a reclaimed stalled job is re-run. A stall is often 593 # systemic (Qdrant/embedding outage), so reclaiming every crashed job at 594 # now() would thundering-herd a recovering dependency every reclaim tick 595 # (*/5min), bypassing TieredEscalationStrategy's per-job backoff. A small 596 # fixed delay staggers the retry. 0 = immediate (legacy behaviour). 597 "ingest_reclaim_retry_delay_seconds": 30, 598 "collection_metadata_source": "qdrant", # qdrant | api 599 # CP base URL for COLLECTION_METADATA_SOURCE=api (e.g. http://control-plane). 600 # Required only when the source is api. 601 "collection_metadata_api_url": None, 602 "embedding_gateway_url": None, # required when embedding_provider=gateway 603 # Provider-namespaced model the gateway serves, "<provider>/<model>" 604 # (the gateway routes on the "/"-prefix; mistral/mistral-embed → Mistral 605 # for the MVP). Only consulted when embedding_provider=gateway. 606 "embedding_gateway_model": "mistral/mistral-embed", 607 # Gateway auth: the MCP server is an OIDC *client* in the gateway's own 608 # M2M realm (parallel to, and distinct from, the tenant realm it already 609 # serves). It obtains a client-credentials token and the gateway maps the 610 # client-id → the tenant's underlying provider API key. All four unset = 611 # call the gateway unauthenticated (matches today's not-yet-authed gateway). 612 "embedding_gateway_token_url": None, # M2M token endpoint 613 "embedding_gateway_client_id": None, 614 "embedding_gateway_client_secret": None, 615 "embedding_gateway_scope": None, # e.g. astrolabe-embedding-gateway/embed 616 # Query-side ACL pre-filter (design §11). OFF by default: a Qdrant 617 # `match any` on `acl_hash` excludes points missing the key, so enabling 618 # this before a real ACL backfill would silently drop legacy results. 619 # verify-on-read remains the correctness backstop regardless. 620 "acl_prefilter_enabled": False, 621 # Usage metering (Deck #67, control-plane usage-metering.md). OFF by 622 # default so OSS self-hosters don't accrue a metering table or write 623 # overhead; Astrolabe Cloud provisioning sets it true. When on, billable 624 # ops record rows into the app-DB usage_events table (best-effort). 625 "usage_metering_enabled": False, 626 } 627 628 629 def _resolve_settings_files() -> list[str]: 630 """Find optional external settings files. 631 632 Priority: 633 1. NEXTCLOUD_MCP_SETTINGS_FILE env var (absolute or relative path). 634 If set but the file does not exist, raise FileNotFoundError — 635 silently falling back to defaults on a typo would be a footgun. 636 .secrets.toml is looked for alongside the explicit file. 637 2. Otherwise ./settings.toml in cwd (for docker / dev workflows), 638 with .secrets.toml also looked for in cwd. 639 640 Returns an empty list if nothing is configured — that's fine, defaults 641 and env vars still apply. 642 """ 643 files: list[str] = [] 644 # A legitimate os.environ read: this runs BEFORE the dynaconf instance is 645 # built (it computes the settings_files list passed to Dynaconf), so it 646 # cannot go through dynaconf — you can't read the location of the settings 647 # file from the settings file. This + the MCP_DEPLOYMENT_MODE env_switcher 648 # are dynaconf's own bootstrap. All config *values* are dynaconf-driven; the 649 # only other reads are the legacy-env deprecation check and 650 # _warn_unknown_env_vars, which inspect env var NAMES, not values. 651 explicit = os.environ.get("NEXTCLOUD_MCP_SETTINGS_FILE") 652 if explicit: 653 p = Path(explicit) 654 if not p.exists(): 655 raise FileNotFoundError( 656 f"NEXTCLOUD_MCP_SETTINGS_FILE points to a file that does " 657 f"not exist: {explicit}" 658 ) 659 files.append(str(p)) 660 secrets = p.parent / ".secrets.toml" 661 else: 662 cwd_settings = Path.cwd() / "settings.toml" 663 if cwd_settings.exists(): 664 files.append(str(cwd_settings)) 665 secrets = Path.cwd() / ".secrets.toml" 666 if secrets.exists(): 667 files.append(str(secrets)) 668 return files 669 670 671 # Dynaconf instance — env vars always win (12-factor). Settings files are 672 # optional; when absent the defaults above provide the full key schema so 673 # env vars still override correctly. See ADR-024/025 for architecture. 674 _dynaconf = Dynaconf( 675 settings_files=_resolve_settings_files(), 676 environments=True, 677 envvar_prefix=False, 678 env_switcher="MCP_DEPLOYMENT_MODE", 679 ignore_unknown_envvars=True, 680 load_dotenv=False, 681 **_DEFAULTS, 682 validators=[ 683 # Port ranges 684 Validator("METRICS_PORT", gte=1, lte=65535), 685 # Positive integers 686 Validator("INGEST_STALLED_JOB_SECONDS", gte=1), 687 Validator("INGEST_TRANSIENT_MAX_ATTEMPTS", gte=1), 688 Validator("INGEST_RECLAIM_RETRY_DELAY_SECONDS", gte=0), 689 Validator("OIDC_DISCOVERY_MAX_ATTEMPTS", gte=1), 690 Validator("OIDC_DISCOVERY_BACKOFF_BASE", gte=0), 691 Validator("OIDC_DISCOVERY_BACKOFF_MAX", gte=0), 692 Validator("OFFICE_CAPTION_MAX_IMAGES", gte=0), 693 Validator("OFFICE_CAPTION_TIMEOUT_SECONDS", gt=0), 694 Validator("COLLABORA_TIMEOUT_SECONDS", gt=0), 695 Validator("PPTX_CAPTION_MAX_IMAGES", gte=0), 696 Validator("PPTX_CAPTION_TIMEOUT_SECONDS", gt=0), 697 Validator("QDRANT_INIT_MAX_ATTEMPTS", gte=1), -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/config.py line 111
- Likely real
- Uncertain, 45% confidence
Possible hardcoded password: 'None'
110 # Set TOKEN_STORAGE_DB to persist tokens across restarts. 111 "token_storage_db": None, 112 # Centralized backend (any SQLAlchemy URL). Wins over TOKEN_STORAGE_DB 113 # when set. Use postgresql+psycopg://user:pw@host/db for HA k8s 114 # deployments so pods can be stateless. The URL is passed through 115 # verbatim — TLS (e.g. ?sslmode=require) and every other parameter are 116 # read from it by libpq/psycopg; the server never rewrites it. See ADR-026. 117 "database_url": None, 118 # Postgres connection pool sizing (ADR-026 → "Concurrency model and 119 # pool sizing"). Per-pod defaults to 2 + 5 overflow = 7 max 120 # connections. psycopg connections are single-flight, so the pool 121 # only needs to cover typical multi-user MCP burst — not every 122 # potential in-flight tool call. Tune up with DATABASE_POOL_SIZE / 123 # DATABASE_MAX_OVERFLOW for high-traffic prod fleets. 124 "database_pool_size": 2, 125 "database_max_overflow": 5, 126 # Webhook delivery authentication (ADR-010): when set, registrations 127 # tell NC to add `Authorization: Bearer <secret>` to webhook deliveries 128 # and the receiver rejects unauthenticated requests. 129 "webhook_secret": None, 130 # Internal URL override for webhook registration; wins over 131 # NEXTCLOUD_MCP_SERVER_URL when set (e.g. split internal/external URLs). 132 "webhook_internal_url": None, 133 # Vector sync 134 "vector_sync_scan_interval": 300, 135 "vector_sync_processor_workers": 3, 136 # Consecutive failed index attempts (embed / Qdrant / transport — NOT a 137 # parse failure, which is terminal on the first try) before a document is 138 # dead-lettered instead of re-queued forever. See vector/dead_letter.py. 139 "vector_sync_max_index_failures": 5, 140 # Optional per-tier concurrency overrides (None = fall back to 141 # vector_sync_processor_workers). Consumed by the `worker` CLI per --tier. 142 "vector_sync_fast_concurrency": None, 143 "vector_sync_structured_concurrency": None, 144 "vector_sync_queue_max_size": 10000, 145 "vector_sync_metrics_refresh_interval": 20, 146 "vector_density_snapshot_enabled": True, 147 "vector_density_snapshot_interval": 300, 148 "vector_density_snapshot_max_documents": 50000, 149 "usage_stock_snapshot_interval": 86400, 150 "vector_ram_hnsw_overhead_factor": 1.5, 151 "vector_sync_user_poll_interval": 60, 152 "health_ready_refresh_interval": 15, 153 # Orphan-sweep at Pod startup (card #101). When True, delete any 154 # placeholders carrying a different / absent ``instance_id`` before 155 # the scanner's first cycle, so a Pod restart mid-batch doesn't 156 # leave work stuck behind the 5x-scan-interval staleness gate. 157 # Escape hatch only — leave on by default. 158 "vector_sync_orphan_sweep_enabled": True, 159 # System tag that marks files for hybrid (dense + BM25 sparse) indexing. 160 # The scanner indexes files carrying this tag; verify-on-read gates results 161 # on current membership of this tag (ADR-019). 162 "vector_sync_tag": "vector-index", 163 # System tag that marks files for keyword-only (BM25 sparse) indexing. 164 # Defaults to ``keyword-index`` (symmetric with ``vector_sync_tag``), so 165 # a user who creates + applies that tag gets keyword-only indexing out of the 166 # box. Files carrying it are indexed sparse-only (no dense embedding, no 167 # embedding cost) into the SAME collection as hybrid files; ``vector-index`` 168 # wins if a file carries both. Set empty to disable the second tag entirely. 169 "vector_sync_keyword_tag": "keyword-index", 170 # Which MIME types tagged-file discovery will enqueue. None (unset) means 171 # every type a registered processor can read; a comma-separated list 172 # narrows it, and an empty string indexes no files. 173 "vector_sync_indexable_mime_types": None, 174 # Mail tag (an IMAP keyword) restricting which messages are indexed. Empty 175 # (the default) indexes every message in every mailbox, which is the 176 # behaviour before this setting existed. Set it to a tag display name and 177 # only messages carrying that tag are indexed — the mail analogue of 178 # ``vector_sync_tag`` for files. 179 "mail_index_tag": "", 180 # Fail-safe against a flaky/empty tag-discovery read: number of *consecutive* 181 # scan cycles for which a given index mode's tag discovery must return zero 182 # files (while Qdrant still holds indexed points for that mode) before the 183 # scanner believes it and starts deleting. A transient empty read (e.g. a 184 # customer-hosted Nextcloud that intermittently answers the systemtag REPORT 185 # with an empty 207) is thereby treated as a failed read, not "all files 186 # gone" — preventing a delete/re-index churn loop. A sustained empty (a real 187 # mass-untag) still eventually deletes once the streak reaches this value. 188 "vector_sync_empty_discovery_delete_threshold": 3, 189 # Verify-on-read concurrency cap (ADR-019) 190 "verification_concurrency": 20, 191 # Qdrant 192 "qdrant_url": None, 193 "qdrant_location": None, 194 "qdrant_api_key": None, 195 "qdrant_collection": "nextcloud_content", 196 # Ollama 197 "ollama_base_url": None, 198 "ollama_embedding_model": "nomic-embed-text", 199 "ollama_verify_ssl": True, 200 # Ollama embeds a batch serially on whatever hardware it has, so the wall 201 # clock of one /api/embed call scales with the batch's total text, not its 202 # item count. Bound the request by characters (GH #1345). 203 "ollama_embed_max_batch_chars": 16000, 204 "ollama_embed_timeout": 120, 205 # OpenAI 206 "openai_api_key": None, 207 "openai_base_url": None, 208 "openai_embedding_model": "text-embedding-3-small", 209 # Bedrock (AWS) 210 "aws_region": None, 211 "aws_access_key_id": None, 212 "aws_secret_access_key": None, 213 "bedrock_embedding_model": None, 214 # Mistral 215 "mistral_api_key": None, 216 "mistral_embedding_model": "mistral-embed", 217 "mistral_base_url": None, 218 # Matryoshka output width requested from the embedding endpoint (None = 219 # the model's full width). Provider-agnostic: only one provider is active 220 # at a time, so a single knob covers openai/ollama/gateway. 221 "embedding_dimensions": None, 222 # Simple (fallback) embedding dimension 223 "simple_embedding_dimension": 384, 224 # Document chunking 225 "document_chunk_size": 2048, 226 "document_chunk_overlap": 200, 227 # Page-aware chunking for paginated docs (PDFs): split on page boundaries 228 # first so no chunk spans a page (exact page_number, clean snippets, and 229 # predictable ~1 chunk/page when chunk_size >= the largest page). 230 "document_chunk_page_aware": True, 231 # Greedy page-packing (density fix, Deck #636): merge consecutive sub-budget 232 # pages into one chunk instead of one-per-page. Off by default until the 233 # post-change density re-measure + pricing re-calibration land. 234 "document_chunk_page_pack": False, 235 # Hybrid-search RRF ranking constant. Qdrant's native RRF hardcodes k=2, 236 # where adjacent ranks differ by 33% and a single-list outlier outranks a 237 # document both retrievers agree on. 60 is the standard value (~2% per rank), 238 # which restores consensus as the dominant signal. 239 "vector_search_rrf_k": 60, 240 # --- Cross-encoder reranking (opt-in, per request) ---------------------- 241 # A cross-encoder reorders the retrieved candidates by scoring each against 242 # the query directly, which is more accurate than the fusion rank it 243 # replaces. Ships OFF because it adds an upstream round-trip to every 244 # search, and how expensive that is depends on how the reranker is deployed 245 # — which this server deliberately knows nothing about beyond the URL. 246 "search_rerank_enabled": False, 247 # Where reranking is served. A FULL endpoint URL, because the path differs 248 # per backend and there is no safe guess: Infinity serves ``POST /rerank``, 249 # vLLM ``POST /v1/rerank``, Cohere ``POST /v2/rerank``, the Astrolabe 250 # embedding gateway ``POST /v1/rerank``. All four speak the same Cohere 251 # request/response protocol, so choosing one is configuration, not code. 252 # 253 # Unset = derive it from EMBEDDING_GATEWAY_URL, which is what every existing 254 # deployment does. Set it to run reranking WITHOUT a gateway — the reason 255 # this exists (discussion #1354): Ollama serves no rerank models at all, so 256 # a self-hoster's only options are a hosted rerank API or a local Infinity / 257 # vLLM, and neither should require standing up a gateway first. 258 "search_rerank_url": None, 259 # Static bearer for SEARCH_RERANK_URL (a Cohere API key, an Infinity 260 # ``--api-key``). Wins over the gateway's M2M OIDC credentials when both are 261 # present. Unset = send no Authorization header, which is correct for a 262 # local Infinity/vLLM on a private network. 263 "search_rerank_api_key": None, 264 # Rerank model, addressed the way the CONFIGURED ENDPOINT expects — which 265 # differs between the two, so the default is only right for one of them. 266 # 267 # Against the embedding gateway the ``<provider>/`` prefix is REQUIRED, not 268 # stylistic: the gateway splits on the FIRST slash to select a backend, so a 269 # bare ``BAAI/bge-reranker-v2-m3`` asks for a provider named ``BAAI`` and 270 # 503s — and because a failed rerank degrades to retrieval order 271 # (``reranked: false``) rather than erroring, that looks like "reranking does 272 # nothing" instead of like a misconfiguration. The prefix is also the backend 273 # selector: ``local/`` for a self-hosted cross-encoder, ``bedrock/`` or 274 # ``openrouter/`` for a hosted one. 275 # 276 # Against a direct SEARCH_RERANK_URL there is no such routing layer, so use 277 # the bare id the server serves (``BAAI/bge-reranker-v2-m3`` for 278 # Infinity/vLLM, ``rerank-v3.5`` for Cohere). The relevance calibration 279 # (ADR-034) strips known provider prefixes, so both spellings resolve to the 280 # same fitted curve. 281 "search_rerank_model": "local/BAAI/bge-reranker-v2-m3", 282 # Candidates handed to the reranker. Reranking can only reorder what 283 # retrieval supplied, so this depth — not the caller's ``limit`` — bounds 284 # how much a reranker can improve. Reranking just the rows a normal search 285 # returns captures little of the available gain. 286 # 287 # Treat this as a ceiling rather than a starting point. Under 288 # ``granularity="document"`` the grouped prefetch is bounded by 289 # MAX_DOCUMENT_PREFETCH, and requesting more groups than the prefetch can 290 # fill makes Qdrant widen its grouping search and reorder the head of the 291 # result set before the reranker sees it. Going deeper requires raising 292 # MAX_DOCUMENT_PREFETCH first, which carries its own latency cost. 293 "search_rerank_pool_size": 200, 294 # Generous headroom over a normal rerank, not a target. Exceeding it 295 # degrades to retrieval order rather than failing the search. 296 "search_rerank_timeout_seconds": 30.0, 297 # Concurrent rerank calls this process keeps in flight against the reranker. 298 # 299 # Conservative at 1 because reranking is the heaviest request this server 300 # makes of a service it may share with its own embedding traffic and with 301 # other callers. Bounding our own concurrency keeps a burst of searches from 302 # queueing unbounded work upstream, and keeps rerank latency predictable 303 # here. It is a client-side courtesy, not a throughput control: what the 304 # reranker does with the request, and how it schedules against everything 305 # else it serves, is its business. Raise it if yours has headroom — a CPU 306 # cross-encoder almost certainly does not. 307 "search_rerank_max_concurrency": 1, 308 # --- SAR export redaction (ADR-040) ------------------------------------- 309 # Names are detected by the embedding gateway's ``POST /v1/ner``, so SAR 310 # export is available only with EMBEDDING_GATEWAY_URL set. 311 # NER model, addressed the gateway way (``<provider>/<model>``). 312 # Subject access request cases and redacted export (ADR-040). Off by 313 # default: a deployment opts in, since only some need it. Also requires 314 # vector sync and EMBEDDING_GATEWAY_URL (for name detection). 315 "sar_enabled": False, 316 "ner_model": "local/urchade/gliner_multi_pii-v1", 317 # Per-request budget. Export runs in the background, so this only needs to 318 # cover one batch on the slowest backend (CPU GLiNER: ~570 chars/s). 319 "ner_timeout_seconds": 120.0, 320 # Texts (of up to 2,000 chars) per /v1/ner request. Small for CPU GLiNER, 321 # which must finish a batch inside the gateway's own upstream timeout; raise 322 # it (e.g. 32) on a GPU backend. 323 "ner_batch_size": 8, 324 # Minimum model confidence for a span to count as a person. Lower raises 325 # recall at the cost of over-redaction, which is the safe direction for a 326 # disclosure; 0.5 is GLiNER's customary operating point. 327 "ner_threshold": 0.5, 328 # Chunking config generation. Bump whenever chunker behaviour changes (size, 329 # overlap, page-aware, page-pack, split strategy) so the pricing model's 330 # density reference can't silently go stale. Pinned in stripe-catalog.tf. 331 "chunking_config_version": 1, 332 # PDF parse isolation (OOM guard) 333 "document_pdf_graphics_limit": 1000, 334 "document_parse_timeout_seconds": 120.0, 335 # Optional wall-clock cap (seconds) on the SYNCHRONOUS parse inside the 336 # nc_webdav_read_file MCP tool. None (default) = disabled: an interactive read 337 # is bounded only by the underlying processor timeout (DOCLING_TIMEOUT / 338 # DOCUMENT_OCR_TIMEOUT_SECONDS). Set a client-friendly bound (e.g. 45-60s) so a 339 # slow VLM/OCR convert returns base64 quickly instead of blocking past an MCP 340 # client's own timeout. Never applies to the async ingest/worker path (ADR-032). 341 "document_read_timeout_seconds": None, 342 "document_parse_mem_limit_mb": 1536, 343 # Pre-parse size cap (MB): PDFs larger than this fail fast with reason 344 # "oversize" instead of burning the OCR timeout to 0 chars on a pathological 345 # file. 0 disables the guard. 346 "document_max_pdf_size_mb": 50.0, 347 # Pre-flight cap (MB) on nc_webdav_write_file's content: a huge PUT built 348 # from a single in-memory MCP tool argument (no chunked/streaming upload 349 # exists for writes, unlike the read-side stream_to_file path) risks 350 # timing out or exhausting memory rather than failing predictably. 0 351 # disables the guard. 352 "webdav_write_max_mb": 50.0, 353 # Transport security (MCP SDK TransportSecuritySettings). Default False 354 # preserves the behaviour hardcoded before these knobs existed: MCP 1.23+ 355 # auto-enables localhost-only host checking, which breaks k8s/Docker service 356 # DNS names (docs/MCP-1.23-DNS-REBINDING-FIX.md). Enable it plus 357 # MCP_ALLOWED_HOSTS when the server is reachable from a browser. 358 "mcp_dns_rebinding_protection": False, 359 # Comma-separated Host / Origin allowlists, honoured only when the above is 360 # enabled. Empty means "the SDK's own defaults". 361 "mcp_allowed_hosts": "", 362 "mcp_allowed_origins": "", 363 # Comma-separated CORS origin allowlist. "*" preserves today's behaviour. 364 "cors_allow_origins": "*", 365 # Page ceiling for markdown reconstruction (see document_markdown_max_pages). 366 "document_markdown_max_pages": 150, 367 # Pages per pypdfium2 extraction window (see document_parse_page_window). 368 "document_parse_page_window": 100, 369 # Concurrent isolated parse subprocesses (see document_parse_process_slots). 370 "document_parse_process_slots": 2, 371 # Stream ingest downloads to disk instead of buffering them in memory. 372 "document_stream_download_enabled": True, 373 # Directory for ingest spool files (default: the system temp dir). 374 "document_spool_dir": None, 375 # Tier-0 classifier (records classification metrics on the tiered path) 376 "document_classify_enabled": True, 377 # Tiered PDF pipeline: pypdfium2 is the default/only hot-path extractor; 378 # "pymupdf" is a deprecated rollback escape hatch. OCR (tier-3) is the only 379 # escalation target and is off by default (no provider wired yet). 380 "document_tier1_engine": "pypdfium2", 381 "document_ocr_enabled": False, 382 # OCR backend: "auto" picks gateway (if EMBEDDING_GATEWAY_URL) else mistral 383 # (if MISTRAL_API_KEY); "gateway"/"mistral" force one; "docling" routes scanned 384 # PDFs to a docling-serve instance (DOCLING_API_URL); "none" disables. "auto" 385 # never selects docling (it needs a self-hosted URL, so it must be explicit). 386 # The gateway routes on the model's "<provider>/" prefix, so it serves Mistral, 387 # surya (in-cluster GPU over the tailnet), etc. — one configurable OCR tier. 388 "document_ocr_provider": "auto", 389 # Provider-namespaced OCR model id (gateway routes on the prefix; the direct 390 # mistral backend strips it). e.g. "mistral/mistral-ocr-latest" (Mistral) or 391 # "surya/surya-ocr-2" (surya via the gateway) — never hard-coded, only this 392 # default. 393 "document_ocr_model": "mistral/mistral-ocr-latest", 394 # OCR escalation triggers (tier-0). A page is OCR-worthy when its text is 395 # near-empty (< min_page_chars) OR low-quality (< min_text_quality) OR (when 396 # detect_scanned) mostly a raster image; a doc escalates when the OCR-worthy 397 # page fraction reaches page_fraction. Calibrate min_text_quality from the 398 # astrolabe_document_text_quality histogram per tenant. 399 "document_ocr_min_text_quality": 0.5, 400 "document_ocr_page_fraction": 0.5, 401 "document_ocr_min_page_chars": 16, 402 "document_ocr_detect_scanned": True, 403 # Tier-0 glyph-corruption trigger. When the fast (pypdfium2) extraction's 404 # doc-level C0-control-char ratio exceeds this, the text layer is treated as 405 # glyph-corrupt (a broken /ToUnicode mapping leaking raw glyph codes) and the 406 # doc escalates fast->structured (pymupdf re-extracts it correctly -- no OCR). 407 # 0 disables. Clean docs sit ~0; affected PDFs measured ~1-11% in testing. 408 "document_glyph_corruption_ratio": 0.02, 409 # OCR backend request timeout (seconds). Slow scanned newspapers can take 410 # 20-60s; raise/lower per tenant. Configurable so a tenant isn't stuck with 411 # the 180s default when its gateway has its own shorter ceiling. 412 "document_ocr_timeout_seconds": 180.0, 413 # OCR execution mode (Deck #332). "sync" (default) transcribes inline via the 414 # backend's synchronous path. "batch" routes to the gateway's async Batch OCR 415 # job (~50% cheaper, minutes-hours latency) for large-corpus backfill — opt-in 416 # and gateway-routed. It requires EMBEDDING_GATEWAY_URL (rejected at startup 417 # otherwise, see __post_init__) and the per-tier procrastinate path; the 418 # inline/memory pool can't defer a poll, so batch raises there rather than 419 # silently downgrading to sync. 420 "document_ocr_mode": "sync", 421 # Seconds between batch-job polls (the procrastinate re-enqueue delay). Each 422 # poll re-runs the tier; keep it well above a few seconds. 423 "document_ocr_batch_poll_seconds": 120, 424 # Observability 425 "metrics_enabled": True, 426 "metrics_port": 9090, 427 "otel_exporter_otlp_endpoint": None, 428 "otel_exporter_verify_ssl": None, 429 "otel_service_name": "nextcloud-mcp-server", 430 "pyroscope_enabled": False, 431 "pyroscope_server_address": None, 432 # Pod identity from the Kubernetes downward API (chart-injected). Facts 433 # about the pod, not user config — absent off-Kubernetes. 434 "pod_namespace": None, 435 "pod_name": None, 436 "log_format": "text", 437 "log_level": "INFO", 438 "log_include_trace_context": True, 439 # Reverse-proxy trust list for X-Forwarded-For (GH #1284). Passed to 440 # uvicorn; None keeps uvicorn's own resolution (which itself falls back to 441 # 127.0.0.1). Declared here so the key is settable from settings.toml, not 442 # only as the env var uvicorn already reads on its own. 443 "forwarded_allow_ips": None, 444 # Document processing (no master switch: each optional processor has its own 445 # ENABLE_* flag, and parsing on read is the caller's per-call decision) 446 "document_processor": "unstructured", 447 "enable_unstructured": False, 448 "unstructured_api_url": "http://unstructured:8000", 449 "unstructured_timeout": 120, 450 "unstructured_strategy": "auto", 451 "unstructured_languages": "eng,deu", 452 "progress_interval": 10, 453 "enable_tesseract": False, 454 "tesseract_cmd": None, 455 "tesseract_lang": "eng", 456 # pymupdf is the built-in "structured" tier and is always available -- there 457 # is no enable flag for it, only these two knobs. 458 "pymupdf_extract_images": True, 459 "pymupdf_image_dir": None, 460 "enable_custom_processor": False, 461 "custom_processor_url": None, 462 "custom_processor_types": "application/pdf", 463 "custom_processor_name": "custom", 464 "custom_processor_api_key": None, 465 "custom_processor_timeout": 60, 466 # Docling document-parsing backend (docling-serve HTTP API). One docling-serve 467 # instance feeds two touchpoints: the images-only DoclingProcessor 468 # (find_processor path, priority 20) and — when DOCUMENT_OCR_PROVIDER=docling — 469 # the PDF OCR backend for scanned/no-text-layer PDFs. The same processor can be 470 # force-selected per call to parse a text-layer PDF (tables/partial text). URL 471 # unset -> the image processor is not registered and the OCR backend resolves to 472 # None. See ADR-031. 473 "enable_docling": False, 474 "docling_api_url": None, 475 "docling_timeout": 120, 476 # docling-serve OCR language codes. The default engine (EasyOCR) uses 2-letter 477 # codes ("en","de"); a Tesseract-backed instance wants "eng","deu". Engine- 478 # dependent, so keep it operator-tunable (see ADR-031). 479 "docling_ocr_lang": "en,de", 480 # Run OCR on IMAGES routed to the DoclingProcessor (find_processor path). The 481 # docling OCR *backend* (scanned PDFs) always OCRs regardless of this flag. 482 "docling_do_ocr": True, 483 # Which docling-serve pipeline to request: "standard" (classic layout+OCR, 484 # default, unchanged) or "vlm" (Vision-LLM OCR). "vlm" needs a docling-serve 485 # instance configured with VLM presets (see ADR-032). 486 "docling_pipeline": "standard", 487 # VLM preset name sent when docling_pipeline == "vlm". None -> docling-serve 488 # picks its own DOCLING_SERVE_DEFAULT_VLM_PRESET. Preset names are server-defined. 489 "docling_vlm_preset": None, 490 # Caption raster pictures embedded in .pptx/.docx/.xlsx via the same 491 # docling-serve instance (ADR-037). Explicit opt-in beyond a bare 492 # DOCLING_API_URL, like DOCUMENT_OCR_PROVIDER=docling needs its own 493 # selection: a deployment that only wants docling for scanned-PDF OCR 494 # shouldn't start captioning every picture in every document for free. The 495 # native readers have no rendering engine, so this only reaches actual 496 # pictures -- native vector drawings (SmartArt, shapes) are unaffected. 497 "office_caption_images": False, 498 # Cap on pictures captioned per file (one docling-serve round trip each). 499 # nc_webdav_read_file blocks synchronously for the whole parse, so this 500 # bounds the worst case rather than leaving it to the file's picture count. 501 "office_caption_max_images": 8, 502 # Per-picture docling-serve request timeout (seconds). Deliberately short 503 # and independent of DOCLING_TIMEOUT/DOCUMENT_OCR_TIMEOUT_SECONDS (other 504 # touchpoints, other latency profiles): a caption is meant to be a quick 505 # per-picture round trip repeated up to OFFICE_CAPTION_MAX_IMAGES times, not 506 # a single long convert. Raise it if DOCLING_PIPELINE=vlm makes captions 507 # time out (VLM is far slower than the standard pipeline -- see ADR-032). 508 "office_caption_timeout_seconds": 15.0, 509 # Collabora Online (coolwsd) base URL for converting legacy .doc/.xls/.ppt 510 # and ODF .odt/.ods/.odp to OOXML, which the native readers then parse 511 # (ADR-039). Unset = those types are not claimed. coolwsd only answers 512 # convert-to for clients in its net.post_allow list (private ranges by 513 # default). 514 "collabora_url": None, 515 # Per-file convert-to request timeout (seconds). A typical document 516 # converts in well under a second; the headroom is for large workbooks. 517 "collabora_timeout_seconds": 60.0, 518 # Deprecated PPTX_CAPTION_* spellings (0.193.0, pptx-only) of the three keys 519 # above; see _apply_legacy_caption_settings. Declared so dynaconf reads 520 # them at all, with the same defaults so their validators still apply. 521 "pptx_caption_images": False, 522 "pptx_caption_max_images": 8, 523 "pptx_caption_timeout_seconds": 15.0, 524 # Tag-based file exclusion (issue #710): comma-separated list of 525 # Nextcloud system tag names. Files/folders carrying any of these tags 526 # are hidden from WebDAV MCP tools. Empty = feature off. 527 "excluded_tags": "", 528 # MCP decomposition hook points (design §10). Every default reproduces 529 # the current monolithic behavior; self-hosters who set none are 530 # unaffected. See docs/architecture/mcp-decomposition.md (sibling repo). 531 "embedding_provider": "autodetect", # autodetect | gateway 532 # Ingest queue backend (Deck #183). None → ``memory`` (the in-process anyio 533 # queue): procrastinate is strictly opt-in, even on a Postgres DATABASE_URL. 534 # Set ``postgres`` explicitly to split ingest into a procrastinate worker; 535 # that requires a PostgreSQL DATABASE_URL. 536 "ingest_queue": None, # memory | postgres 537 # Process role for the per-tenant two-pod model (Deck #183). ``api`` runs the 538 # MCP/query server + scanner (defers jobs); the ``worker`` role is the 539 # `nextcloud-mcp-server worker` process that drains the queue. ``all`` keeps 540 # the monolithic behaviour (API + in-process SQLite pool). 541 "mcp_role": "all", # api | worker | all 542 # Reclaim an ingest job orphaned in ``doing`` by a crashed worker once its 543 # worker heartbeat is this many seconds stale (Deck #183). Default is well 544 # above the longest expected document; raise it for slow embedding backends. 545 "ingest_stalled_job_seconds": 300, 546 # Backstop reclaim: also re-queue a job stuck in ``doing`` for this many seconds 547 # regardless of worker liveness. The heartbeat threshold above only catches DEAD 548 # workers; a job whose OWN completion crashed (e.g. an unhandled queueing_lock 549 # UniqueViolation on the doing->todo retry) is stranded in ``doing`` under a LIVE, 550 # heart-beating worker and is invisible to the heartbeat sweep. 551 # This fires purely on time-in-``doing``, NOT on liveness, so it can't tell "stuck" 552 # from "legitimately slow": if a single HEALTHY process_document attempt ever ran 553 # past this, it'd be re-queued mid-flight and two workers could process the doc 554 # concurrently. That's safe ONLY because ingest re-runs are idempotent (uuid5 555 # Qdrant point IDs — see the queue module's "Design notes"), NOT because the 556 # reclaim distinguishes the two — so keep the default comfortably above the longest 557 # real single attempt. OCR batch polling releases the worker between polls 558 # (BatchPending -> retry_in), so a job never legitimately holds ``doing`` this 559 # long; 1800s is deep headroom. (Deck: ingest doing-strand reclaim.) 560 "ingest_doing_max_seconds": 1800, 561 # Delete succeeded ingest jobs (keeps the queue table lean + the KEDA 562 # queue-depth metric clean). Set false to retain succeeded rows for audit 563 # (note: indexing success is also recorded in logs/metrics regardless). 564 "ingest_delete_succeeded_jobs": True, 565 # Whether the procrastinate worker uses LISTEN/NOTIFY for job pickup (Deck 566 # #424). True (default) = near-instant wakeup via a long-lived LISTEN 567 # connection. Set false to run POLL-ONLY when DATABASE_URL routes through a 568 # transaction-mode connection pooler (PgBouncer transaction mode), which is 569 # incompatible with LISTEN/NOTIFY — the LISTEN registration is dropped when 570 # the backend returns to the pool. Poll-only trades a few seconds of pickup 571 # latency (fetch_job_polling_interval) for pooler safety; job queries still 572 # multiplex through the pooler. Snapshotted at worker startup; needs a 573 # restart to change. 574 "ingest_listen_notify": True, 575 # Per-tier escalation on the procrastinate (postgres) ingest path (Deck 576 # #323). When true, a document that a tier cannot parse well is requeued onto 577 # the next tier's queue (fast -> structured -> ocr) via a native procrastinate 578 # queue-hop. When false the ``fast`` tier is terminal -- reproduces the 579 # pre-#323 behaviour where the cheap tier's output is indexed as-is. No effect 580 # on the in-process ``memory`` backend, which keeps the inline escalation. 581 # HOT: re-read per job (process_document_task), so it takes effect on the next 582 # job -- unlike INGEST_TRANSIENT_MAX_ATTEMPTS, which is snapshotted at worker 583 # startup and needs a restart. 584 "ingest_escalation_enabled": True, 585 # Global cap on SAME-tier retries for transient infra errors (doc fetch / 586 # embed / Qdrant blips) on the procrastinate path. Parse-quality failures 587 # escalate (one parse attempt per tier) and do NOT consume this budget; only 588 # whitelisted transient exceptions retry in place. Shared across tiers because 589 # a queue-hop cannot reset a per-tier counter (see TieredEscalationStrategy). 590 # Snapshotted at worker startup (blueprint build); restart to change it. 591 "ingest_transient_max_attempts": 5, 592 # Delay (seconds) before a reclaimed stalled job is re-run. A stall is often 593 # systemic (Qdrant/embedding outage), so reclaiming every crashed job at 594 # now() would thundering-herd a recovering dependency every reclaim tick 595 # (*/5min), bypassing TieredEscalationStrategy's per-job backoff. A small 596 # fixed delay staggers the retry. 0 = immediate (legacy behaviour). 597 "ingest_reclaim_retry_delay_seconds": 30, 598 "collection_metadata_source": "qdrant", # qdrant | api 599 # CP base URL for COLLECTION_METADATA_SOURCE=api (e.g. http://control-plane). 600 # Required only when the source is api. 601 "collection_metadata_api_url": None, 602 "embedding_gateway_url": None, # required when embedding_provider=gateway 603 # Provider-namespaced model the gateway serves, "<provider>/<model>" 604 # (the gateway routes on the "/"-prefix; mistral/mistral-embed → Mistral 605 # for the MVP). Only consulted when embedding_provider=gateway. 606 "embedding_gateway_model": "mistral/mistral-embed", 607 # Gateway auth: the MCP server is an OIDC *client* in the gateway's own 608 # M2M realm (parallel to, and distinct from, the tenant realm it already 609 # serves). It obtains a client-credentials token and the gateway maps the 610 # client-id → the tenant's underlying provider API key. All four unset = 611 # call the gateway unauthenticated (matches today's not-yet-authed gateway). 612 "embedding_gateway_token_url": None, # M2M token endpoint 613 "embedding_gateway_client_id": None, 614 "embedding_gateway_client_secret": None, 615 "embedding_gateway_scope": None, # e.g. astrolabe-embedding-gateway/embed 616 # Query-side ACL pre-filter (design §11). OFF by default: a Qdrant 617 # `match any` on `acl_hash` excludes points missing the key, so enabling 618 # this before a real ACL backfill would silently drop legacy results. 619 # verify-on-read remains the correctness backstop regardless. 620 "acl_prefilter_enabled": False, 621 # Usage metering (Deck #67, control-plane usage-metering.md). OFF by 622 # default so OSS self-hosters don't accrue a metering table or write 623 # overhead; Astrolabe Cloud provisioning sets it true. When on, billable 624 # ops record rows into the app-DB usage_events table (best-effort). 625 "usage_metering_enabled": False, 626 } 627 628 629 def _resolve_settings_files() -> list[str]: 630 """Find optional external settings files. 631 632 Priority: 633 1. NEXTCLOUD_MCP_SETTINGS_FILE env var (absolute or relative path). 634 If set but the file does not exist, raise FileNotFoundError — 635 silently falling back to defaults on a typo would be a footgun. 636 .secrets.toml is looked for alongside the explicit file. 637 2. Otherwise ./settings.toml in cwd (for docker / dev workflows), 638 with .secrets.toml also looked for in cwd. 639 640 Returns an empty list if nothing is configured — that's fine, defaults 641 and env vars still apply. 642 """ 643 files: list[str] = [] 644 # A legitimate os.environ read: this runs BEFORE the dynaconf instance is 645 # built (it computes the settings_files list passed to Dynaconf), so it 646 # cannot go through dynaconf — you can't read the location of the settings 647 # file from the settings file. This + the MCP_DEPLOYMENT_MODE env_switcher 648 # are dynaconf's own bootstrap. All config *values* are dynaconf-driven; the 649 # only other reads are the legacy-env deprecation check and 650 # _warn_unknown_env_vars, which inspect env var NAMES, not values. 651 explicit = os.environ.get("NEXTCLOUD_MCP_SETTINGS_FILE") 652 if explicit: 653 p = Path(explicit) 654 if not p.exists(): 655 raise FileNotFoundError( 656 f"NEXTCLOUD_MCP_SETTINGS_FILE points to a file that does " 657 f"not exist: {explicit}" 658 ) 659 files.append(str(p)) 660 secrets = p.parent / ".secrets.toml" 661 else: 662 cwd_settings = Path.cwd() / "settings.toml" 663 if cwd_settings.exists(): 664 files.append(str(cwd_settings)) 665 secrets = Path.cwd() / ".secrets.toml" 666 if secrets.exists(): 667 files.append(str(secrets)) 668 return files 669 670 671 # Dynaconf instance — env vars always win (12-factor). Settings files are 672 # optional; when absent the defaults above provide the full key schema so 673 # env vars still override correctly. See ADR-024/025 for architecture. 674 _dynaconf = Dynaconf( 675 settings_files=_resolve_settings_files(), 676 environments=True, 677 envvar_prefix=False, 678 env_switcher="MCP_DEPLOYMENT_MODE", 679 ignore_unknown_envvars=True, 680 load_dotenv=False, 681 **_DEFAULTS, 682 validators=[ 683 # Port ranges 684 Validator("METRICS_PORT", gte=1, lte=65535), 685 # Positive integers 686 Validator("INGEST_STALLED_JOB_SECONDS", gte=1), 687 Validator("INGEST_TRANSIENT_MAX_ATTEMPTS", gte=1), 688 Validator("INGEST_RECLAIM_RETRY_DELAY_SECONDS", gte=0), 689 Validator("OIDC_DISCOVERY_MAX_ATTEMPTS", gte=1), 690 Validator("OIDC_DISCOVERY_BACKOFF_BASE", gte=0), 691 Validator("OIDC_DISCOVERY_BACKOFF_MAX", gte=0), 692 Validator("OFFICE_CAPTION_MAX_IMAGES", gte=0), 693 Validator("OFFICE_CAPTION_TIMEOUT_SECONDS", gt=0), 694 Validator("COLLABORA_TIMEOUT_SECONDS", gt=0), 695 Validator("PPTX_CAPTION_MAX_IMAGES", gte=0), 696 Validator("PPTX_CAPTION_TIMEOUT_SECONDS", gt=0), 697 Validator("QDRANT_INIT_MAX_ATTEMPTS", gte=1), 698 Validator("QDRANT_INIT_BACKOFF_BASE", gte=0), 699 Validator("QDRANT_INIT_BACKOFF_MAX", gte=0), 700 Validator("OLLAMA_EMBED_MAX_BATCH_CHARS", gte=1), -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/config.py line 129
- Likely real
- Uncertain, 45% confidence
Possible hardcoded password: 'None'
128 # and the receiver rejects unauthenticated requests. 129 "webhook_secret": None, 130 # Internal URL override for webhook registration; wins over 131 # NEXTCLOUD_MCP_SERVER_URL when set (e.g. split internal/external URLs). 132 "webhook_internal_url": None, 133 # Vector sync 134 "vector_sync_scan_interval": 300, 135 "vector_sync_processor_workers": 3, 136 # Consecutive failed index attempts (embed / Qdrant / transport — NOT a 137 # parse failure, which is terminal on the first try) before a document is 138 # dead-lettered instead of re-queued forever. See vector/dead_letter.py. 139 "vector_sync_max_index_failures": 5, 140 # Optional per-tier concurrency overrides (None = fall back to 141 # vector_sync_processor_workers). Consumed by the `worker` CLI per --tier. 142 "vector_sync_fast_concurrency": None, 143 "vector_sync_structured_concurrency": None, 144 "vector_sync_queue_max_size": 10000, 145 "vector_sync_metrics_refresh_interval": 20, 146 "vector_density_snapshot_enabled": True, 147 "vector_density_snapshot_interval": 300, 148 "vector_density_snapshot_max_documents": 50000, 149 "usage_stock_snapshot_interval": 86400, 150 "vector_ram_hnsw_overhead_factor": 1.5, 151 "vector_sync_user_poll_interval": 60, 152 "health_ready_refresh_interval": 15, 153 # Orphan-sweep at Pod startup (card #101). When True, delete any 154 # placeholders carrying a different / absent ``instance_id`` before 155 # the scanner's first cycle, so a Pod restart mid-batch doesn't 156 # leave work stuck behind the 5x-scan-interval staleness gate. 157 # Escape hatch only — leave on by default. 158 "vector_sync_orphan_sweep_enabled": True, 159 # System tag that marks files for hybrid (dense + BM25 sparse) indexing. 160 # The scanner indexes files carrying this tag; verify-on-read gates results 161 # on current membership of this tag (ADR-019). 162 "vector_sync_tag": "vector-index", 163 # System tag that marks files for keyword-only (BM25 sparse) indexing. 164 # Defaults to ``keyword-index`` (symmetric with ``vector_sync_tag``), so 165 # a user who creates + applies that tag gets keyword-only indexing out of the 166 # box. Files carrying it are indexed sparse-only (no dense embedding, no 167 # embedding cost) into the SAME collection as hybrid files; ``vector-index`` 168 # wins if a file carries both. Set empty to disable the second tag entirely. 169 "vector_sync_keyword_tag": "keyword-index", 170 # Which MIME types tagged-file discovery will enqueue. None (unset) means 171 # every type a registered processor can read; a comma-separated list 172 # narrows it, and an empty string indexes no files. 173 "vector_sync_indexable_mime_types": None, 174 # Mail tag (an IMAP keyword) restricting which messages are indexed. Empty 175 # (the default) indexes every message in every mailbox, which is the 176 # behaviour before this setting existed. Set it to a tag display name and 177 # only messages carrying that tag are indexed — the mail analogue of 178 # ``vector_sync_tag`` for files. 179 "mail_index_tag": "", 180 # Fail-safe against a flaky/empty tag-discovery read: number of *consecutive* 181 # scan cycles for which a given index mode's tag discovery must return zero 182 # files (while Qdrant still holds indexed points for that mode) before the 183 # scanner believes it and starts deleting. A transient empty read (e.g. a 184 # customer-hosted Nextcloud that intermittently answers the systemtag REPORT 185 # with an empty 207) is thereby treated as a failed read, not "all files 186 # gone" — preventing a delete/re-index churn loop. A sustained empty (a real 187 # mass-untag) still eventually deletes once the streak reaches this value. 188 "vector_sync_empty_discovery_delete_threshold": 3, 189 # Verify-on-read concurrency cap (ADR-019) 190 "verification_concurrency": 20, 191 # Qdrant 192 "qdrant_url": None, 193 "qdrant_location": None, 194 "qdrant_api_key": None, 195 "qdrant_collection": "nextcloud_content", 196 # Ollama 197 "ollama_base_url": None, 198 "ollama_embedding_model": "nomic-embed-text", 199 "ollama_verify_ssl": True, 200 # Ollama embeds a batch serially on whatever hardware it has, so the wall 201 # clock of one /api/embed call scales with the batch's total text, not its 202 # item count. Bound the request by characters (GH #1345). 203 "ollama_embed_max_batch_chars": 16000, 204 "ollama_embed_timeout": 120, 205 # OpenAI 206 "openai_api_key": None, 207 "openai_base_url": None, 208 "openai_embedding_model": "text-embedding-3-small", 209 # Bedrock (AWS) 210 "aws_region": None, 211 "aws_access_key_id": None, 212 "aws_secret_access_key": None, 213 "bedrock_embedding_model": None, 214 # Mistral 215 "mistral_api_key": None, 216 "mistral_embedding_model": "mistral-embed", 217 "mistral_base_url": None, 218 # Matryoshka output width requested from the embedding endpoint (None = 219 # the model's full width). Provider-agnostic: only one provider is active 220 # at a time, so a single knob covers openai/ollama/gateway. 221 "embedding_dimensions": None, 222 # Simple (fallback) embedding dimension 223 "simple_embedding_dimension": 384, 224 # Document chunking 225 "document_chunk_size": 2048, 226 "document_chunk_overlap": 200, 227 # Page-aware chunking for paginated docs (PDFs): split on page boundaries 228 # first so no chunk spans a page (exact page_number, clean snippets, and 229 # predictable ~1 chunk/page when chunk_size >= the largest page). 230 "document_chunk_page_aware": True, 231 # Greedy page-packing (density fix, Deck #636): merge consecutive sub-budget 232 # pages into one chunk instead of one-per-page. Off by default until the 233 # post-change density re-measure + pricing re-calibration land. 234 "document_chunk_page_pack": False, 235 # Hybrid-search RRF ranking constant. Qdrant's native RRF hardcodes k=2, 236 # where adjacent ranks differ by 33% and a single-list outlier outranks a 237 # document both retrievers agree on. 60 is the standard value (~2% per rank), 238 # which restores consensus as the dominant signal. 239 "vector_search_rrf_k": 60, 240 # --- Cross-encoder reranking (opt-in, per request) ---------------------- 241 # A cross-encoder reorders the retrieved candidates by scoring each against 242 # the query directly, which is more accurate than the fusion rank it 243 # replaces. Ships OFF because it adds an upstream round-trip to every 244 # search, and how expensive that is depends on how the reranker is deployed 245 # — which this server deliberately knows nothing about beyond the URL. 246 "search_rerank_enabled": False, 247 # Where reranking is served. A FULL endpoint URL, because the path differs 248 # per backend and there is no safe guess: Infinity serves ``POST /rerank``, 249 # vLLM ``POST /v1/rerank``, Cohere ``POST /v2/rerank``, the Astrolabe 250 # embedding gateway ``POST /v1/rerank``. All four speak the same Cohere 251 # request/response protocol, so choosing one is configuration, not code. 252 # 253 # Unset = derive it from EMBEDDING_GATEWAY_URL, which is what every existing 254 # deployment does. Set it to run reranking WITHOUT a gateway — the reason 255 # this exists (discussion #1354): Ollama serves no rerank models at all, so 256 # a self-hoster's only options are a hosted rerank API or a local Infinity / 257 # vLLM, and neither should require standing up a gateway first. 258 "search_rerank_url": None, 259 # Static bearer for SEARCH_RERANK_URL (a Cohere API key, an Infinity 260 # ``--api-key``). Wins over the gateway's M2M OIDC credentials when both are 261 # present. Unset = send no Authorization header, which is correct for a 262 # local Infinity/vLLM on a private network. 263 "search_rerank_api_key": None, 264 # Rerank model, addressed the way the CONFIGURED ENDPOINT expects — which 265 # differs between the two, so the default is only right for one of them. 266 # 267 # Against the embedding gateway the ``<provider>/`` prefix is REQUIRED, not 268 # stylistic: the gateway splits on the FIRST slash to select a backend, so a 269 # bare ``BAAI/bge-reranker-v2-m3`` asks for a provider named ``BAAI`` and 270 # 503s — and because a failed rerank degrades to retrieval order 271 # (``reranked: false``) rather than erroring, that looks like "reranking does 272 # nothing" instead of like a misconfiguration. The prefix is also the backend 273 # selector: ``local/`` for a self-hosted cross-encoder, ``bedrock/`` or 274 # ``openrouter/`` for a hosted one. 275 # 276 # Against a direct SEARCH_RERANK_URL there is no such routing layer, so use 277 # the bare id the server serves (``BAAI/bge-reranker-v2-m3`` for 278 # Infinity/vLLM, ``rerank-v3.5`` for Cohere). The relevance calibration 279 # (ADR-034) strips known provider prefixes, so both spellings resolve to the 280 # same fitted curve. 281 "search_rerank_model": "local/BAAI/bge-reranker-v2-m3", 282 # Candidates handed to the reranker. Reranking can only reorder what 283 # retrieval supplied, so this depth — not the caller's ``limit`` — bounds 284 # how much a reranker can improve. Reranking just the rows a normal search 285 # returns captures little of the available gain. 286 # 287 # Treat this as a ceiling rather than a starting point. Under 288 # ``granularity="document"`` the grouped prefetch is bounded by 289 # MAX_DOCUMENT_PREFETCH, and requesting more groups than the prefetch can 290 # fill makes Qdrant widen its grouping search and reorder the head of the 291 # result set before the reranker sees it. Going deeper requires raising 292 # MAX_DOCUMENT_PREFETCH first, which carries its own latency cost. 293 "search_rerank_pool_size": 200, 294 # Generous headroom over a normal rerank, not a target. Exceeding it 295 # degrades to retrieval order rather than failing the search. 296 "search_rerank_timeout_seconds": 30.0, 297 # Concurrent rerank calls this process keeps in flight against the reranker. 298 # 299 # Conservative at 1 because reranking is the heaviest request this server 300 # makes of a service it may share with its own embedding traffic and with 301 # other callers. Bounding our own concurrency keeps a burst of searches from 302 # queueing unbounded work upstream, and keeps rerank latency predictable 303 # here. It is a client-side courtesy, not a throughput control: what the 304 # reranker does with the request, and how it schedules against everything 305 # else it serves, is its business. Raise it if yours has headroom — a CPU 306 # cross-encoder almost certainly does not. 307 "search_rerank_max_concurrency": 1, 308 # --- SAR export redaction (ADR-040) ------------------------------------- 309 # Names are detected by the embedding gateway's ``POST /v1/ner``, so SAR 310 # export is available only with EMBEDDING_GATEWAY_URL set. 311 # NER model, addressed the gateway way (``<provider>/<model>``). 312 # Subject access request cases and redacted export (ADR-040). Off by 313 # default: a deployment opts in, since only some need it. Also requires 314 # vector sync and EMBEDDING_GATEWAY_URL (for name detection). 315 "sar_enabled": False, 316 "ner_model": "local/urchade/gliner_multi_pii-v1", 317 # Per-request budget. Export runs in the background, so this only needs to 318 # cover one batch on the slowest backend (CPU GLiNER: ~570 chars/s). 319 "ner_timeout_seconds": 120.0, 320 # Texts (of up to 2,000 chars) per /v1/ner request. Small for CPU GLiNER, 321 # which must finish a batch inside the gateway's own upstream timeout; raise 322 # it (e.g. 32) on a GPU backend. 323 "ner_batch_size": 8, 324 # Minimum model confidence for a span to count as a person. Lower raises 325 # recall at the cost of over-redaction, which is the safe direction for a 326 # disclosure; 0.5 is GLiNER's customary operating point. 327 "ner_threshold": 0.5, 328 # Chunking config generation. Bump whenever chunker behaviour changes (size, 329 # overlap, page-aware, page-pack, split strategy) so the pricing model's 330 # density reference can't silently go stale. Pinned in stripe-catalog.tf. 331 "chunking_config_version": 1, 332 # PDF parse isolation (OOM guard) 333 "document_pdf_graphics_limit": 1000, 334 "document_parse_timeout_seconds": 120.0, 335 # Optional wall-clock cap (seconds) on the SYNCHRONOUS parse inside the 336 # nc_webdav_read_file MCP tool. None (default) = disabled: an interactive read 337 # is bounded only by the underlying processor timeout (DOCLING_TIMEOUT / 338 # DOCUMENT_OCR_TIMEOUT_SECONDS). Set a client-friendly bound (e.g. 45-60s) so a 339 # slow VLM/OCR convert returns base64 quickly instead of blocking past an MCP 340 # client's own timeout. Never applies to the async ingest/worker path (ADR-032). 341 "document_read_timeout_seconds": None, 342 "document_parse_mem_limit_mb": 1536, 343 # Pre-parse size cap (MB): PDFs larger than this fail fast with reason 344 # "oversize" instead of burning the OCR timeout to 0 chars on a pathological 345 # file. 0 disables the guard. 346 "document_max_pdf_size_mb": 50.0, 347 # Pre-flight cap (MB) on nc_webdav_write_file's content: a huge PUT built 348 # from a single in-memory MCP tool argument (no chunked/streaming upload 349 # exists for writes, unlike the read-side stream_to_file path) risks 350 # timing out or exhausting memory rather than failing predictably. 0 351 # disables the guard. 352 "webdav_write_max_mb": 50.0, 353 # Transport security (MCP SDK TransportSecuritySettings). Default False 354 # preserves the behaviour hardcoded before these knobs existed: MCP 1.23+ 355 # auto-enables localhost-only host checking, which breaks k8s/Docker service 356 # DNS names (docs/MCP-1.23-DNS-REBINDING-FIX.md). Enable it plus 357 # MCP_ALLOWED_HOSTS when the server is reachable from a browser. 358 "mcp_dns_rebinding_protection": False, 359 # Comma-separated Host / Origin allowlists, honoured only when the above is 360 # enabled. Empty means "the SDK's own defaults". 361 "mcp_allowed_hosts": "", 362 "mcp_allowed_origins": "", 363 # Comma-separated CORS origin allowlist. "*" preserves today's behaviour. 364 "cors_allow_origins": "*", 365 # Page ceiling for markdown reconstruction (see document_markdown_max_pages). 366 "document_markdown_max_pages": 150, 367 # Pages per pypdfium2 extraction window (see document_parse_page_window). 368 "document_parse_page_window": 100, 369 # Concurrent isolated parse subprocesses (see document_parse_process_slots). 370 "document_parse_process_slots": 2, 371 # Stream ingest downloads to disk instead of buffering them in memory. 372 "document_stream_download_enabled": True, 373 # Directory for ingest spool files (default: the system temp dir). 374 "document_spool_dir": None, 375 # Tier-0 classifier (records classification metrics on the tiered path) 376 "document_classify_enabled": True, 377 # Tiered PDF pipeline: pypdfium2 is the default/only hot-path extractor; 378 # "pymupdf" is a deprecated rollback escape hatch. OCR (tier-3) is the only 379 # escalation target and is off by default (no provider wired yet). 380 "document_tier1_engine": "pypdfium2", 381 "document_ocr_enabled": False, 382 # OCR backend: "auto" picks gateway (if EMBEDDING_GATEWAY_URL) else mistral 383 # (if MISTRAL_API_KEY); "gateway"/"mistral" force one; "docling" routes scanned 384 # PDFs to a docling-serve instance (DOCLING_API_URL); "none" disables. "auto" 385 # never selects docling (it needs a self-hosted URL, so it must be explicit). 386 # The gateway routes on the model's "<provider>/" prefix, so it serves Mistral, 387 # surya (in-cluster GPU over the tailnet), etc. — one configurable OCR tier. 388 "document_ocr_provider": "auto", 389 # Provider-namespaced OCR model id (gateway routes on the prefix; the direct 390 # mistral backend strips it). e.g. "mistral/mistral-ocr-latest" (Mistral) or 391 # "surya/surya-ocr-2" (surya via the gateway) — never hard-coded, only this 392 # default. 393 "document_ocr_model": "mistral/mistral-ocr-latest", 394 # OCR escalation triggers (tier-0). A page is OCR-worthy when its text is 395 # near-empty (< min_page_chars) OR low-quality (< min_text_quality) OR (when 396 # detect_scanned) mostly a raster image; a doc escalates when the OCR-worthy 397 # page fraction reaches page_fraction. Calibrate min_text_quality from the 398 # astrolabe_document_text_quality histogram per tenant. 399 "document_ocr_min_text_quality": 0.5, 400 "document_ocr_page_fraction": 0.5, 401 "document_ocr_min_page_chars": 16, 402 "document_ocr_detect_scanned": True, 403 # Tier-0 glyph-corruption trigger. When the fast (pypdfium2) extraction's 404 # doc-level C0-control-char ratio exceeds this, the text layer is treated as 405 # glyph-corrupt (a broken /ToUnicode mapping leaking raw glyph codes) and the 406 # doc escalates fast->structured (pymupdf re-extracts it correctly -- no OCR). 407 # 0 disables. Clean docs sit ~0; affected PDFs measured ~1-11% in testing. 408 "document_glyph_corruption_ratio": 0.02, 409 # OCR backend request timeout (seconds). Slow scanned newspapers can take 410 # 20-60s; raise/lower per tenant. Configurable so a tenant isn't stuck with 411 # the 180s default when its gateway has its own shorter ceiling. 412 "document_ocr_timeout_seconds": 180.0, 413 # OCR execution mode (Deck #332). "sync" (default) transcribes inline via the 414 # backend's synchronous path. "batch" routes to the gateway's async Batch OCR 415 # job (~50% cheaper, minutes-hours latency) for large-corpus backfill — opt-in 416 # and gateway-routed. It requires EMBEDDING_GATEWAY_URL (rejected at startup 417 # otherwise, see __post_init__) and the per-tier procrastinate path; the 418 # inline/memory pool can't defer a poll, so batch raises there rather than 419 # silently downgrading to sync. 420 "document_ocr_mode": "sync", 421 # Seconds between batch-job polls (the procrastinate re-enqueue delay). Each 422 # poll re-runs the tier; keep it well above a few seconds. 423 "document_ocr_batch_poll_seconds": 120, 424 # Observability 425 "metrics_enabled": True, 426 "metrics_port": 9090, 427 "otel_exporter_otlp_endpoint": None, 428 "otel_exporter_verify_ssl": None, 429 "otel_service_name": "nextcloud-mcp-server", 430 "pyroscope_enabled": False, 431 "pyroscope_server_address": None, 432 # Pod identity from the Kubernetes downward API (chart-injected). Facts 433 # about the pod, not user config — absent off-Kubernetes. 434 "pod_namespace": None, 435 "pod_name": None, 436 "log_format": "text", 437 "log_level": "INFO", 438 "log_include_trace_context": True, 439 # Reverse-proxy trust list for X-Forwarded-For (GH #1284). Passed to 440 # uvicorn; None keeps uvicorn's own resolution (which itself falls back to 441 # 127.0.0.1). Declared here so the key is settable from settings.toml, not 442 # only as the env var uvicorn already reads on its own. 443 "forwarded_allow_ips": None, 444 # Document processing (no master switch: each optional processor has its own 445 # ENABLE_* flag, and parsing on read is the caller's per-call decision) 446 "document_processor": "unstructured", 447 "enable_unstructured": False, 448 "unstructured_api_url": "http://unstructured:8000", 449 "unstructured_timeout": 120, 450 "unstructured_strategy": "auto", 451 "unstructured_languages": "eng,deu", 452 "progress_interval": 10, 453 "enable_tesseract": False, 454 "tesseract_cmd": None, 455 "tesseract_lang": "eng", 456 # pymupdf is the built-in "structured" tier and is always available -- there 457 # is no enable flag for it, only these two knobs. 458 "pymupdf_extract_images": True, 459 "pymupdf_image_dir": None, 460 "enable_custom_processor": False, 461 "custom_processor_url": None, 462 "custom_processor_types": "application/pdf", 463 "custom_processor_name": "custom", 464 "custom_processor_api_key": None, 465 "custom_processor_timeout": 60, 466 # Docling document-parsing backend (docling-serve HTTP API). One docling-serve 467 # instance feeds two touchpoints: the images-only DoclingProcessor 468 # (find_processor path, priority 20) and — when DOCUMENT_OCR_PROVIDER=docling — 469 # the PDF OCR backend for scanned/no-text-layer PDFs. The same processor can be 470 # force-selected per call to parse a text-layer PDF (tables/partial text). URL 471 # unset -> the image processor is not registered and the OCR backend resolves to 472 # None. See ADR-031. 473 "enable_docling": False, 474 "docling_api_url": None, 475 "docling_timeout": 120, 476 # docling-serve OCR language codes. The default engine (EasyOCR) uses 2-letter 477 # codes ("en","de"); a Tesseract-backed instance wants "eng","deu". Engine- 478 # dependent, so keep it operator-tunable (see ADR-031). 479 "docling_ocr_lang": "en,de", 480 # Run OCR on IMAGES routed to the DoclingProcessor (find_processor path). The 481 # docling OCR *backend* (scanned PDFs) always OCRs regardless of this flag. 482 "docling_do_ocr": True, 483 # Which docling-serve pipeline to request: "standard" (classic layout+OCR, 484 # default, unchanged) or "vlm" (Vision-LLM OCR). "vlm" needs a docling-serve 485 # instance configured with VLM presets (see ADR-032). 486 "docling_pipeline": "standard", 487 # VLM preset name sent when docling_pipeline == "vlm". None -> docling-serve 488 # picks its own DOCLING_SERVE_DEFAULT_VLM_PRESET. Preset names are server-defined. 489 "docling_vlm_preset": None, 490 # Caption raster pictures embedded in .pptx/.docx/.xlsx via the same 491 # docling-serve instance (ADR-037). Explicit opt-in beyond a bare 492 # DOCLING_API_URL, like DOCUMENT_OCR_PROVIDER=docling needs its own 493 # selection: a deployment that only wants docling for scanned-PDF OCR 494 # shouldn't start captioning every picture in every document for free. The 495 # native readers have no rendering engine, so this only reaches actual 496 # pictures -- native vector drawings (SmartArt, shapes) are unaffected. 497 "office_caption_images": False, 498 # Cap on pictures captioned per file (one docling-serve round trip each). 499 # nc_webdav_read_file blocks synchronously for the whole parse, so this 500 # bounds the worst case rather than leaving it to the file's picture count. 501 "office_caption_max_images": 8, 502 # Per-picture docling-serve request timeout (seconds). Deliberately short 503 # and independent of DOCLING_TIMEOUT/DOCUMENT_OCR_TIMEOUT_SECONDS (other 504 # touchpoints, other latency profiles): a caption is meant to be a quick 505 # per-picture round trip repeated up to OFFICE_CAPTION_MAX_IMAGES times, not 506 # a single long convert. Raise it if DOCLING_PIPELINE=vlm makes captions 507 # time out (VLM is far slower than the standard pipeline -- see ADR-032). 508 "office_caption_timeout_seconds": 15.0, 509 # Collabora Online (coolwsd) base URL for converting legacy .doc/.xls/.ppt 510 # and ODF .odt/.ods/.odp to OOXML, which the native readers then parse 511 # (ADR-039). Unset = those types are not claimed. coolwsd only answers 512 # convert-to for clients in its net.post_allow list (private ranges by 513 # default). 514 "collabora_url": None, 515 # Per-file convert-to request timeout (seconds). A typical document 516 # converts in well under a second; the headroom is for large workbooks. 517 "collabora_timeout_seconds": 60.0, 518 # Deprecated PPTX_CAPTION_* spellings (0.193.0, pptx-only) of the three keys 519 # above; see _apply_legacy_caption_settings. Declared so dynaconf reads 520 # them at all, with the same defaults so their validators still apply. 521 "pptx_caption_images": False, 522 "pptx_caption_max_images": 8, 523 "pptx_caption_timeout_seconds": 15.0, 524 # Tag-based file exclusion (issue #710): comma-separated list of 525 # Nextcloud system tag names. Files/folders carrying any of these tags 526 # are hidden from WebDAV MCP tools. Empty = feature off. 527 "excluded_tags": "", 528 # MCP decomposition hook points (design §10). Every default reproduces 529 # the current monolithic behavior; self-hosters who set none are 530 # unaffected. See docs/architecture/mcp-decomposition.md (sibling repo). 531 "embedding_provider": "autodetect", # autodetect | gateway 532 # Ingest queue backend (Deck #183). None → ``memory`` (the in-process anyio 533 # queue): procrastinate is strictly opt-in, even on a Postgres DATABASE_URL. 534 # Set ``postgres`` explicitly to split ingest into a procrastinate worker; 535 # that requires a PostgreSQL DATABASE_URL. 536 "ingest_queue": None, # memory | postgres 537 # Process role for the per-tenant two-pod model (Deck #183). ``api`` runs the 538 # MCP/query server + scanner (defers jobs); the ``worker`` role is the 539 # `nextcloud-mcp-server worker` process that drains the queue. ``all`` keeps 540 # the monolithic behaviour (API + in-process SQLite pool). 541 "mcp_role": "all", # api | worker | all 542 # Reclaim an ingest job orphaned in ``doing`` by a crashed worker once its 543 # worker heartbeat is this many seconds stale (Deck #183). Default is well 544 # above the longest expected document; raise it for slow embedding backends. 545 "ingest_stalled_job_seconds": 300, 546 # Backstop reclaim: also re-queue a job stuck in ``doing`` for this many seconds 547 # regardless of worker liveness. The heartbeat threshold above only catches DEAD 548 # workers; a job whose OWN completion crashed (e.g. an unhandled queueing_lock 549 # UniqueViolation on the doing->todo retry) is stranded in ``doing`` under a LIVE, 550 # heart-beating worker and is invisible to the heartbeat sweep. 551 # This fires purely on time-in-``doing``, NOT on liveness, so it can't tell "stuck" 552 # from "legitimately slow": if a single HEALTHY process_document attempt ever ran 553 # past this, it'd be re-queued mid-flight and two workers could process the doc 554 # concurrently. That's safe ONLY because ingest re-runs are idempotent (uuid5 555 # Qdrant point IDs — see the queue module's "Design notes"), NOT because the 556 # reclaim distinguishes the two — so keep the default comfortably above the longest 557 # real single attempt. OCR batch polling releases the worker between polls 558 # (BatchPending -> retry_in), so a job never legitimately holds ``doing`` this 559 # long; 1800s is deep headroom. (Deck: ingest doing-strand reclaim.) 560 "ingest_doing_max_seconds": 1800, 561 # Delete succeeded ingest jobs (keeps the queue table lean + the KEDA 562 # queue-depth metric clean). Set false to retain succeeded rows for audit 563 # (note: indexing success is also recorded in logs/metrics regardless). 564 "ingest_delete_succeeded_jobs": True, 565 # Whether the procrastinate worker uses LISTEN/NOTIFY for job pickup (Deck 566 # #424). True (default) = near-instant wakeup via a long-lived LISTEN 567 # connection. Set false to run POLL-ONLY when DATABASE_URL routes through a 568 # transaction-mode connection pooler (PgBouncer transaction mode), which is 569 # incompatible with LISTEN/NOTIFY — the LISTEN registration is dropped when 570 # the backend returns to the pool. Poll-only trades a few seconds of pickup 571 # latency (fetch_job_polling_interval) for pooler safety; job queries still 572 # multiplex through the pooler. Snapshotted at worker startup; needs a 573 # restart to change. 574 "ingest_listen_notify": True, 575 # Per-tier escalation on the procrastinate (postgres) ingest path (Deck 576 # #323). When true, a document that a tier cannot parse well is requeued onto 577 # the next tier's queue (fast -> structured -> ocr) via a native procrastinate 578 # queue-hop. When false the ``fast`` tier is terminal -- reproduces the 579 # pre-#323 behaviour where the cheap tier's output is indexed as-is. No effect 580 # on the in-process ``memory`` backend, which keeps the inline escalation. 581 # HOT: re-read per job (process_document_task), so it takes effect on the next 582 # job -- unlike INGEST_TRANSIENT_MAX_ATTEMPTS, which is snapshotted at worker 583 # startup and needs a restart. 584 "ingest_escalation_enabled": True, 585 # Global cap on SAME-tier retries for transient infra errors (doc fetch / 586 # embed / Qdrant blips) on the procrastinate path. Parse-quality failures 587 # escalate (one parse attempt per tier) and do NOT consume this budget; only 588 # whitelisted transient exceptions retry in place. Shared across tiers because 589 # a queue-hop cannot reset a per-tier counter (see TieredEscalationStrategy). 590 # Snapshotted at worker startup (blueprint build); restart to change it. 591 "ingest_transient_max_attempts": 5, 592 # Delay (seconds) before a reclaimed stalled job is re-run. A stall is often 593 # systemic (Qdrant/embedding outage), so reclaiming every crashed job at 594 # now() would thundering-herd a recovering dependency every reclaim tick 595 # (*/5min), bypassing TieredEscalationStrategy's per-job backoff. A small 596 # fixed delay staggers the retry. 0 = immediate (legacy behaviour). 597 "ingest_reclaim_retry_delay_seconds": 30, 598 "collection_metadata_source": "qdrant", # qdrant | api 599 # CP base URL for COLLECTION_METADATA_SOURCE=api (e.g. http://control-plane). 600 # Required only when the source is api. 601 "collection_metadata_api_url": None, 602 "embedding_gateway_url": None, # required when embedding_provider=gateway 603 # Provider-namespaced model the gateway serves, "<provider>/<model>" 604 # (the gateway routes on the "/"-prefix; mistral/mistral-embed → Mistral 605 # for the MVP). Only consulted when embedding_provider=gateway. 606 "embedding_gateway_model": "mistral/mistral-embed", 607 # Gateway auth: the MCP server is an OIDC *client* in the gateway's own 608 # M2M realm (parallel to, and distinct from, the tenant realm it already 609 # serves). It obtains a client-credentials token and the gateway maps the 610 # client-id → the tenant's underlying provider API key. All four unset = 611 # call the gateway unauthenticated (matches today's not-yet-authed gateway). 612 "embedding_gateway_token_url": None, # M2M token endpoint 613 "embedding_gateway_client_id": None, 614 "embedding_gateway_client_secret": None, 615 "embedding_gateway_scope": None, # e.g. astrolabe-embedding-gateway/embed 616 # Query-side ACL pre-filter (design §11). OFF by default: a Qdrant 617 # `match any` on `acl_hash` excludes points missing the key, so enabling 618 # this before a real ACL backfill would silently drop legacy results. 619 # verify-on-read remains the correctness backstop regardless. 620 "acl_prefilter_enabled": False, 621 # Usage metering (Deck #67, control-plane usage-metering.md). OFF by 622 # default so OSS self-hosters don't accrue a metering table or write 623 # overhead; Astrolabe Cloud provisioning sets it true. When on, billable 624 # ops record rows into the app-DB usage_events table (best-effort). 625 "usage_metering_enabled": False, 626 } 627 628 629 def _resolve_settings_files() -> list[str]: 630 """Find optional external settings files. 631 632 Priority: 633 1. NEXTCLOUD_MCP_SETTINGS_FILE env var (absolute or relative path). 634 If set but the file does not exist, raise FileNotFoundError — 635 silently falling back to defaults on a typo would be a footgun. 636 .secrets.toml is looked for alongside the explicit file. 637 2. Otherwise ./settings.toml in cwd (for docker / dev workflows), 638 with .secrets.toml also looked for in cwd. 639 640 Returns an empty list if nothing is configured — that's fine, defaults 641 and env vars still apply. 642 """ 643 files: list[str] = [] 644 # A legitimate os.environ read: this runs BEFORE the dynaconf instance is 645 # built (it computes the settings_files list passed to Dynaconf), so it 646 # cannot go through dynaconf — you can't read the location of the settings 647 # file from the settings file. This + the MCP_DEPLOYMENT_MODE env_switcher 648 # are dynaconf's own bootstrap. All config *values* are dynaconf-driven; the 649 # only other reads are the legacy-env deprecation check and 650 # _warn_unknown_env_vars, which inspect env var NAMES, not values. 651 explicit = os.environ.get("NEXTCLOUD_MCP_SETTINGS_FILE") 652 if explicit: 653 p = Path(explicit) 654 if not p.exists(): 655 raise FileNotFoundError( 656 f"NEXTCLOUD_MCP_SETTINGS_FILE points to a file that does " 657 f"not exist: {explicit}" 658 ) 659 files.append(str(p)) 660 secrets = p.parent / ".secrets.toml" 661 else: 662 cwd_settings = Path.cwd() / "settings.toml" 663 if cwd_settings.exists(): 664 files.append(str(cwd_settings)) 665 secrets = Path.cwd() / ".secrets.toml" 666 if secrets.exists(): 667 files.append(str(secrets)) 668 return files 669 670 671 # Dynaconf instance — env vars always win (12-factor). Settings files are 672 # optional; when absent the defaults above provide the full key schema so 673 # env vars still override correctly. See ADR-024/025 for architecture. 674 _dynaconf = Dynaconf( 675 settings_files=_resolve_settings_files(), 676 environments=True, 677 envvar_prefix=False, 678 env_switcher="MCP_DEPLOYMENT_MODE", 679 ignore_unknown_envvars=True, 680 load_dotenv=False, 681 **_DEFAULTS, 682 validators=[ 683 # Port ranges 684 Validator("METRICS_PORT", gte=1, lte=65535), 685 # Positive integers 686 Validator("INGEST_STALLED_JOB_SECONDS", gte=1), 687 Validator("INGEST_TRANSIENT_MAX_ATTEMPTS", gte=1), 688 Validator("INGEST_RECLAIM_RETRY_DELAY_SECONDS", gte=0), 689 Validator("OIDC_DISCOVERY_MAX_ATTEMPTS", gte=1), 690 Validator("OIDC_DISCOVERY_BACKOFF_BASE", gte=0), 691 Validator("OIDC_DISCOVERY_BACKOFF_MAX", gte=0), 692 Validator("OFFICE_CAPTION_MAX_IMAGES", gte=0), 693 Validator("OFFICE_CAPTION_TIMEOUT_SECONDS", gt=0), 694 Validator("COLLABORA_TIMEOUT_SECONDS", gt=0), 695 Validator("PPTX_CAPTION_MAX_IMAGES", gte=0), 696 Validator("PPTX_CAPTION_TIMEOUT_SECONDS", gt=0), 697 Validator("QDRANT_INIT_MAX_ATTEMPTS", gte=1), 698 Validator("QDRANT_INIT_BACKOFF_BASE", gte=0), 699 Validator("QDRANT_INIT_BACKOFF_MAX", gte=0), 700 Validator("OLLAMA_EMBED_MAX_BATCH_CHARS", gte=1), 701 Validator("OLLAMA_EMBED_TIMEOUT", gte=1), 702 Validator("VECTOR_SYNC_SCAN_INTERVAL", gte=1), 703 Validator("VECTOR_SYNC_PROCESSOR_WORKERS", gte=1), 704 # 1 = park on the first exhausted-retry round. 0/negative would park a 705 # document before any attempt was recorded. 706 Validator("VECTOR_SYNC_MAX_INDEX_FAILURES", gte=1), 707 # Optional per-tier concurrency overrides (None = fall back to 708 # VECTOR_SYNC_PROCESSOR_WORKERS). Like the sibling above they must be 709 # >=1 when set — a 0/negative value would otherwise reach 710 # app.run_worker_async(concurrency=...) and fail with an opaque runtime 711 # error instead of a clear config error at startup. 712 Validator( 713 "VECTOR_SYNC_FAST_CONCURRENCY", 714 condition=lambda v: v is None or v >= 1, 715 messages={ 716 "condition": "VECTOR_SYNC_FAST_CONCURRENCY must be >= 1 when set" 717 }, 718 ), -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/config.py line 212
- Likely real
- Uncertain, 45% confidence
Possible hardcoded password: 'None'
211 "aws_access_key_id": None, 212 "aws_secret_access_key": None, 213 "bedrock_embedding_model": None, 214 # Mistral 215 "mistral_api_key": None, 216 "mistral_embedding_model": "mistral-embed", 217 "mistral_base_url": None, 218 # Matryoshka output width requested from the embedding endpoint (None = 219 # the model's full width). Provider-agnostic: only one provider is active 220 # at a time, so a single knob covers openai/ollama/gateway. 221 "embedding_dimensions": None, 222 # Simple (fallback) embedding dimension 223 "simple_embedding_dimension": 384, 224 # Document chunking 225 "document_chunk_size": 2048, 226 "document_chunk_overlap": 200, 227 # Page-aware chunking for paginated docs (PDFs): split on page boundaries 228 # first so no chunk spans a page (exact page_number, clean snippets, and 229 # predictable ~1 chunk/page when chunk_size >= the largest page). 230 "document_chunk_page_aware": True, 231 # Greedy page-packing (density fix, Deck #636): merge consecutive sub-budget 232 # pages into one chunk instead of one-per-page. Off by default until the 233 # post-change density re-measure + pricing re-calibration land. 234 "document_chunk_page_pack": False, 235 # Hybrid-search RRF ranking constant. Qdrant's native RRF hardcodes k=2, 236 # where adjacent ranks differ by 33% and a single-list outlier outranks a 237 # document both retrievers agree on. 60 is the standard value (~2% per rank), 238 # which restores consensus as the dominant signal. 239 "vector_search_rrf_k": 60, 240 # --- Cross-encoder reranking (opt-in, per request) ---------------------- 241 # A cross-encoder reorders the retrieved candidates by scoring each against 242 # the query directly, which is more accurate than the fusion rank it 243 # replaces. Ships OFF because it adds an upstream round-trip to every 244 # search, and how expensive that is depends on how the reranker is deployed 245 # — which this server deliberately knows nothing about beyond the URL. 246 "search_rerank_enabled": False, 247 # Where reranking is served. A FULL endpoint URL, because the path differs 248 # per backend and there is no safe guess: Infinity serves ``POST /rerank``, 249 # vLLM ``POST /v1/rerank``, Cohere ``POST /v2/rerank``, the Astrolabe 250 # embedding gateway ``POST /v1/rerank``. All four speak the same Cohere 251 # request/response protocol, so choosing one is configuration, not code. 252 # 253 # Unset = derive it from EMBEDDING_GATEWAY_URL, which is what every existing 254 # deployment does. Set it to run reranking WITHOUT a gateway — the reason 255 # this exists (discussion #1354): Ollama serves no rerank models at all, so 256 # a self-hoster's only options are a hosted rerank API or a local Infinity / 257 # vLLM, and neither should require standing up a gateway first. 258 "search_rerank_url": None, 259 # Static bearer for SEARCH_RERANK_URL (a Cohere API key, an Infinity 260 # ``--api-key``). Wins over the gateway's M2M OIDC credentials when both are 261 # present. Unset = send no Authorization header, which is correct for a 262 # local Infinity/vLLM on a private network. 263 "search_rerank_api_key": None, 264 # Rerank model, addressed the way the CONFIGURED ENDPOINT expects — which 265 # differs between the two, so the default is only right for one of them. 266 # 267 # Against the embedding gateway the ``<provider>/`` prefix is REQUIRED, not 268 # stylistic: the gateway splits on the FIRST slash to select a backend, so a 269 # bare ``BAAI/bge-reranker-v2-m3`` asks for a provider named ``BAAI`` and 270 # 503s — and because a failed rerank degrades to retrieval order 271 # (``reranked: false``) rather than erroring, that looks like "reranking does 272 # nothing" instead of like a misconfiguration. The prefix is also the backend 273 # selector: ``local/`` for a self-hosted cross-encoder, ``bedrock/`` or 274 # ``openrouter/`` for a hosted one. 275 # 276 # Against a direct SEARCH_RERANK_URL there is no such routing layer, so use 277 # the bare id the server serves (``BAAI/bge-reranker-v2-m3`` for 278 # Infinity/vLLM, ``rerank-v3.5`` for Cohere). The relevance calibration 279 # (ADR-034) strips known provider prefixes, so both spellings resolve to the 280 # same fitted curve. 281 "search_rerank_model": "local/BAAI/bge-reranker-v2-m3", 282 # Candidates handed to the reranker. Reranking can only reorder what 283 # retrieval supplied, so this depth — not the caller's ``limit`` — bounds 284 # how much a reranker can improve. Reranking just the rows a normal search 285 # returns captures little of the available gain. 286 # 287 # Treat this as a ceiling rather than a starting point. Under 288 # ``granularity="document"`` the grouped prefetch is bounded by 289 # MAX_DOCUMENT_PREFETCH, and requesting more groups than the prefetch can 290 # fill makes Qdrant widen its grouping search and reorder the head of the 291 # result set before the reranker sees it. Going deeper requires raising 292 # MAX_DOCUMENT_PREFETCH first, which carries its own latency cost. 293 "search_rerank_pool_size": 200, 294 # Generous headroom over a normal rerank, not a target. Exceeding it 295 # degrades to retrieval order rather than failing the search. 296 "search_rerank_timeout_seconds": 30.0, 297 # Concurrent rerank calls this process keeps in flight against the reranker. 298 # 299 # Conservative at 1 because reranking is the heaviest request this server 300 # makes of a service it may share with its own embedding traffic and with 301 # other callers. Bounding our own concurrency keeps a burst of searches from 302 # queueing unbounded work upstream, and keeps rerank latency predictable 303 # here. It is a client-side courtesy, not a throughput control: what the 304 # reranker does with the request, and how it schedules against everything 305 # else it serves, is its business. Raise it if yours has headroom — a CPU 306 # cross-encoder almost certainly does not. 307 "search_rerank_max_concurrency": 1, 308 # --- SAR export redaction (ADR-040) ------------------------------------- 309 # Names are detected by the embedding gateway's ``POST /v1/ner``, so SAR 310 # export is available only with EMBEDDING_GATEWAY_URL set. 311 # NER model, addressed the gateway way (``<provider>/<model>``). 312 # Subject access request cases and redacted export (ADR-040). Off by 313 # default: a deployment opts in, since only some need it. Also requires 314 # vector sync and EMBEDDING_GATEWAY_URL (for name detection). 315 "sar_enabled": False, 316 "ner_model": "local/urchade/gliner_multi_pii-v1", 317 # Per-request budget. Export runs in the background, so this only needs to 318 # cover one batch on the slowest backend (CPU GLiNER: ~570 chars/s). 319 "ner_timeout_seconds": 120.0, 320 # Texts (of up to 2,000 chars) per /v1/ner request. Small for CPU GLiNER, 321 # which must finish a batch inside the gateway's own upstream timeout; raise 322 # it (e.g. 32) on a GPU backend. 323 "ner_batch_size": 8, 324 # Minimum model confidence for a span to count as a person. Lower raises 325 # recall at the cost of over-redaction, which is the safe direction for a 326 # disclosure; 0.5 is GLiNER's customary operating point. 327 "ner_threshold": 0.5, 328 # Chunking config generation. Bump whenever chunker behaviour changes (size, 329 # overlap, page-aware, page-pack, split strategy) so the pricing model's 330 # density reference can't silently go stale. Pinned in stripe-catalog.tf. 331 "chunking_config_version": 1, 332 # PDF parse isolation (OOM guard) 333 "document_pdf_graphics_limit": 1000, 334 "document_parse_timeout_seconds": 120.0, 335 # Optional wall-clock cap (seconds) on the SYNCHRONOUS parse inside the 336 # nc_webdav_read_file MCP tool. None (default) = disabled: an interactive read 337 # is bounded only by the underlying processor timeout (DOCLING_TIMEOUT / 338 # DOCUMENT_OCR_TIMEOUT_SECONDS). Set a client-friendly bound (e.g. 45-60s) so a 339 # slow VLM/OCR convert returns base64 quickly instead of blocking past an MCP 340 # client's own timeout. Never applies to the async ingest/worker path (ADR-032). 341 "document_read_timeout_seconds": None, 342 "document_parse_mem_limit_mb": 1536, 343 # Pre-parse size cap (MB): PDFs larger than this fail fast with reason 344 # "oversize" instead of burning the OCR timeout to 0 chars on a pathological 345 # file. 0 disables the guard. 346 "document_max_pdf_size_mb": 50.0, 347 # Pre-flight cap (MB) on nc_webdav_write_file's content: a huge PUT built 348 # from a single in-memory MCP tool argument (no chunked/streaming upload 349 # exists for writes, unlike the read-side stream_to_file path) risks 350 # timing out or exhausting memory rather than failing predictably. 0 351 # disables the guard. 352 "webdav_write_max_mb": 50.0, 353 # Transport security (MCP SDK TransportSecuritySettings). Default False 354 # preserves the behaviour hardcoded before these knobs existed: MCP 1.23+ 355 # auto-enables localhost-only host checking, which breaks k8s/Docker service 356 # DNS names (docs/MCP-1.23-DNS-REBINDING-FIX.md). Enable it plus 357 # MCP_ALLOWED_HOSTS when the server is reachable from a browser. 358 "mcp_dns_rebinding_protection": False, 359 # Comma-separated Host / Origin allowlists, honoured only when the above is 360 # enabled. Empty means "the SDK's own defaults". 361 "mcp_allowed_hosts": "", 362 "mcp_allowed_origins": "", 363 # Comma-separated CORS origin allowlist. "*" preserves today's behaviour. 364 "cors_allow_origins": "*", 365 # Page ceiling for markdown reconstruction (see document_markdown_max_pages). 366 "document_markdown_max_pages": 150, 367 # Pages per pypdfium2 extraction window (see document_parse_page_window). 368 "document_parse_page_window": 100, 369 # Concurrent isolated parse subprocesses (see document_parse_process_slots). 370 "document_parse_process_slots": 2, 371 # Stream ingest downloads to disk instead of buffering them in memory. 372 "document_stream_download_enabled": True, 373 # Directory for ingest spool files (default: the system temp dir). 374 "document_spool_dir": None, 375 # Tier-0 classifier (records classification metrics on the tiered path) 376 "document_classify_enabled": True, 377 # Tiered PDF pipeline: pypdfium2 is the default/only hot-path extractor; 378 # "pymupdf" is a deprecated rollback escape hatch. OCR (tier-3) is the only 379 # escalation target and is off by default (no provider wired yet). 380 "document_tier1_engine": "pypdfium2", 381 "document_ocr_enabled": False, 382 # OCR backend: "auto" picks gateway (if EMBEDDING_GATEWAY_URL) else mistral 383 # (if MISTRAL_API_KEY); "gateway"/"mistral" force one; "docling" routes scanned 384 # PDFs to a docling-serve instance (DOCLING_API_URL); "none" disables. "auto" 385 # never selects docling (it needs a self-hosted URL, so it must be explicit). 386 # The gateway routes on the model's "<provider>/" prefix, so it serves Mistral, 387 # surya (in-cluster GPU over the tailnet), etc. — one configurable OCR tier. 388 "document_ocr_provider": "auto", 389 # Provider-namespaced OCR model id (gateway routes on the prefix; the direct 390 # mistral backend strips it). e.g. "mistral/mistral-ocr-latest" (Mistral) or 391 # "surya/surya-ocr-2" (surya via the gateway) — never hard-coded, only this 392 # default. 393 "document_ocr_model": "mistral/mistral-ocr-latest", 394 # OCR escalation triggers (tier-0). A page is OCR-worthy when its text is 395 # near-empty (< min_page_chars) OR low-quality (< min_text_quality) OR (when 396 # detect_scanned) mostly a raster image; a doc escalates when the OCR-worthy 397 # page fraction reaches page_fraction. Calibrate min_text_quality from the 398 # astrolabe_document_text_quality histogram per tenant. 399 "document_ocr_min_text_quality": 0.5, 400 "document_ocr_page_fraction": 0.5, 401 "document_ocr_min_page_chars": 16, 402 "document_ocr_detect_scanned": True, 403 # Tier-0 glyph-corruption trigger. When the fast (pypdfium2) extraction's 404 # doc-level C0-control-char ratio exceeds this, the text layer is treated as 405 # glyph-corrupt (a broken /ToUnicode mapping leaking raw glyph codes) and the 406 # doc escalates fast->structured (pymupdf re-extracts it correctly -- no OCR). 407 # 0 disables. Clean docs sit ~0; affected PDFs measured ~1-11% in testing. 408 "document_glyph_corruption_ratio": 0.02, 409 # OCR backend request timeout (seconds). Slow scanned newspapers can take 410 # 20-60s; raise/lower per tenant. Configurable so a tenant isn't stuck with 411 # the 180s default when its gateway has its own shorter ceiling. 412 "document_ocr_timeout_seconds": 180.0, 413 # OCR execution mode (Deck #332). "sync" (default) transcribes inline via the 414 # backend's synchronous path. "batch" routes to the gateway's async Batch OCR 415 # job (~50% cheaper, minutes-hours latency) for large-corpus backfill — opt-in 416 # and gateway-routed. It requires EMBEDDING_GATEWAY_URL (rejected at startup 417 # otherwise, see __post_init__) and the per-tier procrastinate path; the 418 # inline/memory pool can't defer a poll, so batch raises there rather than 419 # silently downgrading to sync. 420 "document_ocr_mode": "sync", 421 # Seconds between batch-job polls (the procrastinate re-enqueue delay). Each 422 # poll re-runs the tier; keep it well above a few seconds. 423 "document_ocr_batch_poll_seconds": 120, 424 # Observability 425 "metrics_enabled": True, 426 "metrics_port": 9090, 427 "otel_exporter_otlp_endpoint": None, 428 "otel_exporter_verify_ssl": None, 429 "otel_service_name": "nextcloud-mcp-server", 430 "pyroscope_enabled": False, 431 "pyroscope_server_address": None, 432 # Pod identity from the Kubernetes downward API (chart-injected). Facts 433 # about the pod, not user config — absent off-Kubernetes. 434 "pod_namespace": None, 435 "pod_name": None, 436 "log_format": "text", 437 "log_level": "INFO", 438 "log_include_trace_context": True, 439 # Reverse-proxy trust list for X-Forwarded-For (GH #1284). Passed to 440 # uvicorn; None keeps uvicorn's own resolution (which itself falls back to 441 # 127.0.0.1). Declared here so the key is settable from settings.toml, not 442 # only as the env var uvicorn already reads on its own. 443 "forwarded_allow_ips": None, 444 # Document processing (no master switch: each optional processor has its own 445 # ENABLE_* flag, and parsing on read is the caller's per-call decision) 446 "document_processor": "unstructured", 447 "enable_unstructured": False, 448 "unstructured_api_url": "http://unstructured:8000", 449 "unstructured_timeout": 120, 450 "unstructured_strategy": "auto", 451 "unstructured_languages": "eng,deu", 452 "progress_interval": 10, 453 "enable_tesseract": False, 454 "tesseract_cmd": None, 455 "tesseract_lang": "eng", 456 # pymupdf is the built-in "structured" tier and is always available -- there 457 # is no enable flag for it, only these two knobs. 458 "pymupdf_extract_images": True, 459 "pymupdf_image_dir": None, 460 "enable_custom_processor": False, 461 "custom_processor_url": None, 462 "custom_processor_types": "application/pdf", 463 "custom_processor_name": "custom", 464 "custom_processor_api_key": None, 465 "custom_processor_timeout": 60, 466 # Docling document-parsing backend (docling-serve HTTP API). One docling-serve 467 # instance feeds two touchpoints: the images-only DoclingProcessor 468 # (find_processor path, priority 20) and — when DOCUMENT_OCR_PROVIDER=docling — 469 # the PDF OCR backend for scanned/no-text-layer PDFs. The same processor can be 470 # force-selected per call to parse a text-layer PDF (tables/partial text). URL 471 # unset -> the image processor is not registered and the OCR backend resolves to 472 # None. See ADR-031. 473 "enable_docling": False, 474 "docling_api_url": None, 475 "docling_timeout": 120, 476 # docling-serve OCR language codes. The default engine (EasyOCR) uses 2-letter 477 # codes ("en","de"); a Tesseract-backed instance wants "eng","deu". Engine- 478 # dependent, so keep it operator-tunable (see ADR-031). 479 "docling_ocr_lang": "en,de", 480 # Run OCR on IMAGES routed to the DoclingProcessor (find_processor path). The 481 # docling OCR *backend* (scanned PDFs) always OCRs regardless of this flag. 482 "docling_do_ocr": True, 483 # Which docling-serve pipeline to request: "standard" (classic layout+OCR, 484 # default, unchanged) or "vlm" (Vision-LLM OCR). "vlm" needs a docling-serve 485 # instance configured with VLM presets (see ADR-032). 486 "docling_pipeline": "standard", 487 # VLM preset name sent when docling_pipeline == "vlm". None -> docling-serve 488 # picks its own DOCLING_SERVE_DEFAULT_VLM_PRESET. Preset names are server-defined. 489 "docling_vlm_preset": None, 490 # Caption raster pictures embedded in .pptx/.docx/.xlsx via the same 491 # docling-serve instance (ADR-037). Explicit opt-in beyond a bare 492 # DOCLING_API_URL, like DOCUMENT_OCR_PROVIDER=docling needs its own 493 # selection: a deployment that only wants docling for scanned-PDF OCR 494 # shouldn't start captioning every picture in every document for free. The 495 # native readers have no rendering engine, so this only reaches actual 496 # pictures -- native vector drawings (SmartArt, shapes) are unaffected. 497 "office_caption_images": False, 498 # Cap on pictures captioned per file (one docling-serve round trip each). 499 # nc_webdav_read_file blocks synchronously for the whole parse, so this 500 # bounds the worst case rather than leaving it to the file's picture count. 501 "office_caption_max_images": 8, 502 # Per-picture docling-serve request timeout (seconds). Deliberately short 503 # and independent of DOCLING_TIMEOUT/DOCUMENT_OCR_TIMEOUT_SECONDS (other 504 # touchpoints, other latency profiles): a caption is meant to be a quick 505 # per-picture round trip repeated up to OFFICE_CAPTION_MAX_IMAGES times, not 506 # a single long convert. Raise it if DOCLING_PIPELINE=vlm makes captions 507 # time out (VLM is far slower than the standard pipeline -- see ADR-032). 508 "office_caption_timeout_seconds": 15.0, 509 # Collabora Online (coolwsd) base URL for converting legacy .doc/.xls/.ppt 510 # and ODF .odt/.ods/.odp to OOXML, which the native readers then parse 511 # (ADR-039). Unset = those types are not claimed. coolwsd only answers 512 # convert-to for clients in its net.post_allow list (private ranges by 513 # default). 514 "collabora_url": None, 515 # Per-file convert-to request timeout (seconds). A typical document 516 # converts in well under a second; the headroom is for large workbooks. 517 "collabora_timeout_seconds": 60.0, 518 # Deprecated PPTX_CAPTION_* spellings (0.193.0, pptx-only) of the three keys 519 # above; see _apply_legacy_caption_settings. Declared so dynaconf reads 520 # them at all, with the same defaults so their validators still apply. 521 "pptx_caption_images": False, 522 "pptx_caption_max_images": 8, 523 "pptx_caption_timeout_seconds": 15.0, 524 # Tag-based file exclusion (issue #710): comma-separated list of 525 # Nextcloud system tag names. Files/folders carrying any of these tags 526 # are hidden from WebDAV MCP tools. Empty = feature off. 527 "excluded_tags": "", 528 # MCP decomposition hook points (design §10). Every default reproduces 529 # the current monolithic behavior; self-hosters who set none are 530 # unaffected. See docs/architecture/mcp-decomposition.md (sibling repo). 531 "embedding_provider": "autodetect", # autodetect | gateway 532 # Ingest queue backend (Deck #183). None → ``memory`` (the in-process anyio 533 # queue): procrastinate is strictly opt-in, even on a Postgres DATABASE_URL. 534 # Set ``postgres`` explicitly to split ingest into a procrastinate worker; 535 # that requires a PostgreSQL DATABASE_URL. 536 "ingest_queue": None, # memory | postgres 537 # Process role for the per-tenant two-pod model (Deck #183). ``api`` runs the 538 # MCP/query server + scanner (defers jobs); the ``worker`` role is the 539 # `nextcloud-mcp-server worker` process that drains the queue. ``all`` keeps 540 # the monolithic behaviour (API + in-process SQLite pool). 541 "mcp_role": "all", # api | worker | all 542 # Reclaim an ingest job orphaned in ``doing`` by a crashed worker once its 543 # worker heartbeat is this many seconds stale (Deck #183). Default is well 544 # above the longest expected document; raise it for slow embedding backends. 545 "ingest_stalled_job_seconds": 300, 546 # Backstop reclaim: also re-queue a job stuck in ``doing`` for this many seconds 547 # regardless of worker liveness. The heartbeat threshold above only catches DEAD 548 # workers; a job whose OWN completion crashed (e.g. an unhandled queueing_lock 549 # UniqueViolation on the doing->todo retry) is stranded in ``doing`` under a LIVE, 550 # heart-beating worker and is invisible to the heartbeat sweep. 551 # This fires purely on time-in-``doing``, NOT on liveness, so it can't tell "stuck" 552 # from "legitimately slow": if a single HEALTHY process_document attempt ever ran 553 # past this, it'd be re-queued mid-flight and two workers could process the doc 554 # concurrently. That's safe ONLY because ingest re-runs are idempotent (uuid5 555 # Qdrant point IDs — see the queue module's "Design notes"), NOT because the 556 # reclaim distinguishes the two — so keep the default comfortably above the longest 557 # real single attempt. OCR batch polling releases the worker between polls 558 # (BatchPending -> retry_in), so a job never legitimately holds ``doing`` this 559 # long; 1800s is deep headroom. (Deck: ingest doing-strand reclaim.) 560 "ingest_doing_max_seconds": 1800, 561 # Delete succeeded ingest jobs (keeps the queue table lean + the KEDA 562 # queue-depth metric clean). Set false to retain succeeded rows for audit 563 # (note: indexing success is also recorded in logs/metrics regardless). 564 "ingest_delete_succeeded_jobs": True, 565 # Whether the procrastinate worker uses LISTEN/NOTIFY for job pickup (Deck 566 # #424). True (default) = near-instant wakeup via a long-lived LISTEN 567 # connection. Set false to run POLL-ONLY when DATABASE_URL routes through a 568 # transaction-mode connection pooler (PgBouncer transaction mode), which is 569 # incompatible with LISTEN/NOTIFY — the LISTEN registration is dropped when 570 # the backend returns to the pool. Poll-only trades a few seconds of pickup 571 # latency (fetch_job_polling_interval) for pooler safety; job queries still 572 # multiplex through the pooler. Snapshotted at worker startup; needs a 573 # restart to change. 574 "ingest_listen_notify": True, 575 # Per-tier escalation on the procrastinate (postgres) ingest path (Deck 576 # #323). When true, a document that a tier cannot parse well is requeued onto 577 # the next tier's queue (fast -> structured -> ocr) via a native procrastinate 578 # queue-hop. When false the ``fast`` tier is terminal -- reproduces the 579 # pre-#323 behaviour where the cheap tier's output is indexed as-is. No effect 580 # on the in-process ``memory`` backend, which keeps the inline escalation. 581 # HOT: re-read per job (process_document_task), so it takes effect on the next 582 # job -- unlike INGEST_TRANSIENT_MAX_ATTEMPTS, which is snapshotted at worker 583 # startup and needs a restart. 584 "ingest_escalation_enabled": True, 585 # Global cap on SAME-tier retries for transient infra errors (doc fetch / 586 # embed / Qdrant blips) on the procrastinate path. Parse-quality failures 587 # escalate (one parse attempt per tier) and do NOT consume this budget; only 588 # whitelisted transient exceptions retry in place. Shared across tiers because 589 # a queue-hop cannot reset a per-tier counter (see TieredEscalationStrategy). 590 # Snapshotted at worker startup (blueprint build); restart to change it. 591 "ingest_transient_max_attempts": 5, 592 # Delay (seconds) before a reclaimed stalled job is re-run. A stall is often 593 # systemic (Qdrant/embedding outage), so reclaiming every crashed job at 594 # now() would thundering-herd a recovering dependency every reclaim tick 595 # (*/5min), bypassing TieredEscalationStrategy's per-job backoff. A small 596 # fixed delay staggers the retry. 0 = immediate (legacy behaviour). 597 "ingest_reclaim_retry_delay_seconds": 30, 598 "collection_metadata_source": "qdrant", # qdrant | api 599 # CP base URL for COLLECTION_METADATA_SOURCE=api (e.g. http://control-plane). 600 # Required only when the source is api. 601 "collection_metadata_api_url": None, 602 "embedding_gateway_url": None, # required when embedding_provider=gateway 603 # Provider-namespaced model the gateway serves, "<provider>/<model>" 604 # (the gateway routes on the "/"-prefix; mistral/mistral-embed → Mistral 605 # for the MVP). Only consulted when embedding_provider=gateway. 606 "embedding_gateway_model": "mistral/mistral-embed", 607 # Gateway auth: the MCP server is an OIDC *client* in the gateway's own 608 # M2M realm (parallel to, and distinct from, the tenant realm it already 609 # serves). It obtains a client-credentials token and the gateway maps the 610 # client-id → the tenant's underlying provider API key. All four unset = 611 # call the gateway unauthenticated (matches today's not-yet-authed gateway). 612 "embedding_gateway_token_url": None, # M2M token endpoint 613 "embedding_gateway_client_id": None, 614 "embedding_gateway_client_secret": None, 615 "embedding_gateway_scope": None, # e.g. astrolabe-embedding-gateway/embed 616 # Query-side ACL pre-filter (design §11). OFF by default: a Qdrant 617 # `match any` on `acl_hash` excludes points missing the key, so enabling 618 # this before a real ACL backfill would silently drop legacy results. 619 # verify-on-read remains the correctness backstop regardless. 620 "acl_prefilter_enabled": False, 621 # Usage metering (Deck #67, control-plane usage-metering.md). OFF by 622 # default so OSS self-hosters don't accrue a metering table or write 623 # overhead; Astrolabe Cloud provisioning sets it true. When on, billable 624 # ops record rows into the app-DB usage_events table (best-effort). 625 "usage_metering_enabled": False, 626 } 627 628 629 def _resolve_settings_files() -> list[str]: 630 """Find optional external settings files. 631 632 Priority: 633 1. NEXTCLOUD_MCP_SETTINGS_FILE env var (absolute or relative path). 634 If set but the file does not exist, raise FileNotFoundError — 635 silently falling back to defaults on a typo would be a footgun. 636 .secrets.toml is looked for alongside the explicit file. 637 2. Otherwise ./settings.toml in cwd (for docker / dev workflows), 638 with .secrets.toml also looked for in cwd. 639 640 Returns an empty list if nothing is configured — that's fine, defaults 641 and env vars still apply. 642 """ 643 files: list[str] = [] 644 # A legitimate os.environ read: this runs BEFORE the dynaconf instance is 645 # built (it computes the settings_files list passed to Dynaconf), so it 646 # cannot go through dynaconf — you can't read the location of the settings 647 # file from the settings file. This + the MCP_DEPLOYMENT_MODE env_switcher 648 # are dynaconf's own bootstrap. All config *values* are dynaconf-driven; the 649 # only other reads are the legacy-env deprecation check and 650 # _warn_unknown_env_vars, which inspect env var NAMES, not values. 651 explicit = os.environ.get("NEXTCLOUD_MCP_SETTINGS_FILE") 652 if explicit: 653 p = Path(explicit) 654 if not p.exists(): 655 raise FileNotFoundError( 656 f"NEXTCLOUD_MCP_SETTINGS_FILE points to a file that does " 657 f"not exist: {explicit}" 658 ) 659 files.append(str(p)) 660 secrets = p.parent / ".secrets.toml" 661 else: 662 cwd_settings = Path.cwd() / "settings.toml" 663 if cwd_settings.exists(): 664 files.append(str(cwd_settings)) 665 secrets = Path.cwd() / ".secrets.toml" 666 if secrets.exists(): 667 files.append(str(secrets)) 668 return files 669 670 671 # Dynaconf instance — env vars always win (12-factor). Settings files are 672 # optional; when absent the defaults above provide the full key schema so 673 # env vars still override correctly. See ADR-024/025 for architecture. 674 _dynaconf = Dynaconf( 675 settings_files=_resolve_settings_files(), 676 environments=True, 677 envvar_prefix=False, 678 env_switcher="MCP_DEPLOYMENT_MODE", 679 ignore_unknown_envvars=True, 680 load_dotenv=False, 681 **_DEFAULTS, 682 validators=[ 683 # Port ranges 684 Validator("METRICS_PORT", gte=1, lte=65535), 685 # Positive integers 686 Validator("INGEST_STALLED_JOB_SECONDS", gte=1), 687 Validator("INGEST_TRANSIENT_MAX_ATTEMPTS", gte=1), 688 Validator("INGEST_RECLAIM_RETRY_DELAY_SECONDS", gte=0), 689 Validator("OIDC_DISCOVERY_MAX_ATTEMPTS", gte=1), 690 Validator("OIDC_DISCOVERY_BACKOFF_BASE", gte=0), 691 Validator("OIDC_DISCOVERY_BACKOFF_MAX", gte=0), 692 Validator("OFFICE_CAPTION_MAX_IMAGES", gte=0), 693 Validator("OFFICE_CAPTION_TIMEOUT_SECONDS", gt=0), 694 Validator("COLLABORA_TIMEOUT_SECONDS", gt=0), 695 Validator("PPTX_CAPTION_MAX_IMAGES", gte=0), 696 Validator("PPTX_CAPTION_TIMEOUT_SECONDS", gt=0), 697 Validator("QDRANT_INIT_MAX_ATTEMPTS", gte=1), 698 Validator("QDRANT_INIT_BACKOFF_BASE", gte=0), 699 Validator("QDRANT_INIT_BACKOFF_MAX", gte=0), 700 Validator("OLLAMA_EMBED_MAX_BATCH_CHARS", gte=1), 701 Validator("OLLAMA_EMBED_TIMEOUT", gte=1), 702 Validator("VECTOR_SYNC_SCAN_INTERVAL", gte=1), 703 Validator("VECTOR_SYNC_PROCESSOR_WORKERS", gte=1), 704 # 1 = park on the first exhausted-retry round. 0/negative would park a 705 # document before any attempt was recorded. 706 Validator("VECTOR_SYNC_MAX_INDEX_FAILURES", gte=1), 707 # Optional per-tier concurrency overrides (None = fall back to 708 # VECTOR_SYNC_PROCESSOR_WORKERS). Like the sibling above they must be 709 # >=1 when set — a 0/negative value would otherwise reach 710 # app.run_worker_async(concurrency=...) and fail with an opaque runtime 711 # error instead of a clear config error at startup. 712 Validator( 713 "VECTOR_SYNC_FAST_CONCURRENCY", 714 condition=lambda v: v is None or v >= 1, 715 messages={ 716 "condition": "VECTOR_SYNC_FAST_CONCURRENCY must be >= 1 when set" 717 }, 718 ), 719 Validator( 720 "VECTOR_SYNC_STRUCTURED_CONCURRENCY", 721 condition=lambda v: v is None or v >= 1, 722 messages={ 723 "condition": "VECTOR_SYNC_STRUCTURED_CONCURRENCY must be >= 1 when set" 724 }, 725 ), 726 Validator("VECTOR_SYNC_QUEUE_MAX_SIZE", gte=1), 727 Validator("VECTOR_SYNC_METRICS_REFRESH_INTERVAL", gte=1), 728 Validator("VECTOR_DENSITY_SNAPSHOT_INTERVAL", gte=1), 729 Validator("VECTOR_DENSITY_SNAPSHOT_MAX_DOCUMENTS", gte=1), 730 Validator("VECTOR_RAM_HNSW_OVERHEAD_FACTOR", gte=1), 731 Validator("VECTOR_SYNC_USER_POLL_INTERVAL", gte=1), 732 Validator("HEALTH_READY_REFRESH_INTERVAL", gte=1), 733 Validator("PORT", gte=1, lte=65535), 734 Validator("VERIFICATION_CONCURRENCY", gte=1), 735 Validator("DOCUMENT_CHUNK_SIZE", gte=1), 736 Validator("CHUNKING_CONFIG_VERSION", gte=1), 737 Validator("DOCUMENT_PARSE_TIMEOUT_SECONDS", gte=1), 738 Validator("DOCUMENT_OCR_TIMEOUT_SECONDS", gte=1), 739 # DOCUMENT_OCR_MODE is normalised + membership-checked in 740 # Settings.__post_init__ via _enum_fields (case-insensitive, like 741 # DOCUMENT_OCR_PROVIDER) — no strict dynaconf Validator here, so 742 # "Batch"/"SYNC" normalise instead of erroring. 743 # Poll cadence well above a few seconds (each poll re-runs the tier). 744 Validator("DOCUMENT_OCR_BATCH_POLL_SECONDS", gte=5), 745 Validator("DOCUMENT_PARSE_MEM_LIMIT_MB", gte=128), 746 # 0 disables the pre-parse PDF size cap; otherwise it must be positive. 747 Validator("DOCUMENT_MAX_PDF_SIZE_MB", gte=0), 748 # 0 disables page-windowed extraction; otherwise it must be positive. 749 Validator("DOCUMENT_PARSE_PAGE_WINDOW", gte=0), 750 # 0 disables markdown reconstruction; otherwise it must be positive. A 751 # negative value must fail fast rather than silently disable markdown 752 # fleet-wide -- the same silent-disarm class this setting exists to fix. 753 Validator("DOCUMENT_MARKDOWN_MAX_PAGES", gte=0), 754 # At least one parse must be able to run. 755 Validator("DOCUMENT_PARSE_PROCESS_SLOTS", gte=1), 756 # >=1: pymupdf4llm treats graphics_limit=0 as "no cap", which would 757 # re-expose the OOM this guards against. 758 Validator("DOCUMENT_PDF_GRAPHICS_LIMIT", gte=1), 759 # OCR escalation thresholds: quality + page-fraction are [0, 1]. 760 Validator("DOCUMENT_OCR_MIN_TEXT_QUALITY", gte=0, lte=1), 761 Validator("DOCUMENT_OCR_PAGE_FRACTION", gte=0, lte=1), 762 Validator("DOCUMENT_OCR_MIN_PAGE_CHARS", gte=0), 763 Validator("DOCUMENT_GLYPH_CORRUPTION_RATIO", gte=0, lte=1), 764 # Non-negative 765 Validator("DOCUMENT_CHUNK_OVERLAP", gte=0), 766 # RRF ranking constant. Must be >= 1: the fused score is 767 # 1/(rank + k) with rank 0-indexed, so k=0 divides by zero on the 768 # top-ranked point. 769 Validator("VECTOR_SEARCH_RRF_K", gte=1), 770 # Matryoshka truncation width. Optional (None = the model's full 771 # width), but a non-positive value would size the Qdrant collection at 772 # zero — catch it here rather than at collection creation. 773 Validator( 774 "EMBEDDING_DIMENSIONS", 775 condition=lambda v: v is None or v >= 1, 776 messages={"condition": "EMBEDDING_DIMENSIONS must be >= 1 when set"}, 777 ), 778 Validator("SEARCH_RERANK_POOL_SIZE", gte=1), 779 Validator("SEARCH_RERANK_MAX_CONCURRENCY", gte=1), 780 Validator("SEARCH_RERANK_TIMEOUT_SECONDS", gt=0), 781 # Non-empty strings 782 Validator("VECTOR_SYNC_TAG", len_min=1), 783 # VECTOR_SYNC_KEYWORD_TAG is optional (empty disables keyword-only 784 # discovery), so no len_min — but when set it must be a usable tag name. 785 # MAIL_INDEX_TAG is likewise optional (empty indexes all mail), but the 786 # Mail app rejects display names over 128 characters — catch that at 787 # startup instead of failing every mail scan with an HTTP 400. 788 Validator("MAIL_INDEX_TAG", len_max=128), 789 # WEBHOOK_SECRET is optional (None disables webhooks — GHSA-8vh3-g2qg-2h2c), 790 # but when set it must be long enough to resist guessing. Surfaces a 791 # weak/placeholder secret at startup rather than in a later audit. 792 Validator( 793 "WEBHOOK_SECRET", 794 condition=lambda v: v is None or len(v) >= 16, 795 messages={ 796 "condition": "WEBHOOK_SECRET must be at least 16 characters when set" 797 }, 798 ), 799 # Enum constraints (document_* enums are validated + normalized in 800 # __post_init__ via _enum_fields instead, for case-insensitive input). 801 Validator("LOG_FORMAT", is_in=["text", "json"]), -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/config.py line 612
- Likely real
- Uncertain, 45% confidence
Possible hardcoded password: 'None'
611 # call the gateway unauthenticated (matches today's not-yet-authed gateway). 612 "embedding_gateway_token_url": None, # M2M token endpoint 613 "embedding_gateway_client_id": None, 614 "embedding_gateway_client_secret": None, 615 "embedding_gateway_scope": None, # e.g. astrolabe-embedding-gateway/embed 616 # Query-side ACL pre-filter (design §11). OFF by default: a Qdrant 617 # `match any` on `acl_hash` excludes points missing the key, so enabling 618 # this before a real ACL backfill would silently drop legacy results. 619 # verify-on-read remains the correctness backstop regardless. 620 "acl_prefilter_enabled": False, 621 # Usage metering (Deck #67, control-plane usage-metering.md). OFF by 622 # default so OSS self-hosters don't accrue a metering table or write 623 # overhead; Astrolabe Cloud provisioning sets it true. When on, billable 624 # ops record rows into the app-DB usage_events table (best-effort). 625 "usage_metering_enabled": False, 626 } 627 628 629 def _resolve_settings_files() -> list[str]: 630 """Find optional external settings files. 631 632 Priority: 633 1. NEXTCLOUD_MCP_SETTINGS_FILE env var (absolute or relative path). 634 If set but the file does not exist, raise FileNotFoundError — 635 silently falling back to defaults on a typo would be a footgun. 636 .secrets.toml is looked for alongside the explicit file. 637 2. Otherwise ./settings.toml in cwd (for docker / dev workflows), 638 with .secrets.toml also looked for in cwd. 639 640 Returns an empty list if nothing is configured — that's fine, defaults 641 and env vars still apply. 642 """ 643 files: list[str] = [] 644 # A legitimate os.environ read: this runs BEFORE the dynaconf instance is 645 # built (it computes the settings_files list passed to Dynaconf), so it 646 # cannot go through dynaconf — you can't read the location of the settings 647 # file from the settings file. This + the MCP_DEPLOYMENT_MODE env_switcher 648 # are dynaconf's own bootstrap. All config *values* are dynaconf-driven; the 649 # only other reads are the legacy-env deprecation check and 650 # _warn_unknown_env_vars, which inspect env var NAMES, not values. 651 explicit = os.environ.get("NEXTCLOUD_MCP_SETTINGS_FILE") 652 if explicit: 653 p = Path(explicit) 654 if not p.exists(): 655 raise FileNotFoundError( 656 f"NEXTCLOUD_MCP_SETTINGS_FILE points to a file that does " 657 f"not exist: {explicit}" 658 ) 659 files.append(str(p)) 660 secrets = p.parent / ".secrets.toml" 661 else: 662 cwd_settings = Path.cwd() / "settings.toml" 663 if cwd_settings.exists(): 664 files.append(str(cwd_settings)) 665 secrets = Path.cwd() / ".secrets.toml" 666 if secrets.exists(): 667 files.append(str(secrets)) 668 return files 669 670 671 # Dynaconf instance — env vars always win (12-factor). Settings files are 672 # optional; when absent the defaults above provide the full key schema so 673 # env vars still override correctly. See ADR-024/025 for architecture. 674 _dynaconf = Dynaconf( 675 settings_files=_resolve_settings_files(), 676 environments=True, 677 envvar_prefix=False, 678 env_switcher="MCP_DEPLOYMENT_MODE", 679 ignore_unknown_envvars=True, 680 load_dotenv=False, 681 **_DEFAULTS, 682 validators=[ 683 # Port ranges 684 Validator("METRICS_PORT", gte=1, lte=65535), 685 # Positive integers 686 Validator("INGEST_STALLED_JOB_SECONDS", gte=1), 687 Validator("INGEST_TRANSIENT_MAX_ATTEMPTS", gte=1), 688 Validator("INGEST_RECLAIM_RETRY_DELAY_SECONDS", gte=0), 689 Validator("OIDC_DISCOVERY_MAX_ATTEMPTS", gte=1), 690 Validator("OIDC_DISCOVERY_BACKOFF_BASE", gte=0), 691 Validator("OIDC_DISCOVERY_BACKOFF_MAX", gte=0), 692 Validator("OFFICE_CAPTION_MAX_IMAGES", gte=0), 693 Validator("OFFICE_CAPTION_TIMEOUT_SECONDS", gt=0), 694 Validator("COLLABORA_TIMEOUT_SECONDS", gt=0), 695 Validator("PPTX_CAPTION_MAX_IMAGES", gte=0), 696 Validator("PPTX_CAPTION_TIMEOUT_SECONDS", gt=0), 697 Validator("QDRANT_INIT_MAX_ATTEMPTS", gte=1), 698 Validator("QDRANT_INIT_BACKOFF_BASE", gte=0), 699 Validator("QDRANT_INIT_BACKOFF_MAX", gte=0), 700 Validator("OLLAMA_EMBED_MAX_BATCH_CHARS", gte=1), 701 Validator("OLLAMA_EMBED_TIMEOUT", gte=1), 702 Validator("VECTOR_SYNC_SCAN_INTERVAL", gte=1), 703 Validator("VECTOR_SYNC_PROCESSOR_WORKERS", gte=1), 704 # 1 = park on the first exhausted-retry round. 0/negative would park a 705 # document before any attempt was recorded. 706 Validator("VECTOR_SYNC_MAX_INDEX_FAILURES", gte=1), 707 # Optional per-tier concurrency overrides (None = fall back to 708 # VECTOR_SYNC_PROCESSOR_WORKERS). Like the sibling above they must be 709 # >=1 when set — a 0/negative value would otherwise reach 710 # app.run_worker_async(concurrency=...) and fail with an opaque runtime 711 # error instead of a clear config error at startup. 712 Validator( 713 "VECTOR_SYNC_FAST_CONCURRENCY", 714 condition=lambda v: v is None or v >= 1, 715 messages={ 716 "condition": "VECTOR_SYNC_FAST_CONCURRENCY must be >= 1 when set" 717 }, 718 ), 719 Validator( 720 "VECTOR_SYNC_STRUCTURED_CONCURRENCY", 721 condition=lambda v: v is None or v >= 1, 722 messages={ 723 "condition": "VECTOR_SYNC_STRUCTURED_CONCURRENCY must be >= 1 when set" 724 }, 725 ), 726 Validator("VECTOR_SYNC_QUEUE_MAX_SIZE", gte=1), 727 Validator("VECTOR_SYNC_METRICS_REFRESH_INTERVAL", gte=1), 728 Validator("VECTOR_DENSITY_SNAPSHOT_INTERVAL", gte=1), 729 Validator("VECTOR_DENSITY_SNAPSHOT_MAX_DOCUMENTS", gte=1), 730 Validator("VECTOR_RAM_HNSW_OVERHEAD_FACTOR", gte=1), 731 Validator("VECTOR_SYNC_USER_POLL_INTERVAL", gte=1), 732 Validator("HEALTH_READY_REFRESH_INTERVAL", gte=1), 733 Validator("PORT", gte=1, lte=65535), 734 Validator("VERIFICATION_CONCURRENCY", gte=1), 735 Validator("DOCUMENT_CHUNK_SIZE", gte=1), 736 Validator("CHUNKING_CONFIG_VERSION", gte=1), 737 Validator("DOCUMENT_PARSE_TIMEOUT_SECONDS", gte=1), 738 Validator("DOCUMENT_OCR_TIMEOUT_SECONDS", gte=1), 739 # DOCUMENT_OCR_MODE is normalised + membership-checked in 740 # Settings.__post_init__ via _enum_fields (case-insensitive, like 741 # DOCUMENT_OCR_PROVIDER) — no strict dynaconf Validator here, so 742 # "Batch"/"SYNC" normalise instead of erroring. 743 # Poll cadence well above a few seconds (each poll re-runs the tier). 744 Validator("DOCUMENT_OCR_BATCH_POLL_SECONDS", gte=5), 745 Validator("DOCUMENT_PARSE_MEM_LIMIT_MB", gte=128), 746 # 0 disables the pre-parse PDF size cap; otherwise it must be positive. 747 Validator("DOCUMENT_MAX_PDF_SIZE_MB", gte=0), 748 # 0 disables page-windowed extraction; otherwise it must be positive. 749 Validator("DOCUMENT_PARSE_PAGE_WINDOW", gte=0), 750 # 0 disables markdown reconstruction; otherwise it must be positive. A 751 # negative value must fail fast rather than silently disable markdown 752 # fleet-wide -- the same silent-disarm class this setting exists to fix. 753 Validator("DOCUMENT_MARKDOWN_MAX_PAGES", gte=0), 754 # At least one parse must be able to run. 755 Validator("DOCUMENT_PARSE_PROCESS_SLOTS", gte=1), 756 # >=1: pymupdf4llm treats graphics_limit=0 as "no cap", which would 757 # re-expose the OOM this guards against. 758 Validator("DOCUMENT_PDF_GRAPHICS_LIMIT", gte=1), 759 # OCR escalation thresholds: quality + page-fraction are [0, 1]. 760 Validator("DOCUMENT_OCR_MIN_TEXT_QUALITY", gte=0, lte=1), 761 Validator("DOCUMENT_OCR_PAGE_FRACTION", gte=0, lte=1), 762 Validator("DOCUMENT_OCR_MIN_PAGE_CHARS", gte=0), 763 Validator("DOCUMENT_GLYPH_CORRUPTION_RATIO", gte=0, lte=1), 764 # Non-negative 765 Validator("DOCUMENT_CHUNK_OVERLAP", gte=0), 766 # RRF ranking constant. Must be >= 1: the fused score is 767 # 1/(rank + k) with rank 0-indexed, so k=0 divides by zero on the 768 # top-ranked point. 769 Validator("VECTOR_SEARCH_RRF_K", gte=1), 770 # Matryoshka truncation width. Optional (None = the model's full 771 # width), but a non-positive value would size the Qdrant collection at 772 # zero — catch it here rather than at collection creation. 773 Validator( 774 "EMBEDDING_DIMENSIONS", 775 condition=lambda v: v is None or v >= 1, 776 messages={"condition": "EMBEDDING_DIMENSIONS must be >= 1 when set"}, 777 ), 778 Validator("SEARCH_RERANK_POOL_SIZE", gte=1), 779 Validator("SEARCH_RERANK_MAX_CONCURRENCY", gte=1), 780 Validator("SEARCH_RERANK_TIMEOUT_SECONDS", gt=0), 781 # Non-empty strings 782 Validator("VECTOR_SYNC_TAG", len_min=1), 783 # VECTOR_SYNC_KEYWORD_TAG is optional (empty disables keyword-only 784 # discovery), so no len_min — but when set it must be a usable tag name. 785 # MAIL_INDEX_TAG is likewise optional (empty indexes all mail), but the 786 # Mail app rejects display names over 128 characters — catch that at 787 # startup instead of failing every mail scan with an HTTP 400. 788 Validator("MAIL_INDEX_TAG", len_max=128), 789 # WEBHOOK_SECRET is optional (None disables webhooks — GHSA-8vh3-g2qg-2h2c), 790 # but when set it must be long enough to resist guessing. Surfaces a 791 # weak/placeholder secret at startup rather than in a later audit. 792 Validator( 793 "WEBHOOK_SECRET", 794 condition=lambda v: v is None or len(v) >= 16, 795 messages={ 796 "condition": "WEBHOOK_SECRET must be at least 16 characters when set" 797 }, 798 ), 799 # Enum constraints (document_* enums are validated + normalized in 800 # __post_init__ via _enum_fields instead, for case-insensitive input). 801 Validator("LOG_FORMAT", is_in=["text", "json"]), 802 Validator( 803 "LOG_LEVEL", 804 is_in=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], 805 ), 806 ], 807 ) 808 809 810 def _reload_config(): 811 """Reload dynaconf settings from files and environment. 812 813 Call this in tests after modifying os.environ to refresh the cache. 814 Re-validates all validators since reload() only checks unchecked ones. 815 """ 816 _dynaconf.reload() 817 _dynaconf.validators.validate_all() 818 _clear_settings_caches() 819 820 821 _ephemeral_db_path: str | None = None 822 823 824 def get_token_db_path() -> str: 825 """Resolve the token SQLite database path. 826 827 Priority: 828 1. TOKEN_STORAGE_DB if explicitly set — docker-compose pins 829 /app/data/tokens.db this way. Read via dynaconf, which picks up 830 the env var because TOKEN_STORAGE_DB is declared in _DEFAULTS. 831 2. Otherwise a per-process tempfile under tempfile.gettempdir(), 832 allocated lazily and deleted at interpreter exit via atexit. 833 Ephemeral: tokens are wiped on restart, matching the Qdrant 834 ":memory:" default pattern used elsewhere in this project. 835 """ 836 explicit = _dynaconf.get("TOKEN_STORAGE_DB") 837 if explicit: 838 return str(explicit) 839 global _ephemeral_db_path 840 if _ephemeral_db_path is None: 841 fd, path = tempfile.mkstemp( 842 prefix=f"nextcloud-mcp-tokens-{os.getpid()}-", suffix=".db" 843 ) 844 os.close(fd) 845 _ephemeral_db_path = path 846 847 def _cleanup(p: str = path) -> None: 848 try: 849 if os.path.exists(p): 850 os.unlink(p) 851 except OSError: 852 pass 853 854 atexit.register(_cleanup) 855 return _ephemeral_db_path 856 857 858 def is_ephemeral_token_db(path: str) -> bool: 859 """Return True if the given path is the process-local ephemeral tempfile. 860 861 Precondition: `get_token_db_path()` must have been called at least once 862 in this process to allocate the tempfile. If called before allocation, 863 this returns False for any input (including the eventual tempfile path), 864 because there is nothing to compare against yet. In practice every call 865 site in this repo resolves the path via `get_token_db_path()` first. 866 """ 867 return path == _ephemeral_db_path 868 869 870 def get_database_url() -> str: 871 """Resolve the SQLAlchemy database URL for token storage. 872 873 Priority: 874 1. ``DATABASE_URL`` if set — any SQLAlchemy URL is accepted; the primary 875 supported backends are ``postgresql+psycopg://...`` for HA k8s 876 deployments and ``sqlite+aiosqlite:///...`` for development. 877 2. Otherwise build ``sqlite+aiosqlite:///{get_token_db_path()}`` so the 878 legacy ``TOKEN_STORAGE_DB`` env var and the ephemeral-tempfile 879 fallback both keep working unchanged. 880 """ 881 explicit = _dynaconf.get("DATABASE_URL") 882 if explicit: 883 return str(explicit) 884 return f"sqlite+aiosqlite:///{get_token_db_path()}" 885 886 887 def is_sqlite_url(url: str) -> bool: 888 """Return True for SQLite SQLAlchemy URLs (used to gate sqlite-only logic 889 like file-permission hardening and ``sqlite_master`` legacy lookups). 890 891 Recognizes both file-backed (``sqlite+aiosqlite:///path/to/db``) and 892 in-memory (``sqlite+aiosqlite:///:memory:``) URLs. The caller is 893 responsible for handling ``:memory:`` as a magic value where a real 894 filesystem path is expected. 895 """ 896 return url.lower().startswith("sqlite") 897 898 899 def mask_db_password(url: str) -> str: 900 """Return a logger-safe rendering of a SQLAlchemy URL. 901 902 DATABASE_URL routinely carries a password (e.g. 903 ``postgresql+psycopg://mcp:secret@db/mcp``); logging it raw leaks the 904 secret to stdout/stderr and any aggregator. SQLAlchemy's 905 :func:`make_url` + ``render_as_string(hide_password=True)`` substitutes 906 a fixed ``***`` placeholder while keeping the rest of the URL intact 907 so operators can still see which host / driver they're hitting. 908 """ 909 try: 910 from sqlalchemy.engine.url import make_url # noqa: PLC0415 911 912 return make_url(url).render_as_string(hide_password=True) 913 except Exception: 914 # If parsing fails (e.g. an explicit ssl-disable test URL with an 915 # exotic shape), fall back to a regex that scrubs any 916 # ``://user:password@`` pattern. Never raise from a logging path. 917 return re.sub(r"(://[^:/]+):[^@]*@", r"\1:***@", url) 918 919 920 LOGGING_CONFIG = { 921 "version": 1, 922 "disable_existing_loggers": False, 923 "handlers": { 924 "default": { 925 "class": "logging.StreamHandler", 926 "formatter": "http", 927 }, 928 }, 929 "formatters": { 930 "http": { 931 "format": "%(levelname)s [%(asctime)s] %(name)s - %(message)s", 932 "datefmt": "%Y-%m-%d %H:%M:%S", 933 }, 934 }, 935 "loggers": { 936 "": { 937 "handlers": ["default"], 938 "level": "INFO", 939 }, 940 "httpx": { 941 "handlers": ["default"], 942 "level": "INFO", 943 "propagate": False, # Prevent propagation to root logger 944 }, 945 "httpcore": { 946 "handlers": ["default"], 947 "level": "INFO", 948 "propagate": False, # Prevent propagation to root logger 949 }, 950 "uvicorn": { 951 "handlers": ["default"], 952 "level": "INFO", 953 "propagate": False, 954 }, 955 "uvicorn.access": { 956 "handlers": ["default"], 957 "level": "INFO", 958 "propagate": False, 959 }, 960 "uvicorn.error": { 961 "handlers": ["default"], 962 "level": "INFO", 963 "propagate": False, 964 }, 965 }, 966 } 967 968 969 def setup_logging(): 970 logging.config.dictConfig(LOGGING_CONFIG) 971 972 973 # Document Processing Configuration 974 975 976 def get_document_processor_config() -> dict[str, Any]: 977 """Get document processor configuration from dynaconf. 978 979 Each optional processor is gated by its own ``ENABLE_*`` flag. There is no 980 master switch: parsing a document on read is a per-call decision made by the 981 caller (``nc_webdav_read_file``'s ``parse_document`` argument), not an 982 instance-wide setting. The built-in PDF tiers (pypdfium2 / pymupdf / OCR) 983 self-register when ``document_processors`` is first imported and never appear 984 here. 985 986 Returns: 987 Dict with processor configs: 988 { 989 "default_processor": str, 990 "processors": { 991 "unstructured": {...}, 992 "tesseract": {...}, 993 "custom": {...}, 994 } 995 } 996 """ 997 config: dict[str, Any] = { 998 "default_processor": _dynaconf.get("DOCUMENT_PROCESSOR"), 999 "processors": {}, 1000 } 1001 1002 # Unstructured configuration 1003 if _dynaconf.get("ENABLE_UNSTRUCTURED"): 1004 languages_str = _dynaconf.get("UNSTRUCTURED_LANGUAGES") 1005 config["processors"]["unstructured"] = { 1006 "api_url": _dynaconf.get("UNSTRUCTURED_API_URL"), 1007 "timeout": _dynaconf.get("UNSTRUCTURED_TIMEOUT"), 1008 "strategy": _dynaconf.get("UNSTRUCTURED_STRATEGY"), 1009 "languages": [ 1010 lang.strip() for lang in languages_str.split(",") if lang.strip() 1011 ], 1012 "progress_interval": _dynaconf.get("PROGRESS_INTERVAL"), 1013 } 1014 1015 # Tesseract configuration 1016 if _dynaconf.get("ENABLE_TESSERACT"): 1017 config["processors"]["tesseract"] = { 1018 "tesseract_cmd": _dynaconf.get("TESSERACT_CMD"), # None = auto-detect 1019 "lang": _dynaconf.get("TESSERACT_LANG"), 1020 } 1021 1022 # NB: pymupdf is deliberately absent. It is the built-in ``structured`` tier, 1023 # registered when ``document_processors`` is first imported (lazily, on the 1024 # first parse) and tuned from ``Settings.pymupdf_*``. Listing it here would 1025 # make ``processors`` non-empty on every deployment, which is what tells app 1026 # startup whether it has anything to register -- and so whether it must 1027 # import the parse stack at all (#877). 1028 1029 # Custom processor (via HTTP API) 1030 if _dynaconf.get("ENABLE_CUSTOM_PROCESSOR"): 1031 custom_url = _dynaconf.get("CUSTOM_PROCESSOR_URL") 1032 if custom_url: 1033 supported_types_str = _dynaconf.get("CUSTOM_PROCESSOR_TYPES") 1034 supported_types = { 1035 t.strip() for t in supported_types_str.split(",") if t.strip() 1036 } 1037 1038 config["processors"]["custom"] = { 1039 "name": _dynaconf.get("CUSTOM_PROCESSOR_NAME"), 1040 "api_url": custom_url, 1041 "api_key": _dynaconf.get("CUSTOM_PROCESSOR_API_KEY"), 1042 "timeout": _dynaconf.get("CUSTOM_PROCESSOR_TIMEOUT"), 1043 "supported_types": supported_types, 1044 } 1045 1046 # Docling configuration (docling-serve HTTP API). Registered only when a URL is 1047 # set, so a bare ENABLE_DOCLING doesn't shadow other image processors with a 1048 # dead endpoint (mirrors the custom_url guard above). The standalone processor 1049 # auto-serves images; PDFs go through the OCR backend 1050 # (DOCUMENT_OCR_PROVIDER=docling) -- docling is an OCR provider, not a tier of 1051 # its own. 1052 if _dynaconf.get("ENABLE_DOCLING"): 1053 docling_url = _dynaconf.get("DOCLING_API_URL") 1054 if docling_url: 1055 lang_str = _dynaconf.get("DOCLING_OCR_LANG") or "" 1056 config["processors"]["docling"] = { 1057 "api_url": docling_url, 1058 "timeout": _dynaconf.get("DOCLING_TIMEOUT"), 1059 "ocr_lang": [s.strip() for s in lang_str.split(",") if s.strip()], 1060 "do_ocr": _dynaconf.get("DOCLING_DO_OCR"), 1061 # Normalize like Settings.__post_init__ does (.strip().lower()) so the 1062 # image path matches convert_file()'s ``pipeline == "vlm"`` check -- 1063 # otherwise DOCLING_PIPELINE=VLM would silently fall back to standard 1064 # here while the OCR-backend path (Settings-validated) uses vlm. 1065 # vlm_preset is server-defined and case-sensitive, so it stays raw. 1066 "pipeline": (_dynaconf.get("DOCLING_PIPELINE") or "standard") 1067 .strip() 1068 .lower(), 1069 "vlm_preset": _dynaconf.get("DOCLING_VLM_PRESET"), 1070 "progress_interval": _dynaconf.get("PROGRESS_INTERVAL"), 1071 } 1072 1073 return config 1074 1075 1076 @dataclass 1077 class Settings: 1078 """Application settings from environment variables.""" 1079 1080 # Deployment mode (ADR-021: explicit mode selection; updated by ADR-022) 1081 # Optional: If not set, mode is auto-detected from other settings 1082 # Valid values: single_user_basic, multi_user_basic, login_flow 1083 # (ADR-022: `oauth_single_audience` was renamed to `login_flow`.) 1084 deployment_mode: str | None = None 1085 1086 # OAuth/OIDC settings 1087 oidc_discovery_url: str | None = None 1088 oidc_client_id: str | None = None 1089 oidc_client_secret: str | None = None 1090 oidc_issuer: str | None = None 1091 oidc_resource_server_id: str | None = None 1092 oidc_token_type: str = "Bearer" # NEXTCLOUD_OIDC_TOKEN_TYPE 1093 oidc_scopes: str = "" # NEXTCLOUD_OIDC_SCOPES (space-separated) 1094 1095 # OIDC discovery startup resilience. Discovery runs synchronously at boot 1096 # and is fatal on failure; on a freshly-scheduled pod the egress path (e.g. 1097 # Cilium toFQDN allow + egress-gateway SNAT programming) can take a few 1098 # seconds to converge, during which the request is dropped and times out. 1099 # Retry with capped exponential backoff + jitter so a cold-start race 1100 # doesn't crashloop the backend. Set OIDC_DISCOVERY_MAX_ATTEMPTS=1 to 1101 # restore the original fail-fast behavior. 1102 oidc_discovery_max_attempts: int = 10 # OIDC_DISCOVERY_MAX_ATTEMPTS 1103 oidc_discovery_backoff_base: float = 1.0 # OIDC_DISCOVERY_BACKOFF_BASE (s) 1104 oidc_discovery_backoff_max: float = 15.0 # OIDC_DISCOVERY_BACKOFF_MAX (s) 1105 # Startup Qdrant-collection-init retry/backoff. Qdrant may be briefly 1106 # unreachable during a rolling deploy (pod ordering, network-policy 1107 # convergence); retry transient connection failures with capped exponential 1108 # backoff + jitter instead of crashlooping with a full traceback. Genuine 1109 # errors (auth/config, e.g. a 4xx) still fail fast. Set 1110 # QDRANT_INIT_MAX_ATTEMPTS=1 to restore fail-fast. 1111 qdrant_init_max_attempts: int = 30 # QDRANT_INIT_MAX_ATTEMPTS 1112 qdrant_init_backoff_base: float = 1.0 # QDRANT_INIT_BACKOFF_BASE (s) 1113 qdrant_init_backoff_max: float = 10.0 # QDRANT_INIT_BACKOFF_MAX (s) 1114 port: int = 8000 # Server port (PORT); used to build fallback URLs 1115 1116 # Nextcloud settings 1117 nextcloud_host: str | None = None 1118 nextcloud_username: str | None = None 1119 nextcloud_password: str | None = None 1120 1121 # Browser-reachable public URL for OAuth/Login-Flow-v2 redirects when 1122 # NEXTCLOUD_HOST is an internal Docker hostname. Falls back to 1123 # nextcloud_host when unset. 1124 # 1125 # NOTE: this doubles as the OAuth *issuer* URL used for JWT ``iss`` 1126 # validation. In external-IdP mode (e.g. Keycloak) the issuer is the IdP, 1127 # NOT Nextcloud — so this value points at the IdP, not the browser-reachable 1128 # Nextcloud host. Use ``nextcloud_public_url`` / ``nextcloud_browser_url`` 1129 # for anything that must resolve to Nextcloud itself (Login Flow v2 login 1130 # URLs, elicitation links). 1131 nextcloud_public_issuer_url: str | None = None 1132 1133 # Browser-reachable public URL of the *Nextcloud* instance, used to rewrite 1134 # Login Flow v2 login URLs and elicitation links when NEXTCLOUD_HOST is an 1135 # internal Docker hostname. Distinct from ``nextcloud_public_issuer_url`` 1136 # because, in external-IdP (Keycloak/OIDC) deployments, the OAuth issuer is 1137 # the IdP while Login Flow v2 must still point the browser at Nextcloud. 1138 # Falls back to ``nextcloud_public_issuer_url`` then ``nextcloud_host`` (see 1139 # ``nextcloud_browser_url``) so single-IdP (login-flow) deployments that set 1140 # only NEXTCLOUD_PUBLIC_ISSUER_URL keep working unchanged. 1141 nextcloud_public_url: str | None = None 1142 1143 # Browser cookie Secure flag. None = auto-detect from nextcloud_host 1144 # scheme (https → True, else False). Set COOKIE_SECURE=true/false to 1145 # override. 1146 cookie_secure: bool | None = None 1147 1148 # Nextcloud SSL/TLS settings 1149 nextcloud_verify_ssl: bool = True 1150 nextcloud_ca_bundle: str | None = None 1151 1152 # Reuse pooled keep-alive connections for the Nextcloud httpx client. 1153 # Default True preserves low-latency interactive traffic. Set 1154 # NEXTCLOUD_HTTP_KEEPALIVE=false to force a fresh connection per request 1155 # (max_keepalive_connections=0) — mirrors the DATABASE_POOL_SIZE→NullPool 1156 # precedent and prevents a truncated/desynced response from poisoning a 1157 # pooled connection on flaky CDN/WAN paths (see #965). 1158 nextcloud_http_keepalive: bool = True 1159 1160 # Postgres connection pool sizing — DEPRECATED, retained for 1161 # backward compatibility. The psycopg engine switched to NullPool 1162 # in #799 (cross-event-loop crashes under anyio TaskGroups made 1163 # the original QueuePool + pool_pre_ping setup unsafe). These 1164 # fields no longer affect the Postgres engine; the validators 1165 # below still reject invalid values so misconfigured deploys 1166 # fail loudly rather than silently. See ADR-026 § Connection 1167 # pool and docs/configuration.md. 1168 database_pool_size: int = 2 1169 database_max_overflow: int = 5 1170 1171 # ADR-005: Token Audience Validation (required for OAuth mode) 1172 nextcloud_mcp_server_url: str | None = None # MCP server URL (used as audience) 1173 nextcloud_resource_uri: str | None = None # Nextcloud resource identifier 1174 1175 # Token verification endpoints 1176 jwks_uri: str | None = None 1177 introspection_uri: str | None = None 1178 userinfo_uri: str | None = None 1179 1180 # Progressive Consent settings (always enabled - no flag needed) 1181 enable_offline_access: bool = False 1182 1183 # Multi-user BasicAuth pass-through mode (ADR-019 interim solution). 1184 # Internal — not user-settable; the ENABLE_MULTI_USER_BASIC_AUTH env-var 1185 # alias was removed in the ADR-022 follow-up. Auto-set by 1186 # Settings.__post_init__ when MCP_DEPLOYMENT_MODE=multi_user_basic. When True, 1187 # the MCP server extracts BasicAuth credentials from request headers and 1188 # passes them through to Nextcloud APIs (no storage, stateless). Kept 1189 # as a field for backward compat with the runtime call sites that read it. 1190 enable_multi_user_basic_auth: bool = False 1191 1192 # Login Flow v2 derived flag (ADR-022). Internal — not user-settable. 1193 # Auto-set by Settings.__post_init__ when the resolved deployment mode is 1194 # LOGIN_FLOW. Kept as a field for backward compat with the runtime call 1195 # sites that read it (app.py, context.py, scope_authorization.py). 1196 enable_login_flow: bool = False 1197 1198 # Token and webhook storage settings 1199 # TOKEN_ENCRYPTION_KEY: Optional - Only required for OAuth token storage operations. 1200 # Webhook tracking works without encryption key. 1201 # If set, must be a valid base64-encoded Fernet key (32 bytes). -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/document_processors/_isolation.py line 107
- Likely real
- Probably noise, 28% confidence
Try, Except, Continue detected.
106 raw = doc[i].get_text("text") 107 except Exception: # noqa: BLE001 -- per-page best effort, never fail the parse 108 continue 109 raw_len = len(raw.strip()) -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /scripts/dbquery.py line 15
- Likely real
- Probably noise, 12% confidence
Consider possible security implications associated with the subprocess module.
14 import argparse 15 import subprocess 16 import sys -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /scripts/sqlitequery.py line 16
- Likely real
- Probably noise, 12% confidence
Consider possible security implications associated with the subprocess module.
15 import argparse 16 import subprocess 17 import sys -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 9% confidence
Loose version constraint: 'python-json-logger>=3.2.0' — attacker could push malicious update
python-json-logger>=3.2.0 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 4% confidence
Loose version constraint: 'opentelemetry-api>=1.28.2' — attacker could push malicious update
opentelemetry-api>=1.28.2 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 4% confidence
Loose version constraint: 'opentelemetry-instrumentation-logging>=0.49b2' — attacker could push malicious update
opentelemetry-instrumentation-logging>=0.49b2 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 4% confidence
Loose version constraint: 'python-pptx>=1.0.2' — attacker could push malicious update
python-pptx>=1.0.2 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 4% confidence
Loose version constraint: 'python-docx>=1.2.0' — attacker could push malicious update
python-docx>=1.2.0 -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /scripts/dbquery.py line 66
- Likely real
- Probably noise, 3% confidence
subprocess call - check for execution of untrusted input.
65 66 result = subprocess.run( 67 cmd, 68 capture_output=True, 69 text=True, 70 cwd=compose_dir, 71 ) 72 -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /scripts/sqlitequery.py line 97
- Likely real
- Probably noise, 3% confidence
subprocess call - check for execution of untrusted input.
96 97 result = subprocess.run( 98 cmd, 99 capture_output=True, 100 text=True, 101 cwd=compose_dir, 102 ) 103 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'pyjwt>=2.8.0' — attacker could push malicious update
pyjwt[crypto]>=2.8.0 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'psycopg>=3.2' — attacker could push malicious update
psycopg[binary,pool]>=3.2 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'recurring-ical-events>=3.8.0,<4.0' — attacker could push malicious update
recurring-ical-events>=3.8.0,<4.0 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'caldav>=3.0.1,<4.0' — attacker could push malicious update
caldav>=3.0.1,<4.0 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'click>=8.1.8' — attacker could push malicious update
click>=8.1.8 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'pydantic>=2.11.4' — attacker could push malicious update
pydantic>=2.11.4 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'pythonvcard4>=0.2.0' — attacker could push malicious update
pythonvcard4>=0.2.0 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'packaging>=24.0' — attacker could push malicious update
packaging>=24.0 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'numpy>=1.26' — attacker could push malicious update
numpy>=1.26 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'botocore>=1.35.0' — attacker could push malicious update
botocore>=1.35.0 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'lxml>=5.0' — attacker could push malicious update
lxml>=5.0 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'cryptography>=42.0' — attacker could push malicious update
cryptography>=42.0 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'uvicorn>=0.30' — attacker could push malicious update
uvicorn>=0.30 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'anyio>=4.0' — attacker could push malicious update
anyio>=4.0 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'markdownify>=0.14.1' — attacker could push malicious update
markdownify>=0.14.1 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'openpyxl>=3.1.5' — attacker could push malicious update
openpyxl>=3.1.5 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'olefile>=0.47' — attacker could push malicious update
olefile>=0.47 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'procrastinate>=3.8' — attacker could push malicious update
procrastinate>=3.8 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'pyroscope-io>=0.8.0' — attacker could push malicious update
pyroscope-io>=0.8.0 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'aiosqlite>=0.20.0' — attacker could push malicious update
aiosqlite>=0.20.0 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'pypdfium2>=5.9.0' — attacker could push malicious update
pypdfium2>=5.9.0 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'sqlalchemy>=2.0' — attacker could push malicious update
sqlalchemy[asyncio]>=2.0 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'mistralai>=2.4.5' — attacker could push malicious update
mistralai>=2.4.5 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'dynaconf>=3.2.13,<4.0' — attacker could push malicious update
dynaconf>=3.2.13,<4.0 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'openai>=2.8.1' — attacker could push malicious update
openai>=2.8.1 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'langchain-text-splitters>=1.0.0' — attacker could push malicious update
langchain-text-splitters>=1.0.0 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'jinja2>=3.1.6' — attacker could push malicious update
jinja2>=3.1.6 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'opentelemetry-exporter-otlp-proto-grpc>=1.28.2' — attacker could push malicious update
opentelemetry-exporter-otlp-proto-grpc>=1.28.2 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'opentelemetry-instrumentation-httpx>=0.49b2' — attacker could push malicious update
opentelemetry-instrumentation-httpx>=0.49b2 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'opentelemetry-instrumentation-asgi>=0.49b2' — attacker could push malicious update
opentelemetry-instrumentation-asgi>=0.49b2 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'opentelemetry-sdk>=1.28.2' — attacker could push malicious update
opentelemetry-sdk>=1.28.2 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'prometheus-client>=0.21.0' — attacker could push malicious update
prometheus-client>=0.21.0 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'boto3>=1.35.0' — attacker could push malicious update
boto3>=1.35.0 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'fastembed>=0.7.3' — attacker could push malicious update
fastembed>=0.7.3 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'qdrant-client>=1.17.0' — attacker could push malicious update
qdrant-client>=1.17.0 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'authlib>=1.6.5' — attacker could push malicious update
authlib>=1.6.5 -
- Engine
- Dependency checker · CVEs, typosquats and risky installs
- Counts towards
- Supply chain
- File
- pyproject.toml
- Likely real
- Probably noise, 2% confidence
Loose version constraint: 'alembic>=1.14.0' — attacker could push malicious update
alembic>=1.14.0 -
- Engine
- YARA · Malware signature rules
- Counts towards
- Malicious pattern
- File
- /infra/terraform/nextcloud-mcp-server/qdrant.tf
- Likely real
- Probably noise, 0% confidence
YARA rule matched: detect_downloader — Downloads a remote resource - common and low-signal alone; see detect_download_and_execute for the composite version
-
- Engine
- YARA · Malware signature rules
- Counts towards
- Malicious pattern
- File
- /.github/workflows/test.yml
- Likely real
- Probably noise, 0% confidence
YARA rule matched: detect_downloader — Downloads a remote resource - common and low-signal alone; see detect_download_and_execute for the composite version
-
- Engine
- YARA · Malware signature rules
- Counts towards
- Malicious pattern
- File
- /.github/workflows/pact.yml
- Likely real
- Probably noise, 0% confidence
YARA rule matched: detect_downloader — Downloads a remote resource - common and low-signal alone; see detect_download_and_execute for the composite version
-
- Engine
- YARA · Malware signature rules
- Counts towards
- Malicious pattern
- File
- /docker-compose.yml
- Likely real
- Probably noise, 0% confidence
YARA rule matched: detect_downloader — Downloads a remote resource - common and low-signal alone; see detect_download_and_execute for the composite version
-
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/document_processors/_ooxml.py line 132
- Likely real
- Probably noise, 0% confidence
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
131 """ 132 assert self._docling_api_url is not None # guarded by enabled 133 try: -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/vector/visualization.py line 174
- Likely real
- Probably noise, 0% confidence
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
173 # After fit, these attributes are guaranteed to be set 174 assert pca.explained_variance_ratio_ is not None 175 -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/vector/scanner.py line 1018
- Likely real
- Probably noise, 0% confidence
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
1017 if not initial_sync: 1018 assert qdrant_client is not None # narrow for the type checker 1019 # Bucket indexed file points by index mode so the deletion fail-safe -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/vector/qdrant_client.py line 905
- Likely real
- Probably noise, 0% confidence
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
904 # set it, or a sibling waiter set it before we got the lock. 905 assert _qdrant_client is not None 906 return _qdrant_client -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/vector/qdrant_client.py line 810
- Likely real
- Probably noise, 0% confidence
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
809 # Type narrowing: vectors must be VectorParams if not dict 810 assert isinstance(vectors, VectorParams) 811 actual_dimension = vectors.size -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/vector/processor.py line 2006
- Likely real
- Probably noise, 0% confidence
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
2005 # (buffered fallback); both are set for PDF files. 2006 assert source is not None 2007 -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/vector/processor.py line 1631
- Likely real
- Probably noise, 0% confidence
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
1630 else: 1631 assert source is not None 1632 result = await registry.process_source(source) -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/vector/processor.py line 1612
- Likely real
- Probably noise, 0% confidence
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
1611 # when it did not short-circuit on the pre-flight gate. 1612 assert source is not None 1613 # Per-document identity, forwarded to every tier's processor. -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/vector/processor.py line 1561
- Likely real
- Probably noise, 0% confidence
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
1560 assert content_type is not None 1561 assert file_path is not None 1562 -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 635
- Likely real
- Probably noise, 0% confidence
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
634 """ 635 assert self.engine is not None, "engine must be built before migration lock" 636 if is_sqlite_url(self.database_url): -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/auth/storage.py line 676
- Likely real
- Probably noise, 0% confidence
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
675 """ 676 assert self.engine is not None, "RefreshTokenStorage.initialize() not called" 677 start = time.time() -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/client/calendar.py line 1013
- Likely real
- Probably noise, 0% confidence
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
1012 ) 1013 assert calendar.client is not None 1014 response = await calendar.client.report(str(calendar.url), body, depth=1) # type: ignore[misc] # ty: ignore[invalid-await] # dual-mode -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/vector/processor.py line 1560
- Likely real
- Probably noise, 0% confidence
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
1559 # is None on the streamed path -- the document lives in ``source``. 1560 assert content_type is not None 1561 assert file_path is not None -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/providers/mistral.py line 123
- Likely real
- Probably noise, 0% confidence
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
122 123 assert self.embedding_model is not None 124 response = await self.client.embeddings.create_async( -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/providers/mistral.py line 206
- Likely real
- Probably noise, 0% confidence
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
205 """ 206 assert self.embedding_model is not None 207 response = await self.client.embeddings.create_async( -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/providers/openai.py line 153
- Likely real
- Probably noise, 0% confidence
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
152 153 assert self.embedding_model is not None # Type narrowing 154 response = await self.client.embeddings.create( -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/providers/openai.py line 245
- Likely real
- Probably noise, 0% confidence
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
244 """ 245 assert self.embedding_model is not None # Type narrowing 246 response = await self.client.embeddings.create( -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/providers/registry.py line 52
- Likely real
- Probably noise, 0% confidence
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
51 # provider is gateway; assert narrows the type for the checker. 52 assert settings.embedding_gateway_url is not None 53 # __post_init__ enforces all-or-nothing on the M2M creds, so -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/providers/registry.py line 57
- Likely real
- Probably noise, 0% confidence
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
56 if settings.embedding_gateway_client_id: 57 assert settings.embedding_gateway_token_url is not None 58 assert settings.embedding_gateway_client_secret is not None -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/providers/registry.py line 58
- Likely real
- Probably noise, 0% confidence
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
57 assert settings.embedding_gateway_token_url is not None 58 assert settings.embedding_gateway_client_secret is not None 59 token_provider = GatewayTokenProvider( -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/vector/processor.py line 1411
- Likely real
- Probably noise, 0% confidence
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
1410 # Type narrowing: card, board, stack are all set if we reach here 1411 assert card is not None 1412 assert board is not None -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/vector/processor.py line 1412
- Likely real
- Probably noise, 0% confidence
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
1411 assert card is not None 1412 assert board is not None 1413 assert stack is not None -
- Engine
- Bandit · Python security linter
- Counts towards
- Vulnerability
- File
- /nextcloud_mcp_server/vector/processor.py line 1413
- Likely real
- Probably noise, 0% confidence
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
1412 assert board is not None 1413 assert stack is not None 1414
No findings match these filters.