mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-24 16:01:42 +00:00
1727271e19
* feat: add sandboxed docker v2 runtime via '# docker <image>' Run a container image as a subprogram of the job's own nsjail sandbox: extract the image rootfs with podman (rootless) and run it chrooted inside the job's nsjail, so the container inherits the job's confinement and is safe under nsjail / for untrusted code. Selected by '# docker <image>'; a bare '# docker' keeps the v1 (dind) path untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: default to daemonless docker (drop dind from compose, allow docker on cloud) docker-compose no longer ships the dind sidecar (v2 is daemonless: podman + nsjail in the worker); removed the dind service, DOCKER_HOST env, depends_on and volume. Removed the language-picker guard that blocked Docker scripts on the multi-tenant platform, now that v2 makes docker safe to run sandboxed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: select sandboxed container via # sandbox <image>; add pull policy + size guards - Surface moved from '# docker <image>' to '# sandbox <image>' (groups under the sandbox annotation; '# docker' stays v1-only, '# sandbox' stays nsjail-bash). - SANDBOX_IMAGE_PULL_POLICY (default 'newer') so moving tags don't go stale. - SANDBOX_IMAGE_MAX_SIZE_MB rejects oversized images before extraction. - SANDBOX_IMAGE_CACHE_MAX_MB best-effort LRU eviction of podman's image store. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sandbox): support # volume, honor nsjail tmp instance settings, v2 docker template - Thread shared_mount into the sandbox container nsjail config so '# volume' mounts (and the same-worker /tmp/shared folder) apply inside the container. - Use resolve_nsjail_tmp_mount_block for the container's /tmp so it honors the same nsjail_tmp_backing / nsjail_tmpfs_size_mb instance settings as other nsjail jobs. - docker-compose comment + the editor's Docker template now use '# sandbox <image>'. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sandbox): make image size/cache/pull-policy UI instance settings Convert SANDBOX_IMAGE_* from worker env vars to DB-backed instance settings (sandbox_image_max_size_mb, sandbox_image_cache_max_mb, sandbox_image_pull_policy), hot-reloaded via the same mechanism as nsjail_tmpfs_size_mb and configurable in #superadmin-settings. No worker restart needed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sandbox): windmill-managed registry — default registry + private auth Two new instance settings: - sandbox_image_default_registry: prepended to unqualified image refs (alpine -> <registry>/alpine); fully-qualified refs untouched. - sandbox_registry_auth: docker/podman auth.json blob written to a per-job authfile (0600, removed with the job) and passed to podman --authfile for private registries. Both hot-reloaded and configurable in #superadmin-settings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sandbox): protobuf-safe proto_str escaper, atomic 0600 authfile, registry tests Addresses local-review P2s: proto_str now emits valid protobuf octal escapes for control/non-ASCII bytes (not Rust \u{..} that nsjail would reject); the registry authfile is created 0600 atomically (no world-readable window); add a registry_qualified table test + a non-ASCII proto_str case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sandbox): P0 — deliver image env via nsjail envar:, never the launcher process env CI review (P0): the image's OCI Env (attacker-controlled keys+values) was applied to the nsjail launcher process via .envs(), so a hostile image could set LD_PRELOAD/ LD_LIBRARY_PATH/LD_AUDIT on nsjail itself and execute code as the worker outside the jail. Now the image env is rendered as proto-escaped 'envar:' directives (child-only) and nsjail's process env carries only windmill-trusted keys (reserved vars + proxy). Also: warn instead of silently bypassing the size guard on inspect failure; reset the eviction guard via a Drop guard (no stuck flag on panic/early-return). +render_envars test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sandbox): P0 symlink-write escape via rootfs script; P1 redact registry-auth logging CI review: - P0 (Codex): the body was written into the image-controlled rootfs as .windmill_docker_main.sh via write_file (follows symlinks) — a hostile image could plant that path as a symlink to a host file and capture the worker's write before nsjail starts. Now the body is passed straight to 'sh -c <body> sh <args>'; no file is written into the rootfs at all. - P1 (Codex): sandbox_registry_auth flowed through the generic setting loader which logs the value (raw auth.json credentials). Replaced with a secret-aware reload that loads directly and logs only a redacted 'configured=' message. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sandbox): redact sandbox_registry_auth in instance-settings write log too The settings API also logs 'Set global setting <key> to <value>' via format_setting_value; add sandbox_registry_auth to SENSITIVE_SETTINGS so the credential is redacted there as well as on reload. * fix(sandbox): don't silently disable cache eviction on podman images parse error Re-review (cubic/Claude P2): serde_json::from_slice(...).unwrap_or_default() meant any parse hiccup (e.g. podman omitting Size/Created via omitempty for a zero value, or schema drift) silently degraded to an empty Vec and disabled eviction with no log. Now Size/Created are #[serde(default)] (a missing omitempty key -> 0, not a whole-array parse failure) and a real parse error warns + breaks instead of being swallowed. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
306 lines
9.7 KiB
Svelte
306 lines
9.7 KiB
Svelte
<script lang="ts">
|
|
import { Alert } from '$lib/components/common'
|
|
import ToggleHubWorkspace from '$lib/components/ToggleHubWorkspace.svelte'
|
|
import Tooltip from '$lib/components/Tooltip.svelte'
|
|
|
|
import { createEventDispatcher, getContext, untrack } from 'svelte'
|
|
import FlowScriptPicker from '../pickers/FlowScriptPicker.svelte'
|
|
import PickHubScript from '../pickers/PickHubScript.svelte'
|
|
import WorkspaceScriptPicker from '../pickers/WorkspaceScriptPicker.svelte'
|
|
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
|
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
|
|
import { Check, Code, Zap } from 'lucide-svelte'
|
|
import SuspendDrawer from './SuspendDrawer.svelte'
|
|
import { defaultScripts } from '$lib/stores'
|
|
import { defaultScriptLanguages, processLangs } from '$lib/scripts'
|
|
import type { SupportedLanguage } from '$lib/common'
|
|
import DefaultScripts from '$lib/components/DefaultScripts.svelte'
|
|
import type { FlowBuilderWhitelabelCustomUi } from '$lib/components/custom_ui'
|
|
import { canHavePreprocessor } from '$lib/script_helpers'
|
|
|
|
interface Props {
|
|
failureModule: boolean
|
|
preprocessorModule: boolean
|
|
shouldDisableTriggerScripts?: boolean
|
|
noEditor: boolean
|
|
summary?: string | undefined
|
|
}
|
|
|
|
let {
|
|
failureModule,
|
|
preprocessorModule,
|
|
shouldDisableTriggerScripts = false,
|
|
noEditor,
|
|
summary = $bindable(undefined)
|
|
}: Props = $props()
|
|
|
|
const dispatch = createEventDispatcher()
|
|
let kind: 'script' | 'failure' | 'approval' | 'trigger' = $state(
|
|
untrack(() => failureModule)
|
|
? 'failure'
|
|
: summary == 'Trigger'
|
|
? 'trigger'
|
|
: summary == 'Approval'
|
|
? 'approval'
|
|
: 'script'
|
|
)
|
|
let pick_existing: 'workspace' | 'hub' = $state('hub')
|
|
let filter = $state('')
|
|
|
|
let langs = $derived(
|
|
processLangs(undefined, $defaultScripts?.order ?? Object.keys(defaultScriptLanguages))
|
|
.map((l) => [defaultScriptLanguages[l], l])
|
|
.filter(
|
|
(x) => $defaultScripts?.hidden == undefined || !$defaultScripts.hidden.includes(x[1])
|
|
) as [string, SupportedLanguage | 'docker'][]
|
|
)
|
|
|
|
function displayLang(lang: SupportedLanguage | 'docker', kind: string) {
|
|
if (preprocessorModule) {
|
|
return canHavePreprocessor(lang as SupportedLanguage)
|
|
}
|
|
|
|
if (failureModule || kind === 'trigger') {
|
|
if (
|
|
[
|
|
'postgresql',
|
|
'mysql',
|
|
'bigquery',
|
|
'snowflake',
|
|
'mssql',
|
|
'graphql',
|
|
'duckdb',
|
|
'oracledb'
|
|
].includes(lang)
|
|
) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
if (kind === 'script') {
|
|
return lang !== 'docker'
|
|
}
|
|
|
|
if (kind === 'approval') {
|
|
if (
|
|
[
|
|
'postgresql',
|
|
'mysql',
|
|
'bigquery',
|
|
'snowflake',
|
|
'mssql',
|
|
'graphql',
|
|
'duckdb',
|
|
'oracledb'
|
|
].includes(lang)
|
|
) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
let customUi: undefined | FlowBuilderWhitelabelCustomUi = getContext('customUi')
|
|
</script>
|
|
|
|
<div class="p-4 h-full flex flex-col" id="flow-editor-flow-inputs">
|
|
{#if summary == 'Terminate flow'}
|
|
<Alert type="info" title="The flow stops here"
|
|
>This is an identity step with an early stop that has 'true' for expression</Alert
|
|
>
|
|
{:else}{#if !failureModule && !preprocessorModule}
|
|
<div class="center-center">
|
|
<div class="max-w-min">
|
|
<ToggleButtonGroup bind:selected={kind}>
|
|
{#snippet children({ item })}
|
|
<ToggleButton
|
|
value="script"
|
|
icon={Code}
|
|
label="Action"
|
|
tooltip="An action script is simply a script that is neither a trigger nor an approval script. Those are the majority of the scripts."
|
|
{item}
|
|
/>
|
|
{#if !shouldDisableTriggerScripts}
|
|
<ToggleButton
|
|
value="trigger"
|
|
icon={Zap}
|
|
label="Trigger"
|
|
tooltip="Used as a first step most commonly with a state and a schedule to watch for changes on an external system, compute the diff since last time and set the new state. The diffs are then treated one by one with a for-loop."
|
|
{item}
|
|
/>
|
|
{/if}
|
|
<ToggleButton
|
|
value="approval"
|
|
icon={Check}
|
|
label="Approval"
|
|
tooltip="An approval step will suspend the execution of a flow until it has been approved through the resume endpoints or the approval page by and solely by the recipients of those secret urls."
|
|
{item}
|
|
/>
|
|
{/snippet}
|
|
</ToggleButtonGroup>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
{#if kind == 'trigger'}
|
|
<div class="mt-2"></div>
|
|
<Alert title="Trigger scripts" type="info">
|
|
Trigger scripts are designed to pull data from an external source and return all of the new
|
|
items since the last run, without resorting to external webhooks.<br /><br />
|
|
|
|
A trigger script is intended to be used with
|
|
<a
|
|
href="https://www.windmill.dev/docs/core_concepts/scheduling"
|
|
target="_blank"
|
|
class="text-blue-400">schedules</a
|
|
>
|
|
and
|
|
<a
|
|
href="https://www.windmill.dev/docs/core_concepts/resources_and_types#states"
|
|
target="_blank"
|
|
class="text-blue-400">states</a
|
|
>
|
|
in order to compare the execution to the previous one and process each new item in a
|
|
<a
|
|
href="https://www.windmill.dev/docs/flows/flow_loops"
|
|
target="_blank"
|
|
class="text-blue-400">for loop</a
|
|
>. If there are no new items, the flow will be skipped.<br /><br />
|
|
|
|
By default, adding a trigger will set the schedule to 15 minutes. To see all ways to trigger
|
|
a flow, check
|
|
<a
|
|
href="https://www.windmill.dev/docs/getting_started/triggers"
|
|
target="_blank"
|
|
class="text-blue-400">Triggering Flows</a
|
|
>.
|
|
</Alert>
|
|
{/if}
|
|
|
|
{#if kind == 'script' && !noEditor && !preprocessorModule}
|
|
<div class="mt-2"></div>
|
|
<Alert title="Action Scripts" type="info">
|
|
An action script is simply a script that is neither a trigger nor an approval script. Those
|
|
are the majority of the scripts.
|
|
</Alert>
|
|
{/if}
|
|
|
|
{#if kind == 'approval'}
|
|
{#if !noEditor}
|
|
<div class="mt-2"></div>
|
|
<Alert title="Approval/Prompt Step" type="info">
|
|
An approval/prompt step will suspend the execution of a flow until it has been approved
|
|
and/or the prompts have been filled in the UI or through the resume endpoints or the
|
|
approval page by and solely by the recipients of the secret urls. See details in
|
|
'Advanced' -> 'Suspend' settings of the step. A prompt is a specialized approval step with
|
|
payload that can be self-approved by the caller.<br /><br />
|
|
For further details, visit
|
|
<a
|
|
href="https://www.windmill.dev/docs/flows/flow_approval"
|
|
target="_blank"
|
|
class="text-blue-500">Approval/Prompt Steps Documentation</a
|
|
>
|
|
or
|
|
<div class="inline-flex">
|
|
<SuspendDrawer text="Approval/Step prompt helpers" />
|
|
</div>
|
|
</Alert>
|
|
{:else}
|
|
<a
|
|
href="https://www.windmill.dev/docs/flows/flow_approval"
|
|
target="_blank"
|
|
class="text-blue-500">Approval/Prompt Steps Documentation</a
|
|
>
|
|
{/if}
|
|
{/if}
|
|
<h3 class="pb-2 pt-4 flex gap-x-8 flex-wrap">
|
|
<div>
|
|
Inline new <span class="text-blue-500 dark:text-blue-400"
|
|
>{kind == 'script' ? 'action' : kind}</span
|
|
>
|
|
script
|
|
<Tooltip
|
|
documentationLink={kind === 'script'
|
|
? 'https://www.windmill.dev/docs/flows/editor_components#flow-actions'
|
|
: kind === 'trigger'
|
|
? 'https://www.windmill.dev/docs/flows/flow_trigger'
|
|
: kind === 'approval'
|
|
? 'https://www.windmill.dev/docs/flows/flow_approval'
|
|
: 'https://www.windmill.dev/docs/getting_started/flows_quickstart#flow-editor'}
|
|
>
|
|
Embed <span>{kind == 'script' ? 'action' : kind}</span> script directly inside a flow instead
|
|
of saving the script into your workspace for reuse. You can always save an inline script to
|
|
your workspace later.
|
|
</Tooltip>
|
|
</div>
|
|
<DefaultScripts />
|
|
</h3>
|
|
{#if noEditor}
|
|
<div
|
|
class="py-0.5 text-2xs {summary == undefined || summary == ''
|
|
? 'text-red-600'
|
|
: 'text-ternary'}"
|
|
>Pick a summary first, it will be used to create a separate file whose name will be derived
|
|
from the summary</div
|
|
>
|
|
<input class="w-full" type="text" bind:value={summary} placeholder="Summary" />
|
|
<div class="pb-2"></div>
|
|
{/if}
|
|
<div class="flex flex-row flex-wrap gap-2" id="flow-editor-action-script">
|
|
{#each langs.filter((lang) => customUi?.languages == undefined || customUi?.languages?.includes(lang?.[1])) as [label, lang] (lang)}
|
|
{#if displayLang(lang, kind)}
|
|
<FlowScriptPicker
|
|
id={`flow-editor-action-script-${lang}`}
|
|
disabled={noEditor && (summary == undefined || summary == '')}
|
|
{label}
|
|
lang={lang == 'docker' ? 'bash' : lang}
|
|
on:click={() => {
|
|
dispatch('new', {
|
|
language: lang == 'docker' ? 'bash' : lang,
|
|
kind,
|
|
subkind: lang == 'docker' ? 'docker' : preprocessorModule ? 'preprocessor' : 'flow',
|
|
summary
|
|
})
|
|
}}
|
|
/>
|
|
{/if}
|
|
{/each}
|
|
</div>
|
|
|
|
{#if !failureModule && !preprocessorModule && customUi?.aiSandbox != false}
|
|
<h3 class="pb-2 pt-4">AI Sandbox</h3>
|
|
<div class="flex flex-row flex-wrap gap-2">
|
|
<FlowScriptPicker
|
|
label="Claude Code"
|
|
lang="claudesandbox"
|
|
on:click={() => {
|
|
dispatch('new', {
|
|
language: 'bun',
|
|
kind,
|
|
subkind: 'claudesandbox',
|
|
summary
|
|
})
|
|
}}
|
|
/>
|
|
</div>
|
|
{/if}
|
|
|
|
<h3 class="mb-2 mt-6"
|
|
>Use pre-made <span class="text-blue-500 dark:text-blue-400"
|
|
>{kind == 'script' ? 'action' : kind}</span
|
|
> script</h3
|
|
>
|
|
{#if pick_existing == 'hub'}
|
|
<PickHubScript bind:filter {kind} on:pick>
|
|
<ToggleHubWorkspace bind:selected={pick_existing} />
|
|
</PickHubScript>
|
|
{:else}
|
|
<WorkspaceScriptPicker displayLock bind:filter {kind} on:pick>
|
|
<ToggleHubWorkspace bind:selected={pick_existing} />
|
|
</WorkspaceScriptPicker>
|
|
{/if}
|
|
{/if}
|
|
</div>
|