diff --git a/backend/.sqlx/query-2a510a8bec98055796f987d86c344ca116895d71de09db338ec09e425dcebe5e.json b/backend/.sqlx/query-3e869b95799300dff973a3c858f823a2872e33a360ff5a02169e4e80ce3d3b7b.json similarity index 75% rename from backend/.sqlx/query-2a510a8bec98055796f987d86c344ca116895d71de09db338ec09e425dcebe5e.json rename to backend/.sqlx/query-3e869b95799300dff973a3c858f823a2872e33a360ff5a02169e4e80ce3d3b7b.json index 23337708f8..ce3b4fcdf8 100644 --- a/backend/.sqlx/query-2a510a8bec98055796f987d86c344ca116895d71de09db338ec09e425dcebe5e.json +++ b/backend/.sqlx/query-3e869b95799300dff973a3c858f823a2872e33a360ff5a02169e4e80ce3d3b7b.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT email, username, is_admin, is_operator, groups, folders FROM job_perms WHERE job_id = $1 AND workspace_id = $2", + "query": "SELECT email, username, is_admin, is_operator, groups, folders, end_user_email FROM job_perms WHERE job_id = $1 AND workspace_id = $2", "describe": { "columns": [ { @@ -32,6 +32,11 @@ "ordinal": 5, "name": "folders", "type_info": "JsonbArray" + }, + { + "ordinal": 6, + "name": "end_user_email", + "type_info": "Varchar" } ], "parameters": { @@ -46,8 +51,9 @@ false, false, false, - false + false, + true ] }, - "hash": "2a510a8bec98055796f987d86c344ca116895d71de09db338ec09e425dcebe5e" + "hash": "3e869b95799300dff973a3c858f823a2872e33a360ff5a02169e4e80ce3d3b7b" } diff --git a/backend/tests/end_user_email.rs b/backend/tests/end_user_email.rs index 539eeaaa6e..366c220c62 100644 --- a/backend/tests/end_user_email.rs +++ b/backend/tests/end_user_email.rs @@ -153,6 +153,72 @@ async fn create_raw_app_with_inline_script(port: u16, path: &str) -> anyhow::Res Ok(()) } +/// Create an app whose policy allows triggering the fixture flow +async fn create_app_triggering_flow(port: u16, path: &str, flow_path: &str) -> anyhow::Result<()> { + let url = format!("http://localhost:{}/api/w/test-workspace/apps/create", port); + let resp = authed(client().post(&url), SAME_WS_TOKEN) + .json(&json!({ + "path": path, + "summary": "Test app running a flow for WM_END_USER_EMAIL", + "value": { + "type": "app", + "grid": [], + "subgrids": {}, + "hiddenInlineScripts": [] + }, + "policy": { + "execution_mode": "anonymous", + "on_behalf_of": null, + "on_behalf_of_email": null, + "triggerables_v2": { + flow_path: { + "static_inputs": {}, + "one_of_inputs": {} + } + } + } + })) + .send() + .await?; + if !resp.status().is_success() { + anyhow::bail!( + "create app failed: {} - {}", + resp.status(), + resp.text().await? + ); + } + Ok(()) +} + +async fn run_app_flow( + port: u16, + token: &str, + app_path: &str, + flow_path: &str, +) -> anyhow::Result { + let url = format!( + "http://localhost:{}/api/w/test-workspace/apps_u/execute_component/{}", + port, app_path + ); + let resp = authed(client().post(&url), token) + .json(&json!({ + "args": {}, + "component": "run_flow", + "path": flow_path + })) + .send() + .await?; + if !resp.status().is_success() { + anyhow::bail!( + "app flow run failed: {} - {}", + resp.status(), + resp.text().await? + ); + } + let job_id = resp.text().await?; + wait_for_job_result(port, token, &job_id).await +} + async fn run_app_inline_script( port: u16, token: &str, @@ -328,6 +394,42 @@ async fn test_app_wm_end_user_email(db: Pool) -> anyhow::Result<()> { Ok(()) } +/// Every flow step must see the same end user as the flow itself: `end_user_email` is only +/// stamped on the job the app pushes, so it has to be forwarded down to each step. The +/// fixture flow has two steps on purpose - the first one is pushed from the freshly pulled +/// flow job, every later one from a flow job re-fetched without its `job_perms`. +#[cfg(feature = "deno_core")] +#[sqlx::test(fixtures("base", "end_user_email"))] +async fn test_app_flow_step_wm_end_user_email(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + set_jwt_secret().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + let app_path = "f/test/email_flow_app"; + let flow_path = "flow/f/test/get_end_user_email_flow"; + + in_test_worker( + Connection::Sql(db.clone()), + async move { + create_app_triggering_flow(port, app_path, flow_path).await?; + + // The flow result is its last step's result. + let result = run_app_flow(port, OTHER_WS_TOKEN, app_path, flow_path).await?; + assert_eq!( + result, OTHER_WS_EMAIL, + "every flow step should see the end user email, not the app publisher's" + ); + + Ok::<(), anyhow::Error>(()) + }, + port, + ) + .await?; + + Ok(()) +} + #[cfg(feature = "deno_core")] #[sqlx::test(fixtures("base", "end_user_email"))] async fn test_raw_app_wm_end_user_email(db: Pool) -> anyhow::Result<()> { diff --git a/backend/tests/fixtures/end_user_email.sql b/backend/tests/fixtures/end_user_email.sql index 17d459f7de..596d34e3e3 100644 --- a/backend/tests/fixtures/end_user_email.sql +++ b/backend/tests/fixtures/end_user_email.sql @@ -51,7 +51,7 @@ INSERT INTO flow (workspace_id, summary, description, path, versions, schema, va VALUES ( 'test-workspace', 'Returns WM_END_USER_EMAIL', '', 'f/test/get_end_user_email_flow', '{900002}', '{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}', - '{"modules": [{"id": "a", "value": {"type": "rawscript", "language": "deno", "content": "export function main() { return Deno.env.get(\"WM_END_USER_EMAIL\") || \"\"; }", "input_transforms": {}}}]}', + '{"modules": [{"id": "a", "value": {"type": "rawscript", "language": "deno", "content": "export function main() { return Deno.env.get(\"WM_END_USER_EMAIL\") || \"\"; }", "input_transforms": {}}}, {"id": "b", "value": {"type": "rawscript", "language": "deno", "content": "export function main() { return Deno.env.get(\"WM_END_USER_EMAIL\") || \"\"; }", "input_transforms": {}}}]}', 'test-user', '{"g/all": true}' ); @@ -60,6 +60,6 @@ INSERT INTO flow_version (id, workspace_id, path, schema, value, created_by) VALUES ( 900002, 'test-workspace', 'f/test/get_end_user_email_flow', '{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}', - '{"modules": [{"id": "a", "value": {"type": "rawscript", "language": "deno", "content": "export function main() { return Deno.env.get(\"WM_END_USER_EMAIL\") || \"\"; }", "input_transforms": {}}}]}', + '{"modules": [{"id": "a", "value": {"type": "rawscript", "language": "deno", "content": "export function main() { return Deno.env.get(\"WM_END_USER_EMAIL\") || \"\"; }", "input_transforms": {}}}, {"id": "b", "value": {"type": "rawscript", "language": "deno", "content": "export function main() { return Deno.env.get(\"WM_END_USER_EMAIL\") || \"\"; }", "input_transforms": {}}}]}', 'test-user' ); diff --git a/backend/windmill-common/src/auth.rs b/backend/windmill-common/src/auth.rs index 736190295e..1773045da4 100644 --- a/backend/windmill-common/src/auth.rs +++ b/backend/windmill-common/src/auth.rs @@ -282,6 +282,7 @@ pub struct JobPerms { pub is_operator: bool, pub groups: Vec, pub folders: Vec, + pub end_user_email: Option, } impl From for Authed { @@ -503,7 +504,7 @@ pub async fn get_job_perms<'a, E: sqlx::PgExecutor<'a>>( ) -> sqlx::Result> { sqlx::query_as!( JobPerms, - "SELECT email, username, is_admin, is_operator, groups, folders FROM job_perms WHERE job_id = $1 AND workspace_id = $2", + "SELECT email, username, is_admin, is_operator, groups, folders, end_user_email FROM job_perms WHERE job_id = $1 AND workspace_id = $2", job_id, w_id ) diff --git a/backend/windmill-common/src/variables.rs b/backend/windmill-common/src/variables.rs index b43eb07e9c..88b823eda9 100644 --- a/backend/windmill-common/src/variables.rs +++ b/backend/windmill-common/src/variables.rs @@ -470,7 +470,7 @@ pub async fn get_reserved_variables( ContextualVariable { name: "WM_END_USER_EMAIL".to_string(), value: end_user_email.unwrap_or_else(|| "".to_string()), - description: "Email of the end user that executed the current script. Only available when triggered from an app.".to_string(), + description: "Email of the end user that executed the current script, propagated to flow steps. Only set when the run was triggered from an app (empty otherwise).".to_string(), is_custom: false, }, ContextualVariable { diff --git a/backend/windmill-queue/src/jobs.rs b/backend/windmill-queue/src/jobs.rs index ebae8a2647..d05d137e02 100644 --- a/backend/windmill-queue/src/jobs.rs +++ b/backend/windmill-queue/src/jobs.rs @@ -3375,7 +3375,15 @@ impl PulledJob { Some(is_operator), Some(groups), Some(folders), - ) => Some(JobPerms { email, username, is_admin, is_operator, groups, folders }), + ) => Some(JobPerms { + email, + username, + is_admin, + is_operator, + groups, + folders, + end_user_email: self.job.permissioned_as_end_user_email.clone(), + }), _ => None, }; diff --git a/backend/windmill-worker/src/worker_flow.rs b/backend/windmill-worker/src/worker_flow.rs index 6bd468e42e..519f4cb8da 100644 --- a/backend/windmill-worker/src/worker_flow.rs +++ b/backend/windmill-worker/src/worker_flow.rs @@ -4344,10 +4344,17 @@ async fn push_next_flow_job( let flow_root_job = get_root_job_id(&flow_job); // forward root job permissions to the new job - let job_perms: Option = - get_job_perms(&mut *tx, &flow_root_job, &flow_job.workspace_id) - .await? - .map(|x| x.into()); + let root_job_perms = + get_job_perms(&mut *tx, &flow_root_job, &flow_job.workspace_id).await?; + // The end user is a property of whoever triggered the root run, so every step (and + // transitively every subflow's step) must see the same WM_END_USER_EMAIL as the run. + // It is read from the root's `job_perms` rather than off `flow_job`: only a freshly + // pulled job carries `permissioned_as_end_user_email`, and every step past the first + // is pushed from a flow job re-fetched by `get_mini_pulled_job`, which does not. + let end_user_email = root_job_perms + .as_ref() + .and_then(|x| x.end_user_email.clone()); + let job_perms: Option = root_job_perms.map(|x| x.into()); tracing::debug!(id = %flow_job.id, root_id = %job_root, "computed perms for job {i} of {len}"); let tag = resolve_flow_step_tag( @@ -4441,7 +4448,7 @@ async fn push_next_flow_job( new_job_priority_override, job_perms.as_ref(), continue_with_runners, - None, + end_user_email, None, None, ) diff --git a/cli/src/guidance/skills.gen.ts b/cli/src/guidance/skills.gen.ts index 50ac0abc9f..8583bcce76 100644 --- a/cli/src/guidance/skills.gen.ts +++ b/cli/src/guidance/skills.gen.ts @@ -547,6 +547,11 @@ const result: wmill.S3Object = await wmill.writeS3File( Import: import * as wmill from 'windmill-client' +To know who is running the script, read the contextual variables rather than calling the API: +\`process.env.WM_END_USER_EMAIL || process.env.WM_EMAIL\`. WM_END_USER_EMAIL is the app viewer when +the run was triggered from an app and empty otherwise (both variables are always defined), WM_EMAIL +is the user the job is permissioned as. WM_USERNAME is the matching username. + workerHasInternalServer(): boolean /** @@ -577,11 +582,6 @@ async getResource(path?: string, undefinedIfEmpty?: boolean): Promise */ async getRootJobId(jobId?: string): Promise -/** - * @deprecated Use runScriptByPath or runScriptByHash instead - */ -async runScript(path: string | null = null, hash_: string | null = null, args: Record | null = null, verbose: boolean = false, tag: string | null = null): Promise - /** * Run a script synchronously by its path and wait for the result * @param path - Script path in Windmill @@ -646,11 +646,6 @@ async getResult(jobId: string): Promise */ async getResultMaybe(jobId: string): Promise -/** - * @deprecated Use runScriptByPathAsync or runScriptByHashAsync instead - */ -async runScriptAsync(path: string | null, hash_: string | null, args: Record | null, scheduledInSeconds: number | null = null, tag: string | null = null): Promise - /** * Run a script asynchronously by its path * @param path - Script path in Windmill @@ -703,13 +698,6 @@ getStatePath(): string */ async setResource(value: any, path?: string, initializeToTypeIfNotExist?: string): Promise -/** - * Set the state - * @param state state to set - * @deprecated use setState instead - */ -async setInternalState(state: any): Promise - /** * Set the state * @param state state to set @@ -745,12 +733,6 @@ async setFlowUserState(key: string, value: any, errorIfNotPossible?: boolean): P */ async getFlowUserState(key: string, errorIfNotPossible?: boolean): Promise -/** - * Get the internal state - * @deprecated use getState instead - */ -async getInternalState(): Promise - /** * Get the state shared across executions * @param path Optional state resource path override. Defaults to \`getStatePath()\`. @@ -886,15 +868,6 @@ async getResumeUrls(approver?: string, flowLevel?: boolean): Promise<{ cancel: string; }> -/** - * @deprecated use getResumeUrls instead - */ -getResumeEndpoints(approver?: string): Promise<{ - approvalPage: string; - resume: string; - cancel: string; -}> - /** * Get an OIDC jwt token for auth to external services (e.g: Vault, AWS) (ee only) * @param audience audience of the token @@ -917,15 +890,6 @@ base64ToUint8Array(data: string): Uint8Array */ uint8ArrayToBase64(arrayBuffer: Uint8Array): string -/** - * Get email from workspace username - * This method is particularly useful for apps that require the email address of the viewer. - * Indeed, in the viewer context, WM_USERNAME is set to the username of the viewer but WM_EMAIL is set to the email of the creator of the app. - * @param username - * @returns email address - */ -async usernameToEmail(username: string): Promise - /** * Sends an interactive approval request via Slack, allowing optional customization of the message, approver, and form fields. * @@ -1347,6 +1311,11 @@ const result: wmill.S3Object = await wmill.writeS3File( Import: import * as wmill from 'windmill-client' +To know who is running the script, read the contextual variables rather than calling the API: +\`process.env.WM_END_USER_EMAIL || process.env.WM_EMAIL\`. WM_END_USER_EMAIL is the app viewer when +the run was triggered from an app and empty otherwise (both variables are always defined), WM_EMAIL +is the user the job is permissioned as. WM_USERNAME is the matching username. + workerHasInternalServer(): boolean /** @@ -1377,11 +1346,6 @@ async getResource(path?: string, undefinedIfEmpty?: boolean): Promise */ async getRootJobId(jobId?: string): Promise -/** - * @deprecated Use runScriptByPath or runScriptByHash instead - */ -async runScript(path: string | null = null, hash_: string | null = null, args: Record | null = null, verbose: boolean = false, tag: string | null = null): Promise - /** * Run a script synchronously by its path and wait for the result * @param path - Script path in Windmill @@ -1446,11 +1410,6 @@ async getResult(jobId: string): Promise */ async getResultMaybe(jobId: string): Promise -/** - * @deprecated Use runScriptByPathAsync or runScriptByHashAsync instead - */ -async runScriptAsync(path: string | null, hash_: string | null, args: Record | null, scheduledInSeconds: number | null = null, tag: string | null = null): Promise - /** * Run a script asynchronously by its path * @param path - Script path in Windmill @@ -1503,13 +1462,6 @@ getStatePath(): string */ async setResource(value: any, path?: string, initializeToTypeIfNotExist?: string): Promise -/** - * Set the state - * @param state state to set - * @deprecated use setState instead - */ -async setInternalState(state: any): Promise - /** * Set the state * @param state state to set @@ -1545,12 +1497,6 @@ async setFlowUserState(key: string, value: any, errorIfNotPossible?: boolean): P */ async getFlowUserState(key: string, errorIfNotPossible?: boolean): Promise -/** - * Get the internal state - * @deprecated use getState instead - */ -async getInternalState(): Promise - /** * Get the state shared across executions * @param path Optional state resource path override. Defaults to \`getStatePath()\`. @@ -1686,15 +1632,6 @@ async getResumeUrls(approver?: string, flowLevel?: boolean): Promise<{ cancel: string; }> -/** - * @deprecated use getResumeUrls instead - */ -getResumeEndpoints(approver?: string): Promise<{ - approvalPage: string; - resume: string; - cancel: string; -}> - /** * Get an OIDC jwt token for auth to external services (e.g: Vault, AWS) (ee only) * @param audience audience of the token @@ -1717,15 +1654,6 @@ base64ToUint8Array(data: string): Uint8Array */ uint8ArrayToBase64(arrayBuffer: Uint8Array): string -/** - * Get email from workspace username - * This method is particularly useful for apps that require the email address of the viewer. - * Indeed, in the viewer context, WM_USERNAME is set to the username of the viewer but WM_EMAIL is set to the email of the creator of the app. - * @param username - * @returns email address - */ -async usernameToEmail(username: string): Promise - /** * Sends an interactive approval request via Slack, allowing optional customization of the message, approver, and form fields. * @@ -2241,6 +2169,11 @@ const result: wmill.S3Object = await wmill.writeS3File( Import: import * as wmill from 'windmill-client' +To know who is running the script, read the contextual variables rather than calling the API: +\`process.env.WM_END_USER_EMAIL || process.env.WM_EMAIL\`. WM_END_USER_EMAIL is the app viewer when +the run was triggered from an app and empty otherwise (both variables are always defined), WM_EMAIL +is the user the job is permissioned as. WM_USERNAME is the matching username. + workerHasInternalServer(): boolean /** @@ -2271,11 +2204,6 @@ async getResource(path?: string, undefinedIfEmpty?: boolean): Promise */ async getRootJobId(jobId?: string): Promise -/** - * @deprecated Use runScriptByPath or runScriptByHash instead - */ -async runScript(path: string | null = null, hash_: string | null = null, args: Record | null = null, verbose: boolean = false, tag: string | null = null): Promise - /** * Run a script synchronously by its path and wait for the result * @param path - Script path in Windmill @@ -2340,11 +2268,6 @@ async getResult(jobId: string): Promise */ async getResultMaybe(jobId: string): Promise -/** - * @deprecated Use runScriptByPathAsync or runScriptByHashAsync instead - */ -async runScriptAsync(path: string | null, hash_: string | null, args: Record | null, scheduledInSeconds: number | null = null, tag: string | null = null): Promise - /** * Run a script asynchronously by its path * @param path - Script path in Windmill @@ -2397,13 +2320,6 @@ getStatePath(): string */ async setResource(value: any, path?: string, initializeToTypeIfNotExist?: string): Promise -/** - * Set the state - * @param state state to set - * @deprecated use setState instead - */ -async setInternalState(state: any): Promise - /** * Set the state * @param state state to set @@ -2439,12 +2355,6 @@ async setFlowUserState(key: string, value: any, errorIfNotPossible?: boolean): P */ async getFlowUserState(key: string, errorIfNotPossible?: boolean): Promise -/** - * Get the internal state - * @deprecated use getState instead - */ -async getInternalState(): Promise - /** * Get the state shared across executions * @param path Optional state resource path override. Defaults to \`getStatePath()\`. @@ -2580,15 +2490,6 @@ async getResumeUrls(approver?: string, flowLevel?: boolean): Promise<{ cancel: string; }> -/** - * @deprecated use getResumeUrls instead - */ -getResumeEndpoints(approver?: string): Promise<{ - approvalPage: string; - resume: string; - cancel: string; -}> - /** * Get an OIDC jwt token for auth to external services (e.g: Vault, AWS) (ee only) * @param audience audience of the token @@ -2611,15 +2512,6 @@ base64ToUint8Array(data: string): Uint8Array */ uint8ArrayToBase64(arrayBuffer: Uint8Array): string -/** - * Get email from workspace username - * This method is particularly useful for apps that require the email address of the viewer. - * Indeed, in the viewer context, WM_USERNAME is set to the username of the viewer but WM_EMAIL is set to the email of the creator of the app. - * @param username - * @returns email address - */ -async usernameToEmail(username: string): Promise - /** * Sends an interactive approval request via Slack, allowing optional customization of the message, approver, and form fields. * @@ -3965,6 +3857,11 @@ result: S3Object = wmill.write_s3_file( Import: import wmill +To know who is running the script, read the contextual variables rather than calling the API: +\`os.environ.get("WM_END_USER_EMAIL") or os.environ.get("WM_EMAIL")\`. WM_END_USER_EMAIL is the app +viewer when the run was triggered from an app and empty otherwise (both variables are always +defined), WM_EMAIL is the user the job is permissioned as. WM_USERNAME is the matching username. + def worker_has_internal_server() -> bool def get_mocked_api() -> Optional[dict] @@ -4006,11 +3903,6 @@ def post(endpoint, raise_for_status = True, **kwargs) -> httpx.Response # New authentication token string def create_token(duration = dt.timedelta(days=1)) -> str -# Create a script job and return its job id. -# -# .. deprecated:: Use run_script_by_path_async or run_script_by_hash_async instead. -def run_script_async(path: str = None, hash_: str = None, args: dict = None, scheduled_in_secs: int = None, tag: str = None) -> str - # Create a script job by path and return its job id. def run_script_by_path_async(path: str, args: dict = None, scheduled_in_secs: int = None, tag: str = None) -> str @@ -4020,11 +3912,6 @@ def run_script_by_hash_async(hash_: str, args: dict = None, scheduled_in_secs: i # Create a flow job and return its job id. def run_flow_async(path: str, args: dict = None, scheduled_in_secs: int = None, do_not_track_in_parent: bool = True, tag: str = None) -> str -# Run script synchronously and return its result. -# -# .. deprecated:: Use run_script_by_path or run_script_by_hash instead. -def run_script(path: str = None, hash_: str = None, args: dict = None, timeout: dt.timedelta | int | float | None = None, verbose: bool = False, cleanup: bool = True, assert_result_is_not_none: bool = False, tag: str = None) -> Any - # Run script by path synchronously and return its result. def run_script_by_path(path: str, args: dict = None, timeout: dt.timedelta | int | float | None = None, verbose: bool = False, cleanup: bool = True, assert_result_is_not_none: bool = False, tag: str = None) -> Any @@ -4411,11 +4298,6 @@ def get_approval_urls(step_key: str = 'approval', approver: str = None) -> dict # - The function checks for required environment variables (\`WM_FLOW_JOB_ID\`, \`WM_FLOW_STEP_ID\`) to ensure it is run in the appropriate context. def request_interactive_slack_approval(slack_resource_path: str, channel_id: str, message: str = None, approver: str = None, default_args_json: dict = None, dynamic_enums_json: dict = None) -> None -# Get email from workspace username -# This method is particularly useful for apps that require the email address of the viewer. -# Indeed, in the viewer context WM_USERNAME is set to the username of the viewer but WM_EMAIL is set to the email of the creator of the app. -def username_to_email(username: str) -> str - # Send a message to a Microsoft Teams conversation with conversation_id, where success is used to style the message def send_teams_message(conversation_id: str, text: str, success: bool = True, card_block: dict = None) @@ -4449,6 +4331,18 @@ def get_workspace() -> str def get_version() -> str +# Create a script job and return its job ID. +# +# Args: +# hash_or_path: Script hash or path (determined by presence of '/') +# args: Script arguments +# scheduled_in_secs: Delay before execution in seconds +# tag: Override the worker tag the job runs on +# +# Returns: +# Job ID string +def run_script_async(hash_or_path: str, args: Dict[str, Any] = None, scheduled_in_secs: int = None, tag: str = None) -> str + # Run a script synchronously by hash and return its result. # # Args: diff --git a/python-client/wmill/wmill/client.py b/python-client/wmill/wmill/client.py index 01589d4ef7..9400dabdc7 100644 --- a/python-client/wmill/wmill/client.py +++ b/python-client/wmill/wmill/client.py @@ -1377,8 +1377,14 @@ class Windmill: def username_to_email(self, username: str) -> str: """ Get email from workspace username - This method is particularly useful for apps that require the email address of the viewer. - Indeed, in the viewer context WM_USERNAME is set to the username of the viewer but WM_EMAIL is set to the email of the creator of the app. + + .. deprecated:: Read the contextual variables instead: + `os.environ.get("WM_END_USER_EMAIL") or os.environ.get("WM_EMAIL")`. + WM_END_USER_EMAIL is the email of whoever triggered the run when it came from an app, so + the fallback yields the app viewer inside an app and the executing user everywhere else - + without an extra API call, and unlike this method it also resolves viewers who are not + workspace members. An app viewed anonymously has no identity to report: the variable is + then empty and the fallback yields the app publisher. """ return self.get(f"/w/{self.workspace}/users/username_to_email/{username}").text @@ -2221,8 +2227,14 @@ def run_inline_script_preview( def username_to_email(username: str) -> str: """ Get email from workspace username - This method is particularly useful for apps that require the email address of the viewer. - Indeed, in the viewer context WM_USERNAME is set to the username of the viewer but WM_EMAIL is set to the email of the creator of the app. + + .. deprecated:: Read the contextual variables instead: + `os.environ.get("WM_END_USER_EMAIL") or os.environ.get("WM_EMAIL")`. + WM_END_USER_EMAIL is the email of whoever triggered the run when it came from an app, so the + fallback yields the app viewer inside an app and the executing user everywhere else - without + an extra API call, and unlike this function it also resolves viewers who are not workspace + members. An app viewed anonymously has no identity to report: the variable is then empty and + the fallback yields the app publisher. """ return _client.username_to_email(username) diff --git a/system_prompts/auto-generated/prompts.ts b/system_prompts/auto-generated/prompts.ts index dec67ba4cb..affd2b0ce7 100644 --- a/system_prompts/auto-generated/prompts.ts +++ b/system_prompts/auto-generated/prompts.ts @@ -1121,6 +1121,11 @@ export const SDK_TYPESCRIPT = `# TypeScript SDK (windmill-client) Import: import * as wmill from 'windmill-client' +To know who is running the script, read the contextual variables rather than calling the API: +\`process.env.WM_END_USER_EMAIL || process.env.WM_EMAIL\`. WM_END_USER_EMAIL is the app viewer when +the run was triggered from an app and empty otherwise (both variables are always defined), WM_EMAIL +is the user the job is permissioned as. WM_USERNAME is the matching username. + workerHasInternalServer(): boolean /** @@ -1151,11 +1156,6 @@ async getResource(path?: string, undefinedIfEmpty?: boolean): Promise */ async getRootJobId(jobId?: string): Promise -/** - * @deprecated Use runScriptByPath or runScriptByHash instead - */ -async runScript(path: string | null = null, hash_: string | null = null, args: Record | null = null, verbose: boolean = false, tag: string | null = null): Promise - /** * Run a script synchronously by its path and wait for the result * @param path - Script path in Windmill @@ -1220,11 +1220,6 @@ async getResult(jobId: string): Promise */ async getResultMaybe(jobId: string): Promise -/** - * @deprecated Use runScriptByPathAsync or runScriptByHashAsync instead - */ -async runScriptAsync(path: string | null, hash_: string | null, args: Record | null, scheduledInSeconds: number | null = null, tag: string | null = null): Promise - /** * Run a script asynchronously by its path * @param path - Script path in Windmill @@ -1277,13 +1272,6 @@ getStatePath(): string */ async setResource(value: any, path?: string, initializeToTypeIfNotExist?: string): Promise -/** - * Set the state - * @param state state to set - * @deprecated use setState instead - */ -async setInternalState(state: any): Promise - /** * Set the state * @param state state to set @@ -1319,12 +1307,6 @@ async setFlowUserState(key: string, value: any, errorIfNotPossible?: boolean): P */ async getFlowUserState(key: string, errorIfNotPossible?: boolean): Promise -/** - * Get the internal state - * @deprecated use getState instead - */ -async getInternalState(): Promise - /** * Get the state shared across executions * @param path Optional state resource path override. Defaults to \`getStatePath()\`. @@ -1460,15 +1442,6 @@ async getResumeUrls(approver?: string, flowLevel?: boolean): Promise<{ cancel: string; }> -/** - * @deprecated use getResumeUrls instead - */ -getResumeEndpoints(approver?: string): Promise<{ - approvalPage: string; - resume: string; - cancel: string; -}> - /** * Get an OIDC jwt token for auth to external services (e.g: Vault, AWS) (ee only) * @param audience audience of the token @@ -1491,15 +1464,6 @@ base64ToUint8Array(data: string): Uint8Array */ uint8ArrayToBase64(arrayBuffer: Uint8Array): string -/** - * Get email from workspace username - * This method is particularly useful for apps that require the email address of the viewer. - * Indeed, in the viewer context, WM_USERNAME is set to the username of the viewer but WM_EMAIL is set to the email of the creator of the app. - * @param username - * @returns email address - */ -async usernameToEmail(username: string): Promise - /** * Sends an interactive approval request via Slack, allowing optional customization of the message, approver, and form fields. * @@ -1751,6 +1715,11 @@ export const SDK_PYTHON = `# Python SDK (wmill) Import: import wmill +To know who is running the script, read the contextual variables rather than calling the API: +\`os.environ.get("WM_END_USER_EMAIL") or os.environ.get("WM_EMAIL")\`. WM_END_USER_EMAIL is the app +viewer when the run was triggered from an app and empty otherwise (both variables are always +defined), WM_EMAIL is the user the job is permissioned as. WM_USERNAME is the matching username. + def worker_has_internal_server() -> bool def get_mocked_api() -> Optional[dict] @@ -1792,11 +1761,6 @@ def post(endpoint, raise_for_status = True, **kwargs) -> httpx.Response # New authentication token string def create_token(duration = dt.timedelta(days=1)) -> str -# Create a script job and return its job id. -# -# .. deprecated:: Use run_script_by_path_async or run_script_by_hash_async instead. -def run_script_async(path: str = None, hash_: str = None, args: dict = None, scheduled_in_secs: int = None, tag: str = None) -> str - # Create a script job by path and return its job id. def run_script_by_path_async(path: str, args: dict = None, scheduled_in_secs: int = None, tag: str = None) -> str @@ -1806,11 +1770,6 @@ def run_script_by_hash_async(hash_: str, args: dict = None, scheduled_in_secs: i # Create a flow job and return its job id. def run_flow_async(path: str, args: dict = None, scheduled_in_secs: int = None, do_not_track_in_parent: bool = True, tag: str = None) -> str -# Run script synchronously and return its result. -# -# .. deprecated:: Use run_script_by_path or run_script_by_hash instead. -def run_script(path: str = None, hash_: str = None, args: dict = None, timeout: dt.timedelta | int | float | None = None, verbose: bool = False, cleanup: bool = True, assert_result_is_not_none: bool = False, tag: str = None) -> Any - # Run script by path synchronously and return its result. def run_script_by_path(path: str, args: dict = None, timeout: dt.timedelta | int | float | None = None, verbose: bool = False, cleanup: bool = True, assert_result_is_not_none: bool = False, tag: str = None) -> Any @@ -2197,11 +2156,6 @@ def get_approval_urls(step_key: str = 'approval', approver: str = None) -> dict # - The function checks for required environment variables (\`WM_FLOW_JOB_ID\`, \`WM_FLOW_STEP_ID\`) to ensure it is run in the appropriate context. def request_interactive_slack_approval(slack_resource_path: str, channel_id: str, message: str = None, approver: str = None, default_args_json: dict = None, dynamic_enums_json: dict = None) -> None -# Get email from workspace username -# This method is particularly useful for apps that require the email address of the viewer. -# Indeed, in the viewer context WM_USERNAME is set to the username of the viewer but WM_EMAIL is set to the email of the creator of the app. -def username_to_email(username: str) -> str - # Send a message to a Microsoft Teams conversation with conversation_id, where success is used to style the message def send_teams_message(conversation_id: str, text: str, success: bool = True, card_block: dict = None) @@ -2235,6 +2189,18 @@ def get_workspace() -> str def get_version() -> str +# Create a script job and return its job ID. +# +# Args: +# hash_or_path: Script hash or path (determined by presence of '/') +# args: Script arguments +# scheduled_in_secs: Delay before execution in seconds +# tag: Override the worker tag the job runs on +# +# Returns: +# Job ID string +def run_script_async(hash_or_path: str, args: Dict[str, Any] = None, scheduled_in_secs: int = None, tag: str = None) -> str + # Run a script synchronously by hash and return its result. # # Args: diff --git a/system_prompts/auto-generated/script.md b/system_prompts/auto-generated/script.md index a70b5d08b4..dd1fe07ac5 100644 --- a/system_prompts/auto-generated/script.md +++ b/system_prompts/auto-generated/script.md @@ -1437,6 +1437,11 @@ being buffered, bypassing the 10000-row return cap. Import: import * as wmill from 'windmill-client' +To know who is running the script, read the contextual variables rather than calling the API: +`process.env.WM_END_USER_EMAIL || process.env.WM_EMAIL`. WM_END_USER_EMAIL is the app viewer when +the run was triggered from an app and empty otherwise (both variables are always defined), WM_EMAIL +is the user the job is permissioned as. WM_USERNAME is the matching username. + workerHasInternalServer(): boolean /** @@ -1467,11 +1472,6 @@ async getResource(path?: string, undefinedIfEmpty?: boolean): Promise */ async getRootJobId(jobId?: string): Promise -/** - * @deprecated Use runScriptByPath or runScriptByHash instead - */ -async runScript(path: string | null = null, hash_: string | null = null, args: Record | null = null, verbose: boolean = false, tag: string | null = null): Promise - /** * Run a script synchronously by its path and wait for the result * @param path - Script path in Windmill @@ -1536,11 +1536,6 @@ async getResult(jobId: string): Promise */ async getResultMaybe(jobId: string): Promise -/** - * @deprecated Use runScriptByPathAsync or runScriptByHashAsync instead - */ -async runScriptAsync(path: string | null, hash_: string | null, args: Record | null, scheduledInSeconds: number | null = null, tag: string | null = null): Promise - /** * Run a script asynchronously by its path * @param path - Script path in Windmill @@ -1593,13 +1588,6 @@ getStatePath(): string */ async setResource(value: any, path?: string, initializeToTypeIfNotExist?: string): Promise -/** - * Set the state - * @param state state to set - * @deprecated use setState instead - */ -async setInternalState(state: any): Promise - /** * Set the state * @param state state to set @@ -1635,12 +1623,6 @@ async setFlowUserState(key: string, value: any, errorIfNotPossible?: boolean): P */ async getFlowUserState(key: string, errorIfNotPossible?: boolean): Promise -/** - * Get the internal state - * @deprecated use getState instead - */ -async getInternalState(): Promise - /** * Get the state shared across executions * @param path Optional state resource path override. Defaults to `getStatePath()`. @@ -1776,15 +1758,6 @@ async getResumeUrls(approver?: string, flowLevel?: boolean): Promise<{ cancel: string; }> -/** - * @deprecated use getResumeUrls instead - */ -getResumeEndpoints(approver?: string): Promise<{ - approvalPage: string; - resume: string; - cancel: string; -}> - /** * Get an OIDC jwt token for auth to external services (e.g: Vault, AWS) (ee only) * @param audience audience of the token @@ -1807,15 +1780,6 @@ base64ToUint8Array(data: string): Uint8Array */ uint8ArrayToBase64(arrayBuffer: Uint8Array): string -/** - * Get email from workspace username - * This method is particularly useful for apps that require the email address of the viewer. - * Indeed, in the viewer context, WM_USERNAME is set to the username of the viewer but WM_EMAIL is set to the email of the creator of the app. - * @param username - * @returns email address - */ -async usernameToEmail(username: string): Promise - /** * Sends an interactive approval request via Slack, allowing optional customization of the message, approver, and form fields. * @@ -2067,6 +2031,11 @@ appendPartition(opts: Omit,): SqlStatem Import: import wmill +To know who is running the script, read the contextual variables rather than calling the API: +`os.environ.get("WM_END_USER_EMAIL") or os.environ.get("WM_EMAIL")`. WM_END_USER_EMAIL is the app +viewer when the run was triggered from an app and empty otherwise (both variables are always +defined), WM_EMAIL is the user the job is permissioned as. WM_USERNAME is the matching username. + def worker_has_internal_server() -> bool def get_mocked_api() -> Optional[dict] @@ -2108,11 +2077,6 @@ def post(endpoint, raise_for_status = True, **kwargs) -> httpx.Response # New authentication token string def create_token(duration = dt.timedelta(days=1)) -> str -# Create a script job and return its job id. -# -# .. deprecated:: Use run_script_by_path_async or run_script_by_hash_async instead. -def run_script_async(path: str = None, hash_: str = None, args: dict = None, scheduled_in_secs: int = None, tag: str = None) -> str - # Create a script job by path and return its job id. def run_script_by_path_async(path: str, args: dict = None, scheduled_in_secs: int = None, tag: str = None) -> str @@ -2122,11 +2086,6 @@ def run_script_by_hash_async(hash_: str, args: dict = None, scheduled_in_secs: i # Create a flow job and return its job id. def run_flow_async(path: str, args: dict = None, scheduled_in_secs: int = None, do_not_track_in_parent: bool = True, tag: str = None) -> str -# Run script synchronously and return its result. -# -# .. deprecated:: Use run_script_by_path or run_script_by_hash instead. -def run_script(path: str = None, hash_: str = None, args: dict = None, timeout: dt.timedelta | int | float | None = None, verbose: bool = False, cleanup: bool = True, assert_result_is_not_none: bool = False, tag: str = None) -> Any - # Run script by path synchronously and return its result. def run_script_by_path(path: str, args: dict = None, timeout: dt.timedelta | int | float | None = None, verbose: bool = False, cleanup: bool = True, assert_result_is_not_none: bool = False, tag: str = None) -> Any @@ -2513,11 +2472,6 @@ def get_approval_urls(step_key: str = 'approval', approver: str = None) -> dict # - The function checks for required environment variables (`WM_FLOW_JOB_ID`, `WM_FLOW_STEP_ID`) to ensure it is run in the appropriate context. def request_interactive_slack_approval(slack_resource_path: str, channel_id: str, message: str = None, approver: str = None, default_args_json: dict = None, dynamic_enums_json: dict = None) -> None -# Get email from workspace username -# This method is particularly useful for apps that require the email address of the viewer. -# Indeed, in the viewer context WM_USERNAME is set to the username of the viewer but WM_EMAIL is set to the email of the creator of the app. -def username_to_email(username: str) -> str - # Send a message to a Microsoft Teams conversation with conversation_id, where success is used to style the message def send_teams_message(conversation_id: str, text: str, success: bool = True, card_block: dict = None) @@ -2551,6 +2505,18 @@ def get_workspace() -> str def get_version() -> str +# Create a script job and return its job ID. +# +# Args: +# hash_or_path: Script hash or path (determined by presence of '/') +# args: Script arguments +# scheduled_in_secs: Delay before execution in seconds +# tag: Override the worker tag the job runs on +# +# Returns: +# Job ID string +def run_script_async(hash_or_path: str, args: Dict[str, Any] = None, scheduled_in_secs: int = None, tag: str = None) -> str + # Run a script synchronously by hash and return its result. # # Args: diff --git a/system_prompts/auto-generated/sdks/python.md b/system_prompts/auto-generated/sdks/python.md index ae13ae91a9..a80e9a57dd 100644 --- a/system_prompts/auto-generated/sdks/python.md +++ b/system_prompts/auto-generated/sdks/python.md @@ -2,6 +2,11 @@ Import: import wmill +To know who is running the script, read the contextual variables rather than calling the API: +`os.environ.get("WM_END_USER_EMAIL") or os.environ.get("WM_EMAIL")`. WM_END_USER_EMAIL is the app +viewer when the run was triggered from an app and empty otherwise (both variables are always +defined), WM_EMAIL is the user the job is permissioned as. WM_USERNAME is the matching username. + def worker_has_internal_server() -> bool def get_mocked_api() -> Optional[dict] @@ -43,11 +48,6 @@ def post(endpoint, raise_for_status = True, **kwargs) -> httpx.Response # New authentication token string def create_token(duration = dt.timedelta(days=1)) -> str -# Create a script job and return its job id. -# -# .. deprecated:: Use run_script_by_path_async or run_script_by_hash_async instead. -def run_script_async(path: str = None, hash_: str = None, args: dict = None, scheduled_in_secs: int = None, tag: str = None) -> str - # Create a script job by path and return its job id. def run_script_by_path_async(path: str, args: dict = None, scheduled_in_secs: int = None, tag: str = None) -> str @@ -57,11 +57,6 @@ def run_script_by_hash_async(hash_: str, args: dict = None, scheduled_in_secs: i # Create a flow job and return its job id. def run_flow_async(path: str, args: dict = None, scheduled_in_secs: int = None, do_not_track_in_parent: bool = True, tag: str = None) -> str -# Run script synchronously and return its result. -# -# .. deprecated:: Use run_script_by_path or run_script_by_hash instead. -def run_script(path: str = None, hash_: str = None, args: dict = None, timeout: dt.timedelta | int | float | None = None, verbose: bool = False, cleanup: bool = True, assert_result_is_not_none: bool = False, tag: str = None) -> Any - # Run script by path synchronously and return its result. def run_script_by_path(path: str, args: dict = None, timeout: dt.timedelta | int | float | None = None, verbose: bool = False, cleanup: bool = True, assert_result_is_not_none: bool = False, tag: str = None) -> Any @@ -448,11 +443,6 @@ def get_approval_urls(step_key: str = 'approval', approver: str = None) -> dict # - The function checks for required environment variables (`WM_FLOW_JOB_ID`, `WM_FLOW_STEP_ID`) to ensure it is run in the appropriate context. def request_interactive_slack_approval(slack_resource_path: str, channel_id: str, message: str = None, approver: str = None, default_args_json: dict = None, dynamic_enums_json: dict = None) -> None -# Get email from workspace username -# This method is particularly useful for apps that require the email address of the viewer. -# Indeed, in the viewer context WM_USERNAME is set to the username of the viewer but WM_EMAIL is set to the email of the creator of the app. -def username_to_email(username: str) -> str - # Send a message to a Microsoft Teams conversation with conversation_id, where success is used to style the message def send_teams_message(conversation_id: str, text: str, success: bool = True, card_block: dict = None) @@ -486,6 +476,18 @@ def get_workspace() -> str def get_version() -> str +# Create a script job and return its job ID. +# +# Args: +# hash_or_path: Script hash or path (determined by presence of '/') +# args: Script arguments +# scheduled_in_secs: Delay before execution in seconds +# tag: Override the worker tag the job runs on +# +# Returns: +# Job ID string +def run_script_async(hash_or_path: str, args: Dict[str, Any] = None, scheduled_in_secs: int = None, tag: str = None) -> str + # Run a script synchronously by hash and return its result. # # Args: diff --git a/system_prompts/auto-generated/sdks/typescript.md b/system_prompts/auto-generated/sdks/typescript.md index 729e2ce108..56be056d8e 100644 --- a/system_prompts/auto-generated/sdks/typescript.md +++ b/system_prompts/auto-generated/sdks/typescript.md @@ -2,6 +2,11 @@ Import: import * as wmill from 'windmill-client' +To know who is running the script, read the contextual variables rather than calling the API: +`process.env.WM_END_USER_EMAIL || process.env.WM_EMAIL`. WM_END_USER_EMAIL is the app viewer when +the run was triggered from an app and empty otherwise (both variables are always defined), WM_EMAIL +is the user the job is permissioned as. WM_USERNAME is the matching username. + workerHasInternalServer(): boolean /** @@ -32,11 +37,6 @@ async getResource(path?: string, undefinedIfEmpty?: boolean): Promise */ async getRootJobId(jobId?: string): Promise -/** - * @deprecated Use runScriptByPath or runScriptByHash instead - */ -async runScript(path: string | null = null, hash_: string | null = null, args: Record | null = null, verbose: boolean = false, tag: string | null = null): Promise - /** * Run a script synchronously by its path and wait for the result * @param path - Script path in Windmill @@ -101,11 +101,6 @@ async getResult(jobId: string): Promise */ async getResultMaybe(jobId: string): Promise -/** - * @deprecated Use runScriptByPathAsync or runScriptByHashAsync instead - */ -async runScriptAsync(path: string | null, hash_: string | null, args: Record | null, scheduledInSeconds: number | null = null, tag: string | null = null): Promise - /** * Run a script asynchronously by its path * @param path - Script path in Windmill @@ -158,13 +153,6 @@ getStatePath(): string */ async setResource(value: any, path?: string, initializeToTypeIfNotExist?: string): Promise -/** - * Set the state - * @param state state to set - * @deprecated use setState instead - */ -async setInternalState(state: any): Promise - /** * Set the state * @param state state to set @@ -200,12 +188,6 @@ async setFlowUserState(key: string, value: any, errorIfNotPossible?: boolean): P */ async getFlowUserState(key: string, errorIfNotPossible?: boolean): Promise -/** - * Get the internal state - * @deprecated use getState instead - */ -async getInternalState(): Promise - /** * Get the state shared across executions * @param path Optional state resource path override. Defaults to `getStatePath()`. @@ -341,15 +323,6 @@ async getResumeUrls(approver?: string, flowLevel?: boolean): Promise<{ cancel: string; }> -/** - * @deprecated use getResumeUrls instead - */ -getResumeEndpoints(approver?: string): Promise<{ - approvalPage: string; - resume: string; - cancel: string; -}> - /** * Get an OIDC jwt token for auth to external services (e.g: Vault, AWS) (ee only) * @param audience audience of the token @@ -372,15 +345,6 @@ base64ToUint8Array(data: string): Uint8Array */ uint8ArrayToBase64(arrayBuffer: Uint8Array): string -/** - * Get email from workspace username - * This method is particularly useful for apps that require the email address of the viewer. - * Indeed, in the viewer context, WM_USERNAME is set to the username of the viewer but WM_EMAIL is set to the email of the creator of the app. - * @param username - * @returns email address - */ -async usernameToEmail(username: string): Promise - /** * Sends an interactive approval request via Slack, allowing optional customization of the message, approver, and form fields. * diff --git a/system_prompts/auto-generated/skills/write-script-bun/SKILL.md b/system_prompts/auto-generated/skills/write-script-bun/SKILL.md index d7a757085a..75dab4b624 100644 --- a/system_prompts/auto-generated/skills/write-script-bun/SKILL.md +++ b/system_prompts/auto-generated/skills/write-script-bun/SKILL.md @@ -173,6 +173,11 @@ const result: wmill.S3Object = await wmill.writeS3File( Import: import * as wmill from 'windmill-client' +To know who is running the script, read the contextual variables rather than calling the API: +`process.env.WM_END_USER_EMAIL || process.env.WM_EMAIL`. WM_END_USER_EMAIL is the app viewer when +the run was triggered from an app and empty otherwise (both variables are always defined), WM_EMAIL +is the user the job is permissioned as. WM_USERNAME is the matching username. + workerHasInternalServer(): boolean /** @@ -203,11 +208,6 @@ async getResource(path?: string, undefinedIfEmpty?: boolean): Promise */ async getRootJobId(jobId?: string): Promise -/** - * @deprecated Use runScriptByPath or runScriptByHash instead - */ -async runScript(path: string | null = null, hash_: string | null = null, args: Record | null = null, verbose: boolean = false, tag: string | null = null): Promise - /** * Run a script synchronously by its path and wait for the result * @param path - Script path in Windmill @@ -272,11 +272,6 @@ async getResult(jobId: string): Promise */ async getResultMaybe(jobId: string): Promise -/** - * @deprecated Use runScriptByPathAsync or runScriptByHashAsync instead - */ -async runScriptAsync(path: string | null, hash_: string | null, args: Record | null, scheduledInSeconds: number | null = null, tag: string | null = null): Promise - /** * Run a script asynchronously by its path * @param path - Script path in Windmill @@ -329,13 +324,6 @@ getStatePath(): string */ async setResource(value: any, path?: string, initializeToTypeIfNotExist?: string): Promise -/** - * Set the state - * @param state state to set - * @deprecated use setState instead - */ -async setInternalState(state: any): Promise - /** * Set the state * @param state state to set @@ -371,12 +359,6 @@ async setFlowUserState(key: string, value: any, errorIfNotPossible?: boolean): P */ async getFlowUserState(key: string, errorIfNotPossible?: boolean): Promise -/** - * Get the internal state - * @deprecated use getState instead - */ -async getInternalState(): Promise - /** * Get the state shared across executions * @param path Optional state resource path override. Defaults to `getStatePath()`. @@ -512,15 +494,6 @@ async getResumeUrls(approver?: string, flowLevel?: boolean): Promise<{ cancel: string; }> -/** - * @deprecated use getResumeUrls instead - */ -getResumeEndpoints(approver?: string): Promise<{ - approvalPage: string; - resume: string; - cancel: string; -}> - /** * Get an OIDC jwt token for auth to external services (e.g: Vault, AWS) (ee only) * @param audience audience of the token @@ -543,15 +516,6 @@ base64ToUint8Array(data: string): Uint8Array */ uint8ArrayToBase64(arrayBuffer: Uint8Array): string -/** - * Get email from workspace username - * This method is particularly useful for apps that require the email address of the viewer. - * Indeed, in the viewer context, WM_USERNAME is set to the username of the viewer but WM_EMAIL is set to the email of the creator of the app. - * @param username - * @returns email address - */ -async usernameToEmail(username: string): Promise - /** * Sends an interactive approval request via Slack, allowing optional customization of the message, approver, and form fields. * diff --git a/system_prompts/auto-generated/skills/write-script-bunnative/SKILL.md b/system_prompts/auto-generated/skills/write-script-bunnative/SKILL.md index a5a7c02dab..600fed06b2 100644 --- a/system_prompts/auto-generated/skills/write-script-bunnative/SKILL.md +++ b/system_prompts/auto-generated/skills/write-script-bunnative/SKILL.md @@ -173,6 +173,11 @@ const result: wmill.S3Object = await wmill.writeS3File( Import: import * as wmill from 'windmill-client' +To know who is running the script, read the contextual variables rather than calling the API: +`process.env.WM_END_USER_EMAIL || process.env.WM_EMAIL`. WM_END_USER_EMAIL is the app viewer when +the run was triggered from an app and empty otherwise (both variables are always defined), WM_EMAIL +is the user the job is permissioned as. WM_USERNAME is the matching username. + workerHasInternalServer(): boolean /** @@ -203,11 +208,6 @@ async getResource(path?: string, undefinedIfEmpty?: boolean): Promise */ async getRootJobId(jobId?: string): Promise -/** - * @deprecated Use runScriptByPath or runScriptByHash instead - */ -async runScript(path: string | null = null, hash_: string | null = null, args: Record | null = null, verbose: boolean = false, tag: string | null = null): Promise - /** * Run a script synchronously by its path and wait for the result * @param path - Script path in Windmill @@ -272,11 +272,6 @@ async getResult(jobId: string): Promise */ async getResultMaybe(jobId: string): Promise -/** - * @deprecated Use runScriptByPathAsync or runScriptByHashAsync instead - */ -async runScriptAsync(path: string | null, hash_: string | null, args: Record | null, scheduledInSeconds: number | null = null, tag: string | null = null): Promise - /** * Run a script asynchronously by its path * @param path - Script path in Windmill @@ -329,13 +324,6 @@ getStatePath(): string */ async setResource(value: any, path?: string, initializeToTypeIfNotExist?: string): Promise -/** - * Set the state - * @param state state to set - * @deprecated use setState instead - */ -async setInternalState(state: any): Promise - /** * Set the state * @param state state to set @@ -371,12 +359,6 @@ async setFlowUserState(key: string, value: any, errorIfNotPossible?: boolean): P */ async getFlowUserState(key: string, errorIfNotPossible?: boolean): Promise -/** - * Get the internal state - * @deprecated use getState instead - */ -async getInternalState(): Promise - /** * Get the state shared across executions * @param path Optional state resource path override. Defaults to `getStatePath()`. @@ -512,15 +494,6 @@ async getResumeUrls(approver?: string, flowLevel?: boolean): Promise<{ cancel: string; }> -/** - * @deprecated use getResumeUrls instead - */ -getResumeEndpoints(approver?: string): Promise<{ - approvalPage: string; - resume: string; - cancel: string; -}> - /** * Get an OIDC jwt token for auth to external services (e.g: Vault, AWS) (ee only) * @param audience audience of the token @@ -543,15 +516,6 @@ base64ToUint8Array(data: string): Uint8Array */ uint8ArrayToBase64(arrayBuffer: Uint8Array): string -/** - * Get email from workspace username - * This method is particularly useful for apps that require the email address of the viewer. - * Indeed, in the viewer context, WM_USERNAME is set to the username of the viewer but WM_EMAIL is set to the email of the creator of the app. - * @param username - * @returns email address - */ -async usernameToEmail(username: string): Promise - /** * Sends an interactive approval request via Slack, allowing optional customization of the message, approver, and form fields. * diff --git a/system_prompts/auto-generated/skills/write-script-deno/SKILL.md b/system_prompts/auto-generated/skills/write-script-deno/SKILL.md index ea6ebe00aa..2e671f139d 100644 --- a/system_prompts/auto-generated/skills/write-script-deno/SKILL.md +++ b/system_prompts/auto-generated/skills/write-script-deno/SKILL.md @@ -175,6 +175,11 @@ const result: wmill.S3Object = await wmill.writeS3File( Import: import * as wmill from 'windmill-client' +To know who is running the script, read the contextual variables rather than calling the API: +`process.env.WM_END_USER_EMAIL || process.env.WM_EMAIL`. WM_END_USER_EMAIL is the app viewer when +the run was triggered from an app and empty otherwise (both variables are always defined), WM_EMAIL +is the user the job is permissioned as. WM_USERNAME is the matching username. + workerHasInternalServer(): boolean /** @@ -205,11 +210,6 @@ async getResource(path?: string, undefinedIfEmpty?: boolean): Promise */ async getRootJobId(jobId?: string): Promise -/** - * @deprecated Use runScriptByPath or runScriptByHash instead - */ -async runScript(path: string | null = null, hash_: string | null = null, args: Record | null = null, verbose: boolean = false, tag: string | null = null): Promise - /** * Run a script synchronously by its path and wait for the result * @param path - Script path in Windmill @@ -274,11 +274,6 @@ async getResult(jobId: string): Promise */ async getResultMaybe(jobId: string): Promise -/** - * @deprecated Use runScriptByPathAsync or runScriptByHashAsync instead - */ -async runScriptAsync(path: string | null, hash_: string | null, args: Record | null, scheduledInSeconds: number | null = null, tag: string | null = null): Promise - /** * Run a script asynchronously by its path * @param path - Script path in Windmill @@ -331,13 +326,6 @@ getStatePath(): string */ async setResource(value: any, path?: string, initializeToTypeIfNotExist?: string): Promise -/** - * Set the state - * @param state state to set - * @deprecated use setState instead - */ -async setInternalState(state: any): Promise - /** * Set the state * @param state state to set @@ -373,12 +361,6 @@ async setFlowUserState(key: string, value: any, errorIfNotPossible?: boolean): P */ async getFlowUserState(key: string, errorIfNotPossible?: boolean): Promise -/** - * Get the internal state - * @deprecated use getState instead - */ -async getInternalState(): Promise - /** * Get the state shared across executions * @param path Optional state resource path override. Defaults to `getStatePath()`. @@ -514,15 +496,6 @@ async getResumeUrls(approver?: string, flowLevel?: boolean): Promise<{ cancel: string; }> -/** - * @deprecated use getResumeUrls instead - */ -getResumeEndpoints(approver?: string): Promise<{ - approvalPage: string; - resume: string; - cancel: string; -}> - /** * Get an OIDC jwt token for auth to external services (e.g: Vault, AWS) (ee only) * @param audience audience of the token @@ -545,15 +518,6 @@ base64ToUint8Array(data: string): Uint8Array */ uint8ArrayToBase64(arrayBuffer: Uint8Array): string -/** - * Get email from workspace username - * This method is particularly useful for apps that require the email address of the viewer. - * Indeed, in the viewer context, WM_USERNAME is set to the username of the viewer but WM_EMAIL is set to the email of the creator of the app. - * @param username - * @returns email address - */ -async usernameToEmail(username: string): Promise - /** * Sends an interactive approval request via Slack, allowing optional customization of the message, approver, and form fields. * diff --git a/system_prompts/auto-generated/skills/write-script-python3/SKILL.md b/system_prompts/auto-generated/skills/write-script-python3/SKILL.md index b2b03b4f98..212a1965fd 100644 --- a/system_prompts/auto-generated/skills/write-script-python3/SKILL.md +++ b/system_prompts/auto-generated/skills/write-script-python3/SKILL.md @@ -187,6 +187,11 @@ result: S3Object = wmill.write_s3_file( Import: import wmill +To know who is running the script, read the contextual variables rather than calling the API: +`os.environ.get("WM_END_USER_EMAIL") or os.environ.get("WM_EMAIL")`. WM_END_USER_EMAIL is the app +viewer when the run was triggered from an app and empty otherwise (both variables are always +defined), WM_EMAIL is the user the job is permissioned as. WM_USERNAME is the matching username. + def worker_has_internal_server() -> bool def get_mocked_api() -> Optional[dict] @@ -228,11 +233,6 @@ def post(endpoint, raise_for_status = True, **kwargs) -> httpx.Response # New authentication token string def create_token(duration = dt.timedelta(days=1)) -> str -# Create a script job and return its job id. -# -# .. deprecated:: Use run_script_by_path_async or run_script_by_hash_async instead. -def run_script_async(path: str = None, hash_: str = None, args: dict = None, scheduled_in_secs: int = None, tag: str = None) -> str - # Create a script job by path and return its job id. def run_script_by_path_async(path: str, args: dict = None, scheduled_in_secs: int = None, tag: str = None) -> str @@ -242,11 +242,6 @@ def run_script_by_hash_async(hash_: str, args: dict = None, scheduled_in_secs: i # Create a flow job and return its job id. def run_flow_async(path: str, args: dict = None, scheduled_in_secs: int = None, do_not_track_in_parent: bool = True, tag: str = None) -> str -# Run script synchronously and return its result. -# -# .. deprecated:: Use run_script_by_path or run_script_by_hash instead. -def run_script(path: str = None, hash_: str = None, args: dict = None, timeout: dt.timedelta | int | float | None = None, verbose: bool = False, cleanup: bool = True, assert_result_is_not_none: bool = False, tag: str = None) -> Any - # Run script by path synchronously and return its result. def run_script_by_path(path: str, args: dict = None, timeout: dt.timedelta | int | float | None = None, verbose: bool = False, cleanup: bool = True, assert_result_is_not_none: bool = False, tag: str = None) -> Any @@ -633,11 +628,6 @@ def get_approval_urls(step_key: str = 'approval', approver: str = None) -> dict # - The function checks for required environment variables (`WM_FLOW_JOB_ID`, `WM_FLOW_STEP_ID`) to ensure it is run in the appropriate context. def request_interactive_slack_approval(slack_resource_path: str, channel_id: str, message: str = None, approver: str = None, default_args_json: dict = None, dynamic_enums_json: dict = None) -> None -# Get email from workspace username -# This method is particularly useful for apps that require the email address of the viewer. -# Indeed, in the viewer context WM_USERNAME is set to the username of the viewer but WM_EMAIL is set to the email of the creator of the app. -def username_to_email(username: str) -> str - # Send a message to a Microsoft Teams conversation with conversation_id, where success is used to style the message def send_teams_message(conversation_id: str, text: str, success: bool = True, card_block: dict = None) @@ -671,6 +661,18 @@ def get_workspace() -> str def get_version() -> str +# Create a script job and return its job ID. +# +# Args: +# hash_or_path: Script hash or path (determined by presence of '/') +# args: Script arguments +# scheduled_in_secs: Delay before execution in seconds +# tag: Override the worker tag the job runs on +# +# Returns: +# Job ID string +def run_script_async(hash_or_path: str, args: Dict[str, Any] = None, scheduled_in_secs: int = None, tag: str = None) -> str + # Run a script synchronously by hash and return its result. # # Args: diff --git a/system_prompts/generate.py b/system_prompts/generate.py index 74e0cf35a2..16436370a5 100644 --- a/system_prompts/generate.py +++ b/system_prompts/generate.py @@ -110,7 +110,9 @@ def extract_ts_functions(content: str) -> list[dict]: # `@internal` marks an export that exists for another module or for a # test to reach, not for a user to call. The SDK reference these prompts # become is a user-facing API list, so it must not advertise them. - if jsdoc_raw and '@internal' in jsdoc_raw: + # `@deprecated` exports stay callable for existing scripts but must not be + # suggested for new ones. + if jsdoc_raw and ('@internal' in jsdoc_raw or '@deprecated' in jsdoc_raw): continue docstring = clean_jsdoc(jsdoc_raw) if jsdoc_raw else '' @@ -190,6 +192,12 @@ def extract_py_functions(content: str) -> list[dict]: # Get docstring docstring = ast.get_docstring(node) or '' + # Same rule as the TypeScript SDK: a deprecated member stays callable for existing + # scripts but must not be suggested for new ones. The Python SDK marks them with the + # Sphinx `.. deprecated::` directive. + if '.. deprecated::' in docstring: + return + # Build parameter list params = [] args = node.args @@ -705,10 +713,24 @@ def generate_cli_commands_markdown(cli_data: dict) -> str: return md +# Who is running the script is answered by contextual variables, not by an SDK call, so the +# SDK reference has to say so: it is where an agent looks for a `usernameToEmail`-style helper. +IDENTITY_OF_THE_RUN_TS = """To know who is running the script, read the contextual variables rather than calling the API: +`process.env.WM_END_USER_EMAIL || process.env.WM_EMAIL`. WM_END_USER_EMAIL is the app viewer when +the run was triggered from an app and empty otherwise (both variables are always defined), WM_EMAIL +is the user the job is permissioned as. WM_USERNAME is the matching username.""" + +IDENTITY_OF_THE_RUN_PY = """To know who is running the script, read the contextual variables rather than calling the API: +`os.environ.get("WM_END_USER_EMAIL") or os.environ.get("WM_EMAIL")`. WM_END_USER_EMAIL is the app +viewer when the run was triggered from an app and empty otherwise (both variables are always +defined), WM_EMAIL is the user the job is permissioned as. WM_USERNAME is the matching username.""" + + def generate_ts_sdk_markdown(functions: list[dict], _types: list[dict]) -> str: """Generate compact documentation for TypeScript SDK.""" md = "# TypeScript SDK (windmill-client)\n\n" md += "Import: import * as wmill from 'windmill-client'\n\n" + md += IDENTITY_OF_THE_RUN_TS + "\n\n" for i, func in enumerate(functions): if func.get('docstring'): @@ -731,6 +753,7 @@ def generate_py_sdk_markdown(functions: list[dict], _classes: list[dict]) -> str """Generate compact documentation for Python SDK.""" md = "# Python SDK (wmill)\n\n" md += "Import: import wmill\n\n" + md += IDENTITY_OF_THE_RUN_PY + "\n\n" for func in functions: # Skip private functions diff --git a/typescript-client/client.ts b/typescript-client/client.ts index 3c7159fb94..f9d25f8799 100644 --- a/typescript-client/client.ts +++ b/typescript-client/client.ts @@ -1226,8 +1226,11 @@ export function uint8ArrayToBase64(arrayBuffer: Uint8Array): string { /** * Get email from workspace username - * This method is particularly useful for apps that require the email address of the viewer. - * Indeed, in the viewer context, WM_USERNAME is set to the username of the viewer but WM_EMAIL is set to the email of the creator of the app. + * @deprecated Read the contextual variables instead: `process.env.WM_END_USER_EMAIL || process.env.WM_EMAIL`. + * WM_END_USER_EMAIL is the email of whoever triggered the run when it came from an app, so the fallback + * yields the app viewer inside an app and the executing user everywhere else - without an extra API call, + * and unlike this function it also resolves viewers who are not workspace members. An app viewed + * anonymously has no identity to report: the variable is then empty and the fallback yields the publisher. * @param username * @returns email address */