From 27cb8e7ae69c29a39cf4da74a989b4d7823ba653 Mon Sep 17 00:00:00 2001 From: Guilhem Lemouel Date: Tue, 13 Oct 2026 02:10:10 -0700 Subject: [PATCH] fix: refuse a run in the form, and show an adopted job's own arguments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two findings on the deployed page carrying a chat call. `RunForm.run()` mints a variable per `password` argument before it calls `runAction`, so the plan-mode gate in the detail pages ran after the write it was there to prevent — and the keybinding reached it without the button. The refusal moves into the form, as `runRefusal`: set, it takes no writes at all, its submit buttons are disabled, and `run()` says so and returns before the secret processing. The page also stops offering the JSON editor under plan mode, instead of leaving one mounted that silently discards what is typed. An adopted job's arguments are the page's, and the card had no way to show them: it kept rendering the proposal, so a reader who edited a field saw the wrong values and the model was told nothing. `adopt` now hands them over with the job id, and they go through the same redaction and size guard as a run this tool starts. Co-Authored-By: Claude Opus 5 (1M context) --- frontend/src/lib/components/RunForm.svelte | 28 ++++--- .../copilot/chat/AIChatManager.svelte.ts | 12 ++- .../copilot/chat/global/core.test.ts | 78 ++++++++++++++++--- .../components/copilot/chat/global/core.ts | 29 +++---- .../src/lib/components/copilot/chat/shared.ts | 4 +- .../lib/components/details/FlowDetail.svelte | 14 ++-- .../components/details/ScriptDetail.svelte | 14 ++-- .../src/lib/components/details/pendingRun.ts | 10 ++- .../sessions/sessionRuntime.svelte.ts | 2 +- 9 files changed, 132 insertions(+), 59 deletions(-) diff --git a/frontend/src/lib/components/RunForm.svelte b/frontend/src/lib/components/RunForm.svelte index a57d9a902c..cede84f394 100644 --- a/frontend/src/lib/components/RunForm.svelte +++ b/frontend/src/lib/components/RunForm.svelte @@ -75,6 +75,13 @@ } export async function run(overrideScheduledForStr?: string | undefined | null) { + // Ahead of everything, including the flush below: the secret processing further down + // creates a variable per `password` argument, so a refusal that waits for `runAction` + // has already written. The keybinding reaches here without the button. + if (runRefusal) { + sendUserToast(runRefusal, true) + return + } // An editor whose text does not parse never wrote it to `args`, so running now would send // the last value that did parse. Flush first: a keystroke still inside the editor debounce // has not been parsed yet, and per-field editors parse it in an effect, hence the tick. @@ -127,11 +134,12 @@ extra_perms?: Record } | undefined - /** Take no writes from the reader: no edits, no dynamic-select helper, no JSON. All - * three write before Run is pressed — a `password` mints as it is typed, the helper - * runs its entrypoint on mount — so gating Run cannot stop them. For a form standing - * in for a chat card under plan mode, which promised none of it. */ - argsReadonly?: boolean + /** Set to refuse every write this form can make, and to say so when a run is + * attempted. No edits, no dynamic-select helper, no JSON, and no run — `run()` mints + * the `password` arguments it is about to send, so refusing in `runAction` is already + * too late. For a form standing in for a chat card under plan mode, which promised + * none of it. */ + runRefusal?: string runAction: ( scheduledForStr: string | undefined, args: Record, @@ -165,7 +173,7 @@ let { runnable, runAction, - argsReadonly = false, + runRefusal = undefined, buttonText = 'Run', schedulable = true, detailed = true, @@ -184,6 +192,8 @@ actions = undefined }: Props = $props() + const argsReadonly = $derived(!!runRefusal) + let showPsCommonParams = $derived( runnable?.language === 'powershell' && runnable?.schema?.['x-windmill-ps-cmd-binding'] === true ) @@ -393,7 +403,7 @@ variant="accent" unifiedSize="md" btnClasses="!inline-flex" - disabled={!isValid && !jsonView} + disabled={(!isValid && !jsonView) || argsReadonly} on:click={() => run()} shortCut={{ Icon: CornerDownLeft, hide: !viewKeybinding }} > @@ -442,7 +452,7 @@ variant="accent" unifiedSize="md" btnClasses="!inline-flex" - disabled={!isValid && !jsonView} + disabled={(!isValid && !jsonView) || argsReadonly} on:click={() => run(null)} shortCut={{ Icon: CornerDownLeft, hide: !viewKeybinding }} > @@ -455,7 +465,7 @@