Commit Graph
100 Commits
Author SHA1 Message Date
Chris CoutinhoandClaude Opus 4.7 21a4a90144 docs(cla): name the contracting legal entity
Adds "Astrolabe Cloud" as the named maintainer in the CLA Background
so the contracting party is identifiable, addressing reviewer feedback
on PR #723.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:18:38 +02:00
Chris CoutinhoandClaude Opus 4.7 d4a48d4f65 docs: add contributor license agreement
Adds a CLA so future contributions can be relicensed if the project
later offers commercial terms alongside AGPL-3.0. Adapted from the
Apache 2.0 ICLA with Dutch-law modifications: moral rights waiver
under Auteurswet art. 25, GDPR data-processing notice referencing
cla-assistant.io, and Amsterdam jurisdiction.

Signing is administered via the hosted cla-assistant.io service
(configured outside this repo); the Gist referenced there is kept in
sync with CLA.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:03:27 +02:00
Chris CoutinhoandClaude Opus 4.7 4a2e3fc169 test: stagger parallel OAuth fetches for login-flow users
Mirrors the per-user delay pattern used in tests/conftest.py:all_oauth_tokens
(commit 963a504). Without it, all four Playwright browser contexts hit
Nextcloud's OIDC authorize endpoint simultaneously and the last users in
iteration order (charlie/diana) frequently time out on the consent screen
in CI, producing `TimeoutError: Timeout waiting for OAuth callback`.

Uses a 0.5s stagger locally and 10s in GITHUB_ACTIONS, matching the
existing fixture so behaviour stays consistent across the two parallel
fixtures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 07:05:17 +02:00
Chris CoutinhoandClaude Opus 4.7 9cf4e16672 test: poll Astrolabe search until the target note is indexed
The previous run on nc32 failed at the search-result assertion because
`wait_for_vector_sync` returned on the first indexed-count bump (deck
seed cards) before this specific note hit Qdrant. Replace the single
search call with a poll that retries every 2s until the unique term
returns our note, or times out after 60s with a loud diagnostic. The
previously-observed flake would now wait past the deck-card indexing
window rather than racing it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 21:55:16 +02:00
Chris CoutinhoandClaude Opus 4.7 b3bd14f183 test: tighten regression guard and hoist httpx import
Per review:
- Hoist `import httpx` out of the two test function bodies and into
  the module imports at the top of
  test_astrolabe_chunk_context.py.
- Simplify the regression guard in
  test_management_chunk_context_endpoint.py to use
  `mock.assert_awaited_once_with(...)` instead of manually unpacking
  call_args. This is stricter — it fails loudly on signature change —
  and matches the canonical pattern for asserting mock calls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 20:26:41 +02:00
Chris CoutinhoandClaude Opus 4.7 06d871ce22 test: address chunk-context review comments
- Rename test_chunk_context_endpoint_handles_missing_app_password to
  test_chunk_context_endpoint_rejects_invalid_bearer so it reflects
  what is actually exercised: an invalid bearer is rejected upfront at
  validate_token_and_get_user, not at the NotProvisionedError branch.
  The NotProvisionedError path is covered by the corresponding unit
  test in test_management_chunk_context_endpoint.py.
- Hoist `import base64` to module level per PEP 8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 20:17:34 +02:00
Chris CoutinhoandClaude Opus 4.7 bea5c3f5ee test: include Nextcloud CSRF token in chunk-context integration test
Astrolabe's ApiController endpoints (search, chunk-context) require a
CSRF `requesttoken` header — axios picks it up from OC.requestToken
automatically in the SPA, but page.request.get() does not.

The first CI run failed on the search step with 412 CSRF check failed
before reaching the chunk-context assertion that was supposed to
surface the handler bug. Load the Astrolabe page, read OC.requestToken,
and pass it on both calls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 20:15:05 +02:00
Chris CoutinhoandClaude Opus 4.7 8a0d06107e fix(api): use stored app password for chunk-context and pdf-preview
The /api/v1/chunk-context and /api/v1/pdf-preview handlers in
api/visualization.py forwarded the incoming OAuth bearer directly to
Nextcloud via NextcloudClient.from_token. In multi-user BasicAuth mode
Nextcloud has no validator for those bearers on Notes/WebDAV, so it
treats the request as anonymous and returns 401 — surfaced to the user
as a 500 from /apps/astrolabe/api/chunk-context. Search worked because
it only hits Qdrant.

Architecturally, OAuth is only for Astrolabe→MCP server; MCP server→
Nextcloud always uses the per-user app password stored during provision
(background sync already does this via vector.oauth_sync).

- Resolve the Nextcloud client through get_user_client_basic_auth in
  both get_chunk_context and get_pdf_preview, surfacing
  NotProvisionedError as a clean 401 instead of opaque 500.
- Apply the same fix to the session-cookie variant in
  auth/viz_routes.chunk_context_endpoint for the internal viz UI.

Tests:
- New unit file test_management_chunk_context_endpoint.py, including a
  regression guard that asserts get_user_client_basic_auth is awaited
  (so reverting to from_token fails without needing a live Nextcloud).
- Updated test_management_pdf_preview_endpoint.py to mock the new auth
  path (drops extract_bearer_token / NextcloudClient.from_token patches).
- New integration test test_astrolabe_chunk_context.py drives the full
  chain (browser → Astrolabe → MCP → Nextcloud) in multi-user BasicAuth
  mode, plus bare-bones 401 checks on the MCP endpoint.

Full unit suite: 546 passed.

Companion PR on astrolabe (cbcoutinho/astrolabe#66) sends the Nextcloud
UID as loginName in the app-password POST body so the stored record is
complete. Submodule bump to that branch will follow once CI reproduces
the failure on the old submodule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 19:50:13 +02:00
Chris CoutinhoandClaude Opus 4.6 ea7e86f7f4 chore: update astrolabe submodule to v0.13.9
Picks up astrolabe/astrolabe#61 which fixes app password provisioning
failure caused by loginName mismatch in ITokenProvider::generateToken().
This was the root cause of vector sync never indexing in multi-user
BasicAuth mode, which caused the plotly visualization test to fail.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 00:25:47 +02:00
Chris CoutinhoandClaude Opus 4.6 3935f45be8 fix(tests): convert create_mcp_client_session to asynccontextmanager
The multi-user-basic integration job was consistently failing with
`CancelledError: Cancelled via cancel scope ... by <async_generator_athrow>`
followed by a cascade of `anyio.ClosedResourceError` in every subsequent
test. Root cause: `create_mcp_client_session` was declared as an async
generator driven by `async for session in ...:`, so Python's generator
finalizer (`aclose`) ran under pytest-asyncio's cleanup task instead of
the task that owned the nested `streamablehttp_client` cancel scope.
anyio then raised when the inner task group saw its scope being exited
from a foreign task, leaving the memory object streams half-closed and
poisoning the rest of the session.

Switching to `@asynccontextmanager` + `async with ... as session:` makes
`__aenter__`/`__aexit__` run in the frame that owns the context manager,
satisfying anyio's structured concurrency requirements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 12:58:47 +02:00
Chris CoutinhoandClaude Opus 4.6 512de1f6b0 test: address PR #707 reviewer feedback on config path helpers
- _resolve_settings_files() now raises FileNotFoundError when
  NEXTCLOUD_MCP_SETTINGS_FILE points to a missing file, instead of
  silently falling back to defaults (footgun on typos).
- .secrets.toml is now looked for alongside the explicit settings file
  when NEXTCLOUD_MCP_SETTINGS_FILE is set, matching user expectation for
  /etc-style deployments. Unset behaviour (cwd lookup) is unchanged.
- get_token_db_path() drops the redundant os.environ.get() short-circuit;
  TOKEN_STORAGE_DB is already bound through dynaconf because the key is
  declared in _DEFAULTS.
- is_ephemeral_token_db() docstring documents the "must call
  get_token_db_path() first" precondition.
- alembic.ini comment clarifies the ./tokens.db placeholder is cwd-relative
  by design and points readers at the -x database_url escape hatch.
- New tests/unit/test_config_paths.py (12 tests) covering the ephemeral
  tempfile lifecycle, the TOKEN_STORAGE_DB override path, and all six
  _resolve_settings_files() cases including the two new behaviours.

Full unit suite now at 476 passed (464 + 12 new). Ruff + ty clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 21:14:42 +02:00
Chris CoutinhoandClaude Opus 4.6 146b622ebf fix: enable uvx/PyPI deployments without Docker assumptions
Two bugs made `uvx --from . nextcloud-mcp-server run` (and any pip install)
unusable outside Docker:

1. Dynaconf was configured with ignore_unknown_envvars=True and relied on
   settings.toml to declare the key schema. With no settings.toml in a wheel
   install, every env var (NEXTCLOUD_HOST, MCP_DEPLOYMENT_MODE, ...) was
   silently dropped. Moved the schema into a Python _DEFAULTS dict passed
   directly to Dynaconf, kept settings.toml as an optional external override
   (renamed to settings.toml.example, gitignored), and pointed docker-compose
   at the example file.

2. Token SQLite DB defaulted to /app/data/tokens.db in multiple places
   (auth/storage.py, migrations.py, alembic/env.py, cli.py db subcommands),
   which blew up at uvicorn startup with FileNotFoundError on non-Docker
   hosts. Replaced with a new config.get_token_db_path() helper that
   resolves TOKEN_STORAGE_DB if explicitly set, otherwise allocates a
   per-process tempfile cleaned up at interpreter exit via atexit — mirroring
   the "ephemeral by default" pattern used for QDRANT_LOCATION=:memory:.

Containers are unaffected: docker-compose services now explicitly set
TOKEN_STORAGE_DB=/app/data/tokens.db (the fourth service that was missing
this pin has been brought in line with the other three).

Verified end-to-end in an isolated /tmp venv: env-var-only startup, Alembic
migrations run against the tempfile, Application startup complete, /health/live
returns 200, tempfile deleted on SIGTERM. Unit tests (464) + ruff + ty pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:11:23 +02:00
Chris Coutinho fd1846de03 ci: Bump astrolabe 2026-04-11 20:16:32 +02:00
Chris CoutinhoandClaude Opus 4.6 2c0b764aae fix: strip resource server prefix from JWT scopes for tool filtering
External IdPs like AWS Cognito return scopes prefixed with the resource
server identifier (e.g. https://mcp.example.com/notes.read). MCP tools
use bare scope names (notes.read) in @require_scopes decorators. Without
stripping the prefix, scope matching fails and only identity-only tools
(openid/profile/email) are visible — resulting in 4/125 tools shown.

Strip the OIDC_RESOURCE_SERVER_ID prefix in both get_access_token_scopes()
(used by list_tools filtering) and the require_scopes decorator (used at
tool execution time).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 01:27:06 +02:00
Chris CoutinhoandClaude Opus 4.6 33d679e174 feat: add --version option to CLI
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 00:24:50 +02:00
Chris CoutinhoandClaude Opus 4.6 f340380898 fix: address third round of review feedback
Add BasicAuthLifespanContext Protocol to make the contract between
StdioContext and get_client() explicit and type-safe. Document why
mcp.get_context() is required for non-template resources. Add News
and Collectives to README Supported Apps table, fix transport default.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 23:59:39 +02:00
Chris CoutinhoandClaude Opus 4.6 7730f926cb fix: conditionally include offline_access based on IdP discovery
AWS Cognito provides refresh tokens automatically with the authorization
code flow but does not list offline_access as a supported scope. Check
the IdP's scopes_supported discovery field before including it in
requests, and always accept refresh tokens from responses regardless.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 23:48:49 +02:00
Chris CoutinhoandClaude Opus 4.6 1af85bc05e fix: address second round of review feedback
- Remove dead monkeypatch in test_stdio_calls_get_stdio_mcp
- Add _reload_config() teardown to single_user_env fixture
- Tighten AVAILABLE_APPS type to Callable[[FastMCP], None]

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 23:26:46 +02:00
Chris CoutinhoandClaude Opus 4.6 e9c46a04a0 fix: address PR review feedback and fix CI test failures
- Revert default transport to streamable-http (not a breaking change)
- Extract AVAILABLE_APPS constant to server/__init__.py (DRY)
- Wrap get_stdio_mcp ValueError in click.ClickException for clean errors
- Fix test_stdio.py: call _reload_config() so dynaconf sees env changes
- Use lazy %-style logging in stdio.py
- Add private API comments in test assertions
- Derive --enable-app CLI choices from AVAILABLE_APPS
- README: show explicit --transport stdio in uvx examples

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 23:03:12 +02:00
Chris CoutinhoandClaude Opus 4.6 09006fcea9 feat: add stdio transport support for local MCP usage
Add a lightweight stdio transport path so users can run the server
locally with MCP clients like Claude Code using `uvx nextcloud-mcp-server run`.

- New `nextcloud_mcp_server/stdio.py` with minimal FastMCP setup for
  single-user BasicAuth (no OAuth, semantic search, or background sync)
- Default transport changed from streamable-http to stdio
- Dockerfile updated to explicitly use streamable-http for containers
- CLI `--enable-app` now includes news, collectives, and sharing
- README Quick Start section with uvx and MCP client config examples

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 22:48:14 +02:00
Chris CoutinhoandClaude Opus 4.6 1e380caade ci: remove PAT from release workflows, use workflow_call instead
Tags pushed with GITHUB_TOKEN don't trigger other workflows (GitHub's
anti-recursion protection), which is why a PAT was needed. Instead,
chain release and docker workflows directly via workflow_call from
bump-version, eliminating the need for a personal access token.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 22:10:25 +02:00
Chris CoutinhoandClaude Opus 4.6 f8fb34d113 fix: conditionally include offline_access in Flow 2 scope request
Flow 2 hardcoded offline_access in the scope string, but providers
like AWS Cognito don't support this scope (they handle refresh tokens
via client config). This caused invalid_scope errors on the Astrolabe
semantic search enablement flow.

