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>
Switch the diff range from $BASE..HEAD to $BASE so the review covers
working-tree changes (committed + staged + unstaged), letting the skill
run usefully on in-progress work without requiring a commit first.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Deployer role:
- Add servicediscovery actions; module always creates Cloud Map namespace
and service so the policy must grant CreatePrivateDnsNamespace etc.
- Make Route53 + ACM permissions unconditional. The server module always
issues an ACM cert and writes Route53 records (no CloudFront default-cert
path exists), so gating these on route53_zone_ids was broken. Split
Route53 into hosted-zone management (always) plus record-set mutation
(scoped to caller-supplied zones, falls back to *).
- Remove unused cloudfront:* statement; no CloudFront resources in module.
- Replace acm:* wildcard with explicit cert-management action set.
Server module:
- qdrant_image_tag is now nullable with default null and validated against
use_external_qdrant, so external-qdrant callers can omit it instead of
passing a sentinel "unused" value.
- task_role_arn and efs_id outputs marked sensitive; qdrant_dns_name returns
null when use_external_qdrant = true.
- ALB SG now has matching IPv6 egress rule (was v4-only).
- nextcloud_url validates the https:// scheme.
- random_pet.subdomain keeper includes zone_name so a zone migration that
preserves zone_id still triggers regeneration.
- Pin required_version >= 1.9 on both modules.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
- 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>
POC: lift the homelab-grown nextcloud-mcp-server and
nextcloud-mcp-deployer-role Terraform modules into this repo so external
operators can consume them via a `git::` source.
Includes a top-level README documenting the two-phase deploy flow
(bootstrap deployer role with a copy-pasteable IAM policy, then assume the
role to deploy the MCP server) and supports both in-VPC Qdrant and
external/managed Qdrant modes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>
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>
- 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>
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>
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>
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>
- 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>
- _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>
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>
- 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>
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>
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>
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>
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>
Address four small items from the latest PR #743 review:
- login-flow-v2.md Compose example: add an inline comment +
follow-up note pointing readers at Docker secrets for
TOKEN_ENCRYPTION_KEY (the snippet is likely to be copy-pasted
into production).
- auth-flows.md: rename the third column in the Astrolabe → MCP
Server diagram from "Nextcloud OIDC" to "OIDC Provider" so the
diagram matches the multi-IdP framing in the surrounding prose.
- login-flow-v2.md OAuth Endpoints section: rewrite the
ambiguous "token issuance still comes from the IdP" line to
make the cryptographic separation explicit — the MCP server
exposes /token, but tokens are signed by the IdP's key and
validated against its JWKS; the MCP server has no signing keys
of its own.
- README.md auth bullet: replace the jargony "OAuth-to-MCP
supported, with app-password conversion to Nextcloud" with the
reviewer's clearer wording: "MCP clients authenticate via
OAuth, the server handles Nextcloud app passwords
transparently".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>
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>
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>
The docs claimed scopes are mcp:-prefixed (mcp:notes.read,
mcp:notes.write) and that the notes.* pair "covers all Nextcloud
apps". Both are false. Per @require_scopes decorators across
nextcloud_mcp_server/server/, scopes are unprefixed and per-app:
notes.read/write, talk.read/write, files.read/write,
calendar.read/write, contacts.read/write, deck.read/write,
news.read, tables.read/write, cookbook.read/write,
todo.read/write, collectives.read/write, sharing.write,
semantic.read, plus standard OIDC scopes.
Changes:
- login-flow-v2.md: replace the false 2-row "covers all apps"
scope table with the real per-app reference (links to
scope_authorization.discover_all_scopes() as authoritative
source); strip mcp: prefix from intro paragraph, sequence
diagrams, @require_scopes example, WWW-Authenticate header
example. Also fix sticky-session keying advice per reviewer:
route on user identity (sub claim) rather than the raw bearer
token, since tokens rotate on refresh.
- auth-flows.md: clarify "Astrolabe (hosted UI) → MCP" matrix
column header; strip mcp: from sequence diagram and key
characteristics bullet; correct "issued by MCP server" to
"issued by configured IdP" on the Login Flow v2 token.
- authentication.md: strip mcp: from the high-level diagram and
scope-enforcement prose; cross-link to the scope reference.
- configuration.md: add NEXTCLOUD_OIDC_CLIENT_ID,
NEXTCLOUD_OIDC_CLIENT_SECRET, and OIDC_DISCOVERY_URL to the
Login Flow v2 vars table — these were undocumented in the
table after the round-2 multi-IdP fix.
- running.md: drop deprecated `version: '3.8'` from compose
snippets (Compose v2 ignores it and emits warnings).
- testing-oidc-consent.md: fix sample authorize URL and consent
description to use real scope names instead of mcp:-prefixed
ones (the manual test as written would have failed with
invalid_scope).
- CLAUDE.md: replace dead links to deleted oauth-architecture.md,
oauth-setup.md, and audience-validation-setup.md with
login-flow-v2.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous round narrowed the framing too far in the other direction —
made it sound like Nextcloud OIDC is *the* IdP. The MCP server
actually supports any OIDC-compliant provider (Nextcloud's built-in
OIDC, Keycloak, AWS Cognito, Auth0, etc.) selected via
`OIDC_DISCOVERY_URL`. `NEXTCLOUD_OIDC_CLIENT_ID/SECRET` are generic
OIDC client credentials despite the Nextcloud-flavored naming.
Code references:
- IdP discovery: app.py:607-668 (auto-detects integrated vs external
by comparing discovered issuer to NEXTCLOUD_HOST)
- JWKS: unified_verifier.py:71-73 (dynamically discovered, not
hard-coded to Nextcloud)
- IdP selection knob: OIDC_DISCOVERY_URL (config.py)
Changes:
- login-flow-v2.md: redraw "How It Works" diagram to show the IdP as
a separate component; replace "Nextcloud OIDC" with "configurable
IdP" framing throughout; add OIDC_DISCOVERY_URL to the env-var
reference; clarify NEXTCLOUD_OIDC_CLIENT_ID/SECRET are generic OIDC
creds; rename "OAuth Endpoints" subtitle to point at "the configured
IdP".
- running.md: rewrite the OAuth Mode intro and Quick Start note to
mention IdP configurability and OIDC_DISCOVERY_URL.
- configuration.md: update Best Practices "For Production" multi-user
bullet to reference the IdP selector and generic-creds caveat.
- auth-flows.md: generalize Astrolabe-flow and Login Flow v2
characteristics bullets — IdP and JWKS source are configurable.
- keycloak-multi-client-validation.md: REMOVE the "deprecated"
banner I added in 35c115e. The doc covers active behavior in
external-IdP mode (realm-level token validation by user_oidc),
not retired direct-OAuth-to-Nextcloud architecture. Replaced with
a scope note pointing at when this applies.
oauth-impersonation-findings.md keeps its deprecation banner — that
doc *is* about the rejected service-account / impersonation path
(ADR-002 Tier 2, "Will Not Implement"), so the deprecation framing
remains correct there.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous round of review feedback rested on a misunderstanding —
that the MCP server is "the OAuth issuer" under Login Flow v2 and that
NEXTCLOUD_OIDC_CLIENT_ID/SECRET are external-IdP-only. Code says
otherwise (app.py:619/625/703-717, unified_verifier.py:72):
- The MCP server is an OIDC relying party of Nextcloud OIDC. Tokens are
signed by Nextcloud and validated against Nextcloud's JWKS in all
modes — the server has no private signing keys.
- Static NEXTCLOUD_OIDC_CLIENT_ID/SECRET are the preferred way to
register the MCP server as that relying party; RFC 7591 DCR is a
fallback when both are unset.
- Login Flow v2 layers per-user app-password acquisition on top — it
governs the MCP→Nextcloud data leg, not the relying-party setup.
This commit reverts the inaccuracies introduced by 35c115e and reframes
the original `login-flow-v2.md` to match what the code does:
- login-flow-v2.md: revise "How It Works" to describe the MCP server
as an OIDC RP + OAuth facade (not a standalone issuer); rename
"OAuth Issuer Endpoints" → "OAuth Endpoints" with a note that those
endpoints front Nextcloud OIDC; add NEXTCLOUD_OIDC_CLIENT_ID/SECRET
to the required env vars with DCR documented as fallback.
- running.md: restore the static-creds Docker example (deleted in
35c115e on the wrong reasoning that it was tied to the retired
direct-OAuth-to-Nextcloud flow); rewrite the OAuth Mode section
intro to describe the actual relying-party + facade architecture.
- configuration.md: fix Best Practices "For Production" to mention
static creds as preferred / DCR as fallback; restore the .oauth
Docker volume alongside data so DCR-registered MCP-client state and
the encrypted app-password DB both persist.
- auth-flows.md: drop the note added in 35c115e that wrongly claimed
the MCP server validates Bearer tokens against its own JWKS under
Login Flow v2 — it validates against Nextcloud's JWKS in all modes;
reword the Login Flow v2 "Key characteristics" bullet that called
the MCP server "the OAuth authorization server".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>