maestro/docs/tools/runuserscript.md
2026-06-03 05:08:00 +00:00

6.0 KiB

RunUserScript

Executes a user-authored script from the caller's user folder.

Two kinds of scripts are supported:

kind directory runtime signature use case
'script' (default) scripts/ plain Node.js — no Chromium main({ params }) Data processing, API calls, computation, file conversion
'browser-macro' browser-macros/ Playwright — Chromium main({ context, params }) Web automation with a live browser session

Input

{
  name: string,                         // filename — '.js' is appended if absent
  params?: Record<string, unknown>,     // runtime values matching the script's param spec
  kind?: 'script' | 'browser-macro'    // default: 'script'
}

Param validation

Params are validated against the params: block in the script's YAML frontmatter:

  • Extra params not listed in the spec → error containing "param"
  • Wrong type for a declared param → error containing "param"
  • Missing required param (no default) → error containing "param"
  • Params with defaults are filled in automatically when not supplied

On any param error the tool returns isError: true immediately — no subprocess is spawned.

Session integration (browser-macro only)

If a browser-macro script's frontmatter declares session_profile_id: <N>, the tool:

  1. Loads the profile from the DB (owner-gated — must belong to ctx.userId).
  2. Decrypts the user's envelope-encrypted DEK using the master key.
  3. Decrypts the AES-GCM storageState blob using the DEK.
  4. Passes the decrypted Playwright storageState object to the child process.

If any step fails the tool returns isError: true with a descriptive message.

For kind: 'script' (plain runtime), session_profile_id in the frontmatter is ignored — no session is loaded.

Self-healing recorder (browser-macro only)

When a browser-macro fails at runtime, the tool automatically enables the BrowseWeb recorder for the current task (if not already enabled). On task completion, recording-flush stages a candidate patch as browser-macros/{name}.next.js for diff review.

Plain scripts (kind: 'script') do not auto-enable the recorder.

Output format

On success:

<result stringified>

[script logs]
<console.log lines from the child process>

The result is JSON-stringified if it is an object or array; String(result) otherwise. The [script logs] section is only appended when the script produced logs.

On failure (plain):

RunUserScript "{name}" failed: <error message>

On failure (browser-macro):

RunUserScript "{name}" failed: <error message>

The recorder is now enabled for this task; subsequent BrowseWeb actions will be captured.
On task complete, a candidate patch will be saved as browser-macros/{name}.next.js for review.

Error cases

Situation isError message contains
No authenticated user true "authenticated"
Script file not found true "not found"
Frontmatter parse error true "frontmatter"
Param type / missing error true "param"
Session profile not found / not owned true "not found or does not belong"
Profile not active true "not active"
DEK / blob decryption failure true "decrypt"
Script timeout (60 s) true "timeout"
Script exits non-zero true "exited code"
Plain script denied child_process (e.g. spawning python) true "exited code" + "use the Bash tool"

Notes

  • The tool is a META_TOOL — it is available in every movement without listing it in allowed_tools.
  • Use kind: 'browser-macro' for any script that needs a browser (context).
  • Use ListUserAssets first to discover available scripts and their param specs.
  • On browser-macro failure, use BrowseWeb as a manual fallback.

Running Python (don't — use Bash)

RunUserScript runs Node only. There is no Python interpreter path. A common footgun is to write a Node script that does child_process.spawn('python3', ...) and run it here — that cannot work: plain scripts run under Node's --permission model, which denies child_process entirely (you get ERR_ACCESS_DENIED). Even if it were allowed, the child's env is scrubbed, so it would not see the orchestrator's provisioned Python environment.

To run Python, use the Bash tool instead: python3 your_script.py. The Bash sandbox has the pip packages pre-baked (pypdf, pdfplumber, python-docx, python-pptx, openpyxl, pandas, numpy, …). That is the supported, working path.

Security and trust model

RunUserScript is disabled by default. To enable it, add to config.yaml:

tools:
  user_scripts_enabled: true

Only enable for trusted users. User scripts run in a restricted child process:

  • Env is scrubbed — only PATH, HOME, TMPDIR/TMP, LANG, NODE_ENV, and PLAYWRIGHT_BROWSERS_PATH are forwarded. API keys, database passwords, and other secrets in the orchestrator's environment are not visible to the script.
  • CWD is set to the system tmpdir, not the orchestrator workspace.
  • Stdout is capped at 1 MB and stderr at 200 KB; exceeding either limit kills the child.
  • On timeout, the entire process group (including Playwright's Chromium for browser-macros) is killed.

The two runtimes have different capability levels:

  • Plain scripts (kind: 'script') run under Node's Permissions Model (--permission): --allow-fs-read is limited to the child-runner dir and tmpdir, --allow-fs-write to tmpdir, and child_process, worker threads, and native addons are denied. A plain script that tries to spawn a subprocess (e.g. python) fails with ERR_ACCESS_DENIED. See "Running Python" above.
  • Browser-macros (kind: 'browser-macro') cannot use --permission — Chromium launch, native bindings, and outbound HTTPS all need unrestricted child_process/addons/network. They run with full Node.js capability (env-scrubbed only) and rely on container-level isolation. Treat them as trusted code.