trim_start_matches stripped every jwt_guest_ prefix, so a repeated-prefix bearer
shrank to a valid short token that verified and was then cached under the full
oversized bearer key. Strip exactly one prefix, and bound the raw bearer length
(the auth cache keys on it) before verifying or caching. The refusal test now
mints a valid signed token over the cap (which would otherwise verify, the extra
claim ignored) and a repeated-prefix bearer, so it fails if either guard regresses.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
The auth cache keys on the bearer token, and verify_for_workspace decoded a
token of any length (its header for the JWKS kid, then the body) before
rejecting it, so an oversized token could be decoded unauthenticated and, if it
verified, cached at full size. Refuse a token longer than MAX_GUEST_JWT_LEN
(8 KiB) at the top of verify_for_workspace, before the key lookup or any
signature work. Also correct the MAX_JWKS_URL_LEN doc: the bound holds because
the save path validates the URL through fetch_jwks, not because cached_jwks
checks it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
The cap read pem.trim(), but edit_guest_jwt_key stores the untrimmed string, so
a key with 8 KiB of leading or trailing whitespace passed the check and was
stored (and reparsed per request) at full size. Measure the untrimmed input.
Test covers a whitespace-padded key.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
decoding_key_from_pem decoded an unbounded PEM: a well-formed key with an
oversized modulus passes the structural check, is stored in the unbounded TEXT
column, and is reparsed on every guest-JWT request. Refuse one longer than
MAX_GUEST_PEM_LEN (8 KiB) at the same choke point the save path validates
through, the way the JWKS URL is bounded. Also tighten two cap tests to assert
their specific error rather than a substring another cap shares.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
The retained-bytes cap left two dimensions unbounded that reviewers named: the
cache keys on the admin-supplied URL (unbounded TEXT column), and the per-key
fixed cost (many tiny keys serialize small but each Jwk and its map slot cost
memory). Add a URL-length cap in fetch_jwks (the choke point save validates
through, so an overlong URL is never stored or cached) and a usable-key count
cap alongside the retained-bytes cap, both measured after filtering so a mixed-
use set is judged by its signing keys. Every dimension is now bounded.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
The key-count cap did not bound retained memory (from_jwk decodes n/e/x/y with
no length limit, so 50 keys could still carry ~1 MiB), and it counted raw
entries, refusing a valid mixed-use set with many encryption keys wholesale.
Replace it with a cap on the serialized size of the usable, retained keys
(JWKS_MAX_RETAINED_BYTES = 64 KiB), measured after filtering. Cache ceiling is
now bounded (200 entries x 64 KiB); a real set retains a few KB.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
JWKS_MAX_BYTES caps one response and the cache caps entry count (200), but a
densely packed 1 MiB body parses into thousands of keys, so one entry could
retain a few MB and a workspace admin rotating URLs could grow the shared
process by hundreds of MB. Refuse a set larger than JWKS_MAX_KEYS (50) in
parse_jwks_keys, which bounds retention at the source; real issuers publish a
handful of keys.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
The CE parent's only change was bumping ee-repo-ref to pick up the EE-only
OAuth guest-cookie change; keep this branch's ee-repo-ref at the EE companion
head (my guest_jwt EE changes on top of that same EE parent). Also address a
review nit: the app_path refusal messages now name the empty-path case that
is_scope_literal_path also rejects.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
change_workspace_id copies workspace_settings by an explicit column list that
omitted guest_jwt_public_key and guest_jwt_jwks_url, so a rename silently cleared
the guest JWT key, the same failure the parent fixed for guest_access_enabled.
Add both columns to the copy, and extend the rename test to assert the key
travels. Sync verify()'s refusal message with the other two sites (leading `/`).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
parse_jwks_keys filtered on metadata only (kty/alg/use/key_ops), but jsonwebtoken
carries n/e/x/y as strings and defers decoding to auth time, so a JWKS whose only
key had malformed material passed save-time validation and every token failed
later. Keep a key only if DecodingKey::from_jwk decodes it. This is the single
source for both edit_guest_jwt_key and per-request verify.
Update two test comments that credited the SPKI parse alone now that the guard
also accepts a PKCS#1 RSA public key.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
The SPKI guard decoded the body with the strict RFC 7468 decoder, which enforces
64-column wrapping, so a legitimate public key wrapped otherwise (or a PKCS#1
`RSA PUBLIC KEY`) was refused where jsonwebtoken would have parsed it. Decode the
body leniently like jsonwebtoken, then require the DER to be a public-key
structure: an SPKI (RSA or EC) or a PKCS#1 RSA public key. Private material
satisfies neither, so the round-13 bypass stays closed. Test adds a one-line
(non-64-column) public key as a positive control.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
jsonwebtoken 8.3 decides public vs private from the PEM label alone and never
inspects the DER, so private material relabelled `PUBLIC KEY` (or a PKCS#1 key
relabelled `RSA PUBLIC KEY`) passed the earlier label check and would be stored,
then served back through the settings response. decoding_key_from_pem now parses
the DER as a SubjectPublicKeyInfo, which only public keys satisfy, before it is
persisted or used. The regression test relabels complete, valid private keys
(EC PKCS#8, RSA PKCS#1) so the guard is what refuses them, not malformed DER.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
jsonwebtoken 8.3's from_rsa_pem accepts private encodings (PKCS#1, PKCS#8), so
a pasted private key would be stored and then served back through the settings
response. Refuse any private PEM in decoding_key_from_pem, the single choke
point for both the save endpoint and per-request verification.
Also drop the last "canonical" references the is_scope_literal_path switch left
in the JWT arm: the refusal log no longer misdiagnoses a reserved character as a
malformed path, and the relocated guest_session_scopes doc carries the
sentinel/label distinction for both its callers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
Adopt the parent's app-path guard for scopes: windmill_common::auth::
is_scope_literal_path (refuses only `:`, `,`, `*` and a leading `/`) rather than
check_proper_path, since app paths may carry spaces and `@`. The relocated
guest_session_scopes and the JWT arm's verify() both use it. The parent's
re-added local guest_session_scopes in users.rs stays relocated to
windmill_api_auth::scopes, its guard preserved there.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
Save the guest JWT key before the Enterprise-only default-app and rate-limit
writes, so a refused write cannot swallow a valid key change on CE. Name the
JWT entry in the Guests card summary. Complete verify()'s doc with the email
and app_path rules. Anchor the refusal suite with a positive control and make
enable_guests assert its status, so a broken fixture cannot pass it vacuously.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
jsonwebtoken 8.3 models a JWK's `alg` as its signing `Algorithm` enum, so a set
carrying an encryption key (`alg: "RSA-OAEP"`, absent from that enum) alongside a
signing key failed whole-set deserialization and the whole JWKS was rejected,
which real issuers publish. Parse each key on its own and skip one that does not
model as a JWT key, keeping the usable signing keys. Unit-tested with a mixed set.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
- Gate the audit with a conditional upsert (`DO UPDATE ... WHERE NOT
guest_activity.jwt_entry RETURNING 1`) read with fetch_optional. The row comes
back exactly once per email per day, decided by the conflicting tuple, so it
keeps the atomicity `xmax = 0` had (no double audit when two first requests race
on a metered instance, which takes no advisory lock) and still fires on the
first JWT after an IdP sign-in created today's row. The prior CTE decided this
from the statement snapshot and could double-audit.
- Negative-cache only a real allowance refusal (`PermissionDenied`); a transient
DB error inside guest_admission denies this request but no longer locks the
email out for 30 seconds.
- Refuse a JWKS key whose `key_ops` is present and omits `verify`: it is published
for something other than signature verification. Unit-tested.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
- Gate the JWT `users.login_guest` audit on the transition to `jwt_entry = true`,
not physical insertion: a guest who signed in through the IdP earlier the same
day already has a `guest_activity` row with `jwt_entry = false`, and the old
`xmax = 0` test suppressed the first JWT audit. A CTE reads the prior flag and
the RETURNING decides it atomically in the upsert.
- Fold the signed-in mint's inline account check into `has_any_account`, so the
helper has both callers and the two no longer diverge on lowercasing.
- Negative-cache a refused guest JWT for 30s so a replayed bearer past the cap
does not take the instance-wide allowance advisory lock on every request.
- Update the two stale share-link header comments to the `guest.<jwt>` form,
drop the "plan gate" rationale on the entry test's cfg, collapse the blank
lines the SHARE_LINK_SEGMENT removal left, and prettier-format the settings
card after the isEnterprisePlan wrapper was removed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
- admit_and_record_guest_jwt commits the guest_activity row before the
best-effort audit, on a separate connection. The EE audit writer swallows an
audit_partitioned failure but the failing statement still aborts its
transaction, so auditing before the commit would roll the activity row back
while the arm returned success, admitting a guest uncounted and past the
allowance.
- The share-link guest marker is now the prefix `guest.` glued to the token
(`/a/<path>/guest.<jwt>`, `/public/<ws>/<secret>/guest.<jwt>`). The `.` cannot
appear in a custom-path or secret segment, so an external-JWT link whose custom
path ends in a `guest` segment (`/a/foo/guest/<jwt>`) is read as before rather
than hijacked. Removed the unused SHARE_LINK_SEGMENT constant.
- Moved the JWT allowance test to its own binary (app_guest_jwt_allowance.rs):
set_plan flips a process-global license key, so a test sharing the binary with
the existing allowance test would race under --test-threads.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
* feat: add sso_groups_claim setting for login-time instance group sync
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YESxWqzt959S6TY6vbc4eG
* chore: bump ee-repo-ref for the SSO groups claim reconcile
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YESxWqzt959S6TY6vbc4eG
* chore: update ee-repo-ref to 3b89bfc11314a326a191101cfe3ef65f6f7f82a8
This commit updates the EE repository reference after PR #774 was merged in windmill-ee-private.
Previous ee-repo-ref: e388527f9adbbe466fe050ca8d1d236ce3342bc3
New ee-repo-ref: 3b89bfc11314a326a191101cfe3ef65f6f7f82a8
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
- Validate the email claim with `windmill_common::users::VALID_EMAIL` (the `usr`
table's own constraint) plus the 254-byte bound, rather than a hand-rolled
predicate. It requires an `@`, which is what keeps a guest's name its own
principal, never a `u/<user>` or `g/<group>`.
- Reject an `app_path` claim carrying a scope metacharacter (`*`, `,`, `:`,
whitespace) before authenticating: it is interpolated into `apps:read:<path>`
and `apps:run:<path>`, where `*` or `,` would widen the guest past its one app.
- `isJwt` on the custom-path route normalises base64url before `atob`, so a
header carrying `-`/`_` (a `kid`, a signature) is recognised instead of taken
for a path segment; it also checks the three-segment structure.
- Drop the dead stale-key carry-forward in the blocking JWKS path (a stale-good
entry is served by the fast path) and clean up the test's env var.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
A stub issuer that counts inbound connections, ten concurrent cached_jwks calls,
and an assertion that exactly one fetch is made. Pins the property the caching
layer exists for, which no test covered before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
- Cap the email claim at 254 bytes as well as requiring a plain address. An
over-255-byte email fit the signature but overflowed the guest_activity.email
column: the activity write and the login_guest audit both failed while the
guest was still admitted, so a guest could enter uncounted and unaudited. Now
refused before authentication.
- Serve a stale-but-good JWKS entry while a refresh runs off the request path,
so a slow or hanging issuer no longer stalls guest requests for the fetch
timeout at each 15-minute TTL boundary. Only a cold or negative entry blocks,
still under the single-flight lock; the background refresh no-ops when a fetch
is already in flight and keeps the stale keys on failure.
- Tests: an oversized email is refused alongside the group-shaped and
slash-in-email cases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
The email claim becomes the guest's username, and `username_to_permissioned_as`
reads a name with no `@` as `u/<user>` and a `group-` prefix as `g/<group>`. A
token could therefore name the guest after a user or a group. Reject anything
that is not a plain email address (an `@`, no `/`, no whitespace) so a guest's
name is only ever its own principal, never a user's or a group's. Found while
reviewing the identity path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
- Refresh a workspace's JWKS under a per-URL lock, so a burst against a cold or
stale entry triggers one fetch, not one per request (single-flight). The "at
most once per interval" bound now holds while a fetch is in flight, not only
after it lands.
- A cached entry with no keys is a remembered failure; serving it reported an
unreachable issuer as an unknown `kid`. Map an empty entry to an
issuer-unreachable error instead.
- Use the design-system TextInput (textarea variant) for the PEM key field
rather than a raw <textarea>.
- Move `has_any_account` above `username_to_permissioned_as` so it no longer
sits between that function's doc comment and its body.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
- Resolve a guest JWT on the lowercased email. Accounts are stored lowercased,
so a mixed-case `email` claim slipped past the no-account gate and resolved an
account holder to a guest, and split the activity rows the seat count reads.
`has_any_account` now normalises its input too (index-friendly, not
`lower(email)`).
- Cap the auth-cache entry for a guest JWT at 5 minutes rather than the token's
`exp` (up to 24h). A guest JWT is revocable only by the workspace switch or by
rotating the key; the short entry makes a rotated or cleared key bite on
re-verification, and makes the day-keyed activity dedupe reachable across a
midnight (the second-day row was never written).
- Audit `users.login_guest` only when the upsert freshly inserts the row
(`xmax = 0`), decided atomically by the DB, so concurrent first requests and
separate API nodes emit it at most once a day.
- JWKS hardening: read the body with a 1MB cap instead of buffering any size;
an alg-less RSA key accepts the whole RSA family instead of being forced to
RS256; a failed fetch serves the last good keys (or a short negative entry) so
an unreachable issuer is hit at most once per 30s however much unauthenticated
`jwt_guest_` traffic arrives, and an unknown `kid` never triggers a fetch;
lower the fetch timeouts to 5s/10s.
- Settings copy: note that the JWKS should point at an issuer you control, since
neither `iss` nor `aud` is bound.
- Tests: a mixed-case account and an over-24h lifetime are refused; unit tests
pin `jwk_algorithms` (including the alg-less RSA family) and a JWK-derived key
verifying a real token.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
A second way in for a guest (companion to windmill#10929): a JWT the embedding
customer's own backend mints and signs, carried on the app's share link and
verified against a key the workspace admin configured. It needs no
identity-provider round-trip, so it works inside an iframe where popups and
third-party cookies do not. Bearer prefix jwt_guest_, stateless: verified per
request, cached until exp, no token row.
A JWT guest is the same identity as a signed-in guest: no usr row, no password
row, no seat, confined to the one app app_path names. Every guest gate applies:
the plan, the workspace switch (enforced once at the auth door via the sentinel),
the app mode (guest_app_admits), and "no account at all" (has_any_account). The
claim's workspace_id must equal the route's workspace, and a workspace-less route
never accepts it.
Claims honoured: email, workspace_id, app_path, exp (mandatory); nbf/iat
validated when present; the accepted lifetime is capped at 24h. Algorithms:
RS256/384/512, PS256/384/512, ES256/384; HS* is refused. The key is a
per-workspace setting, a PEM public key or a JWKS URL (at most one, a DB CHECK
enforces it), Enterprise-plan gated like the guest switch. The JWKS URL is
validated against private ranges and the fetch is pinned to the validated
address.
Counting: a JWT guest is recorded in guest_activity (once per email, workspace
and day, cached), marked jwt_entry, and not in unique_ext_jwt_token. A first-seen
users.login_guest audit carries the entry kind.
Narrower than jwt_ext_ by design: that key is instance-level and can assert
admin, groups and folders; a guest key is scoped to one workspace and only ever
mints guests. An app-only user a customer routes through jwt_ext_ today is
counted; through a guest JWT they become a free guest, the intended pricing
change, split out as guest_jwt_count in the telemetry so it can be measured.
Changes on the parent branch, additive: ApiAuthed.credential_expiry (a
credential's own expiry when it has no token row); guest_derived_token_constraints
caps on it; guest_session_scopes moved to windmill-api-auth::scopes and
has_any_account to windmill-common::users so the mint and the JWT arm share one
copy; the signed-in mint's login_guest audit now carries entry=idp.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
* feat: make the service log retention period an instance setting
Service log retention was a hardcoded 14 days with no override, unlike job retention. It
becomes the `service_log_retention_secs` global setting (env `SERVICE_LOG_RETENTION_SECS`,
default unchanged at 14 days), reloaded on change like the other retention settings.
The constant becomes `DEFAULT_SERVICE_LOG_RETENTION_SECS` and every reader goes through
`service_log_retention_secs()`, so the `log_file` sweep, the object-storage orphan scan, the
columnar store's compaction and pruning, the retrieval clamp and the search index's trim
window all follow the configured value.
Loaded outside `initial_load`'s `server_mode` guard: a dedicated indexer trims the search
index to a window derived from this value and is not a server.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WsnpNSM6K3oyjwntRwJtVN
* fix: never let a non-positive service log retention expire every log
Every service log cutoff is `now - retention`, so a `0` or negative window puts the cutoff
at or after `now` and the next sweep reads the whole history as expired — deleting the
`log_file` rows and their object-storage files irreversibly.
`0` is reachable two ways now that the window is configurable: it is what an operator types
by analogy with the job retention period sitting directly above it, where `0` does mean keep
forever; and `SecondsInput` writes a `0` into a field that was merely focused, so saving the
Jobs panel is enough. Service logs always have a window, so clamp an unusable value back to
the default in the accessor every reader already goes through. The upper bound is where
`chrono::Duration::seconds` panics, which would abort the sweep that reads it.
The settings field rejects a non-positive value rather than silently correcting it, and its
description now names the database rows too — they are swept on every instance, including
one with no object storage configured.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WsnpNSM6K3oyjwntRwJtVN
* fix: address review findings on the service log retention setting
- Bound the monitor's `log_file` sweep. Every process rotates a log file a minute, so lowering
the retention can make one ordinary setting change expire millions of rows; the unbounded
`DELETE ... RETURNING` materialized all of them, and their deletion futures, in a single
tick. Batched like the settings-page cleanup on the same table.
- Make the retention atomic private and give it one writer, so a value that would expire every
service log cannot reach a cutoff by any path, and say so in the log when one is rejected
rather than falling back silently.
- Cap the retention at a century. The previous ceiling only bounded `TimeDelta` construction,
while consumers compute `now - retention`, which panics past year 262143, and build a
Postgres interval that overflows well before the old cap.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WsnpNSM6K3oyjwntRwJtVN
* fix: cap an oversized service log retention instead of shortening it
The two unusable directions were landing on the same fallback, so configuring a retention
above the ceiling silently produced 14 days — deleting logs the operator had asked to keep
for longer. Too large now caps at the maximum, which preserves that intent; only a
non-positive value, which would expire everything and has no upward reading, falls back to
the default.
Also bound the `log_file` drain to ten batches per pass: `monitor_db` runs under a 600s
timeout that cancels every maintenance future in the same `join!` and reports a critical
error, so a backlog large enough to need batching has to drain across ticks, the way the
neighbouring sweeps already do. The settings field carries the upper bound too, and the
superseded query's offline entry is dropped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WsnpNSM6K3oyjwntRwJtVN
* fix: route the new log-file registration cutoff through the retention accessor
`send_log_files_to_object_store` arrived on main while this branch was open and reads the
retention directly. The atomic behind it is private now, so it goes through the accessor like
every other consumer — which also means the cutoff it uses to skip registering already-expired
files follows the configured retention rather than a fixed two weeks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WsnpNSM6K3oyjwntRwJtVN
* fix: say why every mode loads the service log retention setting
A worker registers its rotated log files against the retention cutoff, so the comment naming
only the indexer no longer covers why the setting sits outside the `server_mode` guard.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WsnpNSM6K3oyjwntRwJtVN
* fix: file service log retention under Monitoring, not Jobs
Service logs are the Windmill processes' own logs — every process rotates and registers its
own, no job involved — so the Jobs panel was grouping by the shape of the widget rather than
by the subject. It sits under Monitoring now, beside the Indexer panel that holds the other
service-log window.
Its own section rather than inside that panel: the panel is badged EE, while this governs the
database sweep that runs on every instance.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WsnpNSM6K3oyjwntRwJtVN
* chore: update ee-repo-ref to a6e3533b26195918a17fea58646f71d2bbcde288
This commit updates the EE repository reference after PR #752 was merged in windmill-ee-private.
Previous ee-repo-ref: 1d93da24bd166b9a5a5cc204034a1d35ffc88474
New ee-repo-ref: a6e3533b26195918a17fea58646f71d2bbcde288
Automated by sync-ee-ref workflow.
* feat: say on the service logs page where the logs actually are
The retention number alone does not tell an operator what it governs, and the answer differs
by instance. Two states are worth calling out because they are the ones where retention does
not mean what it looks like:
Without instance object storage, each process keeps its files on its own disk. The page lists
what every host wrote, since the rows are in the shared database, but can only open the files
of the replica serving the request, and a host's files go with it when it is replaced.
With object storage but "Delete logs from s3 periodically" off — the backend default, since
uploads are gated on a store existing while deletions are gated on that toggle — expiring a
log removes the row and the local file and leaves the uploaded copy behind for good.
The retention field itself now names every copy it covers and says that full-text search
reaches back at most that far, and less when the indexer's own window is shorter.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WsnpNSM6K3oyjwntRwJtVN
* fix: describe raw log files as the transient copy they became
Retiring the raw files landed while this was being written: the indexer now deletes each one
as soon as it is ingested, and the log viewer rebuilds a file from the columnar store once the
raw copy is gone. So the durable copy is the store, and warning that an uploaded file is kept
forever when periodic s3 deletion is off only holds where no indexer runs to ingest it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WsnpNSM6K3oyjwntRwJtVN
* chore: point ee-repo-ref at the EE compile fix
EE main does not build on its own: extracting the index-window expression and adding a fourth
copy of it landed in separate PRs that never conflicted textually. windmill-ee-private#756 is
the one-line fix; this pins it so CI has a tree that compiles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WsnpNSM6K3oyjwntRwJtVN
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>