* feat: add per-route CORS origin allowlist for HTTP triggers
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: fail closed on cold router cache and invalid origin input
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: resolve CORS route from the decoded path like the request handler
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat: add instance-wide default allowed origins for HTTP routes
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: let non-superadmins read the default allowed origins setting
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat: badge the advanced section when a route's origins are restricted
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: state inherited origins on the control and use one hint row
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: trim the origins tooltip and relabel the toggle when a default exists
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: keep the origins format hint visible until an entry is wrong
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: state the at-least-one requirement in the origins hint
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: import the origins validator in the trigger-http tests
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: make an empty allowlist deny rather than fall back to the default
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: address review nits on origin validation and the CORS editor
* fix: derive the origins error from the stored list and tighten host validation
* fix: parse real IPv6 hosts and refuse a newly emptied allowlist
* refactor: make origin validation advisory except for null and non-ascii
* feat: let an empty allowlist be saved as deny every origin
* docs: document the empty allowlist as deny every origin
* fix: bound allowlists, reject commas, and decide cors after the handler
* chore: revert unrelated rustfmt churn in windmill-common tests
* chore: revert unrelated rustfmt churn in windmill-common
* chore: drop the route types the cors restructure replaced
* fix: take the stricter cors decision from before and after the handler
* fix: strip runnable cors headers when the routers are unavailable
* docs: document the allowlist bounds in the openapi schema
* fix: let an unavailable cors read defer to one that resolved
* refactor: carry the resolved cors policy from the handler to the middleware
* docs: describe why an unavailable read fails closed on the paths that reach it
* fix: validate the default origins on the declarative settings path
* test: keep the webhook doc comment with the test it describes
* fix: warn on impossible schemes and ports, and validate the instance setting
* feat: treat an empty allowlist as unset at both levels
* perf: decode the cors path only when the fallback needs it
* docs: document the empty allowlist as unset in the api schema
* docs: describe an empty allowlist as unset in the frontend comments
* docs: say what a null allowlist resolves to, not what it meant before the default existed
* docs: state what the validator refuses and why methods stay broad
* feat: exempt static asset routes from the origin allowlist
* fix: hide the origin control for every static target, not just websites
* fix: exempt only static websites, not single-file static assets
* fix: warn on an unclosed ipv6 host in the origins advisory
* fix: require assets present, not just the static website flag
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
A settings save reported the fork pointers left resolving to nothing only
for the names in `deleted_datatables`, which `wmill sync push` never sends.
The save now works out what it removed from the locked entries, and the
CLI prints the stranded pointers it returns.
Also correct the replication helper's contract: no role or admin check
makes a replication connection safe, so a data table under roles is
refused outright rather than gated as an admin operation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BjfMkJyKzodxkobqGZ6Lqb
* feat: let a native trigger be disabled without deleting it
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: show and control the native trigger pause outside the flow editor
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: create a native trigger already paused instead of pausing it after
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: correct the native trigger enabled comments for create-time init
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Putting it inside `custom_instance_pg_databases` was the wrong call, and it cost two ways.
The catalog serializes a generated Postgres password per role, and that row is the
operator-facing instance config, so the passwords reached `get_instance_config` and its YAML
editor — a live cluster credential in a response body, a UI field and any log of either.
Worse in the other direction: `to_settings_map` strips the catalog, so a full-row upsert of
that key writes the row back without it and the catalog is gone, while the cluster keeps every
login it described.
`custom_instance_replication_pwd` is the precedent and says exactly why — a generated secret,
written only by the server, never operator-authored, hidden so the config machinery cannot
read, rewrite or drop it. The catalog is the same thing, so it now has the same shape:
`datatable_roles`, in `HIDDEN_SETTINGS`, `PROTECTED_SETTINGS` and the agent-worker denylist.
No redaction to keep in step with three code paths, and no way for a neighbouring write to
take it out.
Two races on the same shared documents. `edit_datatable_config` read the stored data tables
outside its transaction and then wrote the whole `datatable` document, so a permissions save
committing in between was silently rolled back; it now reads under `FOR UPDATE`. And
`set_datatable_permissions` validated role ids against the catalog before opening its
transaction, so a deletion in between let it write a deleted role back — including as the
default, which every later job then fails on; it now holds the catalog lock and the settings
row across validation and write.
Completes the authorization contracts the previous commit claimed but did not finish:
`read_datatable_entry` (which it named and missed), `resolve_governing_datatable`, whose whole
job is to answer for a workspace the caller may not belong to, and
`converge_connect_grants_with`, which had not inherited its wrapper's.
Also the generic Python SDK reference: `_format_py_params` learned the bare `*` last time, but
`extract_py_functions` is a second formatter and still rendered `datatable(name, role)`, so
code written from that page passed a keyword-only argument positionally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR
* feat: recognize `// volume:` mounts in PHP scripts
Volume annotations were parsed for every language but PHP, so a PHP script
could not mount a workspace volume. Two things stood in the way: PHP had no
entry in the comment-prefix maps, and a PHP script opens with `<?php`, which
ends the leading comment block the parsers scan before any annotation is read.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3FR7iS9nRhpFt615cnuQ7
* fix: tolerate a PHP opener that carries code, drop the inert CLI hunk
The open-tag skip matched `<?php` exactly, so `<?php declare(strict_types=1);`
still ended the leading comment block and every annotation below it was silently
ignored. Match the tag as a case-insensitive prefix and skip the whole line.
The CLI local-graph hunk could never fire: PHP has no wasm asset parser, so
`fallbackParse` handles it, and its own header scan stops at `<?php` — the script
is dropped as a non-pipeline-member before any volume asset is read. Making only
the CLI PHP-aware would also put the local graph out of parity with the deployed
one, whose `parse_pipeline_annotations` stops there too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3FR7iS9nRhpFt615cnuQ7
* docs: correct the CLI mirror comment, state the own-line annotation rule
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3FR7iS9nRhpFt615cnuQ7
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
A data table backed by the instance database resolved to exactly one Postgres connection,
`custom_instance_user`, for everyone who could reach it at all. There was no way to say
this job reads, that one writes, this one never sees the salaries table.
A data table role is now a real Postgres login on the cluster, defined once for the
instance by a superadmin and named exactly as they named it. A script that declares
`-- role analytics` connects as `analytics`, and Postgres decides what it may touch —
grants are ordinary SQL. Windmill answers only "may this caller ask for this role", from
the tenant lists on the data table entry: `u/alice`, `g/analysts`, `f/finance` or `*`.
A data table with no `permissions` block behaves exactly as before.
Everything that opens a connection on someone's behalf goes through one chokepoint,
`get_datatable_resource_from_db`, which takes the identity explicitly and fails closed when
there is none. The role logs in as itself — never `SET ROLE`, which a script could
`RESET ROLE` its way out of.
A fork's data table entry becomes a pointer at the workspace that governs it rather than a
copy of it. The settings clone used to hand a fork a byte-identical entry naming the
parent's database, which a fork admin could edit to grant themselves `admin` there; a
pointer has nothing local to edit, and its tenants are evaluated as a member of the
governing workspace, by email. `permissions` is stripped from the workspace export and
ignored on import: tenants name principals of one workspace, and a settings push is not
where an access decision should be made.
Operations that see the whole database whatever the roles grant stay with the governing
workspace's admins: editing the roles, a migration that declares none, and opening a
replication stream for a Postgres trigger or capture.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR
* fix(cli): keep permissioned_as on single-item push, as sync push does
* fix(cli): resolve syncBehavior from the target workspace, not the branch alone
* refactor(cli): share the workspace-name resolution between sync and single-item push
* test(cli): import the moved workspace-name helper from its new home
* docs: teach agents to pass a resource as $res:<path> in run arguments
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjRARL7JA7xm772iJP4mJk
* docs: extend run-argument rule to in-editor chats, fix run-as wording
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjRARL7JA7xm772iJP4mJk
* docs: tighten resource run-argument rule after review
- Drop the false rationale that "$var:" only works inside a resource value
from the write_variable description and its runtime rejection message; keep
the rule (a variable cannot reference itself).
- MCP resource-argument description: the title fallback renders "No title",
so say the title is only a label rather than that it can be empty. Guard the
real-newline fix with asserts in the existing enrichment test.
- Eval: assert the full "$res:f/evals/global/github_main" value as one prefix
so a wrong path with a right prefix fails.
- resources.md: narrow "a trigger's payload" to its configured static args.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjRARL7JA7xm772iJP4mJk
* docs: scope the run-argument rule to global chat, add an exact eval matcher
The ai_evals A/B on the two in-editor modes showed no effect: script mode
sonnet 5/5 both with and without the description, flow mode sonnet 5/5 and
haiku 5/5 on the baseline alone. A flow's input schema already carries
`format: resource-<type>`, so those modes have a signal global mode does not
give. Revert both files to keep the tool schemas free of a description that
buys nothing per iteration; global mode keeps it, where haiku goes 0/5 -> 5/5.
Add `stringEqualsAnyOf` to toolCallArgs and use it for the resource reference:
nothing in the eval resolves the value, so a prefix match accepted a near-miss
path like `$res:f/evals/global/github_main_backup`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjRARL7JA7xm772iJP4mJk
* docs: address cubic review — CLI wording, mock resource getter
- `-d --data` help on all four run/preview commands: give $res: and $var:
their own clauses instead of a parenthetical that read as if a resource
were a kind of variable.
- Mock backend: `getBenchmarkResource` now resolves AI-provider seeds as well
as plain ones, so it agrees with `existsResource` and `listResource` — both
report either kind, and a case that listed a resource and then read it by
path got a row it could not fetch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjRARL7JA7xm772iJP4mJk
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: connect to dev server instead of localhost
* fix: derive WebSocket scheme from location.protocol
Mirror the protocol-aware pattern used by initSqlWebSocket in dev.ts
so the WebSocket connects over wss:// when the dev server is reached
through an HTTPS proxy/tunnel, avoiding mixed-content blocking.
* refactor: drop now-unused port parameter of wmillTsDev
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HsfdN82yP88qyQ3h8Lwv2v
---------
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(ai-chat): make reusable skills ai_skill resources you select per workspace
* chore: pin the ee ref to the skill telemetry counters
* fix: address review findings on skill authoring, import and migration
* fix: enforce skill selection in read_skill and stop imports clobbering resources
* feat: carry format_extension from the hub into synced resource types
* fix: let an edit set or clear a resource type's format_extension
* fix: regenerate the sqlx cache and close the review round findings
* fix: close the round-2 findings on folder ACLs, cached sync and truncation
* refactor: make the skills migration non-destructive and use design-system inputs
* fix: close the round-4 findings on folder owners, startup sync and truncation
* fix: clear obsolete extensions, guard folder owners, and report skipped skills
* fix: honor explicit-null extensions and report same-type migration conflicts
* fix: scope skill actions to the committed workspace and paginate the listing
* fix: keep the drawer scoped to the live workspace and surface truncation
* fix: discard a skills refresh for a workspace the chat has left
* chore: update ee-repo-ref to 6efe7a73c745c2e1377a34498523c00d89010a3d
This commit updates the EE repository reference after PR #764 was merged in windmill-ee-private.
Previous ee-repo-ref: 55998c142bc72edd08532748af1974b16035658d
New ee-repo-ref: 6efe7a73c745c2e1377a34498523c00d89010a3d
Automated by sync-ee-ref workflow.
---------
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* fix: keep a local dbt descriptor under sync pull --keep-deleted
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174o6mGTWoanipUgf5zNcVL
* fix: keep an added-shaped dbt descriptor removal under --keep-deleted too
A stateful pull compares `.wmill`, not the working tree, so a descriptor
missing from that map still arrives as `added` while a real file with the
project's warehouse and run arguments sits on disk. Counting only `edited`
left that file deletable, and silently: the flag logged nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174o6mGTWoanipUgf5zNcVL
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: strip the script/ prefix from trigger error handler paths
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: strip the script/ prefix when collecting trigger handler refs
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: relocate prefixed trigger error handlers on project retarget
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: reject a prefixed error_handler_path on triggers instead of resolving it
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: describe error_handler_path as a bare script path in the api schema
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(cli): keep svelte component styles in the raw-app bundle
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test: fold svelte style guard into the plugin test file
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: record the editor-parity constraint on the svelte css option
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(cli): pin esbuild's service cwd before any test file chdirs
esbuild's node API captures process.cwd() when its module is first
imported and spawns its service with that cwd on every (re)start.
createBundle stops the service after each bundle, so the cwd is reused
across the whole run.
Several test files chdir into a temp dir and delete it afterwards. The
first one to bundle therefore pinned the service to a directory that
stopped existing, and the next test to reach esbuild died with
The service was stopped: ENOENT: no such file or directory,
posix_spawn '.../@esbuild/linux-x64/bin/esbuild'
The binary is present; ENOENT is posix_spawn rejecting the missing cwd.
Which file tripped it depended on bun's readdir order, so renaming an
unrelated test file was enough to surface it. Importing esbuild from the
preload pins the service to a cwd that outlives the run, independent of
file ordering.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G88YF3sZFnJZUvTLVjqhZc
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
* fix: keep raw-app files within their app folder on sync pull
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: validate raw-app file keys as stored, closing nul and duplicate-field bypasses
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: guard raw-app runnable ids too and fail closed on unparseable value
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: strip only a leading slash on raw-app file keys to match backend
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: strip only a leading slash on raw-app file keys to match backend
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>