Commit Graph
472 Commits
Author SHA1 Message Date
Chris CoutinhoandClaude Opus 4.7 a995155bd4 fix(deck): address PR #759 round-3 review feedback
- Drop "(in-place)" from filter-helper docstrings; callers should
  consume the return value, mutation is an implementation detail.
- Document that deck_get_archived_stacks always returns cards (an
  archived stack without its cards has no audit value); point to
  description_max_length for size control.
- Document that deck_get_cards applies filtering client-side, so it
  is network-equivalent to deck_get_stack(include_cards=True).
- Pin the empty-list contract: a stack with all-archived cards and
  include_archived_cards=False yields cards == [] (loaded but empty),
  not cards is None (explicitly suppressed).
- Add explicit one-character-over-limit truncation test alongside the
  existing exact-boundary test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 01:09:54 +02:00
Chris CoutinhoandClaude Opus 4.7 b7805c2180 fix(deck): address PR #759 round-2 review feedback
- Move description_max_length validation to tool layer
  (_validate_description_max_length), matching the existing
  _validate_comment_message pattern; helper now trusts callers per
  CLAUDE.md ("validate at system boundaries only").
- Fix mutation/return inconsistency: deck_get_stacks now uses a list
  comprehension to capture _apply_stack_filters' return, matching
  deck_get_stack / deck_get_archived_stacks.
- Rename include_archived -> include_archived_cards on deck_get_cards
  and _apply_card_filters for consistency with deck_get_stacks.
- Route deck_get_archived_stacks through _apply_stack_filters so
  future filters apply uniformly to active + archived paths.
- Trim _truncate_card_descriptions docstring to one line; add inline
  comment in _apply_stack_filters explaining the breaking-change
  default (mirrors Deck UI archived-card filtering).
- Replace fragile call_args[0][1] with call_args.args[1] in the
  archived-stacks client test.
- Modernize Optional[X] -> X | None throughout deck.py (adjacent
  cleanup called out in the review).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:50:56 +02:00
Chris CoutinhoandClaude Opus 4.7 7d633a945d fix(deck): address PR #759 review feedback
- Validate description_max_length is positive (raises ValueError on 0
  or negative); the prior code would have wiped descriptions to a
  single ellipsis character on description_max_length=0.
- Extract filter logic into testable module-level helpers
  (_apply_board_filters, _apply_stack_filters, _apply_card_filters)
  and replace the dense `continue`-based loop in deck_get_stacks with
  the reviewer's elif form.
- Document the truncation length quirk in the helper docstring (result
  is description_max_length + 1 chars when truncation fires).
- Add 13 new unit tests covering the include/exclude flags on board,
  stacks, and flat card lists, plus the new validation paths and an
  explicit "description fits within limit, no ellipsis" case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:41:14 +02:00
Chris CoutinhoandClaude Opus 4.7 d8cd073e66 feat(deck): add response filters and archived stacks tool
Add filtering options to deck read tools to keep responses compact on
boards with accumulated cards/comments, and expose archived stacks so
agents can audit completed work that has been archived off the active
board.

- deck_get_board: include_acl, include_users, include_labels
- deck_get_stacks/deck_get_stack: include_cards, include_archived_cards,
  description_max_length
- deck_get_cards: include_archived, description_max_length
- New deck_get_archived_stacks tool wrapping the existing client method

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:00:09 +02:00
Chris CoutinhoandClaude Opus 4.7 ce80a36877 fix(auth): address PR #757 round-3 review feedback
Three review items from the third-round review on PR #757:

- scope_authorization: split the combined logger.warning(error_msg) in
  the require_scopes decorator's missing-app-password branch into two
  lazy %-style logger calls (one per branch), keeping the f-string
  error_msg for the exception only. The else branch also logs the
  elicit_result for diagnostics. Bypassing lazy %-interpolation in
  security-sensitive code formatted the message regardless of log level
  and matched the repo-wide lazy-logging preference; the new code now
  conforms.
- config + browser_oauth_routes: wire COOKIE_SECURE through Settings
  (cookie_secure: bool | None = None) so _should_use_secure_cookies()
  reads it via get_settings() rather than os.getenv. Completes the
  consolidation pass that touched this file in commit 7464340 and
  removes the last raw os.getenv from browser_oauth_routes.py
  (import os dropped). Dynaconf auto-coerces "true"/"false" → bool;
  "1"/"0" arrive as int and are normalised by an explicit bool() at
  the consumer.
- elicitation: clarify the _astrolabe_settings_url docstring to call
  out that the empty-string case is also a None-return path (matches
  the existing `if not base:` guard).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 17:24:50 +02:00
Chris CoutinhoandClaude Opus 4.7 f31d0544b7 fix(auth): invalidate scope cache on web/REST provisioning paths
The elicitation flow points users to the Astrolabe web route or the
BasicAuth REST endpoint to provision their app password. Both paths
stored the password without clearing the in-process scope cache, so a
user who provisioned through them would keep hitting
ProvisioningRequiredError for up to _SCOPE_CACHE_TTL (5 min) afterwards.

Add invalidate_scope_cache(user_id) to both write-paths (matching the
existing pattern in nc_auth_check_status), correct the now-misleading
comment in scope_authorization.py to name all three invalidation paths,
and add a one-line hint above the first elicitation patch in the test
file so future authors don't "fix" the patch target to the wrong module.

Addresses PR #757 round-3 review feedback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:59:53 +02:00
Chris CoutinhoandClaude Opus 4.7 7464340763 fix(auth): address PR #757 round-2 review feedback
Four review items from the second-round review on PR #757:

- scope_authorization: broaden the post-elicit retry message to acknowledge
  the 5-minute scope-cache TTL — if the LFv2 poller is still in-flight at
  acknowledge-time, the immediate retry can still hit a stale cache.
- elicitation: extract a shared `_run_elicit(ctx, message, schema, *,
  log_label)` helper so `present_login_url` and
  `present_provisioning_required` no longer duplicate the
  hasattr-guard / try-NotImplementedError / try-Exception fallback block.
  The data-acknowledged warning specific to login-flow stays in
  `present_login_url` so behaviour is preserved exactly.
