Skip to content

Add NixOS module and integrated service stack#6

Open
mtuckerb wants to merge 39 commits intofoxsen:mainfrom
mtuckerb:main
Open

Add NixOS module and integrated service stack#6
mtuckerb wants to merge 39 commits intofoxsen:mainfrom
mtuckerb:main

Conversation

@mtuckerb
Copy link
Copy Markdown

@mtuckerb mtuckerb commented Apr 6, 2026

This adds first-class NixOS service support for zotero-selfhost.

What’s included:

  • flake export for nixosModules.default and nixosModules.zotero-selfhost
  • services.zotero-selfhost NixOS module
  • systemd services for dataserver, stream-server, and tinymce-clean-server
  • integrated optional infrastructure for MySQL, Redis, Memcached, OpenSearch, MinIO, and nginx
  • SOPS-backed secret handling via sops-nix
  • helper commands for initialization and user creation
  • deployment docs and example host/SOPS files
  • configurable integrated MySQL package selection

Validation performed:

  • nix flake check --no-build
  • minimal nixosSystem evaluation confirming the expected systemd units exist

This should make the repo usable directly as a flake input for NixOS deployments.

mtuckerb and others added 30 commits April 5, 2026 20:36
The composer install step was failing in Nix's sandboxed build because
it has no network access. This splits the build into two stages:

1. A fixed-output derivation (composerVendor) that fetches PHP
   dependencies via composer - FODs are allowed network access since
   their output is content-addressed.
2. The main dataserverPkg copies the pre-fetched vendor directory and
   runs composer dump-autoload to regenerate autoload files.

Also adds dontCheckForBrokenSymlinks to handle a dangling symlink
in the upstream zotero/dataserver source (htdocs/schema points to a
git submodule not included in fetchFromGitHub).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
writeShellApplication runs ShellCheck, which flags combined
export+assignment from command substitution as SC2155.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Same fix as the composer vendor issue — npm ci/install needs network
access which isn't available in Nix's sandbox. Split each into a
fixed-output derivation for fetching deps and a regular derivation
for assembly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
nodePackages has been removed from nixpkgs. npm ships with the
nodejs package already.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The module was setting services.nginx.recommendedTlsSettings = true
globally, which on nixos-25.05 emits an ssl_conf_command directive
for post-quantum hybrid groups (X25519MLKEM768). Older nginx builds
don't support that directive and refuse to start, breaking the host's
existing virtual hosts.

TLS settings are a host-wide concern, not service-specific. Leave
them to the user.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The current NixOS module has several latent issues that only surface
once the dataserver tries to handle a request — they were hidden
behind a sops-decryption failure that prevented the service from
starting at all. Document the full set of workarounds as a drop-in
override block, plus manual setup steps for Zend Framework 1, the
zotero-schema submodule, the MariaDB user, and the schema-to-DB
ingest, and add a complete walkthrough for building and serving
the zotero/web-library SPA at /library/ with HTTP basic auth.

Update TODO to reflect that web-library is now working and shift
focus to fixing the upstream module bugs at the source.
… NixOS deploy actually works

The previous module had several latent issues that only surfaced once
the dataserver tried to handle a request. They were originally hidden
behind a sops-decryption failure that prevented the service from
starting at all; once the secrets file is encrypted, you hit them in
sequence. This commit fixes all of them at the source so users no
longer need the long workaround block in the README.

dataserverPkg / composerVendor:
* Apply src/patches/dataserver/{0001,0002,0004}.patch (rate limit
  increase, AWS SDK minio config, getUploadBaseURL minio fix). Patch
  0003 (debug logging) is intentionally omitted because it no longer
  applies cleanly to the pinned dataserver source.
* Extract src/patches/dataserver/Zend.tar.gz into include/Zend at
  build time. The dataserver `require_once 'Zend/Db/Profiler.php'`
  but the upstream zotero/dataserver ships an empty Zend/ directory
  (only .gitignore); the docker workflow's utils/patch.sh extracts
  ZF1 from this archive and the nix module wasn't doing the same.
