Commit Graph
2599 Commits
Author SHA1 Message Date
Chris CoutinhoandGitHub ddd6be6c16 Merge pull request #827 from cbcoutinho/fix/gateway-v1-base-path
fix(embedding): normalize gateway base_url to the /v1 base path
2026-06-01 17:55:48 +02:00
Chris CoutinhoandClaude Opus 4.8 4f170d32cb fix(embedding): normalize gateway base_url to the /v1 base path
EMBEDDING_GATEWAY_URL is configured as a bare origin (scheme://host:port) —
the deployment's Service URL. GatewayProvider now appends the gateway's /v1
base path before handing the URL to the OpenAI SDK, so both embed posts
({base}/embeddings) and dimension discovery ({base}/models) land under /v1.
Idempotent: a URL already ending in /v1 is left unchanged.

This lets EMBEDDING_GATEWAY_URL stay a bare domain (matching the gitops
Service URLs) instead of requiring a hand-appended /v1.

Also align the `embedding_gateway_model` field default with _DEFAULTS
("mistral/mistral-embed"). The gateway catalog is provider-namespaced, and
_detect_dimension matches `entry.id == embedding_model`; the stale
un-namespaced default would silently miss the catalog entry and leave the
dimension unresolved (re-triggering the external-mode startup crash).

Tests: bare / trailing-slash / idempotent normalization + a bare-origin
discovery test asserting /v1/models. 16 gateway-provider tests pass;
providers + vector suites green (129 total); ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 17:39:23 +02:00
github-actions[bot] 2d191b4055 bump: version 0.91.3 → 0.92.0 2026-06-01 14:10:34 +00:00
Chris CoutinhoandGitHub 469a84df4b Merge pull request #823 from cbcoutinho/feat/gateway-dimension-discovery
feat(embedding): gateway provider discovers dimension via GET /v1/models
2026-06-01 16:10:04 +02:00
github-actions[bot] 875f756f6e bump: version 0.91.2 → 0.91.3 2026-06-01 14:06:34 +00:00
Chris CoutinhoandGitHub e6d7a705c6 Merge pull request #825 from cbcoutinho/fix/provision-app-password-ocs-nondict-824
fix(api): return 401 not 500 on failed app-password OCS validation (#824)
2026-06-01 16:06:08 +02:00
Chris CoutinhoandClaude Opus 4.8 be360edab0 fix(api): distinguish Nextcloud 5xx from auth failure; tighten body parse (#824)
Address PR #825 review round 2:
- _validate_nextcloud_credentials now only maps OCS HTTP 401/403 to a 401
  "invalid credential"; any other non-200 (5xx, 503 maintenance mode) surfaces
  as 502 "Nextcloud returned a server error" so ops don't chase a phantom bad
  password when Nextcloud is actually down.
- The client-facing 401 message is now a parameter, so delete_app_password keeps
  its "Invalid credentials" wording without unwrapping/rebuilding the helper's
  JSONResponse.
- Body parsing catches (ValueError, UnicodeDecodeError) instead of bare
  Exception, and guards body.get behind isinstance(body, dict) — no longer
  swallows RuntimeError/AttributeError or a non-object JSON body.
- Add a unit test asserting 500/503 -> 502.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 15:15:48 +02:00
Chris CoutinhoandClaude Opus 4.8 3c55d03ecf fix(api): block cross-user delete and address review feedback (#824)
Review of PR #825 surfaced an auth bypass introduced by adding loginName
support to delete_app_password: with the OCS-resolved UID discarded, a user
could authenticate as their own loginName (via the request body) while
targeting another user's path and delete the victim's stored app password.
Add the same UID-mismatch guard provisioning already has, so the
authenticated account must own the path UID (403 otherwise).

Also:
- integration test: build the BasicAuth header via base64 instead of
  httpx.BasicAuth._auth_header (private attribute); mark the throwaway test
  credential NOSONAR(S2068).
- unit tests: cover the httpx.RequestError -> 502 branch, the standard OCS v2
  success shape (meta.statuscode 200), and the cross-user delete 403 guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 13:04:42 +02:00
Chris CoutinhoandClaude Opus 4.8 a11a4e709c fix(api): return 401 not 500 on failed app-password OCS validation (#824)
provision_app_password validated credentials against OCS v1
(/ocs/v1.php/cloud/user), which always returns HTTP 200 — even on auth
failure, where the real status lives in ocs.meta.statuscode (997) and
ocs.data comes back as an empty list []. The status_code != 200 guard
therefore never fired, execution fell through to [].get("id"), and the
resulting AttributeError escaped as an unhandled 500. This blocked
background vector indexing for any user whose supplied loginName didn't
resolve (e.g. display name "Admin" vs loginName "admin").

Extract a shared _validate_nextcloud_credentials helper that:
- queries OCS v2 (/ocs/v2.php), which maps the OCS status onto the HTTP
  status, so a failed credential is a real 401;
- parses the payload defensively (isinstance guards) so a non-dict
  ocs.data can never raise;
- returns a clean 502 for an unreachable Nextcloud or a non-JSON body.

delete_app_password shared the same v1.php dead-guard bug, which made its
credential check a no-op (any valid-format password passed) — an auth
bypass on deletion. Route it through the same helper and accept the
loginName from the request body (mirroring provisioning) so OIDC users
whose UID differs from their loginName are not regressed.

Adds unit regression tests for the OCS failure payload, non-dict data,
and non-JSON response, plus a login-flow integration test that provisions
with capitalized ("Admin") and spaced ("Test User") loginNames and asserts
a 401 rather than a 500.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 12:56:44 +02:00
Chris Coutinho d84a95842f feat(embedding): gateway provider discovers dimension via GET /v1/models
External-mode tenant pods CrashLoop at startup: Qdrant collection init calls
get_dimension() before any embed(), but GatewayProvider only learns its
dimension lazily after the first embed, and the gateway model isn't an OpenAI
model so the base class can't know it statically.

- Add GatewayProvider._detect_dimension() — the async startup hook the
  vector-sync bootstrap already invokes (vector/qdrant_client.py:
  hasattr(provider, "_detect_dimension")) for Ollama. It GETs the gateway's
  GET /v1/models and sets _dimension from the entry whose id matches the
  configured model. Best-effort: any failure (old gateway, model absent,
  network) leaves _dimension unset so the inherited lazy detect-on-first-embed
  still applies — never fatal. Presents the M2M bearer when configured.
- Switch the default embedding_gateway_model to the gateway's provider-
  namespaced id "mistral/mistral-embed" (the gateway routes on the "/"-prefix
  and sends "mistral-embed" upstream); collapse a duplicated config field.

Pairs with astrolabe-cloud-website#229 (gateway /v1/models, namespaced ids).

Tests: discovery sets dim w/o embed, sends bearer, non-fatal on
404/absent/error, skips when already known.
2026-05-31 23:43:19 +02:00
github-actions[bot] c662d57c4f bump: version 0.91.1 → 0.91.2 2026-05-31 20:17:56 +00:00
Chris CoutinhoandGitHub c9c0a1d5d4 Merge pull request #821 from cbcoutinho/fix/app-password-loginname-runtime-auth
fix(auth): authenticate stored app passwords with loginName, not UID
2026-05-31 22:17:34 +02:00
github-actions[bot] 425ea5e0e8 bump: version 0.91.0 → 0.91.1 2026-05-31 20:16:46 +00:00
Chris CoutinhoandGitHub fe0b993174 Merge pull request #822 from cbcoutinho/fix/nats-status-subscriber-resilience
fix(vector): make NATS status subscriber resilient at startup
2026-05-31 22:16:25 +02:00
Chris CoutinhoandClaude Opus 4.8 cd2145df09 fix(vector): make NATS status subscriber resilient at startup
Follow-up to PR #814 review.

NatsStatusSubscriber.run() called task_status.started() *after* the fallible
pull_subscribe, so a NATS broker that wasn't ready when the MCP server started
would crash the lifespan instead of retrying. Bus status is a non-critical
observability path, so:

- signal started() before the first subscribe (semantics: "loop is running",
  not "subscription succeeded");
- retry a failed subscribe with backoff instead of propagating;
- on a real fetch error (not an idle timeout) drop the subscription and
  re-subscribe rather than fetching against a possibly-dead handle.

Also anchor the _content_hash etag-threading TODO to the PR #814 review thread
so it is discoverable outside git blame.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 21:56:12 +02:00
github-actions[bot] f65ebfc65b bump: version 0.90.2 → 0.91.0 2026-05-31 19:48:13 +00:00
Chris CoutinhoandGitHub 3bdacf4ed3 Merge pull request #814 from cbcoutinho/feat/decomp-hook-points
feat: add opt-in MCP decomposition hook points (design §10)
2026-05-31 21:47:52 +02:00
Chris CoutinhoandClaude Opus 4.8 874c7c109b fix: stop S7632 flagging NOSONAR mentioned in prose comments
SonarCloud's python:S7632 parses the literal ``# NOSONAR`` token wherever it
appears — including inside explanatory comments that *quote* the directive —
and treats the following text as a malformed suppression. The actual bare
``# NOSONAR`` suppression lines are fine; the flagged lines were the prose
comments describing them. Reword those comments to drop the inner ``#`` so the
analyzer no longer sees a directive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 21:38:14 +02:00
Chris CoutinhoandClaude Opus 4.8 52da297ded fix(auth): authenticate stored app passwords with loginName, not UID
Nextcloud authenticates app passwords against the *loginName*, which differs
from the UID for OIDC-provisioned users (e.g. user_oidc makes the UID the
display name: UID "Ada Lovelace", loginName "ada@example.com"). The runtime
consumers of stored app passwords bound the UID as the BasicAuth username, so
every Notes/Files/Shares/CalDAV call returned HTTP 401.

PR #818 fixed only the provisioning endpoint; the consuming paths were missed.
Observed on a login_flow tenant (NC's own OIDC app as IdP): the background-sync
scan loop never started ("Credential validation failed ... HTTP 401") and
semantic search returned 0 results because the ACL shared_with_me lookup 401'd
and degraded to a self-only owner filter.

Root cause: NextcloudClient / CalendarClient conflated two identities — the
DAV/URL path identity (the user_id the whole system keys on = NC UID) and the
auth-credential username (the loginName). Decouple them:

- Thread a keyword-only auth_username through NextcloudClient -> CalendarClient
  (defaults to username, so single-user / OAuth where UID == loginName is
  unchanged).
- get_user_client_basic_auth (background sync + the /api/v1/vector-viz/search
  endpoint) authenticates as the stored loginName, UID for paths.
- _get_client_from_login_flow (the get_client(ctx) MCP-tool path) does the same.
- cleanup_invalid_app_passwords validates with the loginName, so it no longer
  401s and wrongly deletes a valid OIDC user's password.

The loginName is already persisted in app_passwords.username and returned by
get_app_password_with_scopes. Adds unit tests covering the UID != loginName
split for both client builders, the calendar credential/path split, and the
cleanup validation. Also genericises the example user in the #818 comment/test
(real name/email -> Ada Lovelace / ada@example.com).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 21:34:05 +02:00
Chris Coutinho 1528a1248d Merge remote-tracking branch 'origin/master' into feat/decomp-hook-points
# Conflicts:
#	nextcloud_mcp_server/vector/scanner.py
2026-05-31 20:10:37 +02:00
Chris CoutinhoandClaude Opus 4.8 ccfe179a38 fix: well-form NOSONAR suppressions (SonarCloud S7632/S7503)
The new decomposition modules used `# NOSONAR: reason` (colon form), which
SonarCloud flags as a malformed suppression comment (python:S7632) and which
fails to suppress the intended issue. Switch to the repo's bare `# NOSONAR`
convention with the rationale in a comment above, matching config.py and
auth/storage.py. This also lets the suppression silence python:S7503 (async
method without await) on the protocol-required no-op aclose stubs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 20:04:44 +02:00
Chris CoutinhoandClaude Opus 4.8 2d845cb70f fix: address PR #814 reviewer follow-ups
- gateway_client: guard token cache with a lazy anyio.Lock so concurrent
  embed calls share one M2M token request instead of racing
- status subscriber: distinguish idle fetch timeouts from real broker
  errors (log + 5s backoff) instead of swallowing all and spinning
- nats: warn when the bus URL uses unencrypted transport (non-tls://)
- collection_metadata: accept an optional shared httpx client, make TLS
  verify explicit, document the unauthenticated control-plane contract
- replace python -O-stripped asserts with explicit ValueError in the bus
  status builder and the api metadata source
- document why the nil-UUID sentinel point can't collide with content ids

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 19:42:09 +02:00
Chris CoutinhoandGitHub e7512d210c Merge pull request #815 from cbcoutinho/renovate/docker.io-library-nextcloud-32.x
chore(deps): update docker.io/library/nextcloud docker tag to v32.0.10
2026-05-31 13:42:09 +02:00
Chris CoutinhoandGitHub 580ff088e1 Merge pull request #816 from cbcoutinho/renovate/nextcloud-32-32.x
chore(deps): update nextcloud-32 docker tag to v32.0.10
2026-05-31 13:41:52 +02:00
Chris CoutinhoandGitHub 0b5614c534 Merge pull request #817 from cbcoutinho/renovate/nextcloud-33-33.x
chore(deps): update nextcloud-33 docker tag to v33.0.4
2026-05-31 13:41:40 +02:00
Chris CoutinhoandGitHub c28c48d298 Merge pull request #820 from cbcoutinho/test/astrolabe-from-appstore
test(astrolabe): install app from Nextcloud app store; bump submodule to v0.16.6
2026-05-31 12:13:53 +02:00
Chris CoutinhoandClaude Opus 4.8 fdc0bdeae4 test(astrolabe): install app from Nextcloud app store; bump submodule to v0.16.6
Stop mounting the vendored astrolabe submodule into the Nextcloud `app`
container by default (comment out the /opt/apps/astrolabe bind mount). With
the mount absent, the post-installation hook (20-install-astrolabe-app.sh)
falls through to `occ app:install astrolabe` + `app:enable`, so the dev/CI
stack now exercises the published app-store package rather than a locally
built dev copy. This catches packaging issues (e.g. missing built assets in
the released app) that a source build would mask.

Bump the third_party/astrolabe submodule to v0.16.6, which includes the
background-indexing re-login fix (astrolabe#93).

The dev mount and the CI "Build Astrolabe app" step are retained (commented
mount can be re-enabled locally) so developers can still iterate against the
vendored source on demand.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 12:04:54 +02:00
github-actions[bot] 0caf2cef8e bump: version 0.90.1 → 0.90.2 2026-05-30 12:41:59 +00:00
Chris CoutinhoandGitHub 9c6ff972f9 Merge pull request #819 from cbcoutinho/fix/vector-sync-isolate-notes-scan
fix(vector-sync): isolate per-app scans so a disabled Notes app can't abort sync
2026-05-30 14:41:40 +02:00
Chris CoutinhoandClaude Opus 4.8 7f40f7fdb3 fix(vector-sync): isolate per-app scans so a disabled Notes app can't abort sync
The Notes scan in scan_user_documents ran inline without a try/except, while
files/news/deck each had their own guard. On instances without the Notes app
installed, notes.get_all_notes() raises HTTPStatusError 404, which propagated
out of scan_user_documents and aborted the entire per-user vector sync before
files/news/deck were ever reached -- yielding "0 documents indexed" and, after
5 consecutive errors, stopping the scanner.

Extract the Notes scan into scan_notes() (mirroring scan_news_items /
scan_deck_cards) and wrap the call in a per-app try/except. A 404 (app not
installed/disabled) is now logged at info and skipped; other apps still scan.
Deletion-tracking runs only after a successful Notes fetch, so a failed fetch
can never mass-delete a user's indexed notes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 14:34:25 +02:00
github-actions[bot] 541abe19f6 bump: version 0.90.0 → 0.90.1 2026-05-30 09:55:32 +00:00
Chris CoutinhoandGitHub f17bd79546 Merge pull request #818 from cbcoutinho/fix/app-password-loginname-validation
fix(api): validate app password against Nextcloud using loginName, not UID
2026-05-30 11:55:14 +02:00
Chris CoutinhoandClaude Opus 4.8 0df3fb4a19 fix(api): validate app password against Nextcloud using loginName, not UID
provision_app_password validated the supplied app password by calling the
OCS cloud/user endpoint with BasicAuth as the *path user_id* (the UID).
Nextcloud keys app-password BasicAuth on the loginName, which differs from
the UID for OIDC-provisioned accounts whose UID is their display name
(UID "Chris Coutinho", loginName "chris@coutinho.io"). Authenticating as
the UID is rejected with HTTP 401 ("App password validation failed"), so
provisioning never completes.

Parse the request body up front and authenticate the OCS validation as the
body's `username` (the Nextcloud loginName), falling back to the path
user_id for legacy callers where UID == loginName. The OCS-returned account
id is still checked against the path user_id (the UID), and the password is
still stored keyed by UID with the loginName alongside.

Note this is not an encoding issue: BasicAuth places the user-id literally
in the header (RFC 7617, no URL-encoding); %20/+/literal-space forms of the
UID all fail — only the loginName authenticates.

Adds a regression test asserting the OCS BasicAuth uses the loginName while
storage is keyed by the UID, plus a backward-compat assertion that callers
without a loginName fall back to the UID.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 11:50:19 +02:00
renovate-bot-cbcoutinho[bot]andGitHub 076ec91981 chore(deps): update nextcloud-33 docker tag to v33.0.4 2026-05-30 04:29:01 +00:00
renovate-bot-cbcoutinho[bot]andGitHub f00b0b8e19 chore(deps): update nextcloud-32 docker tag to v32.0.10 2026-05-30 04:28:54 +00:00
renovate-bot-cbcoutinho[bot]andGitHub fbdf49c309 chore(deps): update docker.io/library/nextcloud docker tag to v32.0.10 2026-05-30 04:28:30 +00:00
Chris CoutinhoandClaude Opus 4.8 b5ed1e3b4d fix: address PR #814 review + SonarCloud gate
SonarCloud:
- Resolve 6 S5332 hotspots (http→https in test fixture URLs).
- S6418: hoist the unauthenticated AsyncOpenAI placeholder to a named constant
  + NOSONAR (genuine non-secret; gateway ignores it when unauthenticated).
- Fix two reliability bugs: None-index guard in the gateway token-cache test
  (S2259) and float `> 0.0` instead of `!= 0.0` in the sentinel test (S1244).
- status.py idle path sleeps 0.1s instead of sleep(0) (S7491); NOSONAR on the
  protocol-required async no-await aclose() stubs (S7503).

Claude review:
- Remove three leftover debug print() calls in app.py (logger.info already
  covers them).
- payload_backfill: drop parsed_at from the backfilled-keys docstring (it is
  per-document state, not a deployment scalar); add a clean 404 precondition
  for BasicAuth deployments without an OAuth token verifier.
- status.py: task_status typed TaskStatus | None (drop type: ignore).
- nats.py: TODO to thread etags for file/deck/news; note etag default → None.
- factory: warn on unknown INGEST_BUS_URL scheme; raise ValueError instead of
  assert for the external-mode preconditions.
- docs/configuration.md: document the decomposition hook-point env vars + that
  nats-py ships core (lazy-imported) and external+bus uses two NATS connections.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 18:36:42 +02:00
Chris Coutinho a92f6260fb Merge remote-tracking branch 'origin/master' into feat/decomp-hook-points
# Conflicts:
#	nextcloud_mcp_server/vector/scanner.py
2026-05-29 18:31:05 +02:00
github-actions[bot] 76a8313717 bump: version 0.89.0 → 0.90.0 2026-05-29 16:01:53 +00:00
Chris CoutinhoandGitHub e1340f5a40 Merge pull request #813 from cbcoutinho/feat/acl-aware-vector-index
feat(search): ACL-aware vector filter via Nextcloud Shares
2026-05-29 18:01:24 +02:00
Chris CoutinhoandClaude Opus 4.8 29500bea7e fix: PR #813 review — cap unified_search multi-type pool; document deck self-only
🟡 Performance: unified_search's _execute sorted but did not cap the merged
multi-doc_type pool, so N doc_types each fetched at search_limit sent
N*search_limit candidates into verify-on-read (one Nextcloud round-trip each).
Cap to search_limit*2 after the sort, matching vector_search, nc_semantic_search
and the viz_routes pattern — bounding verification cost to O(2*search_limit)
regardless of how many doc_types are requested.

🟡 Consistency: _get_deck_metadata_from_qdrant is the one internal Qdrant lookup
that uses a raw user_id filter instead of build_ownership_filter. This is not a
bug — deck cards are a documented cross-user gap (the Deck API is per-user, so
cross-user context can't be fetched with the caller's credentials) — but the
inconsistency was unexplained. Added a comment documenting the deliberate
self-only scope.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 17:49:21 +02:00
Chris CoutinhoandClaude Opus 4.8 063bd06ba6 fix: cache app-password storage to avoid per-request Alembic upgrade race
get_user_client_basic_auth built a fresh RefreshTokenStorage and ran
storage.initialize() — a full Alembic `upgrade` in a worker thread — on every
call. Once the /api/v1 search endpoints (unified_search, vector_search) and the
chunk-context endpoint were wired to use it, concurrent requests ran concurrent
Alembic upgrades, which race on Alembic's non-thread-safe module-global
EnvironmentContext proxy and intermittently raise `KeyError: 'script'` →
HTTP 500 (seen on multi-user-basic/nc31; nc32 got lucky).

Cache one process-wide, already-initialized storage instance behind a lazily
created anyio.Lock so the one-time migration runs exactly once and never races.
Callers passing an explicit `storage` are unaffected. Also removes a redundant
per-request migration from the search hot path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 17:37:36 +02:00
Chris CoutinhoandClaude Opus 4.8 350358b802 fix: PR #813 review — shared-file context in MCP tool path + viz over-fetch cap
🟡 Important: nc_semantic_search's include_context branch did not forward
accessible_owners to get_chunk_with_context, so context expansion for shared
files stayed self-only, found nothing in Qdrant, and silently fell back to the
plain excerpt. Forward accessible_owners (the per-file file_accessible_by_id
gate still enforces access).

🟡 Performance: auth/viz_routes.py's multi-doc_type branch sorted but did not
cap the candidate pool before verify-on-read, so N doc_types × limit*2 went
into verification (N× the Nextcloud round-trips). Cap to limit*2 after the
sort, matching server/semantic.py and the cross-app branch.

Also clear the SonarCloud gate (new_duplicated_lines_density 5.1% > 3%) the
ACL wiring introduced: extract the duplicated /api/v1 client-resolution +
owner-expansion + verify-on-read block from unified_search/vector_search into a
shared _search_with_acl helper, define a constant for the repeated
"Nextcloud host not configured" literal (S1192), and reword the access_filter
move_to_end comment so it isn't misread as commented-out code (S125) while
adding the other-owner count to its debug log (review nits).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 17:16:23 +02:00
Chris CoutinhoandClaude Opus 4.8 8deb48e6fa fix: address PR #813 review round 4 (log leak, cross-user chunk ctx, algo, overlap)
1. Don't log unverified result titles: both search algorithms logged top-5
   titles at DEBUG before verify-on-read; with owner-level share expansion the
   unverified set can contain other users' docs. Algorithms now log a count
   only; the verifying callers (server/semantic, viz_routes, api/visualization)
   log verified titles after verify-on-read.

2. Cross-user FILE chunk context: get_chunk_with_context + the Qdrant chunk
   helpers now take accessible_owners and use build_ownership_filter. For files
   the expanded scope is honoured only after a per-file file_accessible_by_id
   check (accessible_owners is owner-level, so the gate prevents a one-file
   share recipient from reading any of the owner's cached chunks). note/deck/
   news stay self-only (per-user APIs) — a documented gap. Both chunk endpoints
   pass accessible_owners.

3. Algorithm usage: SemanticSearchAlgorithm is not dead (it backs the dense-only
   option on the viz/API surfaces); added a clarifying comment in server/
   semantic.py. Additionally wired accessible_owners + verify-on-read into the
   /api/v1 search routes (unified_search, vector_search) so the astrolabe
   surface is ACL-aware too — degrading gracefully to self-only/unverified for
   non-provisioned callers instead of 401.

4. Overlapping conditions: build_ownership_filter no longer lists self in the
   owner_id MatchAny branch (self is already covered by the user_id branch);
   the owner_id branch carries only the OTHER owners.

Tests: build_ownership_filter dedup + chunk-bbox filter-shape updates; new
ACL-aware get_indexed_doc_types, cached-chunk lookup, and end-to-end cross-user
file chunk-context (recipient gets the chunk, non-recipient denied) tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 16:55:00 +02:00
Chris CoutinhoandClaude Opus 4.8 ac041d5c97 chore: bump astrolabe submodule to v0.16.1 (PR #89 merged)
PR #89 (session-derived JWT auth) merged to astrolabe main and released as
v0.16.1; track the released tag instead of the feature-branch commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 14:57:37 +02:00
Chris CoutinhoandClaude Opus 4.8 423d0a1758 fix: address PR #813 latest review (ACL-aware doc-type discovery, robustness)
- get_indexed_doc_types: add optional accessible_owners param and reuse
  build_ownership_filter so cross-user doc-type discovery matches the real
  search scope (was self-only / ACL-blind); docstring documents the self-only
  default. Covered by test_get_indexed_doc_types_is_acl_aware.
- access_filter: build_ownership_filter now omits the owner_id branch entirely
  for an empty owner set instead of relying on undocumented MatchAny(any=[])
  semantics; updated the empty-list unit test accordingly.
- access_filter: make the uid_owner/owner share-owner extraction explicit
  ("absent, not empty") to avoid skipping on a falsy-but-present field.
- access_filter: add an operator note that pre-owner_id points need a re-index
  to surface to share recipients (ACL search is a no-op for legacy data).
- verification/webdav: lock the file_accessible_by_id(scope="") contract with a
  targeted multi-user test (owner + recipient True, non-recipient False).
- viz_routes: comment that verify-on-read eviction runs inline by design (no
  lifespan task group available on the Starlette route).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 14:57:34 +02:00
Chris CoutinhoandClaude Opus 4.8 531228d407 chore: bump astrolabe submodule (cleartext-transport warn, not block)
Fixes the multi-user-basic regression introduced by the previous bump: the
astrolabe app's hard https-refusal blocked sending the app password to the
in-cluster http MCP endpoint (http://mcp-multi-user-basic:8000), so background
indexing never ran. Now warns instead of blocking; also clears the SonarCloud
S5332 hotspot on the dropped test's http literal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 13:49:21 +02:00
Chris CoutinhoandClaude Opus 4.8 cafbfd15a9 fix: address PR #813 review (owner_id index, cache bound, explicit param)
- vector/qdrant_client.py: add owner_id to _PAYLOAD_INDEX_FIELDS (BLOCKING).
  Every search applies MatchAny(key="owner_id", ...); without a keyword index
  Qdrant full-scans the collection and may 400 on Qdrant Cloud strict mode.
  _ensure_payload_indexes is idempotent so existing collections migrate at
  startup.
- search/access_filter.py: bound the process-global _owners_cache with an LRU
  cap (was one unbounded entry per active user, never evicted); document the
  owner-level over-fetch limitation (a prolific sharer floods the recall
  buffer with ghost candidates that verify-on-read drops, with no second
  Qdrant pass) as a TODO toward per-file filtering.
- search/algorithms.py + semantic.py + bm25_hybrid.py: promote
  accessible_owners from **kwargs to an explicit keyword-only parameter on the
  SearchAlgorithm ABC and both implementations, so a misspelled keyword is a
  type error rather than a silent fall back to self-only scope.
- search/verification.py: document that _verify_files now verifies by global
  file id (WebDAV SEARCH), not by path.
- tests/unit/search/test_access_filter.py: add cache-hit, TTL-expiry,
  failure-not-cached, and LRU-bound tests.

Bumps the astrolabe submodule with the matching #89 review fixes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 13:28:45 +02:00
Chris CoutinhoandClaude Opus 4.8 d883052fb8 feat: add opt-in MCP decomposition hook points (design §10)
Adds the seven §10.2 hook-point modules + five env vars so Astrolabe Cloud can
offload document processing to the external document-processor / embedding
gateway. Purely additive: with every setting unset the server behaves exactly
as today, so self-hosters are unaffected (Deck #92).

Hook points (all default to current monolith behavior):
- config: EMBEDDING_PROVIDER, INGEST_MODE, STATUS_BACKEND,
  COLLECTION_METADATA_SOURCE, FACT_EVENT_EMITTER (+ supporting settings),
  validated in Settings.__post_init__ (fail-fast STATUS_BACKEND=local with
  INGEST_MODE=external); shared canonical.py.
- vector/payload_keys.py + acl_hash.py: cross-impl NAMESPACE/point_id (§2.2)
  and BLAKE2b-128 ACL hash (§11), pinned by fixtures shared with the
  document-processor repo.
- embedding/gateway_client.py: OpenAI-compatible GatewayProvider authenticating
  via M2M OIDC client-credentials (separate realm); manual-only registry entry.
- vector/collection_metadata.py: sentinel-point / API metadata source with env
  fallback.
- vector/queue/: hexagonal ingest producer ports + memory/NATS adapters
  (Postgres seam); INGEST_MODE=external publishes mcp.ingest.requested.{tenant}
  instead of the in-memory stream and skips the in-process processor pool. The
  lifespan becomes a composition root across both deployment branches.
- vector/queue/status.py: STATUS_BACKEND=bus subscriber feeding a StatusStore
  the vector-sync status endpoint reads.
- admin/payload_backfill.py: POST /api/v1/admin/payload-backfill (admin scope);
  processor writes the new payload keys; query-side ACL pre-filter gated behind
  ACL_PREFILTER_ENABLED (default off).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 13:13:25 +02:00
Chris CoutinhoandClaude Opus 4.8 8f0955cfc9 chore: bump astrolabe submodule (SonarCloud test-fixture rename)
Tracks astrolabe feat/session-derived-jwt-auth tip (86616d9). Test-only
change (rename a unit-test constant to clear SonarCloud S2068); no functional
or runtime difference from the previously-verified submodule commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 03:01:10 +02:00