- elicitation: detect missing http:// / https:// scheme in
  `_astrolabe_settings_url`, log a warning, and return None — caller
  renders the safe tool-only fallback instead of producing a broken link.
  New unit test locks this in.
- browser_oauth_routes: replace the stray
  `os.getenv(\"NEXTCLOUD_HOST\")` in `_should_use_secure_cookies` with
  `get_settings().nextcloud_host` for consistency with the rest of the
  file (PR #757 review nit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:33:43 +02:00
Chris CoutinhoandClaude Opus 4.7 f3256e515e refactor(config): consolidate NEXTCLOUD_PUBLIC_ISSUER_URL through Settings
Lift NEXTCLOUD_PUBLIC_ISSUER_URL out of raw os.getenv reads into
Settings.nextcloud_public_issuer_url across all 8 production call sites
(app.py x2, oauth_routes.py x2, browser_oauth_routes.py,
provision_routes.py, userinfo_routes.py, elicitation.py). cli.py
remains the env-write source so the existing config-by-flag pipeline
still works.

Also addresses remaining PR #757 review nits:
- elicitation.py: align URL-present/absent wording on "open in your
  browser" so users don't try clicking in the terminal
- test_scope_authorization_stored.py: lock in the deliberately-shared
  fall-through branch with explicit declined/cancelled decorator tests
- test_elicitation.py: switch from monkeypatch.setenv to
  patch(get_settings) since Settings is now the canonical surface

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:16:19 +02:00
Chris CoutinhoandClaude Opus 4.7 822a8fe2ed fix(auth): address PR #757 review feedback
- Branch the ProvisioningRequiredError message on the elicit result so a
  user who acknowledged the prompt isn't told to call
  nc_auth_provision_access (which would loop an LLM that just confirmed
  via elicitation). Other paths keep the existing instruction.
- Convert present_login_url's f-string logger.warning to lazy %s, matching
  present_provisioning_required and the repo's lazy-logging preference.
- Add a test for NEXTCLOUD_PUBLIC_ISSUER_URL trailing-slash normalization.
- Strengthen the decorator-elicits test: split into the "accepted" and
  "message_only" branches so the error-message change is regression-tested.

Refs: cbcoutinho/nextcloud-mcp-server#757#issuecomment-4363552487

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:34:56 +02:00
Chris CoutinhoandClaude Opus 4.7 2da8b38aeb feat(auth): elicit Astrolabe URL on missing app password
When a tool requiring Nextcloud access is called without a stored app
password (Login Flow v2 mode), the @require_scopes decorator now invokes
MCP elicitation with a clickable Astrolabe settings URL — reconstructed
from NEXTCLOUD_PUBLIC_ISSUER_URL / NEXTCLOUD_HOST — before raising
ProvisioningRequiredError. Clients without elicitation support fall back
to the existing text error.

Surfaced by cbcoutinho/nextcloud-mcp-server#752, where users hit a 401
after OAuth and had no clickable URL to start Login Flow v2 from.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 12:02:18 +02:00
Chris CoutinhoandClaude Opus 4.7 104bbd390d refactor(search): address PR #750 round 12 review feedback
Six review items raised; four required code changes (#3, #4, #5, #6) and
two were resolved without code changes (#1 audit-only, #2 informational).

* search/verification.py — clarify the granularity asymmetry between the
  whole-batch fail-open (structural API failure) and the per-item fail-open
  (single bad stored doc_id). Future readers no longer need to derive why
  the two paths have different blast radii from the code alone.

* models/semantic.py — `dropped_document_count` description now explicitly
  notes that subtracting it from `verified_chunk_count` is not a meaningful
  operation, since the two fields count different units (documents vs
  chunks). Surfaces the unit mismatch where MCP clients actually see it.

* server/semantic.py — clarify the per-doc_type over-fetch comment so the
  N×2 pre-merge Qdrant cost (vs the cross-app branch's 1×2) is explicit
  rather than implied by "same 2× over-fetch budget".

* tests/unit/search/test_verification.py — add four new 429 unit tests
  (notes/news/files/deck) mirroring the existing 5xx-keeps pattern. Locks
  in that `_is_definitive_404_or_403` returns False for 429 so a future
  refactor cannot accidentally treat rate-limit responses as permanent
  revocations.

Audit confirmation for review item #1: all four `WebDAVClient.get_file_info`
call sites already handle the new `HTTPStatusError`-on-404 contract
(verification.py:156, tests/integration/test_rag.py:139,
tests/unit/client/test_webdav.py:153/190). No silent breakage internal to
this repo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 00:26:06 +02:00
Chris CoutinhoandClaude Opus 4.7 1ed8362f78 refactor(search): address PR #750 round 11 review feedback
Three minor fixes from the round-11 review on PR #750:

- bm25_hybrid.py:209 — Comment said `doc_id` is `int (notes) or str (files)`,
  which is backwards. Notes, news_items, and deck_cards are stored as `str`
  (scanner.py:241, 666, 867); files are stored as `int` (scanner.py:425).
  Updated to point readers at scanner.py as the source of truth.

- verification.py:338 — Lowered the News-API 403/404 log line from `info`
  to `debug`. The News app being uninstalled or disabled is a predictable
  operational state (matching the other verifiers' debug-on-not-found
  paths), so this should not generate operator-dashboard noise. Transient
  errors immediately below stay at `warning` because they're unexpected.

- semantic.py:809 — `nc_get_vector_sync_status` was reading
  `document_receive_stream` via `getattr(..., None)`, but the attribute is
  guaranteed-defined on both `AppContext` and `OAuthAppContext` (as a
  field with `None` default). The defensive `getattr` masked typos that
  the eviction_task_group access at semantic.py:197-199 deliberately
  surfaces. Switched to direct access; the `if … is None:` value-check
  below is preserved (the attribute can legitimately be None before sync
  starts).

Items deliberately deferred (with rationale in the plan file):
- News verifier semaphore-hold during get_items (reviewer: "not required
  here, just worth tracking"; ADR already lists follow-ups).
- Hardcoded 2× over-fetch / VERIFICATION_OVERFETCH (TODO already in code).
- Integration test for the real Qdrant eviction filter (reviewer marked
  low-priority; type-preservation chain is unit-tested).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:00:42 +02:00
Chris CoutinhoandClaude Opus 4.7 15ffeca312 refactor(search): address PR #750 round 10 review feedback
- Tighten verify_search_results signature: client: Any → NextcloudClientProtocol
- Collapse 3 copy-pasted lock-justification comments to a single-line pointer
- Add logger.debug timing around the verify_search_results call site
- Add logger.debug timing around the unbounded news.get_items fetch
- Rename SemanticSearchResponse.dropped_count → dropped_document_count to make
  the chunks-vs-documents unit asymmetry explicit at the API boundary
- Drop unreachable duplicate 409 branch in WebDAVClient.move_resource

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 22:44:57 +02:00
Chris CoutinhoandClaude Opus 4.7 852ffa3678 refactor(search): address PR #750 round 9 review feedback
- Add concurrency-safety comments to per-verifier accessible sets in
  _verify_notes/_verify_files/_verify_deck_cards. Same rationale as
  accessible_by_type in verify_search_results: anyio is cooperative,
  set.add() is not an await point.
- Document 401 exclusion in _is_definitive_404_or_403 (treated as
  transient because it usually signals expired credentials, not
  permanent denial).
- Note multi-user compounding in the news verifier semaphore comment:
  N concurrent users hold N slots out of the shared budget.
- Log inaccessible doc ids with a type tag (e.g. "int:42" vs "str:42")
  so ghost-record logs disambiguate id types.
- Type the BatchVerifier alias and the four verifier function signatures
  with NextcloudClientProtocol instead of Any (algorithms.py exposes
  the right interface; the protocol is runtime_checkable).
- Surface verified_chunk_count vs dropped_count semantics in the
  nc_semantic_search tool docstring Returns block (chunks vs unique
  documents).
- Add comments to the two max_concurrent=20 sites in server/semantic.py
  noting they are intentionally distinct from
  settings.verification_concurrency (different request phases).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 22:18:03 +02:00
Chris CoutinhoandClaude Opus 4.7 3153c9dac4 refactor(search): pre-push review fixes for PR #750
Address findings surfaced by `pre-push-review` after the round 8 sweep:

- Add deck verifier symmetry tests (404, transient 5xx, unexpected
  exception, non-numeric metadata) so deck has the same shape as the
  notes/news/files verifiers. Also add unexpected-exception tests for
  the news and file verifiers, which had `except Exception` branches
  no test was reaching. Keeps the registry-style verifier coverage
  uniform.
- Modernize sibling field types in `VectorSyncState`, `AppContext`,
  and `OAuthAppContext` from `Optional[X]` to `X | None`, matching the
  `eviction_task_group: TaskGroup | None` field added in the round 8
  diff (resolves the inconsistency flagged by A6). The lone remaining
  `Optional` import is dropped.
- Reverse cross-reference direction in the verifier docstrings: the
  later-defined `_verify_deck_cards` and `_verify_news_items` now
  point at `_verify_notes` as the canonical hoisted-cast pattern,
  rather than `_verify_notes` forward-referring to verifiers defined
  below it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 21:57:38 +02:00
Chris Coutinho 06fe3916e7 ci: Update CLAUDE.md and add pre-push-review skill. Remove astrolabe from docker-compose.yml volume mount 2026-05-01 21:53:11 +02:00
Chris CoutinhoandClaude Opus 4.7 8a2626da6c refactor(search): address PR #750 round 8 review feedback
- Rename `verified_count` → `verified_chunk_count` to make the count
  granularity explicit at the field name (chunks vs unique docs).
- News verifier now fails open *per-item* on non-numeric stored doc_ids
  (matches notes/files/deck shape); a single bad id no longer rescues
  definitively-missing siblings from eviction.
- Update note-verifier integration test to use string doc_ids end-to-end
  to match production storage (scanner.py:241 stringifies note ids).
- Add regression test for the closed-task-group race guard in
  `verify_search_results` so the RuntimeError swallow is locked in.
- Convert remaining f-string logger calls in `server/semantic.py` to
  lazy %-style formatting (per repo convention).
- Document `evict_on_missing` as a developer/test flag (no env var) and
  flag the `get_file_info` 404→raise contract change in its docstring.
- Add a TODO(ADR-019) breadcrumb for the hardcoded 2× over-fetch so
  future tuning has a clear hook.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 21:40:30 +02:00
Chris CoutinhoandClaude Opus 4.7 3e981e647a refactor(search): address PR #750 round 7 review feedback
Round 7 raised 5 issues; this round addresses all of them and fixes
the underlying causes (not just the comments) where applicable so
they don't get re-flagged in future passes.

Critical:
- verified_count description in SemanticSearchResponse said "unique
  documents" but the value is len(verified_results), a chunk count.
  Description rewritten to accurately document chunk-level granularity
  AND explicitly call out the asymmetry with dropped_count (which
  counts unique (doc_id, doc_type) pairs).

- _verify_files false-eviction risk: the round-6 doc-only fix was
  re-flagged. Address at the source — widen WebDAVClient.get_file_info
  to raise HTTPStatusError on 404 (matching the rest of the client
  convention) and reserve None for the genuinely ambiguous
  malformed-PROPFIND case. _verify_files now keeps the result on None
  (cannot tell whether the file exists) and evicts only on a
  definitive HTTPStatusError 404. Tests updated; new test added for
  the malformed-XML keep-result path.

Non-critical:
- News verifier semaphore lifetime now explicitly documented: one
  slot held for one deduplicated fetch per search is the correct
  backpressure behaviour.

- Cross-reference comments in _verify_notes / _verify_deck_cards no
  longer claim "Mirrors X" pointing at functions defined later in
  the file; now use direction-neutral "parallel implementation in".

- accessible_by_type is mutated by concurrent run_verifier tasks; a
  comment explains why this is race-free under anyio's cooperative
  multitasking (distinct keys per task, no await between read and
  write) so a future reader doesn't add a redundant lock.

- Knock-on: tests/integration/test_rag.py wraps get_file_info in a
  try/except for the new contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 21:15:39 +02:00
Chris CoutinhoandClaude Opus 4.7 e8df6003c5 refactor(search): address PR #750 round 6 review feedback
Closes out the remaining nits flagged in the round-6 review.

Critical:
- _verify_files contract comment now enumerates all None-return cases
  (404 + malformed PROPFIND XML) and documents the false-eviction
  trade-off; self-healing via re-indexing recovers
- int(r.id) cast at the SemanticSearchResult boundary now raises a
  TypeError with explicit doc_type/value context instead of bubbling
  up as an opaque "Search failed: ..." McpError

Design observations:
- nc_semantic_search_answer docstring documents the per-note
  round-trip cost from the post-verification race guard
- News verification latency hint added to configuration.md
- SemanticSearchResponse exposes verified_count + dropped_count so
  short result pages on high-ghost-density indexes are
  distinguishable from genuine scarcity. verify_search_results now
  returns (kept, dropped_count); production caller and tests updated

Minor:
- Comment clarifies the .get() fallback in verify_search_results is
  defensive only (run_verifier always populates the entry)
- Eviction task-group guard narrowed from except Exception to
  except RuntimeError (the only documented failure mode of
  TaskGroup.start_soon on a closed group)
- Indexer logs a warning when a deck_card task is missing
  board_id/stack_id, surfacing data-quality issues at index time
  rather than at verification time
- New unit test covers the news verifier's non-numeric-id fail-open
  path (one bad doc_id keeps the entire batch)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 21:02:27 +02:00
Chris CoutinhoandClaude Opus 4.7 ffcca23a7b refactor(search): address PR #750 round 5 review feedback
Tightens verifier consistency, closes test gaps, hardens the fire-and-forget
eviction snapshot, and routes the new concurrency knob through Settings.

- Pre-flight ``int()`` guard in ``_verify_notes`` mirrors ``_verify_deck_cards``,
  so a non-numeric note id produces a type-specific log line instead of
  falling through to the generic "unexpected error" branch.
- Adds explicit 403 tests for the file and news verifiers (symmetry with the
  existing notes/deck 403 tests) plus a ``non_numeric_id_keeps`` test.
- ``AppContext`` and ``OAuthAppContext`` no longer snapshot
  ``_vector_sync_state.eviction_task_group`` at lifespan-yield time. Both
  expose it as a ``@property`` that reads the singleton dynamically, removing
  the order-sensitive race where a future startup-ordering change could
  silently degrade fire-and-forget eviction to inline forever.
- Adds ``verification_concurrency`` (env var ``VERIFICATION_CONCURRENCY``,
  default 20) to ``Settings`` with a dynaconf validator; ``verify_search_results``
  resolves the cap lazily from settings when the caller doesn't override it.
- Enriches the news verifier TODO to call out that ``batch_size=-1`` is
  intentional — a numeric ceiling would silently break correctness because
  any item beyond the cap would be missing from ``present_ids`` and dropped.
- Updates ``Optional[TaskGroup]`` to ``TaskGroup | None`` per project style.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 20:45:56 +02:00
Chris CoutinhoandClaude Opus 4.7 926722b09d refactor(search): address PR #750 round 4 review feedback
- Guard eviction_task_group.start_soon against shutdown race so a
  RuntimeError on a closed group never surfaces as a search error.
- Correct ADR-019 news_item row: there is no per-item REST endpoint;
  verification batches via get_items(batch_size=-1) and intersects.
- Modernize models/semantic.py typing to PEP 604 / lowercase generics
  per CLAUDE.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 19:28:55 +02:00
Chris CoutinhoandClaude Opus 4.7 aa4b9498a1 refactor(search): address PR #750 round 3 review feedback
- _verify_deck_cards: hoist int(board_id|stack_id|doc_id) out of the generic
  except Exception into an explicit try/except (TypeError, ValueError) before
  the network call, mirroring _verify_news_items. Malformed payloads now log
  a specific warning instead of "unexpected error".
- _verify_news_items: add TODO(perf) above the get_items(batch_size=-1) call
  to mark the known fetch-all cost as a future profiling target.
- SemanticSearchResult.id: revert from int|str back to int. The internal
  SearchResult.id stays int|str for forward-compat; the MCP response model
  narrows at the boundary. server/semantic.py casts r.id to int when
  constructing the response so future string-id types fail loudly here
  instead of silently widening the public API.
- nc_semantic_search: replace the terse "extra for access filtering" comment
  with an ADR-019 NOTE block explaining the 2x over-fetch trade-off and the
  ghost-density under-delivery case (self-heals via lazy eviction).
- tests/integration/test_verify_on_read.py: extend the module docstring to
  call out that only the note verifier is exercised against real Nextcloud,
  while file/deck_card/news_item are unit-only — documenting the suite split
  for future contributors.
- ADR-019: rewrite "Module shape", "Verifier registry", example verifier,
  and "Deduplication" sections to match the shipped BatchVerifier interface
  (was per-id Verifier in the original draft). Add a "Why batch?" paragraph
  explaining the design choice. Update implementation checklist — every
  item is now [x] with corrected verifier names (plural) and the eviction
  module path (vector/eviction.py).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 19:17:35 +02:00
Chris CoutinhoandClaude Opus 4.7 21e5608a39 refactor(search): address PR #750 round 2 review feedback
Implements fire-and-forget eviction (ADR-019 §"Lazy eviction"): the
search response no longer waits on Qdrant deletes, instead spawning
evict() on a long-lived lifespan-owned task group. Falls back to inline
eviction in modes without vector sync and in unit tests.

Also: harden _verify_news_items against non-numeric ids (fail open
instead of crashing the verifier); document the get_file_info None-on-404
contract; add INDEXED_DOC_TYPES single source of truth in vector/scanner.py
referenced by the CI-guard test; write a Verify-on-Read Latency Budget
section in docs/configuration.md covering the unbounded news.get_items
fetch. Closes the two remaining ADR-019 implementation checklist items.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 18:53:32 +02:00
Chris CoutinhoandClaude Opus 4.7 7784ec02d7 refactor(search): address PR #750 review feedback
- Cap all_results to limit*2 after sort in the per-doc_types branch of
  nc_semantic_search to bound over-verification (was unbounded N-types).
- Switch BatchVerifier from (client, doc_ids, user_id) to (client, results,
  semaphore). Verifiers now read file paths and deck board/stack ids from
  SearchResult.metadata instead of doing fresh Qdrant scrolls — eliminates
  one duplicate round-trip per file/deck-card verification.
- Bound per-id verification concurrency with a shared anyio.Semaphore
  (default 20, matching server/semantic.py context-expansion convention).
  Prevents httpx pool exhaustion / rate limiting on large search pages.
- Propagate stack_id from Qdrant payload to SearchResult.metadata in both
  bm25_hybrid.py and semantic.py (board_id was already propagated).
- Drop now-unused _resolve_file_path / _resolve_deck_metadata helpers.
- Drop redundant int(d) in requested predicate from _verify_news_items.
- Rewrite eviction comment to be honest about inline (not background)
  execution and the resulting latency coupling.
- ADR-019 status: Proposed -> Accepted.
- Add news property to NextcloudClientProtocol.
- Widen SearchResult.id and SemanticSearchResult.id to int | str to match
  BatchVerifier signature and document support for future string-id types.
- Flip openWorldHint to True on nc_semantic_search_answer (it calls into
  Nextcloud via nc_semantic_search).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 08:22:28 +02:00
Chris CoutinhoandClaude Opus 4.7 d90e793d19 feat(search): verify-on-read for semantic search results (ADR-019)
The vector index lags Nextcloud (5-min webhook cron + scanner interval),
producing ghost records for deleted/unshared documents until the next
reconciliation. Verify each unique document against Nextcloud at query
time, drop inaccessible results, and lazily evict the corresponding
Qdrant points.

Per-doc_type batch verifiers: notes/files/deck cards run concurrently
per id; news items use a single fetch + intersect to avoid the per-item
fetch-all amplification. Transient errors fail open (keep result, log
warning) — only definitive 4xx drops. Multiple chunks of the same doc
collapse to one verification call.

Wired into nc_semantic_search before the limit trim and before context
expansion. nc_semantic_search_answer's per-note re-fetch retained as a
sub-second race guard since verification now happens upstream.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 07:45:01 +02:00
Chris CoutinhoandClaude Opus 4.7 affe29f72e fix(webhooks): escape webhook_uri, lazy logging, document 401 header omission
Address round-5 reviewer feedback on PR #747:

- Escape `webhook_uri` in the admin pane HTML template so an operator-
  controlled env value (`WEBHOOK_INTERNAL_URL`, `NEXTCLOUD_MCP_SERVER_URL`)
  can't inject markup. The sibling `preset_id` and exception messages were
  already escaped — this one was the odd one out.
- Convert the eight remaining f-string `logger.warning`/`logger.error`
  calls in `api/webhooks.py` to lazy `%s` formatting, matching the style
  already adopted by `webhook_receiver.py` and `webhook_routes.py`.
- Document why the 401 from `handle_nextcloud_webhook` deliberately omits
  `WWW-Authenticate`: NC's webhook delivery worker has no auth-flow state
  machine to negotiate against, the bearer is a static shared secret
  configured out-of-band via `WEBHOOK_SECRET`, and a challenge response
  wouldn't change client behaviour. The existing warning log already
  records the rejection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 14:37:34 +02:00
Chris CoutinhoandClaude Opus 4.7 4a3857aabb fix(webhooks): escape HTML in error responses, compare bearer as bytes
Address the two Security findings from PR review:

- webhook_receiver: encode Authorization header and expected bearer to
  utf-8 bytes before hmac.compare_digest. Conventional form; doesn't
  rely on Python's implicit ASCII encoding.
- webhook_routes: html.escape user-influenced and exception-derived
  strings before interpolating into HTMLResponse content. Covers the
  preset_id path param echoed in the "Unknown preset" branch and the
  str(e) text rendered on handler exceptions.

Adds regression tests verifying compare_digest is invoked on bytes and
that <script> payloads (in preset_id and exception messages) are
emitted as escaped entities, not active markup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 14:17:45 +02:00
Chris CoutinhoandClaude Opus 4.7 c1368b9a7f refactor(webhooks): bound queue waits, route URLs through dynaconf
Addresses round-3 review feedback on PR #747:

- webhook_receiver: wrap send_stream.send() in anyio.fail_after(1.0)
  and return 503 with reason="queue full" if the queue is saturated.
  Avoids pinning the handler until NC's outbound timeout fires; the
  503 retry contract is the same as the existing "sync not running"
  branch.
- webhook_receiver: revise the compare_digest comment to match what
  the function actually guarantees — it avoids the per-character
  short-circuit of `==` but is not fully constant-time across length
  differences.
- _get_webhook_uri: read WEBHOOK_INTERNAL_URL and
  NEXTCLOUD_MCP_SERVER_URL via dynaconf so operators using
  settings.toml (rather than env vars) aren't silently routed into
  the docker/localhost fallback. Adds webhook_internal_url to
  Settings/_DEFAULTS/_field_map; nextcloud_mcp_server_url already
  existed. Docker-detection markers stay on os.getenv since they're
  container-runtime signals, not user-facing config.
- webhook_routes: sweep remaining f-string logger calls to lazy %s
  formatting per CLAUDE.md.
- client/webhooks: modernise full file's type hints to
  dict / list / | None per CLAUDE.md.

Tests:
- New test_returns_503_when_queue_is_full exercises the timeout
  branch with a saturated buffer and a shortened deadline.
- test_webhook_uri tests now patch get_settings (matching the
  auth-pair tests in the same file) instead of monkeypatching env
  vars directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 04:17:00 +02:00
Chris CoutinhoandClaude Opus 4.7 f5f05b7c84 refactor(webhooks): address PR review on auth-pass
- webhook_receiver: always run hmac.compare_digest (drop the
  `not provided or` short-circuit) so the constant-time path is
  taken regardless of whether the Authorization header is present.
- client/webhooks: modernise the new `auth_data` type hint to
  `dict[str, str] | None` per CLAUDE.md.
- tests/client: rename `test_create_webhook_with_auth_headers` →
  `test_create_webhook_with_static_headers` and use
  `auth_method="header"` (NC's webhook_listeners only supports
  "none" and "header"; the previous "bearer" value was invalid).
- auth/webhook_routes: extract `_register_preset_webhooks` from
  `enable_webhook_preset` so the auth-threading behaviour is
  testable without standing up a Starlette app + auth middleware.
- tests/unit: new test_webhook_routes_register covering the helper
  with secret set / unset, and verifying ids round-trip in order.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 04:02:26 +02:00
Chris CoutinhoandClaude Opus 4.7 224428fca5 fix(webhooks): authenticate deliveries via WEBHOOK_SECRET; review nits
Adds optional shared-secret authentication for /webhooks/nextcloud,
addressing the security follow-up flagged in #747.

Behavior:
- WEBHOOK_SECRET set: registrations pass authMethod="header" with
  authData={"Authorization": "Bearer <secret>"} (encrypted at-rest in
  Nextcloud's DB and forwarded on every delivery). The receiver
  validates the same header with hmac.compare_digest before parsing
  any payload; missing/invalid → 401.
- WEBHOOK_SECRET unset: registrations stay on authMethod="none" and
  the receiver accepts unauthenticated POSTs (logging a one-time
  startup warning). Backward compatible — operators can roll out at
  their own pace.

Implementation notes:
- WebhooksClient.create_webhook gains an `auth_data` parameter mapped
  to NC's `authData` body field; this is distinct from the existing
  `headers` parameter (`headers` is plaintext static request headers,
  `authData` is encrypted at-rest in NC and only emitted when
  authMethod="header"). The previous `auth_method="bearer"` mention in
  the docstring was incorrect — NC supports only "none" and "header".
- A small `webhook_auth_pair()` helper in auth/webhook_routes.py
  centralises the secret→(auth_method, auth_data) resolution so the
  preset flow and the Astrolabe-facing /api/v1/webhooks endpoint stay
  in sync.

Also addresses the smaller review points from #747:
- f-string → lazy %s formatting in webhook_receiver.py and
  webhook_routes.py.
- Move `int(time)` inside webhook_parser's try/except so a malformed
  `time` field returns None instead of raising ValueError.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 03:50:28 +02:00
Chris CoutinhoandClaude Opus 4.7 2e2a098bee fix(webhooks): wire receiver to vector sync queue and fix registered URI
The /webhooks/nextcloud endpoint was a no-op stub that logged the
payload and returned 200 OK; webhook deletions never reached Qdrant.
Compounding that, _get_webhook_uri() registered the docker-compose
internal hostname (http://mcp:8000) with Nextcloud whenever
/.dockerenv existed — including ECS Fargate — so cloud deployments
were registering a URL NC could not resolve.

- New vector/webhook_parser.py extracts a DocumentTask from
  NodeCreatedEvent / NodeWrittenEvent / BeforeNodeDeletedEvent
  payloads scoped to */files/Notes/*.md (matching the registered
  preset filters).
- New vector/webhook_receiver.py pushes that task onto the same
  send-stream the scanner uses (app.state.document_send_stream),
  with 503 when sync is not running so NC retries delivery.
- _get_webhook_uri() now prefers NEXTCLOUD_MCP_SERVER_URL over the
  /.dockerenv branch, so the explicit public URL set on cloud tasks
  wins; docker-compose dev still falls back to the internal name when
  no public URL is configured.

Calendar / Tables event parsing is intentionally out of scope here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 03:13:50 +02:00
Chris CoutinhoandClaude Opus 4.7 783adeca6e fix(vector-sync): wire document streams into OAuthAppContext
The nc_get_vector_sync_status MCP tool was returning hardcoded
status="unknown", indexed=0, pending=0 for all OAuth deployments
because OAuthAppContext lacked the document_receive_stream field.
The tool's getattr() lookup against the lifespan context returned
None and triggered an early-return before the Qdrant count query.

Add the four vector-sync fields to OAuthAppContext (matching
AppContext) and populate them from the _vector_sync_state singleton
at the lifespan yield site.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 02:06:40 +02:00
Chris CoutinhoandGitHub a2711b4bda Merge pull request #741 from cbcoutinho/feat/talk-spreed-integration
feat(talk): add MCP integration for Nextcloud Talk (spreed)
2026-04-30 01:26:47 +02:00
Chris CoutinhoandGitHub 4bf6937827 Merge pull request #745 from cbcoutinho/feat/strict-auth-allowlists
feat(auth): drop test-client defaults, add ALLOWED_MGMT_CLIENT allowlist
2026-04-30 01:23:07 +02:00
Chris CoutinhoandClaude Opus 4.7 bd7702ad12 feat(auth): drop test-client defaults, add ALLOWED_MGMT_CLIENT allowlist
Both auth surfaces now fail-closed by default:

- ALLOWED_MCP_CLIENTS: removed the silent `claude-desktop` and
  `test-mcp-client` fallbacks. Empty/unset env var leaves the registry
  empty so /oauth/authorize rejects every client_id.
- ALLOWED_MGMT_CLIENT (new): comma-separated list of OIDC client_ids
  whose tokens are accepted by /api/management/*. Enforced in
  verify_token_for_management_api on both the cache-hit and cache-miss
  paths against the token's client_id claim. Unset/empty rejects all.

Compose: set ALLOWED_MGMT_CLIENT=nextcloudMcpServerUIPublicClient on
mcp-multi-user-basic so the existing Astrolabe integration test
(test_astrolabe_chunk_context.py) still passes.

env.sample documents both vars and notes they may be consolidated later.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:20:21 +02:00
Chris CoutinhoandClaude Opus 4.7 d4760f64dc fix(oauth): follow redirects when fetching OIDC discovery
Nextcloud installs without pretty URLs return a 301 from
`/.well-known/openid-configuration` to
`/index.php/.well-known/openid-configuration` (e.g. Hetzner StorageShare).
`_get_cached_discovery` did not enable follow_redirects, so httpx raised
HTTPStatusError on the 301 and the AS-proxy authorize handler returned
500, breaking client connections (e.g. claude.ai).

Pass `follow_redirects=True` to the httpx client used for the discovery
fetch only — downstream OIDC endpoints (token, userinfo, etc.) are
absolute URLs read from the discovery doc and are unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:18:46 +02:00
Chris CoutinhoandClaude Opus 4.7 f075540232 fix(talk): address remaining PR #741 reviewer feedback
Closes the seven outstanding items from the @claude review on PR #741:

1. Add empty `tests/client/talk/__init__.py` for pytest discovery parity
   with `tests/client/{collectives,news}/`.
2. Standardise boolean query params to integers — `includeStatus` was the
   string `"true"` in `list_conversations`/`list_participants` while every
   other flag (`noStatusUpdate`, `lookIntoFuture`, `setReadMarker`,
   `includeLastKnown`) used `1`/`0`.
3. Replace the `app:install || app:enable` chain in the spreed install hook
   with `app:install --keep-disabled --force || true; app:enable spreed`,
   so unrelated install failures surface as a clear "app not found" from
   `app:enable` rather than being silently masked.
4. Add `_validate_token()` (alphanumeric whitelist) and call it from all
   six TalkClient methods that interpolate the token into a URL path —
   defence-in-depth against pathological tokens reaching httpx.
5. Rename `TalkConversation.type` to `room_type` with `Field(alias="type")`
   and `populate_by_name=True`, so the field no longer shadows Python's
   builtin while preserving spreed's wire format on input. MCP responses
   now serialize `room_type` (field name) instead of `type`.
6. `mark_as_read` now passes `json=body or None` so the bodyless
   "mark everything as read" call doesn't send a spurious `{}` body and
   `Content-Type: application/json` header.
7. `_validate_message_text` rejects whitespace-only messages, not just
   empty strings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:14:32 +02:00
Chris CoutinhoandClaude Opus 4.7 9614c0b361 test(talk): cover include_status + malformed-header paths; drop Content-Type from default headers
Addresses the missing-test and Content-Type points from the latest
PR #741 review:

- client/talk.py _talk_headers(): drop the manual `Content-Type:
  application/json`. httpx sets it automatically on requests that pass
  `json=`, and we no longer leak it onto bodyless GETs and DELETEs.
- tests/client/talk/test_talk_api.py:
  - new `test_talk_list_participants_with_include_status` asserting
    `includeStatus=true` is forwarded.
  - new `test_talk_get_messages_invalid_last_given_header` covering
    the defensive try/except around the `X-Chat-Last-Given` parse —
    asserts the fallback `last_given=None` and that a warning is
    logged.
  - existing `test_talk_list_participants` extended to assert that
    `includeStatus` is *absent* by default.

Unit tests: 13 → 15. Integration tests still 7/7.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 00:47:20 +02:00
Chris CoutinhoandClaude Opus 4.7 8f92a7ea29 docs(talk): address PR #741 reviewer nits
Comment-only follow-up to surface non-obvious behavior at the call
sites flagged in review:

- server/talk.py: note the `uuid.uuid4().hex` 32-char no-dashes format
  (spreed accepts either form).
- models/talk.py: warn that spreed returns `lastReadMessage: 0` rather
  than `null` for unread rooms, so consumers should compare to ``None``
  rather than rely on truthiness.
- 10-install-spreed-app.sh: document that the `app:install || app:enable`
  fallback also masks unrelated install failures, and limit its use to
  dev fixtures.

No runtime behavior changes; tests unchanged (still 13 unit + 7 integ).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 00:29:04 +02:00
Chris CoutinhoandClaude Opus 4.7 b6eb7a6bb8 fix(talk): address PR #741 reviewer feedback
Four targeted fixes from the AI code review:

1. TalkConversation.description: drop the misleading `str | None`
   union (spreed always sends `""`, never null) — type is now `str`
   with default `""`.

2. get_messages: guard the X-Chat-Last-Given int parse with
   try/except so a misbehaving proxy can't crash the read flow;
   logs a warning and falls back to None.

3. get_messages: clamp `limit` to [1, 200] in the client (spreed
   caps server-side at 200 and silently truncates) so the returned
   `count` always matches what was actually requested. Both client
   and server-tool docstrings updated to state the valid range.

4. Add an integration test covering the 32000-char message ceiling
   in talk_send_message — the empty-message case was already tested,
   the over-length case was not.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 00:03:24 +02:00
Chris CoutinhoandClaude Opus 4.7 69814f30e3 feat(talk): add MCP integration for Nextcloud Talk (spreed)
Adds 6 MCP tools so an LLM can read a user's Talk conversations and
post messages on their behalf, addressing the "read my chats and reply"
use case from issue #720:

  - talk_list_conversations
  - talk_get_conversation
  - talk_get_messages
  - talk_list_participants
  - talk_send_message    (auto-attaches a referenceId for retry dedup)
  - talk_mark_as_read

Edit/delete messages, reactions, threads, and call/session ops are
intentionally out of scope for this first PR.

The TalkClient also exposes create_conversation/delete_conversation
for the integration test fixture; these are not registered as MCP
tools. A post-installation hook enables spreed in the docker dev env
so the integration suite has a real Talk backend to talk to.

Closes #720

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 23:19:57 +02:00
Chris CoutinhoandGitHub 43598783e4 Merge pull request #734 from cbcoutinho/fix/calendar-niquests-auth-731
fix(calendar): thread raw credentials to caldav AsyncDAVClient (fixes #731)
2026-04-29 23:04:50 +02:00
Chris CoutinhoandClaude Opus 4.7 2129bd6fac fix(deck): address review feedback on card comment tools
- Wrap raw DeckComment returns in CardCommentResponse(BaseResponse) for
  create/update so the success/timestamp envelope matches other deck tools
  (#737 review issue 2).
- Rename ListCardCommentsResponse.total → count and clarify in the
  description that it's the page size, not a server-side total — the Deck
  list endpoint does not expose one (#737 review issue 3).
- Validate the documented 1000-character limit on create/update with an
  inline length check + ValueError, matching the pattern in
  api/management.py (#737 review issue 4).
- Use modern int | None union syntax for the new parent_id parameter
  (#737 review issue 1); rest of the file is left in the existing
  Optional[...] style.

Also add an MCP-level test that the >1000 char message is rejected, and
update the existing comment tests to unwrap the new comment field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 16:57:46 +02:00
Chris CoutinhoandClaude Opus 4.7 454f6912bc feat(deck): add card comment tools
Expose four new MCP tools backed by existing DeckClient comment methods:

- deck_get_card_comments — list with limit/offset pagination
- deck_create_card_comment — top-level or threaded (via parent_id)
- deck_update_card_comment — author-only on the server
- deck_delete_card_comment — author-only, destructive, idempotent

Adds ListCardCommentsResponse and CardCommentOperationResponse models, and
extends the client unit tests to cover replies, deletion, pagination, and
the request shape for updates.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 13:39:24 +02:00
Chris CoutinhoandGitHub 478a5ae39d Merge pull request #733 from cbcoutinho/fix/index-php-prefix-732
fix(client): route /apps/* through /index.php (fixes #732)
2026-04-27 23:59:18 +02:00
Chris CoutinhoandGitHub 47d966c0e4 Merge pull request #736 from cbcoutinho/fix/notes-v5-write-drift-730
fix(notes): defensively unwrap list-shaped Notes responses (refs #730)
2026-04-27 23:59:03 +02:00
Chris CoutinhoandClaude Opus 4.7 fc62be08e2 fix(notes): defensively unwrap list-shaped Notes responses (refs #730)
Notes app v5.0.0 has scenarios where the API returns a JSON list where the
MCP server expects a single note object — notably the notes_api#fail
catch-all returning [] for unmatched routes. Without a guard, callers hit
a cryptic Pydantic "argument after ** must be a mapping, not list" from
Note(**payload).

Add a small _expect_note_object helper at the client layer:
- dict → pass through (the healthy case)
- single-element list → unwrap and warn (Notes v5.0.0 quirk)
- empty list, multi-element list, non-dict → raise a diagnostic ValueError
  that names the operation and points at the likely root cause (URL prefix,
  unmatched route, wrong API version)

Wire it into get_note / create_note / update so any list-shaped response
fails clearly instead of cryptically.

Six unit tests pin every branch of the helper.

Note: The 405s the issue reports for update_note / append_content match
Notes v5.0.0's documented routes (PUT /api/v1/notes/{id}) per upstream
appinfo/routes.php. They are most likely a downstream effect of #732
(missing /index.php URL prefix on installs without Pretty URLs) — the fix
in PR #733 should resolve those once it lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 18:02:19 +02:00
Chris CoutinhoandClaude Opus 4.7 06f895f5b9 fix(models): coerce Contact.birthday + relax Table.owner_display_name
Two upstream Pydantic ValidationErrors that took down whole list responses.

#704: Contact.birthday is declared str, but vobject parses BDAY as a
datetime.date — any contact with a populated BDAY broke nc_contacts_list_contacts
entirely. Add a field_validator(mode="before") that coerces date / datetime
to ISO strings. Strings and None pass through unchanged. Defense in depth:
existing call sites already coerce, but the model is now correct on its own
so any future code path that constructs Contact from raw vobject output
stays safe.

#728: Tables app v2.0.1 stopped emitting owner_display_name on the top-level
table payload (still present inside views via get_schema), so list_tables
failed for every user with a Pydantic ValidationError. Make the field
Optional[str] = None — captures the value when present, won't blow up when
missing.

Six new direct-construction unit tests in tests/unit/test_response_models.py
pin both fixes (date / datetime / str / None for birthday; with / without
owner_display_name for Table) so the regressions can't recur silently.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 17:14:08 +02:00
Chris CoutinhoandClaude Opus 4.7 2f1b0d2500 fix(calendar): thread raw credentials to caldav AsyncDAVClient
caldav 3.x lists niquests as a mandatory dependency and prefers it over
httpx. Passing httpx.BasicAuth via the auth= argument breaks under the
niquests backend with "Unexpected non-callable authentication" — see #731.

Switch CalendarClient.__init__ from auth=Auth|None to keyword-only
password/token, and forward them to AsyncDAVClient as password= plus an
explicit auth_type ("basic" or "bearer"). caldav then builds whichever
auth object its active backend needs (niquests.auth.HTTPBasicAuth or
httpx.BasicAuth), so we stay backend-agnostic.

Threaded raw credentials through NextcloudClient — added keyword-only
password/token to its __init__, and updated from_env, from_token, and
the four call sites that build NextcloudClient (context.py basic-auth
and Login Flow paths, auth/userinfo_routes.py, vector/oauth_sync.py).

Four new unit tests pin the construction wiring so the niquests
regression can't recur silently — basic, bearer, no-creds, and
password-precedence cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:52:58 +02:00
Chris CoutinhoandClaude Opus 4.7 3fb3680a83 fix(client): route /apps/* through /index.php for non-pretty-URL installs
Bare /apps/<app>/... URLs return 404 on Nextcloud installs without Pretty
URLs (URL rewriting), which is opt-in and not the default — see #732. The
/index.php/apps/... form is the universal entry point and works regardless
of web-server config, matching how /remote.php/dav and /ocs/v2.php already
have dedicated entry points.

Add a small _resolve_url helper on BaseNextcloudClient that rewrites
/apps/... → /index.php/apps/... at the top of _make_request, so every
current call site (notes, deck, cookbook, news) and any future ones are
covered transparently with no per-client churn.

Other path prefixes (/remote.php, /ocs, absolute URLs, already-prefixed
/index.php/apps) pass through unchanged. New unit tests in
tests/unit/client/test_base.py pin all six cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:33:37 +02:00