From a3f637dd1f04f362758f462a0ada923d7d7850e4 Mon Sep 17 00:00:00 2001 From: dinex-dev Date: Fri, 17 Jul 2026 13:37:32 +0530 Subject: [PATCH 1/4] =?UTF-8?q?feat(security):=20RQ-2426=20=E2=80=94=20Dev?= =?UTF-8?q?eloper=20Script=20Mode=20toggle=20+=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Web UI + docs for the desktop "Developer Script Mode" (safe QuickJS sandbox by default vs opt-in full-access for programmatic code rules). - DesktopSettings: DevScriptMode toggle (off by default, confirm dialog on enable), reads/writes the devScriptMode pref over IPC. - Gated by a new FEATURES.DEVELOPER_SCRIPT_MODE, pinned in compatibility.js to a fail-closed placeholder (99.99.99) — replace with the RQ-2426 desktop release so the toggle only shows on compatible builds. - Docs: new interceptor/desktop-app/developer-script-mode page + nav entry, and cross-links from Modify Request/Response Body and Shared State pages. Co-Authored-By: Claude Opus 4.8 --- app/src/config/constants/compatibility.js | 8 ++ app/src/config/constants/sub/features.js | 5 ++ .../DesktopSettings/DevScriptMode/index.tsx | 84 +++++++++++++++++++ .../components/DesktopSettings/index.jsx | 2 + documentation/docs.json | 14 +--- .../advanced-usage/shared-state.mdx | 6 ++ .../rule-types/modify-request-body.mdx | 7 ++ .../rule-types/modify-response-body.mdx | 7 ++ .../images/developer-script-mode/.gitkeep | 0 .../desktop-app/developer-script-mode.mdx | 82 ++++++++++++++++++ 10 files changed, 205 insertions(+), 10 deletions(-) create mode 100644 app/src/features/settings/components/DesktopSettings/DevScriptMode/index.tsx create mode 100644 documentation/images/developer-script-mode/.gitkeep create mode 100644 documentation/interceptor/desktop-app/developer-script-mode.mdx diff --git a/app/src/config/constants/compatibility.js b/app/src/config/constants/compatibility.js index 78aafaa4dd..3823de63bd 100644 --- a/app/src/config/constants/compatibility.js +++ b/app/src/config/constants/compatibility.js @@ -256,4 +256,12 @@ export const FEATURE_COMPATIBLE_VERSION = { [GLOBAL_CONSTANTS.APP_MODES.DESKTOP]: "26.3.19", [GLOBAL_CONSTANTS.APP_MODES.EXTENSION]: null, }, + [FEATURES.DEVELOPER_SCRIPT_MODE]: { + // RQ-2426: TODO — replace "99.99.99" with the desktop release that ships the + // devScriptMode preference handlers + sandboxing proxy (the RQ-2426 desktop PR). + // Placeholder is intentionally fail-closed: the toggle stays HIDDEN on every + // desktop version until this is set, so it never appears on an incompatible build. + [GLOBAL_CONSTANTS.APP_MODES.DESKTOP]: "99.99.99", + [GLOBAL_CONSTANTS.APP_MODES.EXTENSION]: null, // desktop-proxy-only feature + }, }; diff --git a/app/src/config/constants/sub/features.js b/app/src/config/constants/sub/features.js index c98d9e4a5b..3eeb904377 100644 --- a/app/src/config/constants/sub/features.js +++ b/app/src/config/constants/sub/features.js @@ -114,4 +114,9 @@ FEATURES.DESKTOP_BETA_PREVIEW_URL_CONFIGURATION = "desktop_beta_preview_url_conf FEATURES.SECRETS_MANAGER = "secrets_manager"; FEATURES.ALLOW_INSECURE_SSL = "allow_insecure_ssl"; +// RQ-2426: desktop-only "Developer script mode" toggle (safe sandbox vs full-access +// for programmatic code rules). Gated so it only shows on desktop versions that ship +// the devScriptMode preference handlers + sandboxing proxy. +FEATURES.DEVELOPER_SCRIPT_MODE = "developer_script_mode"; + export default FEATURES; diff --git a/app/src/features/settings/components/DesktopSettings/DevScriptMode/index.tsx b/app/src/features/settings/components/DesktopSettings/DevScriptMode/index.tsx new file mode 100644 index 0000000000..0982a0d729 --- /dev/null +++ b/app/src/features/settings/components/DesktopSettings/DevScriptMode/index.tsx @@ -0,0 +1,84 @@ +import React, { useEffect, useState } from "react"; +import { Col, Popconfirm, Row, Switch } from "antd"; +import { toast } from "utils/Toast"; + +const GET_ACTION = "USER_PREFERENCE:GET_DEV_SCRIPT_MODE"; +const SET_ACTION = "USER_PREFERENCE:UPDATE_DEV_SCRIPT_MODE"; + +function storageAction(type: string, data?: any): Promise { + return (window as any)?.RQ?.DESKTOP?.SERVICES?.IPC?.invokeEventInMain("rq-storage:storage-action", { + type, + payload: data !== undefined ? { data } : {}, + }); +} + +/** + * RQ-2426: desktop-only toggle for how "code" rules execute. + * + * OFF (default) → SAFE: rule code runs in the QuickJS-WASM sandbox (no host access). + * ON → DEV : rule code runs with FULL system access + * (require/process/fs/child_process). + * + * Persisted in the desktop user-preference store and applied live on the running + * proxy (no restart). Turning it OFF is always safe and applies immediately; turning + * it ON requires an explicit confirmation because it re-opens an arbitrary + * code-execution path — a shared/imported rule would then run with full privileges. + */ +const DevScriptMode: React.FC = () => { + const [enabled, setEnabled] = useState(false); + const [loading, setLoading] = useState(false); + + useEffect(() => { + storageAction(GET_ACTION) + ?.then((res: boolean) => setEnabled(!!res)) + .catch(() => {}); + }, []); + + const applyMode = async (checked: boolean) => { + setLoading(true); + try { + await storageAction(SET_ACTION, { devScriptMode: checked }); + setEnabled(checked); + toast.success( + checked + ? "Developer script mode enabled — rule scripts now run with full system access." + : "Safe script mode restored — rule scripts run inside the sandbox." + ); + } catch (e) { + toast.error("Failed to update setting"); + } finally { + setLoading(false); + } + }; + + return ( + + +
Developer script mode
+

+ Runs "code" rules with full system access instead of the secure sandbox. Enable only for scripts you fully + trust — a shared or imported rule could run arbitrary code on your machine. +

+ + + {enabled ? ( + // Already on → allow turning it off immediately (safe direction). + applyMode(false)} /> + ) : ( + // Off → require an explicit confirm before granting full access. + applyMode(true)} + > + + + )} + +
+ ); +}; + +export default DevScriptMode; diff --git a/app/src/features/settings/components/DesktopSettings/index.jsx b/app/src/features/settings/components/DesktopSettings/index.jsx index 39ee5c0d79..28e638dc0e 100644 --- a/app/src/features/settings/components/DesktopSettings/index.jsx +++ b/app/src/features/settings/components/DesktopSettings/index.jsx @@ -20,6 +20,7 @@ import { RQButton } from "lib/design-system/components"; import "./DesktopSettings.css"; import LocalLogFile from "./LocalLogFile"; import InsecureCerts from "./InsecureCerts"; +import DevScriptMode from "./DevScriptMode"; export const DesktopSettings = () => { const appMode = useSelector(getAppMode); @@ -238,6 +239,7 @@ export const DesktopSettings = () => { ) : null} {isFeatureCompatible(FEATURES.ALLOW_INSECURE_SSL) && } + {isFeatureCompatible(FEATURES.DEVELOPER_SCRIPT_MODE) && } diff --git a/documentation/docs.json b/documentation/docs.json index 9c32bc2c78..b71a4dc6ce 100644 --- a/documentation/docs.json +++ b/documentation/docs.json @@ -50,7 +50,8 @@ "interceptor/desktop-app/desktop-app-interception", "interceptor/desktop-app/network-table", "interceptor/desktop-app/saving-logs-to-local-file", - "interceptor/desktop-app/allow-insecure-ssl" + "interceptor/desktop-app/allow-insecure-ssl", + "interceptor/desktop-app/developer-script-mode" ] }, { @@ -199,9 +200,7 @@ }, { "group": "Other", - "pages": [ - "guides/other/claim-your-requestly-github-student-pack-benefit" - ] + "pages": ["guides/other/claim-your-requestly-github-student-pack-benefit"] } ] }, @@ -343,12 +342,7 @@ } }, "contextual": { - "options": [ - "copy", - "view", - "chatgpt", - "claude" - ] + "options": ["copy", "view", "chatgpt", "claude"] }, "integrations": { "ga4": { diff --git a/documentation/http-rules/advanced-usage/shared-state.mdx b/documentation/http-rules/advanced-usage/shared-state.mdx index 54c0b6be7d..aa2798ada3 100644 --- a/documentation/http-rules/advanced-usage/shared-state.mdx +++ b/documentation/http-rules/advanced-usage/shared-state.mdx @@ -15,6 +15,12 @@ Shared State allows developers to store and retrieve data within and across Modi Shared State was introduced in the Requestly Interceptor extension version `24.8.13` and Desktop App `1.7.1` . Make sure you are using the latest version. + + On the **desktop app**, programmatic rule scripts run in a secure sandbox by default (no + filesystem or system access). To use Node.js APIs like `require`, `fs`, or `process`, turn on + **Developer Script Mode**. + + ## Use Cases for Shared State 1. **Conditional Request/Response Modification**: Use shared state to modify requests or responses dynamically based on conditions stored in prior requests. For example, you can conditionally fail requests based on request counts. diff --git a/documentation/http-rules/rule-types/modify-request-body.mdx b/documentation/http-rules/rule-types/modify-request-body.mdx index c1edb97aac..da390c34bf 100644 --- a/documentation/http-rules/rule-types/modify-request-body.mdx +++ b/documentation/http-rules/rule-types/modify-request-body.mdx @@ -124,6 +124,13 @@ The Modify Request Body rule enables you to override or programmatically modify You can also use Dynamic Data to override GraphQL APIs by detecting the operation name in the request body. + + + On the **desktop app**, this JavaScript runs inside a secure sandbox by default — it has no + filesystem or system access. If your script needs Node.js APIs such as `require`, `fs`, or + `process`, enable **Developer Script Mode** + (off by default for safety). + diff --git a/documentation/http-rules/rule-types/modify-response-body.mdx b/documentation/http-rules/rule-types/modify-response-body.mdx index be9e4b9301..41f2b5cd13 100644 --- a/documentation/http-rules/rule-types/modify-response-body.mdx +++ b/documentation/http-rules/rule-types/modify-response-body.mdx @@ -109,6 +109,13 @@ Modify API Response Rules are useful in several scenarios: →Learn more about shared state here. + + On the **desktop app**, this JavaScript runs inside a secure sandbox by default — it has no + filesystem or system access. If your script needs Node.js APIs such as `require`, `fs`, or + `process`, enable **Developer Script Mode** + (off by default for safety). + + * diff --git a/documentation/images/developer-script-mode/.gitkeep b/documentation/images/developer-script-mode/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/documentation/interceptor/desktop-app/developer-script-mode.mdx b/documentation/interceptor/desktop-app/developer-script-mode.mdx new file mode 100644 index 0000000000..d25c3d134e --- /dev/null +++ b/documentation/interceptor/desktop-app/developer-script-mode.mdx @@ -0,0 +1,82 @@ +--- +title: "Developer Script Mode" +slug: "developer-script-mode" +path: "/http-interceptor/desktop-app/developer-script-mode" +visibility: "PUBLIC" +format: "MDX" +--- + +Requestly lets you write JavaScript to transform requests and responses — the +**Programmatic (JavaScript)** option in *Modify Request Body* and *Modify Response Body* +rules. On the desktop app this code runs inside the interception proxy. + +To keep you safe, that code runs in a **secure sandbox by default**. **Developer Script Mode** +is an opt-in setting that removes the sandbox and gives your rule scripts full access to your +system. + + + This setting applies only to the **desktop app**, and only to **"code" (Programmatic + JavaScript) rules**. Other rule types are unaffected. + + +## Why the sandbox exists + +Rule scripts travel between people — through shared lists, import/export, and team sync. A +script you import from someone else runs on **your** machine. If those scripts had unrestricted +access to Node.js, an imported rule could read your files, run commands, or make arbitrary +network calls without you realizing it. + +To prevent this, code rules run in an isolated JavaScript engine that has **no access to your +operating system**. + +## Safe mode (default) + +Your script runs in an isolated sandbox. It can do everything needed for typical +request/response manipulation, but it cannot touch the host machine. + +**Available** + +- Standard JavaScript, `console.*`, `args`, and `$sharedState` +- `fetch`, `XMLHttpRequest` +- `URL`, `URLSearchParams`, `TextEncoder` / `TextDecoder`, `Buffer`, `Blob`, `atob` / `btoa`, `structuredClone` +- `crypto` (`randomUUID`, `getRandomValues`, `subtle.digest`) and `require('crypto')` for hashing/HMAC + +**Blocked** + +- `require(...)` for anything other than `crypto` — e.g. `require('fs')`, `require('child_process')` +- `process`, the filesystem, spawning processes, and other Node.js / OS APIs + + + Scripts are also bounded by execution-time and memory limits so a runaway rule can't stall + interception. + + +## Developer Script Mode (opt‑in) + +When enabled, code rules run with **full system access** — the same capabilities a normal +Node.js script has (`require`, `process`, `fs`, `child_process`, …). Use it only when you +genuinely need it and only for scripts you fully trust. + + + In Developer Script Mode a shared or imported rule can run **arbitrary code on your machine** + with your privileges. Enable it only for code you have written or reviewed. + + +## Enabling / disabling + + + + Click the settings icon in the top‑right and go to **Desktop Settings**. You'll find the + **Developer script mode** toggle there (off by default). + + {/* Screenshot: the Desktop Settings screen with the "Developer script mode" row visible and the toggle OFF. Crop to the setting row + its description. */} + + + + Turn **Developer script mode** on. You'll be asked to confirm, since it grants full system + access. Turning it back off is immediate. + + {/* Screenshot: the confirmation dialog/popover that appears when enabling dev mode (the "Dev mode runs rule scripts with FULL system access…" prompt with Enable/Cancel). */} + + + From 69c68b4fcab7c33bcc5db1d3551a4d0d6932aeef Mon Sep 17 00:00:00 2001 From: dinex-dev Date: Fri, 17 Jul 2026 14:53:11 +0530 Subject: [PATCH 2/4] docs(ui): use 'Dynamic (JavaScript)' terminology in Developer Script Mode caption Matches the rule editor's wording instead of the internal 'code rules' term. Co-Authored-By: Claude Opus 4.8 --- .../components/DesktopSettings/DevScriptMode/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/features/settings/components/DesktopSettings/DevScriptMode/index.tsx b/app/src/features/settings/components/DesktopSettings/DevScriptMode/index.tsx index 0982a0d729..a101e11fb0 100644 --- a/app/src/features/settings/components/DesktopSettings/DevScriptMode/index.tsx +++ b/app/src/features/settings/components/DesktopSettings/DevScriptMode/index.tsx @@ -56,8 +56,8 @@ const DevScriptMode: React.FC = () => {
Developer script mode

- Runs "code" rules with full system access instead of the secure sandbox. Enable only for scripts you fully - trust — a shared or imported rule could run arbitrary code on your machine. + Runs Dynamic (JavaScript) request/response rules with full system access instead of the secure sandbox. + Enable only for scripts you fully trust — a shared or imported rule could run arbitrary code on your machine.

From 72144e4f34f8ee1c88adc9f6202d80f3aa982d07 Mon Sep 17 00:00:00 2001 From: dinex-dev Date: Fri, 17 Jul 2026 16:33:17 +0530 Subject: [PATCH 3/4] feat(ui): persistent header badge while Developer Script Mode is on Shows an amber 'Dev script mode' indicator in the desktop header whenever full-access script execution is enabled, so it can't be silently forgotten. Desktop-only, gated by FEATURES.DEVELOPER_SCRIPT_MODE; polls the pref so it reflects live toggles. Co-Authored-By: Claude Opus 4.8 --- .../devScriptModeBadge.scss | 31 ++++++++ .../MenuHeader/DevScriptModeBadge/index.tsx | 74 +++++++++++++++++++ .../DashboardLayout/MenuHeader/MenuHeader.tsx | 2 + 3 files changed, 107 insertions(+) create mode 100644 app/src/layouts/DashboardLayout/MenuHeader/DevScriptModeBadge/devScriptModeBadge.scss create mode 100644 app/src/layouts/DashboardLayout/MenuHeader/DevScriptModeBadge/index.tsx diff --git a/app/src/layouts/DashboardLayout/MenuHeader/DevScriptModeBadge/devScriptModeBadge.scss b/app/src/layouts/DashboardLayout/MenuHeader/DevScriptModeBadge/devScriptModeBadge.scss new file mode 100644 index 0000000000..29e9360bb5 --- /dev/null +++ b/app/src/layouts/DashboardLayout/MenuHeader/DevScriptModeBadge/devScriptModeBadge.scss @@ -0,0 +1,31 @@ +// RQ-2426: persistent "Developer Script Mode is ON" header indicator. +// Amber styling that reads on both light and dark themes (translucent amber). +.dev-script-mode-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 2px 10px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; + line-height: 18px; + white-space: nowrap; + cursor: pointer; + user-select: none; + color: #ffc069; + background: rgba(250, 140, 22, 0.15); + border: 1px solid rgba(250, 140, 22, 0.5); + + &__dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: #fa8c16; + flex: 0 0 auto; + } + + &:hover { + background: rgba(250, 140, 22, 0.25); + border-color: rgba(250, 140, 22, 0.7); + } +} diff --git a/app/src/layouts/DashboardLayout/MenuHeader/DevScriptModeBadge/index.tsx b/app/src/layouts/DashboardLayout/MenuHeader/DevScriptModeBadge/index.tsx new file mode 100644 index 0000000000..9f2655ce41 --- /dev/null +++ b/app/src/layouts/DashboardLayout/MenuHeader/DevScriptModeBadge/index.tsx @@ -0,0 +1,74 @@ +import React, { useEffect, useState } from "react"; +import { useSelector } from "react-redux"; +import { useNavigate } from "react-router-dom"; +import { Tooltip } from "antd"; +import { CONSTANTS as GLOBAL_CONSTANTS } from "@requestly/requestly-core"; +import { getAppMode } from "store/selectors"; +import { isFeatureCompatible } from "utils/CompatibilityUtils"; +import FEATURES from "config/constants/sub/features"; +import { redirectToSettings } from "utils/RedirectionUtils"; +import "./devScriptModeBadge.scss"; + +const GET_ACTION = "USER_PREFERENCE:GET_DEV_SCRIPT_MODE"; + +function getDevScriptModePref(): Promise | undefined { + return (window as any)?.RQ?.DESKTOP?.SERVICES?.IPC?.invokeEventInMain("rq-storage:storage-action", { + type: GET_ACTION, + payload: {}, + }); +} + +/** + * RQ-2426: persistent header indicator shown while Developer Script Mode is ON, so a + * user can't forget that rule scripts are executing with FULL system access (a + * shared/imported rule would run unsandboxed silently otherwise). + * + * Desktop-only; gated by the same feature flag as the toggle. Polls the preference + * so the badge reflects a live toggle without a reload. (Simple by design for now — + * could be made event-driven from the settings toggle later.) + */ +const DevScriptModeBadge: React.FC = () => { + const appMode = useSelector(getAppMode); + const navigate = useNavigate(); + const [enabled, setEnabled] = useState(false); + + const isDesktop = appMode === GLOBAL_CONSTANTS.APP_MODES.DESKTOP; + const isCompatible = isFeatureCompatible(FEATURES.DEVELOPER_SCRIPT_MODE); + + useEffect(() => { + if (!isDesktop || !isCompatible) return; + let active = true; + const check = () => { + getDevScriptModePref() + ?.then((res: boolean) => { + if (active) setEnabled(!!res); + }) + .catch(() => {}); + }; + check(); + const intervalId = setInterval(check, 4000); + return () => { + active = false; + clearInterval(intervalId); + }; + }, [isDesktop, isCompatible]); + + if (!isDesktop || !isCompatible || !enabled) { + return null; + } + + return ( + +
redirectToSettings(navigate, window.location.pathname, "dev_script_mode_badge")} + > + + Dev script mode +
+
+ ); +}; + +export default DevScriptModeBadge; diff --git a/app/src/layouts/DashboardLayout/MenuHeader/MenuHeader.tsx b/app/src/layouts/DashboardLayout/MenuHeader/MenuHeader.tsx index 8a1f36447c..30b260018d 100644 --- a/app/src/layouts/DashboardLayout/MenuHeader/MenuHeader.tsx +++ b/app/src/layouts/DashboardLayout/MenuHeader/MenuHeader.tsx @@ -17,6 +17,7 @@ import { getAppMode, getRequestBot } from "store/selectors"; import { CONSTANTS as GLOBAL_CONSTANTS } from "@requestly/requestly-core"; import { Col } from "antd"; import PremiumPlanBadge from "./PremiumPlanBadge/PremiumPlanBadge"; +import DevScriptModeBadge from "./DevScriptModeBadge"; import { getUserAuthDetails } from "store/slices/global/user/selectors"; import GitHubButton from "react-github-btn"; import "./menuHeader.scss"; @@ -61,6 +62,7 @@ export const MenuHeader = () => {
+ {!isSafariBrowser() && (appMode === GLOBAL_CONSTANTS.APP_MODES.DESKTOP || (appMode !== GLOBAL_CONSTANTS.APP_MODES.DESKTOP && From 9ead8be8ce045cd149ecec4828a7ce6558f643be Mon Sep 17 00:00:00 2001 From: dinex-dev Date: Tue, 21 Jul 2026 14:04:38 +0530 Subject: [PATCH 4/4] feat(ui): Safe/Developer mode selector + refine dev-mode badge - Desktop Settings: replace the switch with an inline Safe Mode / Developer Mode radio selector (api-client style); warn via a danger confirm before switching to Developer Mode. Drop the section's bottom divider. - Header badge: show 'Dev Mode' with a hover popover (explanation + link) and route to /settings/desktop-settings. - Fix Desktop Settings scroll (block scroll container) so tall content isn't clipped, plus bottom padding. Co-Authored-By: Claude Opus 4.8 --- .../DesktopSettings/DesktopSettings.css | 11 +- .../DevScriptMode/devScriptMode.scss | 80 ++++++++++++ .../DesktopSettings/DevScriptMode/index.tsx | 118 +++++++++++------- .../devScriptModeBadge.scss | 31 +++++ .../MenuHeader/DevScriptModeBadge/index.tsx | 34 ++++- 5 files changed, 221 insertions(+), 53 deletions(-) create mode 100644 app/src/features/settings/components/DesktopSettings/DevScriptMode/devScriptMode.scss diff --git a/app/src/features/settings/components/DesktopSettings/DesktopSettings.css b/app/src/features/settings/components/DesktopSettings/DesktopSettings.css index c63eb7aed8..9b18a65276 100644 --- a/app/src/features/settings/components/DesktopSettings/DesktopSettings.css +++ b/app/src/features/settings/components/DesktopSettings/DesktopSettings.css @@ -13,13 +13,20 @@ } .desktop-settings-container { - display: flex; - justify-content: center; + /* Block (not flex) scroll container: the shared .settings-content ancestor is + height:100% + overflow:hidden, so scroll here instead of clipping. Using block + layout (centering the wrapper via margin:auto below) also preserves the wrapper's + bottom padding at the end of the scroll — a flex scroll container clips it. */ + height: 100%; + overflow-y: auto; } .desktop-settings-wrapper { + margin: 0 auto; /* center now that the container is block, not flex */ padding: 2rem; padding-top: 1rem; + /* extra breathing room so the last setting isn't flush against the bottom when scrolled */ + padding-bottom: 4rem; width: 100%; max-width: 1000px; } diff --git a/app/src/features/settings/components/DesktopSettings/DevScriptMode/devScriptMode.scss b/app/src/features/settings/components/DesktopSettings/DevScriptMode/devScriptMode.scss new file mode 100644 index 0000000000..2f7c004d45 --- /dev/null +++ b/app/src/features/settings/components/DesktopSettings/DevScriptMode/devScriptMode.scss @@ -0,0 +1,80 @@ +// RQ-2426: inline Script execution mode selector (Safe / Developer) in Desktop Settings. + +// This is the last section — drop the shared setting-item divider under it. +.dev-script-mode-setting.setting-item-container { + border-bottom: none; +} + +.dev-script-mode-selector { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 12px; + width: 100%; + + .dev-script-mode-option.ant-radio-wrapper { + display: flex; + align-items: flex-start; + width: 100%; + margin: 0; + padding: 12px 14px; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 8px; + white-space: normal; + transition: border-color 0.15s ease, background 0.15s ease; + + &:hover { + border-color: rgba(255, 255, 255, 0.28); + } + + &.ant-radio-wrapper-checked { + border-color: rgba(255, 255, 255, 0.45); + background: rgba(255, 255, 255, 0.04); + } + + .ant-radio { + margin-top: 2px; + } + + // antd puts the label content in the trailing span — stack title over description + > span:last-child { + display: flex; + flex-direction: column; + gap: 4px; + padding-inline-end: 0; + } + } + + .dev-script-mode-option__title { + font-weight: 600; + font-size: 13px; + display: inline-flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + } + + // Green "Default" chip, styled like the api-client "BETA" tag. + .dev-script-mode-option__tag { + font-size: 11px; + font-weight: 500; + padding: 0 6px; + border-radius: 8px; + color: #73d13d; + background: rgba(82, 196, 26, 0.15); + border: 1px solid rgba(82, 196, 26, 0.4); + } + + // Orange "(use only if you trust…)" caveat next to Developer Mode. + .dev-script-mode-option__caveat { + font-size: 12px; + font-weight: 500; + color: #fa8c16; + } + + .dev-script-mode-option__desc { + font-size: 12px; + line-height: 18px; + color: var(--requestly-color-text-subtle, #9b9b9b); + } +} diff --git a/app/src/features/settings/components/DesktopSettings/DevScriptMode/index.tsx b/app/src/features/settings/components/DesktopSettings/DevScriptMode/index.tsx index a101e11fb0..d87a136727 100644 --- a/app/src/features/settings/components/DesktopSettings/DevScriptMode/index.tsx +++ b/app/src/features/settings/components/DesktopSettings/DevScriptMode/index.tsx @@ -1,10 +1,13 @@ import React, { useEffect, useState } from "react"; -import { Col, Popconfirm, Row, Switch } from "antd"; +import { Modal, Radio, RadioChangeEvent } from "antd"; import { toast } from "utils/Toast"; +import "./devScriptMode.scss"; const GET_ACTION = "USER_PREFERENCE:GET_DEV_SCRIPT_MODE"; const SET_ACTION = "USER_PREFERENCE:UPDATE_DEV_SCRIPT_MODE"; +type ScriptMode = "safe" | "dev"; + function storageAction(type: string, data?: any): Promise { return (window as any)?.RQ?.DESKTOP?.SERVICES?.IPC?.invokeEventInMain("rq-storage:storage-action", { type, @@ -13,71 +16,96 @@ function storageAction(type: string, data?: any): Promise { } /** - * RQ-2426: desktop-only toggle for how "code" rules execute. + * RQ-2426: desktop-only selector for how Dynamic (JavaScript) request/response rules + * execute in the proxy. * - * OFF (default) → SAFE: rule code runs in the QuickJS-WASM sandbox (no host access). - * ON → DEV : rule code runs with FULL system access - * (require/process/fs/child_process). + * Safe Mode (default) → QuickJS-WASM sandbox, no host access. + * Developer Mode → legacy full host access (require/process/fs/child_process). * - * Persisted in the desktop user-preference store and applied live on the running - * proxy (no restart). Turning it OFF is always safe and applies immediately; turning - * it ON requires an explicit confirmation because it re-opens an arbitrary - * code-execution path — a shared/imported rule would then run with full privileges. + * Inline radio selector; the chosen mode is applied immediately, persisted in the + * desktop user-preference store and applied live on the running proxy (no restart). */ const DevScriptMode: React.FC = () => { - const [enabled, setEnabled] = useState(false); + const [mode, setMode] = useState("safe"); const [loading, setLoading] = useState(false); useEffect(() => { storageAction(GET_ACTION) - ?.then((res: boolean) => setEnabled(!!res)) + ?.then((res: boolean) => setMode(res ? "dev" : "safe")) .catch(() => {}); }, []); - const applyMode = async (checked: boolean) => { + const applyMode = async (next: ScriptMode) => { + const prev = mode; + setMode(next); // optimistic setLoading(true); try { - await storageAction(SET_ACTION, { devScriptMode: checked }); - setEnabled(checked); + await storageAction(SET_ACTION, { devScriptMode: next === "dev" }); toast.success( - checked - ? "Developer script mode enabled — rule scripts now run with full system access." - : "Safe script mode restored — rule scripts run inside the sandbox." + next === "dev" + ? "Developer Mode enabled — rule scripts now run with full system access." + : "Safe Mode restored — rule scripts run inside the secure sandbox." ); - } catch (e) { - toast.error("Failed to update setting"); + } catch (err) { + setMode(prev); // revert on failure + toast.error("Failed to update script execution mode"); } finally { setLoading(false); } }; + const onChange = (e: RadioChangeEvent) => { + const next = e.target.value as ScriptMode; + if (next === mode || loading) return; + + if (next === "dev") { + // Switching to full-access execution — warn before applying. The radio stays on + // the current value until the user confirms (Cancel is a no-op). + Modal.confirm({ + title: "Switch to Developer Mode?", + content: + "Rule scripts will run with FULL system access — they can read and write your files, execute system commands, and access sensitive information. A shared or imported rule could run arbitrary code on your machine. Only enable this for scripts you fully trust.", + okText: "Enable Developer Mode", + okButtonProps: { danger: true }, + cancelText: "Cancel", + width: 460, + onOk: () => applyMode("dev"), + }); + return; + } + + applyMode("safe"); + }; + return ( - - -
Developer script mode
-

- Runs Dynamic (JavaScript) request/response rules with full system access instead of the secure sandbox. - Enable only for scripts you fully trust — a shared or imported rule could run arbitrary code on your machine. -

- - - {enabled ? ( - // Already on → allow turning it off immediately (safe direction). - applyMode(false)} /> - ) : ( - // Off → require an explicit confirm before granting full access. - applyMode(true)} - > - - - )} - -
+
+
Script execution mode
+

+ Dynamic (JavaScript) request/response rules run code in the desktop proxy. Choose the security level for that + execution. +

+ + + + + Safe Mode Default + + + Rule scripts run in a secure sandbox and cannot access your filesystem or execute system commands. + + + + + + Developer Mode{" "} + (use only if you trust the rule authors) + + + Rule scripts have access to the filesystem, can execute system commands, and access sensitive information. + + + +
); }; diff --git a/app/src/layouts/DashboardLayout/MenuHeader/DevScriptModeBadge/devScriptModeBadge.scss b/app/src/layouts/DashboardLayout/MenuHeader/DevScriptModeBadge/devScriptModeBadge.scss index 29e9360bb5..a6234f3d96 100644 --- a/app/src/layouts/DashboardLayout/MenuHeader/DevScriptModeBadge/devScriptModeBadge.scss +++ b/app/src/layouts/DashboardLayout/MenuHeader/DevScriptModeBadge/devScriptModeBadge.scss @@ -29,3 +29,34 @@ border-color: rgba(250, 140, 22, 0.7); } } + +// Hover popover content (rendered in a portal, so styled as top-level classes). +.dev-script-mode-badge__popover { + max-width: 260px; + + &-title { + font-weight: 600; + font-size: 13px; + margin-bottom: 4px; + color: #fa8c16; + } + + &-desc { + font-size: 12px; + line-height: 18px; + margin-bottom: 8px; + color: var(--requestly-color-text-subtle, #9b9b9b); + } + + &-link { + display: inline-block; + font-size: 12px; + font-weight: 500; + cursor: pointer; + color: var(--requestly-color-primary, #3b82f6); + + &:hover { + text-decoration: underline; + } + } +} diff --git a/app/src/layouts/DashboardLayout/MenuHeader/DevScriptModeBadge/index.tsx b/app/src/layouts/DashboardLayout/MenuHeader/DevScriptModeBadge/index.tsx index 9f2655ce41..becd803388 100644 --- a/app/src/layouts/DashboardLayout/MenuHeader/DevScriptModeBadge/index.tsx +++ b/app/src/layouts/DashboardLayout/MenuHeader/DevScriptModeBadge/index.tsx @@ -1,12 +1,12 @@ import React, { useEffect, useState } from "react"; import { useSelector } from "react-redux"; import { useNavigate } from "react-router-dom"; -import { Tooltip } from "antd"; +import { Popover } from "antd"; import { CONSTANTS as GLOBAL_CONSTANTS } from "@requestly/requestly-core"; import { getAppMode } from "store/selectors"; import { isFeatureCompatible } from "utils/CompatibilityUtils"; import FEATURES from "config/constants/sub/features"; -import { redirectToSettings } from "utils/RedirectionUtils"; +import { redirectToDesktopSettings } from "utils/RedirectionUtils"; import "./devScriptModeBadge.scss"; const GET_ACTION = "USER_PREFERENCE:GET_DEV_SCRIPT_MODE"; @@ -57,17 +57,39 @@ const DevScriptModeBadge: React.FC = () => { return null; } + const popoverContent = ( +
+
Developer Mode is on
+
+ Dynamic (JavaScript) rule scripts run with full system access instead of the secure sandbox. Only keep this on + for scripts you fully trust. +
+ redirectToDesktopSettings(navigate, window.location.pathname, "dev_mode_badge_popover")} + > + Manage in Desktop Settings → + +
+ ); + return ( - +
redirectToSettings(navigate, window.location.pathname, "dev_script_mode_badge")} + onClick={() => redirectToDesktopSettings(navigate, window.location.pathname, "dev_mode_badge")} > - Dev script mode + Dev Mode
-
+ ); };