Skip to content

Comments

runScript inline for path and hash#8019

Open
diegoimbert wants to merge 3 commits intomainfrom
di/run-inline-scripts-by-path-or-hash
Open

runScript inline for path and hash#8019
diegoimbert wants to merge 3 commits intomainfrom
di/run-inline-scripts-by-path-or-hash

Conversation

@diegoimbert
Copy link
Contributor

No description provided.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Feb 19, 2026

Deploying windmill with  Cloudflare Pages  Cloudflare Pages

Latest commit: 7c5b285
Status: ✅  Deploy successful!
Preview URL: https://3d39993c.windmill.pages.dev
Branch Preview URL: https://di-run-inline-scripts-by-pat.windmill.pages.dev

View logs

@claude
Copy link
Contributor

claude bot commented Feb 19, 2026

Claude finished @diegoimbert's task in 2m 37s —— View job


Review of PR #8019: runScript inline for path and hash

  • Read and understand all changed files
  • Review backend API changes (openapi.yaml)
  • Review job execution logic (jobs.rs)
  • Review common job types (windmill-common/jobs.rs)
  • Review worker execution (worker.rs)
  • Check for bugs, security, and performance issues
  • Post detailed review with inline comments
  • Provide testing instructions

Summary

This PR adds two new API endpoints to run deployed scripts inline (bypassing the job queue), analogous to the existing run_inline/preview endpoint:

  • POST /w/{workspace}/jobs/run_inline/p/{path} — run a script by its path
  • POST /w/{workspace}/jobs/run_inline/h/{hash} — run a script by its hash

The implementation follows the existing run_inline_preview_script pattern: the API handler constructs parameters, passes them through the WorkerInternalServerInlineUtils function pointers, and the worker executes the script directly via run_language_executor.

Key Issues

1. 🔴 Security: Missing check_scopes (Critical)

Every other script execution endpoint in jobs.rs calls check_scopes(&authed, || format!("jobs:run:scripts:{path}")) to enforce API token scope restrictions. The two new inline endpoints skip this check entirely. This means scoped API tokens could execute scripts they shouldn't have access to. See inline comment.

2. 🟡 Code Duplication (~170 lines in worker.rs)

The two new closures in worker.rs (run_inline_script_by_path and run_inline_script_by_hash) are nearly identical — they share the same MiniPulledJob construction, same mutable variables, and same run_language_executor call. The only difference is how the script is resolved (path lookup vs direct hash). Consider extracting a shared helper. Similarly, RunInlineScriptByPathFnParams and RunInlineScriptByHashFnParams in windmill-common/jobs.rs share 11 of 12 fields. See inline comment.

3. 🟡 Tag mismatch: "inline_preview" used for production scripts

Both new closures set tag: "inline_preview" on the MiniPulledJob. These endpoints execute deployed scripts (not previews), so the tag may cause confusion in metrics/logging/routing. See inline comment.

4. 🟢 Minor: Duplicate request body structs

InlineByPath and InlineByHash are identical structs. A single type would suffice. See inline comment.

5. 🟢 Empty job_dir / worker_dir

Both closures receive empty strings for job_dir and worker_dir. While this matches the preview endpoint, path/hash scripts may have lockfiles and codebases that require real directories. See inline comment.

What Looks Good

  • The OpenAPI spec additions are clean and consistent with existing endpoint patterns
  • Feature-gating behind inline_preview is correctly applied
  • The #[cfg(not(feature = "inline_preview"))] fallback handlers return appropriate errors
  • The get_latest_deployed_hash_for_path and get_script_content_by_hash calls correctly resolve scripts from the database/cache

Testing Instructions

To verify these changes, use the Windmill API directly (these are backend-only endpoints with no UI). After deploying a build with the inline_preview feature enabled, send POST requests to POST /api/w/{workspace}/jobs/run_inline/p/{script_path} or POST /api/w/{workspace}/jobs/run_inline/h/{script_hash} with a JSON body containing { "args": { ... } } for an existing deployed script. The response should contain the script result directly (synchronous execution, no job ID). Compare behavior with the equivalent run/p/{path} endpoint to confirm results match while verifying no job is created in the queue.

Comment on lines 327 to 360
}