* Add new `services.zotero-selfhost.zoteroSchemaSrc` option that
  defaults to a fetchFromGitHub of zotero/zotero-schema, and copy
  schema.json into htdocs/zotero-schema/ at build time. zotero-schema
  is a git submodule of upstream zotero/dataserver but pkgs.fetchFromGitHub
  doesn't fetch submodules, so without this Schema::init() throws
  "Locales not available" → silent HTTP 500 on /itemTypes etc.

php buildEnv:
* Add include_path and auto_prepend_file to extraConfig. Without
  these, htdocs/index.php's `require('config/routes.inc.php')` fails
  to find include/config/routes.inc.php (silent HTTP 500), and
  Z_ENV_CONTROLLER_PATH from include/header.inc.php is undefined
  (silent HTTP 500). The upstream Apache config auto-prepends
  header.inc.php; `php -S` doesn't unless the ini says so.

systemd:
* Pin pkgs.nodejs_20 for stream-server and tinymce runtime ExecStart.
  pkgs.nodejs currently tracks the latest LTS (24); the bundled
  node-config npm package calls util.isRegExp which was removed in
  Node 22+. Pure-JS deps are portable across node versions so the
  build-time FOD wrappers can stay on pkgs.nodejs without
  invalidating the user-supplied hashes.
* Add tmpfiles.d rules to pre-create the runtime working directories
  for dataserver/htdocs, stream-server, and tinymce-clean-server
  before systemd checks each service's WorkingDirectory= (which is
  enforced before ExecStartPre runs, so the prepare scripts can never
  bootstrap them on first start without this).

initScript:
* Drop `SET @@global.innodb_large_prefix = 1` — variable was removed
  in MariaDB 10.5+; DYNAMIC row format is the default now. Setting
  it returns "Unknown system variable" and aborts the init.
* Fix four INSERTs that broke against the current dataserver schema:
  - libraries: schema has 6 columns (added hasData), insert provided 5
  - users (zotero_master): schema has 3 columns, insert provided 5
  - shardLibraries: schema has 5 columns (added storageUsage),
    insert provided 4
  Use named columns for robustness against future schema additions.
* ALTER TABLE users_email ADD COLUMN validated TINYINT(1) after
  loading www.sql, default 0, mark seeded admin email as validated=1.
  The dataserver's Storage::getInstitutionalUserQuota joins
  users_email to storage_institutions WHERE validated=1, but
  www.sql doesn't include this column. Symptom: HTTP 500 with
  "Unknown column 'validated' in 'WHERE'" when uploading any file
  through the web library or desktop client.
* Run admin/schema_update at the end of init to ingest schema.json
  mappings (item types, fields, creator types, locales) into the
  fields/creatorTypes/itemTypes/itemTypeFields/baseFieldMappings/
  itemTypeCreatorTypes tables. Without this, write operations fail
  with "Field 'X' from schema N not found in ItemFields.inc.php"
  because coredata.sql only ships an old schema. Use
  `php -d auto_prepend_file=` to override the new php.ini auto_prepend
  (admin/schema_update has its own require('header.inc.php'); without
  disabling auto_prepend the function gets declared twice → fatal
  redeclare).

createUserScript:
* Same column-count fixes as initScript: libraries needs hasData,
  zotero_master.users has 3 columns not 5, shardLibraries needs
  storageUsage. Also use users_email with validated=1 for new users.
Patch 0004 from src/patches/dataserver/ hardcodes `http://` in
Storage::getUploadBaseURL() because it was originally written for
local docker dev with no SSL. For production NixOS hosts with ACME
on the attachments hostname this is wrong: the browser receives an
http://attachments.host/zotero/ upload URL, POSTs the PDF body to
it, nginx 301-redirects http→https, and 301 doesn't preserve the
POST body across the redirect → silent upload failure with "Error:
Received error from Zotero server: An error occurred" in the web
library.

Drop patch 0004 from the patches list and substitute the URL via
sed in dataserverPkg.installPhase using a new option
`services.zotero-selfhost.s3.scheme` (enum http|https, default
https). Local docker dev users can set scheme = "http" to restore
the old behavior.
The previous "NixOS gotchas — workarounds you must apply" section
documented a long override block that paste-installed runtime fixes
for ~7 latent bugs. All of those are now fixed at the source in
nix/module.nix (PHP include_path + auto_prepend_file, Zend Framework
1 extraction, zotero-schema fetch, dataserver patches 0001/0002/0004,
nodejs_20 runtime, init script column-count fixes, schema_update,
runtime working directory tmpfiles, dbconnect mysql.user, validated
column on users_email, configurable s3.scheme).

