mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-19 08:01:25 +00:00
23bf6bf3da01d552d2dfab6dbcfd30f758ed34d2
1758 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
119d94601d |
feat(jobs): auto-grant approvers a run-detail view link on the approval page (#9686)
* feat(jobs): auto-grant approvers a run-detail view link on the approval page Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(approval): show a clear run-details button for logged-in workspace members Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(approval): build run-details link from route params, not undefined job getJob is fire-and-forget on the new approval page and is denied for an approver who lacks direct run read access — the exact case this link serves — leaving job undefined and producing /run/undefined. page.params.job is the flow id the view_token is minted for. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6833a554ae |
fix: allow users to always discard their own drafts without write permission (#9659)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Ruben Fiszel <ruben@windmill.dev> |
||
|
|
924f9c7e8d |
fix(backend): strip NUL bytes from draft values on write (#9673)
draft.value is a json column (not jsonb), so a client could store a U+0000 escape in it. Any later text extraction (`->>` / `to_jsonb`) on such a value raises 22P05 "unsupported Unicode escape sequence" — one poisoned draft 500'd the whole GET /drafts/list, silently hiding the home-page "This workspace has N drafts" banner (and breaking the global drafts page). Prevent it at the source: sanitize the value in update_draft (the only path that writes client-supplied draft content) so a NUL never reaches the column. strip_json_nul does a single backslash-parity-aware byte pass that removes real NUL escapes (values and keys alike) while leaving a legitimate escaped backslash intact — O(n) with no serde_json::Value tree to allocate, important because the slow path is also hit by any value legitimately containing the text after a backslash (e.g. script source). The clean path is a single substring check. A SQL migration scrubs rows written before this, gated to genuinely-poisoned rows (a real NUL makes value::jsonb raise, distinguishing it from a legitimately escaped backslash). With the data clean, no read-side query needs to change. Tests: unit tests for the strip helper (escaped-backslash no-op, real+literal collision, odd-backslash-run parity, nested keys/values) and an integration test that POSTs a NUL-bearing draft and asserts it is stored and listed NUL-free (fails without the strip). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
19bc0052f1 |
fix(backend): include raw_app drafts in list_apps draft_users (#9647)
The home list's draft user badges come from list_apps' `draft_users`
subquery, which only matched `draft.typ = 'app'`. `app` and `raw_app`
are separate draft kinds over the one `app` table, so a deployed raw app
with a pending draft had `is_draft = true` (the join already matches both
kinds) but an empty `draft_users` — the row showed a "Draft" badge with
no owner badge. Match `typ IN ('app', 'raw_app')`, consistent with the
`is_draft` join and the draft-only query.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
7155a0bb96 |
feat: Data Pipelines alpha (#9193)
* feat: add workspace asset graph view Workspace-wide canvas of assets and their producer/consumer scripts, reachable from the assets page. Left-to-right layered layout via d3-dag sugiyama, rendered with @xyflow/svelte (same stack as the flow editor). GET /w/:ws/assets/graph returns deduped nodes + edges. Follow-ups: filters (kind/folder/search), node detail drawer, inline script edit from a clicked node. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * all * all * all * update * all * all * all * feat(pipeline): output-kind picker and per-(lang, output) templates Add a third stage to PipelineInsertMenu that asks what kind of asset the new script will produce (datatable / ducklake / s3 parquet / s3 object / none). The picked kind drives a real wmill SDK skeleton — typed datatable inserts, ducklake CREATE+INSERT, s3 parquet COPY, etc. — with the upstream asset auto-wired as the input source when added from an asset node. Reorder languages to bun → duckdb → python → sql so data-shaped languages surface first. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * all * chore(main): release 1.693.4 (#8994) * chore(main): release 1.693.4 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> * feat: ansible delegate_to_git_repo install_requirements, dynamic fields, --limit (#8997) * feat: ansible delegate_to_git_repo install_requirements, dynamic fields, --limit Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: include .yaml variants in collections/roles requirements lookup Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> * fix(cli): only preserve case for raw-app runnableIds, not app/flow summaries (#9000) * fix(cli): only preserve case for raw-app runnableIds, not app/flow summaries PR #8940 stopped lowercasing in sanitizeForFilesystem to fix #8939, where a raw-app runnableId like CamelCaseTSRunnable produced a CamelCase YAML metadata file but a lowercased code file, making them desync and register as duplicate runnables on push. That fix overshot. sanitizeForFilesystem is also reached by newPathAssigner, which serves normal apps and flows where the input is the script's human summary ("Get Users Data") rather than an identifier. There the on-disk filename is the only artifact — there's no companion YAML to keep in sync — so lowercasing was the right behavior. Removing it changed both the on-disk filename and the !inline reference in app.yaml / flow.yaml from get_users_data.inline_script.ts to Get_Users_Data.inline_script.ts on the next pull, surfacing as unwanted case churn for users updating to 1.693.x. Add a preserveCase option to sanitizeForFilesystem (default false → lowercase). newRawAppPathAssigner opts in; newPathAssigner stays on the default. Update unit tests accordingly and add an end-to-end raw-app round-trip in raw_app_sync.test.ts that pushes a CamelCase backend runnable, pulls it back, and asserts both YAML and code file preserve case with no lowercase orphan. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(cli): use readdir for exact-case orphan check on Windows The CamelCase round-trip test used fileExists("camelcasetsrunnable.ts") to assert no lowercase orphan was produced, which false-positives on Windows since the filesystem is case-insensitive and resolves the lookup to the existing CamelCaseTSRunnable.ts. Switch to readdir + toContain so the exact on-disk casing is compared identically on Linux and Windows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cli): wmill-lock.yaml auto-fill + --rehash-only + path-prefix dedup (#8978) * fix(cli): canonical lockfile hashes + lock upgrade migration to v3 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(cli): use __app_hash subpath in rehash missing-entry check Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(cli): run sync pull lockfile auto-fill regardless of changes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: regenerate system prompts for new lock and rehash-only commands Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(cli): address review feedback on lock upgrade Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(cli): drop v3 marker; always run fallback; fail-fast on unknown lockfile version Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(cli): drop yaml-round-trip legacy hash variant; recover via --rehash-only Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(cli): include legacy hash in script push staleness warning check Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * revert(cli): drop canonical hash formula; keep raw-bytes hashing Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * perf(cli): reuse change-tracker map for sync pull lockfile auto-fill Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(cli): address review feedback on rehash-only Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test(cli): pin lockfile hash + yaml format and cover regression cases Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test(cli): byte-stable snapshot tests for flow.yaml format Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test(cli): add app and script-metadata yaml snapshot fixtures Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(cli): address claude review on rehash-only Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor(cli): factorize script-path to remote-path derivation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(cli): address claude + cubic review (dry-run mutation, rehash short-circuit) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor(cli): make rehash a subcommand and factorize fs walks Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(cli): normalize line endings in yaml snapshot tests for windows ci Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(cli): address review feedback on rehash + auto-fill - Flat-layout scripts now clearGlobalLock before rehash write so legacy ./-prefixed duplicates get cleaned up (matches flow/app behavior). - Add MalformedLockfileError; sync pull auto-fill re-throws it alongside UnknownLockVersionError instead of silently warning + continuing. - Document the legacy step-removal false-negative in isFlowDirectlyStale / isAppDirectlyStale and the categorizeLocalFiles ignore-filter invariant. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> * fix: use otel.status_message for OTLP Status.message on failed jobs (#8995) tracing-opentelemetry only recognizes otel.status_code and otel.status_message as fields that map to the OTLP Status proto. The previously-used otel.status_description fell through to the generic attribute recorder, leaving Status.message unset and preventing OTLP consumers from filtering spans on error status. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: route email trigger path through standard info channel (#8996) * docs(skill): document email triggers and S3 attachments Add an "Email triggers" section to the triggers skill covering the local-part config, the parsed_email/raw_email/email_extra_args payload, the URL-style extras convention, where to find trigger_path (only with a preprocessor, at event.trigger_path), and — most importantly — that binary attachments are uploaded to the workspace S3 bucket and surface as `{ s3: "windmill_emails/<job_id>/attachments/<filename>" }`. Scripts must use wmill.loadS3File / wmill.load_s3_file to read them. Also pulls EmailTrigger into the schema mappings so a real `email_trigger.schema.yaml` is generated, and adds Email/Azure to the trigger kinds list in the CLI agent guidance. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: update ee-repo-ref for email trigger path fix Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: update ee-repo-ref to 26184ab7a4aadfc529dcedf038aa08d36c7ad381 This commit updates the EE repository reference after PR #553 was merged in windmill-ee-private. Previous ee-repo-ref: 318a46897a605dc9be3817901f35ba5a99a0a525 New ee-repo-ref: 26184ab7a4aadfc529dcedf038aa08d36c7ad381 Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com> * update git sync version to 1.693.5 * fix: pair PG arg type with actual Rust binding to keep query_typed_raw safe (#8999) * fix: pair PG arg type with actual Rust binding to keep query_typed_raw safe Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(pg): wrap encoder errors with arg context, add fallback test Followups on #8999 review: - Wrap rust-postgres "error serializing parameter N" failures with the arg name, JSON value kind, and asserted Postgres type plus a hint about an explicit cast — so users see actionable context instead of an opaque WrongType. - Drift-prevention meta-test: assert otyp_to_pg_type and convert_val agree on the Type for every recognised arg_t when the JSON value matches its natural Rust kind. Catches future drift if either side changes. - Integration test for the prepare + query_raw fallback path: confirms unrecognised arg_t (custom enum) is routed through prepare and the server-resolved type appears in the failure surface — flips into a test failure if a regression accidentally routes unrecognised types through query_typed_raw. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(pg): add otyp_inferred flag + regex-based placeholder renumbering Two follow-ups from the review of #8999: 1. **Issue #1 (Number/Bool + explicit text decl in WHERE)** Add `Arg::otyp_inferred: bool` to the parser. The PG SQL parser sets it `true` only at the "no info → fall back to text" site (bare `$N`, no inline cast, no `-- $N (TYPE)` decl). All other arg sources keep it `false`. In `convert_val` this flag distinguishes: - explicit text-like target (`-- $1 (text)` or `$1::text`) — coerce `Bool`/`Number` → `Box<String>` so `WHERE text_col = $1` works (`text = text` operator). Pre-#8988 behaviour, restored. - parser-default text (bare `$N`) — bind the value's natural Rust type so the regression case (`Value::Bool` against a real `bool` column via `CAST AS bool`) keeps working. `Arg` is in `windmill-parser`; the new field has `#[serde(default)]` so persisted signatures stay backward-compatible. 2. **Issue #4 ($5/$50 substring rewrite collision)** Replace the per-index `String::replace` chain (which turned `$50` into `$10` when oidx=5 was processed first) with a single regex pass. `\d+` is greedy, so `$5` and `$50` match as distinct units; indices outside the mapping are left intact. 3. Tests: - parser: `test_parse_pgsql_otyp_inferred_flag` covers bare/inline- cast/decl/mixed shapes. - executor unit: `convert_val_bool_against_every_arg_t` and `convert_val_*_number_*` split each text-like target into explicit vs inferred expectations. - executor unit: `renumber_sparse_placeholders_no_collision`. - integration: `test_postgresql_arg_type_combinations` adds 4 cases covering decl(text)+Number/Bool in WHERE, bare $1+Bool, and sparse positional args ($5/$50). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(pg+sdk): enum support, extended String arms, position-aware $N rewrite, SDK quality Backend: 1. **`AnyTextValue` ToSql/FromSql wrapper**: vanilla `tokio_postgres`'s `ToSql for String` / `FromSql for String` reject `Kind::Enum` and `Kind::Domain` even though the wire format is plain UTF-8. The wrapper accepts those kinds in both directions. End result: explicit `$1::my_enum` / `CAST($1 AS my_enum)` casts now round-trip without the ugly `CAST($1::text AS my_enum)` workaround, AND `SELECT enum_col` results come back as JSON strings instead of erroring at the FromSql layer. 2. **#10 — Value::String → numeric/real/double/oid/bool**. Without these arms, a string-encoded value (`"3.14"`, `"true"`) for a non-text / non-temporal arg_t fell through to `Box<String> + TEXT`, which then failed at the server (no implicit cast text→numeric in expression context). Now strings are parsed into the matching native type with clear error messages on parse failure. 3. **Position-aware `$N` rewrite**: replaces the regex-based renumbering (which fixed the `$5/$50` substring collision but still walked through string literals and comments, mangling `'price: $5'` etc.) with a walk over `parse_pg_statement_arg_positions` — the same string/comment/dollar-quote-aware tokenizer used for index discovery. Adds `parse_pg_statement_arg_positions` to the parser's public API. SDK: 4. **BigInt support**: `JSON.stringify(BigInt)` throws. The SDK now stringifies bigints before serialisation; the executor accepts numeric strings into BIGINT arg slots via the existing `Value::String → INT8` parsing arm. SDK-side `inferSqlType` is split so `BigInt` always resolves to `BIGINT` (was reaching `Number.isInteger(BigInt)` which returns false → wrong default). 5. **Homogeneous array auto-tag**: `${[1,2,3]}` against an `int[]` column now emits `$1::BIGINT[]` instead of `$1::JSON`. Detection covers primitive types only (number / bigint / string / boolean); mixed or nested arrays still fall back to JSON. Mixed int/float widens to `DOUBLE PRECISION[]`. 6. **`.query()` positional bug**: previously the `.query()` method abused the template-tag builder, which appended `$N::TYPE` after the user's literal SQL string instead of binding by position (`SELECT $1, $2` became `SELECT $1, $2$1::BIGINT`). Now `.query()` builds the executor-shaped content directly: a `-- $N argN (TYPE)` declaration block followed by the user's SQL verbatim. Tests: - Parser: `test_parse_pg_statement_arg_positions_skips_strings_and_comments` asserts string literals, comments, and dollar-quoted blocks don't produce positions (so renumbering doesn't mangle them). - Executor unit: `renumber_sparse_placeholders_no_collision_no_string_mangling` uses the new position-aware path and includes string-literal + comment + `$$…$$` cases. Existing convert_val tests grow to cover new String→numeric/real/double/oid/bool arms. - Integration: `test_postgresql_arg_type_combinations` adds 13 cases (enum round-trip both directions, string→numeric/real/double/bool/oid, string-literal `$N` non-mangling). The prepare-fallback test now asserts SUCCESS (not failure) for enum encoding via AnyTextValue. - SDK: new `typescript-client/tests/sqlUtils.test.ts` (42 tests) exhaustively covering inferSqlType primitives + arrays, parseTypeAnnotation, datatable() template tag (with all the new shapes — BigInt, homogeneous arrays, RawSql, schema preamble), datatable().query() positional, and ducklake() shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(pg): replace DISCARD ALL with curated reset (preserves typeinfo cache) Found while exhaustively probing custom-type DX: every cached-connection reuse was running `DISCARD ALL`, whose included `DEALLOCATE ALL` deallocates *all* prepared statements server-side — including the typeinfo statements that tokio_postgres caches per-Client to resolve custom enum / domain Oids. tokio_postgres still held `Statement` objects whose names the server had forgotten, so the next custom-type query failed with intermittent "prepared statement \"sN\" does not exist" errors. The failure was easy to reproduce: any sequence that forced typeinfo lookup for two different custom-type kinds on the same cached connection (e.g. enum followed by domain) would hit it. Replace `DISCARD ALL` with a curated reset that explicitly targets the state we actually care about, *without* touching prepared statements: RESET ALL — GUC parameters (search_path, application _name, statement_timeout, …) RESET SESSION AUTHORIZATION — undoes both `SET SESSION AUTHORIZATION` and `SET ROLE` (RESET ALL does NOT — these aren't GUC parameters, so without this an elevated role from a previous job would silently leak) UNLISTEN * — drops LISTEN registrations CLOSE ALL — closes open cursors Trade-off: temp tables, advisory locks (session-scoped), and user-created PREPARE statements may persist across cached-connection reuse — rare in datatable / PG-script workloads. tokio_postgres's typeinfo cache survives intact, so custom enum / domain queries are fast on subsequent reuse. Tests: - `test_postgresql_custom_types_on_cached_connection` — runs 10× alternating enum + domain queries on a cached connection. Pre-fix this failed with `prepared statement "sN" does not exist` after the first reuse; post-fix passes. - `test_postgresql_set_role_does_not_leak_across_cached_connection` — switches `SET ROLE` and `SET SESSION AUTHORIZATION` to a non-postgres role, then runs a follow-up job and asserts current_user/session_user are restored. Specifically catches the case where someone might switch back to `RESET ALL` alone (which doesn't cover SET ROLE / SESSION AUTHORIZATION) and silently introduce a permission-leak vector. - All existing session-isolation tests (`test_postgresql_cached_connection_resets_session`, `test_postgresql_single_worker_session_isolation`, `test_postgresql_100_jobs_cached`) continue to pass. Found via end-to-end probing of datatable / PG-script DX, not previously covered: the existing isolation tests only did `SET ROLE postgres`, the connecting user, so the leak was invisible. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(pg): address PR #8999 review (cubic + claude) cubic (P1, real bug): - `convert_vec_val` for `timetz` array asserted `Type::TIMETZ_ARRAY`, but chrono `NaiveTime` only encodes for TIME (same caveat as the scalar arm). Switch to `Type::TIME_ARRAY`; rely on PG's implicit `time→timetz` assignment cast at the column site. Add an explicit unit test. claude (#1, silent failure → explicit error): - `Bool` + explicit `(char)` / `(character)` decl previously silently bound BOOL, hoping the server would cast at the use site — but PG has no implicit `bool→char` and the resulting error ("operator does not exist: bool = char") was opaque. Now error at bind time with an actionable hint to use `bool` decl or pass the value as a "t"/"f" string. claude (#2, asymmetry doc): - Object/Array still coerce to text on `matches!(typ, Typ::Str(_))` (covers both explicit AND inferred-default text), unlike Bool/Number which key on `explicit_text_target`. The asymmetry is intentional (no implicit `jsonb → text` cast in expression context vs PG having implicit `bool/int → text` casts) — added a body comment so future maintainers don't try to "align" them. claude (#3, perf): - `parse_pg_statement_arg_indices` and `parse_pg_statement_arg_positions` walked the SQL tokenizer twice. Fold into a single pass that derives the index set from the position list. claude (#4, fmt drift): - `cargo fmt` over the parser crates I touched with perl scripts in the earlier commit (windmill-parser-{sql,bash,ts,go,php,java,csharp,nu,py, rust,graphql,yaml,r}). Net cosmetic. claude (#5, parseTypeAnnotation): - One-line caveat in the SDK's `parseTypeAnnotation` that the returned string is presence-only (e.g. `${x}::DOUBLE PRECISION` returns `"DOUBLE"`, `CAST(${x} AS int)` returns `"int)"` — neither matches a real PG type, but the only consumer just checks `!== undefined`). While here — discovered + fixed independently while exhaustively probing DX: - **Replace `DISCARD ALL` with curated reset** (`RESET ALL; RESET SESSION AUTHORIZATION; UNLISTEN *; CLOSE ALL;`). DISCARD's `DEALLOCATE ALL` killed tokio_postgres' typeinfo cache, producing intermittent `prepared statement "sN" does not exist` errors on custom-type queries after cached-conn reuse. New regression tests: `test_postgresql_custom_types_on_cached_connection` and `test_postgresql_set_role_does_not_leak_across_cached_connection` (the latter catches the case where someone might switch back to `RESET ALL` alone and silently introduce a permission-leak vector — RESET ALL doesn't cover SET ROLE / SET SESSION AUTHORIZATION). - **ISO-8601 timestamp results** (`pg_cell_to_json_value`). Pre-fix `TIMESTAMP` was rendered with a space separator ("2024-01-15 10:30:00") and `TIMESTAMPTZ` with " UTC" suffix ("2024-01-15 10:30:00 UTC") — neither parseable by `date-fns parseISO`, JavaScript `new Date()` is lenient enough to handle them but several frontend `App*Input.svelte` components use parseISO and fail silently. Switched to ISO-8601 with `T` separator and `+00:00` offset; arg-parsing path still accepts the legacy " UTC" suffix for back-compat. Test coverage: - 17/17 unit (`pg_executor::tests`) - 9/9 integration (`backend/tests/worker.rs`, `test_postgresql_*`) - 27/27 parser (`windmill-parser-sql`) - 42/42 SDK (`typescript-client/tests/sqlUtils.test.ts`) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(pg): bounded one-shot warning on numeric precision loss + ISO-8601 + NaN handling Found while probing PG-script DX with millions of numeric cells: 1. **Numeric precision-loss warning**: `numeric` results are still serialised as JSON Number (back-compat — switching to JSON String would silently break user code doing arithmetic on results), but we now detect `Decimal -> f64 -> Decimal` round-trip failure and emit a single job-log warning recommending a `::text` cast in the SQL. Bounded by `NUMERIC_PRECISION_CHECK_BUDGET = 256` cells per query (one atomic load + one fetch_sub on the hot path; first lossy value short-circuits to a single load thereafter). Worst-case overhead on a 1M-cell numeric-heavy query: ~25µs of checks + 5ns × N atomic loads (vs. ~100ms unbounded). 2. **ISO-8601 timestamps**: `pg_cell_to_json_value` previously returned `"2024-01-15 10:30:00"` (TIMESTAMP) and `"2024-01-15 10:30:00 UTC"` (TIMESTAMPTZ) — neither parseable by date-fns `parseISO`, which is what the apps `App*Input.svelte` components use, so timestamp values silently failed to round-trip into date pickers. Switch to ISO-8601 (`T` separator + `+00:00` offset) on the result side; arg-parser continues to accept the legacy `" UTC"`-suffixed format for back-compat. 3. **Float NaN / Infinity results**: `Number::from_f64` returns None for NaN / ±Inf, which `pg_cell_to_json_value` was raising as "invalid json-float" — failing the *entire* query if any cell held one of these special values. Now serialise them as JSON strings ("NaN", "Infinity", "-Infinity") and let the rest of the row come through. Arg-side: `s.parse::<f64>()` already accepts the same strings. Tests: - `decimal_fits_f64_losslessly_predicate` — covers fits / doesn't-fit cases for the precision-loss predicate. - `precision_check_budget_caps_per_query_overhead` — locks in the budget cap and the loss-flag short-circuit. - All 9 PG integration tests + 17 unit tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(pg): add pg_advisory_unlock_all to reset; warn on missing args; honor decl defaults While probing PG-script DX further found three more frictions: 1. **Advisory lock leak** (cubic P2): switching from `DISCARD ALL` to `RESET ALL; RESET SESSION AUTHORIZATION; UNLISTEN *; CLOSE ALL;` meant session-scoped advisory locks (`pg_advisory_lock`) leaked across cached-connection reuse. Add `SELECT pg_advisory_unlock_all()` to the chain — `DISCARD ALL` covered this implicitly via `DISCARD PLANS / DEALLOCATE / pg_advisory_unlock_all` and we lost it in the switch. 2. **Missing-arg silent NULL**: an arg declared in the SQL (e.g. `-- $1 amount (numeric)`) but not provided in the args object was bound as NULL with no error / warning. Misspelling the key in the args object silently produced a row of NULLs — a notorious DX debugging trap. Now: collect the names of declared-but-missing args during dispatch and emit a single one-shot warning to the job logs at end-of-query naming each one. Bound NULL is preserved for back-compat. 3. **Declaration defaults ignored**: `-- $1 a (int) = 5` carries `arg.default = Some(Number(5))`, but the dispatch fell straight to NULL when the arg was missing. Now: respect the default — user-supplied value > declaration default > NULL. Also fixes the warning logic above (only warn for args that *don't* have a default). Tests: existing 19 unit + 9 integration pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(pg): multi-word PG types with [] suffix lost the array-ness; array arms accept stringified values Two more frictions found while probing SDK end-to-end against a real datatable resource: 1. **Multi-word array types lose the [] suffix in the parser**. `transform_types_with_spaces` recognises aliases for "double precision", "character varying", "timestamp with time zone", etc. but its return type was `&'a str` — only the bare alias, never with a trailing `[]`. The `RE_CODE_PGSQL` regex's `\w+` captures stop at the first space, so the regex's own `(?:\[\])?` array-suffix branch sees only `"double"` (not `"double precision[]"`); the `[]` was silently lost. Result: `$1::double precision[]` (which the SDK now emits for homogeneous float arrays via the new auto-tag) routed through `Value::Array → Type::JSONB` and the server failed with "cannot cast type jsonb to double precision[]". Fix: switch `transform_types_with_spaces` to return `Cow<'a, str>` and re-check the trailing bytes after a multi-word match. If they start with `[]`, return `format!("{alias}[]")` — Owned. Single-word types and the no-match path keep returning Borrowed slices, so no allocation in the hot path. 2. **Array arms in `convert_vec_val` rejected stringified values for numeric / int* / bool / oid / real / double**. The scalar `convert_val` already parses strings into the matching native type for these arg_ts, but the array variant only accepted JSON-native counterparts. Sending `["1.5", "2.5", "3.5"]` against `$1::numeric[]` (e.g. via `unnest` for bulk loading, or `JSON.stringify(BigInt[])` round-trip) failed with "Mixed types in array". Now the array arms mirror the scalar ones — `as_<native>().or_else(|| as_str().and_then(parse))` — so both shapes round-trip cleanly. Tests: 19 unit + 9 integration pass; existing parser tests cover the multi-word array forms (the regex-cap behaviour didn't break for single-word types, and Cow plumbing is transparent to all callers). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(parsers): add otyp_inferred field to Arg literals in tests + 3 missed src files CI failures: the perl-driven sweep that added `otyp_inferred: false` to every `Arg { ... }` literal when I introduced the field in the parser schema covered `src/lib.rs` files but missed: - parsers/windmill-parser-bash/src/lib.rs (mass-edited but a later format pass un-applied a few sites) - parsers/windmill-parser-go/src/lib.rs (same) - parsers/windmill-parser-graphql/src/lib.rs (same) - parsers/windmill-parser-nu/tests/tests.rs (test file — not swept the first time) - parsers/windmill-parser-ts/tests/tests.rs (test file — same) Also tightened the regex to handle `oidx: None` without the trailing comma (some test files had the field as the last initialiser line). `cargo build --features <CI feature combo> --workspace --all-targets` is clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(sdk): Date → TIMESTAMPTZ; NaN / ±Infinity → string Two more frictions found while running the actual SDK end-to-end against a live datatable resource: 1. **JS `Date`** fell into the typeof "object" branch and was tagged `::JSON`. It worked accidentally for `${date}::timestamptz` via PG's `json → text → timestamptz` implicit cast chain, but `${date}` against a `timestamptz` column without a user-supplied cast bound the value as a JSON string and the comparison `timestamptz = json` failed. Now: `inferSqlType` recognises `Date` and tags `::TIMESTAMPTZ`; `serializeArgValue` emits `Date.toISOString()` so the executor's `Value::String → TIMESTAMPTZ` arm parses it cleanly. 2. **JS `NaN` / `±Infinity`** silently became NULL. `JSON.stringify(NaN)` returns `"null"` per the JS spec, so the value reached the executor as JSON null — the SDK's `::DOUBLE PRECISION` tag then bound a NULL double. Fix: detect non-finite numbers in `serializeArgValue` and stringify them as `"NaN" / "Infinity" / "-Infinity"`. The executor's `Value::String → FLOAT8` arm (`f64::from_str`) accepts these literals directly, and the result-side already renders the values as JSON strings (matching round-trip). SDK unit tests grow from 42 → 44 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(pg): integration coverage for multi-word arrays + stringified array elements Locks in the two array fixes from the previous commit (`fix(pg): multi-word PG types with [] suffix lost the array-ness`) with end-to-end cases in `test_postgresql_arg_type_combinations`: - `double precision[]`, `character varying[]`, `timestamp without time zone[]` — verifies the parser keeps the `[]` suffix after multi-word alias resolution. - `numeric[]` / `int[]` / `bool[]` from stringified primitives — verifies the array arms of `convert_vec_val` apply the same string-coercion the scalar arms do. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style: fix indentation drift on otyp_inferred lines cargo fmt cleanup of leftover indentation where the perl-driven sweep that introduced the otyp_inferred field landed at the wrong column. No behaviour change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> * feat: support assigning a worker tag to app inline scripts (#9002) * feat: support assigning a worker tag to app/raw-app inline scripts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: omit empty tag field from inline script raw_code payload Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * style: shrink tag popover width --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> * feat(pipeline): 2-col picker, draft path edit, save-all + leave guard Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * all * all * update * fix(cli): forward HEADERS env var on every backend fetch call (#9075) Several `fetch()` callers in the CLI bypassed `OpenAPI.HEADERS` and skipped the `HEADERS` env var, causing requests to fail behind auth gateways like Cloudflare Access (same shape as #6421): - `pushScript()` `/scripts/create` and `/scripts/create_snapshot` — regressed in #8936 when the call switched from `wmill.createScript()` (SDK) to a raw `fetch` for the `skip_if_noop` query param. - Script preview `/jobs/run/preview_bundle`. - App dev `/jobs_u/getupdate_sse` SSE stream. - `wmill docs` `/api/inkeep`. All four now spread `getHeaders()` and call `detectAuthGatewayChallenge()` so a Cloudflare/SSO challenge surfaces a clear error instead of an opaque JSON parse failure. Adds `test/headers_env_var.test.ts`: spins up an auth-gateway proxy that 403s requests missing `CF-Access-Client-Id` / `CF-Access-Client-Secret` and otherwise reverse-proxies to the test backend, then runs `wmill sync push` of a fresh script through the proxy. Negative case (no `HEADERS` env) verifies the proxy actually gates; positive case asserts every request including `/scripts/create` reaches the backend with the headers attached. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cli): add --parallel flag to generate-metadata (#9074) * feat(cli): add --parallel flag to generate-metadata * fix(cli): validate --parallel input and harden flush ordering * perf(flows): skip flow_env DB+transform work when no resolution is needed (#9078) * fix(cli-tests): stabilize flow lock-gen race + Windows path (#9080) * fix(cli-tests): stabilize flow lock-gen race + Windows path Three CLI test failures on the latest main, all flaky on CI: 1. `Mixed Case Paths: pull and push flow with capitalized folder` and `Integration: Mixed scripts and flows with nonDottedPaths are idempotent`: flow create/update queues an async FlowDependencies job that fills inline-script lockfiles and rewrites flow.value. The tests pulled/pushed before the worker finished, so dry-run idempotency saw phantom `*.inline_script.lock` adds and `flow.yaml` edits. Added a `waitForFlowDependencyJob` helper that polls `/flows/get` for the latest `dependency_job` and `/jobs_u/completed/get` until it lands, and called it after each API/CLI flow write in both tests. 2. `HEADERS env var is forwarded on every CLI fetch` (Windows-only, added in #9075): the new test built the CLI entrypoint via `new URL("..", import.meta.url).pathname`, which yields `/C:/...` on Windows and `Bun.spawn` rejected before reaching the proxy, leaving `rejectedRequests.length` at 0. Switched to `fileURLToPath` + `node:path.join` to match `cargo_backend.ts`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(cli-tests): use /flows/deployment_status to actually wait for dep job CI reviewers (Claude, Codex) flagged the prior `waitForFlowDependencyJob` as a no-op: it read `flow.dependency_job` from `/api/w/{ws}/flows/get`, but `Flow` / `FlowWithStarred` (backend/windmill-types/src/flows.rs:20-60) do not include that field. The helper exited on the first iteration without polling. Switch to `/api/w/{ws}/flows/deployment_status/p/{path}`, which returns `{ lock_error_logs, job_id }`. `job_id` is the FlowDependencies UUID written into `deployment_metadata` in the same tx as the dep-job push (backend/windmill-api-flows/src/flows.rs:660-672 and :1275-1292), so by the time the create/update API call returns, the response carries the latest dep-job UUID. Then poll `/jobs_u/completed/get/{job_id}` as before. Local runtime for `mixed_case_paths.test.ts` jumps from ~9s to ~32s, confirming the helper now actually waits instead of returning immediately. The 404 short-circuit in `sync_pull_push.test.ts` still works — `get_deployment_status` returns 404 when the flow is absent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * perf(flows): cache resolved flow_env per flow execution (#9079) * perf(flows): cache resolved flow_env per flow execution * perf(flows): tighten flow_env cache cap to 1024 and clarify memory note * perf(flows): don't cache transient flow_env resolution failures * chore(main): release 1.698.0 (#9076) * chore(main): release 1.698.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> * fix: reject root-rooted paths in ansible playbook validator on windows (#9081) * fix(native-triggers): serialize Google channel renewal across replicas (#9060) * fix(native-triggers): serialize Google channel renewal across replicas `sync_all_triggers` runs every 5 minutes on every windmill-app replica with no leader election. Multiple replicas were each rotating the webhook token, creating a new Google watch channel, and racing the trigger UPDATE — leaving the loser's new token (in `token`) and channel (in Google) orphaned. Cloud was accumulating ~5 leaked tokens/week without the silent best-effort `delete_token_by_hash` ever logging a warning. Wrap each per-trigger renewal in a transaction and acquire the row with `SELECT … FOR UPDATE SKIP LOCKED`. Contending replicas skip the row instead of duplicating the work. The lock spans `rotate_webhook_token` → Google API call → `update_native_trigger_service_config` and is only released on commit. Re-checks `should_renew_channel` after acquiring the lock so a replica that committed seconds earlier doesn't trigger a duplicate renewal. The pattern matches existing batch-cleanup paths in `monitor.rs` (job-retention sweep) and other `FOR UPDATE SKIP LOCKED` call sites. Also logs at `debug!` when `delete_token_by_hash` finds no matching row, so future investigations can distinguish "deleted" from "not found" without changing the `Ok(false)` contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fixup! fix(native-triggers): serialize Google channel renewal across replicas * fixup! fix(native-triggers): serialize Google channel renewal across replicas fixup! fix(native-triggers): serialize Google channel renewal across replicas Address claude review: - #5: per-skip log info -> debug (expected outcome under SKIP LOCKED) - #2: warn moved out of delete_token_by_hash to the call site that knows the expected state (try_renew_channel_locked); other callers are race-prone and shouldn't warn - #3: NULL service_config now warns (anomalous case) - #4: post-Google-API DB-update + commit failures log distinctly so the channel-orphan case is grep-able Plus: add 14d expiry to Google webhook tokens via ServiceName::webhook_token_expiration, mint fresh ephemeral-webhook-{service}-{rd5} labels at create + rotate so the existing 'ephemeral-' filter excludes them from user-token email/critical-alert paths (no filter changes in 3 places). Orphans now self-clean via the existing expiry sweep in monitor.rs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fixup! fix(native-triggers): serialize Google channel renewal across replicas fixup! fix(native-triggers): serialize Google channel renewal across replicas Address second-round review: - Claude #1 (P2): username_override_from_label now strips the 'ephemeral-' prefix for ephemeral-webhook-* labels, so created_by stays webhook-{service}-{rd5} instead of changing to label-ephemeral-webhook-... (preserves audit/job-list filter compatibility) - Codex (P2): updated renew_channel doc — labels are no longer copied; rotate mints fresh ephemeral-webhook-google-{rd5} with 14d expiration - Claude #3 (optional): test_rotate_webhook_token now asserts the rotated Google token has an ephemeral-webhook-google-* label and a populated expiration Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fixup! fix(native-triggers): serialize Google channel renewal across replicas fixup! fix(native-triggers): serialize Google channel renewal across replicas Reconsider the previous fixup: stripping the 'ephemeral-' prefix made created_by no longer match token.label exactly, defeating the linking purpose. Just allowlist 'ephemeral-webhook-' alongside the other recognized webhook/email/ws prefixes — created_by becomes ephemeral-webhook-google-XXXXX, matching token.label exactly. The 'ephemeral-' substring also informs operators that this is a system-managed auto-expiring token vs a user-managed webhook trigger. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(cli): bump svelte version in `wmill app new` template (#9084) * fix(cli): bump svelte version in `wmill app new` template The svelte5 template pinned `svelte` to `5.45.2`, but the Svelte compiler bundled in `wmill app dev` emits `$.delegated('click', ...)` calls. The `delegated` export was added later, so 5.45.2 doesn't have it — esbuild warns `Import "delegated" will always be undefined`, replaces the call with `void 0`, and the page crashes at first event-handler bind (white screen). Bump to `^5.55.5` so the compiler and runtime stay in sync. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(frontend): bump svelte version in raw_apps UI template Mirror the CLI fix: the UI's `Add raw app` flow scaffolds a package.json with `svelte: "5.45.2"`. That works today only because the bundled rolldown worker also pins 5.45.2 — when the worker is upgraded past 5.51.1, the compiler will emit `$.delegated()` and the runtime won't have it, producing the same white-page crash that hit the CLI. 5.55.5 still exports `event` (used by the current bundled compiler), so this is forward-compatible: it works with the 5.45.2 compiler now and won't break when the worker is upgraded. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * perf(flows): gate flow_env resolve on expr text and share cache with handle_flow (#9085) * feat: parse windmill_failure field to tag run as failure (#9073) * feat: parse windmill_failure field in job result to tag run as failure * feat: preserve top-level fields when windmill_failure tags a run as failure * fix: address review findings on windmill_manual_failure * refactor: rename windmill_manual_failure to wm_failure and add wm_* aliases * fix: prefer injected ManualFailure error over sibling name/message in OTel * fix: hide _ENTRYPOINT_OVERRIDE jobs from script/flow history panel (#9088) * fix(flows): populate error handler input args from failure picker (#9087) * fix(flows): populate error handler input args from failure picker * style(flows): fix indentation in failure-step branch * fix(python): verify wheel RECORD on cache pull/install, finalize piptar (#9090) The Python per-package dependency cache could persist an incomplete wheel extraction with `.valid.windmill` set, then propagate that broken artifact to every worker through the object store. Customer hit this on argon2-cffi==25.1.0 (missing argon2/_utils.py), and previously on botocore/httpx (truncated tars). Symptom is a runtime ImportError that looks like a missing dependency declaration rather than a Windmill bug. Three changes that together stop the propagation: 1. After `pull_from_tar`, parse the wheel's `<dist-info>/RECORD` and confirm every listed path exists on disk before writing `.valid.windmill`. On failure, wipe the directory and fall through to a fresh local install — the next install also self-heals the broken object-store entry by pushing a fresh tar. 2. After `uv pip install` succeeds, run the same RECORD check before queuing the piptar upload or writing `.valid.windmill`. A bad install never becomes the source of a broken tar in the object store. 3. Finalize the tar (`drop(tar.into_inner()?)`) before reading its bytes for upload, so we never push an unfinalized archive (no end-of-archive marker) to the object store. Verified with a 60-package end-to-end integration test (first-fill → clear-local-cache → re-pull-from-objectstore → corrupt-objectstore-tar → detect-and-self-heal). All 27 packages on the live test pulled cleanly, and the deliberately corrupted argon2-cffi tar was caught with the exact expected log line ("wheel RECORD lists files missing on disk: argon2/_utils.py") and replaced with a fresh tar. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(main): release 1.699.0 (#9082) * chore(main): release 1.699.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> * feat(cli): auto-infer args for `wmill app push` (#9091) Run `wmill app push` from inside an app folder (e.g. `f/foo/my_app.app/`) with no args. The local path defaults to CWD, and the remote path is derived from CWD relative to `wmill.yaml`, with `.app`/`.raw_app`/ `__app`/`__raw_app` suffixes stripped. Either, both, or neither positional argument can be passed. Also resolves `file_path` against the user's original CWD before `resolveWorkspace` may chdir to the wmill.yaml root, so a relative `file_path` argument is interpreted from where the user invoked the command (previously it could resolve against the wrong directory). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * all * fix(pipeline): live-update graph for annotations and body assets * fix(pipeline): persist draft body edits across node switches * fix(pipeline): persist live writes per draft to keep output node fresh after switch * feat(pipeline): animate graph edges only while a runnable is executing * feat(pipeline): add run button on script nodes + recomputing hint on preview * feat(pipeline): compact preview layout, two-way Test/Run sync * fix(pipeline): test button cross-browser placement (no overflow trick) * style(log-viewer): replace took/mem-peak labels with timer/cpu icons * style(log-viewer): hyphenate Auto-scroll label and prevent wrapping * style(log-viewer): lowercase auto-scroll label, force vertical scrollbar * style(log-viewer): force horizontal scrollbar instead of vertical * fix(log-viewer): scope overflow-x to top bar so pre doesn't drive panel width * fix(pipeline): overlay live body-asset writes for persisted scripts too * fix(pipeline): persist inferred body assets at save so edges survive page reload * fix(pipeline): snapshot live draft writes at persist time so they survive reload * fix(pipeline): keep inferred body writes on the canvas across selection changes * fix(pipeline): untrack inferredWrites cache mutation to break effect loop * fix(pipeline): refetch asset graph after persisted-script save * feat(pipeline): optional AI prompt when creating a pipeline script * all * all * test: cover asset-trigger dispatch end-to-end through worker * feat(pipeline): split-button Test with optional downstream cascade * feat(pipeline): cascade option on graph Run + match button heights * style(pipeline): match caret bg/text to Test button's accent-secondary * feat(pipeline): split Run pill on graph node exposes cascade option * feat: live run activity + status badges in pipeline asset graph - folder-scoped queue poll lights up the downstream asset-trigger cascade (not just the launched script); zero requests at rest, catch-up for fast hops, auto-disarm when idle - per-runnable node badge: last-run status + session run count - animate unsaved/live-parsed edges (was unconditionally suppressed) - background-pane click no longer clears selection - run-bridge guarded so node selection/save no longer triggers a test Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: live activity log, optimistic badges, node-avoiding graph edges - collapsible folder activity log (PipelineEventLog): live job feed, polls only while open/active, slow idle cadence, capped + pruned - composable: observe mode + events list + run-count anchored to graph-open time (pre-existing history excluded) - optimistic node badge: launched script shows running instantly via the zero-latency activeRunnable hint, keeps the polled run count - activity pane height capped (min(18rem,40vh)) then scrolls - route asset-graph edges through sugiyama-computed waypoints so they go around nodes instead of under them; bezier fallback for adjacent-layer / draft-overlay edges Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: prefetch all folder script assets so graph is stable on load On pipeline load, eagerly infer body assets for every persisted folder script and seed the existing inferredWritesByPath overlay, instead of only filling it when a node is selected. Scripts whose persisted asset rows are missing (e.g. object-form writeS3File) now have their edges from first paint, so clicking a node no longer re-layouts the graph. One-shot per (workspace, base-graph) load, untracked map reads, generation-cancelled, pool-capped fetches. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * perf: guard no-op poll re-layout; dedupe write-asset extraction - skip reactive ids/states/events reassignment when unchanged, so an idle poll tick no longer re-runs the full sugiyama layout every 3-6s - bound countedJobIds (rebuilt from eventsById in lockstep with prune) - extract shared extractWrites() helper, replacing 4 copy-pasted write-asset filter/map blocks in the pipeline page - compute activeRunnable node-id once, reuse for the active-edge set and the optimistic badge (flattened ternary); trim narrating docs Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: live read-lineage overlay for inferred body assets Renaming e.g. duckdb read_parquet('s3://...') / loadS3File now updates the asset->reader edge live instead of only after Save re-derives the persisted asset rows. - extractReads() (+ shared refsByAccess) mirroring extractWrites - inferredReadsByPath sticky cache, filled by handleAssetsChange and the load prefetch alongside writes - replace the write-only overlay loop with one overlayLineage(map, access) helper invoked for both 'w' and 'r' (net DRY) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: detect S3 assets passed as SDK object arg in ts parser Mirrors merged PR #9181 so feat/asset-graph-view is self-contained (local origin/main is stale and lacks it). Object/{ s3, storage } form of writeS3File/loadS3File is now detected, not only the bare s3:// string literal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: regenerate wasm Cargo.lock + frontend package-lock Lockfile churn from local wasm-pack (asset target) + npm operations during the asset-graph work. No source/dependency-intent change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: revert to bezier graph edges; add parsing-assets hint The sugiyama-waypoint routing looked worse than the original; revert AssetGraphEdge/assetGraphLayout to the pre-routing bezier logic (same as the flow editor's BaseEdge) and drop the now-unused route plumbing from the canvas. Add a small 'Parsing assets…' hint shown while the load-time prefetch sweep is still inferring folder scripts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: extract pure resolveGraph merge + unit tests Move the ~230-line graphWithDraft precedence/merge (base < session- inferred < draft-seeded < open-script-live, +read/write/annotation overlays, +dedup) out of the 1648-line route into a pure, testable resolveGraph() module; the route's graphWithDraft is now a thin $derived. Behaviour extracted verbatim. 10 unit tests cover the precedence matrix. Phase 1 of the state/render split. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style: graph controls top-right, lift minimap, hide Save when unchanged Controls -> top-right horizontal, no lock toggle; MiniMap !mb-10 so it clears the activity bar; hide the per-script Save button when the script is already at its latest save point (drafts still show Create). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: scope runtime-asset prune by id to spare static lineage rows prune_runtime_assets deleted by (workspace_id, path, kind) tuple, so trimming surplus usage_kind='job' rows for an s3 path also wiped the static usage_kind='script'/'flow' producer rows for the same path — silently breaking the asset-trigger cascade (fetch_producer_writes found no writes; downstream never dispatched; required band-aid re-syncs). Delete the surplus job rows by id instead; the inner query is already scoped to usage_kind='job'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: don't re-pulse already-running jobs after they finish The catch-up pulse re-added a completed job to the active set if its start was within the (lagging) lookback window — even one we'd already animated the whole time it ran — keeping its edges lit ~a poll interval past completion (~5s after a 3.5s test). Track job ids seen in-flight and skip the pulse for them; it still fires for hops whose whole lifetime fell between two polls. Bound the set in lockstep with eventsById; cleared on dispose. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: don't catch-up-pulse the runnable launched from the graph If the poll never sampled a launched run's in-flight window, the catch-up pulse re-flashed its edges one tick after it correctly stopped (the page already animated it zero-latency via activeRunnable). arm(launchedId) records the launched runnable id; catch-up skips it. Cascade hops (other ids) still pulse. launchedIds cleared on stop. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style: nudge graph controls left to clear panel toggle Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: partition value resolver + asset-cascade propagation windmill-common/partition: pure resolver — time kinds (tz/format/start anchor) + dynamic $.a.b JSONPath; 9 unit tests. asset_dispatch: read the producer's resolved partition and thread it into every cascaded subscriber's args + trigger.partition, so a chain resolves once at the top. No migration (cascade needs no spec lookup). Stage 1+3 of pipeline partition runtime; run-start resolution is Stage 2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: show args form in compact pipeline preview when script has inputs AssetGraphDetailsPane keeps the compact (hideArgs) preview but, via a new previewPanel.argsAboveLogs flag, renders a compact SchemaForm between the floating Test button and the logs/result panel when the script declares inputs (e.g. a partitioned script needing a `partition` arg). The preview pane also grows ~18pts so the args form doesn't shrink logs/result. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: parser join-mode (`// trigger all`) + script_trigger.join_all Stage A: JoinMode{Any(default),All} + `// trigger any|all` directive in parse_pipeline_annotations; TriggerSpec::is_partition_bearing() (path contains {partition}); join_mode threaded through all 4 asset-parser crates (ts/py/sql/yaml). Stage B: reversible migration adds script_trigger.join_all; insert_script_trigger writes it; deploy path sets it from the parsed annotation. No reader yet (AND-join dispatch is the next stage) so runtime behaviour is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: resolve pipeline partition at job execution time Stage C: in handle_code_execution_job, once the script content is loaded, parse the // partitioned annotation (free here) and resolve the concrete partition once — schedule fire-time (scheduled_for anchor, not wall-clock) for time kinds, triggering payload for dynamic. The value is injected into the in-memory args the body sees (via a shadowed job clone) and persisted back to v2_job.args so dispatch_asset_triggers propagates the same value down the cascade. Already-set (explicit/backfill/cascade) partitions are never re-resolved (run identity immutable); unresolvable partitioned runs fail with a clear error. Integration test exercises the full worker loop + cascade propagation. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: AND-join barrier for partitioned pipeline subscribers Stage D: a // trigger all subscriber no longer fires on any input. New join_pending_inputs slot table keyed (workspace, subscriber, partition); fetch_subscribers now returns join_all and the dispatch loop records each partition-bearing input arrival, pushing the subscriber once only when every partition-bearing input it declares is present for that partition. Per-partition slots, cleared on fire (re-accumulate, no double-fire), skew-immune (unlike debounce). Case-3 guard: an unpartitioned producer or a reference (non-{partition}) input never fires a partitioned join. Integration test covers wait/fire/isolation/no-double-fire. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: opt-in // debounce for asset-cascade subscribers (parser + schema) Stage E1+E2. Parser: script-level // debounce <dur> + per-// on debounce=<dur> override (edge wins, else script default, else none = fan-out, unchanged); TriggerSpec::Asset carries the per-edge override; split_trailing_kv_opts separates the ref from trailing key=val opts. Schema/deploy: reversible migration adds script_trigger.debounce_s; parse_duration_secs (bare int or <n>s|m|h|d, fail-safe on garbage) resolves the effective per-edge window at deploy and writes it per row. No reader yet (dispatch wiring is E3) so runtime is unchanged. New unit tests for the parser directive and duration parsing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: apply opt-in debounce to asset-cascade subscriber dispatch Stage E3. fetch_subscribers now also returns debounce_s; push_subscriber builds real DebouncingSettings (delay + a (subscriber, partition) key, so distinct partitions never collapse and latest-in-window falls out) instead of ::default() when the edge opted in. Default stays no-debounce (fan-out — the prior deliberate behaviour, now overridable rather than reversed). Wiring test asserts the dispatched job carries the configured window/key and an undebounced edge carries none. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: atomic AND-join gate + preserve resolved partition; drop scratch artifacts Addresses local-review findings before PR: - P1: record_and_check_join_slot was a non-atomic check-then-act on a pooled connection; concurrent completion of a subscriber's last two partition-bearing inputs on different workers could double-dispatch. Now one transaction guarded by a tx-scoped advisory lock keyed on (workspace, subscriber, partition) so the gate fires exactly once. - P2: the preprocessed-args overwrite in result_processor replaced args wholesale, dropping a partition resolved by resolve_partition_for_job; the UPDATE now preserves an existing persisted partition key. - P2: gate resolve_partition_for_job on a cheap code.contains check so non-pipeline script jobs skip the annotation scan on the hot path. - P2: remove 40 scratch screenshot PNGs, a flicker-debug script and a local scheduler lock accidentally committed; gitignore the lock. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: AND-join fires once under concurrent upstream completion Regression for the check-then-act race fixed by the advisory-locked transactional gate: releases N producer dispatches simultaneously via a barrier and asserts the AND subscriber is pushed exactly once and the slot is cleared. The invariant holds for the correct gate regardless of interleaving; a non-atomic regression fails it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: fuller partitioned join + multi-hop pipeline coverage Exercises a complex pipeline combining options end to end: two partitioned producers fanning into a // trigger all join, then a multi-hop downstream chain. Asserts the resolved partition propagates unchanged at every hop, chain depth increments per hop, the AND barrier fires exactly once, and a second partition opens an independent slot with no cross-partition bleed across the whole graph. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor: simplify pipeline code per review (dedup, single-parse, constant) - ParseAssetsOutput::new() collapses the 6-line annotation copy-paste across the 4 asset-parser crates to one call site. - asset_dispatch: parse the cascade trigger object once and pass it to the depth/partition readers instead of deserializing it twice; add a TRIGGER_ARG constant for the previously stringly-typed key (3 sites). - scripts deploy: drop a redundant debounce_default clone. No behavior change; 29 parser + 6 dispatch integration tests green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: reap abandoned AND-join slots after a TTL (default 60d, per-slot) join_pending_inputs slots are normally cleared when the join fires; partial slots whose inputs never all arrive (upstream removed/renamed, one-off dynamic partition key, permanent skew) would otherwise leak. windmill_queue::asset_dispatch::reap_stale_join_slots, called from the monitor's delete_expired_items loop, deletes a (workspace, subscriber, partition) slot only when its MOST RECENT row is older than JOIN_SLOT_TTL_SECS (60d) — per-slot, never per-row, so a legitimately slow join is not corrupted mid-accumulation. Conservative default; per-join configurable TTL via the annotation is a planned follow-up. Test covers stale-reaped / fresh-kept / mixed-slot-kept. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * update * feat: path-less native trigger markers + missing-trigger placeholder * feat: pipeline // tag and // retry annotations + dispatch_event log * fix: derive test-pane min from split-axis dimension (height in bottom layout) * feat: show last run logs/result when a script node is selected * fix: backfill asset rows from script.assets for pre-feature scripts * feat: job-id link + dispatch popover above script log/result * style: drop 'dispatched' label, keep just the check icon * fix: drop tag picker from pipeline script editor (set via // tag annotation) * Nicer UI * refactor: move google ai proxy handling to windmill-ai (#9260) * refactor: add ai proxy execution mode * refactor: move google ai proxy handling * refactor: share google ai request building * fix: early return should consider failure_module result (#9241) * fix(flows): flag noLogs jobs and lazily resolve them in log panel (#9099) * fix(flows): flag noLogs jobs and lazily resolve them in log panel * fix appending to flag * fix: preserve WM_LOGS_SKIPPED sentinel on SSE/replay completion pickMoreCompleteLogs resolved both sentinel and undefined to '', so the SSE completion event (whose job field is fetched .without_logs()) would clobber the sentinel placed by flagSkippedLogs. The module log panel then saw '' instead of the sentinel, defeating the lazy-resolve path. Also wire onLogsResolved on the OutputPickerInner inline LogViewer so a lazy resolve writes back to flowStateStore.previewLogs, matching ModulePreviewResultViewer and avoiding repeated fetches on remount. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(main): release 1.705.0 (#9229) * chore(main): release 1.705.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> * chore: add playwright mcp for frontend verification (#9269) * feat: CLI datatable serve / psql (#9267) * feat(cli): add datatable list and run commands * feat(cli): render datatable query results as a table * feat(cli): serve datatables as a postgres-wire endpoint * feat(cli): add 'datatable psql' to launch psql against the proxy * feat(cli): route datatable serve by client-supplied database name * override database list + password option * fix: support extended queries in datatable serve * fix: correct cloud size threshold log and parse CLI descriptions with parens/trailing comma * refactor: extract raw_output envelope encoding into pg_raw_output module --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> * oom_adj nit * feat: add UV_PYTHON_INSTALL_MIRROR env and instance setting (#9271) * feat: add UV_PYTHON_INSTALL_MIRROR env and instance setting Allows operators to point `uv python install` at a private mirror of the python-build-standalone releases. Configurable via the `UV_PYTHON_INSTALL_MIRROR` env var or the `uv_python_install_mirror` instance setting, with the env var as the boot fallback and the instance setting taking precedence at reload. Fixes WIN-1966 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: hoist uv_python_install_mirror binding above sandboxing branch The non-sandboxed uv pip install branch referenced a binding that was only declared inside the sandboxed branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: neutral placeholder for uv_python_install_mirror The previous placeholder was the default public URL the setting is meant to redirect away from. A neutral example mirror URL is clearer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(indexer): tell admins when ingress routes search to wrong pod (#9274) * [ee] fix(indexer): tell admins when ingress routes search to wrong pod When the IndexReader is absent on the pod handling a search request but another pod is actively holding the indexer lock, the EE handler now returns a tailored error pointing at the ingress/load-balancer configuration instead of the generic "indexer not running" message. The indexer status endpoint reads the DB lock so it reports "running" from any pod, but search endpoints need the in-memory IndexReader that only exists on the lock holder. In multi-replica deployments this looks like the indexer is healthy but every search 404s. Companion: windmill-labs/windmill-ee-private#TBD Fixes WIN-1968. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: update ee-repo-ref to eb18d7b4c0e37fea3f6e1e2cc44e0fddd74ff817 This commit updates the EE repository reference after PR #586 was merged in windmill-ee-private. Previous ee-repo-ref: 7dd43d1850813071cc18ba49ba090583e7321f4b New ee-repo-ref: eb18d7b4c0e37fea3f6e1e2cc44e0fddd74ff817 Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com> * feat(cli): add `wmill init prompts` and custom override slot (#9266) * feat(cli): add `wmill init prompts` and custom override slot Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(cli): replace init prompts with refresh prompts + AGENTS.md/AGENTS.cli.md split Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cli): dedupe claude skills via @-includes and add prompts freshness check Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(cli): drop migration-choice flags from `refresh prompts` Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(cli): add 'Running and previewing local changes' section to AGENTS.cli.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(cli): write full skill content to .claude/, drop @-include wrapper Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cli): reconcile CLAUDE.md the same way as AGENTS.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(cli): address PR review nits — argv parsing, lazy import, comment detection, error propagation Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: add yolo mode for ai chat tools (#9258) * feat: add yolo mode for ai chat tools * nit * fix: align chat footer controls * feat: add ai chat autonomy modes * feat: add autonomy mode dropdown * fix: highlight yolo autonomy icon * fix: auto accept flow edits * fix: hide unsupported autonomy modes * fix: handle auto-accept flow editor races * fix(debugger): add non-root user support to Dockerfile (#9277) Mirrors the main Windmill Dockerfile pattern: creates a windmill user (UID/GID 1000) and makes cache/work directories world-writable so the image runs cleanly under Kubernetes securityContext.runAsNonRoot or runAsUser: 1000 without permission errors on Bun, pip, or windmill cache writes. Fixes WIN-1969 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ai): enforce RLS and scope check on user-supplied X-Resource-Path (#9276) * fix(ai): enforce RLS and scope check on user-supplied X-Resource-Path The AI proxy handler accepts an X-Resource-Path header to override the configured workspace AI provider. When supplied, the handler loaded the resource value from the resource table using the root DB pool with no resources:read scope check, so any authenticated workspace user could point X-Resource-Path at a restricted AI resource (e.g. one in a folder they cannot read) and the proxy would use that resource's provider credentials for the outbound AI request. For user-supplied resource paths, now require resources:read:{path} scope and fetch the resource through user_db.begin(&authed) so RLS enforces the same folder/group boundary as the resource API. The RLS- scoped $var: resolution stays in place as defense in depth. The admin-configured workspace/instance ai_config path is unchanged. Fixes WIN-1971 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(ai): regression test for X-Resource-Path RLS enforcement Cover all four cases: - non-admin pointing X-Resource-Path at a restricted resource is rejected - non-admin pointing it at a resource they own still works - admin can point it at any resource - workspace-configured proxy flow (no X-Resource-Path) is unchanged Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: add userdraft listing primitives (#9268) * feat: add userdraft listing primitives * fix: cancel stale userdraft discard writes * docs: remove global ai userdraft plan * feat(nsjail): optional disk-backed /tmp via instance setting (#9272) * feat(nsjail): optional disk-backed /tmp via instance setting * test(nsjail): unit-test tmp mount resolver and narrow visibility * refactor(nsjail): switch tmp backing to select + conditional UI * ui(nsjail): make tmpfs the visible default in /tmp backing select * fix(nsjail): refuse preexisting jail_tmp to block symlink escape * fix(nsjail): allow jail_tmp reuse on sequential nsjail calls Codex flagged that python/ruby/rust executors invoke nsjail twice per job_dir (install then run). The previous resolver treated any preexisting jail_tmp as hostile and silently fell back to tmpfs on the second call, so disk-backed mode never reached the main script run for those langs. Use symlink_metadata().is_dir() to distinguish a real directory left by an earlier call in the same job_dir (safe to reuse) from a symlink or other entity (still refused, as the codebase-tar escape requires). Also loosen the frontend visibility predicate: only hide nsjail settings when job_isolation is explicitly 'none' or 'unshare', so deployments that enable nsjail via DISABLE_NSJAIL=false with no DB setting can still see the controls. * chore(main): release 1.706.0 (#9270) * chore(main): release 1.706.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> * fix(nsjail): gate unix-symlink test behind cfg(unix) for Windows build (#9280) The disk_backed_refuses_preexisting_symlink_at_jail_tmp test calls std::os::unix::fs::symlink directly, which doesn't exist on Windows targets. Without a cfg gate, `cargo check --tests` fails on Windows with E0433. Other symlink call sites in this crate (php_executor, bun_executor, rust_executor, etc.) already follow this pattern. Fixes WIN-1972 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Reduce slim image vulnerability surface (#9279) * Reduce slim image vulnerability surface * chore(docker): drop apt-get upgrade -y from slim images apt-get upgrade hurts build reproducibility (same Dockerfile + same commit at different times produces divergent images) and trips hadolint DL3005. The freshness it buys is dominated by simply rebuilding against the periodically-refreshed debian:bookworm-slim base image. The --no-install-recommends and apt-list cleanup wins are kept. --------- Co-authored-by: Ruben Fiszel <ruben@windmill.dev> * fix(git-sync): bump to hub/28234 with stateless gpg.program wrapper (WIN-1974) (#9282) * fix(git-sync): revert LATEST_GIT_SYNC_SCRIPT_PATH to hub/28230 to restore GPG-signed deploys (WIN-1974) hub/28231 (PR #9230) is the "thin" script that hands the actual `git commit` to the CLI's hidden `sync git-deploy`. The hub script still does the GPG setup (import key into a fresh GNUPGHOME, dummy `gpg -bsau` to warm the agent passphrase cache, then `git config user.signingkey` + `commit.gpgsign` locally), but the commit no longer runs in the same `git_push` flow — it runs minutes later inside the CLI after workspace API resolution, zip pull, file extraction, and lockfile autofill. By the time the spawned `git commit` asks gpg-agent for the cached passphrase, the cache state is no longer reliable (or the spawned `gpg` ends up talking to a fresh agent), so signing fails non-interactively with `gpg failed to sign the data`. hub/28230 is hub/28217's in-script logic rebuilt with windmill-cli@1.703.3: the GPG setup and the in-script `sh_run("git commit ...")` happen back-to-back in `git_push`, so the cache is always fresh. It preserves wm_deploy / fork branch behavior, the EE deployment-callback `main()` signature is unchanged, and the only min-version check in EE (`is_script_meets_min_version(28103)`) is comfortably below 28230 — so this revert is safe. Forward fix (separate PR): publish a new thin script that, alongside the existing GPG setup, writes a `gpg.program` wrapper using `--pinentry-mode loopback --passphrase-file` so signing is independent of the agent's cache state. Re-bump past 28231 then. Fixes WIN-1974 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(git-sync): check in source-of-truth for the next hub script (gpg.program wrapper) This is the script that will be published to hub.windmill.dev once verified on a customer GPG-signed deploy. It replaces hub/28231's agent-cache pre-warm (`gpg -bsau` with --passphrase) with a stateless gpg.program wrapper + chmod-600 passphrase file. Every git-invoked gpg call goes through the wrapper, which always uses --pinentry-mode loopback (and --passphrase-file when a passphrase exists). Signing no longer depends on gpg-agent having a cached passphrase by the time the CLI's `git commit` runs — which closes WIN-1974. Not wired in yet: LATEST_GIT_SYNC_SCRIPT_PATH stays on hub/28230 until this script is uploaded and the new hub id is known. This file is checked in so the diff is reviewable, future bumps have a source of truth, and a CLI regression test can `cat` it for fixture parity. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(frontend): skip format/pattern validation for $var/$res/$jsonvar references in ArgInput A resource field with a `pattern` constraint (e.g. the gpg_key.private_key field, whose pattern enforces a `-----BEGIN PGP PRIVATE KEY BLOCK-----` prefix) rejects values like `$var:u/me/gpg-private-key` with an "invalid format" error in the resource editor — even though `$var:`/`$res:`/`$jsonvar:` are placeholders the backend resolves at runtime, not the actual string that needs to match the regex. Bail out of all format/pattern checks (email, ipv4, ipv6, uuid, custom pattern) when the value is one of these references. Required/numeric bounds/array checks still apply since they're shape-level, not regex. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(git-sync): bump LATEST_GIT_SYNC_SCRIPT_PATH to hub/28234 (gpg.program-wrapper fix) hub/28234 is the forward fix for WIN-1974: replaces hub/28231's agent-cache pre-warm (which became stale by the time the CLI's `git commit` ran) with a stateless `gpg.program` wrapper that uses `--pinentry-mode loopback` (and `--passphrase-file` when a passphrase exists) on every gpg invocation. Bundled CLI is windmill-cli@1.705.0. Verified via reproducer at /tmp/git-sync-diff/test-gpg-fix.sh: deliberately killing gpg-agent between GPG setup and `git commit` reproduces the customer's `gpg failed to sign the data` error verbatim under the old flow, and the wrapper signs through it. Holds for passphrase-protected keys, split-subkey [C]+[S] layouts, and unprotected keys. Drops the local source-of-truth copy (`hub-scripts/`) — hub is canonical now that 28234 is published. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(git-sync): drop verbose comment above LATEST_GIT_SYNC_SCRIPT_PATH The git history (this PR) carries the why; the constant name + value carry the what. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(cli): wmill sync git-deploy stops committing; caller owns commit+push (#9284) Single contract for the deployment-callback path: the CLI does branch checkout + pull, the caller (hub script in production, test in test) does git add + commit + push. This restores the WIN-1974 invariant — GPG setup and `git commit` run back-to-back in the same process, so the agent's pre-warmed passphrase cache is still warm at sign time — without needing a `--skip-commit` flag for the hub case and a default "also-commit" for everything else. Same behavior in every call site. Changes: - sync.ts: drop the gitSyncDeployPush call from pull()'s deploy path (both the onlyCreateBranch fast-return and the post-pull commit). `gitSyncDeployPush` stays exported for any caller that wants the same commit/push semantics — just not invoked by the CLI subcommand. - gitsync_promotion.test.ts: e2e test now does its own git add + commit + push after `wmill sync git-deploy`, mirroring what the hub script does in production. Same regression coverage (wm_deploy branch created in Case A, main untouched; main updated in Case B, no new wm_deploy). CLI typecheck unchanged (two pre-existing TarAsZip errors at lines 2578/3307, present before this PR). All 743 unit tests still pass. The accompanying hub script (option-C — CLI for branch+pull, script for commit+push) lives at /tmp/git-sync-diff/sync-script-to-git-repo-windmill.option-C.ts. Once published, a follow-up bumps LATEST_GIT_SYNC_SCRIPT_PATH to its id. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * bump git sync to 28236 * fix: fork compare visibility for non-admins and stale-token superadmins (#9283) * fix: use fork-scoped authed for fork visibility in compare_workspaces * test: add EE end-to-end repro for fork rename visibility * chore: restore concurrency_locks sqlx cache lost in cleanup * test: add regression for stale-superadmin-token fork visibility bug * chore: update sqlx cache for new test queries * chore(main): release 1.706.1 (#9281) * chore(main): release 1.706.1 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> * feat: add wmill job rerun subcommand (#9275) * feat: add wmill job rerun subcommand * feat: add wmill job restart subcommand for flow restart-at-step * chore(system_prompts): point plugin skills sync at plugins/windmill/ (#9287) * chore(system_prompts): point plugin skills sync at plugins/windmill/ The plugin checkout's plugin folder is being renamed from `plugins/windmill-code-plugin/` to `plugins/windmill/` to shorten the slash-command namespace and align with the matching Cursor plugin layout. Paired with windmill-labs/windmill-claude-plugin#8. That PR must merge first so the next sync run finds the new folder. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(system_prompts): update plugin-dir example to plugins/windmill Co-authored-by: centdix <centdix@users.noreply.github.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: centdix <centdix@users.noreply.github.com> * fix(cli): wmill sync pull updates wmill-lock.yaml for raw apps (#9289) * fix: flow recording teardown crash + rename package to @windmill-labs/components (#9288) * fix: guard against null recording during FlowRecordingReplay teardown Navigating away from a flow recording inside a workspace file-tree view threw `TypeError: Cannot read properties of null (reading 'flow')` from FlowGraphViewer once during the teardown tick. Svelte 5 compiles child component props as live getters that close over `$$props.recording.flow`. When `recording` flips to null on the parent's navigation, an outer `{#if !recording?.flow}` doesn't stop those getters from firing one more time as derived effects re-evaluate before the unmount lands — so the getter dereferences null and throws. Fix at the two layers where the deref actually happens: - FlowRecordingReplay: use `recording?.flow` at the binding sites (FlowViewer + graph-snippet FlowGraphViewer) so the compiler emits an optional-chained getter, and guard the snippet branch with `{:else if recording?.flow}` so it doesn't mount when there's nothing to show. - FlowGraphViewer: finish the optional chaining the rest of the file already used everywhere else (`flow?.value?.skip_expr`, `flow?.value?.cache_ttl`, `flow?.schema`). When the upstream binding returns undefined during teardown, the graph degrades to an empty frame instead of crashing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: rename package to @windmill-labs/components - frontend/package.json: rename `windmill-components` → `@windmill-labs/components` - frontend/publish.sh: drop the in-place sed rename dance; the checked-in name now matches what's published, so `npm run package && npm publish` is enough - frontend/package-lock.json, system_prompts/auto-generated/prompts.d.ts: regenerated by `npm run package` under the new name Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * default script name * save logic * Keyboard nav * finish keynav * nits * CI fix * nit stop propagation * Merge branch 'main' into feat/asset-graph-view * commit * update * fix: cropped save button on small screens * progress * managed scheduled removed * all * progress * feat: add data upload pipeline trigger with auto S3 picker Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: avoid pane editor remount flicker when deploying a pipeline draft Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: show only the edited script's I/O in the asset graph, not the saved version's Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: derive script asset rows server-side at deploy Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: shared fixture corpus keeps annotation parsers in parity Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: dev-run draft pipeline chains, live badges, deploy drift warning Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: ungate cascade producers, squash pipeline migrations Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: drop committed cli-sync fixtures and stray screenshots Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: show skip-asset-dispatch flag as badge instead of args row Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: pipeline view mode default with activity feed, drafts overlay chip Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: treat DROP TABLE as table-level write in sql asset parser Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: wmill datatable create + actionable sql extension error Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: ephemeral data-pipelines demo sync repo zip for handoff Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: wmill pipeline list/show renders the asset DAG in the terminal Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * nits * nits * nits * nits * fix: defer draft persist-back past the batch so discard sticks first click Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: band-reserving tidy-tree asset graph layout with join breakpoints Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: route skip-layer and long graph edges around occupied columns Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: seed s3 template outputs with canonical leading-slash paths Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * all * feat: bundle data-pipeline drafts into the DB-backed user draft system Pipeline drafts were browser-only (localStorage `pipeline-<folder>`), so they didn't sync across devices, weren't server-visible, and never showed in the drafts list. Store them instead as one per-user `draft` row of a new `data_pipeline` kind, keyed at the folder (`f/<folder>/data_pipeline`), holding the same `{ drafts, activeDraftPath }` bundle. Stage 1 — backend kind: add `data_pipeline` to DRAFT_KIND (migration) and `UserDraftItemKind` (deployed_table=None, private). The list/update handlers and folder-path access check already cover a backing-table-less kind. Stage 2 — sync: add `GET /drafts/get_own/{kind}/{path}` so an editor with no deployed-overlay GET can load its own draft. The pipeline page now hydrates from the DB on mount (one-time localStorage import for in-flight drafts) and persists via UserDraftDbSyncer (debounce + optimistic-concurrency), keeping a localStorage crash mirror. Stage 3 — surface: the drafts review page renders the bundle as a "pipeline" row that opens `/pipeline/<folder>` (open-only; excluded from bulk deploy). Verified end-to-end in-browser: DB-seeded draft hydrates to "Edit (1)", edits persist back, and the row shows with Open pipeline / Discard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: pipeline Activity panel grouping, run↔graph highlight, deploy-conflict handling Activity panel (view mode): - Group cascade runs by the connected component of the asset-dispatch graph (new GET /jobs/asset_dispatch_edges over the dispatch_event table, incl. join_pending inputs), headed by the earliest originating run + its trigger, with a "+N" chip for joins fed by multiple triggers. - Success/failure count histogram with drag-to-filter brushing, an always-on time axis + per-bar tooltips, a Reset, and Last hour/24h/48h/7/30/90d ranges. - Node run-count/status badges now derive from the same merged historic+live events the panel shows (previously session-only). Run ↔ graph highlight: - Hovering a run row (or a group header → the whole cascade) rings the node(s), animates their incident edges, and borders the adjacent assets in the edge hue (blue write / gray read); expanding a run pins a soft-blue ring. - Switching edit→view re-surfaces the Activity feed. Deploy: - Live-content autosave for the open pipeline draft + an autosave indicator. - Re-saving a script now chains off the hash just created instead of a stale parent_hash (fixes the "lineage must be linear" error on a second save), and a genuine concurrent deploy opens a keep-mine / view-latest conflict modal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: pipeline editor badge requires asset-parse, not just main-function parse A pipeline script's asset lineage is load-bearing — a deploy that can't parse assets silently records no edges. The editor "parsable" dot only reflected inferArgs (the main function), so a body the asset parser rejects (e.g. a trailing `/////` in DuckDB) still showed green and deployed with empty lineage. ScriptEditor gains `requireValidAssets` (set by the pipeline pane); when on, the EditorBar badge is green only if BOTH the main function and inferAssets parse, with the tooltip distinguishing "Main function not parsable" / "Assets not parsable" / "Parsable". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: route asset-graph edges around nodes that sit in their path Edges could draw straight through an unrelated node (a join fan-out or long cross-component edge), making it ambiguous whether that node shared the input. AssetGraphEdge only saw its own endpoints, so it could only detour the near-vertical same-column skip case. The canvas now (once per layout, O(edges × nodes) — no per-frame cost) samples each edge's straight run against every non-incident node center and, on a crossing, passes a clear gutter lane to the edge via `data.detourX`; AssetGraphEdge routes the rounded-orthogonal detour through it. Verified: 0 edge↔node box crossings on the orders pipeline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: deploy pipeline drafts with freshly-inferred assets, not a stale snapshot "Save all" spread `...draft.script` into createScript, which carries a `assets` snapshot that isn't refreshed when the body is edited. So a renamed/removed output (e.g. an old `CREATE TABLE exciting_en32z9` later changed to `exciting_880909`) was re-deployed as a phantom write edge and lingered as an orphan asset on the graph — shown with no producer, and shifting position on click as the graph re-derived. saveDraft now re-runs inferAssets on the current body and passes the result as `assets`, overriding the snapshot — mirroring the per-pane save. The backend clears+reinserts from the sent set, so a re-deploy drops the stale rows. Verified: deploying with the fresh asset set removes the orphan from the graph. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: collect upstream reads from CTAS and CREATE VIEW in SQL asset parser `CREATE TABLE x AS SELECT … FROM y` (and `CREATE VIEW`) recorded only the write to x — the source read of y was silently dropped. Table-level reads are gathered in the `Statement::Query` arm via handle_table_with_joins; the generic table-factor visitor only picks up read-functions and string literals, not plain `FROM <table>` references. The AS-query of a CTAS isn't a `Statement::Query`, so its FROM tables were never walked. On the pipeline canvas this meant a `datatable://…` upstream consumed by a CTAS step showed no read node/edge — the step looked like it produced its output from nothing. Factor the Query arm's read collection into handle_query_reads and call it from the CreateTable (when it has an AS-query) and CreateView arms, balancing the cte_name_stack push in post_visit_statement. Updated the drop_then_create test (which had pinned the old drop-the-read behavior) and added CTAS + CREATE VIEW read coverage. Verified against the rebuilt asset wasm: the live editor now infers the read. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * update * updates * refactor: dedup asset-graph code, squash migrations, drop artifacts Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf: gate asset dispatch on a cached per-workspace producer set Cache the producer-path→writes map per workspace and invalidate it from the asset-clear paths via the notify_event polling system, so a top-level script/preview completion that isn't an asset producer costs an in-memory lookup instead of a per-completion query. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: remove dead unquote fn that failed backend check under -D warnings Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: green the frontend check (pin published wasm-asset, fix type errors) Pin windmill-parser-wasm-asset to the published 1.728.1 (was a file: link to a gitignored, CI-unbuilt pkg-asset). Exclude test files from svelte-check (the parity test reads a backend fixture via node:fs, which the browser app tsconfig has no @types/node for; vitest still runs them). Fix pre-existing branch type errors: drop the unsupported 2nd getScriptByPath arg, cast script.schema to Schema for inferArgs, coerce has_preprocessor to a definite boolean, and wrap the cancelJob handler so it isn't possibly-undefined. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: move pipeline partition resolution to ee-private (free-CE) Partition resolution becomes a private module (partition_ee in windmill-ee-private, hidden from the public repo) with an OSS no-op fallback (partition_oss); call sites resolve via the aliased windmill_common::partition. Not enterprise-gated — free to run in CE. Bumps ee-repo-ref to the ee branch carrying partition_ee. Verified building in default, private, and private,enterprise (offline). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: move asset-cascade join/debounce/retry to ee-private (free-CE) Join barrier, debounce, and retry become the private windmill_queue::cascade module (cascade_ee in windmill-ee-private); OSS gets cascade_oss no-op fallbacks (plain OR fan-out). Core cascade stays public. Bumps ee-repo-ref. Verified default/private/private,enterprise. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: skeleton enterprise pipeline freshness + backfill (TODO, ee-private) Gated windmill_common::pipeline_advanced (private; pipeline_advanced_ee) with OSS fallback; entry points return a clear not-implemented error. Deploy surfaces a TODO when a script declares // freshness. Bumps ee-repo-ref. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: repair asset_trigger_dispatch test after cascade carve-out + cache its queries Stage-2 moved reap_stale_join_slots to windmill_queue::cascade; update the integration test's import. Also commit the test's sqlx query cache (was never prepared with --tests, so SQLX_OFFLINE cargo test failed pre-existing). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: invalidate producer-cache in asset dispatch tests (mirror deploy) The tests seed asset rows directly and run no notify poller, so the per-workspace producer cache went stale across tests → 0 dispatched. Clear it at the seed point, as a deploy would via notify_event. All 8 asset_trigger_dispatch tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: update ee-repo-ref to ba677ea142011462ad4dfe77e8375a6dd274cdef This commit updates the EE repository reference after PR #619 was merged in windmill-ee-private. Previous ee-repo-ref: 925c350cff55d3ea738d9e2e4098d9ce4bdda418 New ee-repo-ref: ba677ea142011462ad4dfe77e8375a6dd274cdef Automated by sync-ee-ref workflow. * test: disable producer cache in asset dispatch tests (isolated-DB safe) The .remove(WS) approach still raced: #[sqlx::test] gives each test its own DB but they share one workspace id, so the WS-keyed process-global cache clobbered across DBs under concurrent threads. Add an ASSET_PRODUCER_CACHE_DISABLED test hook and set it in the tests so every dispatch reads its own DB. 8/8 pass at --test-threads=10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: replace asset-cascade depth cap with cycle detection The hardcoded MAX_CHAIN_DEPTH=5 truncated legitimate deep pipelines (silently — the check returned before event logging). Replace it with per-edge cycle detection: carry the producer lineage in trigger.chain and skip only a subscriber already in the chain, recording a visible cycle_detected dispatch_event. Acyclic pipelines of any depth now cascade fully; a high MAX_CHAIN_LEN backstop guards against runaway. Tests + UI label updated; 8/8 pass at --test-threads=10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: update dispatch_event reason examples (depth_cap → cycle_detected) Comment-only; the migration is idempotent and already in the potentially_stale self-heal list, so the checksum change re-applies cleanly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: park cascade retry (P1 dead-end) + clear stale script_triggers on rename Two deploy-path fixes: - Retry is parked: a retried subscriber is wrapped in a SingleStepFlow, whose run is a flow step and ineligible for asset dispatch, so it would silently dead-end the cascade (P1). Stop persisting retry to script_trigger and warn at deploy; TODO(pipeline-retry) to re-enable once dispatch handles flow-wrapped producers. (Dispatch plumbing kept + still tested via direct seeding.) - Rename leaves stale script_trigger rows: clear was keyed on ns.path only, so old-path '// on' edges lingered and could trigger a script later recreated at that path. Also clear the old path on rename (assets already handled via the parent-hash clear). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> Co-authored-by: hugocasa <hugo@casademont.ch> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com> Co-authored-by: Arnaud <31803803+Araden14@users.noreply.github.com> Co-authored-by: Diego Imbert <diego@windmill.dev> Co-authored-by: centdix <40307056+centdix@users.noreply.github.com> Co-authored-by: Diego Imbert <70353967+diegoimbert@users.noreply.github.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Aldrin Jenson <aldrinjenson@gmail.com> Co-authored-by: centdix <centdix@users.noreply.github.com> |
||
|
|
5508f1da9c |
feat(frontend): View Diff and in-place Load for other users' drafts (#9621)
* feat(frontend): replace other-user draft "View JSON" with "View Diff" Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(frontend): replace other-user draft "Fork" with in-place "Load" Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(frontend): detect first overlay edit by value divergence, not a timer Replaces the 700ms arming timer (which leaked across sessions and silently swallowed sub-window edits) with a deterministic check: a blocked save opens the overwrite prompt only once the cell value diverges from the loaded value. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): overlay leak on revisit, diff z-index, home-popover edit affordances - Clear a stale "editing another user's draft" overlay when its editor is reloaded without a fresh Load, so returning to the item edits our own draft. - Open View Diff above the others-drafts modal (close it first) instead of rendering the drawer behind it. - Add an Edit button to our own row in the home draft popover; use a pencil icon (not a download) for Load. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: admin "Migrate" action for legacy drafts (delete / assign to self) Adds an admin-gated `POST /drafts/migrate_legacy/{kind}/{path}` endpoint to resolve pre-migration workspace-level drafts (email NULL): delete the row, or move its value onto the admin's own row. Surfaces a "Migrate" button on legacy rows in the home-page draft popover and the in-editor others-drafts modal (workspace admins / superadmins only), opening a modal with Delete and Assign to self. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): close home draft popover before opening View Diff / Migrate The hover popover sits above the diff drawer and migrate modal (z-index), so it covered them. Close it first so they render on top. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): remount the flow builder on "Reset to draft" from an overlay FlowBuilder captures the flow at mount, so reloading the value alone left the foreign graph on screen — reset appeared to do nothing. Force a remount (renderEditor=false → loadFlow) like navigation does. Scripts (imperative setCode) and apps (redraw++) already remount, so only flows needed this. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): refresh the home row after migrating a legacy draft invalidateAll() didn't refetch the home list (it loads items client-side), so the legacy badge entry lingered after delete / assign-to-self. Bubble an onMigrated callback up to the row's `change` event, reusing the same reload chain (Item → ItemsList loadScripts/Flows/Apps) as delete/archive. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * nit * nit * fix(frontend): match app overlay baseline to the migrated value AppEditor migrateApp()s the app on mount, so the draft cell settles to the migrated value. The overlay used the raw loaded value as the divergence baseline, so a post-mount mirror write could trip "Overwrite your current draft?" before any edit. Migrate the baseline too (like the deployed-baseline and raw_app bundle do) so it matches the settled cell. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): address review on legacy-draft migrate + overlay - Legacy "Assign to self" now confirms before replacing an existing own draft (MigrateLegacyDraftModal gains an `ownDraftExists` step, threaded from the home badge and the in-editor others-drafts modal). - Gate overlay mode on a per-response `hasOwnDraft` instead of the sticky `loadedFromDraft`, so navigating to a no-own-draft item in the same editor route can't wrongly enter overlay. Fixed in all 4 editor routes. - Raw-app "View Diff" now projects the deployed app into the flat draft-bundle shape (via a shared `extractDataConfig`) instead of diffing `.value` against the bundle, so the drawer shows a real diff. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8021775f5f |
fix(drafts): preserve original timestamp when migrating localStorage drafts (#9638)
The localStorage→DB user-draft migration upserted via /drafts/update, whose SQL always stamped created_at = now(). Every migrated draft therefore resurfaced to the top as freshly created, regardless of its real age. Add an optional created_at override to the update_draft request, threaded into the upsert as COALESCE($8, now()) / created_at = EXCLUDED.created_at. Normal saves omit it and still stamp now(); the migration passes the draft's original write time (or epoch 0 when unknown) so migrated drafts keep their age. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e09cd5862c |
feat: per-user draft review & deploy page (gating, badges, rename, raw-app deploy fixes) (#9625)
* feat: per-user draft gating, badges and rename display on deploy page Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): don't strike the path when a draft adds a summary to a summary-less item Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): don't strike draft-only items' auto-generated path against the pretty path Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): deploy raw-app drafts from top-level files so the bundle isn't dropped Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(frontend): share raw-app source→draft-value projection across chat and deploy page Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): deploy renamed/new flow, app and raw-app drafts at draft_path, not the temp storage path Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(frontend): add a design-system Checkbox and use it for deploy-page row/select-all checkboxes Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: "Show all drafts" toggle on the deploy-drafts page Replace the deploy-drafts page's legacy-hiding "Only my drafts" toggle with a "Show all drafts" toggle that switches the listing scope between the current user's own drafts (+ legacy no-owner rows) and every user's drafts in the workspace. Backend (`drafts.rs`, `openapi.yaml`): - `/drafts/list` gains an `all_users` query param that drops the owner filter, and a per-row `mine` flag (own draft or legacy no-owner row). `DISTINCT ON` now prefers the user's own row, then the legacy row, then another user's, so `mine`/`legacy_draft` describe the kept row. Frontend (`CompareDrafts.svelte`, `workspaceDrafts.svelte.ts`): - "Show all drafts" toggle (default off). The all-users superset is fetched lazily via the shared resource only while the toggle is on, so the page's fork draft-count (own drafts) is unaffected. - Other users' drafts are view-only: disabled checkbox + Discard with a "belongs to another user" tooltip; Show diff stays enabled. Selection, select-all and the deploy count only ever include the user's own drafts. The multi-user warning triangle shows on owned rows only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(backend): gate all_users draft listing by read permission Addresses the PR review on the per-user deploy-drafts page: - `/drafts/list?all_users=true` previously had only `WHERE workspace_id = $1` with no read-permission check, so any non-operator could enumerate every draft's path, summary and authors — including items they can't read. Now rows the caller doesn't own (`mine = false`) are gated through `require_can_read_path` (the same gate `/drafts/get` uses) and dropped when unreadable; both its `NotFound` and `NotAuthorized` denials are treated as "not visible". - Skip the per-row `require_can_write_path` probe on those non-owned rows (they're never selectable — `isSelectable` requires `mine`): set `can_write = false` directly, removing a redundant N RLS write-probes when `all_users` is on. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): only confirm destructive draft discards on the deploy page Discarding a draft is non-destructive in every case except removing the last draft of a never-deployed item (`draft_only` with no other user's draft), which permanently deletes it. Confirm only that case; reverting a draft over a deployed item, or discarding your copy while another user still holds a draft, now runs immediately (the ⚠️ already signals the multi-user case). Drops the redundant "other users still have a draft" / "deployed version unaffected" confirmation branches. Harden the destructive check: it keyed off `otherDraftUsers()`, which subtracts `currentUsername`; while `$userStore.username` is unhydrated, your own draft looked like another user's, flipping a draft-only item to "non-destructive" and deleting it with no confirmation. Now: deployed counterpart → never destructive; `draft_only` with unknown `currentUsername` → treated as destructive (confirm). The delete modal also shows the friendly `draft_path` instead of the raw `draft_{uuid}` storage path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): deploy low-code app drafts (value + summary persistence) A visual (low-code) app draft is autosaved as the *bare* App value (grid/theme/... plus a draft-only `draft_path`), not wrapped in { value, summary, policy } like script/flow drafts. The Review & Deploy page read `requestBody.value = d.value` — undefined for that shape — so deploying any low-code app draft (created or edited) sent no value and failed. Read the value from the draft object itself, strip the draft-only `draft_path` from it, and use that as the deploy path. Also persist the app summary, which was dropped entirely: the autosave stores the bare App value (the summary normally lives only in the `app` table column, set on deploy), so a draft never carried it — reopening a draft or deploying it lost the summary. Mirror the summary onto the autosaved App (like `draft_path`), read it back when loading a draft, and on deploy send it as the summary column while stripping it (and `draft_path`) from the deployed value so the value stays clean. Verified end-to-end: a new low-code app with a summary deploys at its pretty path with the summary set, content intact, and no draft_path/summary leaked into the deployed value; the draft is cleaned up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4e4b2247ef |
fix: db-backed draft fixes — review-page UX, legacy drafts, session restore (#9600)
* fix(frontend): session-pane draft seeding + restore actions Seed per-tab last_sync from the server draft's draft_saved_at in the loadFlow/loadScript "no local draft" branches (mirroring loadRawApp) so the seeding save attaches a matching last_sync and the server no longer clobbers an existing server draft with a fresh created_at. Replace the no-op loadFlow/loadRawApp-based diff-drawer restore handlers with proper restoreDeployed/restoreDraft that reset the live UserDraft cell (the inbound sync then updates the preview) and delete the per-user server draft, mirroring ScriptEditorView. Add rawAppValueToDraft to project a deployed raw-app value into the draft shape. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(api): move UserDraftOverlay/UserDraftItemKind out of openflow inline block These two schemas were defined between the python-client's "# -- INLINE START/END --" markers, whose contents build.sh replaces with the openflow legacy wildcard $ref. That deleted both definitions during bundling while ~19 path responses still referenced them, failing the python-client build. Relocated them after the marker block. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(frontend): explain legacy drafts in the draft badge popover The home-page draft badge lists each draft owner; a workspace-level row from before the per-user drafts migration shows as "Legacy workspace draft". Add an info tooltip next to it explaining that a legacy draft isn't tied to any user (email NULL) so everyone with access to the path sees it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(compare): show friendly draft path on the review & deploy page list_drafts now surfaces the draft JSON's `draft_path` (when set and different from the storage path) alongside summary, mirroring the home-page list endpoints. CompareDrafts displays it instead of the `u/{user}/draft_{uuid}` storage path, while all fetch/deploy/discard calls keep using the storage path (the draft's server-side key). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(compare): delete the storage-path draft when deploying a renamed draft Deploying a draft from the review page replays the editor's create/update at the draft's friendly path, which deletes the draft server-side only at that path. A never-deployed item parked at `u/{user}/draft_{uuid}` therefore left its storage-path draft behind on deploy and kept listing. Delete the storage-path draft for every kind after a successful deploy, mirroring the editors' discardDraftAfterDeploy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(compare): badge legacy drafts on the review & deploy page list_drafts now reports `legacy_draft` (true when the listed row is a workspace-level NULL-email draft and no per-user row exists at the path). CompareDrafts shows a "Legacy draft" badge with a hover tooltip explaining these predate the per-user drafts migration and aren't tied to a user. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(compare): allow discarding a legacy draft from the review page Legacy drafts (workspace-level, email NULL) aren't owned by the authed user, so the email-scoped draft delete in update_draft never matched them and the discard was a silent no-op. Add a delete-only `legacy` flag that retargets the DELETE (and the conflict re-read) to the NULL-email row, and route the review page's discard of a legacy draft through it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(backend): prune orphaned sqlx offline cache entries Re-ran the canonical update_sqlx.sh after rebasing windmill-ee-private onto origin/main and re-running substitute_ee_code.sh. Compiling the full workspace with all features recorded every live query and pruned 55 stale cache entries no longer produced by any query (22 are the removed `draft_only`-on-app lookups dropped by the db-backed user drafts work; the rest pre-existing orphans). Orphan entries don't break offline builds — this is cleanup only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(drafts): stop migrated draft-only items flooding the home list 20260609165313_remove_draft_only inserted the legacy (email IS NULL) draft stubs without an explicit created_at, so every row defaulted to the migration's now() (transaction_timestamp, constant for the whole transaction) and they all bunched at the migration instant — flooding the top of the newest-first home list. Add a corrective migration that resets those rows' created_at to the epoch so they sort to the bottom (their real per-item timestamps are unrecoverable — the source rows were deleted and the draft value carries no timestamp; editing one bumps created_at to now() and floats it back up). The rows are identified exactly via _sqlx_migrations.installed_on, which sqlx writes in the same transaction as the migration so it is byte-identical to the inserted rows' created_at; rows edited since no longer match and are left alone. Leaving remove_draft_only intact (rather than neutralizing it) keeps its essential schema work running everywhere; this migration runs right after and corrects the timestamps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(migration): note both timestamps are timestamptz in draft created_at repair Pre-empt a misread: draft.created_at became TIMESTAMPTZ in 20260514233244, so `created_at = installed_on` is an exact instant comparison, not a tz-sensitive timestamp/timestamptz cast. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(compare): resolve friendly draft path per kind + strip email from u/ path list_drafts read the friendly path only from value->>'draft_path', which is empty for scripts — the script editor binds the Path widget to script.path, so the typed path round-trips through the draft JSON's own `path` (flows/apps/raw -apps use draft_path). Read the right field per kind, matching the home-page list endpoints, so renamed never-deployed scripts show their friendly name. Also truncate the user segment at `@` when displaying a `u/{user}/…` path: auto-generated draft slots are `u/{user}/draft_{uuid}`, and in the admins workspace (or email-as-username setups) `{user}` is the full email (`u/admin@windmill.dev/…` → `u/admin/…`). Display only — the path/key used for fetch/deploy/discard is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(raw-app): make diff-drawer "restore to deployed" reset like the autosave indicator The diff drawer's restoreDeployed ran the same runResetToDeployed as the AutosaveIndicator's "Reset to deployed", but its onResetToDeployed callback also did `redraw++`, remounting RawAppEditor mid-reset (inside the stopSync bracket); the fresh mount's draft write resurrected the draft, so the restore appeared to do nothing. Extract a single `reloadDeployed` callback (drop the draft handle + reload without the draft overlay) and use it for the diff drawer, the conflict modal, and the AutosaveIndicator so all three reset the same way. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(compare): don't show auto-generated draft path as the bold title A never-named draft lives at a synthetic `u/{user}/draft_{uuid}` slot. When it had no summary and no friendly draft path, that uuid showed as the row's bold title. Return '' from displayPath for auto-generated paths so they aren't bolded — the row still shows the storage path in its secondary (grey) line. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(diff-drawer): remove obsolete draft-vs-current tab selector The "Latest saved draft <> Current" comparison is obsolete. Remove the whole diff-type tab selector; normal-mode diffs now always show deployed-vs-current, simple-mode shows its single custom diff. Drop the now-unreachable restore-to-draft button and the `restoreDraft` prop (plus the dead handlers in the session editor views). The content/metadata selector is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(compare): make "Reset to deployed" work from the diff drawer Route the raw-app session preview and the low-code app editor diff-drawer restore through the same reset-to-deployed callback the AutosaveIndicator uses. - Raw-app session: add a deployedOnly path to loadRawApp that bypasses the draft (cell + server overlay) and reloads the deployed value; the diff drawer's restore now runs it via runResetToDeployed instead of rebuilding the draft shape in place (which hung and never reset). Also wires the in-session AutosaveIndicator reset. - Low-code app editor: drop the goto in the diff-drawer restoreDeployed that re-ran the page load with the draft overlay on and resurrected the draft; share one reloadDeployed across the diff drawer, AutosaveIndicator and the load-latest-deploy modal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
82e2197922 |
chore: remove deprecated enable_1m_context from AI providers (#9580)
* chore: remove deprecated enable_1m_context from AI provider code 1M context is now standard on Anthropic models — the beta header `anthropic-beta: context-1m-2025-08-07` is no longer needed. Remove the field from ProviderCredentials and AnthropicQueryBuilder, and stop injecting the beta header in both the API proxy and worker query builder paths. The field is kept (as `_enable_1m_context`) on the ProviderResource deserialization structs in both windmill-api and windmill-ai so existing resources with the field still deserialize without error. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: drop vestigial _enable_1m_context field from AI resources Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: assert legacy enable_1m_context keys still deserialize Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
3bf6e102af |
fix(apps): apply scope-path predicate to app list/search endpoints (#9581)
The list_apps and list_search_apps endpoints did not filter returned rows against the calling token's resource-qualified scope. A token scoped to apps:read:u/foo/specific_app could list every app in the workspace, including full app_version.value definitions. Apply build_scope_path_predicate, mirroring the protection already in place for script, flow, resource and variable list endpoints. Fixes WIN-2046 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a69505df9b |
fix: expose parent_hash in MCP createScript tool for updates (#9586)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1fc355709c |
feat: Db-backed user drafts (#9351)
* Db draft removal * refactor: drop unsaved-changes confirmation modal from editors * fix: remove nodraft from flow row edit link * fix: remove nodraft from app and raw app edit buttons * fix: remove nodraft from all edit links * fix: merge backend defaults into legacy autosaves to avoid spurious restore toast on raw apps * feat: add username column to draft table for user-scoped drafts * feat: add sync_drafts and list_users_with_draft_on_path endpoints * feat: add UserDraftDbSyncer service for bi-directional draft sync * feat: wire UserDraft.save through DbSyncer + conflict modal * refactor: gate useLocalStorageValue nested-update effect behind opt-in flag * refactor: move sync force flag from request-level to per-entry * feat: sync all userdraft kinds, switch draft owner to email FK, add id PK, scope draft list to readable paths * refactor: route draft permission check through authed.folders + RLS, drop client-supplied email * feat: support draft deletion via sync (value: null) with same conflict semantics * feat: surface other users' drafts in editors with diff+fork action * refactor: unify draft schema migrations and type kinds via DRAFT_KIND enum * perf: add (workspace_id, email, created_at) partial index for sync hot path * chore: update ee-repo-ref to a30079e75dc5b7d7413aa8ee20e40e80bfea9cbd This commit updates the EE repository reference after PR #597 was merged in windmill-ee-private. Previous ee-repo-ref: 55c19293232be379a3044eb78f677b545882ffd6 New ee-repo-ref: a30079e75dc5b7d7413aa8ee20e40e80bfea9cbd Automated by sync-ee-ref workflow. * fix(userdraft): trigger sync on deep mutations via readFieldsRecursively * Rollback UserDraft * remove queuing logic * pushDrafts * refactor: remove draft sync layer and conflict modal * feat: add save_draft, list_drafts, get_draft routes * feat: add get_draft overlay to getScriptByPath * feat: extend get_draft overlay to flow, app, resource, variable, schedule, triggers * feat: support null value in save_draft for deletes * readLastSyncMap * feat: redirect /add pages to /edit/draft_uuid with new_draft flag * fix: inline get_draft query field instead of flattening * fix: drop dangling nobackenddraft assignment in flows edit * feat: include user drafts in list endpoints with is_draft flag * fix: prefix draft paths with u/{user} and seed editor state on new_draft * fix: route draft-only deletes through UserDraftDbSyncer on home page * feat: delete user drafts when their underlying item is deleted * fix: empty path seed on new_draft so friendly auto-name fires * feat: re-add Draft and Draft only badges on home page rows * fix: synthesize value wrapper on draft-only raw_app response * fix: tolerate missing latest-version on draft-only flow reload * fix: skip first observable change in DB sync effect to match LS persist * fix: remove URL-hash sync from script editor (already marked TEMP) * refactor: drop localStorage layer from UserDraft * refactor: drop vestigial LS-era code from UserDraft * feat: migrate localStorage drafts to DB on layout mount * fix: migrate session runtime + script view to per-user draft API * feat: add 'Reset to deployed' action on draft-loaded toast * feat: hide 'Reset to deployed' action when no deployed version exists * createCoalescingKeyedRunner * example ts doc * createDebouncerByKey * refactor: drop await on draft-delete in reset flows, refetch deployed directly * fix: bridge saved-draft shape to wire shape in apps/resources/variables loaders * feat: route UserDraftDbSyncer.save through debouncer + coalescing runner * feat: add immediate-save bypass that cancels pending debouncer + runner tasks * fix: seed UserDraft cell from spec defaultValue on acquire * fix: redirect /add routes at load phase to eliminate white flash * fix: drop +page.js files in /add routes that conflicted with +page.ts * refactor: send draft as separate .draft field instead of deep-merging onto deployed * feat: surface draft path in home list when user typed one different from URL * feat: add UserDraft.stopSync/restartSync, wire on script + low-code app /add init * fix: thread URL path into ScriptBuilder.stopSync (was using empty initialPath) * fix: also stopSync in route's new_draft branch + queue pre-acquire suspensions * feat: add AutosaveIndicator backed by reactive UserDraftDbSyncer.getState * refactor: drop draft-loaded toast in non-route editors, banner now compares draft vs deployed * fix: gate per-user draft-only rows in listings on include_draft_only flag * feat: flush pending draft saves via keepalive fetch on tab hide / pagehide * autosave indicator nits * fix: route create-vs-update on /add deploys; seed policy.execution_mode; sync script template * chore: add [draft-sync] console logs to trace script bootstrap autosave * fix: seed auto-generated path in script new-draft route to suppress Path widget's autosave-triggering mutation * fix: defer script restartSync until script.path lands (Path widget gated on $userStore + $workspaceStore) * fix: poll script.path via tick() until Path widget settles before restartSync * chore: log inferArgs underlying error on deploy to diagnose 'Could not parse code' toast * fix: wait for script.path to stabilize across two ticks before restartSync * revert: drop unsuccessful path-stabilization heuristics + leftover [draft-sync] logs * fix: seed new-draft script schema as emptySchema() so inferArgs doesn't trip on undefined properties * fix: heal legacy drafts with schema={} (no .properties) on deploy * autosave indicator * refactor(editors): drop UnsavedConfirmationModal mount + Show diff button * feat(drafts): collaboration banner, cross-tab conflict detection, raw app template picker - Other-users-drafts banner (Modal2): the deployed-overlay response now carries `other_drafts_users` (workspace usernames only, never emails); each row offers View JSON + Fork. Drops the standalone `listUsersWithDraftOnPath` endpoint; `getDraftForUser` now takes a workspace `username` query param (resolved to email server-side). - Cross-tab/browser save conflict detection: the syncer attaches `last_sync` to every save (defaults to non-force); on a `conflict` response it parks a snapshot in a reactive map. Each route mounts a `DraftSyncConflictModal` and seeds the per-tab `last_sync` via `recordRemoteSync(query, draft_saved_at)` on every `get_draft` load. Keepalive flush also respects optimistic concurrency. - Raw app template picker re-added after the /add ⇒ /edit refactor: framework (React 19 / 18 / Svelte 5), data table + schema config, and optional AI prompt — extracted into `RawAppTemplatePicker.svelte` and driven by `new_draft=true` on the edit route. * fix(drafts): suppress autosave during /add template seeding on script + raw app editors - ScriptBuilder: delay `restartSync` 500ms past `initContent` + stores- ready so the Path widget's `$workspaceStore && $userStore`-gated `initPath → reset → onMetaChange → bind:path` cascade lands inside the suspension window. Two `tick()` waits weren't enough — the bind:path mutation fired ~100ms after the prior `restartSync` and posted as a "user edit". - apps_raw route: suspend autosave on `new_draft=true` and resume only after the framework picker closes (via `onStart` or X dismissal), with a two-tick settle so the picker's seeded `files/runnables/data/policy` mirror to `draftHandle.draft` observably advances `lastSerialized` before sync re-arms. * fix(drafts): land /add redirects on the real workspace username, not "me" The `/add` → `/edit/u/{username}/draft_{uuid}` redirects ran during SvelteKit's load phase, BEFORE the (logged) layout's async `getUserExt` populated `userStore`. `get(userStore)?.username` returned undefined and fell back to the `'me'` placeholder on every fresh nav, producing `u/me/draft_{uuid}` paths instead of the user's real namespace — broke ownership checks against `authed.username` and silently scoped autosaves under the wrong path. Layout now persists `username` to localStorage on every successful `getUserExt`, and `getUsernameForNamespace` (new shared helper, used by all four `/add/+page.ts` files) reads the live store first, falls back to the cached value, and only then to `'me'` for true first-ever loads. * fix(drafts): key low-code app autosave on the URL path, not the empty string `AppEditor` keyed its `UserDraft.use` handle on `newApp ? '' : path` — a legacy leftover from when `/apps/add` was its own URL (no path). With the `/add` ⇒ `/edit/u/{user}/draft_{uuid}` redirect, `newApp=true` made autosaves land on the `('app', '')` row instead of the URL path: - The `apps/list?include_draft_only=true` query joins drafts onto `app.path`, surfacing drafts at the URL path. The empty-path row didn't match the user's URL so the draft never appeared in the home list. - Refreshing `/apps/edit/u/{user}/draft_{uuid}` re-fetches at the URL path with `?get_draft=true`, finds nothing, and 404s. Drop the ternary so the handle always uses `path` — the same as scripts/flows/raw_apps. The route's `?new_draft=true` branch already seeds the empty-template baseline, so there's no longer a "the draft sits under '' until first save" race to worry about. * fix(raw_app): propagate template picker X / Esc dismissal so autosave resumes The picker mounted `<Modal kind="X" open ...>` (one-way prop, not `bind:open`). When the user dismissed via X / Esc / click-outside, the inner Modal flipped its own local `open` to false (hiding the UI) but never wrote back to the picker's `open` $bindable. The route's `templatePicker → false` watcher — the one that calls `restartSync` two ticks after the picker closes — never fired, so autosave stayed suspended and the user's edits after dismissal were silently dropped. Switch the inner Modal to `bind:open` so the dismissal bubbles all the way up to the route's state. "Start without AI" already worked because its `onStart` handler explicitly sets the picker's `open = false`. * nit unused * fix(drafts): make the home-page View/Edit JSON action work on draft-only apps The "View/Edit JSON" entry on the home page called `AppService.getAppByPath` without `get_draft=true`, so for draft-only items at `u/{user}/draft_{uuid}` the backend 404'd with "App not found at path …". Pass `get_draft=true` and render the synthesized stand-in's editable shape: - App drafts come back as `{summary, value, path, policy, ...}` — `value` is the App definition the editor was working on; show that. - Raw-app drafts come back as the flattened `{files, runnables, data, summary, policy, ...}` with no nested `value`; show the whole shape. On save, draft-only items can't go through `updateApp` (no deployed row). Route the edit through `UserDraftDbSyncer.save` (with `immediate: true` so `await` resolves after the POST lands) and relabel the button "Save draft" + Save icon. Deployed items keep the existing "Deploy" flow unchanged. * fix(drafts): render the right shape in View/Edit JSON for draft-only items The previous fix landed `fapp.value` into the editor, but the deployed-overlay flattens the bare editable shape into `inner`/the top-level response — drafts have no nested `.value`. So: - App drafts (`{grid, breakpoints, hiddenInlineScripts, …}`) rendered as empty (`fapp.value` was undefined). - Raw-app drafts 404'd outright: `get_draft=true` with no `rawApp` flag can't tell which draft kind to look up, defaults to `app`, doesn't find one. Thread the row's `raw_app` flag from AppRow → `appExport.open(path, rawApp)` → `getAppByPath({..., rawApp})` so raw-app drafts resolve to the right `UserDraftItemKind`. Read `fapp.draft` (the bare editable shape from `fetch_draft_only`) into the JSON editor for draft-only items — clean payload, no `is_draft` / `no_deployed` / overlay noise. Save the same bare shape back through the syncer so the regular editor reads it unchanged on the next mount. * fix(drafts): skip public-secret-URL fetch in the Deploy drawer for draft-only apps Opening the Deploy drawer on a `/edit/u/{user}/draft_{uuid}` app fired `AppService.getPublicSecretOfApp` immediately because the gating effect only checked `appPath != ''` + `savedApp`. The `/secret_of/{path}` route plain-SELECTs `app.id`, so a draft-only path 404'd with "App not found at name …" and the public-URL ClipboardPanel spun forever waiting on `secretUrl`. Thread the existing `newApp` signal (already on `AppEditorHeader` / `RawAppEditorHeader`) into `AppEditorHeaderDeploy`, gate the fetch behind `!newApp`, and render the existing "Deploy this app once to get the public secret URL" placeholder instead of the spinner for draft-only items. * fix(drafts): disable Diff button on draft-only items across the 4 editors Diff has no baseline to compare against on draft-only items — the button used to be gated by the pre-PR `/add` route's own state, but the `/add → /edit` redirect landed everything under the regular `/edit` page where the gate was missing. - ScriptBuilder: gate the topbar Diff on `savedScript.no_deployed`; seed `no_deployed: true` on the route's `new_draft` empty NewScript so the gate fires before the first deploy. - FlowBuilder: gate the topbar Diff on `newFlow` (route already sets it from `backendFlow.no_deployed` and the new-draft branch). - AppEditorHeader: gate both the "Diff" dropdown action and the Deploy-drawer's "Diff" button on `newApp`. - RawAppEditorHeader: gate the topbar Diff + the Deploy-drawer's "Diff" button on `newApp`. Each gate also rewrites the tooltip ("Deploy this … once to compare against the deployed version") so the hover state explains why. * fix(drafts): disable the "No login required" toggle on draft-only apps Flipping the toggle called `setPublishState`, which POSTs the new `policy` through `AppService.updateApp` — that handler's `UPDATE app ... RETURNING path` finds nothing on a draft-only path and `not_found_if_none` 404s with "App not found at name …" (apps.rs:1975). Gate the Toggle on `!newApp` too so the user has to deploy once before configuring the publish state. * refactor(drafts): drop dead draft_path field from list responses The draft-only listing branches in scripts/flows/apps computed a `draft_path` from the draft JSON (when the user-typed path differed from the URL's autogenerated `u/{user}/draft_{uuid}`), and `{Script,Flow,App} Row.svelte` preferred it over `path` for the row title. In practice that path is never written: the app, raw-app and flow editors all warn "Deploy the X to make the path change effective" — the rename only lands on deploy, never in the draft. So the field is always None and the home rows always show the autogenerated slot anyway. Drop the field from the three `Listable*` structs, the three draft-only push sites, the three OpenAPI response schemas, and the three frontend row components. Client regenerated. * fix(drafts): seed a friendly name on /flows/add The flow route passed `initialPath={page.params.path ?? ''}` to FlowBuilder, so on the `/flows/add → /flows/edit/u/{user}/draft_{uuid}` redirect the Path widget's `initPath` saw a non-empty `initialPath` and skipped the `reset()` branch that auto-generates the friendly `<random_adj>_flow` name. The other three editors all clear `initialPath` in their `new_draft` branch for exactly this reason. Track `initialPath` as route-owned state (defaults to the URL path) and clear it to '' inside the `new_draft` branch, then bind it through to FlowBuilder so any post-deploy update from the editor still propagates. * feat(drafts): render friendly user-typed path on home list for all 4 kinds Reinstate `draft_path` on `Listable{Script,Flow,App}` so the home rows prefer the user-typed name over the autogenerated `u/{user}/draft_{uuid}` URL slot, with two source rules — one per how each editor wires the Path widget: - Scripts already work: `ScriptBuilder` binds the Path widget directly to `script.path`, so the typed path round-trips through the draft JSON's own `path` field. Backend extracts `v["path"]` when it differs from `row.path`. - Flows / apps / raw apps don't write the typed path into the autosaved value (`Flow.path` is one-way-bound to `$pathStore`; the bare `App` / raw-app value has no `path` field at all). Introduce an explicit `draft_path` field on the draft JSON, written by the editor ONLY when the typed path differs from the deployed/seeded `savedX.path`: - FlowBuilder: $effect on `$pathStore` mutates `flow.draft_path`. - AppEditorHeader: $effect on `newEditedPath` mutates `$app.draft_path`. - RawAppEditorHeader: $effect surfaces `pendingDraftPath` up via the bind chain (RawAppEditor → route); the route's draftHandle.draft spread includes `draft_path` when set. Backend extracts `v["draft_path"]` and `None` when unchanged or after deploy (deploy clears the whole draft, so the field naturally disappears post-deploy without bookkeeping). Flow route's `new_draft` branch now stops sync around the Path widget cascade, with a 700ms scheduled `restartSync` (mirrors the existing scripts/apps/raw_apps stoppers) — the new draft_path mutation lands inside that window so `/flows/add` no longer fires an autosave before the user's first edit. openapi/sqlx regenerated. * fix(drafts): preserve the user-typed draft_path on reload of draft-only items The flow / app / raw-app editors all dropped the saved `draft_path` back to the URL's `u/{user}/draft_{uuid}` slot the moment the user reloaded a draft-only edit page: the route sourced the Path widget's initial path from `page.params.path` instead of the previously-saved `draft_path`, and the first user edit then mirrored that URL path back into the autosaved draft — silently overwriting the friendly name in both the row and the editor. - Flow route: after computing `effectiveFlow`, override `flowInitialPath` with `effectiveFlow.draft_path` when set. - App route: pass `newPath={(app.value as any)?.draft_path ?? app.path}` through to `AppEditor`; AppEditorHeader's `newEditedPath` default now prefers a non-empty `newPath` over the random `<adj>_app` seed (the `newApp && !newPath` branch keeps the `/apps/add` friendly auto-name). - Raw-app route: surface `savedRawAppDraft.draft_path` onto `backendApp` so the `extractRawApp` path seeds `newPath` with the friendly name. Reload + a subsequent edit now leaves `draft_path` intact for all three kinds; verified end-to-end via the `/drafts/get_draft/...` endpoint. * fix(ui): default Modal2 target to 'body' so omitting the prop doesn't throw Modal2 defaulted `target = ''` and forwarded it to `Portal`, which calls `document.querySelector(target)` — an empty selector throws "Failed to execute 'querySelector' on 'Document': The provided selector is empty" and the modal silently fails to mount. That's why `OtherUsersDraftsModal` (and `DraftSyncConflictModal`) never appeared on editors where another user had a draft — both omit the `target` prop. Other Modal2 callers (StorageSettings, CriticalAlert, CustomInstanceDbWizardModal, …) pass an explicit `target="#content"` and were unaffected. Match Portal's own default of `'body'` so omitting the prop is now a no-op rather than a runtime throw. * fix(drafts): Reset to deployed no longer resurrects the draft The toast's "Reset to deployed" callback POSTed `value: null` to the syncer, then handed control to the route's `onResetToDeployed` (which wipes the in-memory handle and reloads the deployed payload via `getDraft: false`). Both writes flowed through the reactive sync effect: the wipe scheduled a delete, the reload scheduled a re-save of the deployed value as the new draft. Coalescing collapsed them and the draft came back — making the "discard" action effectively a no-op. Wrap the whole callback in `UserDraft.stopSync` / `restartSync`. The explicit `value: null` POST still goes through (it's a direct `UserDraftDbSyncer.save` that doesn't depend on the reactive effect), the route's wipe-then-reload mutations advance `lastSerialized` silently under suspension, and the next user edit (after two ticks past the deployed-seed write) is the first real save again. * ui nit * feat(drafts): autosave-indicator popover with Reset-to-deployed action Click the cloud icon → popover with "All changes are saved as a draft on the server. The draft is per-user — your teammates' editors keep their own." When the editor isn't on a draft-only path AND the user has a draft (UserDraft.has returns true), a "Reset to deployed" button mirrors the load-time toast action — stops sync, POSTs `value: null`, runs the route's reload-without-draft callback, restarts sync past two ticks so the deployed-seed write doesn't resurrect the draft. Threaded `onResetToDeployed` from each route down to its builder (ScriptBuilder / FlowBuilder / AppEditorHeader / RawAppEditorHeader) and into the indicator. `draftOnly` is wired from `savedScript.no_deployed` / `newFlow` / `newApp` so the action hides where there's nothing to fall back to. The indicator's trigger now has a hover affordance + matches Portal's default target ('body') via Modal2's earlier fix. * fix(drafts): wait for the fork POST to land before navigating OtherUsersDraftsModal's Fork action called UserDraft.save, which routes through the autosave debouncer (1500ms). The subsequent goto fired within the same tick, so the destination editor's get_draft=true read ran before the POST landed and 404'd — refreshing worked because by then the debounced save had fired. Call UserDraftDbSyncer.save with immediate: true and await it. The syncer cancels any queued debouncer task for the key and resolves the promise only after the POST completes, so the route load can find the forked draft on the first try. * fix(drafts): conflict detection — keep last_sync map tab-local instead of in localStorage Two tabs editing the same draft both load with last_sync = T0. Tab-1 saves; the server accepts, returns T1, and the syncer wrote T1 into localStorage. Tab-2 then tries to save: it reads the SHARED localStorage map, sees T1 instead of its own baseline T0, sends last_sync = T1, and the backend's WHERE clause (`created_at <= last_sync`) is true → tab-2 clobbers tab-1's edit without ever seeing a conflict. Move the map to tab-local memory (`new Map<string, …>`). Reload of the tab now starts with an empty map; that's fine because the editor's load path calls `recordRemoteSync(query, draft_saved_at)` right after `get_draft=true` returns, reseeding from the authoritative server timestamp before any user edit could fire a save. * fix(drafts): OtherUsersDraftsModal — close on Fork, don't leak clicks through nested JSON Two bugs in the per-editor "another user has a draft" banner: - Fork landed the immediate save but didn't close the banner before navigating. Svelte hadn't torn down the previous route's components by the time goto returned, so the banner lingered on top of the destination editor. Comment the explicit isOpen=false on the happy path so it's clear it MUST run before goto. - Clicking anywhere on the screen while the View JSON drilldown was open closed the underlying banner too. Modal2's clickOutside action fired on every Modal2 instance — both the JSON modal and the underlying banner — because both attach their own listener at the document level. Add `closeOnOutsideClick` opt-out on Modal2 and pass `closeOnOutsideClick={!jsonOpen}` to the outer modal so clicks outside the JSON drilldown only close the drilldown. Drive-by: Modal2's keydown handler now ignores Escape when its own isOpen is false (was a no-op closer that would still preventDefault on every key press, swallowing key events for any siblings). * fix(drafts): conflict modal wording — drafts are user-scoped, not teammate-scoped * fix(drafts): defer reset-to-deployed restart until first user interaction Two-tick `restartSync` was too aggressive: editor remounts emit a tail of cascading writes (Monaco setValue acks, schema re-infer, UI Builder iframe handshakes, schedule-config recomputes, …) that land well after two ticks and would clobber the just-deleted draft with an upsert of the deployed value — making "Reset to deployed" a no-op in practice, the user kept seeing the draft come back. Centralise the suspension lifecycle in a new `runResetToDeployed` helper. It stopSyncs around the reset, POSTs the explicit delete, runs the route's wipe-and-reload, and then arms a one-shot listener on document keydown / input / pointerdown that restartSyncs on the user's next real interaction. A 5-second fallback re-arms sync if the user walks away without touching the editor, so suspensions don't leak. Use it from both the load-time toast (`notifyDraftLoaded`) and the autosave-indicator popover so the two stay in sync — fixes both entry points. * indicator ui nits * fix(drafts): split tab-switch and unload flushes — kill self-conflict on visibility change The single keepalive flush bound to both `visibilitychange → hidden` and `pagehide` self-conflicted on tab switch: visibilitychange fires on every tab/app switch with the page still alive, the keepalive POST advanced the server's `created_at` to a fresh `now()`, the client discarded the response (no listener), the local `lastSync` stayed at the old value, and the next foreground autosave sent that stale timestamp → server saw `created_at > last_sync` → conflict modal for the user's own background-tab write. A still-pending debouncer task made it worse: it fired a second runner POST after the keepalive with the same stale `last_sync`, the second self-conflicted too. Split into two paths: - `visibilitychange → hidden` → `flushOnVisibilityHidden`: route through the normal runner pipeline. The page is alive, so the response can land and `setLastSync` keeps the baseline current. Call `debouncer.cancel(key)` first so a queued keystroke can't double-fire with the same stale `last_sync`. - `pagehide` → `flushOnPageHide`: keep the `keepalive: true` raw fetch for the genuinely-going-away case (the JS context is torn down, the response is necessarily discarded). Same `debouncer.cancel(key)` guard. On the next mount, the route's `recordRemoteSync(query, draft_saved_at)` reseeds `lastSync` from authoritative server state before any user edit can fire a save. * fix(drafts): drop the visibilitychange flush — debouncer keeps running on hidden tabs Tab switching just hides the page; the JS context survives and the debouncer's `setTimeout` keeps counting down. When it fires, the runner POSTs normally and the server's response updates `lastSync`. There's nothing left for a visibilitychange-driven flush to do that the ordinary pipeline doesn't already handle, and adding one only creates extra POSTs to reason about. `pagehide` remains the single trigger for the keepalive flush — that's the case where the JS context is actually being torn down and the runner's pending fetch would otherwise be killed mid-flight. * nit * refactor(drafts): drop LS-era pipeline; backend is canonical on load The PR's iteration left behind a meta/staleness pipeline carried over from the localStorage era — per-rev tracking, a LocalDraftStaleModal, a 'Restored from local storage' toast, and a localDraft-vs-backend comparison branch in every editor loader. With drafts now living in the DB and the optimistic-concurrency lastSync check handling divergence, that whole stack is dead weight. Worse, the comparison branch caused 'Load from server' in the conflict modal to do nothing: the loader preferred the in-memory cell over the backend, so the user-clicked 'load from server' just re-displayed the local edits AND fired two confusing toasts (Restored from local storage + Loaded your saved draft). The rip: * userDraft.svelte.ts: drop UserDraftMeta, StoredDraft.meta, checkStaleness, UserDraftStalenessCause, normalizeForCompare, localDraftDiffers, saveMeta, getMeta, setDraftAndMeta, setMeta, handle.meta/setDraftAndMeta/setMeta, force option. Handle is now just { draft }. * userDraftToast.ts: drop notifyRestoredFromLocal + RestoreFromLocalActions. Update copy. * LocalDraftStaleModal.svelte: deleted. * AppEditor.svelte: drop initialRevs prop and the firstMirror wipe-then-restore dance (it existed only to consume the meta-mismatch skip slot). * All 4 editor routes: backend is canonical on load — the in-memory cell is overwritten with the deployed+draft overlay, the syncer's seed guard swallows the first write so we don't POST it back. * VariableEditor / ResourceEditor: drop the staleness pipeline + rev bookkeeping; backend wins on open. * useTriggerDraftSync.svelte.ts: inline the JSON-normalize + deepEqual utility as a private cfgDiffers helper (kept for the form-vs-deployed dirty check, which is a genuine semantic compare, not LS legacy). * copilot core.ts / userDraftAdapter.ts: drop meta argument from saveAppDraft, loadAppDraftValue, write*Draft. Test assertions on getMeta dropped. Net: -22 typecheck errors, fewer moving parts, conflict modal works. EOF ) * refactor(drafts): remove dead endpoints + UserDraftDbSyncer.getLastSync The list_drafts and get_draft (own) routes were added during PR iteration and never wired up to any frontend caller — the editor overlay path uses the per-kind get-by-path getDraft query parameter, and the home page lists drafts via the per-kind list endpoints, not via /drafts. Drop both routes (+ sqlx caches + OpenAPI entries). UserDraftDbSyncer.getLastSync was a peep-hole for callers that never materialised — the per-tab lastSync map is only ever read by postSave internally, where the bookkeeping already lives inline. * refactor(drafts): extract DraftEditorModals trailer block The four editor routes (scripts/flows/apps/apps_raw) mounted an identical pair of trailer modals — DraftSyncConflictModal + OtherUsersDraftsModal — wrapped in the same guard chain and {#key path} remount. Lift the markup into one component; routes thread their itemKind, path, editPathFor, and loader callback. Pure markup extraction, no state ownership change. Drops the unused userStore import where the trailer was the only consumer. * refactor(drafts): UserDraft.useReactive — kill array-of-one boilerplate The script + flow routes both wanted a handle that re-keys when the URL path changes. UserDraft.use() can't do that (its opts getter is untracked), so each route hand-rolled the same useMany-array-of-one + proxy idiom: const handles = useMany(() => [{ kind, path: reactive }]) const handle = { get draft() { return handles[0]?.draft }, ... } Add UserDraft.useReactive(getSpec) that internally wraps useMany with a single spec and returns the stable proxy. Callers collapse to one line. * refactor(drafts): unify bootstrap suspension via armRestartOnFirstInteraction The flow and raw-app routes each rolled their own end-of-bootstrap resume: a 700ms setTimeout for flows and a templatePicker watcher with double-tick gating for raw-apps. Both are timing-fragile (the comments admit it) and drift from each other. armRestartOnFirstInteraction already existed in userDraftToast.ts for reset-to-deployed: keydown/input/pointerdown listeners (capture phase) that fire restartSync on the first real user touch, with a 5s belt-and-braces fallback. Export it and use it everywhere we'd previously have picked a magic number. For raw-apps this is a tiny behavioural change: the user's template choice now POSTs immediately (the pointerdown that picks the template also resumes sync, so the picker's onStart write rides the wake-up). Previously the choice only persisted on the user's NEXT edit. That's strictly better — navigating away preserves the choice now. * refactor(drafts): type App.draft_path; drop the as-any cast The audit asked for the three editors to converge on one draft_path injection pattern. For App and Flow, the in-builder $effect-mutates- the-store idiom is wedged into a shape that doesn't natively own the field — App's editor type genuinely has no draft_path so the writer had to cast through `as any`, and consumers downstream did the same. The minimum viable fix: declare draft_path on the local App type (it's already a field on the autosaved JSON). Lifting the writes upward into a route-side merger would mean restructuring the AppEditor mirror $effect and the FlowBuilder pathStore plumbing — larger change for the same shape, deferred to a follow-up. Flow already has the typed cast localised at one site. Will get the OpenAPI-level draft_path field as part of task 47 (drop as-any casts on backend overlay reads). * refactor(drafts): extract makeDraftAddLoad helper Four identical /add/+page.ts files differing only by the edit-route prefix. Lift the redirect into a factory, slim each entry point to two lines. * refactor(drafts): type UserDraftOverlay.other_drafts_users in the OpenAPI The backend response carried other_drafts_users on every get-by-path that supports the draft overlay, but the OpenAPI schema didn't declare the field. Each route had to cast the typed response to `any` to read it (and the sibling draft_saved_at), which obscured the real shape from the type system and rotted the discoverability of the draft surface. Add it to UserDraftOverlay. Frontend casts collapse to plain property reads in the three editor routes. * feat(drafts): list & open draft-only items for variables, resources, schedules, triggers For scripts/flows/apps the list and get-by-path endpoints already surface per-user drafts that have no deployed counterpart — that's what gates the home page from 404'ing on an AI-agent-created draft. Extend the same support to the other UserDraftItemKinds: Backend (list endpoints): - Add include_draft_only to ListVariableQuery, ListResourceQuery, ListScheduleQuery, StandardTriggerQuery (the latter covers the 11 trigger kinds via the generic TriggerCrud). - Append per-user draft rows whose path has no deployed row. Same gate as scripts/flows/apps: non-operators, page 0, no narrowing filters. Synthesis is per-kind: ListableVariable/Resource get field-for-field synthesis; ScheduleLight reads NewSchedule shape; Trigger<T> uses a best-effort JSON merge + serde_json::from_value (rows skipped on deserialize failure rather than failing the list). - Add draft_only: Option<bool> with sqlx(default) to each row type so it serializes as the column is opt-in. Backend (get-by-path endpoints): - get_variable, get_resource, get_schedule, get_trigger<T> fall back to fetch_draft_only when the deployed row is missing and the caller passed get_draft=true. Mirrors scripts/flows/apps. OpenAPI: - Shared IncludeDraftOnly parameter under components/parameters, wired into the 11 trigger list endpoints + listRawApps. Inline declarations on listVariable / listResource / listSchedules / listAzureTriggers. - draft_only field on ListableVariable, ListableResource, Schedule, TriggerExtraProperty. Frontend: - variables, resources, schedules, and the 10 trigger list pages (routes + 9 *_triggers) pass includeDraftOnly: true on the initial fetch and render <DraftBadge draft_only> on synthesized rows. Trigger pages got a sed/perl bulk update — pattern is the same across kinds. * fix(drafts): swap crypto.randomUUID() for the project's randomUUID helper crypto.randomUUID() is gated on a secure origin (HTTPS or localhost). Self-hosted Windmill instances often run on a bare HTTP origin or a LAN IP where the WebCrypto API is unavailable, so the /add redirect would throw before issuing the 307. Use the existing RFC4122 v4 helper in FlowChatManager that the rest of the codebase already imports for this exact reason. * fix(editor): leading-edge fire + max-wait cap on Monaco debounce The Editor debounced `onDidChangeModelContent` purely on the trailing edge — every keystroke rescheduled a 500ms timer, and uninterrupted typing held the bindable `code` prop stale until a pause. Stacked behind our 1.5s autosave debouncer that meant our clock didn't even start ticking until 500ms after the user paused, and the `code` binding never updated mid-burst for downstream consumers (lint, live preview, change listeners). Switch to leading + trailing + max-wait: * First keystroke of a burst fires `updateCode` synchronously, then stamps a wall-clock chain start. * Each subsequent keystroke (re)arms a trailing timer at `min(now + changeTimeout, chainStart + maxChangeTimeout)` — the cap is what makes continuous typing materialize at least once per maxChangeTimeout window instead of indefinitely. * When the trailing fires it resets the chain so the next keystroke after a pause is a fresh leading fire. New prop `maxChangeTimeout` (default 1000ms) sits next to the existing `changeTimeout` (default 500ms). Dispose path clears the chain stamp alongside the timer. * feat(drafts): wire Ctrl/Cmd+S to flush the pending autosave immediately Each builder already had a Ctrl/Cmd+S keybinding routed through a saveDraft() no-op left over from the LS-era — the comment said "persistence happens via the page-level UserDraft autosave" but the shortcut was the user's only way to actually force a save without waiting for the 1.5s debounce. Restore the intent. * UserDraftDbSyncer.flush({ workspace, itemKind, path }) — new method that re-submits whatever's queued in pendingSaveOpts with immediate: true. No-op when nothing's pending. * Editor.svelte.flushPendingChanges() — exposes a synchronous updateCode() with chain reset, so callers can drain Monaco's own trailing debounce before asking the syncer to flush. Without this step a Ctrl+S within ~500ms of typing would POST the pre-burst content. * ScriptBuilder.saveDraft() — editor?.flushPendingChanges() → await tick() → UserDraftDbSyncer.flush(). Toast on result. * FlowBuilder.saveDraft() — no direct Monaco ref (flows have many per-module editors); just flushes the syncer. Editor.svelte's new 1s max-wait cap means at most the last <1s of typing in a module Monaco won't be in this POST; it follows in the next autosave round. * RawAppEditor.handleKeydown — adds a 's' case that flushes before the focus guard, so the shortcut fires regardless of where focus is in the editor pane. * fix(drafts): low-code apps — drop spurious autosave on /edit + remount on Load from server Two bugs in low-code app editor (raw apps use a separate code path): 1. Every /edit visit looked like an autosave because loadApp() called UserDraft.discard('app', path, undefined). The comment claimed "this load doesn't POST" but discard always POSTs value: null server-side — that surfaced as a DELETE-my-draft on every page load AND a flash in the AutosaveIndicator. The discard was originally intended to wipe the in-memory cell so AppEditor remounts "fresh". But the path-change $effect upstream already sets app = undefined before each loadApp, which unmounts AppEditor and releases the handle's entry — so a remount via app = backendApp naturally starts with an empty handle. Drop the discard. 2. The conflict modal's "Load from server" called loadApp() but didn't remount AppEditor. Since AppEditor's stateApp is captured once at mount and doesn't react to prop changes, the editor kept showing the conflicting local edits even after a successful reload. Wrap the onLoadFromServer to await loadApp() then bump redraw to force a fresh mount. * feat(drafts): home-page Draft badge — show user-initial circles, drop the '+' The home-page Draft badge previously showed '+Draft' as a flat label. Add per-user awareness: up to 3 user-initial circles render to the left of the label, ordered alphabetically; with 4+ users we collapse to the first 2 + a '+N' overflow circle so rows stay compact. Backend: * New `DraftUserRef { username: Option<String> }` in windmill-types::user_drafts, re-exported from windmill-common so the list endpoints in scripts/flows/apps crates share one import path (windmill-types/windmill-common can't be reordered without a cycle). * ListableScript / ListableFlow / ListableApp gain a `draft_users: Option<sqlx::types::Json<Vec<DraftUserRef>>>` field. The list SQL adds a per-row subquery `SELECT json_agg(...) FROM draft d LEFT JOIN usr u ...` that aggregates the workspace users with a per-user draft at this path. NULL (no drafts) decodes to None; LEFT JOIN against `usr` lets orphaned drafts (user removed from workspace) still surface with username = None. * Synthesized draft-only rows set draft_users to a single-element vector with the authed user (those rows come from `email = $2`). OpenAPI: `draft_users` added to listScripts / listFlows / ListableApp response shapes as an array of `{ username }` with nullable username. Frontend DraftBadge: * Accepts `draft_users: { username?: string | null }[]`. Renders up to MAX_CIRCLES (3) initial circles; at 4+ users renders first 2 + a gray '+N' overflow circle. * Initials: 'john.doe'/'john_doe' → 'JD', 'alice' → 'AL', the legacy NULL-email row → '?'. * Color picked deterministically from a 6-entry palette so the same user gets the same circle color across rows. * Label is now just 'Draft' (dropped the '+'). 'Draft only' is unchanged. * Tooltip lists every user in full. ScriptRow / FlowRow / AppRow thread `draft_users` through their prop types and pass it to DraftBadge. * fix(drafts): suppress 'You have unsaved changes' banner when deployed baseline is null A brand-new variable/resource/trigger (no deployed row yet) has `getDeployed() == null`, but the caller's `show` prop is computed off `current != deployed` which is trivially true while the user types. Result: the banner appeared with 'Show diff' (no-op — the drawer early-returns on null deployed) and a 'Discard' that's semantically backwards (there's nothing to revert to). Gate `show` internally on `getDeployed() != null`. The check sits in the banner rather than each caller because every caller would otherwise need the same boilerplate guard. * fix(drafts): hide LocalDraftBanner when deployed and current match the DiffDrawer's compare Earlier I gated the banner on `getDeployed() != null`, but the user still saw it fire on entries where 'Show diff' opens to 'No changes detected'. That means `show` (the caller's coarse dirty check) flagged a difference the DiffDrawer treats as a no-op — typically toggle defaults (`false ↔ undefined`), removed empty arrays, or key-ordering noise that `cleanValueProperties + orderedYamlStringify` collapses. Replicate the drawer's comparison inside the banner: stringify both sides through the same pipeline and only render when the keys differ. A single `diffKey()` helper keeps the logic local; the catch-and-empty fallback survives a non-serializable side rather than throwing. * ui(drafts): nest user-initial circles inside the Draft badge Previously the circles sat alongside the Badge in a parent flex container; the result read as two separate UI elements. The Badge component already exposes its children as a snippet rendered inside its own flex row, so moving the circles into it makes them feel like part of the same chip. Knock-on tweaks: shrunk the circles from h-4/w-4 to h-3.5/w-3.5 so the badge stays compact, and tinted each circle's ring with the badge's indigo palette (instead of plain white) so the overlap reads as a deliberate stack rather than dots floating on top of the chip. * feat(drafts): drop the authed user's circle, mark own drafts with a '*' suffix Three tweaks to the home-page Draft badge: 1. Filter the authed user out of `draft_users` before rendering circles. The row already signals 'this user has a draft' via the asterisk (below), so a circle for them would be redundant noise. New `currentUsername` prop on DraftBadge — pass `$userStore?.username` from each row. The tooltip still lists every user (with `(you)` next to the authed one) so the full picture is one hover away. 2. The badge already showed whenever `is_draft || draft_users.length > 0` (per-user OR any-user). Spelled the rationale out in a comment — no logic change. 3. Append '*' to the displayed summary when `is_draft` is true. Falls back to `draft_path`/`path` when summary is empty so the marker never decorates an empty string. Threaded the same expression into ScriptRow / FlowRow / AppRow. Slice/overflow math now keys on the post-filter `otherUsers` list, so dropping the authed user doesn't silently shrink the visible count (e.g. 3 users incl. self → 2 circles, not 1 circle + a '+1' bubble). * feat(drafts): clone per-user drafts when forking a workspace `clone_workspace_data` clones every other workspace-scoped table on fork creation (resources, variables, scripts, flows, apps, raw apps, triggers, schedules) but quietly dropped the `draft` table. With per-user drafts that meant any open editor in the parent lost its pending edits the moment a fork was created — surprising and inconsistent with how forks treat the deployed surface. New `clone_drafts` mirrors the existing clone helpers: a single INSERT...SELECT into the target workspace, preserving `path`, `typ`, `value`, `created_at`, and `email`. The `email` FK targets `password.email` which is instance-scoped so it carries across workspaces without remap. `created_at` is preserved on purpose so the per-tab `last_sync` baseline lines up with the parent's timeline — otherwise the fork's next autosave would race a stale `last_sync` and trip the conflict modal on every cloned draft. Plain INSERT (not UPSERT) is safe because the fork target is empty at create time; no conflict against the partial unique indexes (`draft_pkey_with_user` / `draft_pkey_legacy`). The synthetic BIGSERIAL `id` PK is regenerated by the default so it stays out of the column list. * ui(drafts): pin the authed user to the first circle instead of hiding them Previously the authed user was filtered out of the circle row entirely on the theory that the row's '*' suffix already signalled 'this user has a draft'. New requirement: they should always lead the circle row when they have a draft so the visual half of the signal lines up across rows (consistent leading-slot identity, easy scan). Switch from a filter to a sort: `orderedUsers` finds the authed user in `draft_users` and splices them to index 0; everyone else keeps the backend's alphabetical order behind. Slice/overflow math now keys on `orderedUsers`, which guarantees the authed user never falls into the '+N' bubble — they're at position 0 and the slice keeps the head. The popover's '(you)' annotation moves to the circle's title attr too, so hovering the leading circle confirms the identity. * feat(drafts): drop draft_only column from script/flow/app Drafts now live in the `draft` table exclusively — `draft_only` stubs in script/flow/app are redundant. Migration `INSERT INTO draft ... ON CONFLICT (workspace_id, path, typ) WHERE email IS NULL DO NOTHING` so real per-user drafts already at the same path are preserved; only rare stubs that lost their draft get a synthesised workspace-level row. Stubs are then deleted (FKs cascade to *_version) and the column is dropped. List endpoints keep a synthesised `draft_only: true` on rows sourced from the draft table itself (sqlx default on the struct field). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ui(drafts): surface draft state in AutosaveIndicator instead of toast+auto-modal The "Loaded your saved draft" toast and the auto-opening OtherUsersDraftsModal both surprised users on every editor mount. Move both signals into the AutosaveIndicator label: "Loaded from draft" or "Others are working on this {kind}" (priority) sits where Saving/Saved do, with a one-shot light-green flash behind the indicator that fades to transparent. Saving/Saved still win when they fire. The popover gains a "See others' drafts" button that flips the modal open on demand; the modal itself is now externally controlled via a bindable \`isOpen\` threaded through DraftEditorModals. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ui(drafts): per-user View JSON / Fork actions in DraftBadge popover Hover popover used to be a plain text list of usernames. Now each row gets a colored circle icon + name + "(you)" for the authed user, and every OTHER user's row carries View JSON / Fork buttons mirroring the OtherUsersDraftsModal. For draft-only entries owned solely by the authed user, the popover ends with "Only you can see this {kind}" so the row's privacy is obvious. ScriptRow / FlowRow / AppRow thread workspace + itemKind + path + editPathFor through; AppRow switches between app / raw_app on app.raw_app. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * nit * fix(drafts): clone only the forker's per-user drafts on workspace fork clone_drafts copied every user's drafts, but only the forker gets added to the fork's usr table. Drafts owned by absent users LEFT-JOIN to NULL in the home page's draft_users aggregate, surfacing as multiple legacy-style rows at one path and crashing the popover with each_key_duplicate. Filter the clone to email = forker OR email IS NULL, and key the popover's #each by index defensively so future legacy collisions can't crash the page either. Also re-adds `draft_only: None` to NewScript/CreateFlowBody literals in tests — the auto-generated windmill-api-client still carries the field and the previous commit dropped them too aggressively. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(drafts): always populate other_drafts_users in maybe_overlay_draft Reset-to-deployed reloads the deployed payload with get_draft=false, which made the backend return other_drafts_users=[]. The route then reassigned otherDraftsUsers to the empty list, dropping the count to 0 and hiding "See others' drafts" in the AutosaveIndicator popover — but the other users' drafts hadn't actually gone anywhere. Fetch the list independently of get_draft so the popover stays accurate across reset reloads. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(drafts): alert user when their draft is older than the latest deploy Open a modal on editor mount when the per-user draft was saved before the latest deploy at the same path — i.e. a teammate deployed a new version while this user's draft was sitting. Two choices: discard the stale draft and pick up the deploy, or keep editing the older draft. DraftEditorModals computes the staleness from the timestamps each route threads in (script.created_at, flow.edited_at, app_version.created_at) and the "Load latest deploy" callback reuses the route's existing reset-to-deployed logic. Wired for script / flow / app / raw_app editors; trigger / resource / variable drawer editors follow a different pattern and aren't covered here. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(drafts): deploy only wipes the deployer's draft, not everyone else's Script / flow / app deploys ran an unconditional DELETE on every draft at the path, so a teammate's deploy silently destroyed any other user's pending draft. After the wipe, the other user's tab kept auto-saving — re-creating the row at a NOW timestamp newer than the deploy — and StaleDraftModal never fired because draft_saved_at had been bumped past the deploy. Filter the DELETE to email = deployer (plus the legacy NULL row), so other users' drafts persist and the stale-draft prompt actually fires on their next reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(drafts): surface save failures in AutosaveIndicator instead of pretending Saved postSave caught network errors with `console.error` and let the runner finish normally. The indicator read the saving → none transition as a successful save and flashed "Saved" even when the request had thrown. Track failed keys in a SvelteMap, expose `'failed'` as a new UserDraftSyncState, render "Save failed" in red with a CloudOff icon. Failure clears on the next successful save for the same key, or when recordRemoteSync seeds a fresh authoritative timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(drafts): surface 'Save failed' inside the AutosaveIndicator popover too The popover used to repeat the cheerful "All changes are saved as a draft on the server..." copy even when the inline label said "Save failed", which read as contradictory. Add a red, text-xs warning at the top of the popover body when the sync state is `failed`, explaining that the latest edits didn't reach the server and that editing again retries the save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(drafts): surface the actual error message in the AutosaveIndicator popover Replace the generic "your latest changes did not reach the server" copy with the real failure detail. The syncer now stores the extracted message in the failures map (formatSaveError walks body / message / statusText) and exposes it via the state handle's `failureMessage` getter. Popover renders it in red, monospaced, scrollable so a long server traceback doesn't blow out the popover. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(drafts): suppress Saving/Saved indicator during a reset-to-deployed discard A `value: null` POST is a discard, not a save, but it ran through the same runner the indicator watched — so resetting to deployed flashed "Saving..." → "Saved", reading as "your draft just landed" while we were actually wiping it. Track in-flight discards in a SvelteSet, expose a distinct `'discarding'` UserDraftSyncState, and the indicator stays quiet for it: no spinner, no label change, and the `discarding → none` transition deliberately skips the "Saved" flash. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Revert "fix(drafts): suppress Saving/Saved indicator during a reset-to-deployed discard" This reverts commit |
||
|
|
066d7a4726 |
block operators from inline preview job execution (#9572)
`POST /api/w/{workspace}/jobs/run_inline/preview` ran request-supplied
code inline (in-process, e.g. DuckDB) but was missing the operator
authorization guard that its sibling `/jobs/run/preview` enforces. An
authenticated operator — the most restricted role, which must not run
preview jobs — could execute arbitrary code in a single request
(file read/write, and OS command execution via the DuckDB `shellfs`
extension when worker egress is available).
This is the incomplete-fix residual of CVE-2026-22683 / GHSA-9q9g-rp9x-244h,
whose v1.615.0 patch covered the entity-CRUD endpoints but left this
direct inline-exec sink uncovered.
Add the same `is_operator` guard from `run_preview_script`. Audited the
rest of the preview/inline arbitrary-code endpoints (run_preview_script,
run_bundle_preview_script, run_preview_flow_job, the wait_result
wrappers, run_dynamic_select inline variant, dependency jobs) — all
already carry the guard. The `run_inline_script_by_path`/`by_hash`
endpoints run deployed scripts (operator-allowed, scope-checked) and
correctly remain ungated.
Fixes WIN-2043 (GHSA-pp5h-96x3-3wqq).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
d0aaba0f16 |
require pinned sha for inline raw_code in viewer app run mode (#9570)
* fix: require pinned sha for inline raw_code in viewer app run mode * fix: gate rd_string import behind parquet feature to fix oss build * fix: also require pin for raw_code with app_script id in viewer run mode |
||
|
|
d98efb5711 |
prevent path traversal via log_file_index in log endpoints (#9569)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
765f50c474 |
feat: folder-level label inheritance for scripts, flows and jobs (#9524)
* feat: folder-level label inheritance for scripts, flows and jobs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: use SECURITY DEFINER folder_labels() for RLS-consistent inheritance Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: extend folder label inheritance to apps, resources, variables, schedules Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cf9ad54181 |
feat: workspace protection rule to restrict anonymous app deployment (#9509)
* feat: restrict anonymous app execution mode to admins Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: add regression test for anonymous app admin gate Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: gate anonymous app mode behind workspace protection rule Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: lock app row on anonymous-mode check, fail closed while rules load Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5f41ddd3a5 |
fix: require auth to view approval details when user_auth_required (#9482)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
192574ab8f |
fix(forks): keep trigger/schedule operational state owned by the parent - WIN-2019 (#9476)
* fix(forks): defer trigger/schedule state to parent for clean git merge Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(forks): read parent trigger/schedule state on non-RLS pool for complete substitution Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(forks): read schedule fork-ness on non-RLS pool; clarify mutator-rule wording Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fbdf81ba5f |
fix: authenticate slack callback payload with per-workspace hmac (#9461)
* fix: authenticate slack callback payload with per-workspace hmac Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: regression tests for unauthenticated slack callback decryption Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: verify slack submission signature before resume + close workspace oracle Addresses review: verify private_metadata HMAC before handle_resume_action so a tampered/unsigned submission is rejected up front, and map get_workspace_key failure to the generic 401 so the status code is not a workspace-existence oracle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: domain-separate slack payload hmac from resume-secret hmac Both MAC families key Hmac<Sha256> on the same per-workspace key; resume secrets are distributed to approvers in resume URLs, so add a fixed domain tag (slack_payload_v1) to the slack payload MAC to make the two non-interchangeable by construction rather than by byte-layout coincidence. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e1e7af6a25 |
fix: prevent token label collision bypassing job read access control (#9462)
* fix: prevent token label collision bypassing job read access control Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: regression tests for token label collision job read access Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: bind job-read override fast-path to permissioned_as_email Replaces the reserved-label / label-* exclusion approach: webhook-/http-/email- labels are created through the public token API by the trigger panels, so they cannot be reserved, and blocking label-* regressed legitimate re-reads. Instead the username_override fast-path now requires the job's permissioned_as_email (non-forgeable, never derived from the label) to equal the caller's email. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fad1a549d9 |
feat(otel): connect jobs to the inbound distributed trace (#9456)
* feat(otel): propagate inbound W3C traceparent to job spans Capture the inbound traceparent header at the run endpoints (WebhookArgs::to_args_from_format) into a reserved _wm_traceparent arg key (gated on OTEL_TRACING_ENABLED), riding the args jsonb like _ENTRYPOINT_OVERRIDE. At pickup, create_span_with_name attaches a span link from the job's worker span to the originating distributed trace, so a job triggered by an instrumented service is connected to the caller's trace while keeping its UUID-derived trace id (trace-by-job-id unaffected). The link/parse logic lives in the EE otel modules; this OSS side only captures the header and calls the (no-op outside EE) hook. Companion EE PR required. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: bump ee-repo-ref to inbound-trace-propagation EE branch Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(agents): don't attribute work to specific customers in repo content Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(otel): relocate job + script spans into the inbound trace Builds on the captured _wm_traceparent: the worker job span is re-parented on the inbound caller context, the script subprocess's TRACEPARENT env is the inbound context (so its spans join the caller's trace), and the context is propagated to flow steps so the whole flow relocates. Carried to the worker via a new LogContext.inbound_traceparent field. Non-inbound jobs are unchanged. Adds a relocation integration test. Companion EE PR required. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: bump ee-repo-ref to inbound-trace-propagation relocate commit Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(otel): harden inbound traceparent capture Address review feedback: - strip any caller-supplied _wm_traceparent from args/extra before stashing the header-captured value, so the reserved key is Windmill-controlled only - valid_w3c_traceparent: reject version ff and require lowercase hex, so we don't forward an inbound header that downstream OTel parsers would reject - clarify that the capture helper does not validate the W3C format (done at use) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: update ee-repo-ref to 2c7964460327fab5e3a27c0f74b8d6f26ab7f79a This commit updates the EE repository reference after PR #604 was merged in windmill-ee-private. Previous ee-repo-ref: 8fc04fb105dc49769205f7174d551a0d134d1bec New ee-repo-ref: 2c7964460327fab5e3a27c0f74b8d6f26ab7f79a Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com> |
||
|
|
90677872f6 |
fix: distinguish canceled jobs in runs (#9452)
* fix: distinguish canceled jobs in runs
* fix: order status=failure|canceled by completed_at to use partial index
The new `status` query param replaced the legacy `success=false` filter on
the Runs page, but the ORDER BY switch in list_completed_jobs_query only
flipped to v2_job_completed.completed_at for success==Some(false). With
status=failure|canceled (and success=None), the query fell back to ordering
by v2_job.created_at, which the partial index
ix_v2_job_completed_failure_workspace (workspace_id, completed_at DESC WHERE
status IN ('failure','canceled')) cannot serve.
EXPLAIN ANALYZE on 500k rows (1% failure/canceled): ordering by completed_at
uses the partial index (~150 buffers, 0.3ms); ordering by created_at scans
the v2_job created_at index and probes/discards 99% of rows via the join
(~49k buffers, 31ms). Switch the ordering to completed_at for
failure/canceled so the partial index serves both filtering and ordering.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: trim order-by regression test to the failure/canceled case
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: only treat canceled as a terminal status icon for completed jobs
Guard the canceled branch in JobStatusIcon and getJobStatusKind with
`'success' in job` so a job that is still running while being canceled keeps
its running icon/favicon until it completes, instead of immediately showing
the gray Canceled state. Also clarify the openapi `status` param is an exact
match (status=success excludes skipped, unlike success=true).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
cf5fefb521 | feat: add metadata generation model setting (#9418) | ||
|
|
8053266f88 |
fix(mcp): resolve MCP resource token via caller RLS + SSRF-guard url (#9428)
* fix(mcp): resolve MCP resource token via caller RLS + SSRF-guard url Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(mcp): clone user_db for oauth2 refresh and drop advisory ids from comments Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(mcp): disable redirects on MCP client to prevent SSRF bypass Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
89a7a37776 |
fix(backend): authorize single-job read endpoints by job/flow visibility (#9416)
* fix(backend): authorize single-job read endpoints by job/flow visibility Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(jobs): share read links + cached access checks for run visibility - Cache the job read-access RLS probe (size-bounded LRU keyed by the caller's authz-relevant identity + job id; no TTL since job-side inputs are immutable). - Inherit visibility along the full parent_job chain so any flow you can see lets you read its (deeply nested) steps. - Share read links: GET /jobs/job_view_token/{id} mints a stateless HMAC(workspace_key, job_id) token (only if the caller can read the job); the token grants an authenticated member read of that job and its flow subtree via a ?view_token query param or X-View-Token header. Run page gains a Share button and honors a ?view_token link. - Denied-but-existing reads now return 403 with guidance to request a share link (vs 404 for non-existent), and the run page renders that case with instructions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(jobs): address PR review — scope-tag check on mint, constant-time view-token verify - P1 (Codex): get_job_view_token now enforces the caller's if_jobs:filter_tags scope before minting, so a tag-scoped token can't mint a transferable link for a job outside its tags. Adds a scoped-token regression test (allowed + denied). - Constant-time view-token verification (HmacSha256::verify_slice) instead of comparing hex strings (Claude/Pi nit). - get_completed_job_result: an authed reader passing an invalid suspended-secret triple now falls through to the normal visibility gate instead of erroring out (Claude nit); unauthenticated callers still rejected. - Length-prefix the read-access cache key fields so no input values can collide. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(api): add job_view_token to openapi spec; use generated client in run page Addresses Codex review nit: the new GET /jobs/job_view_token/{id} endpoint was missing from openapi.yaml (the source the frontend client is generated from). Adds the path + operationId getJobViewToken, and switches the run page's Share button from a raw fetch to JobService.getJobViewToken. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): carry view_token on share-link downloads Addresses Codex review: download actions bypass the request interceptor that adds X-View-Token (downloadViaClient uses raw fetch; cookie-mode downloads use plain hrefs), so a share-link viewer got 403 downloading logs/results/args. Append the view_token query param to the job download paths (result/logs/args/flow-all-logs) via a new appendViewToken() helper, covering both client-fetch and href modes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(jobs): enforce tag scope in require_job_read_access (view-token use side) Addresses Codex P1: the view_token use-side bypassed if_jobs:filter_tags on handlers that don't tag-filter their data query (result_by_id, get_flow_job_debug_info, get_otel_traces) — a tag-scoped token could use someone else's valid share token to read out-of-scope job data. Move the tag-scope check into require_job_read_access (runs before any created_by/view_token/RLS grant), so it applies uniformly to every gated handler; removes the now-redundant explicit check in get_job_view_token. Adds a use-side regression test (scoped token + valid out-of-scope view_token denied on otel/result_by_id; in-scope still allowed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): include workspace in share read link Addresses Codex P1: the copied share URL omitted the workspace. The token is signed with the run's workspace key and the logged layout only switches $workspaceStore when the URL carries workspace=, so a recipient whose persisted active workspace differs would open the link against the wrong workspace and the token would fail validation. Pin workspace= alongside view_token in the link. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(jobs): authorize get_result_maybe get_started branch for queued jobs Addresses Codex P1: get_completed_job_result_maybe only gated when a completed row existed; with ?get_started=true a non-reader reached the fallback branch and got started:true for a running private job. Now fetches created_by and authorizes (created_by/view_token/RLS, or anonymous for unauth) before disclosing running-state; a non-existent job still returns started:false (leaks nothing). Adds a regression test with a queued (no completed row) private job. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e8ad53dae9 |
fix: resolve username rename failing on apps with runnable deps (#9401)
The instance username-conflict resolver rewrote workspace_runnable_dependencies.app_path to the new user path before the app row itself was renamed, violating fk_workspace_runnable_dependencies_app_path. That FK is ON UPDATE CASCADE, so renaming the app already propagates the new path; the manual rewrite was redundant and mis-ordered. Any user owning an app under u/<username>/ with a tracked runnable dependency hit HTTP 500 and could not have their username conflict resolved. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
2c0c2c467f |
fix(apps): make public apps opt into cross-origin isolation via wm_coep (GIT-884) (#9374)
* fix(apps): make public apps opt into cross-origin isolation via wm_coep
Public app pages served at /public/* and custom paths /a/* were not
getting the COEP/COOP/CORP headers, so they were blocked when embedded
as an iframe inside a cross-origin-isolated page (e.g. another raw app,
which sets Cross-Origin-Embedder-Policy: require-corp). A nested
document loaded into a require-corp context must itself set COEP for
the iframe to load.
Rather than applying the isolation headers to all public pages (which
would also force COEP on classic apps and break subresources without
CORP, e.g. external image URLs or embeds), public apps now opt in via
a `wm_coep` query param on the embed URL:
<iframe src="https://<domain>/public/<ws>/<secret>?wm_coep=on">
The app publish drawer gains a URL/Embed toggle: "URL" shows the plain
shareable link (param-free), "Embed" shows a ready-to-copy iframe
snippet with wm_coep baked in, so the flag is discoverable exactly when
embedding and absent otherwise.
`wm_coep` is consumed internally and stripped from the app `query`
context so it doesn't collide with app-defined params. Only params we
own are stripped (an explicit set), not the whole `wm_` prefix.
Fixes GIT-884
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* nit
* nit
* fix(apps): only bake wm_coep into embed snippet for raw apps
AppEditorHeaderDeploy is shared by the classic (AppEditorHeader) and raw
(RawAppEditorHeader) deploy drawers. The embed snippet unconditionally
appended ?wm_coep=on, which for a classic/low-code app forces COEP
require-corp on the document and breaks no-CORP cross-origin subresources
(external <img> in AppImage/AppStatCard/AppNavbar, {@html} embeds in
AppHtml, CDN import() in AppCustomComponent) — the exact regression the
opt-in design avoids.
Add a `rawApp` prop (default false); the raw header passes rawApp. The
flag is appended only for raw apps; classic apps get a plain iframe
snippet, and the wm_coep helper text is shown only for raw apps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
b0c3b01d31 |
fix(cli): preserve user drafts on sync push and permissioned-as (#9381)
CLI deploys (sync push, set-permissioned-as) went through the same create/update endpoints as a UI "deploy from draft", which delete the draft at that path. That silently wiped teammates' in-progress drafts on every push. Add a transient skip_draft_deletion deploy flag (mirroring deployment_message) that the CLI sets; the backend then skips the DELETE FROM draft for scripts, flows, and apps. UI deploys are unchanged. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4b06881918 |
fix(ai): validate token_url for SSRF in OAuth credentials flow (#9385)
get_token_using_oauth resolved the AI OAuth resource's token_url and POSTed to it without any SSRF validation, while base_url is validated in get_base_url. A workspace member with resources:write could point token_url at an internal/metadata address (e.g. 169.254.169.254), turning the server into an authenticated blind SSRF probe. Validate the resolved token_url with validate_url_for_ssrf before the request, gated behind the same ALLOW_PRIVATE_AI_BASE_URLS opt-in as base_url so private AI deployments keep working consistently for both URL fields. ALLOW_PRIVATE_AI_BASE_URLS is now pub so windmill-api can reuse it instead of re-parsing the env var. |
||
|
|
96a8eb63d4 |
disable redirect following on AI proxy client to close SSRF (#9370)
* fix(ai): disable redirect following on AI proxy client to close SSRF The AI proxy validates the configured base_url against SSRF rules but the shared HTTP client followed up to 10 redirects without revalidating the hops, so a public base_url could 3xx the server into a private/internal address (e.g. the Docker socket or cloud metadata). Disable redirect following so the validated host is the only one the server connects to. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(ai): remove heavy redirect SSRF integration test Drop the integration-test-level regression for redirect following; it spins up a full API server + DB for a one-line client-config change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
bb90f4ce83 |
fix(api): authorize and harden log-file reading endpoints (#9368)
* fix(api): don't follow symlinks when reading service log files Defense in depth on top of the existing `..` path-traversal check in the get_log_file handler: reject the request if the final path component is a symlink, so a planted symlink in the logs directory cannot be used to read arbitrary files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(api): authorize and harden the jobs_u get_log_file endpoint The unauthenticated jobs_u get_log_file endpoint served any job's log file to anyone who knew the job UUID, with no authorization. Gate it the same way as get_job_logs: look up the job (the log directory name is the job id) filtered by workspace and the caller's scope tags, and only allow non-logged-in callers to read logs of jobs created by the anonymous user. Also add defense in depth: refuse to read through a symlink so a planted symlink in the logs directory cannot be used to exfiltrate arbitrary files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
2553fbfe31 | feat: add deepseek fim support (#9365) | ||
|
|
a7d85a39ff |
refactor: clean up ai provider proxy logic (#9360)
* refactor: clean up ai provider proxy logic * docs: remove completed ai refactor plan * fix: audit failed google global proxy calls |
||
|
|
59ab038d77 |
fix(monitor): cleanup stale server_heartbeat background_task_state rows (#9338)
`announce_server_started` writes a `server_heartbeat:{INSTANCE_NAME}` row
on each startup. INSTANCE_NAME is a fresh random string per process, so
the row is never updated again and a new row is inserted on every
restart, growing background_task_state unboundedly.
Add an hourly monitor task that deletes server_heartbeat:* rows older
than 7 days. Older rows cannot influence check_any_server_started (which
only considers heartbeats refreshed after the restart was initiated), so
they are safe to prune.
Fixes WIN-1990.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
4be930f585 |
refactor: unify AI provider credentials (#9317)
* refactor: use provider credentials for worker builders * refactor: resolve api proxy credentials directly * fix: lazy load frontend eval modes |
||
|
|
108a88a180 |
fix(jobs): authorization bypass in only_result job updates (WIN-1980) (#9301)
* fix(jobs): enforce anonymous-only guard on `only_result` job updates
The `jobs_u/getupdate/{id}` and `jobs_u/getupdate_sse/{id}` endpoints
accept `only_result=true`. In that branch, `get_job_update_data` queried
the result solely by (workspace_id, job_id) and skipped the
`created_by == "anonymous"` check that the non-only_result path and
adjacent unauthenticated endpoints apply. An unauthenticated requester
who learned a private job UUID could therefore retrieve that job's
output.
Hoist the guard to the top of `get_job_update_data` so both branches are
covered.
Fixes WIN-1980
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor: fold `created_by` check into existing only_result queries
Avoids the extra `SELECT created_by` round-trip per call by joining
`v2_job` once in the two queries that handled the unauth path and
checking inline. Behavior is identical to the prior commit; the SSE
polling loop now does one query per poll instead of two for
unauthenticated callers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor: cache anonymous_verified across SSE polls
Replace the LEFT JOIN approach with an upfront `SELECT created_by`
guarded by a new `&mut bool anonymous_verified` parameter that mirrors
`early_return_suppressed`. The SSE polling loop now performs the auth
check exactly once per stream rather than per poll, and the data SQL
reverts to its original form so authenticated callers pay no extra
cost. `created_by` cannot change after job creation, so caching the
verification across polls is safe.
Cost matrix:
- Authed (any path): 0 extra queries
- Unauthed one-shot: 1 extra query (unavoidable)
- Unauthed SSE: 1 extra query at stream start, 0 per poll
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor: scope anonymous check to only_result branch
The non-only_result branch already enforces the `created_by` check via
its main query, so a top-level hoisted check duplicated work for
unauthenticated default-path callers. Move the check inside the
`if only_result.unwrap_or(false)` block — exactly where the bypass
lives — and leave the non-only_result path untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
c96be0e903 |
refactor: move bedrock proxy handling to windmill-ai (#9309)
* refactor: move bedrock proxy handling to windmill-ai * docs: track ai refactor follow-ups |
||
|
|
0692b97c8a |
fix(ai): enforce RLS and scope check on user-supplied X-Resource-Path (#9276)
* fix(ai): enforce RLS and scope check on user-supplied X-Resource-Path
The AI proxy handler accepts an X-Resource-Path header to override the
configured workspace AI provider. When supplied, the handler loaded the
resource value from the resource table using the root DB pool with no
resources:read scope check, so any authenticated workspace user could
point X-Resource-Path at a restricted AI resource (e.g. one in a folder
they cannot read) and the proxy would use that resource's provider
credentials for the outbound AI request.
For user-supplied resource paths, now require resources:read:{path}
scope and fetch the resource through user_db.begin(&authed) so RLS
enforces the same folder/group boundary as the resource API. The RLS-
scoped $var: resolution stays in place as defense in depth. The
admin-configured workspace/instance ai_config path is unchanged.
Fixes WIN-1971
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(ai): regression test for X-Resource-Path RLS enforcement
Cover all four cases:
- non-admin pointing X-Resource-Path at a restricted resource is rejected
- non-admin pointing it at a resource they own still works
- admin can point it at any resource
- workspace-configured proxy flow (no X-Resource-Path) is unchanged
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
2db1c0a1fc | fix: early return should consider failure_module result (#9241) | ||
|
|
289549a048 |
refactor: move google ai proxy handling to windmill-ai (#9260)
* refactor: add ai proxy execution mode * refactor: move google ai proxy handling * refactor: share google ai request building |
||
|
|
0f7dd86e5c |
feat: persistent in-editor drafts via UserDraft (#9121)
* refactor(frontend): remove localStorage-backed autosave drafts
Strip the per-editor localStorage autosave for flows, apps and raw apps,
along with the associated restore toasts and diff actions, so we can
replace them with a unified UserDraft service in a follow-up. The
backend DraftService (DB-backed drafts) is untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(frontend): add UserDraft service for per-workspace local drafts
Introduces UserDraft, a key-value store keyed by
`{workspace}/{itemKind}/{path}` and backed by localStorage. Supports
save/get/remove plus a reactive use() handle so multiple component
instances observing the same draft stay in sync via a shared $state
loaded through useLocalStorageValue. Designed to host drafts for
scripts, flows, apps, raw apps, resources, variables, and all trigger
kinds.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* tests
* nit schedule_ prefix
* feat(frontend): persist deep mutations in useLocalStorageValue
Track the serialized value alongside the $state and add an $effect that
deep-reads it (via readFieldsRecursively). When a deep mutation produces
a serialization that differs from the last persisted blob, write it to
localStorage. The setter keeps writing synchronously so callers reading
localStorage right after assignment still see the new value; the effect
no-ops on those because lastSerialized was already updated by the setter.
Undefined values are persisted as a removal.
UserDraft no longer needs its own removeItem workarounds for undefined
values — useLocalStorageValue handles that uniformly now.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(frontend): add defaultValue + empty-path handling to UserDraft
UserDraft.use() accepts an opts.defaultValue used when no localStorage
entry exists yet. It is not persisted on first read — only an actual
mutation writes through.
Empty paths (new items) bypass localStorage entirely. The entry still
lives in the in-memory Map so multiple components on the same /add page
share state, but save/get/remove/use never read or write localStorage
with an empty path. Once the item is saved and the route navigates to
its new URL, a fresh use() on the non-empty path takes over.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(frontend): wire script editor to UserDraft
The script editor's top-level state now lives in UserDraft.use(), keyed
on the route's path (page.params.path on /scripts/edit, '' on /scripts/add).
Deep edits inside ScriptBuilder persist automatically; deploy and draft
restore now call UserDraft.remove to clear the local autosave alongside
the backend draft.
Replaces the URL-hash autosave that ScriptBuilder used to write via
replaceStateFn — that prop is now gone, the encodeScriptState debounce
is gone, and Triggers no longer takes a saveSessionDraft callback.
Viewing a specific historical hash (?hash=...) is kept draft-free by
passing '' as the path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(frontend): wire flow editor to UserDraft
flows/add and flows/edit drive the flow value through a StateStore
adapter backed by UserDraft.use, so every edit auto-persists at
userdraft/w/{ws}/flow/{path} without touching FlowBuilder's internal
.val convention. On returning visits the local autosave wins and a
toast offers a diff against the latest backend draft/deployed version;
on a fresh visit the backend value is written into the handle. Deploy,
save-as-draft rename, restore-draft and restore-deployed each call
UserDraft.remove on the route path so the local autosave doesn't
outlive the action.
Adds UserDraft.has() for "is there already a local draft?" detection
in the load path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(frontend): wire app editor to UserDraft
AppEditor registers a UserDraft.use<App> handle for its current path
(empty path for /apps/add stays in-memory) and a single $effect
deep-tracks the internal stateApp and forwards every mutation to the
handle. useLocalStorageValue's lastSerialized check then dedupes the
actual localStorage writes per tick, so even fast drag/resize loops
only persist when the JSON output really changes.
/apps/edit overlays a local autosave from UserDraft.get on top of the
backend value when one exists, with the existing "Discard / Show diff"
toast wired to UserDraft.remove. Deploy, save-as-draft, restore-draft
and restore-deployed all call UserDraft.remove on the relevant path,
including the JSON editor save paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(frontend): wire raw app editor to UserDraft
/apps_raw/edit owns the canonical raw-app state (files, runnables,
data, summary) in four $state vars; a single $effect deep-tracks them
and forwards the bundle to a UserDraft.use<RawAppDraft> handle so each
mutation tick persists at userdraft/w/{ws}/raw_app/{path} (deduped by
useLocalStorageValue's serialized check). On load the route overlays
the local autosave on top of backend.draft/deployed and offers a
"Discard / Show diff" toast when they diverge; matching local entries
are silently dropped. Deploy, save-as-draft rename, restore-draft and
restore-deployed each call UserDraft.remove on the route path.
/apps_raw/add keeps the same shape (UserDraft.use with empty path)
so the draft is in-memory only and we drop it explicitly when the
initial save creates the real path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(frontend): wire resource editor to UserDraft
ResourceEditor registers a UserDraft.use<ResourceState> handle keyed
on the initialPath (empty for new resources, in-memory only). A
$effect deep-tracks the current workspace's edit state and forwards
mutations to the handle; on bootstrap and lazy backend-fetch the
local autosave wins over the backend value when they diverge. After
a successful save() we call UserDraft.remove so the local autosave
doesn't outlive the deploy. Cross-workspace deploys always start from
the live backend value rather than the local draft.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(frontend): wire variable editor to UserDraft
VariableEditor persists the current workspace's edit state via
UserDraft.save on every mutation, keyed on editPath ('' for new
variables → in-memory only). Backend fetches now overlay a matching
local autosave when one exists, and initNew() rehydrates from the
in-memory empty-path entry so opening a fresh "Add variable" drawer
keeps any unsaved work from the previous open. After a successful
save we drop the corresponding entry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* editor external changes sync
* fix(frontend): don't UserDraft.remove flows while route is still mounted
The /flows/add and /flows/edit routes drive FlowBuilder from a flowStore
whose getter reads flowHandle.draft directly. Calling UserDraft.remove
synchronously before goto() therefore wiped the in-memory entry, made
flowStore.val collapse to emptyFlow(), and tripped
UnsavedConfirmationModal against the just-saved value — even though the
deploy/save-draft itself succeeded.
Drop those explicit removes in onSaveInitial, /add onDeploy, and
/edit onDeploy. The empty-path entry self-cleans on unmount via
onDestroy ref counting; for the non-empty edit path the next visit's
load-time diff will silently overwrite localStorage when the local
autosave matches the deployed value. Restore-draft/restore-deployed
keep their explicit remove because they navigate to the same route
(no modal) and loadFlow immediately rehydrates the handle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Revert "fix(frontend): don't UserDraft.remove flows while route is still mounted"
This reverts commit
|
||
|
|
22ec4da5f0 |
tighten security from vuln report (#9264)
* fix: harden app preview S3, WM_ env reservation, set_progress scoping * fixup: minimize #1 fix to single SQL-level filter * fixup: apply WM_* filter to HTTP agent-worker branch + normalize app S3 scope path |
||
|
|
bb78b1c06d |
fix(s3): sandbox stored XSS via download response headers (#9263)
* [ee] fix(s3): sandbox stored XSS via download response headers Reported chain: a workspace user uploads xss.html via apps_u/upload_s3_file with content_type=text/html&content_disposition=inline; when an admin clicks the resulting download URL the browser renders the attacker page in Windmill's origin and can escalate via the SameSite=Lax session cookie. Fix on the download side only — leaves upload semantics unchanged so existing integrations are not affected: - download_s3_file_internal (used by apps_u/download_s3_file and job_helpers/download_s3_file) emits X-Content-Type-Options: nosniff and Content-Security-Policy: sandbox on every response (EE). - The HTTP static-asset trigger emits the same headers on single-file responses. Static-website responses keep their existing semantics (CSP sandbox would break a legitimate static site); restricting write access to those buckets remains the documented mitigation. Sandbox loads any HTML/SVG into an opaque origin so the page cannot reach the viewer's cookie or /api/*. Images, PDFs, and fetch-driven previews are unaffected (browsers ignore CSP for <img>/<embed> and for fetch responses). Companion: windmill-ee-private fix/s3-content-type-xss. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: update ee-repo-ref to daffe7bb81cfcaca666c61de1ee838a44d60ebc2 This commit updates the EE repository reference after PR #585 was merged in windmill-ee-private. Previous ee-repo-ref: e889b86ee1c68c2f7cf9b07ec4b8ba6e6b66a169 New ee-repo-ref: daffe7bb81cfcaca666c61de1ee838a44d60ebc2 Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com> |
||
|
|
9c28bbfd69 |
feat(frontend): new path component (#9017)
* stash
* ui nits
* Fix contenteditable feedback look (duplicate typing)
* fix right icon wrong position with placeholder
* user editor in Path editor takes correct width
* nits
* nit
* chore: remove assets-operator changes (moved to separate PR)
These files were mistakenly included in this PR and belong in a dedicated PR
("Allow assets page to operators").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: remove sidebar assets-operator change (moved to separate PR)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix disabled
* border nit
* Fix disabled styling
* Apply suggestion from @cubic-dev-ai[bot]
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
* nit
* Update frontend/src/lib/components/text_input/TextInput.svelte
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
* Fix disabled tabindex and aria-disabled on contenteditable Select
The useContentEditable branch had an unconditional tabindex="0", keeping
a disabled Select in the tab order, and was missing aria-disabled.
Mirror the TextInput div branch.
Co-authored-by: Diego Imbert <diegoimbert@users.noreply.github.com>
* fix: drop obsolete hideFullPath prop from EditorHeader Path usage
* invalidate autocomplete paths on deploy
* nit pixel
* use Badge in auto complete
* nit prevent default
* fix(autocomplete): don't let stale fetch clobber forced refresh
A non-forced fetchWorkspacePaths() that started before invalidateWorkspacePaths()
could still resolve afterward, overwrite the cache, and clear forceNextFetch —
making the post-deploy refresh a no-op. Only write back from the promise that
is still the current pending one, and only clear the force flag when the
completing fetch was itself forced.
* refactor(path): drop unreachable 'group' branch in owner-kind setter
The Select only offers user/folder, so the 'group' branch was dead. Leave a
short note pointing at validateName which still accepts 'group' for
forward-compat.
* fix(path): respect disableEditing on owner-kind selector
Other path-editor controls disable on (disabled || disableEditing); the
owner-kind Select only checked `disabled`, so read-only users (trigger
editors with !can_write) could still toggle User/Folder and mutate the
bound path. Reuse the existing nameDisabled flag.
* Revert "fix(autocomplete): don't let stale fetch clobber forced refresh"
This reverts commit
|
||
|
|
2a780ad87a |
feat: resolve relative imports from local content in script/flow preview (#9233)
* feat: thread temp_script_refs into preview jobs Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: resolve python preview relative imports from temp script refs Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: use local relative imports in wmill script preview Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: use local relative imports in wmill flow preview Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: add temp_script_refs to Preview and FlowPreview openapi schemas Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: pass temp_script_refs to bun lockfile gen for no-lock preview Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: route script preview through shared buildPreviewTempScriptRefs Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: resolve local relative imports in wmill app dev inline scripts Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: address cubic review — bundle cache key, preview-mode gate, error masking Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * perf: skip dep-tree build when previewed script has no relative imports; narrow old-backend classifier Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: address review issues (preview-only gate, bundle preview, app dev cwd) Three P1s flagged in repeated codex/pi reviews on PR #9233: - Gate _TEMP_SCRIPT_REFS extraction on JobKind::Preview (bun + python executors) and propagation in worker_flow on JobKind::FlowPreview. job.args includes caller-controlled request args, so honoring this key on deployed runs would let a caller swap import resolution to local content uploaded via /raw_temp. - run_bundle_preview_script now injects temp_script_refs into PushArgs.extra, mirroring run_preview_script — closes the silent data drop for the bundle preview path. - wmill app dev chdirs to the wmill.yaml root before buildPreviewTempScriptRefs and restores after, so the `cd <app>__raw_app && wmill app dev` invocation (cwd is the raw_app folder, no app_folder arg) still walks sibling workspace scripts like f/lib.ts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(ee): bump ee-repo-ref to 5b347d6 (handle_python_deps arity fix) Picks up the EE arity fix so cargo_test + check_ee_full compile cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(ee): bump ee-repo-ref to 52e273d (agent-workers bundle path arity fix) Picks up windmill-ee-private 52e273d which adds the missing &None arg to compute_bundle_local_and_remote_path in windmill-api-agent-workers/src/ee.rs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(ee): bump ee-repo-ref to 2d6ffd3 (EE main merged in) Previous bump pinned an older EE commit, missing the audit-log object-store export module (EE PR #579, commit ec3cd35) and other EE main updates. The CE backend's `crate::ee_oss::anchor_audit_logs_s3_checkpoint_env_var` and `export_audit_logs_to_object_store` references need the new EE definitions. Merged origin/main into the EE branch and pinned the merge commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: update ee-repo-ref to b0c87b1272c25dca4aa9148c87fb024a9d9ef322 This commit updates the EE repository reference after PR #583 was merged in windmill-ee-private. Previous ee-repo-ref: 2d6ffd32c99bd93e79cf78675cb89499a81b17e1 New ee-repo-ref: b0c87b1272c25dca4aa9148c87fb024a9d9ef322 Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com> |
||
|
|
4b1bea8aed |
fix: enforce auth guards on app component preview execution (#9235)
* fix: enforce auth guards on app component preview execution Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: guard previewed runnable path and worker tag in app preview Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: validate app_script id ownership and keep root push isolation Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: scope app preview guards to operator check + referenced runnables Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: require jobs:run scope and tag check on app preview (apps:run escalation) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
55dcee2424 | refactor: move anthropic proxy building (#9238) | ||
|
|
ad5ec293b5 |
fix: reject path traversal in MCP endpoint path parameters (#9211)
* fix: reject path traversal in MCP endpoint path parameters Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: narrow MCP path-param validator to structural escapes only Codex review: rejecting whitespace/`:`/`@` regressed legitimate Windmill paths (app paths with spaces, email-style usernames like u/admin@windmill.dev/...). These are ordinary path-segment data in an absolute URL and cannot redirect the request. Reject only structural escapes: control chars, `\`, `%`, `?`, `#`, and `.`/`..`/empty segments. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |