From 760d305b30abc3748fe88253ec84b93bd5cb98b0 Mon Sep 17 00:00:00 2001 From: f3tch Date: Fri, 28 Aug 2026 18:08:17 +0500 Subject: [PATCH] feat: add explicit system access flag --- README.md | 30 ++++++++++---- SECURITY.md | 6 +-- src/cli.ts | 10 ++++- src/index.moz.ts | 7 ++-- src/index.public.ts | 7 ++-- src/index.ts | 15 +++---- src/system-access.ts | 35 ++++++++++++++++ src/tools/firefox-management.ts | 10 ++++- src/tools/firefox-prefs.ts | 14 +++---- src/tools/module.ts | 5 +-- src/tools/privileged-context.ts | 12 +++--- src/tools/registry.ts | 4 +- src/tools/webextension.ts | 13 +++--- tests/cli/prefs-parsing.test.ts | 17 ++++++++ tests/system-access.test.ts | 56 ++++++++++++++++++++++++++ tests/tools/firefox-management.test.ts | 12 ++++++ tests/tools/firefox-prefs.test.ts | 8 ++-- tests/tools/privileged-context.test.ts | 2 +- tests/tools/registry.test.ts | 30 ++++++++++++++ 19 files changed, 234 insertions(+), 59 deletions(-) create mode 100644 src/system-access.ts create mode 100644 tests/system-access.test.ts diff --git a/README.md b/README.md index 7d496fe6..a5b87862 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Browser MCP servers carry inherent risks. A few key practices: - **Use a dedicated Firefox profile.** Never run the server against your regular profile — the agent has access to whatever the browser can reach, including cookies and saved sessions. - **Be cautious about which sites you visit.** Pages can return content designed to manipulate the agent (prompt injection). Stick to sites you control or trust. -- **Enable only the tool modules you need.** The default `basic` preset already includes `evaluate_script`; `--tool-preset slim` drops it. Higher presets such as `--tool-preset developer` (debugging, network, console, profiler) and `--tool-preset mozilla` (privileged context) expand what the agent can do further. +- **Enable only the tool modules you need.** The default `basic` preset already includes `evaluate_script`; `--tool-preset slim` drops it. Higher presets such as `--tool-preset developer` (debugging, network, console, profiler) expand what the agent can do further. The `mozilla` preset also selects privileged tools, which require separate consent through `--allow-system-access`. See [SECURITY.md](SECURITY.md) for a full breakdown of risks and how to report vulnerabilities. @@ -132,10 +132,11 @@ You can pass flags or environment variables (names on the right): - `--connect-existing` — attach to an already-running Firefox instead of launching a new one (`CONNECT_EXISTING=true`) - `--marionette-port` — Marionette port for connect-existing mode, default 2828 (`MARIONETTE_PORT`) - `--pref name=value` — set Firefox preference at startup via `moz:firefoxOptions` (repeatable) +- `--allow-system-access` — explicitly allow privileged Firefox access. This permits privileged modules to be exposed when selected and launches Firefox with the required system access. It does not select any tools by itself. - `--tool-preset` — select which tool modules to enable: `slim`, `basic` (default), `developer`, `mozilla`, or `all`. See [Tool modules and presets](#tool-modules-and-presets). (`TOOL_PRESET`) - `--tools` — explicit list of tool modules to enable, overriding `--tool-preset` entirely (e.g. `--tools pages network script`). See [Tool modules and presets](#tool-modules-and-presets). - `--enable-script` — _deprecated, use `--tool-preset developer` or `--tools ... script debugging`._ Selects the `developer` tool preset. (`ENABLE_SCRIPT=true`) -- `--enable-privileged-context` — _deprecated, use `--tool-preset mozilla` or `--tools ... privileged prefs`._ Selects the `mozilla` tool preset. Requires `MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1` (`ENABLE_PRIVILEGED_CONTEXT=true`) +- `--enable-privileged-context` — _deprecated, use `--tool-preset mozilla` or `--tools ... privileged prefs`._ Selects the `mozilla` tool preset. Requires `--allow-system-access`. (`ENABLE_PRIVILEGED_CONTEXT=true`) - `--android-device` — enable Firefox for Android mode; value is the ADB device serial (e.g. `emulator-5554`). Run `adb devices` to list connected devices. Omit the value or use `auto` to select the single connected device automatically. - `--android-wipe-app-data` — confirm that Android mode wipes all data of the target app. Required together with `--android-device`. (`ANDROID_WIPE_APP_DATA=true`) - `--android-package` — Android app package name, default `org.mozilla.firefox`. Other packages: `org.mozilla.firefox_beta` for Firefox Beta, `org.mozilla.fenix` for Firefox Nightly, `org.mozilla.fenix.debug` for Firefox Nightly Debug, `org.mozilla.geckoview_example` for geckoview (`ANDROID_PACKAGE`) @@ -171,11 +172,24 @@ npx @mozilla/firefox-devtools-mcp --tool-preset developer # Enable only the modules you need npx @mozilla/firefox-devtools-mcp --tools pages network console + +# Explicitly allow and select privileged tools +npx @mozilla/firefox-devtools-mcp@latest --tool-preset mozilla --allow-system-access ``` -The `prefs` and `privileged` modules require `MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1` and are only -available in the Mozilla-internal build. The public package skips them even if requested and -logs a warning naming the modules it dropped. +The `prefs` and `privileged` modules require `--allow-system-access`. The flag grants permission +and launches Firefox with the required environment, while `--tool-preset mozilla` or `--tools` +selects which modules to expose. Without the flag, privileged modules are skipped even if selected. +Passing `MOZ_REMOTE_ALLOW_SYSTEM_ACCESS` through `--env` or `restart_firefox` does not grant access. + +For Codex: + +```bash +codex mcp add firefox-devtools -- \ + npx -y @mozilla/firefox-devtools-mcp@latest \ + --tool-preset mozilla \ + --allow-system-access +``` ### Useful preferences (`--pref`) @@ -242,10 +256,10 @@ Both flags are required because the MCP uses both WebDriver Classic (`--marionet - Console: list/clear (list supports optional `saveTo`) - Screenshot: page/by uid (with optional `saveTo` for CLI environments) - Script: evaluate_script (optional `sandbox` for an isolated realm; optional `saveTo` for bulky results) -- Privileged Context: list/select privileged ("chrome") contexts, evaluate_privileged_script (requires `MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1`) -- WebExtension: install_extension, uninstall_extension, list_extensions (list requires `MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1`) +- Privileged Context: list/select privileged ("chrome") contexts, evaluate_privileged_script (requires `--allow-system-access`) +- WebExtension: install_extension, uninstall_extension, list_extensions (list requires `--allow-system-access`) - Firefox Management: get_firefox_info, get_firefox_output, restart_firefox -- Firefox Preferences: get_firefox_prefs, set_firefox_prefs (requires `MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1`) +- Firefox Preferences: get_firefox_prefs, set_firefox_prefs (requires `--allow-system-access`) - Profiler: profiler_is_active, profiler_start (preset or explicit config), profiler_stop (saves profile to downloads directory) - Screencast: screencast_start (records the page viewport to a video file in the downloads directory), screencast_stop (requires Firefox 154+) - Utilities: accept/dismiss dialog, history back/forward, set viewport diff --git a/SECURITY.md b/SECURITY.md index 42ae018e..25a1b246 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -33,13 +33,13 @@ Lets the agent execute arbitrary JavaScript in any page context. If the agent is ### Privileged context tools (modules `privileged` and `prefs`) -Tools that operate in Firefox's privileged (chrome) context: listing and selecting privileged contexts, evaluating privileged scripts, reading and writing Firefox preferences, and listing extensions. These tools require the `MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1` environment variable to be set, which is checked by the WebDriver implementation in Firefox to allow using any command that targets privileged contexts. They are only available in the Mozilla-internal build; the public package skips them even if requested. +Tools that operate in Firefox's privileged (chrome) context include listing and selecting privileged contexts, evaluating privileged scripts, reading and writing Firefox preferences, and listing extensions. They are exposed only when the MCP starts with `--allow-system-access` and the user also selects the `mozilla` preset or the privileged modules explicitly. The flag launches Firefox with the system access required by WebDriver. Neither `--env` nor later tool calls can substitute for this startup consent. Unless you are developing or modifying Firefox itself, you likely do not need these modules. To set preferences at startup, you can always use the `--pref name=value` command-line argument instead. If you are missing commands or features to debug web content, please file a bug on [Bugzilla](https://bugzilla.mozilla.org/enter_bug.cgi?format=__default__&blocked=2026717&product=Developer%20Infrastructure&component=Firefox%20MCP) or reach out in the [#firefox-devtools-mcp Matrix room](https://chat.mozilla.org/#/room/#firefox-devtools-mcp:mozilla.org). -> **Warning:** When the privileged modules are used together with `MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1`, the agent gains access to privileged Firefox APIs with no web-content sandbox boundary. Depending on what the agent does with that access, this can extend to operating-system–level actions. Only use this combination in fully isolated environments. +> **Warning:** When privileged modules are used with `--allow-system-access`, the agent gains access to privileged Firefox APIs with no web-content sandbox boundary. Depending on what the agent does with that access, this can extend to operating-system–level actions. Only use this combination in fully isolated environments. -The deprecated `--enable-script` and `--enable-privileged-context` flags select the `developer` and `mozilla` presets respectively. They still work, but `--tool-preset` and `--tools` describe what is actually enabled. +The deprecated `--enable-script` and `--enable-privileged-context` flags select the `developer` and `mozilla` presets respectively. They do not grant system access. `--tool-preset` and `--tools` describe what is selected, while `--allow-system-access` is the separate consent step for privileged modules. ## Risky Flags diff --git a/src/cli.ts b/src/cli.ts index e7b1a5fa..0a1d2dd0 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -184,6 +184,12 @@ export const cliOptions = { description: 'Path to a file where MCP server logs will be written. Set DEBUG=* to also enable verbose debug logs.', }, + allowSystemAccess: { + type: 'boolean', + description: + 'Allow privileged Firefox access. Use with --tool-preset mozilla or an explicit privileged tool selection.', + default: false, + }, tools: { type: 'array', string: true, @@ -205,7 +211,7 @@ export const cliOptions = { type: 'string', description: `Preset selecting which tool modules to enable: ${PRESET_NAMES.join(' < ')}. ` + - 'Ignored when --tools is given. Privileged modules (mozilla/all) require MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1.', + 'Ignored when --tools is given. Privileged modules (mozilla/all) require --allow-system-access.', default: process.env.TOOL_PRESET || 'basic', }, enableScript: { @@ -217,7 +223,7 @@ export const cliOptions = { enablePrivilegedContext: { type: 'boolean', description: - 'Deprecated: use --tools/--tool-preset. Enable privileged context tools and Firefox prefs tools. Requires MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1.', + 'Deprecated: use --tools/--tool-preset. Enable privileged context tools and Firefox prefs tools. Requires --allow-system-access.', default: (process.env.ENABLE_PRIVILEGED_CONTEXT ?? 'false') === 'true', }, unrestrictedSavePaths: { diff --git a/src/index.moz.ts b/src/index.moz.ts index ba9a7343..fa8f1019 100644 --- a/src/index.moz.ts +++ b/src/index.moz.ts @@ -13,15 +13,16 @@ if (process.env.NODE_ENV !== 'production') { } } -// Moz entry point: accepts all CLI arguments including --enable-privileged-context. -// The public entry point (index.public.ts) strips that flag to prevent privileged tool exposure. +// Moz entry point: accepts all CLI arguments including the deprecated +// --enable-privileged-context selector. Privileged access still requires +// --allow-system-access. import { parseArguments } from './cli.js'; import { run } from './index.js'; export { FirefoxDevTools } from './firefox/index.js'; export { FirefoxDisconnectedError, isDisconnectionError } from './utils/errors.js'; -run(parseArguments, import.meta.url, true).catch((error) => { +run(parseArguments, import.meta.url).catch((error) => { console.error('Fatal error in main', error); process.exit(1); }); diff --git a/src/index.public.ts b/src/index.public.ts index de276056..c2f86f27 100644 --- a/src/index.public.ts +++ b/src/index.public.ts @@ -13,16 +13,15 @@ if (process.env.NODE_ENV !== 'production') { } } -// Public entry point: --enable-privileged-context is excluded from the CLI so -// privileged tools can never be enabled in the public package. -// The moz entry point (index.moz.ts) accepts all CLI arguments. +// Public entry point: the deprecated --enable-privileged-context selector is +// excluded from the CLI. Privileged access requires --allow-system-access. import { parseArguments } from './cli.js'; import { run } from './index.js'; export { FirefoxDevTools } from './firefox/index.js'; export { FirefoxDisconnectedError, isDisconnectionError } from './utils/errors.js'; -run((v) => parseArguments(v, process.argv, false), import.meta.url, false).catch((error) => { +run((v) => parseArguments(v, process.argv, false), import.meta.url).catch((error) => { console.error('Fatal error in main', error); process.exit(1); }); diff --git a/src/index.ts b/src/index.ts index e05b32d9..3c4cabe5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,7 @@ import { FirefoxDevTools } from './firefox/index.js'; import type { FirefoxLaunchOptions } from './firefox/types.js'; import { buildToolset } from './tools/registry.js'; import { errorResponse } from './utils/response-helpers.js'; +import { applySystemAccessPolicy, clearSystemAccessEnvironment } from './system-access.js'; type Args = ReturnType; @@ -29,7 +30,7 @@ if (!major || major < 20) { } // Set by run() before the server starts; initialized to satisfy the type checker. -export let args = {} as Args; +export let args = {} as Readonly; // Global context (lazy initialized on first tool call) let firefox: FirefoxDevTools | null = null; @@ -135,6 +136,7 @@ export async function getFirefox(): Promise { }; } + options = applySystemAccessPolicy(options, args.allowSystemAccess === true); firefox = new FirefoxDevTools(options); try { await firefox.connect(); @@ -156,10 +158,7 @@ export async function getFirefox(): Promise { export async function run( parseArgsFn: (version: string) => Args, - importMetaUrl: string, - // Fail closed: privileged tools must be opted into explicitly (index.moz.ts - // passes true). A caller that omits this never exposes privileged modules. - allowPrivileged = false + importMetaUrl: string ): Promise { // Only run if this entry file is executed directly (not imported as a library). // We need to normalize both paths to handle different execution contexts (npx, node, etc.) @@ -177,7 +176,9 @@ export async function run( return; } - args = parseArgsFn(SERVER_VERSION); + args = Object.freeze(parseArgsFn(SERVER_VERSION)); + const allowSystemAccess = args.allowSystemAccess === true; + clearSystemAccessEnvironment(process.env); if (args.logFile) { setupLogFile(args.logFile); @@ -196,7 +197,7 @@ export async function run( preset: args.toolPreset, enableScript: Boolean(args.enableScript), enablePrivilegedContext: Boolean(args.enablePrivilegedContext), - allowPrivileged, + allowPrivileged: allowSystemAccess, }); for (const warning of warnings) { log(warning); diff --git a/src/system-access.ts b/src/system-access.ts new file mode 100644 index 00000000..1154a0b5 --- /dev/null +++ b/src/system-access.ts @@ -0,0 +1,35 @@ +import type { FirefoxLaunchOptions } from './firefox/types.js'; + +export const SYSTEM_ACCESS_ENV = 'MOZ_REMOTE_ALLOW_SYSTEM_ACCESS'; + +export function isSystemAccessEnvironmentVariable(name: string): boolean { + return name.toUpperCase() === SYSTEM_ACCESS_ENV; +} + +export function applySystemAccessPolicy( + options: FirefoxLaunchOptions, + allowSystemAccess: boolean +): FirefoxLaunchOptions { + const env = Object.fromEntries( + Object.entries(options.env ?? {}).filter(([name]) => !isSystemAccessEnvironmentVariable(name)) + ); + + if (allowSystemAccess) { + env[SYSTEM_ACCESS_ENV] = '1'; + } + + return { + ...options, + env: Object.keys(env).length > 0 ? env : undefined, + }; +} + +export function clearSystemAccessEnvironment( + environment: Record +): void { + for (const name of Object.keys(environment)) { + if (isSystemAccessEnvironmentVariable(name)) { + delete environment[name]; + } + } +} diff --git a/src/tools/firefox-management.ts b/src/tools/firefox-management.ts index e2b9e809..37dd800a 100644 --- a/src/tools/firefox-management.ts +++ b/src/tools/firefox-management.ts @@ -5,6 +5,7 @@ import { readFileSync, existsSync, statSync } from 'node:fs'; import { errorResponse, successResponse } from '../utils/response-helpers.js'; +import { isSystemAccessEnvironmentVariable, SYSTEM_ACCESS_ENV } from '../system-access.js'; import { defineModule, defineToolHandler, type ToolDefinition } from './module.js'; // ============================================================================ @@ -210,7 +211,7 @@ export const restartFirefoxTool = { type: 'string', }, description: - 'New environment variables in KEY=VALUE format (optional, e.g., ["MOZ_LOG=HTMLMediaElement:5", "MOZ_LOG_FILE=/tmp/ff.log"])', + 'New environment variables in KEY=VALUE format (optional, e.g., ["MOZ_LOG=HTMLMediaElement:5", "MOZ_LOG_FILE=/tmp/ff.log"]). System access is controlled only by the MCP startup flag.', }, headless: { type: 'boolean', @@ -224,7 +225,7 @@ export const restartFirefoxTool = { prefs: { type: 'object', description: - 'Firefox preferences to set at startup. Values are auto-typed: true/false become booleans, integers become numbers, everything else is a string. Requires MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1.', + 'Firefox preferences to set at startup. Values are auto-typed: true/false become booleans, integers become numbers, everything else is a string. Requires --allow-system-access.', additionalProperties: { oneOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }], }, @@ -260,6 +261,11 @@ export const handleRestartFirefox = defineToolHandler(async (input: unknown) => for (const envStr of env) { const [key, ...valueParts] = envStr.split('='); if (key && valueParts.length > 0) { + if (isSystemAccessEnvironmentVariable(key)) { + throw new Error( + `${SYSTEM_ACCESS_ENV} is controlled by --allow-system-access and cannot be changed by restart_firefox` + ); + } newEnv[key] = valueParts.join('='); } } diff --git a/src/tools/firefox-prefs.ts b/src/tools/firefox-prefs.ts index b12d5301..404f8a3c 100644 --- a/src/tools/firefox-prefs.ts +++ b/src/tools/firefox-prefs.ts @@ -1,7 +1,7 @@ /** * Firefox Preferences Tools * Tools for getting and setting Firefox preferences via Services.prefs API - * Requires MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 + * Requires MCP startup with --allow-system-access */ import { successResponse } from '../utils/response-helpers.js'; @@ -16,7 +16,7 @@ import type { McpToolResponse } from '../types/common.js'; export const setFirefoxPrefsTool = { name: 'set_firefox_prefs', description: - 'Set Firefox preferences at runtime a privileged API. Requires MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 env var.', + 'Set Firefox preferences at runtime via a privileged API. Requires MCP startup with --allow-system-access.', annotations: { readOnlyHint: false, }, @@ -61,7 +61,7 @@ export const handleSetFirefoxPrefs = defineToolHandler( const contexts = result.contexts || []; if (contexts.length === 0) { throw new Error( - 'No privileged contexts available. Ensure MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 is set.' + 'No privileged contexts available. Ensure the MCP was started with --allow-system-access.' ); } @@ -115,7 +115,7 @@ export const handleSetFirefoxPrefs = defineToolHandler( } catch (error) { if (error instanceof Error && error.message.includes('UnsupportedOperationError')) { throw new Error( - 'Chrome context access not enabled. Set MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 environment variable and restart Firefox.' + 'Chrome context access not enabled. Restart the MCP with --allow-system-access.' ); } throw error; @@ -130,7 +130,7 @@ export const handleSetFirefoxPrefs = defineToolHandler( export const getFirefoxPrefsTool = { name: 'get_firefox_prefs', description: - 'Get Firefox preference values via a privileged API. Requires MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 env var.', + 'Get Firefox preference values via a privileged API. Requires MCP startup with --allow-system-access.', annotations: { readOnlyHint: true, }, @@ -167,7 +167,7 @@ export const handleGetFirefoxPrefs = defineToolHandler( const contexts = result.contexts || []; if (contexts.length === 0) { throw new Error( - 'No privileged contexts available. Ensure MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 is set.' + 'No privileged contexts available. Ensure the MCP was started with --allow-system-access.' ); } @@ -243,7 +243,7 @@ export const handleGetFirefoxPrefs = defineToolHandler( } catch (error) { if (error instanceof Error && error.message.includes('UnsupportedOperationError')) { throw new Error( - 'Chrome context access not enabled. Set MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 environment variable and restart Firefox.' + 'Chrome context access not enabled. Restart the MCP with --allow-system-access.' ); } throw error; diff --git a/src/tools/module.ts b/src/tools/module.ts index afc191b3..ca1cf627 100644 --- a/src/tools/module.ts +++ b/src/tools/module.ts @@ -45,9 +45,8 @@ export interface ToolEntry { /** * A group of related tools that can be enabled or disabled as a unit. - * `privileged` modules require the moz build and MOZ_REMOTE_ALLOW_SYSTEM_ACCESS - * at runtime; they are dropped entirely when the server is not allowed to expose - * privileged tools (see buildToolset). + * `privileged` modules require MCP startup consent; they are dropped entirely + * when the server is not allowed to expose privileged tools (see buildToolset). */ export interface ToolModule { name: string; diff --git a/src/tools/privileged-context.ts b/src/tools/privileged-context.ts index c20a7191..dffbd58d 100644 --- a/src/tools/privileged-context.ts +++ b/src/tools/privileged-context.ts @@ -1,6 +1,6 @@ /** * Privileged context management tools for MCP - * Requires MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 + * Requires MCP startup with --allow-system-access */ import { successResponse, errorResponse, previewExcerpt } from '../utils/response-helpers.js'; @@ -17,7 +17,7 @@ import type { McpToolResponse } from '../types/common.js'; export const listPrivilegedContextsTool = { name: 'list_privileged_contexts', description: - 'List privileged (privileged) browsing contexts. Requires MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 env var. Use restart_firefox with env parameter to enable.', + 'List privileged (chrome) browsing contexts. Requires MCP startup with --allow-system-access.', annotations: { readOnlyHint: true, }, @@ -30,7 +30,7 @@ export const listPrivilegedContextsTool = { export const selectPrivilegedContextTool = { name: 'select_privileged_context', description: - 'Select a privileged browsing context by ID and set WebDriver Classic context to "chrome" . Requires MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 env var.', + 'Select a privileged browsing context by ID and set WebDriver Classic context to "chrome". Requires MCP startup with --allow-system-access.', annotations: { readOnlyHint: false, }, @@ -49,7 +49,7 @@ export const selectPrivilegedContextTool = { export const evaluatePrivilegedScriptTool = { name: 'evaluate_privileged_script', description: - 'Execute JS function in a privileged (chrome) browsing context. Requires MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 env var. Get context ids from list_privileged_contexts.', + 'Execute JS function in a privileged (chrome) browsing context. Requires MCP startup with --allow-system-access. Get context ids from list_privileged_contexts.', annotations: { readOnlyHint: false, }, @@ -95,7 +95,7 @@ function formatContextList(contexts: any[]): string { } const SYSTEM_ACCESS_ERROR = - 'Privileged context access not enabled. Set MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 environment variable and restart Firefox.'; + 'Privileged context access not enabled. Restart the MCP with --allow-system-access.'; // Top-level entries of the chrome-scoped tree are the privileged contexts; // their children are content tabs and must not be accepted. @@ -162,7 +162,7 @@ export const handleSelectPrivilegedContext = defineToolHandler( } catch { return errorResponse( new Error( - `Switched to context ${contextId} but failed to set Marionette privileged context. Your Firefox build may not support privileged context or MOZ_REMOTE_ALLOW_SYSTEM_ACCESS is not set.` + `Switched to context ${contextId} but failed to set Marionette privileged context. Your Firefox build may not support privileged context or the MCP was not started with --allow-system-access.` ) ); } diff --git a/src/tools/registry.ts b/src/tools/registry.ts index be177712..65602ae8 100644 --- a/src/tools/registry.ts +++ b/src/tools/registry.ts @@ -50,7 +50,7 @@ export function buildToolset(options: ToolsetOptions): Toolset { * - Otherwise the named `preset` applies (default: basic). * - Legacy `enableScript` / `enablePrivilegedContext` flags force the * `developer` / `mozilla` presets on top and emit deprecation warnings. - * - Privileged modules are dropped when `allowPrivileged` is false (public build). + * - Privileged modules are dropped when `allowPrivileged` is false. */ function selectModules(options: ToolsetOptions): { moduleNames: string[]; warnings: string[] } { const { tools: requested, preset, enableScript, enablePrivilegedContext } = options; @@ -118,7 +118,7 @@ function selectModules(options: ToolsetOptions): { moduleNames: string[]; warnin const dropped = moduleNames.filter((name) => privilegedModuleNames.has(name)); if (dropped.length > 0) { warnings.push( - `Privileged tool modules are not available in this build and were skipped: ${dropped.join(', ')}` + `Privileged tool modules require --allow-system-access and were skipped: ${dropped.join(', ')}` ); } moduleNames = moduleNames.filter((name) => !privilegedModuleNames.has(name)); diff --git a/src/tools/webextension.ts b/src/tools/webextension.ts index c3cc5538..4f28724b 100644 --- a/src/tools/webextension.ts +++ b/src/tools/webextension.ts @@ -6,7 +6,7 @@ * list_extensions: Uses chrome-privileged AddonManager API as workaround for * missing webExtension.getExtensions BiDi command * - * Note: list_extensions requires MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 + * Note: list_extensions requires MCP startup with --allow-system-access */ import { successResponse } from '../utils/response-helpers.js'; @@ -148,10 +148,9 @@ export const handleUninstallExtension = defineToolHandler( export const listExtensionsTool = { name: 'list_extensions', description: - // MOZ_REMOTE_ALLOW_SYSTEM_ACCESS is required because the tool relies on the - // privileged AddonManager API as a workaround for the currently missing - // webExtension.getExtensions WebDriver BiDi command. - 'List installed Firefox extensions with UUIDs and background scripts. Requires MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 env var.', + // System access is required because the tool relies on the privileged + // AddonManager API while webExtension.getExtensions is unavailable. + 'List installed Firefox extensions with UUIDs and background scripts. Requires MCP startup with --allow-system-access.', annotations: { readOnlyHint: true, }, @@ -247,7 +246,7 @@ export const handleListExtensions = defineToolHandler( const contexts = result.contexts || []; if (contexts.length === 0) { throw new Error( - 'No privileged contexts available. Ensure MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 is set.' + 'No privileged contexts available. Ensure the MCP was started with --allow-system-access.' ); } @@ -341,7 +340,7 @@ export const handleListExtensions = defineToolHandler( } catch (error) { if (error instanceof Error && error.message.includes('UnsupportedOperationError')) { throw new Error( - 'Chrome context access not enabled. Set MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 environment variable and restart Firefox.' + 'Chrome context access not enabled. Restart the MCP with --allow-system-access.' ); } throw error; diff --git a/tests/cli/prefs-parsing.test.ts b/tests/cli/prefs-parsing.test.ts index 988235a3..c0fa574e 100644 --- a/tests/cli/prefs-parsing.test.ts +++ b/tests/cli/prefs-parsing.test.ts @@ -115,6 +115,23 @@ describe('CLI --enable-privileged-context flag', () => { }); }); +describe('CLI --allow-system-access flag', () => { + it('should default to false', () => { + const args = parseArguments('1.0.0', ['node', 'script']); + expect(args.allowSystemAccess).toBe(false); + }); + + it('should be true when --allow-system-access is passed', () => { + const args = parseArguments('1.0.0', ['node', 'script', '--allow-system-access']); + expect(args.allowSystemAccess).toBe(true); + }); + + it('should be available in the public option set', () => { + const args = parseArguments('1.0.0', ['node', 'script', '--allow-system-access'], false); + expect(args.allowSystemAccess).toBe(true); + }); +}); + describe('CLI --pref option', () => { it('should accept --pref argument', () => { const args = parseArguments('1.0.0', ['node', 'script', '--pref', 'test=value']); diff --git a/tests/system-access.test.ts b/tests/system-access.test.ts new file mode 100644 index 00000000..2423f6c3 --- /dev/null +++ b/tests/system-access.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest'; +import { + applySystemAccessPolicy, + clearSystemAccessEnvironment, + SYSTEM_ACCESS_ENV, +} from '../src/system-access.js'; + +describe('system access policy', () => { + it('removes requested system access without consent', () => { + const options = { + env: { + MOZ_LOG: 'RemoteAgent:5', + [SYSTEM_ACCESS_ENV]: '1', + }, + }; + + expect(applySystemAccessPolicy(options, false)).toEqual({ + env: { MOZ_LOG: 'RemoteAgent:5' }, + }); + expect(options.env[SYSTEM_ACCESS_ENV]).toBe('1'); + }); + + it('forces the canonical system access value with consent', () => { + const options = { + env: { + moz_remote_allow_system_access: '0', + MOZ_LOG: 'RemoteAgent:5', + }, + }; + + expect(applySystemAccessPolicy(options, true)).toEqual({ + env: { + MOZ_LOG: 'RemoteAgent:5', + [SYSTEM_ACCESS_ENV]: '1', + }, + }); + }); + + it('keeps an empty environment undefined without consent', () => { + expect(applySystemAccessPolicy({}, false)).toEqual({ env: undefined }); + expect(applySystemAccessPolicy({ env: { [SYSTEM_ACCESS_ENV]: '1' } }, false)).toEqual({ + env: undefined, + }); + }); + + it('removes inherited system access without changing unrelated variables', () => { + const environment: Record = { + PATH: '/bin', + Moz_Remote_Allow_System_Access: '1', + }; + + clearSystemAccessEnvironment(environment); + + expect(environment).toEqual({ PATH: '/bin' }); + }); +}); diff --git a/tests/tools/firefox-management.test.ts b/tests/tools/firefox-management.test.ts index d71fbcbf..9afeaa49 100644 --- a/tests/tools/firefox-management.test.ts +++ b/tests/tools/firefox-management.test.ts @@ -106,6 +106,18 @@ describe('Firefox Management Tools', () => { }) ); }); + + it('should reject attempts to change system access through env', async () => { + const { handleRestartFirefox } = await import('../../src/tools/firefox-management.js'); + + const result = await handleRestartFirefox({ + env: ['MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1'], + }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('--allow-system-access'); + expect(mockSetNextLaunchOptions).not.toHaveBeenCalled(); + }); }); describe('when Firefox IS running', () => { diff --git a/tests/tools/firefox-prefs.test.ts b/tests/tools/firefox-prefs.test.ts index cfc2d602..7db3a7e3 100644 --- a/tests/tools/firefox-prefs.test.ts +++ b/tests/tools/firefox-prefs.test.ts @@ -105,7 +105,7 @@ describe('Firefox Prefs Tool Handlers', () => { expect(result.content[0].text).toContain('No preferences to set'); }); - it('should return helpful error when MOZ_REMOTE_ALLOW_SYSTEM_ACCESS results in no privileged contexts', async () => { + it('should return helpful error when system access is unavailable', async () => { delete process.env.MOZ_REMOTE_ALLOW_SYSTEM_ACCESS; // Without MOZ_REMOTE_ALLOW_SYSTEM_ACCESS, no privileged contexts are available @@ -122,7 +122,7 @@ describe('Firefox Prefs Tool Handlers', () => { const result = await handleSetFirefoxPrefs({ prefs: { 'test.pref': 'value' } }); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('MOZ_REMOTE_ALLOW_SYSTEM_ACCESS'); + expect(result.content[0].text).toContain('--allow-system-access'); }); it('should set preferences successfully', async () => { @@ -250,7 +250,7 @@ describe('Firefox Prefs Tool Handlers', () => { expect(result.content[0].text).toContain('names parameter is required'); }); - it('should return helpful error when MOZ_REMOTE_ALLOW_SYSTEM_ACCESS results in no privileged contexts', async () => { + it('should return helpful error when system access is unavailable', async () => { delete process.env.MOZ_REMOTE_ALLOW_SYSTEM_ACCESS; // Without MOZ_REMOTE_ALLOW_SYSTEM_ACCESS, no privileged contexts are available @@ -267,7 +267,7 @@ describe('Firefox Prefs Tool Handlers', () => { const result = await handleGetFirefoxPrefs({ names: ['test.pref'] }); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('MOZ_REMOTE_ALLOW_SYSTEM_ACCESS'); + expect(result.content[0].text).toContain('--allow-system-access'); }); it('should get preferences successfully', async () => { diff --git a/tests/tools/privileged-context.test.ts b/tests/tools/privileged-context.test.ts index 6788baef..dc83dc66 100644 --- a/tests/tools/privileged-context.test.ts +++ b/tests/tools/privileged-context.test.ts @@ -180,7 +180,7 @@ describe('Privileged Context Tool Handlers', () => { }); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('MOZ_REMOTE_ALLOW_SYSTEM_ACCESS'); + expect(result.content[0].text).toContain('--allow-system-access'); }); }); diff --git a/tests/tools/registry.test.ts b/tests/tools/registry.test.ts index 10b52518..ef2bae5b 100644 --- a/tests/tools/registry.test.ts +++ b/tests/tools/registry.test.ts @@ -62,6 +62,14 @@ describe('Tool registry', () => { .sort(); expect(privileged).toEqual(['prefs', 'privileged']); }); + + it('registers list_extensions only in the privileged module', () => { + const owners = MODULES.filter((module) => + module.tools.some(({ definition }) => definition.name === 'list_extensions') + ).map((module) => module.name); + + expect(owners).toEqual(['privileged']); + }); }); describe('presets', () => { @@ -99,6 +107,28 @@ describe('Tool registry', () => { ); }); + it('does not select privileged modules from permission alone', () => { + const { moduleNames, handlers } = buildToolset({ + preset: 'basic', + allowPrivileged: true, + }); + + expect(moduleNames).not.toContain('prefs'); + expect(moduleNames).not.toContain('privileged'); + expect(handlers.has('list_extensions')).toBe(false); + }); + + it('exposes privileged modules when selected and allowed', () => { + const { moduleNames, handlers } = buildToolset({ + preset: 'mozilla', + allowPrivileged: true, + }); + + expect(moduleNames).toContain('prefs'); + expect(moduleNames).toContain('privileged'); + expect(handlers.has('list_extensions')).toBe(true); + }); + it('lets --tools replace the preset entirely', () => { const { moduleNames } = buildToolset({ tools: ['network', 'pages'],