From d433f38015ff22d57b9be037fa89554dbe201c68 Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Wed, 14 Oct 2026 15:05:31 -0700 Subject: [PATCH 1/3] release: on-prem 2026-10-14-001 [ENG-1743] Images from currents release/2026-10-14-001. VERSION, the .env.example image tag and the generated compose files move to it, and [Unreleased] becomes the release entry with a summary, the migration timings from ENG-1741, and Added, Changed and Fixed sections. The upgrade note no longer says CI build ID search can miss runs while the migrations run: RUN_SEARCH_CLICKHOUSE is off on-prem, so search reads MongoDB, which the migrations don't gate. BETTER_AUTH_RATE_LIMIT_* and OAUTH_RATE_LIMIT_*_MAX (ENG-1701, ENG-1711) were configurable but undocumented; .env.example and configuration.md list them. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UhM4wHR2xPP8bbrkXGaqUD --- docs/configuration.md | 3 +++ on-prem/.env.example | 14 +++++++++++++- on-prem/CHANGELOG.md | 20 +++++++++++++++++++- on-prem/VERSION | 2 +- on-prem/docker-compose.cache.yml | 2 +- on-prem/docker-compose.database.yml | 2 +- on-prem/docker-compose.full.yml | 2 +- 7 files changed, 39 insertions(+), 6 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 72e187e..a9fd5fa 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -76,6 +76,8 @@ Less commonly changed settings with sensible defaults. | `INVITE_EMAIL_BCC` | string | _(empty)_ | BCC address for invitation emails | | `INVITE_EXPIRATION_DAYS` | int | `14` | Number of days before invitations expire | | `EMAIL_LINKS_BASE_URL` | string | `${APP_BASE_URL}` | Base URL for links in emails (derived from APP_BASE_URL) | +| `BETTER_AUTH_RATE_LIMIT_MAX` | int | `100` | Requests to `/api/auth/*` per client address per window. The dashboard reads the session on every page load, so a team behind one NAT or proxy shares one budget, and past it pages load with no data | +| `BETTER_AUTH_RATE_LIMIT_WINDOW_SECONDS` | int | `10` | The window for `BETTER_AUTH_RATE_LIMIT_MAX` | ### Remote MCP and OAuth @@ -90,6 +92,7 @@ Remote MCP lets coding agents use Currents tools over HTTP at `/mc | `OAUTH_CIMD_ENABLED` | bool | `true` | Accept clients identified by a client ID metadata document: an https URL the server fetches. Hosts resolving to private, loopback or link-local addresses are refused | | `OAUTH_HTTP_ALLOWED_HOSTS` | string | _(empty)_ | For an evaluation without TLS: a `.localhost` name (e.g. `currents.localhost`) the OAuth endpoints may serve over `http`. Browsers resolve `.localhost` names to this machine. Another `http` host starts, but OAuth clients are sent to an `https` issuer the install does not serve | | `OAUTH_JWKS_URL` | string | `/api/auth/jwks` | Where the API fetches its own signing keys to check access tokens. With a `.localhost` `APP_BASE_URL`, set `http://localhost:4000/api/auth/jwks`: the container cannot resolve `.localhost` names, so every token is rejected. Set the same when the container cannot reach or trust `APP_BASE_URL` (split DNS, an internal CA) | +| `OAUTH_RATE_LIMIT_REGISTER_MAX`, `OAUTH_RATE_LIMIT_TOKEN_MAX`, `OAUTH_RATE_LIMIT_AUTHORIZE_MAX`, `OAUTH_RATE_LIMIT_USERINFO_MAX` | int | `5`, `20`, `30`, `60` | OAuth requests per client address per 60 seconds, on top of `BETTER_AUTH_RATE_LIMIT_MAX` | ### SAML SSO (Optional) diff --git a/on-prem/.env.example b/on-prem/.env.example index ae9b839..a92f0a6 100644 --- a/on-prem/.env.example +++ b/on-prem/.env.example @@ -53,7 +53,7 @@ GITLAB_STATE_SECRET= # Currents application images DC_CURRENTS_IMAGE_REPOSITORY=513558712013.dkr.ecr.us-east-1.amazonaws.com/currents/on-prem/ -DC_CURRENTS_IMAGE_TAG=2026-07-26-006 +DC_CURRENTS_IMAGE_TAG=2026-10-14-001 # Application URLs APP_BASE_URL=http://localhost:4000 @@ -133,6 +133,12 @@ AUTOMATED_REPORTS_EMAIL_BCC= INVITE_EMAIL_BCC= INVITE_EXPIRATION_DAYS= +# Rate limit on /api/auth/*, per client address. The dashboard reads the session +# on every page load, so a team behind one NAT or proxy shares one budget, and +# past it pages load with no data. Raise it for a large team on one address. +# BETTER_AUTH_RATE_LIMIT_MAX=100 # requests per window +# BETTER_AUTH_RATE_LIMIT_WINDOW_SECONDS=10 + # Observability (Coralogix) # CORALOGIX_API_ENDPOINT= # CORALOGIX_API_KEY= @@ -172,6 +178,12 @@ OAUTH_DYNAMIC_CLIENT_REGISTRATION_ENABLED=true # Public API origin in share links and the OAuth token audience (default: APP_BASE_URL) # API_BASE_URL= +# OAuth requests per client address per 60 seconds, on top of the limit above. +# OAUTH_RATE_LIMIT_REGISTER_MAX=5 +# OAUTH_RATE_LIMIT_TOKEN_MAX=20 +# OAUTH_RATE_LIMIT_AUTHORIZE_MAX=30 +# OAUTH_RATE_LIMIT_USERINFO_MAX=60 + # ============================================================================= # SAML SSO (Optional) # ============================================================================= diff --git a/on-prem/CHANGELOG.md b/on-prem/CHANGELOG.md index c4991c1..19da17e 100644 --- a/on-prem/CHANGELOG.md +++ b/on-prem/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +## [2026-10-14-001] - 2026-10-14 + +Feature release: remote MCP with sign-in through an OAuth provider, context sharing and session files in a second bucket, and artifact expiry in the bundled RustFS. It changes the SAML SSO sign-in URL, so read Breaking Changes before upgrading. Update `DC_CURRENTS_IMAGE_TAG`, regenerate the compose file if you use custom templates, add the new `.env` block, then `docker compose pull && docker compose up -d`. + ### Breaking Changes - **SAML SSO: the ACS URL moves** from `/api/auth/sso/saml2/callback/` to `/api/auth/sso/saml2/sp/acs/`. The old path answers `404`, so SSO sign-in fails until your IdP sends assertions to the new one. The root admin's email and password sign-in is unaffected. Schedule the IdP change for the upgrade window: 1. In the IdP's SAML app, set the Single Sign-On / ACS URL (Entra ID: Reply URL) to the new URL. Set the Recipient and Destination URLs to the new URL as well. In Okta, leave "Use this for Recipient URL and Destination URL" checked (the default) and both follow the Single Sign-On URL. Currents rejects an assertion whose Recipient or Destination is any other URL. @@ -14,7 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - **SAML SSO: sign-ins started from the IdP's app dashboard** (an Okta tile, the Entra My Apps portal) are refused unless `SSO_SAML_ALLOW_IDP_INITIATED=true`. Sign-ins started from the Currents sign-in page work either way. ### Upgrade Notes -- The scheduler runs this release's database migrations when it starts, which can take several minutes on a large dataset. This is not downtime: the dashboard, API and run recording keep working. Until the migrations finish, filtering runs by status and searching by CI build ID can leave out runs recorded before the upgrade, and new results can take longer to process. Check that the scheduler's last startup in `docker compose logs scheduler` ends with `Startup tasks complete` (see [Database migration issues](../docs/upgrading.md#database-migration-issues)). If it logs an error instead, such as a ClickHouse `Timeout error.`, the remaining migrations don't run until the scheduler starts again: run `docker compose restart scheduler`. Finished migrations are skipped, and the run backfills skip the runs they already updated. +- The scheduler runs this release's database migrations when it starts. With 100,000 runs they took under a minute in testing: about 25 seconds to store each run's status in MongoDB and 10 seconds to re-insert every run into ClickHouse. Both scale with the number of runs, so expect several minutes for a million. This is not downtime: the dashboard, API and run recording keep working. Until the migrations finish, filtering runs by status can leave out runs recorded before the upgrade, and new results can take longer to process. Check that the scheduler's last startup in `docker compose logs scheduler` ends with `Startup tasks complete` (see [Database migration issues](../docs/upgrading.md#database-migration-issues)). If it logs an error instead, such as a ClickHouse `Timeout error.`, the remaining migrations don't run until the scheduler starts again: run `docker compose restart scheduler`. Finished migrations are skipped, and the run backfills skip the runs they already updated. ### Compose File Changes - The bundled RustFS image is `rustfs/rustfs:1.0.1` (requires `./scripts/generate-compose.sh` if using custom templates) @@ -28,15 +32,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - `OAUTH_PROVIDER_ENABLED`, `OAUTH_DYNAMIC_CLIENT_REGISTRATION_ENABLED`, `OAUTH_CIMD_ENABLED`, `OAUTH_HTTP_ALLOWED_HOSTS`, `OAUTH_JWKS_URL` — sign-in to remote MCP with a Currents account. `.env.example` turns the provider on; an upgraded `.env` without `OAUTH_PROVIDER_ENABLED=true` keeps it off. It needs an `https` `APP_BASE_URL`, or `http` on `localhost` (or, for an evaluation, a `.localhost` name in `OAUTH_HTTP_ALLOWED_HOSTS`, with `OAUTH_JWKS_URL` on `localhost`): with `http` on any other host the API refuses to start, so set `OAUTH_PROVIDER_ENABLED=false` - `API_BASE_URL` — public API origin, default `APP_BASE_URL` - `SSO_SAML_ALLOW_IDP_INITIATED` — accept SAML sign-ins started from the IdP's app dashboard. Default `false` +- `BETTER_AUTH_RATE_LIMIT_MAX` (default `100`), `BETTER_AUTH_RATE_LIMIT_WINDOW_SECONDS` (default `10`) — requests to `/api/auth/*` per client address. Raise them for a large team behind one NAT or proxy +- `OAUTH_RATE_LIMIT_REGISTER_MAX`, `OAUTH_RATE_LIMIT_TOKEN_MAX`, `OAUTH_RATE_LIMIT_AUTHORIZE_MAX`, `OAUTH_RATE_LIMIT_USERINFO_MAX` — OAuth requests per client address per 60 seconds (defaults `5`, `20`, `30`, `60`) - `DC_RUSTFS_DELETE_ARTIFACTS_AFTER_DAYS` — delete artifacts in the bundled RustFS after this many days. Unset keeps them ### Changed Environment Variables - `DC_RUSTFS_IMAGE` defaults to `rustfs/rustfs:1.0.1`. If your `.env` sets it, update the value to pick up the fixes below. +### Added +- Remote MCP at `/mcp`: coding agents use Currents tools with a Currents API key, a personal access token, or a sign-in through the OAuth provider +- Personal access tokens, for the REST API and MCP +- Context sharing and share links, organization logo editing and session file uploads, stored in the app content bucket + +### Changed +- The Improve Currents consent is not shown, and a new organization is created with no AI consent granted. It never applied to a self-hosted install. +- Images are built on Debian trixie (`node:24-trixie-slim`) + ### Removed Environment Variables - `SSO_SAML_SP_CERT_FILE` — the API never read it. Signed AuthnRequests need only `SSO_SAML_SP_KEY_FILE`; upload the certificate to your IdP. ### Fixed +- Share links, MCP and the assistant pointed at `api.currents.dev` instead of the install's own API. `API_BASE_URL` now defaults to `APP_BASE_URL`. +- The dashboard signed users out everywhere when sign-in requests hit the rate limit, and all users shared one limit. It now applies per client address, and `BETTER_AUTH_RATE_LIMIT_*` sets it. +- Records of who did something showed a user ID instead of an email address. - The bundled RustFS enforces bucket lifecycle rules. `1.0.0-alpha.79` accepted a lifecycle configuration and returned it from `GetBucketLifecycleConfiguration`, but never expired the matching objects. - The bundled RustFS includes the fix for CVE-2026-73288, where lifecycle and scanner sweeps could expire objects under COMPLIANCE retention when lock metadata was missing or unreadable. diff --git a/on-prem/VERSION b/on-prem/VERSION index 6f51d99..116b59a 100644 --- a/on-prem/VERSION +++ b/on-prem/VERSION @@ -1 +1 @@ -2026-07-26-006 +2026-10-14-001 diff --git a/on-prem/docker-compose.cache.yml b/on-prem/docker-compose.cache.yml index e7b1fc5..df53f1f 100644 --- a/on-prem/docker-compose.cache.yml +++ b/on-prem/docker-compose.cache.yml @@ -1,5 +1,5 @@ # Currents On-Prem Docker Compose -# Version: 2026-07-26-006 +# Version: 2026-10-14-001 # Profile: cache - Cache (redis) # # Generated by: ./scripts/generate-compose.sh diff --git a/on-prem/docker-compose.database.yml b/on-prem/docker-compose.database.yml index c86966c..05d8130 100644 --- a/on-prem/docker-compose.database.yml +++ b/on-prem/docker-compose.database.yml @@ -1,5 +1,5 @@ # Currents On-Prem Docker Compose -# Version: 2026-07-26-006 +# Version: 2026-10-14-001 # Profile: database - Database services (redis, mongodb, clickhouse) # # Generated by: ./scripts/generate-compose.sh diff --git a/on-prem/docker-compose.full.yml b/on-prem/docker-compose.full.yml index 771cff8..d1fe60f 100644 --- a/on-prem/docker-compose.full.yml +++ b/on-prem/docker-compose.full.yml @@ -1,5 +1,5 @@ # Currents On-Prem Docker Compose -# Version: 2026-07-26-006 +# Version: 2026-10-14-001 # Profile: full - All services (redis, mongodb, clickhouse, rustfs) # # Generated by: ./scripts/generate-compose.sh From e772778c0134844d759ba679658dc25b3adcbe21 Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Wed, 14 Oct 2026 15:10:14 -0700 Subject: [PATCH 2/3] fix: give RustFS its data directory before it starts [ENG-1743] On Linux, Docker creates a missing bind-mount source as root, and RustFS runs as uid 10001, so a fresh full-profile install's rustfs exited with "Permission denied (os error 13)". rustfs-data-init runs the RustFS image as root and chowns /data, non-recursively, before rustfs starts. It needs no root on the host, works however the stack is started, and pulls no new image. The issue's setup.sh fix couldn't work as written: chown to another uid needs root, and setup.sh runs as the operator. The scheduler's startup directory needs nothing: nothing writes to it. Tested in Docker-in-Docker (Linux bind mounts) on a fresh ./data: main's compose leaves data/rustfs at 0:0 and rustfs exits; this one leaves it at 10001:10001, rustfs is healthy, rustfs-init creates both buckets, and a second up works. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UhM4wHR2xPP8bbrkXGaqUD --- on-prem/CHANGELOG.md | 2 ++ on-prem/docker-compose.full.yml | 14 ++++++++++++++ on-prem/templates/compose.rustfs.yml | 15 +++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/on-prem/CHANGELOG.md b/on-prem/CHANGELOG.md index 19da17e..92c3f45 100644 --- a/on-prem/CHANGELOG.md +++ b/on-prem/CHANGELOG.md @@ -24,6 +24,7 @@ Feature release: remote MCP with sign-in through an OAuth provider, context shar - The bundled RustFS image is `rustfs/rustfs:1.0.1` (requires `./scripts/generate-compose.sh` if using custom templates) - The `rustfs` service sets `RUSTFS_CORS_ALLOWED_ORIGINS: "*"`. `1.0.1` sends no CORS headers without it, which breaks trace.playwright.dev and the dashboard's stdout and attachment previews. If you run `1.0.1` from your own compose file, add it there too. - The `api`, `changestreams-worker` and `write-worker` services read the app content bucket from `APP_CONTENT_STORAGE_*`, and `api` sets `MCP_ENABLED` (requires `./scripts/generate-compose.sh` if using custom templates) +- `rustfs-data-init` runs before `rustfs` and makes uid `10001` the owner of its data directory (requires `./scripts/generate-compose.sh` if using custom templates) - `rustfs-init` creates the app content bucket beside the artifact bucket and adds its lifecycle rules. Each run rewrites only the rules it owns, matched by ID (`ai-context-share`, `share-3d`, `share-7d`, `trace-links`, `delete-artifacts-after-days`), and keeps any others ### New Environment Variables @@ -52,6 +53,7 @@ Feature release: remote MCP with sign-in through an OAuth provider, context shar - `SSO_SAML_SP_CERT_FILE` — the API never read it. Signed AuthnRequests need only `SSO_SAML_SP_KEY_FILE`; upload the certificate to your IdP. ### Fixed +- On a fresh Linux install with the `full` profile, RustFS exited with `Permission denied`: Docker created `./data/rustfs` owned by root, and RustFS runs as uid `10001`. - Share links, MCP and the assistant pointed at `api.currents.dev` instead of the install's own API. `API_BASE_URL` now defaults to `APP_BASE_URL`. - The dashboard signed users out everywhere when sign-in requests hit the rate limit, and all users shared one limit. It now applies per client address, and `BETTER_AUTH_RATE_LIMIT_*` sets it. - Records of who did something showed a user ID instead of an email address. diff --git a/on-prem/docker-compose.full.yml b/on-prem/docker-compose.full.yml index d1fe60f..53c77bf 100644 --- a/on-prem/docker-compose.full.yml +++ b/on-prem/docker-compose.full.yml @@ -320,11 +320,25 @@ services: - default volumes: - ${DC_RUSTFS_VOLUME:-./data/rustfs}:/data # /data is RustFS's default storage location + depends_on: + rustfs-data-init: + condition: service_completed_successfully healthcheck: test: ['CMD-SHELL', 'curl -f http://localhost:9000/health || exit 1'] interval: 2s timeout: 5s retries: 10 + # RustFS runs as uid 10001 and exits with "Permission denied" on a data + # directory it doesn't own. On Linux, Docker creates a missing bind-mount + # source as root. Same image, run as root, so nothing new to pull. Not + # recursive: RustFS owns everything it creates inside. + rustfs-data-init: + image: ${DC_RUSTFS_IMAGE:-rustfs/rustfs:1.0.1} + user: "0" + entrypoint: ["chown", "10001:10001", "/data"] + volumes: + - ${DC_RUSTFS_VOLUME:-./data/rustfs}:/data + restart: "no" rustfs-init: image: ${DC_AWS_CLI_IMAGE:-amazon/aws-cli:2.32.31} depends_on: diff --git a/on-prem/templates/compose.rustfs.yml b/on-prem/templates/compose.rustfs.yml index 01bc205..1b853de 100644 --- a/on-prem/templates/compose.rustfs.yml +++ b/on-prem/templates/compose.rustfs.yml @@ -22,12 +22,27 @@ services: - default volumes: - ${DC_RUSTFS_VOLUME:-./data/rustfs}:/data # /data is RustFS's default storage location + depends_on: + rustfs-data-init: + condition: service_completed_successfully healthcheck: test: ['CMD-SHELL', 'curl -f http://localhost:9000/health || exit 1'] interval: 2s timeout: 5s retries: 10 + # RustFS runs as uid 10001 and exits with "Permission denied" on a data + # directory it doesn't own. On Linux, Docker creates a missing bind-mount + # source as root. Same image, run as root, so nothing new to pull. Not + # recursive: RustFS owns everything it creates inside. + rustfs-data-init: + image: ${DC_RUSTFS_IMAGE:-rustfs/rustfs:1.0.1} + user: "0" + entrypoint: ["chown", "10001:10001", "/data"] + volumes: + - ${DC_RUSTFS_VOLUME:-./data/rustfs}:/data + restart: "no" + rustfs-init: image: ${DC_AWS_CLI_IMAGE:-amazon/aws-cli:2.32.31} depends_on: From 1b89e3e242fa2e2bb093293fe7e9520b9f979633 Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Wed, 14 Oct 2026 15:59:28 -0700 Subject: [PATCH 3/3] fix: let RustFS start when its data directory can't be chowned [ENG-1743] On a root-squashed NFS or CIFS mount chown fails even when uid 10001 already has access, and a failed rustfs-data-init kept rustfs from starting at all. It now skips the chown when /data already belongs to 10001, logs a failed chown, and always exits 0; a directory RustFS really can't write still fails in rustfs, as before. Tested in Docker-in-Docker: a fresh root-owned ./data is chowned and rustfs is healthy, a second up works, and on a read-only mount the init logs the chown failure and exits 0. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UhM4wHR2xPP8bbrkXGaqUD --- on-prem/docker-compose.full.yml | 12 ++++++++++-- on-prem/templates/compose.rustfs.yml | 12 ++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/on-prem/docker-compose.full.yml b/on-prem/docker-compose.full.yml index 53c77bf..df0ce78 100644 --- a/on-prem/docker-compose.full.yml +++ b/on-prem/docker-compose.full.yml @@ -331,11 +331,19 @@ services: # RustFS runs as uid 10001 and exits with "Permission denied" on a data # directory it doesn't own. On Linux, Docker creates a missing bind-mount # source as root. Same image, run as root, so nothing new to pull. Not - # recursive: RustFS owns everything it creates inside. + # recursive: RustFS owns everything it creates inside. It always exits 0: + # chown fails on a root-squashed NFS or CIFS mount whose owner is already + # right, and a failed init would keep RustFS from starting at all. rustfs-data-init: image: ${DC_RUSTFS_IMAGE:-rustfs/rustfs:1.0.1} user: "0" - entrypoint: ["chown", "10001:10001", "/data"] + entrypoint: + - sh + - -c + - | + [ "$$(stat -c %u /data)" = 10001 ] && exit 0 + chown 10001:10001 /data || echo "rustfs-data-init: could not chown /data; RustFS needs uid 10001 to write it" + exit 0 volumes: - ${DC_RUSTFS_VOLUME:-./data/rustfs}:/data restart: "no" diff --git a/on-prem/templates/compose.rustfs.yml b/on-prem/templates/compose.rustfs.yml index 1b853de..504ccb9 100644 --- a/on-prem/templates/compose.rustfs.yml +++ b/on-prem/templates/compose.rustfs.yml @@ -34,11 +34,19 @@ services: # RustFS runs as uid 10001 and exits with "Permission denied" on a data # directory it doesn't own. On Linux, Docker creates a missing bind-mount # source as root. Same image, run as root, so nothing new to pull. Not - # recursive: RustFS owns everything it creates inside. + # recursive: RustFS owns everything it creates inside. It always exits 0: + # chown fails on a root-squashed NFS or CIFS mount whose owner is already + # right, and a failed init would keep RustFS from starting at all. rustfs-data-init: image: ${DC_RUSTFS_IMAGE:-rustfs/rustfs:1.0.1} user: "0" - entrypoint: ["chown", "10001:10001", "/data"] + entrypoint: + - sh + - -c + - | + [ "$$(stat -c %u /data)" = 10001 ] && exit 0 + chown 10001:10001 /data || echo "rustfs-data-init: could not chown /data; RustFS needs uid 10001 to write it" + exit 0 volumes: - ${DC_RUSTFS_VOLUME:-./data/rustfs}:/data restart: "no"