Only include offline_access when enable_offline_access is explicitly
set, matching the behavior of DCR scope registration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 20:31:55 +02:00
Chris CoutinhoandClaude Opus 4.6 c3da7acc87 fix: fall back to client_id when aud claim is absent (Cognito compat)
AWS Cognito access tokens do not include an `aud` claim per RFC 7519 —
they use `client_id` instead. This causes `_has_mcp_audience` to reject
all Cognito-issued tokens with "Missing MCP audience. Got []".

When `aud` is empty, fall back to the `client_id` JWT claim for audience
validation. The MCP server's own client_id will be present there since
the AS proxy exchanges the authorization code using its credentials.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 18:06:15 +02:00
Chris CoutinhoandClaude Opus 4.6 cc6ba65993 fix: address second round of PR review for scope prefix
- Use dynaconf (get_settings()) instead of os.getenv for OIDC_RESOURCE_SERVER_ID
- Re-add Settings field, _field_map entry, and settings.toml default
- Add trailing-slash guard (.rstrip("/")) to prevent double-slash in scopes
- Add double-prefixing guard: skip scopes already carrying the prefix
- Add @pytest.mark.unit to test module
- Add test for already-prefixed scopes
- Document OIDC_RESOURCE_SERVER_ID in docs/configuration.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 16:58:29 +02:00
Chris CoutinhoandClaude Opus 4.6 f67d4d1116 fix: address PR review for OIDC scope prefix feature
Add offline_access to OIDC standard scopes exclusion list to prevent it
from being incorrectly prefixed, which would break Cognito refresh token
flows. Extract scope transformation into testable _transform_scopes_for_idp()
helper, add debug logging for prefixed scopes, remove unused Settings field
(oauth_routes.py consistently uses os.getenv), and add unit tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 16:44:08 +02:00
Chris CoutinhoandClaude Opus 4.6 e21ddd91b9 feat: add OIDC resource server scope prefix for Cognito compatibility
When OIDC_RESOURCE_SERVER_ID is set, prefix resource scopes with the
identifier when forwarding to the IdP (e.g., calendar.read becomes
https://example.com/calendar.read). Required for IdPs like AWS Cognito
that mandate {resource_server_id}/{scope} format for custom scopes.
OIDC standard scopes (openid, profile, email) are forwarded as-is.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 16:33:47 +02:00
Chris Coutinho f9f637b1ce Merge remote-tracking branch 'origin/master' into chore/remove-helm-chart 2026-04-07 16:32:53 +02:00
Chris CoutinhoandClaude Opus 4.6 c4b74e7e20 chore: remove helm chart (migrated to cbcoutinho/helm-charts)
The helm chart has been migrated to a dedicated repository at
https://github.com/cbcoutinho/helm-charts. This removes the chart
source, release workflow, bump script, and updates all documentation
to point to the new repository.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 16:31:35 +02:00
Chris CoutinhoandClaude Opus 4.6 76b1fc4447 docs: address PR review feedback on ADR-025 dynaconf configuration management
Incorporate reviewer feedback across three review rounds:

- Remove post_hooks from Phase 1 constructor; defer to Phase 4
- Fix Validator syntax: use condition=lambda instead of ne= kwarg
- Add MCP_DEPLOYMENT_MODE validator to catch typos at startup
- Add CRITICAL to LOG_LEVEL validator enum
- Make OTEL_TRACES_SAMPLER_ARG validation conditional on ratio samplers
- Add all missing provider env vars to settings.toml (Bedrock, Anthropic, Ollama, Simple)
- Add provider secrets to .secrets.toml.example
- Fix DynaconfDict import to stable public API path
- Strengthen ignore_unknown_envvars risk: CI lint check mandatory before Phase 2
- Document ValidationError vs ValueError breaking change in Phase 3
- Acknowledge environments=True legacy risk with mitigation
- Address root_path pip-install concern (intentional: pip uses env vars)
- Add enable_token_exchange to adapter example; note exhaustive field mapping
- Clarify Provider Registry is Phase 6 with explanation of os.getenv coexistence
- Improve test isolation fixture with teardown reload + _dynaconf visibility note
- Add Docker Compose volume mount host-file existence note

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 15:48:50 +02:00
Chris Coutinho 5b093e49b1 Merge remote-tracking branch 'origin/master' into docs/adr-024-dynaconf-config-management 2026-04-07 14:17:57 +02:00
Chris CoutinhoandClaude Opus 4.6 b8b1616897 fix: resolve dynaconf settings.toml not found in non-editable installs
The root_path for dynaconf resolved to site-packages instead of the
application root when installed non-editable (Docker). This caused all
settings without env var overrides to be None, crashing on startup with
a TypeError in chunk size validation.

Fix root_path to fall back to CWD when settings.toml isn't at the
source-tree path, and refactor get_settings() to only pass values
dynaconf actually has — letting Settings dataclass defaults apply for
unconfigured keys. Mount settings.toml into all docker-compose MCP
services as a read-only volume.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 14:17:34 +02:00
Chris CoutinhoandClaude Opus 4.6 29fd0486c9 refactor: change OAuth scope separator from colon to dot for IDP compatibility
Many identity providers (AWS Cognito, Okta, Azure AD) reject or mishandle
colons in OAuth scope names. This migrates all custom scopes from
`resource:action` to `resource.action` format (e.g., `notes:read` →
`notes.read`), which is universally accepted and aligns with industry
conventions (Microsoft, Google).

Includes Alembic migration 004 for stored scope strings and ADR-024
documenting the rationale and RFC references.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 10:07:02 +02:00
Chris CoutinhoandClaude Opus 4.6 c8e4cbe825 feat: implement dynaconf configuration management (ADR-024 phases 1-3)
Replace ~80 manual os.getenv() calls in config.py with dynaconf-backed
configuration, enabling TOML file-based config alongside existing env
var support. Zero breaking changes — Settings dataclass interface and
all consumers unchanged.

Phase 1: Create settings.toml with all config keys and defaults,
.secrets.toml.example template, update .gitignore, initialize Dynaconf
instance with envvar_prefix=False and environment section switching.

Phase 2: Wire adapter — replace os.getenv() with _dynaconf.get() in
get_settings(), get_document_processor_config(), and deprecation/
dependency resolution helpers. Automatic type coercion eliminates ~30
manual int()/float()/.lower()=="true" patterns.

Phase 3: Add 12 declarative validators for port ranges, positive
integers, enum constraints, and float ranges. Remove redundant negative
overlap check from Settings.__post_init__.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:22:19 +02:00
Chris Coutinho f34c74afbc Merge remote-tracking branch 'origin/master' into docs/adr-024-dynaconf-config-management 2026-04-05 19:42:26 +02:00
Chris CoutinhoandClaude Opus 4.6 b07b713146 fix: address PR review feedback for client registry and DCR proxy
- Document wildcard scope policy in ClientRegistry class docstring
- Add hostname None guard and IPv6 loopback (::1) to redirect URI validation
- Simplify redirect URI scheme validation into single guard clause
- Add try/finally cleanup to DCR client deletion test
- Validate 302 Location header in unknown client rejection test
- Add unit tests for IPv6 loopback, malformed URIs, and DCR proxy paths

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 19:29:23 +02:00
Chris Coutinho 8e381a943c build: Update lockfile 2026-04-05 19:24:58 +02:00
Chris CoutinhoandClaude Opus 4.6 e28aa6eb3e docs: address review feedback on ADR-024 dynaconf configuration management
Address all 9 review points from PR #680:
- Fix post_hooks code examples to use correct return-dict signature
- Expand test isolation section with fixture factory, DynaconfDict, and
  reload patterns
- Document ignore_unknown_envvars silent failure mode in Negative
  Consequences and add env var audit to Phase 1 checklist
- Fix NEXTCLOUD_HOST validator to be unconditional (required in all modes)
- Document environments=True edge cases (unset mode, ENV_FOR_DYNACONF
  shadowing)
- Add upper bound to dynaconf version pin (>=3.2.13,<4.0)
- Tighten Pydantic Settings comparison to acknowledge 2.x TOML support
- Make .gitignore additions explicit in Phase 1 checklist
- Clarify that shell-level .env loading still works with load_dotenv=False

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 18:34:11 +02:00
Chris CoutinhoandClaude Opus 4.6 7d775d2a52 refactor: remove ALLOWED_MCP_CLOUD_CLIENTS and add keycloak CI profile
Remove the unused ALLOWED_MCP_CLOUD_CLIENTS env var — all clients are
defined via ALLOWED_MCP_CLIENTS or the static well-known defaults.
Add keycloak as an integration test profile in CI now that login-flow
replaces the old bearer token approach for external IdPs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 15:06:56 +02:00
Chris CoutinhoandClaude Opus 4.6 5d0e4addd0 build: add dynaconf dependency for ADR-024 configuration management
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 15:00:17 +02:00
Chris CoutinhoandClaude Opus 4.6 3c6f67887f docs: address review feedback on ADR-024 dynaconf configuration management
Fix incorrect hook syntax (@hookable.post → Dynaconf(post_hooks=[...])),
broken Qdrant mutual exclusivity validator, missing root_path for settings
file resolution, and empty string defaults that bypass validators. Add test
isolation section, mark Phase 4 as optional/future with risk note, and
correct Pydantic comparison (already a project dependency).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:59:42 +02:00
Chris CoutinhoandClaude Opus 4.6 91e7665f41 refactor: consolidate ALLOWED_MCP_CLIENTS and add redirect URI validation
Merge ALLOWED_MCP_CLOUD_CLIENTS into a single ALLOWED_MCP_CLIENTS env var
that supports both simple client IDs and pipe-separated client_id|redirect_uri
entries. Enforce HTTPS for non-localhost redirect URIs, warn on malformed
entries, and use wildcard scopes for all static clients (upstream IdP enforces
actual scopes). Add deprecation warning for the old env var.

Also fixes DCR proxy error messages to reference only ALLOWED_MCP_CLIENTS and
use "Upstream" instead of "Nextcloud" for IdP-agnostic language. Enables
Login Flow v2 + DCR on the mcp-keycloak docker-compose service.

Adds 17 unit tests for ClientRegistry parsing/validation and 7 keycloak
integration tests for DCR lifecycle, AS metadata, and client authorization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:33:29 +02:00
Chris CoutinhoandClaude Opus 4.6 e272a938df docs: add ADR-024 for dynaconf configuration management
Propose migrating from manual os.getenv() calls to dynaconf for
file-based configuration. Key decisions: envvar_prefix=False for
backward compatibility, MCP_DEPLOYMENT_MODE as environment switcher,
TOML settings files with secret separation, and incremental migration
via adapter pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 13:43:23 +02:00
Chris CoutinhoandClaude Opus 4.6 2a34015443 fix: support cloud OAuth clients and graceful DCR fallback
Claude AI (web) sends a Cognito-issued client_id with an HTTPS redirect
URI, but the client registry only supported localhost redirect URIs via
ALLOWED_MCP_CLIENTS. Add ALLOWED_MCP_CLOUD_CLIENTS env var for web-based
clients with format "client_id|redirect_uri".

Also fix the DCR proxy to return a clear error when the upstream IdP
(e.g. Cognito) doesn't support dynamic client registration, instead of
silently falling back to a Nextcloud-specific endpoint that fails.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 22:17:01 +02:00
Chris CoutinhoandClaude Opus 4.6 5730313574 refactor: remove RFC 8693 token exchange and Keycloak OAuth implementation
Nextcloud doesn't support OAuth bearer tokens without upstream patches,
making the RFC 8693 token exchange path untestable and dead code.

Removed:
- nextcloud_mcp_server/auth/token_exchange.py (597 lines)
- nextcloud_mcp_server/auth/keycloak_oauth.py (586 lines)
- OAUTH_TOKEN_EXCHANGE deployment mode from AuthMode enum
- get_session_client_from_context() from context_helper.py
- get_session_token() from token_broker.py
- enable_token_exchange / token_exchange_cache_ttl config fields
- oauth_token_exchange_total Prometheus metric
- Keycloak fixture block from tests/conftest.py (~408 lines)
- Token exchange unit tests from test_config_validators.py,
  test_unified_verifier.py, test_management_status_endpoint.py

Preserved:
- Multi-audience OAuth mode (OAUTH_SINGLE_AUDIENCE)
- Login Flow v2 provisioning with elicitation support
- Token broker background token management
- All existing test coverage for non-exchange paths

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:22:20 +02:00
Chris CoutinhoandClaude Opus 4.6 c6316dbb91 fix: address PR review — remove token exchange tests, improve logging
- Remove all RFC 8693 token exchange tests (integration, manual, keycloak)
  since Nextcloud doesn't support bearer tokens without upstream patches
- Remove manual impersonation/ADR-004 scripts and their docs
- Clean up token_exchange singleton from integration conftest
- Improve logging in _complete_login_flow_v2_as_user with step-by-step
  [username] prefixed messages matching _complete_login_flow_v2 style
- Remove unnecessary time staggering from all_login_flow_user_tokens;
  concurrent token acquisition works without artificial delays

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 17:43:44 +02:00
Chris CoutinhoandClaude Opus 4.6 6278b6eb75 test: add multi-user permission tests for login-flow deployment
The OAuth profile removal dropped cross-user permission tests (deck, files,
notes) that validated Nextcloud sharing/ACL enforcement through MCP tools.
These tested general functionality, not OAuth-specific behavior.

Restores coverage with login-flow fixtures and 9 tests covering file share
read/write enforcement, folder sharing, Deck board ACL view/edit, and
per-user resource isolation for files, boards, and notes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 17:02:05 +02:00
Chris CoutinhoandClaude Opus 4.6 b4c3b48e61 fix: address PR review — stale mcp-oauth refs, Playwright TimeoutError catch
- Replace 4 stale mcp-oauth references in CLAUDE.md with mcp-login-flow
- Import and catch playwright.async_api.TimeoutError in consent retry loop
  (Playwright's TimeoutError doesn't inherit from Python's built-in)
- Replace unreachable `return True` with explicit RuntimeError raise
- Add clarifying comment for hardcoded login-flow port default

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 16:32:17 +02:00
Chris CoutinhoandClaude Opus 4.6 86f350ab49 fix: update expected auth tools list for login-flow scope test
The login-flow MCP server exposes 3 additional auth tools
(nc_auth_provision_access, nc_auth_check_status, nc_auth_update_scopes)
from ADR-022 that require only 'openid' scope. Update the
no-custom-scopes test to expect 7 auth tools instead of 4.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 16:25:43 +02:00
Chris CoutinhoandClaude Opus 4.6 aeddc28ca6 refactor: remove oauth profile, migrate MCP/OAuth tests to login-flow
Remove the oauth Docker Compose profile (mcp-oauth service, port 8001)
which used OAuth bearer tokens for direct NC API access, requiring
upstream OIDC patches. All NC access should use app passwords via
Login Flow v2 or BasicAuth.

Changes:
- Remove mcp-oauth service from docker-compose.yml
- Remove oauth mode from CI test matrix
- Delete oauth pass-through tests (core, permissions, token exchange)
- Delete oauth-specific tests (elicitation, NC PHP app, astrolabe)
- Migrate MCP/OAuth integration tests to login-flow profile:
  - DCR lifecycle, deletion, token type tests
  - Scope authorization (tool filtering) tests
  - Token introspection tests
- Fix flaky consent screen automation: replace JS btn.click() with
  Playwright native click + retry (handles Vue.js event binding race)
- Add scope-filtered OAuth client fixtures to login-flow conftest
- Keep keycloak profile for external IdP testing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 16:05:14 +02:00
Chris CoutinhoandClaude Opus 4.6 270ef82527 docs: optimize CLAUDE.md for db scripts, uv run, and deployment modes
- Discourage raw docker exec for database queries; use scripts/dbquery.py
  and scripts/sqlitequery.py exclusively
- Ensure all python commands use uv run prefix (mcp run, pytest, etc.)
- Replace Progressive Consent section with concise Deployment Modes
  overview (single-user, multi-user BasicAuth, Login Flow v2)
- Normalize docker-compose to docker compose throughout

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 15:52:51 +02:00
Chris CoutinhoandClaude Opus 4.6 da380a38c6 fix: convert BDAY datetime.date to string before Pydantic validation
pythonvCard4 parses vCard BDAY fields into datetime.date objects, but
the Contact model expects Optional[str]. This caused a validation error
that crashed the entire contact list. Convert at the client layer
(consistent with the calendar client pattern) with a defensive check
at the server mapping layer.

Closes #672

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 09:11:45 +02:00
Chris Coutinho 388ad404b4 chore: Update mariadb pin 2026-03-31 17:22:30 +02:00
Chris Coutinho cf09a71217 chore: Update image tags 2026-03-31 17:18:37 +02:00
Chris CoutinhoandClaude Opus 4.6 d06b862d24 fix: require bearer token on provision endpoints (open redirect mitigation)
Both /app/provision and /app/provision/status now require a valid
Nextcloud OIDC bearer token via the Authorization header, reusing the
existing validate_token_and_get_user pattern from the management API.

This eliminates the open redirect vulnerability (only authenticated
Astrolabe users can trigger the flow) and prevents unauthenticated
resource exhaustion via Login Flow v2 session creation.

The authenticated user_id from the token replaces the untrusted
user_id query parameter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 17:09:37 +02:00
Chris CoutinhoandClaude Opus 4.6 775bee83e3 fix: address PR review round 3 — info disclosure, conditional routes, cleanup
- Replace exception details in user-facing error page with generic message
- Only register /app/provision routes when enable_login_flow is true
- Piggyback expired provision session cleanup on hourly cleanup loop
- Add multi-process limitation comment on in-memory session store
- Add comment explaining login_url vs poll_endpoint rewrite asymmetry
- Document curl dependency in Dockerfile (healthcheck probes)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 11:37:42 +02:00
Chris CoutinhoandClaude Opus 4.6 2508f36ebf fix: address PR review round 2 — expiry checks, race guards, poll tests
- Log warning if /app mount not found when sharing poll task group
- Add docstring explaining unconditional task group creation
- Check session expires_at in provision_status to catch stale sessions
- Guard _poll_and_store status writes against cleanup-while-polling race
- Use "error" status (not "expired") when app_password is missing
- Remove hardcoded "Astrolabe Background Sync" user_agent string
- Fix async mock pattern (new_callable=AsyncMock) in test
- Add autouse fixture to clear _provision_sessions between tests
- Add _poll_and_store unit tests: completed, expired, error, cleanup
- Document all status values in provision_status docstring

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 09:29:14 +02:00
Chris CoutinhoandClaude Opus 4.6 777a09c806 fix: address PR review — XSS escape, asyncio→anyio, URL rewrite dedup
- Escape HTML in _render_error to prevent XSS from exception messages
- Replace asyncio.create_task/sleep with anyio task group and sleep,
  tying poll task lifetime to the app lifespan for proper cleanup
- Extract rewrite_url_origin() utility to fix duplicated URL rewriting
  logic and replace urlparse._replace with stable urlunparse API
- Add warning log for insecure HTTP redirect URIs
- Add unit tests for validation, XSS escaping, route handlers, and
  URL rewriting (16 new tests in test_provision_routes.py)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 08:51:41 +02:00
Chris CoutinhoandClaude Opus 4.6 c21776948d fix: use app password auth for background sync in Login Flow mode
Login Flow v2 is a deployment-wide mode where all users authenticate
with app passwords (not OAuth refresh tokens). Set use_basic_auth=True
when enable_login_flow is true so the background sync user manager
queries the app_passwords table and scanners use app password
authentication for Nextcloud API calls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 00:55:31 +02:00
Chris CoutinhoandClaude Opus 4.6 474cfe5e98 fix: discover Login Flow v2 users in OAuth mode user manager
When enable_login_flow is true, also check the app_passwords table
for provisioned users. Previously, OAuth mode only queried the
refresh_tokens table, missing users who were provisioned via
Login Flow v2 (which stores app passwords, not refresh tokens).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 00:48:02 +02:00
Chris CoutinhoandClaude Opus 4.6 eefede8c47 fix: rewrite Login Flow v2 poll endpoint URL to use configured host
Nextcloud returns poll/login URLs using its internal hostname (e.g.
http://localhost/login/v2/poll) which is unreachable from the MCP
server container in Docker networks. Rewrite the poll endpoint's
origin to use the configured NEXTCLOUD_HOST so server-side polling
works correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 23:21:41 +02:00
Chris CoutinhoandClaude Opus 4.6 c2f23a566c fix: handle internal hostname without port in Login Flow v2 URL rewriting
Nextcloud may omit default ports in the login_url (e.g. http://app
instead of http://app:80). Extract just scheme+hostname from
NEXTCLOUD_HOST for the URL replacement check.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 23:11:42 +02:00
Chris CoutinhoandClaude Opus 4.6 1a51f5bbf5 refactor: use redirect-based Login Flow v2 provision instead of popup
Replace the popup-based approach with a direct redirect to Nextcloud's
login page. This is more compatible with Playwright E2E tests and
simpler for users. The background polling task still runs server-side
to store the app password when the user grants access.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 22:17:54 +02:00
Chris CoutinhoandClaude Opus 4.6 081ecbe401 feat: add web-based Login Flow v2 provisioning endpoint
Add /app/provision and /app/provision/status endpoints for browser-based
Login Flow v2 app password provisioning. Used by Astrolabe's "Enable
Semantic Search" to chain OAuth (bearer token) + Login Flow v2 (app
password) in a single user interaction.

The provision page initiates Login Flow v2, opens Nextcloud's login URL
in a popup, polls for completion via background task, and redirects back
to the caller's redirect_uri on success.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 21:56:16 +02:00
Chris CoutinhoandClaude Opus 4.6 b8dc1d7f52 feat: add Tailscale Funnel config for Claude AI connector testing
Add docker compose services (tailscale-mcp + nginx-claude-filter) behind
a claude-funnel profile that expose the login-flow MCP server via
Tailscale Funnel with IP-based access control:

- /mcp endpoint restricted to Claude AI outbound IPs (160.79.104.0/21)
- /oauth/*, /.well-known/*, /app paths open to all IPs (user login flow)
- All other paths return 404

Also add favicon.png served at /favicon.ico for connector directory
discovery (Google favicon service).

Usage:
  docker compose --profile login-flow --profile claude-funnel up -d

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 21:00:00 +02:00
Chris CoutinhoandClaude Opus 4.6 25788eecc7 fix: allow HTTPS redirect URIs for non-localhost OAuth clients
Relax redirect_uri validation to accept HTTPS for remote hosts (e.g.,
cloud-hosted MCP clients like Claude AI) while keeping HTTP allowed
for localhost per RFC 8252 loopback exception.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:03:25 +02:00
Chris CoutinhoandClaude Opus 4.6 f151eb10b3 fix: move Astrolabe OAuth hook to before-starting for reliable OIDC client creation
Move 26-configure-astrolabe-oauth.sh from post-installation (runs once
on first boot) to before-starting (runs on every start). This ensures
the Astrolabe OIDC client is created as soon as MCP_SERVER_URL is
available, even if it wasn't set during initial installation.

Also copy 25-configure-mcp-server-url.sh to before-starting so the
mcp_server_url config stays current across container recreations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:39:49 +02:00
Chris CoutinhoandClaude Opus 4.6 fe8799a133 fix: resolve OAuth compatibility issues for login-flow deployment
- Drop OIDC fork: comment out third_party/oidc mount, use upstream
  v1.16.3 from app store (fixes consent redirect race, PR #631)
- Support client_secret_basic auth: add _extract_basic_auth() helper
  so TS MCP SDK can authenticate at token endpoint (RFC 6749 §2.3.1)
- Multi-issuer JWT validation: accept tokens with internal Docker
  issuer (http://app:80) or public URL (NEXTCLOUD_PUBLIC_ISSUER_URL)
  since AS proxy obtains tokens server-to-server
- Introspection fallback: try token introspection when JWT verification
  fails, supporting both JWT and opaque token types
- Register all tool scopes in DCR: add semantic:read, collectives:read,
  collectives:write to OIDC client allowed_scopes so tokens include
  them and semantic search tools are visible to authenticated clients
- Auto-create Astrolabe OAuth client: new app-hook creates OIDC client
  and stores credentials in config.php so the "Authorize via OAuth"
  button works without manual setup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 15:05:26 +02:00
Chris CoutinhoandClaude Opus 4.6 1482d2d43d fix: pin Renovate Nextcloud updates to matching major version
The custom regex manager matched all nextcloud_image entries with the
same depName, causing Renovate to bump all matrix entries (NC 31, 32, 33)
to the latest version instead of only the targeted major.

Fix by capturing nextcloud_version to create version-specific dep names
(nextcloud-31, nextcloud-32, nextcloud-33) with allowedVersions rules
constraining each to its own major. Also pins docker-compose.yml to 32.x
and removes redundant inline # renovate: comments that could cause
duplicate matching.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 10:48:55 +01:00
Chris CoutinhoandClaude Opus 4.6 52470ea713 fix: address PR review feedback (round 9)
- Fix emoji clearing bug: use _UNSET sentinel in update_collective so
  emoji=None sends {"emoji": null} instead of raising ValueError
- Move collectives_get_trashed_collectives to Read Tools section
- Remove redundant is_trash field from ListTrashedPagesResponse
- Add page lifecycle note to collectives_trash_page docstring
- Add unit test for clearing collective emoji via update_collective
- Add integration test for clearing collective emoji via MCP tool

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 09:19:53 +01:00
Chris CoutinhoandClaude Opus 4.6 cb16060b1b fix: address PR review feedback (round 8)
- Fix inconsistent error code in set_collective_emoji (400 → -32603)
- Allow clearing emoji via set_collective_emoji(emoji=None)
- Remove destructiveHint from trash operations (soft deletes are recoverable)
- Change delete_collective to idempotentHint=False (requires trash precondition)
- Add restore_collective and get_trashed_collectives tools
- Add unit tests for ValueError guard, clear-emoji path, and new tools
- Add integration test for full trash/restore/delete lifecycle
- Verify move_page returns new title in response message

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:26:26 +01:00
Chris CoutinhoandClaude Opus 4.6 7224a2ebe3 fix: address PR review feedback (round 7) and fix CI
- Rename collectives_update_collective to collectives_set_collective_emoji
  (more precise since only emoji is settable)
- Use standard JSON-RPC error code -32603 (INTERNAL_ERROR) instead of -1
- Handle UnicodeDecodeError when reading page content via WebDAV
- Replace brittle 'Welcome' content assertion with length check

Fixes CI: test_update_operations_not_idempotent no longer matches the
renamed tool, which is correctly idempotent (no ETag involved).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 23:18:42 +01:00
Chris CoutinhoandClaude Opus 4.6 b6d01fc0bd fix: pin starlette<1.0 to prevent startup crash (#648)
Starlette 1.0.0 removed the @app.middleware() decorator, which breaks
nextcloud-mcp-server on fresh installs. Pin starlette<1.0 until we
address the full set of breaking changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 19:30:57 +01:00
Chris CoutinhoandClaude Opus 4.6 85119bde91 fix: address PR review feedback (round 6)
- Fix assign_tag sending Content-Type header with no body
- Mark collectives_update_collective as idempotent (no ETag involved)
- Raise OCSError when 'data' key missing instead of silent fallback
- Tighten color validator to 3 or 6 hex chars only
- Add comment explaining null emoji semantics in set_page_emoji

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 14:38:11 +01:00
Chris CoutinhoandClaude Opus 4.6 aa46c6147b fix: address PR review feedback (round 5)
- Validate OCS envelope in trash_collective, delete_collective, trash_page
- Guard _unwrap_ocs against non-OCS responses with informative OCSError
- Remove _get_ocs_headers() indirection, use class constants directly
- Split headers: _OCS_HEADERS (GET) vs _OCS_HEADERS_JSON (with body)
- Fix docstring claiming emoji param is required when it is optional
- Rename misleading test, add test for non-OCS envelope handling

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 13:49:28 +01:00
Chris CoutinhoandClaude Opus 4.6 95edd9ba8e fix: add trash/delete collective tools and address review feedback (round 4)
Add collectives_trash_collective and collectives_delete_collective MCP
tools with proper destructiveHint annotations. Refactor integration test
fixture to use MCP tools for cleanup instead of direct httpx/OCS calls.
Optimize _get_ocs_headers() to class-level constant.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:36:40 +01:00
Chris CoutinhoandClaude Opus 4.6 f3caad122d fix: address PR review feedback (round 3)
Bugs:
- assign_tag/remove_tag now call _unwrap_ocs to surface OCS-level errors
- trash_page changed to idempotentHint=False (trashing twice errors)
- WebDAV path parts stripped of slashes to prevent double-slash paths

Robustness:
- _unwrap_ocs uses ocs.get("data", {}) instead of ocs["data"]
- Unit test added for missing data key in OCS envelope

Minor:
- MCP error codes use -1 (project convention) instead of HTTP status codes
- update_collective docstring notes that emoji is required
- CollectiveTag.color validated as hex format via field_validator

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 13:25:09 +01:00
Chris CoutinhoandClaude Opus 4.6 44a27bd9e9 fix: address PR review feedback (round 2)
Bug fixes:
- Catch OCSError/HTTPStatusError in all server tools, convert to McpError
- Guard update_collective against empty body (raise ValueError)
- Use restore_page response data in status message

ADR-017 annotation fix:
- Distinguish "remove" (reversible association) from "delete" (permanent):
  remove_tag and deck_remove_label_from_card no longer set destructiveHint
- Update annotation test to exclude "remove" from destructive keywords

Data model improvements:
- Add trashTimestamp field to PageInfo
- Create ListTrashedPagesResponse with is_trash context flag
- Add collective_id to ListTagsResponse

Test robustness:
- Read NC credentials from environment variables (not hardcoded)
- Filter landing page by parentId == 0 instead of assuming pages[0]

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 09:08:13 +01:00
Chris CoutinhoandClaude Opus 4.6 3393cd9756 fix: correct tool annotations to match ADR-017 conventions
- Add destructiveHint=True to collectives_remove_tag (matches "remove"
  keyword pattern in annotation tests)
- Change collectives_update_collective to idempotentHint=False (update
  operations are non-idempotent per project convention)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 08:14:59 +01:00
Chris CoutinhoandClaude Opus 4.6 e5ad625a66 fix: address PR review feedback for Collectives support
- Validate OCS envelope status before unwrapping data (raise OCSError on
  statuscode >= 400)
- Fix test data: filePath should be "" for root-level pages, not filename
- Catch specific exceptions (HTTPStatusError, OSError) instead of bare
  Exception in WebDAV content fetch, include error in log message
- Return updated resource data from update_collective, move_page, and
  set_page_emoji instead of discarding API responses
- Fix create_page docstring to mention collectivePath/filePath/fileName
- Remove unused additional_headers parameter from _get_ocs_headers
- Add unit test for OCS error status validation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 08:07:37 +01:00
Chris CoutinhoandClaude Opus 4.6 32f5a0fe52 feat: add Nextcloud Collectives app support (#621)
Implement MCP tools for the Collectives wiki/documentation app, enabling
agentic workflows for team knowledge base management.

16 tools covering collectives, pages, tags, search, and trash:
- Read: list collectives, list/get pages (with WebDAV content), search,
  list tags, list trashed pages
- Write: create/update collective, create/move/trash/restore pages,
  set emoji, create/assign/remove tags

Includes Docker hook for app installation, OCS API client with envelope
unwrapping, Pydantic models, unit tests (16), and integration tests (10).

Closes #621

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 07:53:50 +01:00
Chris CoutinhoandClaude Opus 4.6 581718ee42 build: update qdrant-client to upstream >=1.17.0
The fork (cbcoutinho/qdrant-client fix/fusion-score-threshold) has been
merged upstream as PR #1138 and released in v1.17.0. Remove the git source
override in [tool.uv.sources] to unblock clean PyPI publishing, since git
dependencies are excluded from wheels.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:18:38 +01:00
Chris CoutinhoandClaude Opus 4.6 7956c3c061 refactor: remove Smithery deployment mode
Smithery is no longer a supported deployment mode. Remove all Smithery-specific
code paths, middleware, configuration, and tests. This simplifies the codebase
by eliminating DeploymentMode enum, SmitheryConfigMiddleware, session config
context variables, and the smithery_main entrypoint.

Files deleted: Dockerfile.smithery, smithery.yaml, smithery_main.py
ADR-016 retained with deprecated status for historical reference.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:15:47 +01:00
Chris CoutinhoandClaude Opus 4.6 96839662bc fix: increase vector sync wait timeout to prevent sampling test timeouts in CI
Extract reusable wait_for_vector_sync() helper with 90s max_wait (up from
30s) to handle slow single-worker processing in CI. Increase processor
workers to 2 for the mcp service to parallelize note indexing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 18:11:15 +01:00
Chris CoutinhoandClaude Opus 4.6 322e92276e fix: reduce vector sync scan interval to 5s for single-user service
The test_semantic_search_answer_successful_sampling test creates a note
and waits 30s for indexing, but the scanner only ran every 60s. Aligning
with the CI overlay's 5s interval ensures new notes are indexed in time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 16:59:36 +01:00
Chris CoutinhoandClaude Opus 4.6 331abdd00a fix: expose public status endpoints in all modes and enable vector sync (#637)
Make /api/v1/status and /api/v1/vector-sync/status available in all
non-Smithery deployment modes so Astrolabe can show server status even
in single-user BasicAuth mode. Previously these were only mounted when
OAuth or multi-user BasicAuth with offline access was enabled.

- Split management API routes into public (Tier 1) and authenticated (Tier 2+)
- Enable semantic search with in-memory Qdrant for single-user docker service
- Update astrolabe submodule with admin settings fix

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 13:18:49 +01:00
Chris CoutinhoandClaude Opus 4.6 3aefe175e6 chore: update oidc submodule to consent redirect fix branch
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 18:37:06 +01:00
Chris CoutinhoandClaude Opus 4.6 ad4ceaff30 fix: resolve OIDC consent flow 500 errors on NC 32
Root cause: ConsentController::grant() only passed client_id and scope
in the post-consent redirect, relying on PHP session fallback for state,
response_type, redirect_uri etc. On NC 32 (PHP 8.4), session values
were intermittently lost between session->close() and the subsequent GET
request, causing 500 errors from trim(null) / matchRedirectUri(null).

OIDC app fixes:
- Pass all OAuth params in consent redirect URL (eliminates session race)
- Add null safety guard in authorize endpoint (400 instead of 500)

Test infra fixes:
- Wait for OIDC redirect chain to settle before handling consent screen
  (fixes "Execution context was destroyed" Playwright errors)
- Capture nextcloud.log in CI failure artifacts for PHP error debugging

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 16:47:20 +01:00
Chris CoutinhoandClaude Opus 4.6 945b01cbf5 fix: address PR #632 review comments
- Update stale httpx reference to niquests in calendar.py type comment
- Replace inline inspect.isawaitable with _maybe_await helper in tests
- Fix incorrect port number in docker-compose unstructured comment
- Remove commented-out smithery service block (dead code)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 17:03:46 +01:00
Chris CoutinhoandClaude Opus 4.6 d9b010ab26 fix(ci): build OIDC app for all test modes including single-user
The OIDC submodule volume mount is on the base app service, so all
modes mount it. Without composer install, the post-install hook enables
a broken app (missing vendor/autoload.php), causing Nextcloud to fail.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 10:34:10 +01:00
Chris CoutinhoandClaude Opus 4.6 7a2280a981 fix: patch OIDC consent flow regression and add CI build step
The OIDC app 1.16.2 broke the consent flow by only falling back to
session params when client_id is missing. After consent, the redirect
includes client_id and scope but loses state, response_type, and
redirect_uri — causing a 500. The submodule fix restores per-param
session fallback when ANY critical param is missing.

Also adds a CI build step for the OIDC app (composer + npm) so the
JS assets (oidc-consent.js, oidc-redirect.js) are available in OAuth
test profiles.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 10:00:48 +01:00
Chris Coutinho 69b84102b1 chore: update oidc app 2026-03-16 18:42:24 +01:00
Chris Coutinho b266c35725 Merge remote-tracking branch 'origin/master' into fix/caldav-href-handling-629 2026-03-16 18:38:50 +01:00
Chris CoutinhoandClaude Opus 4.6 e24e49218e fix(caldav): address PR #632 review feedback
- Modernize typing: replace Dict/List/Optional with dict/list/X|None
- Add comment explaining _hacks="insist" mirrors upstream pattern
- Add comments noting caldav v3 raises PutError on HTTP failure
- Narrow except Exception to caldav_error.NotFoundError in delete methods
- Replace private _maybe_await import in tests with stdlib inspect.isawaitable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 22:08:42 +01:00
Chris CoutinhoandClaude Opus 4.6 36a664dda4 fix(caldav): migrate to upstream caldav v3.0.1 to fix href handling (#629)
When Nextcloud stores CalDAV objects, the server-side filename may differ
from the VTODO/VEVENT UID. The caldav fork constructed object URLs from
the UID instead of the actual <d:href> from REPORT responses, causing
list_todos to return wrong hrefs, delete_todo to silently no-op, and
update_todo to fail.

Upstream caldav v3.0.1 fixes this in _async_request_report_build_resultlist
by passing url=self.url.join(url) when constructing result objects.

Key changes:
- Replace caldav fork with upstream caldav>=3.0.1,<4.0
- Update imports to caldav.aio module
- Add _maybe_await() helper for v3's dual-mode methods that return
  either objects or coroutines depending on async context
- Add _async_object_by_uid() to work around upstream's get_object_by_uid
  not being async-aware (it iterates a coroutine synchronously)
- Adapt save_event/save_todo (no longer return tuples)
- Pass url=calendar.url.join(href) in _search_events_by_date
- Pass include_completed=True in list_todos to match previous behavior
- Add integration test for filename != UID scenario

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 18:45:24 +01:00
Chris CoutinhoandClaude Opus 4.6 fdb7b87baf fix: handle pythonvCard4 dict-format fields and missing phone numbers (#601)
Fix three related contacts bugs:
- Parse dict-format vCard fields ({value, type}) that pythonvCard4 returns,
  which previously crashed Pydantic validation expecting plain strings
- Include tel field in client output so phone numbers reach MCP tools
- Clarify addressbook parameter expects URI slug, not displayname

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 09:32:53 +01:00
Chris CoutinhoandClaude Opus 4.6 47fb562326 fix: replace assert with proper guard and invalidate scope cache after provisioning
Replace `assert entry.code_challenge` with a proper if-guard returning a
500 JSON error in the token endpoint, since Python's -O flag strips
asserts and would silently disable PKCE enforcement.

Invalidate the scope cache immediately after Login Flow v2 provisioning
completes, so users no longer hit ProvisioningRequiredError for up to
5 minutes after successfully authenticating.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 09:31:36 +01:00
Chris CoutinhoandClaude Opus 4.6 1fae6920be fix: disable NC rate limiting in dev/CI and add token endpoint diagnostics
Disable Nextcloud's bruteforce protection and rate limiting via a new
post-installation hook, preventing 429 errors during repeated DCR calls
in CI. Add warning-level logging to all 8 error paths in the AS proxy
token endpoint to make login-flow 400 errors diagnosable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 08:57:02 +01:00
Chris CoutinhoandClaude Opus 4.6 f43343356e fix: address review feedback — security, caching, CI 429 retry
- Add 429 retry with exponential backoff to register_client() (fixes CI
  oauth matrix failures from parallel DCR requests)
- Make client_id, redirect_uri, and PKCE mandatory at token endpoint
- Add null-checks for discovery_url and OAuth credentials in proxy flows
- Add OIDC discovery document caching with 5-min TTL
- Add per-IP rate limiting on /oauth/register DCR proxy
- Discover DCR endpoint from OIDC discovery instead of hardcoding
- Extract extract_user_id_from_token to auth/token_utils.py (breaks
  circular imports between server/ and auth/ layers)
- Add TTL scope cache in scope_authorization.py (avoids DB hit per tool)
- Add defense-in-depth scope validation in storage layer
- Broaden elicitation exception handling with graceful fallback
- Add idempotentHint to nc_auth_check_status, return "pending" status
  after accepted elicitation, add polling interval to description
- Change ALL_SUPPORTED_SCOPES from tuple to frozenset for O(1) lookups
- Replace Optional[str] with str | None throughout config.py
- Use default_factory for ProxyCodeEntry/ASProxySession dataclasses
- Add proxy code/session cleanup to background loop
- Fix OIDC verification CI step to only run for oauth/login-flow modes
- Add unit tests for access.py REST endpoints (10 tests)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 17:22:23 +01:00
Chris CoutinhoandClaude Opus 4.6 0a53aa5fcd ci: enable Playwright browser tests in GitHub Actions
The GITHUB_ACTIONS skip was added before Playwright automation existed,
when tests required manual browser interaction. Now that Playwright
handles the OAuth flow programmatically, the skip is unnecessary —
GitHub Actions fully supports Playwright with localhost networking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 16:09:14 +01:00
Chris CoutinhoandClaude Opus 4.6 abd43f8028 ci: disable NC 33 matrix until upstream apps support it
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 15:57:21 +01:00