Commit Graph
183 Commits
Author SHA1 Message Date
Chris Coutinho 40e56aeaea build: Bump astrolabe submodule 2026-06-06 09:34:30 +02:00
Chris CoutinhoandClaude Opus 4.8 531607a1a1 fix(tests): repair multi-user-basic Astrolabe integration suite
The Astrolabe PHP→Vue settings refactor dropped three stable element ids
(#mcp-enable-background-button, #mcp-revoke-background-button,
#mcp-revoke-background-form) that the multi-user-basic integration suite
drives the background-sync enable/disable/revoke flows through. Their
absence timed out the 5s Playwright locators and failed four tests:

- test_astrolabe_multi_user_background_sync::test_multi_user_astrolabe_background_sync_enablement
- test_astrolabe_multi_user_background_sync::test_revoke_background_sync_access
- test_astrolabe_chunk_context::test_chunk_context_endpoint_uses_app_password
- test_astrolabe_plotly_visualization::test_astrolabe_plotly_visualization_with_basic_auth

(the latter two enable background sync via complete_astrolabe_authorization
before exercising the app-password / indexed-search paths).

Two-part fix:

1. Bump the astrolabe submodule to v0.20.1 (cbcoutinho/astrolabe#116),
   which restores the three element ids on the refactored NcButtons.

2. Defense-in-depth in the test helpers: resolve the enable/revoke buttons
   by their stable id first, falling back to the button's accessible name
   so a future id rename degrades to a slower-but-working lookup instead of
   a hard timeout. Avoids a combined `.or_()` locator, which would
   strict-mode-violate (the id button also matches by text).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 22:53:43 +02:00
Chris Coutinho c47467f769 build: Bump astrolabe submodule 2026-06-04 21:33:05 +02:00
Chris CoutinhoandClaude Opus 4.8 e4d81d47d9 feat: harmonize MCP tool + userinfo page to documents/chunks model
Extend the documents-vs-chunks split to the remaining status surfaces so all
three report consistently (Deck #195):

- nc_get_vector_sync_status MCP tool + VectorSyncStatusResponse: add
  indexed_documents (distinct) and indexed_chunks; keep indexed_count as a
  deprecated alias of indexed_chunks. Reuses count_indexed.
- userinfo HTML page (/app/vector-sync/status): show Indexed Documents AND
  Indexed Chunks rows; switch its count to count_indexed (which also excludes
  placeholder points — the old raw count included them).
- /api/v1/vector-sync/status: restore indexed_count as a deprecated alias of
  indexed_chunks so existing consumers (integration tests, pre-#115 UI) keep
  working; the change is now purely additive for indexed_count.

Tests: VectorSyncStatusResponse documents/chunks/alias + zeroed defaults.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 20:28:44 +02:00
Chris CoutinhoandClaude Opus 4.8 8c9f97d6c4 docs: clarify postgres-mode None + test exact=True default (review #850)
- Note at both metrics-task call sites that receive_stream is None in postgres
  mode (get_ingest_pending falls back to procrastinate counts).
- Add test_default_is_exact_true covering the status-endpoint count path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 19:47:29 +02:00
Chris Coutinho 1e615c2bf1 build: Bump astrolabe submodule 2026-06-04 00:32:41 +02:00
Chris Coutinho aa2f01cd4b build: Bump astrolabe submodule 2026-06-03 14:59:59 +02:00
Chris Coutinho aa9f094424 build: Bump astrolabe submodule 2026-06-03 02:02:09 +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
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 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 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
Chris CoutinhoandClaude Opus 4.8 32a852ad90 test: fix multi-user-basic astrolabe app-password validation regression
Bumps the astrolabe submodule to validate the one-click background-sync app
password internally (IProvider::getToken) instead of via an HTTP loopback to
overwrite.cli.url, which is unreachable from inside the app container and broke
the multi-user-basic integration legs (enable_background_sync never succeeded).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 02:43:29 +02:00
Chris CoutinhoandClaude Opus 4.8 c92d866615 chore: bump astrolabe submodule (SonarCloud reduce fix)
No MCP-server runtime change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 02:19:01 +02:00
Chris CoutinhoandClaude Opus 4.8 92ee14b943 chore: bump astrolabe submodule (PR #89 review fixes)
Aligns PR #813's submodule pointer with astrolabe PR #89 tip (2fc6c7d):
review fixes (rawurlencode, php://input, cache key, app-password name, etc.)
plus Psalm/SonarCloud cleanup. No MCP-server runtime change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 02:15:56 +02:00
Chris CoutinhoandClaude Opus 4.8 6206f4a634 chore: bump astrolabe submodule (SonarCloud fixes)
Aligns PR #813's submodule pointer with astrolabe PR #89 tip (05176f9).
No MCP-server runtime change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 01:35:57 +02:00
Chris CoutinhoandClaude Opus 4.8 bbc4c5e81c chore: bump astrolabe submodule (Psalm fixes)
Aligns PR #813's submodule pointer with astrolabe PR #89 tip (091c68b),
which makes `composer run psalm` clean. No MCP-server runtime change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 01:06:19 +02:00
Chris CoutinhoandClaude Opus 4.8 9ee95cb7a6 chore: bump astrolabe submodule (Psalm fix in revokeFromMcpServer)
No runtime change — keeps the PR #813 submodule pointer aligned with the
astrolabe PR #89 tip (587caa6).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 23:50:29 +02:00
Chris CoutinhoandClaude Opus 4.8 d9a716080a fix(auth): make provision/revoke consistent with the app-password store
The OAuth provisioning tools (check_provisioning_status, revoke_nextcloud_
access) only consulted the refresh-token store + Astrolabe status, ignoring the
app_passwords store that Login Flow v2 (nc_auth_provision_access) and the
management API write to — the same store require_provisioning / get_client use
to grant tool access. Result: status reported "not provisioned" while tools
worked, and revoke said "nothing to revoke" while the credential persisted.

- _get_provisioning_status: also check storage.get_app_password_with_scopes,
  reporting is_provisioned with credential_type=app_password,
  flow_type=login_flow_v2.
- _revoke_nextcloud_access: when the credential is an app password, delete it
  from storage + invalidate the scope cache (no IdP token to revoke);
  refresh-token revocation via the Token Broker is unchanged.
- tests/unit/test_oauth_tools_app_password_provisioning.py: cover status +
  revoke for the app-password path.
- bump astrolabe submodule (deprovision MCP on disable); fix a stale assertion
  in the migrated bg-sync test (one-click flow has no separate app-password
  generation step).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 23:22:40 +02:00
Chris CoutinhoandClaude Opus 4.8 4ed228613e test(astrolabe): migrate suite to session-JWT auth model
Astrolabe was refactored to mint session-derived JWTs (TokenGenerationRequest
Event) and a one-click background-indexing opt-in, dropping the OAuth
authorize/callback/refresh surface. Bump the submodule and bring the test
suite in line:

- New test_astrolabe_session_jwt_search.py: a logged-in user searches via the
  minted JWT with no provisioning (replaces the obsolete login_flow_provisioning
  OAuth-authorize test; token_refresh test deleted — refresh flow is gone).
- settings_buttons: assert the new revoke endpoint + that oauth/disconnect is
  gone (404).
- multi_user_background_sync / plotly / chunk_context: drop the OAuth authorize
  step; provision via the one-click "Enable background indexing" button
  (#mcp-enable-background-button -> #mcp-revoke-background-button) instead of
  generating + pasting an app password.
- docker-compose.yml: mount the astrolabe submodule into the app container.
- third_party/astrolabe: bump to the one-click opt-in commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 23:06:27 +02:00
Chris Coutinho dc0653c415 ci: Bump astrolabe 2026-05-28 21:18:22 +02:00
Chris Coutinho a26971e9b1 ci: Update astrolabe submodule 2026-05-28 20:32:15 +02:00
Chris Coutinho 904c7938c3 chore: bump astrolabe 2026-05-24 12:43:04 +02:00
Chris Coutinho 39d70cfdf5 chore: Update third_party/astrolabe 2026-05-17 10:53:55 +02:00
Chris CoutinhoandClaude Opus 4.7 fd8c037eea fix(login-flow): allow Astrolabe's OAuth client on the management API
The `mcp-login-flow` profile's `ALLOWED_MGMT_CLIENT` was set to the
test-fixture id `nextcloudMcpServerUIPublicClient` only, but the actual
Astrolabe app provisions its OIDC client as
`astrolabeMcpClientOAuth00000000000` (see
`app-hooks/before-starting/26-configure-astrolabe-oauth.sh:39`). All
tokens issued through the "Enable Semantic Search" flow were rejected
with HTTP 401 by `unified_verifier.py:222-227`'s allowlist check, and
the Astrolabe UI's retry loop subsequently exhausted the
`api/passwords.py` 5/hr rate limit (HTTP 429).

Switch the `mcp-login-flow` allowlist to Astrolabe's client id so
production-shaped traffic actually validates. The `mcp-multi-user-basic`
profile keeps `nextcloudMcpServerUIPublicClient` for the
`configure_astrolabe_for_mcp_server` test fixture.

Also bump `third_party/astrolabe` 0.13.12 → 0.14.0 to pull in the
chunk-context indexed-lookup fix (#75) and the PDF bbox highlight
overlay (#76) that match the master-side changes already merged on
this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:03:42 +02:00
Chris Coutinho 7e6ce7d3b5 build: Update astrolabe commit 2026-05-01 21:43:55 +02:00
Chris CoutinhoandClaude Opus 4.6 ea7e86f7f4 chore: update astrolabe submodule to v0.13.9
Picks up astrolabe/astrolabe#61 which fixes app password provisioning
failure caused by loginName mismatch in ITokenProvider::generateToken().
This was the root cause of vector sync never indexing in multi-user
BasicAuth mode, which caused the plotly visualization test to fail.

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 10:00:48 +01:00
Chris Coutinho 69b84102b1 chore: update oidc app 2026-03-16 18:42:24 +01:00
Chris CoutinhoandClaude Opus 4.6 9d1a84af5a feat(auth): implement OAuth AS proxy to fix audience mismatch (ADR-023)
MCP clients like Claude Code were unable to use tools because tokens
obtained directly from Nextcloud had the wrong audience claim. The MCP
server now acts as its own OAuth Authorization Server, proxying auth
to Nextcloud with its own client_id so tokens have the correct audience.

New endpoints: /.well-known/oauth-authorization-server, /oauth/token,
/oauth/register. Modified /oauth/authorize from pass-through to
intermediary pattern. PRM now points authorization_servers to the MCP
server instead of Nextcloud.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 11:25:54 +01:00
Chris CoutinhoandClaude Opus 4.6 d09ebf20cc feat(ci): add Nextcloud version matrix (NC 31, 32, 33)
- Add cross-product matrix (3 versions x 4 auth modes = 12 CI jobs)
- Parameterize Nextcloud image in docker-compose.yml via NEXTCLOUD_IMAGE env var
- Pin NC 31.0.8, 32.0.6, 33.0.0 with SHA digests in workflow
- Add Renovate customManagers to auto-update NC images in workflow
- Fix Astrolabe install hook to prefer volume mount over app store
- Bump Astrolabe submodule to support NC 33 (max-version 31→33)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 11:13:38 +01:00
Chris CoutinhoandClaude Opus 4.6 87ec3c4f5b chore: update third_party submodule pointers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 09:17:29 +01:00
Chris CoutinhoandClaude Opus 4.6 dfc75a8619 refactor: extract Astrolabe to separate repository
Astrolabe has been extracted to its own repository at
github.com/cbcoutinho/astrolabe for independent releases.

Changes:
- Replace third_party/astrolabe/ directory with git submodule
- Remove astrolabe-ci.yml and appstore-build-publish.yml workflows
- Remove scripts/bump-astrolabe.sh
- Remove Astrolabe sections from bump-version.yml workflow
- Remove Astrolabe build steps from test.yml CI workflow
- Remove astrolabe volume mount from docker-compose.yml
- Simplify astrolabe install hook to always use app store
- Update CONTRIBUTING.md to reflect two-component monorepo

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 10:10:29 +01:00
github-actions[bot] 1dd5698389 bump: version 0.10.0 → 0.10.1 2026-02-03 06:50:26 +00:00
renovate-bot-cbcoutinho[bot]andGitHub 7cc852f0da chore(deps): update dependency phpunit/phpunit to v10.5.63 2026-01-31 11:08:40 +00:00
Chris Coutinho 9ec00d4de5 chore: Update screenshot names 2026-01-30 19:14:10 +00:00
Chris CoutinhoandGitHub 25dee9bfaf Merge pull request #496 from cbcoutinho/renovate/vue-monorepo
chore(deps): update dependency vue to v3.5.27
2026-01-28 13:46:20 +01:00
renovate-bot-cbcoutinho[bot]andGitHub 77fabccdb7 chore(deps): update dependency @nextcloud/vue to v9.4.0 2026-01-28 11:11:49 +00:00
renovate-bot-cbcoutinho[bot]andGitHub 2648ef2567 chore(deps): update dependency vue to v3.5.27 2026-01-28 11:11:24 +00:00
github-actions[bot] 252df1d398 bump: version 0.9.0 → 0.10.0 2026-01-28 07:39:10 +00:00
Chris CoutinhoandClaude Opus 4.5 9491d698e8 fix(astrolabe): add pagination and psalm fixes for token refresh
- Add pagination to getAllUsersWithTokens() with limit/offset params
- Update RefreshUserTokens to process users in batches of 100
- Add lock TTL documentation to withTokenLock() docstring
- Fix psalm type errors in getAccessToken() method
- Add unit tests for pagination and batched processing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 08:13:22 +01:00
Chris CoutinhoandClaude Opus 4.5 5b71ac3251 fix(astrolabe): add locking to prevent token refresh race condition
Adds distributed locking using Nextcloud's ILockingProvider to prevent
race conditions between background job and on-demand token refresh.

Uses double-check locking pattern:
1. Quick check without lock - return immediately if token is valid
2. Acquire exclusive lock if token needs refresh
3. Re-check after lock - another process may have refreshed
4. Refresh only if still needed
5. Graceful degradation on LockedException

Changes:
- McpTokenStorage: add ILockingProvider, withTokenLock() method
- McpTokenStorage: update getAccessToken() with locking pattern
- RefreshUserTokens: wrap refresh in withTokenLock(), catch LockedException
- Add comprehensive unit tests for locking behavior

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 22:23:42 +01:00