Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/src/config/constants/compatibility.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
};
5 changes: 5 additions & 0 deletions app/src/config/constants/sub/features.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React, { useEffect, useState } from "react";
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<any> {
return (window as any)?.RQ?.DESKTOP?.SERVICES?.IPC?.invokeEventInMain("rq-storage:storage-action", {
type,
payload: data !== undefined ? { data } : {},
});
}

/**
* RQ-2426: desktop-only selector for how Dynamic (JavaScript) request/response rules
* execute in the proxy.
*
* Safe Mode (default) → QuickJS-WASM sandbox, no host access.
* Developer Mode → legacy full host access (require/process/fs/child_process).
*
* 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 [mode, setMode] = useState<ScriptMode>("safe");
const [loading, setLoading] = useState(false);

useEffect(() => {
storageAction(GET_ACTION)
?.then((res: boolean) => setMode(res ? "dev" : "safe"))
.catch(() => {});
}, []);

const applyMode = async (next: ScriptMode) => {
const prev = mode;
setMode(next); // optimistic
setLoading(true);
try {
await storageAction(SET_ACTION, { devScriptMode: next === "dev" });
toast.success(
next === "dev"
? "Developer Mode enabled — rule scripts now run with full system access."
: "Safe Mode restored — rule scripts run inside the secure sandbox."
);
} 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 (
<div className="w-full mt-16 setting-item-container dev-script-mode-setting">
<div className="title">Script execution mode</div>
<p className="setting-item-caption">
Dynamic (JavaScript) request/response rules run code in the desktop proxy. Choose the security level for that
execution.
</p>

<Radio.Group className="dev-script-mode-selector" value={mode} onChange={onChange} disabled={loading}>
<Radio value="safe" className="dev-script-mode-option">
<span className="dev-script-mode-option__title">
Safe Mode <span className="dev-script-mode-option__tag">Default</span>
</span>
<span className="dev-script-mode-option__desc">
Rule scripts run in a secure sandbox and cannot access your filesystem or execute system commands.
</span>
</Radio>

<Radio value="dev" className="dev-script-mode-option">
<span className="dev-script-mode-option__title">
Developer Mode{" "}
<span className="dev-script-mode-option__caveat">(use only if you trust the rule authors)</span>
</span>
<span className="dev-script-mode-option__desc">
Rule scripts have access to the filesystem, can execute system commands, and access sensitive information.
</span>
</Radio>
</Radio.Group>
</div>
);
};

export default DevScriptMode;
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -238,6 +239,7 @@ export const DesktopSettings = () => {
</>
) : null}
{isFeatureCompatible(FEATURES.ALLOW_INSECURE_SSL) && <InsecureCerts />}
{isFeatureCompatible(FEATURES.DEVELOPER_SCRIPT_MODE) && <DevScriptMode />}
<LocalLogFile />
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// 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);
}
}

// 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;
}
}
}
Loading