Replace the override block with a much shorter "One-time manual
setup" section that only covers things the module legitimately can't
do for you: encrypting the sops file, creating the MariaDB user,
running zotero-selfhost-init, restarting minio if it picked up empty
credentials, and (optionally) creating an API key for the web
library SPA.

The stream-server uws→ws workaround stays as its own subsection
because it's still a real out-of-band hack — uws is an abandoned
native module and the proper fix is to either patch upstream
zotero/stream-server or replace streamServerSrc.

Net change: -230 lines (834 → 604).
Patch 0002 (config-aws-for-local-minio-server) hardcodes both
'endpoint' => 'http://...' and 'scheme' => 'http' in the AWS SDK
config. After our successful upload to minio, the dataserver runs
Storage::registerUpload which does a HeadObject call through the
SDK; the SDK uses the http URL, the http nginx listener 301-
redirects to https or returns 403, and the SDK turns that into a
500 from the registration endpoint. The browser sees this as
"An error occurred" after the multipart POST to minio already
succeeded with HTTP 201.

Apply the same sed substitution as the Storage.inc.php fix using
cfg.s3.scheme so the SDK and the upload URL agree.
…ible VALUES()

zotero/dataserver line 573 of model/Storage.inc.php uses
  INSERT INTO storageFileItems (...) VALUES (?,?,?,?) AS new
  ON DUPLICATE KEY UPDATE storageFileID=new.storageFileID, ...
which is MySQL 8.0+ syntax. MariaDB 11.4 (the NixOS default) doesn't
recognize the `AS new` row alias and aborts with "near 'AS new'".
This blocks PDF upload registration: the multipart POST to minio
succeeds with HTTP 201 but the dataserver's Storage::registerUpload
fails to record the file metadata, returning HTTP 500 to the client.

Substitute the row-alias form for the legacy VALUES(col) form via
sed in dataserverPkg.installPhase. VALUES(col) in ON DUPLICATE KEY
UPDATE is deprecated in MySQL 8.0.20+ but still works there, and is
the only form MariaDB supports.
Without this, on a fresh host the minio start script reads
/run/secrets/zotero-selfhost/s3-{access,secret}-key as empty strings
(the files don't exist yet because sops-install-secrets hasn't run)
and minio falls back to its default minioadmin:minioadmin credentials.
The dataserver then can't authenticate against minio (it's using the
sops credentials) and PDF uploads return 403 InvalidAccessKeyId.

The fix only affects fresh boots; on a host where minio has already
started successfully it's a no-op.
…needed)

The upstream zotero/dataserver wraps download URLs in a custom HMAC-
signed format that points at $ATTACHMENT_PROXY_URL, expecting a
separate "attachment proxy" service to verify the signature, decode
the payload, and stream the file from S3 / minio. Upstream zotero.org
runs that proxy as a separate component but doesn't open-source it,
and the NixOS module doesn't ship a substitute. Symptom: the web
library's PDF reader fetches a download URL from /file/view/url and
gets back something like
  https://attachments.example.com/<base64-json>/<hmac>/<filename>
which minio interprets as an invalid S3 key and rejects with HTTP 400.

