feat: add sandbox annotations and volume mounts#8058
Conversation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Deploying windmill with
|
| Latest commit: |
c149262
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://a057d48f.windmill.pages.dev |
| Branch Preview URL: | https://sandbox-volumes.windmill.pages.dev |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add volume as a recognized asset kind in openflow spec and asset parser - Add file_count column to volume table with migration - Track file count during volume sync-back operations - List volumes from both volume table and asset table (UNION query) - Add volumes button on assets page with drawer showing volume list - Add explore button in volumes drawer to open S3 file picker at volume prefix - Fix S3 file picker to dynamically set rootPath for folder navigation - Fix JobAssetsViewer to fetch code by script_hash when raw_code is missing - Add "sandbox mode (nsjail)" log message to bun, python, and deno executors Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Restore create_wm_deployers_group to its original 20260224000000 timestamp from main, and move add_volumes to 20260226000000 to avoid collision. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Volume names in annotations can now use $workspace and $args[key] placeholders (same syntax as tag interpolation), resolved at runtime from job args. Examples: # volume: $workspace-data /tmp/data # volume: cache-$args[env] /tmp/cache Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add allowDelete prop to S3FilePicker that enables the delete button even in readOnlyMode. Enabled on the assets page volume explorer. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ndbox template - Add AI generation button (ResourceGen) in resource editor and ApiConnectForm to generate resource values from a prompt, with fileset-aware mode - Fix FilesetEditor not updating when args change externally (e.g. from AI gen) - Fix folder deletion in fileset editor by ensuring intermediate folder nodes always get trailing / in their path - Error instead of silently skipping volume mounts when no workspace S3 storage is configured - Update claude sandbox template with agent_instructions support Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…enterprise Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts: # backend/.sqlx/query-08f288d2781d823e109a9e5b8848234ca7d1efeee9661f3901f298da375e73f7.json # backend/ee-repo-ref.txt # frontend/package-lock.json
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| let volume_mounts = { | ||
| let comment_prefix = match language { | ||
| ScriptLang::Python3 | ||
| | ScriptLang::Bash | ||
| | ScriptLang::Powershell | ||
| | ScriptLang::Ansible | ||
| | ScriptLang::Ruby => "#", | ||
| ScriptLang::Deno | ||
| | ScriptLang::Bun | ||
| | ScriptLang::Bunnative | ||
| | ScriptLang::Nativets | ||
| | ScriptLang::Go => "//", | ||
| _ => "", |
There was a problem hiding this comment.
Question: The comment prefix mapping here doesn't include all languages that have annotations. For example, ScriptLang::Ruby uses "#" but Ruby doesn't seem to have sandbox annotations defined. Also, if a language like PHP (which isn't listed) ever needs volume support, the empty string "" from the catch-all would make parse_volume_annotations parse nothing since no line starts with an empty prefix. Consider adding a comment to clarify which languages are intentionally supported for volumes.
| "What is the fastest OSS workflow engine?" : | ||
| "What did I ask you before?" | ||
|
|
||
| process.env.ANTHROPIC_API_KEY = anthropic.apiKey; |
There was a problem hiding this comment.
Security consideration: The template sets process.env.ANTHROPIC_API_KEY = anthropic.apiKey as an environment variable. Since this runs inside a sandbox (nsjail), this is somewhat contained, but worth noting that any child process spawned by the Claude agent will also inherit this key. The permissionMode: 'bypassPermissions' and allowDangerouslySkipPermissions: true on line 44-45 also mean the Claude agent has full unrestricted access within the sandbox. This is expected for the use case but ensure the sandbox nsjail config is tight enough (no network to exfiltrate the key, etc.). The nsjail config does have iface_no_lo: true and clone_newnet: false, which allows network access — so the API key could theoretically be exfiltrated via network by the agent.
|
/ai-fast re-review this |
|
Claude finished @rubenfiszel's task in 2m 52s —— View job Re-review: feat: add sandbox annotations and volume mounts
Overall AssessmentWell-structured PR adding per-script sandbox forcing and volume mounts via annotations. The architecture follows existing codebase patterns: new What looks good
Issues1. Performance — Regex compiled on every call (
static RE: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
regex::Regex::new(r#"\$args\[((?:\w+\.)*\w+)\]"#).unwrap()
});
let re = &*RE;2. Security — path traversal on volume target ( When sandboxing is disabled, let resolved = std::path::Path::new(job_dir).join(&volume.target);
// No validation that resolved stays within job_dir
std::os::unix::fs::symlink(&state.local_dir, &resolved).ok();Consider canonicalizing and validating the resolved path stays within 3. Reliability — 120-second busy-wait blocks worker ( The lease acquisition loop sleeps for up to 120 seconds with no way to detect job cancellation. Consider checking 4. No validation on volume names ( The parser accepts any non-whitespace as a volume name. After interpolation, names are used in S3 paths ( Minor observations
New additions since last review
| Branch |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…absolute mount paths Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
# Conflicts: # frontend/src/routes/(root)/(logged)/assets/+page.svelte
EE files (_ee.rs) are gitignored and should only be tracked in windmill-ee-private. The symlink was accidentally committed, causing CI to fail with "not writing through dangling symlink". Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add extract_worker_name() stub to OSS AgentCache so lib.rs doesn't need #[cfg(feature = "private")] gates. Updates ee-repo-ref. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit updates the EE repository reference after PR #433 was merged in windmill-ee-private. Previous ee-repo-ref: fee39059a9fc290754aea7ab18f2adff0c81242c New ee-repo-ref: bd5e01ced1b319d11c7b24556285996e48fe793b Automated by sync-ee-ref workflow.
|
🤖 Updated |
…overflow The run_language_executor async function grew ~1000 lines with volume handling code, making the generated future struct too large for the default stack. Extract volume setup and sync-back into 4 separate async functions (setup_volumes_sql_worker, setup_volumes_http_worker, sync_volumes_sql_worker, sync_volumes_http_worker) so each has its own future struct, reducing the parent function's stack frame. Fixes test_workflow_as_code stack overflow. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add POST /volumes/create endpoint with CLOUD_HOSTED limit
- Add GET /volumes/storage endpoint for volume storage name (non-admin)
- Add "New volume" popover button in VolumesDrawer
- Fix volume explore to use volumes/{workspace}/{name}/ prefix
- Fix volume explore to use correct secondary storage
- Keep VolumesDrawer open when exploring (stacked drawers)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Adds sandbox annotations and persistent volumes for Windmill scripts, with a first-class Claude Code sandbox template that combines both features.
Sandbox Annotations
Scripts can opt into nsjail/restricted execution with a single annotation in the script header:
--allow-all).process.getuid()work correctly.Docker: Claude Code CLI
All three Dockerfiles (full, slim, slim-ee) now install the Claude Code CLI at
/usr/bin/claude, accessible inside nsjail sandboxes (which mount/usrbut not/root).Volumes
Volumes provide persistent, workspace-scoped storage that survives across job executions. Files are stored in the workspace's configured S3/object storage and synced to/from the worker filesystem at job start/end.
Annotation syntax
Dynamic volume names
Volume names support interpolation for dynamic per-input or per-workspace volumes:
$workspace→ replaced with the current workspace ID$args[param_name]→ replaced with the value of a script input parameter$args[config.env]→ supports nested object accessExample:
// volume: cache-$args[env] .cachewith inputenv = "prod"→ volume namecache-prodHow it works
.., restricted absolute prefixes), max 10 volumes per job, no duplicate names/targetsvolumes/{workspace_id}/{volume_name}/) with:Lease system
Volumes use an exclusive lease to prevent concurrent writes:
INSERT ON CONFLICTwith 60-second expiryTwo execution paths
download_volumeandsync_volume_backdirectly with the workspace's S3 clientPOST /begin,GET /file/*path,PUT /file/*path,POST /commit) which proxy to S3 through the API server with JWT-authenticated worker identityDatabase schema
Volumes are also registered as an asset kind (
AssetKind::Volume) for the unified asset system, with granular permissions support viaextra_perms.API endpoints
Under
/api/w/:workspace/volumes:GET /list— List all volumes in the workspaceDELETE /:name— Delete a volume (checks for active lease, removes S3 objects)POST /:name/begin— Acquire lease, return manifest + symlinksGET /:name/file/*path— Download a file (lease required)PUT /:name/file/*path— Upload a file (lease required)POST /:name/commit— Release lease, update metadata, handle deletions + symlinksAgent worker endpoints are mounted under
/api/w/:workspace/agent_workers/volumeswith JWT authentication.Security
volumes/{workspace_id}/{name}/prevents cross-workspace collisions when sharing a bucket..traversal, and targets that resolve outside the volume directory..segments, and absolute pathsClaude Code Sandbox Template
A new "Claude Sandbox" script template that combines both features:
This creates a sandboxed environment where:
// sandboxensures nsjail isolation — Claude Code runs in a restricted filesystem with no network access to internal services// volume: claude .claudepersists the.claude/directory (session state, CLAUDE.md, skills) across runs, enabling:The template accepts an
AgentInstructionsinput (aRecord<string, string>of file paths to content) for injecting CLAUDE.md and skill files at runtime, making it composable with Windmill's fileset resources.Frontend
VolumeDetailDrawerandVolumesDrawercomponents for browsing, managing permissions, and deleting volumes in the asset explorerClaudeIconcomponent used in the script template pickerShareModalFiles changed
New crate:
windmill-worker-volumeslib.rs— Types (VolumeMount,VolumeState,FileEntry,SyncStats), annotation parser, name/target validation, dynamic name interpolation, 30+ unit testsvolume_oss.rs— OSS stubs fordownload_volume,sync_volume_back,volume_nsjail_mountvolume_ee.rs(EE) — Full implementation: S3 sync, filesystem cache with LRU eviction, MD5 change detection, parallel downloads/uploads, symlink handling, nsjail mount generationBackend modifications
windmill-worker/src/worker.rs— Volume lifecycle integration inrun_language_executor: parse → validate → download → mount → execute → sync back. Both SQL and HTTP agent worker paths.windmill-worker/src/python_executor.rs—#sandboxannotation parsing + nsjail enforcementwindmill-worker/src/bun_executor.rs—//sandboxannotation + nsjail enforcement + non-root fixwindmill-worker/src/deno_executor.rs—//sandboxannotation + restricted permissionswindmill-api/src/volumes_oss.rs— Volume REST API with EE switchwindmill-api/src/volumes_ee.rs(EE) — Full endpoint implementationswindmill-api/src/lib.rs— Agent worker volume routing +inject_agent_authedmiddlewarewindmill-api-agent-workers/src/lib.rs—extract_worker_nameon OSSAgentCachewindmill-api-agent-workers/src/ee.rs(EE) —extract_worker_nameon EEAgentCache20260226000000_add_volumes—volumetable +AssetKind::volumeFrontend
VolumeDetailDrawer.svelte,VolumesDrawer.svelte— Volume management UIClaudeIcon.svelte— Claude logo iconclaude_sandbox.ts.template— Claude Code sandbox script templateDocker
Dockerfile,DockerfileSlim,DockerfileSlimEe— Claude Code CLI installationTest plan
cargo checkpasses (CE and EE)cargo test -p windmill-worker-volumes— 30+ unit tests pass# sandboxin Python script → nsjail used// volume: test /tmp/data→ files persist across runs🤖 Generated with Claude Code