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
- 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
The guest policy moved (parent merge): guests are free up to the instance
allowance, then metered on Enterprise and hard-capped elsewhere. The JWT arm now
calls guest_admission inside the transaction that records guest_activity (the
advisory lock spans the count check and the row), and the door re-reads
guest_session_stands (switch, instance switch, no account) for every guest
request, so the JWT arm needs nothing extra for those. The plan gate on the key
config is gone (guests are free on any plan).
Adds an allowance test: with the window full on a capped instance, a stranger's
JWT is refused (401) and a returning guest's is admitted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
- 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
- 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
- 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
* fix: let operators use wmill.datatable() from within running jobs
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHR4fytgt6m4q37WCXs2Rp
* fix: refuse content-driven redirects and deferral in the operator datatable exemption
* fix: check the datatable exemption against the expanded query, not the raw content
* fix: fail closed on a language-overriding expansion and state the exemption's real scope
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* fix: let a job token read the automate_username_creation setting
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test: use an ungated global setting as the confinement control
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix: require admin on workspace tarball settings export
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: name the refused flag in the settings export error
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix: require item read scope on workspace tarball export
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: accept a wildcard path grant for whole-domain scope checks
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: let a wildcard path grant delegate the unqualified scope
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
* fix: scope capture deletion to the workspace in the request path
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test: layer the capture fixture on base instead of duplicating it
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix: require an unscoped token to read the workspace encryption key
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: hold the encryption key's write path to the same token bar
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: audit a workspace export only once nothing can still reject it
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore: carry the new audit operation into the served openapi spec
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix: confine job tokens to workspace-scoped API routes
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: keep the object-storage connection test reachable from a job token
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: keep the workspace-exists check the CLI makes reachable from a job
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test: reconcile the job-token caps after #10124
The workspace-confinement middleware answers a workspace-less route before the
privilege gate behind it runs, so the cases #10124 added on those routes now see
403 rather than 401. Rejection is what they assert, but two of them needed more
than a status change:
- `list_worker_groups` asserted only that the response body omits the static env
value, which an error body satisfies for the wrong reason. It now asserts the
status, keeping the secret check as a second assertion.
- `require_super_admin` lost its only unshadowed route. `GET
/api/w/{workspace}/users/list_addable` is gated solely by that call and names a
workspace, so it reaches the gate and pins it at 401.
The module doc states the two-layer rule once; the file covers both caps, so it
is no longer named for either one alone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: correct the workspaces/exists rationale and the parquet gate note
`workspace` carries no row-level security, so `exists_workspace` running through
`user_db` does not filter by membership as the comment claimed. State what the
route actually discloses — whether a workspace id is taken.
The object-storage case explained why a 404 would satisfy the assertion for the
wrong reason, which described the earlier `assert_ne!(403)`; against the 422 it
now asserts, a 404 fails. Say instead why the case is gated on the feature.
* fix: let a job token keep the workspace-less routes that carry no workspace
Confinement refused every route outside the allowlist, including ones that
answer purely from the caller's own account or from the request body. Those
cross no workspace boundary, so refusing them buys nothing:
- `users/email` returns a value already inside the token, and
`workspaces/allowed_domain_auto_invite` tests the caller's own address against
a static list. Neither opens a transaction.
- `users/usage` reads the caller's own row; `users/tutorial_progress` reads and
upserts a UI bitfield keyed on the same email.
- `schedules/preview` takes no `ApiAuthed` at all — it computes the occurrences
of the cron expression in the body and returns nothing the caller did not send.
The rule, not the list, is what the doc comment states: answers from the caller's
own account, the request body, or content identical for every workspace; never
naming another workspace, never instance configuration. The candidates it
excludes are written down with their reasons, since `users/list_invites` reads as
caller-scoped until you notice the response carries a workspace id per invite.
Regression covers both directions — the new entries answer, and the rejected
caller-scoped reads stay refused.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test: reach the privilege gates confinement hides
`require_devops_role` and `require_instance_admin` gate only workspace-less
routes, so confinement answers every request that would reach them: no HTTP case
can tell whether they still cap job tokens, and both could lose that check with
this suite green. `require_super_admin` has a workspace-scoped route to reach it;
these two have none, so call them directly instead.
The identity used is the fixture's real superadmin, so the passing half proves
the rejection keys off `job_id` rather than off the user.
Also separate two claims the write-allowlist doc had merged into one sentence:
no entry writes outside the caller's own account, but what each may read differs,
and the workspace-existence check answers for any id.
* docs: say that the object-storage probe writes
The write-allowlist lead claimed no entry writes state outside the caller's own
account. `test_s3_bucket` puts an object into the store the body names and
deletes it again, so it does write; a failure between the two leaves the object
behind. The invariant that holds is about Windmill state.
Say so in the lead, and describe the put/delete in the entry itself rather than
leaving "acts only on the store the request body describes" to imply a read.
* test: cover the last two job-token gates confinement hides
Seven guards key on `ApiAuthed::job_id`. Three keep a workspace-scoped route and
are exercised over HTTP; the other four are reachable only through workspace-less
routes, which confinement now answers first, so nothing observed whether they
still cap job tokens.
`require_devops_role` and `require_instance_admin` were already called directly.
Add the two that were not: `forbid_superadmin_job_token`, and
`forbid_elevated_job_token`, whose call sites are `create_token`,
`update_token_scopes` and `set_password` — all workspace-less.
Both key on two conditions rather than one, so all three combinations are pinned:
neither fires without job provenance, and neither fires for an unelevated
identity. The second matters — collapsing either into a blanket job-token refusal
would stop ordinary users creating tokens, and no other case would catch it.
The doc comment records which of the seven each route covers.
* docs: correct which job-token gates have no observable route
The previous commit put `forbid_elevated_job_token` among the guards reachable
only through workspace-less routes, and its message named three call sites. It
has six, and two are workspaced: `mint_app_embed_token` and
`mint_raw_app_sdk_token`. Its superadmin branch is therefore already exercised
over HTTP — the 401 the embed-token case asserts is this gate.
So three of the seven lack an observable route, not four. Its direct assertions
stay: the embed-token case only ever reaches it with an elevated identity, and
the unelevated-negative case is what would catch the gate being collapsed into a
blanket job-token refusal.
* test: pin is_instance_admin, and stop enumerating gates in prose
`is_instance_admin` is `authed.is_admin && authed.job_id.is_none()`, so a census
built by searching for `job_id.is_some()` could not see it. Both its call sites
are workspace-less, and it returns a bool that selects obfuscation rather than
refusing — a job token reading `true` leaks `env_vars_static` instead of being
turned away. Pin both directions.
The doc comment tried to account for every job-token guard and which route
exercised it. It was wrong three times running: the count, the call sites of
`forbid_elevated_job_token`, and the claim that the CUSTOM_INSTANCE_DB case
covers `is_super_admin_authed` when that path tests `job_id` inline. A table that
has to be rederived from six crates to stay true does not belong in a comment, so
it now states only why these calls are direct.
* docs: name the right is_instance_admin caller
The comment credited "the concurrency-group listing" with obfuscating rows. The
second caller is `prune_concurrency_group`, which returns PermissionDenied; the
obfuscating one is `list_worker_groups`. Keep the claim to that single caller,
which is what makes this guard fail by leaking `env_vars_static` rather than by
admitting a request.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(security): a WM_TOKEN job token can never be a global superadmin (GHSA-hfh4-cx4h-3fcr)
Privilege escalation: an app/flow/schedule/trigger execution policy's `on_behalf_of`
(which a `wm_deployers` member can set) could point at a superadmin email. The
resulting job `WM_TOKEN` then passed the email-based superadmin checks, granting
instance superadmin. `forbid_superadmin_job_token` only guarded ~15 of ~75 routes.
Fix at the token layer: a WM_TOKEN must never satisfy a superadmin gate,
regardless of whose email it runs as (sentinel OR a real superadmin).
- `ApiAuthed` gains a `job_id` field, stamped once in `AuthCache::get_opt_job_authed`
from the resolved token's job_id (correct even on cache hits).
- `require_super_admin(db, email)` -> `require_super_admin(db, &ApiAuthed)`, rejects
`authed.job_id.is_some()`. `require_super_admin_email` kept for the few internal
callers without an ApiAuthed.
- `is_super_admin_authed(db, &ApiAuthed)` for the boolean `is_super_admin_email`
authorization branches on request handlers (workspace deletion, fork drops,
dev-workspace attach/archive, object-storage SSRF exemption, custom dbname, EE GHES
+ connected repositories, ...). Migrate ~75 sites (OSS + EE).
- CUSTOM_INSTANCE_DB reads the *authenticated* job_id, not the caller-supplied
`?job_id` query param. Worker-tag check takes a precomputed job-aware `is_super_admin`
on the request path.
Execution-time on-behalf checks (scheduled/flow worker-tag, Cloud enqueue quota,
is_devops_email) are hardened in a follow-up — see
docs/followup-onbehalf-execution-privilege-hardening.md.
Regression tests: a superadmin-email WM_TOKEN is rejected on `require_super_admin`
routes, on `DELETE /workspaces/delete/{w}` (403, workspace preserved), and on the
CUSTOM_INSTANCE_DB lookup with no `?job_id` (401); real superadmin tokens still succeed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: cap devops role at workspace admin and reject reserved on_behalf_of identities
Extends the job-token cap with three pieces:
- `require_devops_role` takes `&ApiAuthed` and rejects job tokens.
`is_devops_email` is true for superadmin emails, so every worker-management,
instance-config and service-log route was reachable by the same superadmin
`WM_TOKEN` that `require_super_admin` already rejects.
- A `job_id` claim that does not parse as a uuid rejects the token rather than
resolving to `None`, which would clear the job provenance and uncap it. Applies
to the internal JWT and the external `jwt_ext_` path.
- Defense in depth at store time: `validate_on_behalf_of` refuses the reserved
internal sentinels as an `on_behalf_of` on apps/flows/scripts/schedules/triggers,
and app execution refuses a policy carrying one — covering already-persisted and
forked-app rows that predate the cap. Deploying on behalf of a real user,
including a real superadmin, stays allowed; the cap handles that at execution.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(mcp): preserve job-token provenance when minting the proxy JWT
The MCP endpoint-tool proxy re-mints a JWT from the caller's ApiAuthed to
forward the proxied request, but passed job_id: None. A job's WM_TOKEN is
capped at workspace admin (GHSA-hfh4-cx4h-3fcr); dropping the job_id here
re-minted an uncapped token that satisfies require_super_admin /
require_devops_role on the proxied route (e.g. listWorkers exposing worker
IPs, job/workspace IDs, and sensitive tags).
Carry api_authed.job_id into create_jwt_token. Adds an in-module regression
that decodes the forwarded JWT and asserts the job_id is preserved for a job
caller and absent for a non-job caller.
Reported by Codex CI review (P1) on #10124.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: cap the admin-or-devops gate at workspace admin for job tokens
require_admin_or_devops (the EE critical-alerts endpoints) grants when the
caller is a workspace admin OR an instance devops. is_devops_email is true
for superadmins, so a WM_TOKEN running on-behalf of a superadmin who is not a
member of the target workspace could clear the devops branch and read/ack that
workspace's critical alerts (GHSA-hfh4-cx4h-3fcr). This gate takes a bare
email, not an ApiAuthed, so the token-layer cap could not see it.
Thread the caller's job-token provenance and reject the devops branch for job
tokens, matching require_devops_role. The workspace-admin branch stays allowed
— that is the cap ceiling. Adds an enterprise-gated regression proving the
bypass is closed and a real superadmin token still clears the gate.
Found while auditing the PR for bare-email gates the choke-point cap misses.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: cap instance-global is_admin gates at workspace admin for job tokens
Three instance-global routes gate on the caller's own `is_admin` claim, which
`ApiAuthed.is_admin` carries into a WM_TOKEN (it is a workspace-admin claim,
true for superadmins too). A job token is capped at workspace admin
(GHSA-hfh4-cx4h-3fcr), so its is_admin claim must not authorize instance
actions on a route with no workspace binding:
- `unarchive_workspace` — unarchive an arbitrary workspace by id
- `prune_concurrency_group` — delete a global concurrency group
- `list_worker_groups` — return unobfuscated `env_vars_static` (may hold secrets)
Add job-token-aware `is_instance_admin` / `require_instance_admin` helpers (the
same shape as `require_super_admin` / `require_devops_role`) and use them at
these three sites. Workspace-scoped `require_admin(authed.is_admin, ...)` gates
are intentionally left unchanged — a workspace-admin job token is within the
cap there. Regression added covering all three; verified it lets a WM_TOKEN
unarchive/leak without the fix and is blocked with it.
Reported by Codex CI review (P1) on #10124.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(mcp): drop orphaned path_field_renames from EndpointTool test helper
The merge with main adopted main's mcp path-substitution refactor (#10162),
which removed the `path_field_renames` field from `EndpointTool` and its
consumer (`substitute_path_params` no longer takes per-field path renames).
main's `runner.rs` `ep` test helper still constructed the struct with
`path_field_renames: None`, so the workspace test build (cargo test --all,
which compiles windmill-mcp's own #[cfg(test)] module under the `server`
feature) failed with E0560. A plain `cargo check` does not compile that test
module, so it only surfaced in CI's cargo_test.
Remove the orphaned field to match the struct.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test: describe the sentinel-rejection policy the forged-identity test asserts
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: complete ApiAuthed initializers in feature-gated tests after merge
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: stop job tokens minting credentials that shed their provenance
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: cap the MCP OAuth approval mint at the same elevated-job-token gate
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: cap the self-service password reset at the elevated-job-token gate
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: cap app embed/SDK mints and scope widening at the elevated-job-token gate
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: keep job tokens from destroying the account they run on behalf of
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: deny job tokens a foreign-workspace admin claim and workspace ejection
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: keep the follow-up inventory in the PR instead of the repo
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: make the session workspace status gate job-token aware
session_workspace_status derived its superadmin branch from a bare email
check, so a job token carrying a superadmin identity resolved the existence
of workspaces it has no relationship with rather than seeing them as
deleted. Switch to is_super_admin_authed, matching every other instance
gate reached from a request ApiAuthed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* revert: leave the global concurrency-group listing on the plain admin gate
The listing exposes concurrency keys across workspaces, which is metadata
rather than a capability, and it 401s rather than degrading. Keep the guard
on the prune route next to it, which is the destructive one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: keep the instance-admin gate on the global concurrency listing
The listing spans every workspace's concurrency keys, and the gate rejects
only job tokens: the !is_admin branch is the pre-existing check, so
workspaced tokens and interactive admins are unaffected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to d30af67d38954f9012f7bad08da23e347344b4c6
This commit updates the EE repository reference after PR #664 was merged in windmill-ee-private.
Previous ee-repo-ref: 7870573dbc3360f99bada143f094c67dce0d9e9c
New ee-repo-ref: d30af67d38954f9012f7bad08da23e347344b4c6
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: hugocasa <hugo@casademont.ch>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
WindmillFinder's ModuleSpec lacked origin, so __file__ was never set on
loaded modules. inspect.getfile() then raised "is a built-in module",
breaking typeguard's @typechecked and anything else that introspects
module source. Use spec_from_file_location() which sets origin correctly.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: hugocasa <hugo@casademont.ch>
* fix: confine path-scoped jobs:run tokens to their runnable's jobs
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: project singlestepflow onto its runnable and confine kind-only run scopes
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: keep every by-id job read reachable by a jobs:run token
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: whitelist the dbt and wac-approval by-id job reads for run tokens
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: let an apps:run scope satisfy job-read confinement for that app's runs
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: apply run-scope confinement on top of the approval-token read bypass
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: confine the resume-secret job reads to the run scope as well
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: let the global AI chat call connected MCP servers as the user
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: address review findings on the chat MCP tools
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: connect MCP servers from a predefined list in chat and agent steps
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: show the OAuth redirect URL in the instance connect settings
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: clarify the OAuth redirect URL copy in instance settings
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: match the instance settings warning style and drop the redirect tooltip
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: use the standard warning alert for the redirect url mismatch
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: correct the GitHub token guidance in the MCP registry
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: warn when an OAuth connect lacks the scopes an MCP server needs
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: request the connect's scopes when the oauth popup is opened directly
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: connect an oauth-app MCP server without leaving the panel
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: seed connect scopes from the instance config only
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: make the chat use only the MCP servers you turn on
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor: align the MCP connect UI with the design system
* feat: make a pasted url the default way to connect an mcp server
* feat: show provider icons on the suggested mcp servers
* fix: make both mcp sign-in paths behave the same and stop reloading on toggle
* fix: clarify the mcp tool step's server field and drop its info alert
* fix: name the mcp resource in the tool step and move the transport note into the connect box
* fix: drop the redundant description on the mcp resource field
* fix: make the mcp connections trigger icon-only
* fix: scope enabled mcp servers to the account and address review nits
* fix: wait for connect scopes and create session connections in the operating workspace
* feat: move mcp connections into the chat's plus menu and fix review findings
* fix: show mcp servers as checkboxes so off reads as a state
* feat: give menu rows an on/off switch and use it for mcp servers
* fix: lead the mcp menu rows with the switch
* feat: keep the menu open while toggling and simplify the connect card
* fix: ask for the server before the credential in the connect card
* fix: show one credential path at a time in the connect card
* fix: label the path field and move token guidance into its tooltip
* fix: open straight into connect and keep the server menu scannable
* feat: warn when an mcp connection lands outside your own space
* refactor: require the workspace on the mcp connect components and rename the oauth child
* fix: replace the oauth variable on reconnect and bound every mcp result
* feat: show a connected server's provider icon in the connections list
* feat: resolve mcp provider icons from the url and clarify the path field
* style: align the mcp connect card with the design system surfaces
* style: drop the redundant oauth support line and name the scopes oauth scopes
* feat: keep the mcp connect card open in the connections drawer
* feat: preopen the mcp connect card under the agent step resource picker
* feat: resolve a typed mcp url to its registry entry and describe the token field
* style: name both mcp connect actions connect
* style: name the mcp oauth actions connect with the provider
* style: say in the path description what the connect action will save
* style: name the resource type in the mcp connect path description
* feat: cache mcp provider icons and confirm disconnect in a modal
* fix: keep the mcp menu switches live and the disconnect modal above the drawer
* style: fall back to the plug icon in the mcp menu rows
* fix: never destroy a foreign variable or resource when connecting an mcp server
* fix: prove a token variable is ours before writing it and bound mcp search failures
* fix: pin an mcp oauth popup to the target it was opened for
* fix: bind an mcp credential to the server and popup it was requested for
* fix: bound mcp tool calls with a deadline and drop stale server listings
* fix: keep the disconnect confirmation handler returning void
* fix: tie the mcp tool cache to the resource revision and the grant to its scopes
* fix: verify mcp read-only server-side, keep oauth connector mounted
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>