Add a new patch (0005) that rewrites Zotero_Attachments::generateSignedURL
to generate AWS SDK V4 presigned GET URLs when the payload contains a
'hash' (download URLs). Minio validates the V4 signature itself and
serves the file directly. The legacy proxy format is kept as a
fallback for the upload-URL code path (which uses a different field
shape and isn't actually exercised by the working upload flow that
uses Storage::getUploadFileInfo's signed POST policy).
The upstream zotero/stream-server repo depends on `uws` (µWebSockets)
10.148.1, an abandoned native binding with no prebuilt binaries for
modern Node and source that no longer compiles against modern Node
ABI. The streamServerNodeDeps FOD fails on any reasonably modern host.

Add a new patches list streamServerPatches with one patch (0001)
that replaces require('uws') with require('ws') in connections.js
and server.js, handles the upgradeReq → req second-arg API change in
the connection handler, and updates package.json to depend on
ws ^7.5.10 (which was already a devDependency).

Apply the patches to both streamServerNodeDeps (so npm install sees
the new package.json) and streamServerPkg (so the runtime js is
patched). Switch streamServerNodeDeps from `npm ci` to `npm install`
because the patch invalidates the upstream package-lock.json — the
FOD outputHash still pins reproducibility.

streamServerNodeDepsHash default is set to a placeholder; bumping
this commit on any consumer requires re-running nix build to discover
the real hash, then setting it on services.zotero-selfhost.streamServerNodeDepsHash.
…atch

Computed via FederalNix rebuild against the previous commit
(0dc653e). The hash is the result of `npm install --omit=dev` on the
patched stream-server source where uws@10.148.1 has been replaced
with ws@^7.5.10. Pinned for reproducibility.
Add services.zotero-selfhost.webLibrary.enable. When true, the
upstream zotero/web-library is fetched (via fetchgit with
fetchSubmodules=true since the SPA has 6 submodules), built as a
fixed-output derivation (network needed for npm install + locale +
styles fetches), and served at the configured hostname's root.

Source patches applied via sed in the build derivation:
- src/js/constants/defaults.js: rewrite api.zotero.org → cfg.webLibrary.hostname
  for apiAuthorityPart, websiteUrl, and streamingApiUrl.
- src/js/utils.js: rewrite item canonical URL + parser regex to use
  the configured hostname (and accept upstream zotero.org URLs in
  the parser for backward compat with synced items).
- src/html/index.html: replace the demo zotero-web-library-config
  block with userId/userSlug/apiKey from new options, and the
  menu config with a single My Library entry pointing at the
  configured userSlug.
- scripts/task-runner.mjs: wrap process.stdin.setRawMode in
  isTTY guards (the upstream interactive task picker crashes when
  stdin isn't a TTY, which is the case in the nix sandbox).
- scripts/build.mjs: replace `cp -aL` with `cp -arL` because
  uutils-coreutils on NixOS doesn't accept GNU's `-a` shorthand.

When webLibrary.enable is true, the nginx vhost layout switches to
serve the SPA at root with a regex location carving out dataserver
API paths (/users, /groups, /itemTypes, etc) that takes precedence.
The SPA's React Router uses root-relative paths so it MUST be
mounted at root — sub-path mounting breaks internal navigation on
refresh. Optional basicAuthFile gates access to the bundle (which
embeds the apiKey).

webLibraryHash defaults to a placeholder; bumping webLibrarySrc or
any webLibrary.* option that affects the build requires re-running
nixos-rebuild to discover the real hash from the FOD error message.

New options:
- services.zotero-selfhost.webLibrary.enable
- services.zotero-selfhost.webLibrary.hostname (default: infrastructure.hostname)
- services.zotero-selfhost.webLibrary.apiKey (required when enable=true)
- services.zotero-selfhost.webLibrary.userId (default: "1")
- services.zotero-selfhost.webLibrary.userSlug (default: "admin")
- services.zotero-selfhost.webLibrary.basicAuthFile (default: null)
- services.zotero-selfhost.webLibrarySrc (default: zotero/web-library v1.7.5)
- services.zotero-selfhost.webLibraryHash (default: placeholder)
The v1.7.5 tag has a different scripts/ layout (no scripts/build.mjs,
no scripts/task-runner.mjs) which doesn't match the source patches
in webLibraryPkg. main HEAD has the build flow our patches target.
The previous heredoc approach with python triple-quoted strings
nested inside Nix's `''...''` multiline string syntax was getting
mangled (Nix interprets `''` as the escape character and ate the
python string delimiters). Move the python script out to a
pkgs.writeText file referenced from the install phase.
…tall

Some web-library transitive dependencies (e.g. unrs-resolver via
napi-postinstall) have postinstall hooks that use #!/usr/bin/env
shebangs which fail in the nix build sandbox (no /usr/bin/env).
Install with --ignore-scripts to skip those, then patchShebangs the
node_modules tree to rewrite absolute store paths.
scripts/fetch-or-build-modules.mjs uses curl and unzip via
child_process.exec to download pre-built reader/pdf-worker/note-editor
zips from zotero-download.s3.amazonaws.com. python3 is needed for
the webLibraryHtmlPatcher script.
…irectly

The upstream scripts/build.mjs uses scripts/task-runner.mjs which
buffers each sub-task's stdout and only prints a one-line label.
On failure that label is `…` and the actual error message is
swallowed, making sandbox builds undebuggable. Run each prepare
script and each build step directly so failures surface real
stdout/stderr.
…dInputs

scripts/fetch-or-build-modules.mjs runs `git rev-parse HEAD` inside
each modules/<submodule> directory to determine which prebuild
artifact to fetch from zotero-download.s3.amazonaws.com. fetchgit
strips .git from submodules by default. Set leaveDotGit=true to
preserve it (the FOD outputHash still pins reproducibility downstream)
and add pkgs.git to nativeBuildInputs so the rev-parse call works.
Update the source sha256 to match the new hash with .git preserved.
mtuckerb and others added 9 commits April 8, 2026 16:44
Computed via FederalNix rebuild against the previous commit (96810ee).
The hash pins the result of npm install + npm run build for the
zotero/web-library main HEAD pinned in webLibrarySrc, with all the
source patches applied and the prepare/build steps run directly
(bypassing the task runner).
Add src/patches/zotero-client/0002-zotero7-config-for-self-host.patch
that updates the five URL fields the self-host actually serves
(DOMAIN_NAME, BASE_URI, WWW_BASE_URL, API_URL, STREAMING_URL) in
Zotero 7's resource/config.mjs (the new ESM module location). The
old 0001 patch targets Zotero 5/6's resource/config.js layout.

Add utils/patch-zotero7-client.sh which auto-detects the Zotero 7
install path (Linux / macOS / Windows), backs up the original
config.mjs, and substitutes the user's hostname into the five
fields. Idempotent and reversible (saves the original to
config.mjs.zotero-org.bak).

Caveat documented in the script: Zotero auto-update overwrites
config.mjs, so users must either rerun the script after each update
or disable auto-updates in Zotero preferences.
Modern Zotero (6+) honors two runtime preference overrides for the
sync URLs:
  extensions.zotero.api.url       (overrides ZOTERO_CONFIG.API_URL)
  extensions.zotero.streaming.url (overrides ZOTERO_CONFIG.STREAMING_URL)

These are read by syncRunner.js, syncAPIClient.js, retractions.js,
and streamer.js — i.e. the entire sync code path. So redirecting
the desktop client at a self-hosted dataserver does NOT require
patching resource/config.mjs (which is bundled inside omni.ja in
Zotero 7+ and would need extract/repack/clear-startup-cache cycles
that fight Zotero's auto-updater).

Replace the previous patch-zotero7-client.sh + 0002 patch with a
configure-zotero-client.sh that auto-detects the Zotero profile
directory (Linux / macOS / Windows / Snap), writes the two prefs
to user.js (which Zotero evaluates as user-pref overrides at
startup), and prints next-step instructions for the user.

The user.js approach survives Zotero auto-updates (the file lives
in the profile dir, not the install dir). Idempotent: re-running
with the same hostname is a no-op; running with a different host
rewrites the values.

Drop the old patch-zotero7-client.sh and 0002 patch since they
target a layout that doesn't exist in Zotero 7/8 (loose
resource/config.js was Zotero 5/6 only — modern bundles it in
omni.ja and prefs are the user-facing override path anyway).
Upstream zotero/dataserver enforces a 300 MB per-user default quota
(matching the zotero.org free tier) that surfaces as "out of space"
errors from the desktop sync engine after a few hundred attachments.
For a self-hosted deployment where the operator already controls
storage at the minio/S3 layer, this is a confusing first-run wall
with no obvious fix.

0006-remove-storage-quota.patch bumps Zotero_Storage::$defaultQuota
from 300 MB to 10 TB. The per-user override path through
storageAccounts still works for operators who want real per-user
limits, so this only changes the default for unconfigured users.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
For deployments where the host already runs an S3-compatible backend
(another minio, AWS S3, R2, B2, etc), the bundled minio is redundant —
double the disk + RAM + bucket of credentials, and a fresh-boot race
where minio falls back to default minioadmin credentials before the
sops secrets are decrypted.

Add `services.zotero-selfhost.infrastructure.minio.enable` (default
true for backward compat). When set to false:

- The bundled `zotero-selfhost-minio.service` is not declared
- The dataserver service no longer waits for it at startup
- The integrated nginx attachments vhost reverse-proxies to whatever
  `services.zotero-selfhost.s3.endpointUrl` points at, instead of the
  hardcoded loopback minioPort

The operator is then expected to set s3.endpointUrl explicitly and
populate the sops s3-access-key/s3-secret-key entries with credentials
that work against their existing backend. The bucket bootstrap in
dataserver-prepare runs against s3.endpointUrl regardless, so the
zotero/zotero-fulltext buckets will be auto-created on the external
backend on first start (assuming the credentials have mb permission).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
nginx defaults client_max_body_size to 1m. The integrated attachments
vhost (the public reverse proxy in front of S3 / minio for attachment
uploads + downloads) inherits that default, which silently rejects any
Zotero PDF over 1 MB with HTTP 413 — the desktop client surfaces this
as a generic "file sync error" with no useful diagnostics.

Add `services.zotero-selfhost.infrastructure.attachmentMaxBodySize`
defaulting to 2g, applied as extraConfig on the attachments vhost. 2g
covers every reasonable Zotero attachment (papers, scanned books, large
slide decks) while still capping pathological uploads.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Zotero_Storage::$uploadQueueLimit = 10 is the zotero.org abuse-prevention
default — every user is capped at 10 in-flight upload URL requests in
any 5-minute window. The desktop client surfaces breaches as a generic
"file sync error" with no useful diagnostics, and stale rows from prior
failed attempts (quota wall, body-size wall, missing-bucket wall) sit
in storageUploadQueue for 5 full minutes before aging out — so even
after fixing the underlying problem the queue stays full and every
retry gets 413'd until the rows expire.

Bump to 200. Self-hosted single-user (or small-team) deployments will
routinely batch dozens of uploads in a single restore-to-online
migration. 200 is still bounded so a runaway client can't DoS the
server, but high enough that no realistic single-user workload will
ever notice. The 300-second timeout is unchanged so genuinely stale
entries still age out.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Zotero_FullText::indexItem requests S3 StorageClass=STANDARD_IA for any
fulltext payload over 75 KB gzipped, as a cost-tier optimization on
the hosted zotero.org backend. minio doesn't accept STANDARD_IA and
rejects the put with "Invalid storage class". The exception bubbles
up through updateMultipleFromJSON's catch block and rolls back the
MySQL itemFulltext write. The desktop client surfaces this as a
generic "ZoteroObjectUploadError: An error occurred" with no detail
about which specific S3 call failed.

The bug looks intermittent because items with smaller extracted PDF
text bodies fall under the 75 KB threshold and use the STANDARD branch
(which minio accepts), while longer PDFs always fail. From a user's
perspective, fulltext sync "works for some items" — the worst kind of
bug to debug.

Drop the StorageClass parameter entirely. minio doesn't have cheaper
tiers to opt into, and a self-host operator who wants tiering can
configure it via lifecycle policies on the bucket itself.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Zotero desktop polls /retractions/list on every sync to check for
retracted publications. This endpoint exists on hosted zotero.org but
is not implemented in upstream zotero/dataserver, so on a self-host
the request falls through to the SPA root location and trips the
basic-auth gate (when webLibrary.basicAuthFile is set). The client
logs:

    HTTP GET https://.../retractions/list failed with status code 401

…on every sync attempt. Doesn't block sync but surfaces as a recurring
"unable to check for retractions" warning in the Zotero error console.

Add an explicit `location = /retractions/list` to the integrated
nginx vhost that returns an empty JSON array with auth_basic off.
The client interprets this as "no retractions known" and stops
complaining. No backend code change required.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant