mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-17 16:03:06 +00:00
* fix: disable a dynamic input when its schema field is disabled Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh * refactor: extract the run form's argument hygiene into job_args Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh * feat: give Tabs an opt-in sliding selection indicator Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh * refactor: share the chat's scroll-fade measurement Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh * feat: give the chat a run-form contract and incremental job output Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh * feat: run and test a script from the chat through an argument form Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh * feat: carry a chat run's card and job across saves and reloads Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh * feat: render a chat run as a tool call row with its form, logs and result Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh * feat: open a pending run form in the sessions preview pane Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh * test: benchmark running a deployed script from the chat Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh * feat: offer a test run's dynamic options from the draft it previews Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: say that a test run's dynselect helper executes on form display * fix: send a schema default the model omitted when yolo skips the form * fix: infer a test run's schema when the stored one declares no properties * docs: tighten the note on the form's mount-time helper job * fix: apply a nested schema default the bypass posture counts as answered * fix: apply a declared default to a null value and an optional nested field * fix: check required fields inside a supplied optional object before bypassing * fix: read required args as own properties before bypassing the form * fix: stop the turn from the run form's action row in the preview panel * refactor: drop the run-form prediction and share its secret minting * refactor: prefill a proposed secret instead of emptying the field * docs: correct the comments the run-form prediction left behind * fix: keep a proposed secret out of the chat's stored messages * docs: say what a literal secret argument now does * test: restore the copilotInfo export the aiStore mock omits * docs: cut the run form's helper-script note to its constraints * refactor: settle a run form from one entry and fetch a job's logs once * fix: separate colliding secret paths, gate plan mode, keep polled logs * fix: mint before the form opens, skip empty fields, show what ran * revert: mint a run form's secrets at submit, not before it opens * fix: settle a cancelled run card on the form's arguments, not the proposal * fix: settle a stopped run form like a cancelled one, and keep an empty secret empty * fix: snapshot a run's arguments before minting its secrets --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
120 lines
4.6 KiB
TypeScript
120 lines
4.6 KiB
TypeScript
import type { Schema } from '$lib/common'
|
|
import { VariableService } from '$lib/gen'
|
|
import { get } from 'svelte/store'
|
|
import { userStore, workspaceStore } from '$lib/stores'
|
|
import { generateRandomString } from '$lib/utils'
|
|
import { stateSnapshot } from '$lib/stateSnapshot.svelte'
|
|
import { isSecretProp, mapArgLeaves } from './job_args'
|
|
|
|
/** Where a caller's own ephemeral secrets live, so a field can tell one it minted from a
|
|
* workspace variable someone linked by hand. */
|
|
export function ephemeralSecretPrefix(username: string): string {
|
|
return `u/${username}/secret_arg/`
|
|
}
|
|
|
|
/**
|
|
* Mint the ephemeral secret variable a sensitive argument is submitted as, and return its path.
|
|
* It expires on its own, so a run that is abandoned leaves no permanent secret behind.
|
|
*/
|
|
export async function mintEphemeralSecret(
|
|
workspace: string,
|
|
username: string,
|
|
value: string
|
|
): Promise<string> {
|
|
const path = ephemeralSecretPrefix(username) + generateRandomString(12)
|
|
await VariableService.createVariable({
|
|
workspace,
|
|
requestBody: {
|
|
value,
|
|
is_secret: true,
|
|
path,
|
|
description: 'Ephemeral secret variable',
|
|
expires_at: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7).toISOString()
|
|
}
|
|
})
|
|
return path
|
|
}
|
|
|
|
/** `$var:` hands the job the variable's text and `$jsonvar:` hands it the parsed value, so a
|
|
* field that cannot hold a string needs the second one whichever the caller named. */
|
|
function referencePrefix(prop: any, value: unknown): '$var:' | '$jsonvar:' {
|
|
return typeof value === 'string' && prop?.type !== 'object' && prop?.type !== 'array'
|
|
? '$var:'
|
|
: '$jsonvar:'
|
|
}
|
|
|
|
/**
|
|
* Turn every sensitive argument into a reference before the job is submitted: a plaintext value
|
|
* is minted into an ephemeral secret variable, so what is stored on the job — and readable by
|
|
* anyone who can see its run — names a secret instead of holding one.
|
|
*
|
|
* The single place that decides how a secret reaches a job: {@link PasswordArgInput} mints
|
|
* through it while the user types, and a run the autonomy posture starts without a form calls it
|
|
* in the widget's stead.
|
|
*/
|
|
export async function processSecretArgs(
|
|
args: Record<string, any>,
|
|
schema: Schema | undefined,
|
|
// Workspace the ephemeral secret variable is created in — must match the
|
|
// workspace the preview job runs in, else $jsonvar: resolves to a missing var.
|
|
forceWorkspace?: string
|
|
): Promise<Record<string, any>> {
|
|
if (!schema?.properties) return args
|
|
|
|
const workspace = forceWorkspace ?? get(workspaceStore)
|
|
const user = get(userStore)
|
|
if (!workspace || !user) return args
|
|
|
|
const username = (user.username ?? user.email)?.split('@')[0]
|
|
if (!username) return args
|
|
|
|
// Detached from the caller: every one binds a form that stays editable across the awaits
|
|
// below, and the two walks address a leaf by its path — an array reordered between them
|
|
// would hand a row the reference minted for another row's secret.
|
|
args = stateSnapshot(args)
|
|
|
|
// A value that already names a variable is one; anything else is the secret itself. An empty
|
|
// field holds nothing to mint, and ArgInput synthesises '' for every untouched string.
|
|
const holdsSecret = (value: unknown) =>
|
|
value != null &&
|
|
value !== '' &&
|
|
!(
|
|
typeof value === 'string' &&
|
|
(value.startsWith('$var:') || value.startsWith('$jsonvar:') || value.startsWith('$res:'))
|
|
)
|
|
|
|
// Collected first and substituted after, because the walk is synchronous and minting is not.
|
|
// Keyed by the whole path the walk reports, which is what tells two same-named leaves apart.
|
|
const pending: { key: string; prop: any; value: unknown }[] = []
|
|
mapArgLeaves(args, schema as any, isSecretProp, (value, prop, path) => {
|
|
if (holdsSecret(value)) pending.push({ key: JSON.stringify(path), prop, value })
|
|
return value
|
|
})
|
|
|
|
const minted = new Map<string, string>()
|
|
for (const { key, prop, value } of pending) {
|
|
const reference = referencePrefix(prop, value)
|
|
const variable = await mintEphemeralSecret(
|
|
workspace,
|
|
username,
|
|
reference === '$var:' ? String(value) : JSON.stringify(value)
|
|
)
|
|
minted.set(key, reference + variable)
|
|
}
|
|
|
|
return mapArgLeaves(args, schema as any, isSecretProp, (value, prop, path) => {
|
|
const replacement = minted.get(JSON.stringify(path))
|
|
if (replacement !== undefined) return replacement
|
|
// A plain variable named for a field that cannot hold a string: the caller meant that
|
|
// variable's contents, which is the same secret read the way the field needs it.
|
|
if (
|
|
typeof value === 'string' &&
|
|
value.startsWith('$var:') &&
|
|
referencePrefix(prop, value) === '$jsonvar:'
|
|
) {
|
|
return '$jsonvar:' + value.slice('$var:'.length)
|
|
}
|
|
return value
|
|
})
|
|
}
|