# 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 ```ts { name: string, // filename — '.js' is appended if absent params?: Record, // 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: `, 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: ``` [script logs] ``` 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: ``` On failure (browser-macro): ``` RunUserScript "{name}" failed: 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`: ```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.