Skip to content

fix: defer flow transform variable() fetches to child step jobs#10005

Draft
hugocasa wants to merge 3 commits into
mainfrom
hugo/win-2145-propagate-secret-masks-from-flow-input-transforms-to-child
Draft

fix: defer flow transform variable() fetches to child step jobs#10005
hugocasa wants to merge 3 commits into
mainfrom
hugo/win-2145-propagate-secret-masks-from-flow-input-transforms-to-child

Conversation

@hugocasa

@hugocasa hugocasa commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes WIN-2145.

When variable() is called inside a flow step's input transform expression (e.g. mycli --password=${variable("f/secrets/some_password")}), the secret was registered for masking only against jobs currently running on that worker — i.e. the parent flow job. The child step job is created afterwards by push(), so when it started (on any worker) it got a fresh empty mask set and the secret appeared unmasked in the child job's logs.

This PR propagates the flow job's accumulated secret masks to each pushed child job via push_args.extra under a _secret_masks key, following the _TEMP_SCRIPT_REFS / WM_TRACEPARENT propagation pattern. The child's worker decrypts and registers the masks right after register_running_job, before the job produces any logs.

Two deliberate deviations from the issue's proposed fix:

  • The masks are encrypted, not plaintext. push_args.extra is persisted to the child's v2_job.args, so a plaintext JSON array would have stored raw secret values in the DB and shown them in the run UI — a worse leak than the logs. The blob is encrypted with the workspace key + root job id, the exact scheme already used for $encrypted: args, and the child derives the same key via get_root_job_id.
  • The flow job is kept tracked during push_next_flow_job. For steps after the first, push_next_flow_job runs on the step-completion path where the flow job is not in the running-jobs set (it was unregistered right after pushing step 1), so variable() fetches during transforms would register nothing and there would be nothing to propagate. A track_job_for_masks RAII guard registers the flow job for the duration of the invocation (no-op when already tracked) and seeds it with masks inherited from its own args, so subflows keep propagating transitively.

Changes

  • windmill-common/src/sensitive_log_masks.rs: SECRET_MASKS_ARG constant, get_secrets_for_job (sorted snapshot), and track_job_for_masks + MaskTrackingGuard (RAII, only unregisters what it registered).
  • windmill-worker/src/worker_flow.rs: guard + args-seeding at the top of push_next_flow_job; per-iteration snapshot in the push loop (parallel for-loop iterations can register more secrets via their own simple input transforms) with the encrypted blob cached and only rebuilt when the set changes; injected into push_args.extra.
  • windmill-worker/src/worker.rs: after register_running_job, decrypt and register propagated masks (best-effort; skipped on agent workers since an HTTP connection cannot fetch the workspace key — same pre-existing limitation as $encrypted: args).
  • windmill-worker/src/common.rs: register_secret_masks_from_args helper; strip _secret_masks from args.json alongside _MODULES / _TEMP_SCRIPT_REFS.
  • frontend/src/lib/components/JobArgs.svelte: hide _secret_masks from the args table and show a small "secret masks" badge instead (same treatment as _TEMP_SCRIPT_REFS).
  • New integration test test_flow_transform_secret_masking covering both push paths: step "a" (flow job pushed while running under handle_flow) and step "b" (pushed from the step-completion path), asserting masked child logs, no plaintext in logs, and no plaintext in the stored _secret_masks arg. Added a quickjs feature to the test crates so the JS-evaluator-gated test can run without the heavy deno_core build; CI enables both.
  • .sqlx: two new offline cache entries for the test queries (via ./update_sqlx.sh).

Test plan

  • cargo test -p windmill-api-integration-tests --features quickjs --test sensitive_log_masking — new flow test + full pre-existing masking suite pass
  • cargo check -p windmill-common -p windmill-worker
  • npm run check — 0 errors
  • Live E2E on a dev instance: ran a two-step preview flow whose transforms fetch a secret variable; both child jobs' logs show e2e*****9w1, the plaintext appears nowhere in job_logs, and the stored _secret_masks arg is an encrypted blob
  • Verified the run detail page in the browser: masked logs, _secret_masks hidden from the inputs table, "secret masks" badge shown

Screenshots

Child step job of the E2E flow (the visible e2e_secret_password_… value in the cmd arg is a throwaway test string; transform results landing in child args is pre-existing behavior — this PR is about logs):

inputs-badge

masked-logs

Known limitations

  • Masks accumulate per push_next_flow_job invocation (in-memory, per-process): a secret fetched by step 1's transform is masked in step 1's job, but not in a later step's job unless that step's transform fetches it again. Cross-step accumulation would require persisting masks in the flow status; out of scope here.
  • Agent workers (HTTP connection) skip registration of propagated masks — they cannot decrypt without DB access, matching the existing $encrypted: args limitation.

🤖 Generated with Claude Code


Summary by cubic

Propagates secret masks from flow input transforms to child step jobs and defers eligible variable() fetches to child workers, so secrets are masked in child logs and never stored in child args. Addresses WIN-2145.

  • New Features

    • Defer variable() in flow input transforms to $var: or {"$interpolate": [...]} args that the child worker resolves at start, registering masks and keeping raw secrets out of stored child args.
    • Enabled only for leaf script steps and simple loop modules, skipped for cache_ttl steps, and gated by a min-version check for $interpolate support; non-deferable expressions fall back to eager evaluation.
  • Bug Fixes

    • Propagate the flow job’s accumulated masks to each child via push_args.extra._secret_masks, encrypted with the workspace key + root job id, and register them right after register_running_job and before any logs; agent workers skip if they can’t decrypt.
    • Keep the flow job tracked during push_next_flow_job on the completion path and seed from its own args so transforms can register masks; supports subflows.
    • Strip _secret_masks from args.json and hide it in the UI; add an integration test covering $interpolate, $var, and eager-fallback cases; enable quickjs for tests; update .sqlx cache.

Written for commit 9a74690. Summary will update on new commits.

Review in cubic

…jobs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 8, 2026

Copy link
Copy Markdown

Deploying windmill with  Cloudflare Pages  Cloudflare Pages

Latest commit: 9a74690
Status: ✅  Deploy successful!
Preview URL: https://706f0460.windmill.pages.dev
Branch Preview URL: https://hugo-win-2145-propagate-secr.windmill.pages.dev

View logs

hugocasa and others added 2 commits July 8, 2026 14:26
…interpolate args

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… eager

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hugocasa hugocasa changed the title fix: propagate secret masks from flow input transforms to child step jobs fix: defer flow transform variable() fetches to child step jobs Jul 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant