Repository navigation
Release v0.7.0
Release v0.7.0
Minor Changes
- #120 Thanks @NeilMazumdar! - Add static-header (API-key) auth for the MCP endpoint so external clients that can only send fixed headers — e.g. LibreChat — can connect without minting a per-request JWT. Set
MCP_API_KEYto accept a pre-sharedAuthorization: Bearer <key>; the sandbox owner (sub) is derived from a forwarded identity header (MCP_IDENTITY_HEADER, defaultx-librechat-user-id), giving each end-user an isolated sandbox. New env vars:MCP_API_KEY,MCP_IDENTITY_HEADER,MCP_DEFAULT_SUB,MCP_STATIC_TENANT. Static auth is additive and off unlessMCP_API_KEYis set — JWT clients on/mcpand all/v1/*routes are unchanged.
Startup hardening:MCP_IDENTITY_HEADERcannot be a reserved transport header (authorization,cookie,content-type,accept,mcp-session-id,mcp-protocol-version,last-event-id) — otherwise every request would deriveownerfrom a shared value and collapse all users into one sandbox. Themcp_static_auth_enabledstartup log records only whether a fallback owner is configured (hasDefaultSub), never theMCP_DEFAULT_SUBvalue. - #125 Thanks @Hazzng! - feat(gc): multi-tenant orphan-blob garbage collection via
pnpm db:gc.
Restores thepnpm db:gcCLI as a real, multi-tenant orphan-blob sweep for an external scheduler (cron / k8s CronJob). Orphan blobs (rows inblobsreferenced by zeroinodes) previously accumulated forever.- New migration
0006addsblobs.last_referenced_at(instant, catalog-only — legacy rows stay NULL and are treated as ancient/collectible). Every blob reference (insert + dedup re-adoption) now bumps it viaON CONFLICT (sha256) DO UPDATE, which also touches the blob so the grace window tracks real usage. gcOrphanBlobsrewritten to a null-safeNOT EXISTSanti-join with a grace window (minAgeMs), returning the deleted sha256s. It runs with no sandbox context (RLS escape) so the anti-join sees every inode; a blob referenced by another sandbox survives.- The sweep runs at REPEATABLE READ with bounded retries to close the dedup re-adoption race: under READ COMMITTED a concurrent writer that re-adopts an existing orphan blob could leave its committed inode without content (the GC's
NOT EXISTSre-check keeps a stale snapshot ofinodes). REPEATABLE READ turns that conflict into a serialization failure that is retried, so even--min-age-ms 0is safe under concurrent writes. - Deleted blobs are purged from the tenant-scoped Redis blob cache (
RedisBlobCache.mdel, fail-open). - New env
BLOB_GC_MIN_AGE_MS(default 3h) sets the grace window;pnpm db:gc -- --min-age-ms 0collects all orphans now,--tenant <id>restricts to one tenant.
- New migration
Patch Changes
- #122 Thanks @Hazzng! - Fix local Postgres dev setup for the integration test suite (#119).
- Add
docker-compose.local.yml(Postgres 16 + Redis 7). Itsinitdbscript
(scripts/initdb/00-create-app-role.sql) provisions a non-superusersqlfs_app
role that owns thesqlfsdatabase — required because migration0005enables
FORCE ROW LEVEL SECURITY, which a superuser silently bypasses (the RLS isolation
tests fail under the defaultpostgressuperuser). The file was previously
referenced by the README but.gitignored, so it could never be committed. - Document the non-superuser-owner requirement and the full local-DB workflow in
CONTRIBUTING.md(new "Local database" section). - Correct the
DATABASE_DIRECT_URLrow in the README env table: it is optional and
used only by drizzle-kit (pnpm db:generate); the server's boot-time migration
runner usesDATABASE_URL. - Update
.env.exampledefaults to match the compose stack. - Remove the broken, unused
pnpm db:migratescript (drizzle-kitmigratewith no
journal). Migrations are applied automatically on server boot.
- Add
- #125 Thanks @Hazzng! - fix(fs): delete inodes when their link count reaches zero (no
nlink=0tombstones).
The PostgresrmComposite,writeFileComposite, andmvCompositepaths decremented an inode'snlinkand deleted it (when it hit 0) within a single CTE statement. Postgres applies only the UPDATE when a row is both updated and deleted in one statement, so the inode was left atnlink=0instead of being removed — a tombstone that still referencedcontent_sha256. This pinned the blob (defeating the new orphan-blob GC, whose anti-join saw the tombstone) and leaked inode rows on every file delete, overwrite, and move-overwrite.
Each path now splits the work into two mutually-exclusive branches against the statement snapshot — delete whennlink <= 1, decrement whennlink > 1— so each inode row is touched exactly once.gcOrphanBlobsadditionally ignoresnlink = 0inodes so blobs pinned by tombstones left behind by older builds become collectible. Hardlinked inodes are unaffected (still decremented, not deleted, while other links remain).
Image: ghcr.io/hazzng/sql-fs:v0.7.0