mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-12 16:02:56 +00:00
fix(sessions): mint a chat flow's secret arguments before its run
A chat flow's inputs reach the job straight from the composer, with no RunForm between them to turn a password-marked value into a reference as it is typed. The handover put the proposed arguments there directly, so a literal secret was stored in the job's arguments, readable by anyone who can read the run — the form path had always minted it first. runFlowForChat now does, which covers the reader's own typed inputs as well as a proposal's. Also from round 2: - a viewer pinned to an older version kept that pin when the handover merely focused it, so Run executed a version the arguments were not prepared against - the proposal now decides every input the schema declares, including the ones it omits: merging let a value saved on an earlier visit ride along in a run the chat announced without it Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3d80243963
commit
465c15aa6b
@@ -27,6 +27,8 @@
|
||||
import { Badge as HeaderBadge, Alert } from '$lib/components/common'
|
||||
import MoveDrawer from '$lib/components/MoveDrawer.svelte'
|
||||
import RunForm from '$lib/components/RunForm.svelte'
|
||||
import { processSecretArgs } from '$lib/components/secretArgUtils'
|
||||
import type { Schema } from '$lib/common'
|
||||
import ShareModal from '$lib/components/ShareModal.svelte'
|
||||
import { enterpriseLicense, userStore, userWorkspaces, workspaceStore } from '$lib/stores'
|
||||
import { useOperatorBuilderFlows } from '$lib/operatorWriteRights'
|
||||
@@ -323,11 +325,21 @@
|
||||
conversationId: string,
|
||||
additionalInputs?: Record<string, any>
|
||||
): Promise<string> {
|
||||
// A chat flow's inputs reach the job straight from the composer, with no RunForm in
|
||||
// between to mint a secret as it is typed — so this is the only place a value the
|
||||
// schema marks `password` can become a reference. Without it the literal is stored in
|
||||
// the job's arguments, where anyone who can read the run can read it. Idempotent, so a
|
||||
// reference that was already minted costs a walk and no round trip.
|
||||
const requestBody = await processSecretArgs(
|
||||
{ user_message: userMessage, ...(additionalInputs ?? {}) },
|
||||
flow?.schema as Schema | undefined,
|
||||
workspace
|
||||
)
|
||||
const run = await JobService.runFlowByPath({
|
||||
workspace: workspace!,
|
||||
path,
|
||||
memoryId: conversationId,
|
||||
requestBody: { user_message: userMessage, ...(additionalInputs ?? {}) },
|
||||
requestBody,
|
||||
skipPreprocessor: true
|
||||
})
|
||||
return run
|
||||
|
||||
@@ -191,12 +191,24 @@
|
||||
return panel?.host.offerMessage(text) ?? false
|
||||
}
|
||||
|
||||
/** Set the flow's non-message inputs for the run a chat tool proposed. Separate from the
|
||||
/** Set the flow's non-message inputs to the run a chat tool proposed. Separate from the
|
||||
* message because the two can land apart: a composer the reader is typing in declines the
|
||||
* message, and the inputs still belong to the proposal they can send afterwards. */
|
||||
* message, and the inputs still belong to the proposal they can send afterwards.
|
||||
*
|
||||
* Every input the schema declares is taken from the proposal, including the ones it leaves
|
||||
* out — those go back to their default. Merging instead would let a value the reader saved
|
||||
* on an earlier visit ride along in a run the agent announced without it, which is a
|
||||
* different run from the one the chat described. Inputs the composer owns rather than the
|
||||
* schema (its model wiring, its attachments) are not the proposal's to touch. */
|
||||
export function applyInputs(inputs: Record<string, any>): void {
|
||||
if (Object.keys(inputs).length === 0) return
|
||||
inputValues = { ...inputValues, ...inputs }
|
||||
const declared = Object.keys(additionalInputsSchema?.properties ?? {})
|
||||
if (declared.length === 0) return
|
||||
const next = { ...inputValues }
|
||||
for (const key of declared) {
|
||||
if (key in inputs) next[key] = inputs[key]
|
||||
else delete next[key]
|
||||
}
|
||||
inputValues = next
|
||||
}
|
||||
|
||||
/** Whether the conversation on screen has a composer to offer a message to. A caller that
|
||||
|
||||
@@ -624,7 +624,21 @@ function createRuntime(session: Session): SessionRuntime {
|
||||
const target = previewTargetForSessionTarget(kind, path)
|
||||
if (target?.type !== 'item') return false
|
||||
seededRunArgs.set(seededKey(kind, path), { args, seq: ++seededRunSeq })
|
||||
previewTabs.open({ ...target, item: { ...target.item, summary }, mode: 'view' })
|
||||
const viewTarget = { ...target, item: { ...target.item, summary }, mode: 'view' as const }
|
||||
// A viewer already open on an older version of this item keeps that pin when it is
|
||||
// merely focused, and Run there executes that version — not the deployment these
|
||||
// arguments were prepared against. Re-point it instead, which drops the pin.
|
||||
const pinned = previewTabs.tabs.find((t) => {
|
||||
const slot = resolvePreviewTab(t.url)
|
||||
return (
|
||||
slot.kind === 'viewer' &&
|
||||
slot.viewerKind === kind &&
|
||||
slot.path === path &&
|
||||
t.url.includes('version=')
|
||||
)
|
||||
})
|
||||
if (pinned) previewTabs.navigate(viewTarget, pinned.id)
|
||||
else previewTabs.open(viewTarget)
|
||||
return true
|
||||
}
|
||||
manager.closeRunForm = (toolCallId) => previewTabs.closeRunForm(toolCallId)
|
||||
|
||||
Reference in New Issue
Block a user