pub struct RunInlineScriptByPathFnParams {
pub workspace_id: String,
pub path: String,
pub args: Option<HashMap<String, Box<RawValue>>>,
pub created_by: String,
pub permissioned_as: String,
pub permissioned_as_email: String,
pub base_internal_url: String,
pub worker_name: String,
pub conn: crate::worker::Connection,
pub client: AuthedClient,
pub job_dir: String,
pub worker_dir: String,
pub killpill_rx: tokio::sync::broadcast::Receiver<()>,
}

pub struct RunInlineScriptByHashFnParams {
pub workspace_id: String,
pub hash: i64,
pub args: Option<HashMap<String, Box<RawValue>>>,
pub created_by: String,
pub permissioned_as: String,
pub permissioned_as_email: String,
pub base_internal_url: String,
pub worker_name: String,
pub conn: crate::worker::Connection,
pub client: AuthedClient,
pub job_dir: String,
pub worker_dir: String,
pub killpill_rx: tokio::sync::broadcast::Receiver<()>,
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code duplication: RunInlineScriptByPathFnParams and RunInlineScriptByHashFnParams are nearly identical.

These two structs share 11 out of 12 fields — only the identifier field differs (path: String vs hash: i64). Consider unifying them into a single struct with an enum for the identifier:

pub enum InlineScriptTarget {
    Path(String),
    Hash(i64),
}

pub struct RunInlineScriptFnParams {
    pub workspace_id: String,
    pub target: InlineScriptTarget,
    pub args: Option<HashMap<String, Box<RawValue>>>,
    // ... all the shared fields
}

This would also let you collapse the two nearly-identical closures in worker.rs into a single implementation. Up to you whether it's worth the refactor now, but the current approach results in ~170 lines of duplicated logic in worker.rs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/ai can you do a commit for that

Copy link
Contributor

@claude claude bot Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude finished @diegoimbert's task in 6m 47s —— View job


Refactoring inline script code duplication

  • Read current code in windmill-common/src/jobs.rs
  • Read current code in windmill-worker/src/worker.rs
  • Read current code in windmill-api/src/jobs.rs
  • Unify RunInlineScriptByPathFnParams and RunInlineScriptByHashFnParams into a single struct with an enum
  • Collapse duplicate closures in worker.rs into a single implementation
  • Unify duplicate InlineByPath / InlineByHash request body structs
  • Add missing check_scopes call for hash endpoint
  • Fix duplicate lines from prior commit in run_inline_script_by_path
  • Run cargo check to validate
  • Commit and push

Summary

Pushed commit a32924f with the following changes across 3 files (-84 lines net):

  1. windmill-common/src/jobs.rs — Replaced RunInlineScriptByPathFnParams and RunInlineScriptByHashFnParams with a single RunInlineScriptFnParams struct using an InlineScriptTarget enum (Path(String) / Hash(i64)). Collapsed two function pointers in WorkerInternalServerInlineUtils into one run_inline_script.

  2. windmill-worker/src/worker.rs — Merged the two nearly-identical closures (run_inline_script_by_path / run_inline_script_by_hash) into a single run_inline_script closure that pattern-matches on InlineScriptTarget. Also changed the tag from "inline_preview" to "inline" since these execute deployed scripts.

  3. windmill-api/src/jobs.rs — Merged duplicate InlineByPath/InlineByHash structs into InlineScriptArgs. Extracted a shared run_inline_script_inner helper. Added missing check_scopes to the hash endpoint. Fixed duplicate lines introduced by the prior commit.

Comment on lines 4784 to 4870
run_inline_script_by_path: Arc::new(|params: RunInlineScriptByPathFnParams| {
Box::pin(async move {
let db = params
.conn
.as_sql()
.ok_or_else(|| {
error::Error::InternalErr(
"run_inline_script_by_path requires a SQL connection".to_string(),
)
})?
.clone();
let script_hash_info =
get_latest_deployed_hash_for_path(None, db, &params.workspace_id, &params.path)
.await?;
let script_hash = ScriptHash(script_hash_info.hash);
let content_info =
get_script_content_by_hash(&script_hash, &params.workspace_id, &params.conn)
.await?;
let job = MiniPulledJob {
workspace_id: params.workspace_id,
id: Uuid::new_v4(),
args: params.args.map(Json),
parent_job: None,
created_by: params.created_by,
scheduled_for: chrono::Utc::now(),
started_at: None,
runnable_path: Some(params.path),
kind: JobKind::Script,
runnable_id: Some(script_hash),
canceled_reason: None,
canceled_by: None,
permissioned_as: params.permissioned_as,
permissioned_as_email: params.permissioned_as_email,
flow_status: None,
tag: "inline_preview".to_string(),
script_lang: content_info.language,
same_worker: true,
pre_run_error: None,
flow_innermost_root_job: None,
root_job: None,
timeout: None,
flow_step_id: None,
cache_ttl: None,
cache_ignore_s3_path: None,
priority: None,
preprocessed: None,
script_entrypoint_override: None,
trigger: None,
trigger_kind: None,
visible_to_owner: false,
permissioned_as_end_user_email: None,
runnable_settings_handle: None,
concurrent_limit: None,
concurrency_time_window_s: None,
};
let mut mem_peak: i32 = -1;
let mut canceled_by: Option<CanceledBy> = None;
let mut column_order: Option<Vec<String>> = None;
let mut new_args: Option<HashMap<String, Box<RawValue>>> = None;
let mut occupancy_metrics = OccupancyMetrics::new(Instant::now());
let mut has_stream: bool = false;
let mut killpill_rx = params.killpill_rx;

run_language_executor(
&job,
&params.conn,
&params.client,
None,
&params.job_dir,
&params.worker_dir,
&mut mem_peak,
&mut canceled_by,
&params.base_internal_url,
&params.worker_name,
&mut column_order,
&mut new_args,
&mut occupancy_metrics,
&mut killpill_rx,
None,
&mut has_stream,
content_info.language,
&content_info.content,
&content_info.envs,
&content_info.codebase,
&content_info.lockfile,
true,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty job_dir and worker_dir passed from API handler.

Both run_inline_script_by_path and run_inline_script_by_hash receive job_dir: "" and worker_dir: "" from the API handler. While this matches run_inline_preview_script, it's worth confirming that run_language_executor can handle scripts fetched by path/hash with empty directories — preview scripts execute arbitrary code inline, but path/hash scripts may depend on lockfiles or codebases (content_info.lockfile, content_info.codebase) that could require a real working directory for dependency installation.

If this is intentional and works as expected for all supported languages, a brief comment explaining why empty strings are acceptable here would help future readers.

Comment on lines 2870 to 2880
#[cfg(feature = "inline_preview")]
#[derive(Debug, Deserialize)]
struct InlineByPath {
args: Option<HashMap<String, Box<JsonRawValue>>>,
}

#[cfg(feature = "inline_preview")]
#[derive(Debug, Deserialize)]
struct InlineByHash {
args: Option<HashMap<String, Box<JsonRawValue>>>,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: InlineByPath and InlineByHash are identical structs.

Both contain only args: Option<HashMap<String, Box<JsonRawValue>>>. You could use a single type alias or struct:

#[cfg(feature = "inline_preview")]
#[derive(Debug, Deserialize)]
struct InlineScriptArgs {
    args: Option<HashMap<String, Box<JsonRawValue>>>,
}

This is a minor nit — using a single type makes it clearer that the request format is the same for both endpoints.

args: params.args.map(Json),
parent_job: None,
created_by: params.created_by,
scheduled_for: chrono::Utc::now(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential issue: tag: "inline_preview" for non-preview jobs.

Both new closures set tag: "inline_preview".to_string(), which was appropriate for the existing preview endpoint since those are genuinely preview runs. For path/hash runs, these are executing deployed, production scripts — the tag should likely reflect that (e.g., "inline" or something distinct from "inline_preview"). This could cause confusion in logging, metrics, or any tag-based routing logic.

diegoimbert and others added 2 commits February 19, 2026 17:18
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
- Replace RunInlineScriptByPathFnParams and RunInlineScriptByHashFnParams
  with a single RunInlineScriptFnParams using InlineScriptTarget enum
- Collapse two nearly-identical closures in worker.rs into one
- Merge duplicate InlineByPath/InlineByHash into InlineScriptArgs
- Extract shared run_inline_script_inner helper in API handler
- Add missing check_scopes to run_inline_script_by_hash endpoint
- Fix duplicate lines from prior commit in run_inline_script_by_path
- Change tag from "inline_preview" to "inline" for deployed scripts

Co-authored-by: Diego Imbert <diegoimbert@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant